依据 DevDocs/design/04 §3.2「官方核对后的修正」9 条 + 本地 DevDocs/reference/deepseek/facts-for-cyrene.md(官方原句/URL)。 - 🔴 reasoning_effort 白名单改为官方集合 [none, low, high, max] + 别名映射(minimal→low, medium→high, xhigh→high, ultra→max);**删除就近降级**,非法值一律丢弃+WARN。此前 none(关思考) 被降级成 low(开思考) 属语义反转 - 🔴 DeepSeek thinking 开关真发顶层 {"thinking":{"type":"enabled|disabled"}}(官方默认 enabled,不传≠关);此前只打 WARN 导致 mode:off 静默无效 - DeepSeek 的 /anthropic 端点忽略 budget_tokens → 不再对其抬 max_tokens(会误导预算/日志),改为按官方用 output_config.effort - 注释纠正:penalty 属接口级 deprecated(非仅思考模式);top_p 仅思考模式生效且下限 0.95(不加该字段) - tool_choice 雷登记:思考模式下 required/指定 tool 会 400(思考默认开) - 旧模型名清理(deepseek-chat/reasoner 已于 2026-07-24 停用) - 请求体级测试 + 变异验证(把 none 改回 low / 把 thinking 改回只 WARN → 对应断言 FAIL)
127 lines
5.7 KiB
Go
127 lines
5.7 KiB
Go
package model
|
|
|
|
import (
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
// Anthropic extended-thinking protocol bounds (design/04 §3.2).
|
|
const (
|
|
// anthropicMinThinkingBudget is the smallest budget_tokens the Messages API
|
|
// accepts for thinking.type=enabled; anything below it is rejected with a
|
|
// 400, so a lower non-zero budget is raised and reported rather than sent.
|
|
anthropicMinThinkingBudget = 1024
|
|
// anthropicDefaultThinkingBudget is used when thinking is switched on without
|
|
// an explicit budget.
|
|
anthropicDefaultThinkingBudget = 4096
|
|
// anthropicThinkingBodyReserve is the body room kept on top of the thinking
|
|
// budget: max_tokens is shared by thinking and the visible answer, so the
|
|
// budget must never be allowed to consume it all (design/04 §3.2 red line:
|
|
// "预算与正文不得互相吃光").
|
|
anthropicThinkingBodyReserve = 2048
|
|
)
|
|
|
|
// deepSeekAnthropicEndpoint reports whether an Anthropic-protocol request is
|
|
// really served by DeepSeek's /anthropic endpoint. The base URL is checked first
|
|
// because the endpoint — not the model name — decides whether budget_tokens is
|
|
// honoured, and a DeepSeek endpoint is commonly configured with a non-DeepSeek
|
|
// model name (the service silently maps unknown names onto deepseek-flash).
|
|
func deepSeekAnthropicEndpoint(model, baseURL string) bool {
|
|
if strings.Contains(strings.ToLower(baseURL), "deepseek") {
|
|
return true
|
|
}
|
|
return reasoningVendorFor(model, baseURL) == vendorDeepSeek
|
|
}
|
|
|
|
// anthropicThinkingFor resolves the extended-thinking directive together with the
|
|
// max_tokens it requires. It returns the (possibly raised) max_tokens and the
|
|
// wire block; a nil block means "do not send thinking" (no directive, or an
|
|
// explicit off). Because max_tokens is shared by thinking and the visible answer,
|
|
// the returned budget always leaves anthropicThinkingBodyReserve for the body,
|
|
// and a request whose budget would eat the answer raises max_tokens instead — the
|
|
// adjustment is logged so it is never a silent rewrite.
|
|
//
|
|
// This is the Anthropic endpoint's rule only. A DeepSeek /anthropic endpoint
|
|
// ignores budget_tokens, so it must not go through here (design/04 §3.2 fact 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 resolves the reasoning knobs for a DeepSeek
|
|
// /anthropic request. Two differences from the Anthropic endpoint drive it: that
|
|
// endpoint ignores budget_tokens, so max_tokens is left untouched (raising it
|
|
// would misreport the budget the provider actually applies), and strength is
|
|
// controlled by output_config.effort, which has no "none" value — there the "off"
|
|
// intent is carried by the thinking switch itself (design/04 §3.2 facts 2 and 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 reports the effective thinking state of a resolved DeepSeek
|
|
// /anthropic request: an omitted switch leaves the provider default in place, and
|
|
// DeepSeek's default is enabled.
|
|
func anthropicThinkingOn(th *anthropicThinking) bool {
|
|
return th == nil || th.Type != thinkingTypeDisabled
|
|
}
|