package model import ( "log/slog" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig" ) // 本文件把调用方的推理旋钮映射到**端点自己的** wire 形状(裁决 03.4.2 / 08.4.1: // 不同提供商的思考开关形状各不相同,必须逐 provider 独立适配,禁止「识别到配置 // 却只 WARN 不下发」的假适配)。形状由 models.json 组的 `reasoning.kind` 显式声明 // (配置载体见 internal/config/runtimeconfig),未声明时按模型名/BaseURL 推断厂商, // 且只在形状**无歧义**时才下发。 // // 依据(实现前逐条核对,不猜字段名): // - DeepSeek:顶层 `thinking:{type:enabled|disabled}` + `reasoning_effort` // (官方取值 none/low/high/max 与别名表;思考**默认开**)。来源: // `DevDocs/reference/deepseek/guides-thinking_mode.md`(官方文档本地副本)。 // - OpenAI 系:只有 `reasoning_effort`,没有布尔思考开关。来源:design/04 §3.2。 // - vLLM(承载 Qwen3 等):`chat_template_kwargs.enable_thinking`。来源:design/04 // §3.2 的 Qwen3 行(记作 `extra_body.chat_template_kwargs.enable_thinking`; // extra_body 是 OpenAI SDK「把键合并进顶层 body」的机制,与 DeepSeek 官方文档 // 对 `thinking` 的描述同构,故线上形状是顶层 `chat_template_kwargs`)。 // - DashScope 兼容模式:顶层 `enable_thinking`。来源:design/04 §3.2(「DashScope // 等互不相同」)+仓库既有实现(reasoning.go 原注释)。⚠️ 本仓库没有 DashScope // 官方文档本地副本,因此该形状**只在配置显式声明 reasoning.kind=dashscope 时 // 下发**,绝不从模型名/BaseURL 自动推断(同一模型名可能由 vLLM 或 DashScope // 承载,形状不同,发错会 400)。 // - Anthropic:自己的 `thinking:{type,budget_tokens}`,见 anthropic_reasoning.go。 // switchShape 描述「只有布尔开关、没有 reasoning_effort」的端点形状。 type switchShape struct { // provider 是 WARN 与日志里报告的名字(vllm / dashscope)。 provider string // field 是该形状开关的线上字段路径,只用于日志(含配置指引)。 field string } var ( // shapeVLLM 是 vLLM 类部署:开关在 chat_template_kwargs 里。 shapeVLLM = switchShape{provider: "vllm", field: "chat_template_kwargs.enable_thinking"} // shapeDashScope 是 DashScope 兼容模式:开关在请求体顶层。 shapeDashScope = switchShape{provider: "dashscope", field: "enable_thinking"} ) // openAIReasoning 是一次调用要并入 OpenAI 兼容请求体的推理指令。它是各形状的 // 并集,但**只会填入该端点形状对应的那一个字段**:形状之间互不替代。 type openAIReasoning struct { // thinking 是 DeepSeek 的顶层开关 {"thinking":{"type":...}}。 thinking *openAIThinking // effort 是 reasoning_effort(OpenAI 系与 DeepSeek 用)。 effort string // template 是 vLLM 的 {"chat_template_kwargs":{"enable_thinking":..}}。 template *openAIChatTemplateKwargs // enable 是 DashScope 兼容模式的顶层 enable_thinking。 enable *bool // provider 是本次解析归属的 provider 名,供日志定位。 provider string // providerDefaultOn 报告该端点**未声明开关时**的思考默认状态。只有 DeepSeek // 有官方依据(默认 enabled),其余形状为 false:没有依据就不断言其默认。 providerDefaultOn bool } // thinkingOn 报告该请求的思考是否处于开启状态(显式开关优先,其次是 // reasoning_effort none 这个官方关闭值,最后才是端点自身默认)。它只用于 // 提示性 WARN(强制 tool_choice 与思考不兼容),不改变请求。 func (r openAIReasoning) thinkingOn() bool { switch { case r.thinking != nil: return r.thinking.Type == thinkingTypeEnabled case r.template != nil && r.template.EnableThinking != nil: return *r.template.EnableThinking case r.enable != nil: return *r.enable case r.effort != "": return r.effort != effortNone } return r.providerDefaultOn } // setSwitch 把布尔开关按形状挂到对应字段上。 func (r *openAIReasoning) setSwitch(shape switchShape, enable bool) { value := enable switch shape { case shapeVLLM: r.template = &openAIChatTemplateKwargs{EnableThinking: &value} case shapeDashScope: r.enable = &value } } // resolveOpenAIReasoning 按 provider 形状解析推理指令。kind 是配置声明的形状 // (空/"auto" = 推断);vendor 是推断出的厂商,仅在 auto 下参与决策。 func resolveOpenAIReasoning(log *slog.Logger, kind string, vendor reasoningVendor, model string, th *Thinking, effort string) openAIReasoning { switch strings.ToLower(strings.TrimSpace(kind)) { case runtimeconfig.ReasoningKindDeepSeek: return resolveDeepSeekReasoning(log, model, th, effort) case runtimeconfig.ReasoningKindOpenAI: return resolveEffortOnlyReasoning(log, vendorOpenAI, model, th, effort) case runtimeconfig.ReasoningKindVLLM: return resolveSwitchReasoning(log, shapeVLLM, model, th, effort) case runtimeconfig.ReasoningKindDashScope: return resolveSwitchReasoning(log, shapeDashScope, model, th, effort) case "", runtimeconfig.ReasoningKindAuto: return resolveAutoReasoning(log, vendor, model, th, effort) default: // 配置里写了一个本版本不认识的形状:保守不发任何字段,但必须报告, // 否则「配了却不生效」正是本裁决要消灭的假适配。 log.Warn("reasoning.kind unrecognized: no reasoning field is sent", "kind", kind, "model", model, "field", "reasoning.kind") return openAIReasoning{provider: strings.TrimSpace(kind)} } } // resolveAutoReasoning 处理未声明形状的端点:按推断出的厂商选择**该厂商唯一 // 无歧义的**形状,厂商认识但形状有歧义(Qwen 既可能是 vLLM 也可能是 DashScope) // 时不下发开关,只报告并给出配置指引。 func resolveAutoReasoning(log *slog.Logger, vendor reasoningVendor, model string, th *Thinking, effort string) openAIReasoning { switch vendor { case vendorDeepSeek: return resolveDeepSeekReasoning(log, model, th, effort) case vendorOpenAI: return resolveEffortOnlyReasoning(log, vendorOpenAI, model, th, effort) default: return resolveNoShapeReasoning(log, vendor, model, th, effort) } } // resolveDeepSeekReasoning 映射 DeepSeek 的 OpenAI 兼容形状:顶层 thinking 开关 // 与 reasoning_effort。DeepSeek 的思考**默认开启**,所以「不传」不等于「关」; // 要关只能显式 disabled 或 effort none(官方文档 guides-thinking_mode)。 func resolveDeepSeekReasoning(log *slog.Logger, model string, th *Thinking, effort string) openAIReasoning { effort = resolveReasoningEffort(log, vendorDeepSeek, effort, model) out := openAIReasoning{effort: effort, provider: string(vendorDeepSeek), providerDefaultOn: true} if th != nil && th.BudgetTokens > 0 { log.Warn("thinking.budget_tokens ignored: the DeepSeek chat-completions protocol takes reasoning_effort, not a token budget", "provider", string(vendorDeepSeek), "field", "thinking.budget_tokens", "budget_tokens", th.BudgetTokens, "model", model) } if th == nil || th.Enable == nil { return out } if !*th.Enable { if effort != "" { log.Warn("reasoning_effort dropped: thinking is explicitly disabled", "provider", string(vendorDeepSeek), "field", "reasoning_effort", "value", effort, "model", model) } out.thinking = &openAIThinking{Type: thinkingTypeDisabled} out.effort = "" return out } if effort == effortNone { // 「关」胜过「开」:provider 未声明优先级,让强度档把思考重新打开正是 // 本解析要阻止的语义反转(design/04 §3.2 fact 2)。 log.Warn("reasoning_effort none contradicts thinking enabled: thinking disabled wins", "provider", string(vendorDeepSeek), "field", "reasoning_effort", "value", effort, "model", model) out.thinking = &openAIThinking{Type: thinkingTypeDisabled} out.effort = "" return out } out.thinking = &openAIThinking{Type: thinkingTypeEnabled} return out } // resolveEffortOnlyReasoning 映射「只有 reasoning_effort」的端点(OpenAI 系): // 强度档按该厂商白名单校验,布尔开关没有 wire 字段可发,因此只报告并说明 // 官方替代路径(effort none 才是关闭推理的方式)。 func resolveEffortOnlyReasoning(log *slog.Logger, vendor reasoningVendor, model string, th *Thinking, effort string) openAIReasoning { effort = resolveReasoningEffort(log, vendor, effort, model) out := openAIReasoning{effort: effort, provider: string(vendor)} if th == nil { return out } if th.Enable != nil { log.Warn("thinking switch ignored: this endpoint has no boolean thinking switch; reasoning_effort none is the documented way to disable reasoning", "provider", string(vendor), "field", "thinking.enable", "enable", *th.Enable, "model", model) } if th.BudgetTokens > 0 { log.Warn("thinking budget ignored: OpenAI-compatible providers take reasoning_effort, not a token budget", "provider", string(vendor), "field", "thinking.budget_tokens", "budget_tokens", th.BudgetTokens, "model", model) } return out } // resolveSwitchReasoning 映射「只有布尔开关」的端点(vLLM / DashScope 类): // 开关按形状下发;该形状没有 reasoning_effort 字段,因此强度档被丢弃并报告—— // 唯一的例外是官方关闭值 `none`,它被翻译成**显式关思考**(红线:none = 关思考, // 绝不翻译成任何强度档,那会把「关」变成「开」)。 func resolveSwitchReasoning(log *slog.Logger, shape switchShape, model string, th *Thinking, effort string) openAIReasoning { out := openAIReasoning{provider: shape.provider} var enable *bool if th != nil { if th.BudgetTokens > 0 { log.Warn("thinking budget ignored: this endpoint takes a boolean thinking switch, not a token budget", "provider", shape.provider, "field", "thinking.budget_tokens", "budget_tokens", th.BudgetTokens, "model", model, "endpoint_field", shape.field) } enable = th.Enable } if effort == effortNone { if enable != nil && *enable { log.Warn("reasoning_effort none contradicts thinking enabled: thinking disabled wins", "provider", shape.provider, "field", "reasoning_effort", "value", effort, "model", model) } else if enable == nil { log.Warn("reasoning_effort has no field on this endpoint: none is applied as the explicit thinking switch off", "provider", shape.provider, "field", shape.field, "value", effort, "model", model) } out.setSwitch(shape, false) return out } if effort != "" { dropUnsupportedEffort(log, shape, model, effort) } if enable != nil { out.setSwitch(shape, *enable) } return out } // dropUnsupportedEffort 报告该形状没有 reasoning_effort 字段(含配置指引), // 绝不改写成近似档位:改档会反转调用方意图,正是旧实现踩过的坑。 func dropUnsupportedEffort(log *slog.Logger, shape switchShape, model, effort string) { log.Warn("reasoning_effort dropped: this endpoint has no reasoning_effort field", "provider", shape.provider, "field", "reasoning_effort", "value", effort, "model", model, "endpoint_field", shape.field) } // resolveNoShapeReasoning 处理「厂商未知或形状有歧义」的端点:不发送任何推理 // 字段(发错形状会 400),但逐项报告被忽略的旋钮与配置指引(design/04 §3.2: // 禁止静默无效)。 func resolveNoShapeReasoning(log *slog.Logger, vendor reasoningVendor, model string, th *Thinking, effort string) openAIReasoning { reportUnmappedThinking(log, vendor, model, th) if effort != "" { log.Warn("reasoning_effort ignored: provider does not support it", "provider", string(vendor), "field", "reasoning_effort", "value", effort, "model", model) } return openAIReasoning{provider: string(vendor)} } // reportUnmappedThinking 报告一个 Thinking 指令为何无法由该端点承载:厂商认识 // 但形状有歧义(Qwen 既可能由 vLLM 承载、也可能由 DashScope 承载,两者字段形状 // 不同),或厂商未知。两条路都给出配置指引,让主人能显式声明形状而不是让我们猜。 // DeepSeek 的开关由 resolveDeepSeekReasoning 映射,不经过这里。 func reportUnmappedThinking(log *slog.Logger, vendor reasoningVendor, model string, th *Thinking) { if th == nil { return } if th.Enable != nil { switch vendor { case vendorQwen: log.Warn("enable_thinking not forwarded: the endpoint's switch shape is ambiguous (vLLM wants chat_template_kwargs.enable_thinking, DashScope wants a top-level enable_thinking) and sending the wrong shape can 400; declare reasoning.kind to choose one", "provider", "qwen", "field", "extra_body.chat_template_kwargs.enable_thinking", "enable", *th.Enable, "model", model, "config_field", "reasoning.kind") default: log.Warn("thinking switch ignored: provider has no boolean thinking switch", "provider", string(vendor), "field", "thinking.enable", "enable", *th.Enable, "model", model) } return } if th.BudgetTokens > 0 { log.Warn("thinking budget ignored: OpenAI-compatible providers take reasoning_effort, not a token budget", "provider", string(vendor), "field", "thinking.budget_tokens", "budget_tokens", th.BudgetTokens, "model", model) } }