- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
127 lines
5.6 KiB
Go
127 lines
5.6 KiB
Go
package model
|
||
|
||
import (
|
||
"log/slog"
|
||
"strings"
|
||
)
|
||
|
||
// Anthropic 扩展思考协议的边界(design/04 §3.2)。
|
||
const (
|
||
// anthropicMinThinkingBudget 是 Messages API 在 thinking.type=enabled 时
|
||
// 接受的最小 budget_tokens;低于它会被 400 拒绝,
|
||
// 因此更低但非零的预算会被抬高并上报,而不是照发。
|
||
anthropicMinThinkingBudget = 1024
|
||
// anthropicDefaultThinkingBudget 在开启思考但未给
|
||
// 显式预算时使用。
|
||
anthropicDefaultThinkingBudget = 4096
|
||
// anthropicThinkingBodyReserve 是在思考预算之上留给正文的空间:
|
||
// max_tokens 由思考与可见答案共享,所以
|
||
// 预算绝不允许把它全部吃光(design/04 §3.2 红线:
|
||
// "预算与正文不得互相吃光").
|
||
anthropicThinkingBodyReserve = 2048
|
||
)
|
||
|
||
// deepSeekAnthropicEndpoint 报告一次 Anthropic 协议请求是否
|
||
// 真的由 DeepSeek 的 /anthropic 端点服务。先检查 base URL,
|
||
// 因为是端点——而不是模型名——决定 budget_tokens 是否
|
||
// 被尊重,而 DeepSeek 端点常常配着一个非 DeepSeek 的
|
||
// 模型名(该服务会把未知名称静默映射到 deepseek-flash)。
|
||
func deepSeekAnthropicEndpoint(model, baseURL string) bool {
|
||
if strings.Contains(strings.ToLower(baseURL), "deepseek") {
|
||
return true
|
||
}
|
||
return reasoningVendorFor(model, baseURL) == vendorDeepSeek
|
||
}
|
||
|
||
// anthropicThinkingFor 解出扩展思考指令以及它所需的
|
||
// max_tokens。它返回(可能已被抬高的)max_tokens 与
|
||
// 线格式块;块为 nil 表示「不发送思考」(无指令,或
|
||
// 显式关闭)。由于 max_tokens 由思考与可见答案共享,
|
||
// 返回的预算总会为正文留下 anthropicThinkingBodyReserve,
|
||
// 而预算会吃光答案的请求则改为抬高 max_tokens——该
|
||
// 调整会被记录,因此绝不是一次静默改写。
|
||
//
|
||
// 这条规则只适用于 Anthropic 端点。DeepSeek 的 /anthropic 端点
|
||
// 会忽略 budget_tokens,所以它绝不能走这里(design/04 §3.2 事实 6)。
|
||
func anthropicThinkingFor(th *Thinking, maxTokens int, log *slog.Logger) (int, *anthropicThinking) {
|
||
if th == nil || (th.Enable != nil && !*th.Enable) {
|
||
return maxTokens, nil
|
||
}
|
||
budget := th.BudgetTokens
|
||
if budget == 0 {
|
||
budget = anthropicDefaultThinkingBudget
|
||
}
|
||
if budget < anthropicMinThinkingBudget {
|
||
log.Warn("thinking budget raised to the protocol minimum",
|
||
"provider", "anthropic", "field", "thinking.budget_tokens",
|
||
"budget_tokens", th.BudgetTokens, "sent", anthropicMinThinkingBudget)
|
||
budget = anthropicMinThinkingBudget
|
||
}
|
||
if need := budget + anthropicThinkingBodyReserve; maxTokens < need {
|
||
log.Warn("max_tokens raised to leave room for thinking plus the answer",
|
||
"provider", "anthropic", "budget_tokens", budget,
|
||
"max_tokens_requested", maxTokens, "max_tokens_sent", need,
|
||
"body_reserve", anthropicThinkingBodyReserve)
|
||
maxTokens = need
|
||
}
|
||
return maxTokens, &anthropicThinking{Type: anthropicThinkingTypeEnabled, BudgetTokens: budget}
|
||
}
|
||
|
||
// anthropicDeepSeekReasoning 解出 DeepSeek /anthropic 请求的
|
||
// 推理旋钮。与 Anthropic 端点的两点差异决定了它:该
|
||
// 端点忽略 budget_tokens,所以 max_tokens 保持不动(抬高它
|
||
// 会错误反映提供商实际采用的预算),而强度由
|
||
// output_config.effort 控制,它没有 "none" 取值——在那里「关闭」
|
||
// 意图由 thinking 开关自身承载(design/04 §3.2 事实 2 与 6)。
|
||
func anthropicDeepSeekReasoning(log *slog.Logger, model string, th *Thinking, effort string) (*anthropicThinking, *anthropicOutputConfig) {
|
||
var block *anthropicThinking
|
||
if th != nil {
|
||
if th.BudgetTokens > 0 {
|
||
log.Warn("thinking.budget_tokens ignored by the DeepSeek /anthropic endpoint: strength is output_config.effort, so max_tokens was not raised for the budget",
|
||
"provider", "deepseek", "field", "thinking.budget_tokens",
|
||
"budget_tokens", th.BudgetTokens, "model", model)
|
||
}
|
||
switch {
|
||
case th.Enable != nil:
|
||
block = &anthropicThinking{Type: thinkingSwitchValue(*th.Enable), BudgetTokens: th.BudgetTokens}
|
||
case th.BudgetTokens > 0:
|
||
block = &anthropicThinking{Type: thinkingTypeEnabled, BudgetTokens: th.BudgetTokens}
|
||
}
|
||
}
|
||
effort = strings.TrimSpace(effort)
|
||
if effort == "" {
|
||
return block, nil
|
||
}
|
||
if effort == effortNone {
|
||
if block == nil || block.Type == thinkingTypeEnabled {
|
||
log.Warn(`reasoning_effort none mapped to thinking.type=disabled: the DeepSeek /anthropic endpoint has no output_config.effort "none"`,
|
||
"provider", "deepseek", "field", "reasoning_effort", "value", effort, "model", model)
|
||
}
|
||
return &anthropicThinking{Type: thinkingTypeDisabled}, nil
|
||
}
|
||
if block != nil && block.Type == thinkingTypeDisabled {
|
||
log.Warn("output_config.effort dropped: thinking is explicitly disabled",
|
||
"provider", "deepseek", "field", "reasoning_effort", "value", effort, "model", model)
|
||
return block, nil
|
||
}
|
||
tier, ok := normalizeDeepSeekEffort(effort)
|
||
if !ok {
|
||
log.Warn("reasoning_effort dropped: unrecognized value",
|
||
"provider", "deepseek", "field", "reasoning_effort", "value", effort, "model", model)
|
||
return block, nil
|
||
}
|
||
if tier != effort {
|
||
log.Warn("reasoning_effort alias normalized to the tier DeepSeek applies",
|
||
"provider", "deepseek", "field", "reasoning_effort",
|
||
"value", effort, "sent", tier, "model", model)
|
||
}
|
||
return block, &anthropicOutputConfig{Effort: tier}
|
||
}
|
||
|
||
// anthropicThinkingOn 报告一个已解出的 DeepSeek /anthropic 请求的
|
||
// 实际思考状态:省略开关会保留提供商默认值,而
|
||
// DeepSeek 的默认是开启。
|
||
func anthropicThinkingOn(th *anthropicThinking) bool {
|
||
return th == nil || th.Type != thinkingTypeDisabled
|
||
}
|