thinking_protocol.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. package service
  2. import "strings"
  3. // ThinkingProtocol 描述上游对 thinking block 的处理契约。
  4. // 不同上游对历史 thinking block 的语义要求是相反的:
  5. // - Anthropic 官方:要求 thinking block 携带有效 signature,否则 400
  6. // "thinking.signature: Field required"
  7. // - DeepSeek `/anthropic`、Kimi `/coding` 等第三方 Anthropic 兼容上游:
  8. // 要求历史 thinking block 原样回传,否则 400
  9. // "The content[].thinking in the thinking mode must be passed back to the API"
  10. //
  11. // 见 .pensieve/short-term/knowledge/thinking-block-filter-third-party-upstream-inversion/
  12. type ThinkingProtocol int
  13. const (
  14. // ThinkingProtocolUnknown 表示无法识别协议族(默认保守不剥离)。
  15. ThinkingProtocolUnknown ThinkingProtocol = iota
  16. // ThinkingProtocolAnthropicStrict 表示 Anthropic 官方语义:
  17. // 历史 thinking block 必须携带有效 signature,缺失/非法签名应剥离。
  18. ThinkingProtocolAnthropicStrict
  19. // ThinkingProtocolPassbackRequired 表示第三方兼容上游语义:
  20. // 所有历史 thinking block 必须原样回传,预过滤会破坏契约。
  21. ThinkingProtocolPassbackRequired
  22. )
  23. // ResolveThinkingProtocol 根据「作为 thinking block 处理参考的模型 ID」推断 thinking 协议族。
  24. //
  25. // 传入参数的语义随调用路径不同:
  26. // - **Anthropic gateway**(转发原始 Anthropic 请求):传 mappedModel(账号级 model mapping
  27. // 后的上游 model ID)。例:用户配置「claude-sonnet-4-6 → deepseek-v4-pro」后,
  28. // 传 deepseek-v4-pro 才能被正确判为 passback-required。
  29. // - **Gemini messages compat**(Anthropic body → Gemini upstream):传 originalModel
  30. // (客户端 Anthropic 请求的 model ID)。原因:此场景下上游是 Gemini,但被剥
  31. // 离的 body 是 Anthropic 格式,需按客户端请求的 Anthropic 子协议族判定剥离行为。
  32. //
  33. // 匹配规则按厂商前缀硬编码:
  34. // - anthropic-strict: claude-* / opus-* / sonnet-* / haiku-*
  35. // - passback-required: deepseek-* / kimi-* / moonshot-* / glm-* /
  36. // minimax-* / minimax-m* / (qwen-|qwen2-|qwen3-|qwen4-)*-thinking /
  37. // Kimi Code bare aliases k3 / k3-256k(精确匹配,避免宽泛 k3 前缀)
  38. // - unknown: 其他模型(保守不剥离)
  39. //
  40. // 已知局限:前缀贪婪匹配(如 `claudette-`、`claude-foreign-relay-` 也会被分类为
  41. // strict)。当遇到伪装命名时改成显式名单匹配,但现实场景几乎不会出现。
  42. //
  43. // 不覆盖的厂商(截至 2026-04):
  44. // - Doubao / Seed (ByteDance):走 Volcano Engine OpenAI 协议,非 Anthropic 路径
  45. // - Hunyuan T1 (Tencent):未提供 Anthropic 兼容端点
  46. // - 若未来出现这些厂商的 Anthropic 兼容代理,需扩展前缀列表
  47. func ResolveThinkingProtocol(modelID string) ThinkingProtocol {
  48. if modelID == "" {
  49. return ThinkingProtocolUnknown
  50. }
  51. id := strings.ToLower(modelID)
  52. // Passback-required 优先匹配(特定厂商前缀),避免误判 claude-* 时也命中。
  53. // kimi-k3* 已由 kimi- 前缀覆盖;Kimi Code endpoint 的 bare model ID(k3 / k3-256k)
  54. // 无厂商前缀,仅精确匹配,避免 "foo-k3" 等未知型号被宽泛前缀误判。
  55. switch {
  56. case strings.HasPrefix(id, "deepseek-"),
  57. strings.HasPrefix(id, "kimi-"),
  58. strings.HasPrefix(id, "moonshot-"),
  59. strings.HasPrefix(id, "glm-"),
  60. id == "k3",
  61. id == "k3-256k":
  62. return ThinkingProtocolPassbackRequired
  63. }
  64. // MiniMax M 系列:走 https://api.minimax.io/anthropic 端点,
  65. // 官方明文要求 thinking block round-trip(interleaved thinking 协议)。
  66. // 实例:MiniMax-M2、MiniMax-M2.1、MiniMax-M2.5、MiniMax-M2.7、MiniMax-M2.7-highspeed
  67. // 大小写在 ToLower 后统一为 minimax-。
  68. if strings.HasPrefix(id, "minimax-m") {
  69. return ThinkingProtocolPassbackRequired
  70. }
  71. // Qwen thinking 变体:覆盖 qwen-/qwen2-/qwen3-/qwen4- 前缀 + 包含 -thinking
  72. // 实例:qwen3-235b-a22b-thinking-2507、qwen3-next-80b-a3b-thinking、qwen-3-72b-thinking
  73. if (strings.HasPrefix(id, "qwen-") ||
  74. strings.HasPrefix(id, "qwen2-") ||
  75. strings.HasPrefix(id, "qwen3-") ||
  76. strings.HasPrefix(id, "qwen4-")) && strings.Contains(id, "-thinking") {
  77. return ThinkingProtocolPassbackRequired
  78. }
  79. switch {
  80. case strings.HasPrefix(id, "claude-"),
  81. strings.HasPrefix(id, "opus-"),
  82. strings.HasPrefix(id, "sonnet-"),
  83. strings.HasPrefix(id, "haiku-"):
  84. return ThinkingProtocolAnthropicStrict
  85. }
  86. return ThinkingProtocolUnknown
  87. }
  88. // ShouldPreFilterThinkingBlocks 判断是否应在转发前剥离无效 thinking block。
  89. // 仅 anthropic-strict 协议族需要预过滤;passback-required/unknown 都跳过,
  90. // 因为「保留 thinking block」对 anthropic-strict 之外的上游一律更安全。
  91. func ShouldPreFilterThinkingBlocks(modelID string) bool {
  92. return ResolveThinkingProtocol(modelID) == ThinkingProtocolAnthropicStrict
  93. }
  94. // ShouldRectifyThinkingSignatureError 判断是否应在 400 后触发 thinking 签名整流 retry。
  95. // 仅 anthropic-strict 触发;passback-required 路径的 400 一般不是签名缺失问题,
  96. // retry 任何 thinking 变形都不会修好,反而会破坏契约。unknown 同理保守不 retry。
  97. func ShouldRectifyThinkingSignatureError(modelID string) bool {
  98. return ResolveThinkingProtocol(modelID) == ThinkingProtocolAnthropicStrict
  99. }
  100. // ShouldApplyRetryFilters 判断是否应执行 retry 路径的 thinking/tool block 整流。
  101. // 与预过滤保持对称:仅 anthropic-strict 走变形;passback-required 与 unknown
  102. // 一律返回原 body 不变形——避免在不熟悉的上游上做出可能破坏契约的猜测。
  103. func ShouldApplyRetryFilters(modelID string) bool {
  104. return ResolveThinkingProtocol(modelID) == ThinkingProtocolAnthropicStrict
  105. }