| 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 |