返回 DeepSeek-Reasonix
reasoning_capability.go
根目录 / internal / provider / openai / reasoning_capability.go
1 package openai
2
3 import (
4 "reasonix/internal/provider"
5 )
6
7 // ReasoningForConfig is pure: capability discovery never reads credentials or
8 // performs I/O. It shares the adapter's endpoint and protocol predicates.
9 func ReasoningForConfig(cfg provider.Config) provider.ReasoningCapability {
10 cfg = provider.ApplyOpenCodeGoContract("openai", cfg)
11 protocol, _ := cfg.Extra["reasoning_protocol"].(string)
12 protocol = normalizeReasoningProtocol(protocol)
13 if protocol == "none" {
14 return provider.ReasoningOptions("")
15 }
16 var cap provider.ReasoningCapability
17 switch {
18 case usesKimiK3Contract(protocol, cfg.BaseURL, cfg.Model):
19 return provider.ReasoningOptions("max", "low", "high", "max")
20 case protocol == "glm" || (protocol == "" && (IsZhipu(cfg.BaseURL) || IsLongCat(cfg.BaseURL))):
21 cap = provider.ReasoningOptions("enabled", "enabled", "disabled")
22 case protocol == "" && IsMiniMax(cfg.BaseURL):
23 cap = provider.ReasoningOptions("adaptive", "adaptive", "disabled")
24 case protocol == "deepseek" || (protocol == "" && IsDeepSeek(cfg.BaseURL)):
25 cap = provider.ReasoningOptions("high", "disabled", "high", "max")
26 if cfg.Model == "deepseek-v4-flash" || cfg.Model == "deepseek-v4-pro" || IsOfficialDeepSeekVisionModel(cfg.Model) {
27 cap = provider.ReasoningOptions("high", "disabled", "low", "high", "max")
28 }
29 case protocol == "" && IsOllamaCloud(cfg.BaseURL):
30 cap = provider.ReasoningOptions("", "none", "low", "medium", "high", "max")
31 case protocol == "openai" || (protocol == "" && IsMiMo(cfg.BaseURL)):
32 cap = provider.ReasoningOptions("", "low", "medium", "high")
33 default:
34 cap = provider.UnknownReasoning()
35 }
36 cap = provider.DeclaredReasoning(cfg, cap)
37 if protocol == "glm" || (protocol == "" && (IsZhipu(cfg.BaseURL) || IsLongCat(cfg.BaseURL))) {
38 cap = provider.RestrictReasoning(cap, "enabled", "disabled")
39 }
40 if protocol == "" && IsMiniMax(cfg.BaseURL) {
41 cap = provider.RestrictReasoning(cap, "adaptive", "disabled")
42 }
43 if configuredThinkingType(cfg) == "disabled" {
44 return provider.ReasoningOptions("disabled", "disabled")
45 }
46 return cap
47 }
48 func (c *client) ReasoningCapability() provider.ReasoningCapability { return c.reasoning.Clone() }
49
50 func configuredEffort(cfg provider.Config) (string, error) {
51 effort, _ := cfg.Extra["effort"].(string)
52 protocol, _ := cfg.Extra["reasoning_protocol"].(string)
53 cap := ReasoningForConfig(cfg)
54 if effort == "auto" || effort == "off" || protocol == "none" || configuredThinkingType(cfg) == "disabled" {
55 return effort, nil
56 }
57 return effort, cap.Validate(cfg.Model, effort)
58 }
59
60 // reasoningState is the immutable reasoning contract resolved at construction.
61 type reasoningState struct {
62 ollamaCloud bool
63 thinkingLocked bool
64 reasoning provider.ReasoningCapability
65 }
66
67 func (c *client) applyReasoning(out *chatRequest, req provider.Request) {
68 maxOutputTokens := out.MaxTokens
69 switch {
70 case c.kimiK3:
71 // K3 fixes its sampling values and recommends omitting them. It also
72 // names the output budget max_completion_tokens rather than max_tokens.
73 out.Temperature = nil
74 out.MaxTokens = 0
75 out.MaxCompletionTokens = maxOutputTokens
76 out.ExtraBody = omitExtraBodyFields(out.ExtraBody,
77 "temperature", "top_p", "n", "presence_penalty", "frequency_penalty", "max_completion_tokens")
78 case IsOpenAI(c.baseURL):
79 // OpenAI's current Chat Completions contract replaces max_tokens with
80 // max_completion_tokens, which includes visible and reasoning tokens and
81 // is required by o-series models. Compatible gateways retain max_tokens.
82 out.MaxTokens = 0
83 out.MaxCompletionTokens = maxOutputTokens
84 case c.deepseek:
85 // DeepSeek's CoT is controlled by `thinking` plus `reasoning_effort` for
86 // depth. Thinking is on by default but can be turned off for one
87 // stateless request through EffortOverride=disabled.
88 out.Thinking = &thinkingMode{Type: c.deepSeekRequestThinking(req)}
89 if out.Thinking.Type == "disabled" {
90 out.ReasoningEffort = ""
91 }
92 case c.minimax:
93 // M3 uses a single `thinking.type` field with two valid values:
94 // "adaptive" (default, thinking on) and "disabled" (off). Reasoning
95 // depth is not a knob on M3, so reasoning_effort is omitted entirely.
96 t := c.requestEffort(req)
97 if t == "" {
98 t = "adaptive" // /effort auto == the M3 model default
99 }
100 out.Thinking = &thinkingMode{Type: t}
101 out.ReasoningEffort = ""
102 case c.zhipu:
103 // Zhipu GLM's binary thinking knob: "enabled" (default, thinking on) or
104 // "disabled". reasoning_effort is silently ignored by the endpoint, so we
105 // omit it and drive chain-of-thought purely through thinking.type.
106 t := c.requestEffort(req)
107 if t == "" {
108 t = "enabled" // auto == the GLM default (thinking on)
109 }
110 if c.thinkingType != "" && req.EffortOverride == "" {
111 t = c.thinkingType // explicit `thinking` config overrides the effort knob
112 }
113 out.Thinking = &thinkingMode{Type: t}
114 out.ReasoningEffort = ""
115 case c.longcat:
116 // LongCat's binary thinking knob: "enabled" (default, thinking on) or
117 // "disabled". The API documents reasoning_content in OpenAI responses but
118 // not reasoning_effort, so keep depth out of the request.
119 t := c.requestEffort(req)
120 if t == "" {
121 t = c.thinkingType
122 }
123 if t == "" {
124 t = "enabled"
125 }
126 out.Thinking = &thinkingMode{Type: t}
127 out.ReasoningEffort = ""
128 case c.ollamaCloud:
129 if out.ReasoningEffort == "none" {
130 out.ReasoningEffort = ""
131 }
132 case c.thinkingType != "":
133 // Generic OpenAI-compatible provider with an explicit `thinking` config
134 // field (e.g. opencode.ai) — emit thinking.type; reasoning_effort, if any,
135 // is left untouched for backends that also honour it.
136 out.Thinking = &thinkingMode{Type: c.thinkingType}
137 }
138 }
139
139 lines GO