doc(thinking-protocol): clarify mappedModel vs originalModel semantics per call path

回应 PR #3247 Copilot review:

1. NormalizeChineseLLMThinking 文档写 'MiniMax M3 / M3.x' 但实现覆盖
   minimax-m* (M2.x 也命中)。改成 'MiniMax M-series (M2.x/M3.x)'。

2. Copilot 质疑 gemini_messages_compat 传 originalModel 不是
   mappedModel 是误用。实际是有意为之的跨协议场景:
   - 上游是 Gemini,但被剥离的 body 是 Anthropic 格式
   - 剥离逻辑要按客户端请求的 Anthropic 子协议族判定
   - 传 mappedModel (gemini-3.1-pro) 会被判为 Unknown→不剥离→retry 死循环

   未改代码逻辑,改为更新文档:
   - thinking_protocol.go ResolveThinkingProtocol 添加「调用路径语义」
     说明 Anthropic gateway 与 Gemini compat 两条路径的参数语义差异。
   - gemini_messages_compat_service.go 调用点加 inline 注释解释
     路径语义为什么该传 originalModel。
This commit is contained in:
alfadb
2026-06-16 19:37:31 +08:00
parent 56c6325d15
commit 5c5283979b
3 changed files with 16 additions and 7 deletions
+4 -4
View File
@@ -1294,10 +1294,10 @@ func RectifyThinkingBudget(body []byte) ([]byte, bool) {
// NormalizeChineseLLMThinking rewrites the top-level `thinking` object for Chinese
// LLM providers that use Anthropic-compatible endpoints but have different accepted
// values for `thinking.type`. Currently scoped to:
// - MiniMax M3 / M3.x (`MiniMax-m*`): official docs accept only `thinking.type`
// of "adaptive" or "disabled"; "enabled" is not a valid value and may be
// rejected/ignored. Pi-ai and other Anthropic-SDK clients default to "enabled"
// (Anthropic-original) and never auto-rewrite for non-Anthropic models.
// - MiniMax M-series (`MiniMax-m*`, covering M2.x / M3 / M3.x): official docs accept
// only `thinking.type` of "adaptive" or "disabled"; "enabled" is not a valid value
// and may be rejected/ignored. Pi-ai and other Anthropic-SDK clients default to
// "enabled" (Anthropic-original) and never auto-rewrite for non-Anthropic models.
//
// Non-MiniMax models (Kimi/GLM/DeepSeek) currently accept "enabled" as-is, so this
// function is intentionally a no-op for them. New Chinese LLM quirks should be
@@ -832,6 +832,9 @@ func (s *GeminiMessagesCompatService) Forward(ctx context.Context, c *gin.Contex
var strippedClaudeBody []byte
stageName := ""
// 路径说明:本处上游是 Gemini,但被剥离的 body 是 Anthropic 格式。传 originalModel
// (客户端原 Anthropic model)而非 mappedModel(上游 Gemini model),让剥离逻辑按
// 客户端请求的 Anthropic 子协议族判定(详见 ResolveThinkingProtocol 文档)。
switch signatureRetryStage {
case 0:
// Stage 1: disable thinking + thinking->text
@@ -26,9 +26,15 @@ const (
ThinkingProtocolPassbackRequired
)
// ResolveThinkingProtocol 根据「实际发给上游的模型 ID」推断 thinking 协议族。
// 必须传入映射后的模型 ID(mappedModel),不要传客户端原始请求模型 ID(reqModel),
// 否则用户配置「claude-sonnet-4-6 → deepseek-v4-pro」时会判错。
// ResolveThinkingProtocol 根据「作为 thinking block 处理参考的模型 ID」推断 thinking 协议族。
//
// 传入参数的语义随调用路径不同:
// - **Anthropic gateway**(转发原始 Anthropic 请求):传 mappedModel(账号级 model mapping
// 后的上游 model ID)。例:用户配置「claude-sonnet-4-6 → deepseek-v4-pro」后,
// 传 deepseek-v4-pro 才能被正确判为 passback-required。
// - **Gemini messages compat**(Anthropic body → Gemini upstream):传 originalModel
// (客户端 Anthropic 请求的 model ID)。原因:此场景下上游是 Gemini,但被剥
// 离的 body 是 Anthropic 格式,需按客户端请求的 Anthropic 子协议族判定剥离行为。
//
// 匹配规则按厂商前缀硬编码:
// - anthropic-strict: claude-* / opus-* / sonnet-* / haiku-*