gateway_request.go 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596
  1. package service
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "math"
  8. "regexp"
  9. "sort"
  10. "strings"
  11. "unsafe"
  12. "github.com/Wei-Shaw/sub2api/internal/domain"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  16. "github.com/tidwall/gjson"
  17. "github.com/tidwall/sjson"
  18. )
  19. var (
  20. // 这些字节模式用于 fast-path 判断,避免每次 []byte("...") 产生临时分配。
  21. patternTypeThinking = []byte(`"type":"thinking"`)
  22. patternTypeThinkingSpaced = []byte(`"type": "thinking"`)
  23. patternTypeRedactedThinking = []byte(`"type":"redacted_thinking"`)
  24. patternTypeRedactedSpaced = []byte(`"type": "redacted_thinking"`)
  25. patternThinkingField = []byte(`"thinking":`)
  26. patternThinkingFieldSpaced = []byte(`"thinking" :`)
  27. patternEmptyContent = []byte(`"content":[]`)
  28. patternEmptyContentSpaced = []byte(`"content": []`)
  29. patternEmptyContentSp1 = []byte(`"content" : []`)
  30. patternEmptyContentSp2 = []byte(`"content" :[]`)
  31. // Fast-path patterns for empty text blocks: {"type":"text","text":""}
  32. patternEmptyText = []byte(`"text":""`)
  33. patternEmptyTextSpaced = []byte(`"text": ""`)
  34. patternEmptyTextSp1 = []byte(`"text" : ""`)
  35. patternEmptyTextSp2 = []byte(`"text" :""`)
  36. sessionUserAgentProductPattern = regexp.MustCompile(`([A-Za-z0-9._-]+)/[A-Za-z0-9._-]+`)
  37. sessionUserAgentVersionPattern = regexp.MustCompile(`\bv?\d+(?:\.\d+){1,3}\b`)
  38. )
  39. // SessionContext 粘性会话上下文,用于区分不同来源的请求。
  40. // 仅在 GenerateSessionHash 第 3 级 fallback(消息内容 hash)时混入,
  41. // 避免不同用户发送相同消息产生相同 hash 导致账号集中。
  42. type SessionContext struct {
  43. ClientIP string
  44. UserAgent string
  45. APIKeyID int64
  46. }
  47. type jsonRange struct {
  48. start int // 原始请求体中的起始偏移(闭区间)
  49. end int // 原始请求体中的结束偏移(开区间)
  50. kind gjson.Type // JSON 值类型,用于调用方做轻量分支
  51. }
  52. type RequestBodyRef struct {
  53. data []byte
  54. }
  55. func NewRequestBodyRef(data []byte) *RequestBodyRef {
  56. return &RequestBodyRef{data: data}
  57. }
  58. func (b *RequestBodyRef) Bytes() []byte {
  59. if b == nil {
  60. return nil
  61. }
  62. return b.data
  63. }
  64. func (b *RequestBodyRef) Len() int {
  65. if b == nil {
  66. return 0
  67. }
  68. return len(b.data)
  69. }
  70. func (b *RequestBodyRef) Replace(data []byte) {
  71. if b == nil {
  72. return
  73. }
  74. b.data = data
  75. }
  76. func missingJSONRange() jsonRange {
  77. return jsonRange{start: -1, end: -1}
  78. }
  79. func rangeFromResult(r gjson.Result) jsonRange {
  80. if r.Raw == "" || r.Index <= 0 {
  81. return missingJSONRange()
  82. }
  83. end := r.Index + len(r.Raw)
  84. if end < r.Index {
  85. return missingJSONRange()
  86. }
  87. return jsonRange{start: r.Index, end: end, kind: r.Type}
  88. }
  89. func (r jsonRange) exists() bool {
  90. return r.start >= 0 && r.end >= r.start
  91. }
  92. // clearGatewayRequestDerivedState 清空绑定当前 body 的轻量派生字段,防止 ReplaceBody 后读到旧值。
  93. func clearGatewayRequestDerivedState(parsed *ParsedRequest) {
  94. if parsed == nil {
  95. return
  96. }
  97. parsed.Model = ""
  98. parsed.Stream = false
  99. parsed.MetadataUserID = ""
  100. parsed.HasSystem = false
  101. parsed.ThinkingEnabled = false
  102. parsed.OutputEffort = ""
  103. parsed.Speed = ""
  104. parsed.MaxTokens = 0
  105. parsed.systemRange = missingJSONRange()
  106. parsed.messagesRange = missingJSONRange()
  107. parsed.inputRange = missingJSONRange()
  108. }
  109. func clearGatewayRequestRanges(parsed *ParsedRequest) {
  110. if parsed == nil {
  111. return
  112. }
  113. parsed.HasSystem = false
  114. parsed.systemRange = missingJSONRange()
  115. parsed.messagesRange = missingJSONRange()
  116. parsed.inputRange = missingJSONRange()
  117. }
  118. func setGatewayRequestRanges(parsed *ParsedRequest, protocol string, jsonStr string) {
  119. if parsed == nil {
  120. return
  121. }
  122. switch protocol {
  123. case domain.PlatformGemini:
  124. if sysParts := gjson.Get(jsonStr, "systemInstruction.parts"); sysParts.Exists() && sysParts.IsArray() {
  125. parsed.systemRange = rangeFromResult(sysParts)
  126. }
  127. if contents := gjson.Get(jsonStr, "contents"); contents.Exists() && contents.IsArray() {
  128. parsed.messagesRange = rangeFromResult(contents)
  129. }
  130. default:
  131. if sys := gjson.Get(jsonStr, "system"); sys.Exists() {
  132. parsed.HasSystem = true
  133. parsed.systemRange = rangeFromResult(sys)
  134. }
  135. if msgs := gjson.Get(jsonStr, "messages"); msgs.Exists() && msgs.IsArray() {
  136. parsed.messagesRange = rangeFromResult(msgs)
  137. }
  138. if protocol == "responses" {
  139. if input := gjson.Get(jsonStr, "input"); input.Exists() {
  140. parsed.inputRange = rangeFromResult(input)
  141. }
  142. }
  143. }
  144. }
  145. const claudeCodeLongContextModelSuffix = "[1m]"
  146. // Claude Code treats [1m] as a client-side context selector and normally removes it
  147. // before provider requests. Normalize leaked suffixes, including its duplicated form.
  148. func normalizeClaudeCodeLongContextModel(model string) string {
  149. for len(model) > len(claudeCodeLongContextModelSuffix) &&
  150. strings.EqualFold(model[len(model)-len(claudeCodeLongContextModelSuffix):], claudeCodeLongContextModelSuffix) {
  151. model = model[:len(model)-len(claudeCodeLongContextModelSuffix)]
  152. }
  153. return model
  154. }
  155. // parseGatewayRequestCurrentBody 只做标量和 raw range 轻量解析,不恢复 system/messages 对象图。
  156. func parseGatewayRequestCurrentBody(parsed *ParsedRequest, protocol string) error {
  157. if parsed == nil || parsed.Body == nil {
  158. return fmt.Errorf("empty request body")
  159. }
  160. bodyBytes := parsed.Body.Bytes()
  161. if !gjson.ValidBytes(bodyBytes) {
  162. return DescribeInvalidJSON(bodyBytes)
  163. }
  164. // 只在当前函数内零拷贝读取 JSON 字段;ReplaceBody 后必须重新进入本函数刷新派生状态。
  165. jsonStr := *(*string)(unsafe.Pointer(&bodyBytes))
  166. clearGatewayRequestDerivedState(parsed)
  167. parsed.protocol = protocol
  168. modelResult := gjson.Get(jsonStr, "model")
  169. if modelResult.Exists() {
  170. if modelResult.Type != gjson.String {
  171. return fmt.Errorf("invalid model field type")
  172. }
  173. parsed.Model = modelResult.String()
  174. if protocol == domain.PlatformAnthropic {
  175. normalizedModel := normalizeClaudeCodeLongContextModel(parsed.Model)
  176. if normalizedModel != parsed.Model {
  177. normalizedBody, err := sjson.SetBytes(bodyBytes, "model", normalizedModel)
  178. if err != nil {
  179. return fmt.Errorf("normalize model field: %w", err)
  180. }
  181. parsed.Body.Replace(normalizedBody)
  182. bodyBytes = normalizedBody
  183. jsonStr = *(*string)(unsafe.Pointer(&bodyBytes))
  184. parsed.Model = normalizedModel
  185. }
  186. }
  187. }
  188. streamResult := gjson.Get(jsonStr, "stream")
  189. if streamResult.Exists() {
  190. if streamResult.Type != gjson.True && streamResult.Type != gjson.False {
  191. return fmt.Errorf("invalid stream field type")
  192. }
  193. parsed.Stream = streamResult.Bool()
  194. }
  195. parsed.MetadataUserID = gjson.Get(jsonStr, "metadata.user_id").String()
  196. thinkingType := gjson.Get(jsonStr, "thinking.type").String()
  197. parsed.ThinkingEnabled = thinkingType == "enabled" || thinkingType == "adaptive"
  198. parsed.OutputEffort = strings.TrimSpace(gjson.Get(jsonStr, "output_config.effort").String())
  199. if protocol == domain.PlatformAnthropic {
  200. parsed.Speed = strings.ToLower(strings.TrimSpace(gjson.Get(jsonStr, "speed").String()))
  201. }
  202. maxTokensResult := gjson.Get(jsonStr, "max_tokens")
  203. if maxTokensResult.Exists() && maxTokensResult.Type == gjson.Number {
  204. f := maxTokensResult.Float()
  205. if !math.IsNaN(f) && !math.IsInf(f, 0) && f == math.Trunc(f) &&
  206. f <= float64(math.MaxInt) && f >= float64(math.MinInt) {
  207. parsed.MaxTokens = int(f)
  208. }
  209. }
  210. setGatewayRequestRanges(parsed, protocol, jsonStr)
  211. return nil
  212. }
  213. func refreshGatewayRequestRanges(parsed *ParsedRequest, protocol string) error {
  214. return parseGatewayRequestCurrentBody(parsed, protocol)
  215. }
  216. // DescribeInvalidJSON returns a diagnostic error for a request body that
  217. // failed JSON validation. It re-parses with encoding/json (failure path only)
  218. // to pinpoint the first offending byte, so operators can distinguish genuinely
  219. // invalid JSON from a truncated / partially consumed body. The error carries
  220. // only length/offset/character information — never body content — so callers
  221. // may safely wrap or log it.
  222. func DescribeInvalidJSON(body []byte) error {
  223. var raw json.RawMessage
  224. if err := json.Unmarshal(body, &raw); err != nil {
  225. var syntaxErr *json.SyntaxError
  226. if errors.As(err, &syntaxErr) {
  227. return fmt.Errorf("invalid json (len=%d, offset=%d): %s", len(body), syntaxErr.Offset, syntaxErr.Error())
  228. }
  229. return fmt.Errorf("invalid json (len=%d): %w", len(body), err)
  230. }
  231. // gjson rejected the body but encoding/json accepted it (divergent edge
  232. // cases, e.g. certain malformed UTF-8 sequences); report the basics.
  233. return fmt.Errorf("invalid json (len=%d)", len(body))
  234. }
  235. // ParsedRequest 保存网关请求的预解析结果
  236. //
  237. // 性能优化说明:
  238. // 原实现在多个位置重复解析请求体(Handler、Service 各解析一次):
  239. // 1. gateway_handler.go 解析获取 model 和 stream
  240. // 2. gateway_service.go 再次解析获取 system、messages、metadata
  241. // 3. GenerateSessionHash 又一次解析获取会话哈希所需字段
  242. //
  243. // 新实现一次解析,多处复用:
  244. // 1. 在 Handler 层统一调用 ParseGatewayRequest 一次性解析
  245. // 2. 将解析结果 ParsedRequest 传递给 Service 层
  246. // 3. 避免重复 json.Unmarshal,减少 CPU 和内存开销
  247. type ParsedRequest struct {
  248. Body *RequestBodyRef // 原始请求体引用(保留用于转发);替换内容请走 ReplaceBody
  249. Model string // 请求的模型名称
  250. Stream bool // 是否为流式请求
  251. MetadataUserID string // metadata.user_id(用于会话亲和)
  252. HasSystem bool // 是否包含 system 字段(包含 null 也视为显式传入)
  253. ThinkingEnabled bool // 是否开启 thinking(部分平台会影响最终模型名)
  254. OutputEffort string // output_config.effort(Claude API 的推理强度控制)
  255. Speed string // Anthropic speed(当前可计费值为 "fast")
  256. MaxTokens int // max_tokens 值(用于探测请求拦截)
  257. SessionContext *SessionContext // 可选:请求上下文区分因子(nil 时行为不变)
  258. protocol string // 当前 Body 的协议格式,用于 Body 替换后刷新 raw range
  259. systemRange jsonRange // system/systemInstruction.parts 的 raw JSON 范围,绑定 Body 当前内容
  260. messagesRange jsonRange // messages/contents 的 raw JSON 范围,绑定 Body 当前内容
  261. inputRange jsonRange // Responses API input 的 raw JSON 范围,绑定 Body 当前内容
  262. // GroupID 请求所属分组 ID(来自 API Key)
  263. GroupID *int64
  264. // OnUpstreamAccepted 上游接受请求后立即调用(用于提前释放串行锁)
  265. // 流式请求在收到 2xx 响应头后调用,避免持锁等流完成
  266. OnUpstreamAccepted func()
  267. }
  268. // NormalizeSessionUserAgent reduces UA noise for sticky-session and digest hashing.
  269. // It preserves the set of product names from Product/Version tokens while
  270. // discarding version-only changes and incidental comments.
  271. func NormalizeSessionUserAgent(raw string) string {
  272. raw = strings.TrimSpace(raw)
  273. if raw == "" {
  274. return ""
  275. }
  276. matches := sessionUserAgentProductPattern.FindAllStringSubmatch(raw, -1)
  277. if len(matches) == 0 {
  278. return normalizeSessionUserAgentFallback(raw)
  279. }
  280. products := make([]string, 0, len(matches))
  281. seen := make(map[string]struct{}, len(matches))
  282. for _, match := range matches {
  283. if len(match) < 2 {
  284. continue
  285. }
  286. product := strings.ToLower(strings.TrimSpace(match[1]))
  287. if product == "" {
  288. continue
  289. }
  290. if _, exists := seen[product]; exists {
  291. continue
  292. }
  293. seen[product] = struct{}{}
  294. products = append(products, product)
  295. }
  296. if len(products) == 0 {
  297. return normalizeSessionUserAgentFallback(raw)
  298. }
  299. sort.Strings(products)
  300. return strings.Join(products, "+")
  301. }
  302. func normalizeSessionUserAgentFallback(raw string) string {
  303. normalized := strings.ToLower(strings.Join(strings.Fields(raw), " "))
  304. normalized = sessionUserAgentVersionPattern.ReplaceAllString(normalized, "")
  305. return strings.Join(strings.Fields(normalized), " ")
  306. }
  307. // ParseGatewayRequest 解析网关请求体并返回结构化结果。
  308. // protocol 指定请求协议格式(domain.PlatformAnthropic / domain.PlatformGemini),
  309. // 不同协议使用不同的 system/messages 字段名。
  310. func ParseGatewayRequest(body *RequestBodyRef, protocol string) (*ParsedRequest, error) {
  311. parsed := &ParsedRequest{Body: body}
  312. if err := parseGatewayRequestCurrentBody(parsed, protocol); err != nil {
  313. return nil, err
  314. }
  315. return parsed, nil
  316. }
  317. func (p *ParsedRequest) raw(r jsonRange) []byte {
  318. if p == nil || p.Body == nil || !r.exists() {
  319. return nil
  320. }
  321. body := p.Body.Bytes()
  322. if r.end > len(body) {
  323. return nil
  324. }
  325. return body[r.start:r.end]
  326. }
  327. func (p *ParsedRequest) SystemRaw() []byte {
  328. return p.raw(p.systemRange)
  329. }
  330. func (p *ParsedRequest) MessagesRaw() []byte {
  331. return p.raw(p.messagesRange)
  332. }
  333. func (p *ParsedRequest) InputRaw() []byte {
  334. return p.raw(p.inputRange)
  335. }
  336. func (p *ParsedRequest) DecodeSystem(dst any) error {
  337. raw := p.SystemRaw()
  338. if len(raw) == 0 {
  339. return nil
  340. }
  341. return json.Unmarshal(raw, dst)
  342. }
  343. func (p *ParsedRequest) DecodeMessages(dst any) error {
  344. raw := p.MessagesRaw()
  345. if len(raw) == 0 {
  346. return nil
  347. }
  348. return json.Unmarshal(raw, dst)
  349. }
  350. func (p *ParsedRequest) SystemValue() (any, bool) {
  351. raw := p.SystemRaw()
  352. if len(raw) == 0 {
  353. return nil, false
  354. }
  355. var system any
  356. if err := json.Unmarshal(raw, &system); err != nil {
  357. return nil, false
  358. }
  359. return system, true
  360. }
  361. // CloneForBody 为单次账号尝试创建独立 body 视图,避免 failover 复用已改写的 ParsedRequest。
  362. func (p *ParsedRequest) CloneForBody(body []byte) (*ParsedRequest, error) {
  363. if p == nil {
  364. return nil, fmt.Errorf("parse request: empty request")
  365. }
  366. clone := *p
  367. clone.Body = NewRequestBodyRef(body)
  368. clone.OnUpstreamAccepted = nil
  369. if err := refreshGatewayRequestRanges(&clone, clone.protocol); err != nil {
  370. return nil, err
  371. }
  372. return &clone, nil
  373. }
  374. // ReplaceBody 统一刷新当前 body 和 raw range,保证后续 helper 读取的是最新请求体。
  375. func (p *ParsedRequest) ReplaceBody(data []byte) error {
  376. if p == nil {
  377. return fmt.Errorf("parse request: empty request")
  378. }
  379. if p.Body == nil {
  380. p.Body = NewRequestBodyRef(data)
  381. } else {
  382. p.Body.Replace(data)
  383. }
  384. if err := refreshGatewayRequestRanges(p, p.protocol); err != nil {
  385. clearGatewayRequestRanges(p)
  386. return err
  387. }
  388. return nil
  389. }
  390. // sliceRawFromBody 返回 Result.Raw 对应的原始字节切片。
  391. // 优先使用 Result.Index 直接从 body 切片,避免对大字段(如 messages)产生额外拷贝。
  392. // 当 Index 不可用时,退化为复制(理论上极少发生)。
  393. func sliceRawFromBody(body []byte, r gjson.Result) []byte {
  394. if r.Index > 0 {
  395. end := r.Index + len(r.Raw)
  396. if end <= len(body) {
  397. return body[r.Index:end]
  398. }
  399. }
  400. // fallback: 不影响正确性,但会产生一次拷贝
  401. return []byte(r.Raw)
  402. }
  403. // stripEmptyTextBlocksFromSlice removes empty text blocks from a content slice (including nested tool_result content).
  404. // Returns (cleaned slice, true) if any blocks were removed, or (original, false) if unchanged.
  405. func stripEmptyTextBlocksFromSlice(blocks []any) ([]any, bool) {
  406. var result []any
  407. changed := false
  408. for i, block := range blocks {
  409. blockMap, ok := block.(map[string]any)
  410. if !ok {
  411. if result != nil {
  412. result = append(result, block)
  413. }
  414. continue
  415. }
  416. blockType, _ := blockMap["type"].(string)
  417. // Strip empty text blocks
  418. if blockType == "text" {
  419. if txt, _ := blockMap["text"].(string); txt == "" {
  420. if result == nil {
  421. result = make([]any, 0, len(blocks))
  422. result = append(result, blocks[:i]...)
  423. }
  424. changed = true
  425. continue
  426. }
  427. }
  428. // Recurse into tool_result nested content
  429. if blockType == "tool_result" {
  430. if nestedContent, ok := blockMap["content"].([]any); ok {
  431. if cleaned, nestedChanged := stripEmptyTextBlocksFromSlice(nestedContent); nestedChanged {
  432. if result == nil {
  433. result = make([]any, 0, len(blocks))
  434. result = append(result, blocks[:i]...)
  435. }
  436. changed = true
  437. blockCopy := make(map[string]any, len(blockMap))
  438. for k, v := range blockMap {
  439. blockCopy[k] = v
  440. }
  441. blockCopy["content"] = cleaned
  442. result = append(result, blockCopy)
  443. continue
  444. }
  445. }
  446. }
  447. if result != nil {
  448. result = append(result, block)
  449. }
  450. }
  451. if !changed {
  452. return blocks, false
  453. }
  454. return result, true
  455. }
  456. // StripEmptyTextBlocks removes empty text blocks from the request body (including nested tool_result content).
  457. // This is a lightweight pre-filter for the initial request path to prevent upstream 400 errors.
  458. // Returns the original body unchanged if no empty text blocks are found.
  459. func StripEmptyTextBlocks(body []byte) []byte {
  460. // Fast path: check if body contains empty text patterns
  461. hasEmptyTextBlock := bytes.Contains(body, patternEmptyText) ||
  462. bytes.Contains(body, patternEmptyTextSpaced) ||
  463. bytes.Contains(body, patternEmptyTextSp1) ||
  464. bytes.Contains(body, patternEmptyTextSp2)
  465. if !hasEmptyTextBlock {
  466. return body
  467. }
  468. jsonStr := *(*string)(unsafe.Pointer(&body))
  469. msgsRes := gjson.Get(jsonStr, "messages")
  470. if !msgsRes.Exists() || !msgsRes.IsArray() {
  471. return body
  472. }
  473. var messages []any
  474. if err := json.Unmarshal(sliceRawFromBody(body, msgsRes), &messages); err != nil {
  475. return body
  476. }
  477. modified := false
  478. for _, msg := range messages {
  479. msgMap, ok := msg.(map[string]any)
  480. if !ok {
  481. continue
  482. }
  483. content, ok := msgMap["content"].([]any)
  484. if !ok {
  485. continue
  486. }
  487. if cleaned, changed := stripEmptyTextBlocksFromSlice(content); changed {
  488. modified = true
  489. msgMap["content"] = cleaned
  490. }
  491. }
  492. if !modified {
  493. return body
  494. }
  495. msgsBytes, err := json.Marshal(messages)
  496. if err != nil {
  497. return body
  498. }
  499. out, err := sjson.SetRawBytes(body, "messages", msgsBytes)
  500. if err != nil {
  501. return body
  502. }
  503. return out
  504. }
  505. // FilterThinkingBlocks removes thinking blocks from request body
  506. // Returns filtered body or original body if filtering fails (fail-safe)
  507. // This prevents 400 errors from invalid thinking block signatures.
  508. //
  509. // mappedModel 是「实际发给上游的模型 ID」(after account model mapping),用于按
  510. // 协议族分流。仅 anthropic-strict 走原过滤逻辑;passback-required 与 unknown
  511. // 一律保留全部 thinking block,避免误伤第三方兼容上游
  512. // (DeepSeek `/anthropic`、Kimi `/coding`、GLM、Moonshot 等),详见
  513. // .pensieve/short-term/knowledge/thinking-block-filter-third-party-upstream-inversion/。
  514. //
  515. // 策略 (anthropic-strict only):
  516. // - 当 thinking.type 不是 "enabled"/"adaptive":移除所有 thinking 相关块
  517. // - 当 thinking.type 是 "enabled"/"adaptive":仅移除缺失/无效 signature 的 thinking 块(避免 400)
  518. // (blocks with missing/empty/dummy signatures that would cause 400 errors)
  519. func FilterThinkingBlocks(body []byte, mappedModel string) []byte {
  520. if !ShouldPreFilterThinkingBlocks(mappedModel) {
  521. return body
  522. }
  523. return filterThinkingBlocksInternal(body, false)
  524. }
  525. // FilterThinkingBlocksForRetry strips thinking-related constructs for retry scenarios.
  526. //
  527. // Why:
  528. // - Upstreams may reject historical `thinking`/`redacted_thinking` blocks due to invalid/missing signatures.
  529. // - Anthropic extended thinking has a structural constraint: when top-level `thinking` is enabled and the
  530. // final message is an assistant prefill, the assistant content must start with a thinking block.
  531. // - If we remove thinking blocks but keep top-level `thinking` enabled, we can trigger:
  532. // "Expected `thinking` or `redacted_thinking`, but found `text`"
  533. //
  534. // Strategy (B: preserve content as text):
  535. // - Disable top-level `thinking` (remove `thinking` field).
  536. // - Convert `thinking` blocks to `text` blocks (preserve the thinking content).
  537. // - Remove `redacted_thinking` blocks (cannot be converted to text).
  538. // - Ensure no message ends up with empty content.
  539. //
  540. // mappedModel 用于按协议族分流:仅 anthropic-strict 执行上述变形;
  541. // passback-required (DeepSeek/Kimi/GLM 等) 与 unknown 一律返回原 body,
  542. // 因为这类上游的契约就是「thinking block 原样回传」(或我们不了解),
  543. // retry 任何变形都不会修好 400,反而破坏契约。详见 thinking_protocol.go。
  544. func FilterThinkingBlocksForRetry(body []byte, mappedModel string) []byte {
  545. // 仅 anthropic-strict 走整流;passback-required 与 unknown 都返回原 body。
  546. if !ShouldApplyRetryFilters(mappedModel) {
  547. return body
  548. }
  549. hasThinkingContent := bytes.Contains(body, patternTypeThinking) ||
  550. bytes.Contains(body, patternTypeThinkingSpaced) ||
  551. bytes.Contains(body, patternTypeRedactedThinking) ||
  552. bytes.Contains(body, patternTypeRedactedSpaced) ||
  553. bytes.Contains(body, patternThinkingField) ||
  554. bytes.Contains(body, patternThinkingFieldSpaced)
  555. // Also check for empty content arrays and empty text blocks that need fixing.
  556. // Note: This is a heuristic check; the actual empty content handling is done below.
  557. hasEmptyContent := bytes.Contains(body, patternEmptyContent) ||
  558. bytes.Contains(body, patternEmptyContentSpaced) ||
  559. bytes.Contains(body, patternEmptyContentSp1) ||
  560. bytes.Contains(body, patternEmptyContentSp2)
  561. // Check for empty text blocks: {"type":"text","text":""}
  562. // These cause upstream 400: "text content blocks must be non-empty"
  563. hasEmptyTextBlock := bytes.Contains(body, patternEmptyText) ||
  564. bytes.Contains(body, patternEmptyTextSpaced) ||
  565. bytes.Contains(body, patternEmptyTextSp1) ||
  566. bytes.Contains(body, patternEmptyTextSp2)
  567. // Fast path: nothing to process
  568. if !hasThinkingContent && !hasEmptyContent && !hasEmptyTextBlock {
  569. return body
  570. }
  571. // 尽量避免把整个 body Unmarshal 成 map(会产生大量 map/接口分配)。
  572. // 这里先用 gjson 把 messages 子树摘出来,后续只对 messages 做 Unmarshal/Marshal。
  573. jsonStr := *(*string)(unsafe.Pointer(&body))
  574. msgsRes := gjson.Get(jsonStr, "messages")
  575. if !msgsRes.Exists() || !msgsRes.IsArray() {
  576. return body
  577. }
  578. // Fast path:只需要删除顶层 thinking,不需要改 messages。
  579. // 注意:patternThinkingField 可能来自嵌套字段(如 tool_use.input.thinking),因此必须用 gjson 判断顶层字段是否存在。
  580. containsThinkingBlocks := bytes.Contains(body, patternTypeThinking) ||
  581. bytes.Contains(body, patternTypeThinkingSpaced) ||
  582. bytes.Contains(body, patternTypeRedactedThinking) ||
  583. bytes.Contains(body, patternTypeRedactedSpaced) ||
  584. bytes.Contains(body, patternThinkingFieldSpaced)
  585. if !hasEmptyContent && !hasEmptyTextBlock && !containsThinkingBlocks {
  586. if topThinking := gjson.Get(jsonStr, "thinking"); topThinking.Exists() {
  587. if out, err := sjson.DeleteBytes(body, "thinking"); err == nil {
  588. out = removeThinkingDependentContextStrategies(out)
  589. return out
  590. }
  591. return body
  592. }
  593. return body
  594. }
  595. var messages []any
  596. if err := json.Unmarshal(sliceRawFromBody(body, msgsRes), &messages); err != nil {
  597. return body
  598. }
  599. modified := false
  600. // Disable top-level thinking mode for retry to avoid structural/signature constraints upstream.
  601. deleteTopLevelThinking := gjson.Get(jsonStr, "thinking").Exists()
  602. for i := 0; i < len(messages); i++ {
  603. msgMap, ok := messages[i].(map[string]any)
  604. if !ok {
  605. continue
  606. }
  607. role, _ := msgMap["role"].(string)
  608. content, ok := msgMap["content"].([]any)
  609. if !ok {
  610. // String content or other format - keep as is
  611. continue
  612. }
  613. // 延迟分配:只有检测到需要修改的块,才构建新 slice。
  614. var newContent []any
  615. modifiedThisMsg := false
  616. ensureNewContent := func(prefixLen int) {
  617. if newContent != nil {
  618. return
  619. }
  620. newContent = make([]any, 0, len(content))
  621. if prefixLen > 0 {
  622. newContent = append(newContent, content[:prefixLen]...)
  623. }
  624. }
  625. for bi := 0; bi < len(content); bi++ {
  626. block := content[bi]
  627. blockMap, ok := block.(map[string]any)
  628. if !ok {
  629. if newContent != nil {
  630. newContent = append(newContent, block)
  631. }
  632. continue
  633. }
  634. blockType, _ := blockMap["type"].(string)
  635. // Strip empty text blocks: {"type":"text","text":""}
  636. // Upstream rejects these with 400: "text content blocks must be non-empty"
  637. if blockType == "text" {
  638. if txt, _ := blockMap["text"].(string); txt == "" {
  639. modifiedThisMsg = true
  640. ensureNewContent(bi)
  641. continue
  642. }
  643. }
  644. // Convert thinking blocks to text (preserve content) and drop redacted_thinking.
  645. switch blockType {
  646. case "thinking":
  647. modifiedThisMsg = true
  648. ensureNewContent(bi)
  649. thinkingText, _ := blockMap["thinking"].(string)
  650. if thinkingText != "" {
  651. newContent = append(newContent, map[string]any{"type": "text", "text": thinkingText})
  652. }
  653. continue
  654. case "redacted_thinking":
  655. modifiedThisMsg = true
  656. ensureNewContent(bi)
  657. continue
  658. }
  659. // Handle blocks without type discriminator but with a "thinking" field.
  660. if blockType == "" {
  661. if rawThinking, hasThinking := blockMap["thinking"]; hasThinking {
  662. modifiedThisMsg = true
  663. ensureNewContent(bi)
  664. switch v := rawThinking.(type) {
  665. case string:
  666. if v != "" {
  667. newContent = append(newContent, map[string]any{"type": "text", "text": v})
  668. }
  669. default:
  670. if b, err := json.Marshal(v); err == nil && len(b) > 0 {
  671. newContent = append(newContent, map[string]any{"type": "text", "text": string(b)})
  672. }
  673. }
  674. continue
  675. }
  676. }
  677. // Recursively strip empty text blocks from tool_result nested content.
  678. if blockType == "tool_result" {
  679. if nestedContent, ok := blockMap["content"].([]any); ok {
  680. if cleaned, changed := stripEmptyTextBlocksFromSlice(nestedContent); changed {
  681. modifiedThisMsg = true
  682. ensureNewContent(bi)
  683. blockCopy := make(map[string]any, len(blockMap))
  684. for k, v := range blockMap {
  685. blockCopy[k] = v
  686. }
  687. blockCopy["content"] = cleaned
  688. newContent = append(newContent, blockCopy)
  689. continue
  690. }
  691. }
  692. }
  693. if newContent != nil {
  694. newContent = append(newContent, block)
  695. }
  696. }
  697. // Handle empty content: either from filtering or originally empty
  698. if newContent == nil {
  699. if len(content) == 0 {
  700. modified = true
  701. placeholder := "(content removed)"
  702. if role == "assistant" {
  703. placeholder = "(assistant content removed)"
  704. }
  705. msgMap["content"] = []any{map[string]any{"type": "text", "text": placeholder}}
  706. }
  707. continue
  708. }
  709. if len(newContent) == 0 {
  710. modified = true
  711. placeholder := "(content removed)"
  712. if role == "assistant" {
  713. placeholder = "(assistant content removed)"
  714. }
  715. msgMap["content"] = []any{map[string]any{"type": "text", "text": placeholder}}
  716. continue
  717. }
  718. if modifiedThisMsg {
  719. modified = true
  720. msgMap["content"] = newContent
  721. }
  722. }
  723. if !modified && !deleteTopLevelThinking {
  724. // Avoid rewriting JSON when no changes are needed.
  725. return body
  726. }
  727. out := body
  728. if deleteTopLevelThinking {
  729. if b, err := sjson.DeleteBytes(out, "thinking"); err == nil {
  730. out = b
  731. } else {
  732. return body
  733. }
  734. // Removing "thinking" makes any context_management strategy that requires it invalid
  735. // (e.g. clear_thinking_20251015). Strip those entries so the retry request does not
  736. // receive a 400 "strategy requires thinking to be enabled or adaptive".
  737. out = removeThinkingDependentContextStrategies(out)
  738. }
  739. if modified {
  740. msgsBytes, err := json.Marshal(messages)
  741. if err != nil {
  742. return body
  743. }
  744. out, err = sjson.SetRawBytes(out, "messages", msgsBytes)
  745. if err != nil {
  746. return body
  747. }
  748. }
  749. return out
  750. }
  751. // removeThinkingDependentContextStrategies 从 context_management.edits 中移除
  752. // 需要 thinking 启用的策略(如 clear_thinking_20251015)。
  753. // 当顶层 "thinking" 字段被禁用时必须调用,否则上游会返回
  754. // "strategy requires thinking to be enabled or adaptive"。
  755. func removeThinkingDependentContextStrategies(body []byte) []byte {
  756. jsonStr := *(*string)(unsafe.Pointer(&body))
  757. editsRes := gjson.Get(jsonStr, "context_management.edits")
  758. if !editsRes.Exists() || !editsRes.IsArray() {
  759. return body
  760. }
  761. var filtered []json.RawMessage
  762. hasRemoved := false
  763. editsRes.ForEach(func(_, v gjson.Result) bool {
  764. if v.Get("type").String() == "clear_thinking_20251015" {
  765. hasRemoved = true
  766. return true
  767. }
  768. filtered = append(filtered, json.RawMessage(v.Raw))
  769. return true
  770. })
  771. if !hasRemoved {
  772. return body
  773. }
  774. if len(filtered) == 0 {
  775. if b, err := sjson.DeleteBytes(body, "context_management.edits"); err == nil {
  776. return b
  777. }
  778. return body
  779. }
  780. filteredBytes, err := json.Marshal(filtered)
  781. if err != nil {
  782. return body
  783. }
  784. if b, err := sjson.SetRawBytes(body, "context_management.edits", filteredBytes); err == nil {
  785. return b
  786. }
  787. return body
  788. }
  789. // anthropicBetaContextManagementToken 是 context_management 字段受的 beta token。
  790. // 与 claude.BetaContextManagement 保持一致;在本文件本地定义以避免震荡
  791. // claude package 的该常量含义。
  792. const anthropicBetaContextManagementToken = "context-management-2025-06-27"
  793. // sanitizeAnthropicBodyForBetaTokens 是对 Anthropic 直连路径上 body↔beta header
  794. // **能力维度**对称约束的统一实现,与 Bedrock 路径的
  795. // `sanitizeBedrockFieldsForBetaTokens` 对称。
  796. //
  797. // 问题场景:
  798. // - context_management 是 Claude Code CLI 2.1.87+ 默认携带的 beta 字段
  799. // (含 clear_thinking_20251015 等清理策略)
  800. // - 其被 Anthropic 上游接受的前提是 anthropic-beta header 含
  801. // `context-management-2025-06-27`
  802. // - 若两侧不一致上游 Pydantic schema 拒收:
  803. // "context_management: Extra inputs are not permitted"
  804. //
  805. // fallbacks 场景(与 context_management 同构):
  806. // - `fallbacks` / `fallback_credit_token` 是 beta Messages API 的
  807. // server-side refusal fallback 字段;标准 Messages schema 没有它们,
  808. // 客户端(Claude Code / SDK / OpenCode 等)最近开始默认透传
  809. // `"fallbacks":"default"`(或模型列表)
  810. // - 上游接受的前提是 anthropic-beta 含 `server-side-fallback-2026-07-01`
  811. // (fallback_credit_token 额外接受 credit beta,见下)
  812. // - 缺 token 时上游 Pydantic extra='forbid' 拒收:
  813. // "fallbacks: Extra inputs are not permitted"
  814. // - 本仓不写入该字段,全部来自客户端透传;OAuth mimic 用
  815. // FullClaudeCodeMimicryBetas 覆盖客户端 beta(该列表不含 fallback beta),
  816. // 若不 strip,body 字段与 header 不对称 → 所有模型 400
  817. //
  818. // thinking.block_binding 场景:
  819. // - Claude Fable 5.1 的会话前缀绑定控制受
  820. // `thinking-binding-controls-2026-08-01` beta 保护
  821. // - 缺 token 时上游拒收:
  822. // "thinking.adaptive.block_binding: Extra inputs are not permitted"
  823. //
  824. // 本函数按最终发送的 anthropic-beta header 决定是否保留 body 中的上述字段:
  825. // 缺对应 beta token → strip;客户端 header 已带对应 beta → 保留(不过度删除)。
  826. // 这将限制完全建立在 "能力维度" 上,与 model 名 / token type / mimicry 子路径无关。
  827. //
  828. // 调用约束:必须在 CCH 签名之前调用,否则签名 hash 与最终 body
  829. // 不一致,上游会以 third-party 拒收。
  830. //
  831. // 返回 (sanitized, changed):changed 表示是否发生实际删除,供调用方决定
  832. // 是否重用原 body 引用。
  833. func sanitizeAnthropicBodyForBetaTokens(body []byte, anthropicBetaHeader string) ([]byte, bool) {
  834. if len(body) == 0 {
  835. return body, false
  836. }
  837. changed := false
  838. // context_management:需要 context-management beta。
  839. if b, deleted := stripAnthropicBodyFieldUnlessBeta(
  840. body, "context_management", anthropicBetaHeader, anthropicBetaContextManagementToken,
  841. ); deleted {
  842. body, changed = b, true
  843. }
  844. // thinking.block_binding:需要 thinking-binding-controls beta。
  845. if b, deleted := stripAnthropicBodyFieldUnlessBeta(
  846. body, "thinking.block_binding", anthropicBetaHeader, claude.BetaThinkingBindingControls,
  847. ); deleted {
  848. body, changed = b, true
  849. }
  850. // fallbacks:server-side refusal fallback,仅接受 server-side-fallback beta。
  851. if b, deleted := stripAnthropicBodyFieldUnlessBeta(
  852. body, "fallbacks", anthropicBetaHeader, claude.BetaServerSideFallback,
  853. ); deleted {
  854. body, changed = b, true
  855. }
  856. // fallback_credit_token:server-side-fallback 或(新旧任一)fallback-credit beta
  857. // 任意一个即可保留。
  858. if b, deleted := stripAnthropicBodyFieldUnlessBeta(
  859. body, "fallback_credit_token", anthropicBetaHeader,
  860. claude.BetaServerSideFallback, claude.BetaFallbackCredit, claude.BetaFallbackCreditLegacy,
  861. ); deleted {
  862. body, changed = b, true
  863. }
  864. return body, changed
  865. }
  866. // stripAnthropicBodyFieldUnlessBeta 当 field 存在且 anthropic-beta header 不含
  867. // requiredTokens 中**任何一个** token 时删除该字段(保留条件:含任一 required token)。
  868. // 单 token 调用即「缺该 beta 则 strip」。返回 (newBody, deleted)。
  869. func stripAnthropicBodyFieldUnlessBeta(body []byte, field, anthropicBetaHeader string, requiredTokens ...string) ([]byte, bool) {
  870. if !gjson.GetBytes(body, field).Exists() {
  871. return body, false
  872. }
  873. for _, token := range requiredTokens {
  874. if anthropicBetaTokensContains(anthropicBetaHeader, token) {
  875. return body, false
  876. }
  877. }
  878. b, err := sjson.DeleteBytes(body, field)
  879. if err != nil {
  880. // 不应发生:gjson 刚验证过字段存在 + body 是合法 JSON。如果 sjson 仍报错,
  881. // 调用方会拿到原 body(视为未删除),但此前 computeFinalAnthropicBeta 可能已按
  882. // "strip 后" 计算了 finalBeta——两侧会不一致。记录 warning 最小限度提醒运维。
  883. logger.LegacyPrintf("service.gateway",
  884. "[BetaFieldSanitize] sjson.DeleteBytes(%s) failed unexpectedly: %v (body len=%d). "+
  885. "body and final anthropic-beta header may be out of sync.", field, err, len(body))
  886. return body, false
  887. }
  888. return b, true
  889. }
  890. // anthropicBetaTokensContains 检测逗号分隔的 anthropic-beta header 是否含指定 token。
  891. // 宋体空格宽容;区分大小写(Anthropic beta token 始终是小写)。
  892. func anthropicBetaTokensContains(header, token string) bool {
  893. if header == "" || token == "" {
  894. return false
  895. }
  896. for _, part := range strings.Split(header, ",") {
  897. if strings.TrimSpace(part) == token {
  898. return true
  899. }
  900. }
  901. return false
  902. }
  903. // FilterSignatureSensitiveBlocksForRetry is a stronger retry filter for cases where upstream errors indicate
  904. // signature/thought_signature validation issues involving tool blocks.
  905. //
  906. // This performs everything in FilterThinkingBlocksForRetry, plus:
  907. // - Convert `tool_use` blocks to text (name/id/input) so we stop sending structured tool calls.
  908. // - Convert `tool_result` blocks to text so we keep tool results visible without tool semantics.
  909. //
  910. // Use this only when needed: converting tool blocks to text changes model behaviour and can increase the
  911. // risk of prompt injection (tool output becomes plain conversation text).
  912. //
  913. // mappedModel 同 FilterThinkingBlocksForRetry:仅 anthropic-strict 执行变形;
  914. // passback-required 与 unknown 都返回原 body,避免在不熟悉的上游上盲目变形。
  915. func FilterSignatureSensitiveBlocksForRetry(body []byte, mappedModel string) []byte {
  916. if !ShouldApplyRetryFilters(mappedModel) {
  917. return body
  918. }
  919. // Fast path: only run when we see likely relevant constructs.
  920. if !bytes.Contains(body, []byte(`"type":"thinking"`)) &&
  921. !bytes.Contains(body, []byte(`"type": "thinking"`)) &&
  922. !bytes.Contains(body, []byte(`"type":"redacted_thinking"`)) &&
  923. !bytes.Contains(body, []byte(`"type": "redacted_thinking"`)) &&
  924. !bytes.Contains(body, []byte(`"type":"tool_use"`)) &&
  925. !bytes.Contains(body, []byte(`"type": "tool_use"`)) &&
  926. !bytes.Contains(body, []byte(`"type":"tool_result"`)) &&
  927. !bytes.Contains(body, []byte(`"type": "tool_result"`)) &&
  928. !bytes.Contains(body, []byte(`"thinking":`)) &&
  929. !bytes.Contains(body, []byte(`"thinking" :`)) {
  930. return body
  931. }
  932. var req map[string]any
  933. if err := json.Unmarshal(body, &req); err != nil {
  934. return body
  935. }
  936. modified := false
  937. // Disable top-level thinking for retry to avoid structural/signature constraints upstream.
  938. if _, exists := req["thinking"]; exists {
  939. delete(req, "thinking")
  940. modified = true
  941. // Remove context_management strategies that require thinking to be enabled
  942. // (e.g. clear_thinking_20251015), otherwise upstream returns 400.
  943. if cm, ok := req["context_management"].(map[string]any); ok {
  944. if edits, ok := cm["edits"].([]any); ok {
  945. filtered := make([]any, 0, len(edits))
  946. for _, edit := range edits {
  947. if editMap, ok := edit.(map[string]any); ok {
  948. if editMap["type"] == "clear_thinking_20251015" {
  949. continue
  950. }
  951. }
  952. filtered = append(filtered, edit)
  953. }
  954. if len(filtered) != len(edits) {
  955. if len(filtered) == 0 {
  956. delete(cm, "edits")
  957. } else {
  958. cm["edits"] = filtered
  959. }
  960. }
  961. }
  962. }
  963. }
  964. messages, ok := req["messages"].([]any)
  965. if !ok {
  966. return body
  967. }
  968. newMessages := make([]any, 0, len(messages))
  969. for _, msg := range messages {
  970. msgMap, ok := msg.(map[string]any)
  971. if !ok {
  972. newMessages = append(newMessages, msg)
  973. continue
  974. }
  975. role, _ := msgMap["role"].(string)
  976. content, ok := msgMap["content"].([]any)
  977. if !ok {
  978. newMessages = append(newMessages, msg)
  979. continue
  980. }
  981. newContent := make([]any, 0, len(content))
  982. modifiedThisMsg := false
  983. for _, block := range content {
  984. blockMap, ok := block.(map[string]any)
  985. if !ok {
  986. newContent = append(newContent, block)
  987. continue
  988. }
  989. blockType, _ := blockMap["type"].(string)
  990. switch blockType {
  991. case "thinking":
  992. modifiedThisMsg = true
  993. thinkingText, _ := blockMap["thinking"].(string)
  994. if thinkingText == "" {
  995. continue
  996. }
  997. newContent = append(newContent, map[string]any{"type": "text", "text": thinkingText})
  998. continue
  999. case "redacted_thinking":
  1000. modifiedThisMsg = true
  1001. continue
  1002. case "tool_use":
  1003. modifiedThisMsg = true
  1004. name, _ := blockMap["name"].(string)
  1005. id, _ := blockMap["id"].(string)
  1006. input := blockMap["input"]
  1007. inputJSON, _ := json.Marshal(input)
  1008. text := "(tool_use)"
  1009. if name != "" {
  1010. text += " name=" + name
  1011. }
  1012. if id != "" {
  1013. text += " id=" + id
  1014. }
  1015. if len(inputJSON) > 0 && string(inputJSON) != "null" {
  1016. text += " input=" + string(inputJSON)
  1017. }
  1018. newContent = append(newContent, map[string]any{"type": "text", "text": text})
  1019. continue
  1020. case "tool_result":
  1021. modifiedThisMsg = true
  1022. toolUseID, _ := blockMap["tool_use_id"].(string)
  1023. isError, _ := blockMap["is_error"].(bool)
  1024. content := blockMap["content"]
  1025. contentJSON, _ := json.Marshal(content)
  1026. text := "(tool_result)"
  1027. if toolUseID != "" {
  1028. text += " tool_use_id=" + toolUseID
  1029. }
  1030. if isError {
  1031. text += " is_error=true"
  1032. }
  1033. if len(contentJSON) > 0 && string(contentJSON) != "null" {
  1034. text += "\n" + string(contentJSON)
  1035. }
  1036. newContent = append(newContent, map[string]any{"type": "text", "text": text})
  1037. continue
  1038. }
  1039. if blockType == "" {
  1040. if rawThinking, hasThinking := blockMap["thinking"]; hasThinking {
  1041. modifiedThisMsg = true
  1042. switch v := rawThinking.(type) {
  1043. case string:
  1044. if v != "" {
  1045. newContent = append(newContent, map[string]any{"type": "text", "text": v})
  1046. }
  1047. default:
  1048. if b, err := json.Marshal(v); err == nil && len(b) > 0 {
  1049. newContent = append(newContent, map[string]any{"type": "text", "text": string(b)})
  1050. }
  1051. }
  1052. continue
  1053. }
  1054. }
  1055. newContent = append(newContent, block)
  1056. }
  1057. if modifiedThisMsg {
  1058. modified = true
  1059. if len(newContent) == 0 {
  1060. placeholder := "(content removed)"
  1061. if role == "assistant" {
  1062. placeholder = "(assistant content removed)"
  1063. }
  1064. newContent = append(newContent, map[string]any{"type": "text", "text": placeholder})
  1065. }
  1066. msgMap["content"] = newContent
  1067. }
  1068. newMessages = append(newMessages, msgMap)
  1069. }
  1070. if !modified {
  1071. return body
  1072. }
  1073. req["messages"] = newMessages
  1074. newBody, err := json.Marshal(req)
  1075. if err != nil {
  1076. return body
  1077. }
  1078. return newBody
  1079. }
  1080. // filterThinkingBlocksInternal removes invalid thinking blocks from request
  1081. // 策略:
  1082. // - 当 thinking.type 不是 "enabled"/"adaptive":移除所有 thinking 相关块
  1083. // - 当 thinking.type 是 "enabled"/"adaptive":仅移除缺失/无效 signature 的 thinking 块
  1084. func filterThinkingBlocksInternal(body []byte, _ bool) []byte {
  1085. // Fast path: if body doesn't contain "thinking", skip parsing
  1086. if !bytes.Contains(body, []byte(`"type":"thinking"`)) &&
  1087. !bytes.Contains(body, []byte(`"type": "thinking"`)) &&
  1088. !bytes.Contains(body, []byte(`"type":"redacted_thinking"`)) &&
  1089. !bytes.Contains(body, []byte(`"type": "redacted_thinking"`)) &&
  1090. !bytes.Contains(body, []byte(`"thinking":`)) &&
  1091. !bytes.Contains(body, []byte(`"thinking" :`)) {
  1092. return body
  1093. }
  1094. var req map[string]any
  1095. if err := json.Unmarshal(body, &req); err != nil {
  1096. return body
  1097. }
  1098. // Check if thinking is enabled
  1099. thinkingEnabled := false
  1100. if thinking, ok := req["thinking"].(map[string]any); ok {
  1101. if thinkType, ok := thinking["type"].(string); ok && (thinkType == "enabled" || thinkType == "adaptive") {
  1102. thinkingEnabled = true
  1103. }
  1104. }
  1105. messages, ok := req["messages"].([]any)
  1106. if !ok {
  1107. return body
  1108. }
  1109. filtered := false
  1110. for _, msg := range messages {
  1111. msgMap, ok := msg.(map[string]any)
  1112. if !ok {
  1113. continue
  1114. }
  1115. role, _ := msgMap["role"].(string)
  1116. content, ok := msgMap["content"].([]any)
  1117. if !ok {
  1118. continue
  1119. }
  1120. newContent := make([]any, 0, len(content))
  1121. filteredThisMessage := false
  1122. for _, block := range content {
  1123. blockMap, ok := block.(map[string]any)
  1124. if !ok {
  1125. newContent = append(newContent, block)
  1126. continue
  1127. }
  1128. blockType, _ := blockMap["type"].(string)
  1129. if blockType == "thinking" || blockType == "redacted_thinking" {
  1130. // When thinking is enabled and this is an assistant message,
  1131. // only keep thinking blocks with valid signatures
  1132. if thinkingEnabled && role == "assistant" {
  1133. signature, _ := blockMap["signature"].(string)
  1134. if signature != "" && signature != antigravity.DummyThoughtSignature {
  1135. newContent = append(newContent, block)
  1136. continue
  1137. }
  1138. }
  1139. filtered = true
  1140. filteredThisMessage = true
  1141. continue
  1142. }
  1143. // Handle blocks without type discriminator but with "thinking" key
  1144. if blockType == "" {
  1145. if _, hasThinking := blockMap["thinking"]; hasThinking {
  1146. filtered = true
  1147. filteredThisMessage = true
  1148. continue
  1149. }
  1150. }
  1151. newContent = append(newContent, block)
  1152. }
  1153. if filteredThisMessage {
  1154. msgMap["content"] = newContent
  1155. }
  1156. }
  1157. if !filtered {
  1158. return body
  1159. }
  1160. newBody, err := json.Marshal(req)
  1161. if err != nil {
  1162. return body
  1163. }
  1164. return newBody
  1165. }
  1166. // NormalizeClaudeOutputEffort normalizes Claude's output_config.effort value.
  1167. // Returns nil for empty or unrecognized values.
  1168. func NormalizeClaudeOutputEffort(raw string) *string {
  1169. value := strings.ToLower(strings.TrimSpace(raw))
  1170. if value == "" {
  1171. return nil
  1172. }
  1173. switch value {
  1174. case "low", "medium", "high", "xhigh", "max":
  1175. return &value
  1176. default:
  1177. return nil
  1178. }
  1179. }
  1180. // DefaultEffortForThinkingEnabled 给"开启了 thinking 但协议层没有 effort 档位概念"
  1181. // 的国产模型族返回一个默认 effort 字符串("high"),用于 usage_log.reasoning_effort
  1182. // 字段,避免该字段长期为 NULL 导致用量分析无法区分 thinking 开/关。
  1183. //
  1184. // 适用范围(按 ResolveThinkingProtocol 的 PassbackRequired 集合做白名单过滤):
  1185. // - Kimi (kimi-* / moonshot-*)
  1186. // - GLM (glm-*)
  1187. // - MiniMax (minimax-m*)
  1188. // - Qwen thinking 变体 (qwen[1-4]?-*-thinking)
  1189. //
  1190. // **排除 DeepSeek**:DeepSeek 原生支持 reasoning_effort: high/max,客户端可显式指定,
  1191. // 网关不应注入默认值覆盖客户端意图(即便客户端没发,DeepSeek 上游自己会用 high default
  1192. // ——但那是上游行为,不是我们的语义注入)。
  1193. //
  1194. // 适用场景由调用方守卫:仅当 (1) ResolveThinkingProtocol == PassbackRequired
  1195. // (2) 已确认 thinking 启用(Anthropic: parsed.ThinkingEnabled;OpenAI: 见
  1196. // OpenAIBodyHasThinkingEnabled) (3) 已有 effort 解析返回 nil 三者同时成立时调用。
  1197. //
  1198. // 返回值固定指向 "high"。理由:Kimi/GLM/MiniMax 启用 thinking 都是"深度推理模式",
  1199. // 等同 Claude/OpenAI 的 high 档位语义;用 high 比 medium/normal 更贴近实际行为,
  1200. // 也与 DeepSeek thinking-enabled 的默认 effort 一致。
  1201. //
  1202. // 未来兼容性:如果这些厂商后续加入真实 effort 档位(如 Kimi 跟进 DeepSeek 的
  1203. // reasoning_effort: high/max),客户端开始显式发 effort 值时,调用方的守卫条件 (3)
  1204. // 会因 extractor 返回非 nil 而不触发本函数,自动让出。
  1205. func DefaultEffortForThinkingEnabled(mappedModel string) *string {
  1206. if ResolveThinkingProtocol(mappedModel) != ThinkingProtocolPassbackRequired {
  1207. return nil
  1208. }
  1209. // DeepSeek 在 PassbackRequired 集合里但有原生 effort 支持,排除。
  1210. if strings.HasPrefix(strings.ToLower(mappedModel), "deepseek-") {
  1211. return nil
  1212. }
  1213. effort := "high"
  1214. return &effort
  1215. }
  1216. // OpenAIBodyHasThinkingEnabled 检测 OpenAI 协议的请求体里是否启用了 thinking。
  1217. //
  1218. // 国产 OpenAI-兼容上游(GLM via thinkingFormat=zai / Kimi 等)在请求体里用
  1219. // `thinking: {type: "enabled"}` 或 `thinking: {type: "adaptive"}` 表达启用。
  1220. // 仅 "enabled" / "adaptive" 视为开启;"disabled" 或缺省 → 视为关闭。
  1221. //
  1222. // 配合 DefaultEffortForThinkingEnabled 使用:OpenAI 路径上 reasoning_effort 解析为空
  1223. // 但本函数返回 true 时,给 usage_log 填默认 effort。
  1224. func OpenAIBodyHasThinkingEnabled(body []byte) bool {
  1225. thinkingType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, "thinking.type").String()))
  1226. return thinkingType == "enabled" || thinkingType == "adaptive"
  1227. }
  1228. // ApplyThinkingEnabledFallback 补丁已解析出的 effort,仅在 effort 为 nil 且
  1229. // 检测到 body 里 thinking 启用 + mappedModel 属于国产 passback-required 上游时,
  1230. // 返回 DefaultEffortForThinkingEnabled 的默认值("high")。不覆盖已解析出的值。
  1231. //
  1232. // 适用于 OpenAI 网关的多条路径调用方(避免重复的 if-nil 表达式)。
  1233. func ApplyThinkingEnabledFallback(effort *string, body []byte, mappedModel string) *string {
  1234. if effort != nil {
  1235. return effort
  1236. }
  1237. if !OpenAIBodyHasThinkingEnabled(body) {
  1238. return nil
  1239. }
  1240. return DefaultEffortForThinkingEnabled(mappedModel)
  1241. }
  1242. // NormalizeGLMOpenAIReasoningEffort rewrites OpenAI Chat Completions
  1243. // reasoning_effort values to the GLM native scale used by z.ai: high/max.
  1244. // It only applies to glm-* mapped models and leaves all other providers untouched.
  1245. func NormalizeGLMOpenAIReasoningEffort(body []byte, mappedModel string) ([]byte, bool) {
  1246. if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(mappedModel)), "glm-") {
  1247. return body, false
  1248. }
  1249. path := "reasoning.effort"
  1250. raw := strings.TrimSpace(gjson.GetBytes(body, path).String())
  1251. if raw == "" {
  1252. path = "reasoning_effort"
  1253. raw = strings.TrimSpace(gjson.GetBytes(body, path).String())
  1254. }
  1255. if raw == "" {
  1256. return body, false
  1257. }
  1258. mapped := normalizeGLMOpenAIReasoningEffort(raw)
  1259. if isGLM53Model(mappedModel) && mapped == "high" && normalizeEffortToken(raw) == "low" {
  1260. mapped = "low"
  1261. }
  1262. if mapped == "" || mapped == raw {
  1263. return body, false
  1264. }
  1265. modified, err := sjson.SetBytes(body, path, mapped)
  1266. if err != nil {
  1267. return body, false
  1268. }
  1269. return modified, true
  1270. }
  1271. func normalizeEffortToken(raw string) string {
  1272. value := strings.ToLower(strings.TrimSpace(raw))
  1273. return strings.NewReplacer("-", "", "_", "", " ", "").Replace(value)
  1274. }
  1275. func isGLM53Model(model string) bool {
  1276. return strings.EqualFold(strings.TrimSpace(model), "glm-5.3")
  1277. }
  1278. func normalizeGLMOpenAIReasoningEffort(raw string) string {
  1279. value := normalizeEffortToken(raw)
  1280. if value == "" {
  1281. return ""
  1282. }
  1283. switch value {
  1284. case "low", "medium", "high":
  1285. return "high"
  1286. case "xhigh", "extrahigh", "max", "ultracode":
  1287. return "max"
  1288. default:
  1289. return ""
  1290. }
  1291. }
  1292. // NormalizeGLM53AnthropicThinking maps explicit client thinking effort onto the
  1293. // GLM-5.3 Anthropic-compatible scale. Requests without an effort or thinking
  1294. // preference are left unchanged so the upstream default remains in effect.
  1295. func NormalizeGLM53AnthropicThinking(body []byte, mappedModel string) ([]byte, bool) {
  1296. if !isGLM53Model(mappedModel) {
  1297. return body, false
  1298. }
  1299. raw := gjson.GetBytes(body, "output_config.effort").String()
  1300. if strings.TrimSpace(raw) == "" {
  1301. raw = gjson.GetBytes(body, "thinking.type").String()
  1302. }
  1303. var effort string
  1304. switch normalizeEffortToken(raw) {
  1305. case "disabled", "off", "none", "minimal", "low":
  1306. effort = "low"
  1307. case "enabled", "adaptive", "medium", "high":
  1308. effort = "high"
  1309. case "xhigh", "max", "ultra":
  1310. effort = "max"
  1311. default:
  1312. return body, false
  1313. }
  1314. modified, err := sjson.SetBytes(body, "thinking.type", "enabled")
  1315. if err != nil {
  1316. return body, false
  1317. }
  1318. modified, err = sjson.SetBytes(modified, "output_config.effort", effort)
  1319. if err != nil {
  1320. return body, false
  1321. }
  1322. return modified, true
  1323. }
  1324. // =========================
  1325. // Thinking Budget Rectifier
  1326. // =========================
  1327. const (
  1328. // BudgetRectifyBudgetTokens is the budget_tokens value to set when rectifying.
  1329. BudgetRectifyBudgetTokens = 32000
  1330. // BudgetRectifyMaxTokens is the max_tokens value to set when rectifying.
  1331. BudgetRectifyMaxTokens = 64000
  1332. // BudgetRectifyMinMaxTokens is the minimum max_tokens that must exceed budget_tokens.
  1333. BudgetRectifyMinMaxTokens = 32001
  1334. )
  1335. // isThinkingBudgetConstraintError detects whether an upstream error message indicates
  1336. // a budget_tokens constraint violation (e.g. "budget_tokens >= 1024").
  1337. // Matches three conditions (all must be true):
  1338. // 1. Contains "budget_tokens" or "budget tokens"
  1339. // 2. Contains "thinking"
  1340. // 3. Contains ">= 1024" or "greater than or equal to 1024" or ("1024" + "input should be")
  1341. func isThinkingBudgetConstraintError(errMsg string) bool {
  1342. m := strings.ToLower(errMsg)
  1343. // Condition 1: budget_tokens or budget tokens
  1344. hasBudget := strings.Contains(m, "budget_tokens") || strings.Contains(m, "budget tokens")
  1345. if !hasBudget {
  1346. return false
  1347. }
  1348. // Condition 2: thinking
  1349. if !strings.Contains(m, "thinking") {
  1350. return false
  1351. }
  1352. // Condition 3: constraint indicator
  1353. if strings.Contains(m, ">= 1024") || strings.Contains(m, "greater than or equal to 1024") {
  1354. return true
  1355. }
  1356. if strings.Contains(m, "1024") && strings.Contains(m, "input should be") {
  1357. return true
  1358. }
  1359. return false
  1360. }
  1361. // RectifyThinkingBudget modifies the request body to fix budget_tokens constraint errors.
  1362. // It sets thinking.budget_tokens = 32000, thinking.type = "enabled" (unless adaptive),
  1363. // and ensures max_tokens >= 32001.
  1364. // Returns (modified body, true) if changes were applied, or (original body, false) if not.
  1365. func RectifyThinkingBudget(body []byte) ([]byte, bool) {
  1366. // If thinking type is "adaptive", skip rectification entirely
  1367. thinkingType := gjson.GetBytes(body, "thinking.type").String()
  1368. if thinkingType == "adaptive" {
  1369. return body, false
  1370. }
  1371. modified := body
  1372. changed := false
  1373. // Set thinking.type = "enabled"
  1374. if thinkingType != "enabled" {
  1375. if result, err := sjson.SetBytes(modified, "thinking.type", "enabled"); err == nil {
  1376. modified = result
  1377. changed = true
  1378. }
  1379. }
  1380. // Set thinking.budget_tokens = 32000
  1381. currentBudget := gjson.GetBytes(modified, "thinking.budget_tokens").Int()
  1382. if currentBudget != BudgetRectifyBudgetTokens {
  1383. if result, err := sjson.SetBytes(modified, "thinking.budget_tokens", BudgetRectifyBudgetTokens); err == nil {
  1384. modified = result
  1385. changed = true
  1386. }
  1387. }
  1388. // Ensure max_tokens >= BudgetRectifyMinMaxTokens
  1389. maxTokens := gjson.GetBytes(modified, "max_tokens").Int()
  1390. if maxTokens < int64(BudgetRectifyMinMaxTokens) {
  1391. if result, err := sjson.SetBytes(modified, "max_tokens", BudgetRectifyMaxTokens); err == nil {
  1392. modified = result
  1393. changed = true
  1394. }
  1395. }
  1396. return modified, changed
  1397. }
  1398. // NormalizeChineseLLMThinking rewrites the top-level `thinking` object for Chinese
  1399. // LLM providers that use Anthropic-compatible endpoints but have different accepted
  1400. // values for `thinking.type`. Currently scoped to:
  1401. // - MiniMax M-series (`MiniMax-m*`, covering M2.x / M3 / M3.x): official docs accept
  1402. // only `thinking.type` of "adaptive" or "disabled"; "enabled" is not a valid value
  1403. // and may be rejected/ignored. Pi-ai and other Anthropic-SDK clients default to
  1404. // "enabled" (Anthropic-original) and never auto-rewrite for non-Anthropic models.
  1405. //
  1406. // Non-MiniMax models (Kimi/GLM/DeepSeek) currently accept "enabled" as-is, so this
  1407. // function is intentionally a no-op for them. New Chinese LLM quirks should be
  1408. // added here as separate case branches.
  1409. //
  1410. // Returns (modified body, true) if a rewrite was applied, or (original body, false)
  1411. // if no rewrite was needed. Caller should be on the Anthropic forward path AFTER
  1412. // FilterThinkingBlocks and BEFORE building the upstream request, only for
  1413. // passback-required models (ResolveThinkingProtocol == PassbackRequired).
  1414. func NormalizeChineseLLMThinking(body []byte, mappedModel string) ([]byte, bool) {
  1415. modelLower := strings.ToLower(mappedModel)
  1416. if !strings.HasPrefix(modelLower, "minimax-m") {
  1417. return body, false
  1418. }
  1419. thinkingType := gjson.GetBytes(body, "thinking.type").String()
  1420. if thinkingType != "enabled" {
  1421. return body, false
  1422. }
  1423. modified, err := sjson.SetBytes(body, "thinking.type", "adaptive")
  1424. if err != nil {
  1425. return body, false
  1426. }
  1427. return modified, true
  1428. }