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 }