Add native max thinking level
- commit
- 1fa7669275469900f85c336f7bb7cf4f62e333b5
- author
- patriceckhart · · 2026-07-10 05:32 UTC
- parent
- c6cf9e7
- signature
- unsigned
- refs
- tag: v0.2.72
22 files · +253 -61
M README.md +3 -3
| @@ -174,7 +174,7 @@ zot --help | |||
| 174 | 174 | | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. | | |
| 175 | 175 | | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). | | |
| 176 | 176 | | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). | | |
| 177 | - | | `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). | | |
| 177 | + | | `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set thinking level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. | | |
| 178 | 178 | | `-c`, `--continue` | Resume the latest session for this cwd. | | |
| 179 | 179 | | `-r`, `--resume` | Pick a session to resume. | | |
| 180 | 180 | | `--session <path>` | Resume a specific session file. | | |
| @@ -333,7 +333,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||
| 333 | 333 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | |
| 334 | 334 | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | |
| 335 | 335 | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | |
| 336 | - | - **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call. | |
| 336 | + | - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. | |
| 337 | 337 | - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md). | |
| 338 | 338 | - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running. | |
| 339 | 339 | ||
| @@ -530,7 +530,7 @@ Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta | |||
| 530 | 530 | ||
| 531 | 531 | > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro). | |
| 532 | 532 | ||
| 533 | - | Reasoning levels (`--reasoning off|minimum|low|medium|high|maximum`, also configurable in `/settings` as **thinking level**) map differently per generation. Budget-based providers use roughly 1k/2k/8k/16k/32k thinking tokens for minimum/low/medium/high/maximum, with provider/model caps applied (Gemini 2.5 Pro caps at 32k; Flash at 24k). Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any "medium" or higher request. Effort-based OpenAI-compatible chat providers map minimum to `low`, low/medium directly, and high/maximum to `high`; the Codex/Responses backend maps maximum to `xhigh` where supported. `off` sends no reasoning config. 2.0-family Gemini models have no thinking config at all. | |
| 533 | + | Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable in `/settings` as **thinking level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config. Gemini 2.0 models have no thinking config. | |
| 534 | 534 | ||
| 535 | 535 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | |
| 536 | 536 | ||
| @@ -174,7 +174,7 @@ zot --help | |||||
| 174 | | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. | | 174 | | `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. | | ||
| 175 | | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). | | 175 | | `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`). | | ||
| 176 | | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). | | 176 | | `--append-system-prompt <text>` | Append text to the system prompt (repeatable). | | ||
| 177 | - | | `--reasoning off\|minimum\|low\|medium\|high\|maximum` | Set thinking level on supported models (default: off). | | 177 | + | | `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set thinking level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. | |
| 178 | | `-c`, `--continue` | Resume the latest session for this cwd. | | 178 | | `-c`, `--continue` | Resume the latest session for this cwd. | | ||
| 179 | | `-r`, `--resume` | Pick a session to resume. | | 179 | | `-r`, `--resume` | Pick a session to resume. | | ||
| 180 | | `--session <path>` | Resume a specific session file. | | 180 | | `--session <path>` | Resume a specific session file. | | ||
| @@ -333,7 +333,7 @@ Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` o | |||||
| 333 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | 333 | - **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents. | ||
| 334 | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | 334 | - **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`. | ||
| 335 | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | 335 | - **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields). | ||
| 336 | - | - **thinking level** — choose reasoning for supported models: off (default; no reasoning), minimum (~1k tokens), low (~2k), medium (~8k), high (~16k), maximum (~32k). The change is persisted to `config.json` and applied to the running agent's next model call. | 336 | + | - **thinking level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. |
| 337 | - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md). | 337 | - **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md). | ||
| 338 | - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running. | 338 | - **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running. | ||
| 339 | 339 | ||||
| @@ -530,7 +530,7 @@ Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta | |||||
| 530 | 530 | ||||
| 531 | > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro). | 531 | > **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro). | ||
| 532 | 532 | ||||
| 533 | - | Reasoning levels (`--reasoning off|minimum|low|medium|high|maximum`, also configurable in `/settings` as **thinking level**) map differently per generation. Budget-based providers use roughly 1k/2k/8k/16k/32k thinking tokens for minimum/low/medium/high/maximum, with provider/model caps applied (Gemini 2.5 Pro caps at 32k; Flash at 24k). Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any "medium" or higher request. Effort-based OpenAI-compatible chat providers map minimum to `low`, low/medium directly, and high/maximum to `high`; the Codex/Responses backend maps maximum to `xhigh` where supported. `off` sends no reasoning config. 2.0-family Gemini models have no thinking config at all. | 533 | + | Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable in `/settings` as **thinking level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6 and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config. Gemini 2.0 models have no thinking config. |
| 534 | 534 | ||||
| 535 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | 535 | You can add additional Gemini model IDs to `models.json` under the `google` provider. | ||
| 536 | 536 | ||||
M docs/rpc.md +10 -0
| @@ -153,6 +153,16 @@ Switch model within the same provider. | |||
| 153 | 153 | ||
| 154 | 154 | Cross-provider swaps require relaunching `zot rpc` with the new `--provider`. | |
| 155 | 155 | ||
| 156 | + | ### `set_reasoning` | |
| 157 | + | ||
| 158 | + | Set the thinking level for subsequent prompts without restarting the process. Accepted values are `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, and `max`. | |
| 159 | + | ||
| 160 | + | ```json | |
| 161 | + | {"id":"8","type":"set_reasoning","reasoning":"max"} | |
| 162 | + | ``` | |
| 163 | + | ||
| 164 | + | The `max` tier is sent natively to supported models and clamped for other backends. | |
| 165 | + | ||
| 156 | 166 | ### `get_models` | |
| 157 | 167 | ||
| 158 | 168 | List models known for the current provider. | |
| @@ -153,6 +153,16 @@ Switch model within the same provider. | |||||
| 153 | 153 | ||||
| 154 | Cross-provider swaps require relaunching `zot rpc` with the new `--provider`. | 154 | Cross-provider swaps require relaunching `zot rpc` with the new `--provider`. | ||
| 155 | 155 | ||||
| 156 | + | ### `set_reasoning` | |||
| 157 | + | ||||
| 158 | + | Set the thinking level for subsequent prompts without restarting the process. Accepted values are `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, and `max`. | |||
| 159 | + | ||||
| 160 | + | ```json | |||
| 161 | + | {"id":"8","type":"set_reasoning","reasoning":"max"} | |||
| 162 | + | ``` | |||
| 163 | + | ||||
| 164 | + | The `max` tier is sent natively to supported models and clamped for other backends. | |||
| 165 | + | ||||
| 156 | ### `get_models` | 166 | ### `get_models` | ||
| 157 | 167 | ||||
| 158 | List models known for the current provider. | 168 | List models known for the current provider. | ||
M docs/themes.md +1 -0
| @@ -191,6 +191,7 @@ Most color fields are xterm-256 indexes (`0`–`255`). | |||
| 191 | 191 | - `error` — errors, refused calls, diff deletions. | |
| 192 | 192 | - `warning` — warnings and high context-usage state. | |
| 193 | 193 | - `spinner` — reserved spinner color slot. | |
| 194 | + | - `thinkingMax` or `thinking_max` — status color used when the opt-in `max` thinking level is active. Missing values inherit the built-in theme color. | |
| 194 | 195 | - `selection_bg` — highlighted row background. | |
| 195 | 196 | - `selection_fg` — highlighted row foreground. | |
| 196 | 197 | ||
| @@ -191,6 +191,7 @@ Most color fields are xterm-256 indexes (`0`–`255`). | |||||
| 191 | - `error` — errors, refused calls, diff deletions. | 191 | - `error` — errors, refused calls, diff deletions. | ||
| 192 | - `warning` — warnings and high context-usage state. | 192 | - `warning` — warnings and high context-usage state. | ||
| 193 | - `spinner` — reserved spinner color slot. | 193 | - `spinner` — reserved spinner color slot. | ||
| 194 | + | - `thinkingMax` or `thinking_max` — status color used when the opt-in `max` thinking level is active. Missing values inherit the built-in theme color. | |||
| 194 | - `selection_bg` — highlighted row background. | 195 | - `selection_bg` — highlighted row background. | ||
| 195 | - `selection_fg` — highlighted row foreground. | 196 | - `selection_fg` — highlighted row foreground. | ||
| 196 | 197 | ||||
M packages/agent/args.go +3 -3
| @@ -203,10 +203,10 @@ func ParseArgs(in []string) (Args, error) { | |||
| 203 | 203 | return a, err | |
| 204 | 204 | } | |
| 205 | 205 | switch strings.ToLower(v) { | |
| 206 | - | case "", "off", "minimum", "minimal", "low", "medium", "high", "maximum", "max": | |
| 206 | + | case "", "off", "minimum", "minimal", "low", "medium", "high", "xhigh", "maximum", "max": | |
| 207 | 207 | a.Reasoning = strings.ToLower(v) | |
| 208 | 208 | default: | |
| 209 | - | return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|maximum") | |
| 209 | + | return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|xhigh|max") | |
| 210 | 210 | } | |
| 211 | 211 | case "--temperature": | |
| 212 | 212 | v, err := want(&i, arg) | |
| @@ -379,7 +379,7 @@ func PrintHelp(version string) { | |||
| 379 | 379 | row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, | |
| 380 | 380 | row{"--base-url URL", "override provider api base url"}, | |
| 381 | 381 | row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"}, | |
| 382 | - | row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"}, | |
| 382 | + | row{"--reasoning off|minimum|low|medium|high|xhigh|max", "set thinking level on supported models"}, | |
| 383 | 383 | row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, | |
| 384 | 384 | ) | |
| 385 | 385 | section("prompt and session flags", | |
| @@ -203,10 +203,10 @@ func ParseArgs(in []string) (Args, error) { | |||||
| 203 | return a, err | 203 | return a, err | ||
| 204 | } | 204 | } | ||
| 205 | switch strings.ToLower(v) { | 205 | switch strings.ToLower(v) { | ||
| 206 | - | case "", "off", "minimum", "minimal", "low", "medium", "high", "maximum", "max": | 206 | + | case "", "off", "minimum", "minimal", "low", "medium", "high", "xhigh", "maximum", "max": |
| 207 | a.Reasoning = strings.ToLower(v) | 207 | a.Reasoning = strings.ToLower(v) | ||
| 208 | default: | 208 | default: | ||
| 209 | - | return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|maximum") | 209 | + | return a, fmt.Errorf("--reasoning must be off|minimum|low|medium|high|xhigh|max") |
| 210 | } | 210 | } | ||
| 211 | case "--temperature": | 211 | case "--temperature": | ||
| 212 | v, err := want(&i, arg) | 212 | v, err := want(&i, arg) | ||
| @@ -379,7 +379,7 @@ func PrintHelp(version string) { | |||||
| 379 | row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, | 379 | row{"--api-key KEY", "api key for this run (env / auth.json fallback)"}, | ||
| 380 | row{"--base-url URL", "override provider api base url"}, | 380 | row{"--base-url URL", "override provider api base url"}, | ||
| 381 | row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"}, | 381 | row{"--insecure", "skip TLS certificate verification (for self-signed-cert endpoints)"}, | ||
| 382 | - | row{"--reasoning off|minimum|low|medium|high|maximum", "set thinking level on supported models"}, | 382 | + | row{"--reasoning off|minimum|low|medium|high|xhigh|max", "set thinking level on supported models"}, |
| 383 | row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, | 383 | row{"--temperature N", "sampling temperature, 0 to 2 (omit for provider default)"}, | ||
| 384 | ) | 384 | ) | ||
| 385 | section("prompt and session flags", | 385 | section("prompt and session flags", | ||
M packages/agent/modes/interactive.go +2 -1
| @@ -2927,7 +2927,8 @@ func (i *Interactive) openSettingsDialog() { | |||
| 2927 | 2927 | {value: "low", label: "low", desc: "light (~2k tokens)"}, | |
| 2928 | 2928 | {value: "medium", label: "medium", desc: "moderate (~8k tokens)"}, | |
| 2929 | 2929 | {value: "high", label: "high", desc: "deep (~16k tokens)"}, | |
| 2930 | - | {value: "maximum", label: "maximum", desc: "highest (~32k tokens)"}, | |
| 2930 | + | {value: "xhigh", label: "xhigh", desc: "extra-high effort"}, | |
| 2931 | + | {value: "max", label: "max", desc: "unconstrained effort on supported models"}, | |
| 2931 | 2932 | } | |
| 2932 | 2933 | reasoning := provider.NormalizeReasoning(i.cfg.Reasoning) | |
| 2933 | 2934 | reasoningChoice := 0 | |
| @@ -2927,7 +2927,8 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 2927 | {value: "low", label: "low", desc: "light (~2k tokens)"}, | 2927 | {value: "low", label: "low", desc: "light (~2k tokens)"}, | ||
| 2928 | {value: "medium", label: "medium", desc: "moderate (~8k tokens)"}, | 2928 | {value: "medium", label: "medium", desc: "moderate (~8k tokens)"}, | ||
| 2929 | {value: "high", label: "high", desc: "deep (~16k tokens)"}, | 2929 | {value: "high", label: "high", desc: "deep (~16k tokens)"}, | ||
| 2930 | - | {value: "maximum", label: "maximum", desc: "highest (~32k tokens)"}, | 2930 | + | {value: "xhigh", label: "xhigh", desc: "extra-high effort"}, |
| 2931 | + | {value: "max", label: "max", desc: "unconstrained effort on supported models"}, | |||
| 2931 | } | 2932 | } | ||
| 2932 | reasoning := provider.NormalizeReasoning(i.cfg.Reasoning) | 2933 | reasoning := provider.NormalizeReasoning(i.cfg.Reasoning) | ||
| 2933 | reasoningChoice := 0 | 2934 | reasoningChoice := 0 | ||
M packages/agent/rpc.go +20 -0
| @@ -30,6 +30,7 @@ import ( | |||
| 30 | 30 | // {"id":"3","type":"compact"} | |
| 31 | 31 | // {"id":"4","type":"get_state"} | |
| 32 | 32 | // {"id":"5","type":"set_model","model":"claude-opus-4-5"} | |
| 33 | + | // {"id":"6","type":"set_reasoning","reasoning":"max"} | |
| 33 | 34 | // {"id":"6","type":"get_messages"} | |
| 34 | 35 | // {"id":"7","type":"clear"} | |
| 35 | 36 | // {"id":"8","type":"get_models"} | |
| @@ -301,6 +302,24 @@ func (s *rpcServer) dispatch(cmd, id string, raw []byte) { | |||
| 301 | 302 | s.model = req.Model | |
| 302 | 303 | s.writeResponse(id, cmd, map[string]any{"model": req.Model}) | |
| 303 | 304 | ||
| 305 | + | case "set_reasoning": | |
| 306 | + | var req struct { | |
| 307 | + | Reasoning string `json:"reasoning"` | |
| 308 | + | } | |
| 309 | + | if err := json.Unmarshal(raw, &req); err != nil { | |
| 310 | + | s.writeError(id, cmd, err.Error()) | |
| 311 | + | return | |
| 312 | + | } | |
| 313 | + | reasoning := provider.NormalizeReasoning(req.Reasoning) | |
| 314 | + | switch reasoning { | |
| 315 | + | case "", "minimum", "low", "medium", "high", "xhigh", "max": | |
| 316 | + | default: | |
| 317 | + | s.writeError(id, cmd, "reasoning must be off|minimum|low|medium|high|xhigh|max") | |
| 318 | + | return | |
| 319 | + | } | |
| 320 | + | s.agent.Reasoning = reasoning | |
| 321 | + | s.writeResponse(id, cmd, map[string]any{"reasoning": reasoning}) | |
| 322 | + | ||
| 304 | 323 | case "get_models": | |
| 305 | 324 | out := []map[string]any{} | |
| 306 | 325 | for _, m := range provider.ModelsForProvider(s.provider) { | |
| @@ -388,6 +407,7 @@ func (s *rpcServer) snapshotState() map[string]any { | |||
| 388 | 407 | return map[string]any{ | |
| 389 | 408 | "provider": s.provider, | |
| 390 | 409 | "model": s.model, | |
| 410 | + | "reasoning": s.agent.Reasoning, | |
| 391 | 411 | "cwd": s.args.CWD, | |
| 392 | 412 | "message_count": len(s.agent.Messages()), | |
| 393 | 413 | "busy": s.activeCancel != nil, | |
| @@ -30,6 +30,7 @@ import ( | |||||
| 30 | // {"id":"3","type":"compact"} | 30 | // {"id":"3","type":"compact"} | ||
| 31 | // {"id":"4","type":"get_state"} | 31 | // {"id":"4","type":"get_state"} | ||
| 32 | // {"id":"5","type":"set_model","model":"claude-opus-4-5"} | 32 | // {"id":"5","type":"set_model","model":"claude-opus-4-5"} | ||
| 33 | + | // {"id":"6","type":"set_reasoning","reasoning":"max"} | |||
| 33 | // {"id":"6","type":"get_messages"} | 34 | // {"id":"6","type":"get_messages"} | ||
| 34 | // {"id":"7","type":"clear"} | 35 | // {"id":"7","type":"clear"} | ||
| 35 | // {"id":"8","type":"get_models"} | 36 | // {"id":"8","type":"get_models"} | ||
| @@ -301,6 +302,24 @@ func (s *rpcServer) dispatch(cmd, id string, raw []byte) { | |||||
| 301 | s.model = req.Model | 302 | s.model = req.Model | ||
| 302 | s.writeResponse(id, cmd, map[string]any{"model": req.Model}) | 303 | s.writeResponse(id, cmd, map[string]any{"model": req.Model}) | ||
| 303 | 304 | ||||
| 305 | + | case "set_reasoning": | |||
| 306 | + | var req struct { | |||
| 307 | + | Reasoning string `json:"reasoning"` | |||
| 308 | + | } | |||
| 309 | + | if err := json.Unmarshal(raw, &req); err != nil { | |||
| 310 | + | s.writeError(id, cmd, err.Error()) | |||
| 311 | + | return | |||
| 312 | + | } | |||
| 313 | + | reasoning := provider.NormalizeReasoning(req.Reasoning) | |||
| 314 | + | switch reasoning { | |||
| 315 | + | case "", "minimum", "low", "medium", "high", "xhigh", "max": | |||
| 316 | + | default: | |||
| 317 | + | s.writeError(id, cmd, "reasoning must be off|minimum|low|medium|high|xhigh|max") | |||
| 318 | + | return | |||
| 319 | + | } | |||
| 320 | + | s.agent.Reasoning = reasoning | |||
| 321 | + | s.writeResponse(id, cmd, map[string]any{"reasoning": reasoning}) | |||
| 322 | + | ||||
| 304 | case "get_models": | 323 | case "get_models": | ||
| 305 | out := []map[string]any{} | 324 | out := []map[string]any{} | ||
| 306 | for _, m := range provider.ModelsForProvider(s.provider) { | 325 | for _, m := range provider.ModelsForProvider(s.provider) { | ||
| @@ -388,6 +407,7 @@ func (s *rpcServer) snapshotState() map[string]any { | |||||
| 388 | return map[string]any{ | 407 | return map[string]any{ | ||
| 389 | "provider": s.provider, | 408 | "provider": s.provider, | ||
| 390 | "model": s.model, | 409 | "model": s.model, | ||
| 410 | + | "reasoning": s.agent.Reasoning, | |||
| 391 | "cwd": s.args.CWD, | 411 | "cwd": s.args.CWD, | ||
| 392 | "message_count": len(s.agent.Messages()), | 412 | "message_count": len(s.agent.Messages()), | ||
| 393 | "busy": s.activeCancel != nil, | 413 | "busy": s.activeCancel != nil, | ||
A packages/agent/rpc_reasoning_test.go +35 -0
| @@ -0,0 +1,35 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/patriceckhart/zot/packages/core" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestRPCSetReasoningMax(t *testing.T) { | |
| 12 | + | var out bytes.Buffer | |
| 13 | + | s := &rpcServer{agent: &core.Agent{}, out: &out} | |
| 14 | + | s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"max"}`)) | |
| 15 | + | ||
| 16 | + | if s.agent.Reasoning != "max" { | |
| 17 | + | t.Fatalf("reasoning = %q, want max", s.agent.Reasoning) | |
| 18 | + | } | |
| 19 | + | if !strings.Contains(out.String(), `"reasoning":"max"`) { | |
| 20 | + | t.Fatalf("response = %q", out.String()) | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func TestRPCSetReasoningRejectsUnknownLevel(t *testing.T) { | |
| 25 | + | var out bytes.Buffer | |
| 26 | + | s := &rpcServer{agent: &core.Agent{}, out: &out} | |
| 27 | + | s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"extreme"}`)) | |
| 28 | + | ||
| 29 | + | if s.agent.Reasoning != "" { | |
| 30 | + | t.Fatalf("reasoning = %q, want unchanged", s.agent.Reasoning) | |
| 31 | + | } | |
| 32 | + | if !strings.Contains(out.String(), `"success":false`) { | |
| 33 | + | t.Fatalf("response = %q", out.String()) | |
| 34 | + | } | |
| 35 | + | } | |
| @@ -0,0 +1,35 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/patriceckhart/zot/packages/core" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestRPCSetReasoningMax(t *testing.T) { | |||
| 12 | + | var out bytes.Buffer | |||
| 13 | + | s := &rpcServer{agent: &core.Agent{}, out: &out} | |||
| 14 | + | s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"max"}`)) | |||
| 15 | + | ||||
| 16 | + | if s.agent.Reasoning != "max" { | |||
| 17 | + | t.Fatalf("reasoning = %q, want max", s.agent.Reasoning) | |||
| 18 | + | } | |||
| 19 | + | if !strings.Contains(out.String(), `"reasoning":"max"`) { | |||
| 20 | + | t.Fatalf("response = %q", out.String()) | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func TestRPCSetReasoningRejectsUnknownLevel(t *testing.T) { | |||
| 25 | + | var out bytes.Buffer | |||
| 26 | + | s := &rpcServer{agent: &core.Agent{}, out: &out} | |||
| 27 | + | s.dispatch("set_reasoning", "1", []byte(`{"reasoning":"extreme"}`)) | |||
| 28 | + | ||||
| 29 | + | if s.agent.Reasoning != "" { | |||
| 30 | + | t.Fatalf("reasoning = %q, want unchanged", s.agent.Reasoning) | |||
| 31 | + | } | |||
| 32 | + | if !strings.Contains(out.String(), `"success":false`) { | |||
| 33 | + | t.Fatalf("response = %q", out.String()) | |||
| 34 | + | } | |||
| 35 | + | } | |||
A packages/agent/sdk/reasoning_test.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | package sdk | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/patriceckhart/zot/packages/core" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestRuntimeSetReasoningMax(t *testing.T) { | |
| 10 | + | r := &Runtime{agent: &core.Agent{}} | |
| 11 | + | if err := r.SetReasoning("max"); err != nil { | |
| 12 | + | t.Fatal(err) | |
| 13 | + | } | |
| 14 | + | if r.agent.Reasoning != "max" { | |
| 15 | + | t.Fatalf("reasoning = %q, want max", r.agent.Reasoning) | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | func TestRuntimeSetReasoningRejectsUnknownLevel(t *testing.T) { | |
| 20 | + | r := &Runtime{agent: &core.Agent{}} | |
| 21 | + | if err := r.SetReasoning("extreme"); err == nil { | |
| 22 | + | t.Fatal("expected invalid reasoning error") | |
| 23 | + | } | |
| 24 | + | } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | package sdk | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/patriceckhart/zot/packages/core" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestRuntimeSetReasoningMax(t *testing.T) { | |||
| 10 | + | r := &Runtime{agent: &core.Agent{}} | |||
| 11 | + | if err := r.SetReasoning("max"); err != nil { | |||
| 12 | + | t.Fatal(err) | |||
| 13 | + | } | |||
| 14 | + | if r.agent.Reasoning != "max" { | |||
| 15 | + | t.Fatalf("reasoning = %q, want max", r.agent.Reasoning) | |||
| 16 | + | } | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func TestRuntimeSetReasoningRejectsUnknownLevel(t *testing.T) { | |||
| 20 | + | r := &Runtime{agent: &core.Agent{}} | |||
| 21 | + | if err := r.SetReasoning("extreme"); err == nil { | |||
| 22 | + | t.Fatal("expected invalid reasoning error") | |||
| 23 | + | } | |||
| 24 | + | } | |||
M packages/agent/sdk/sdk.go +19 -1
| @@ -58,7 +58,7 @@ type Config struct { | |||
| 58 | 58 | AppendSystemPrompt []string | |
| 59 | 59 | ||
| 60 | 60 | // Reasoning sets the reasoning effort for models that support it | |
| 61 | - | // ("low", "medium", "high"). Empty = no reasoning. | |
| 61 | + | // ("minimum", "low", "medium", "high", "xhigh", "max"). Empty disables it. | |
| 62 | 62 | Reasoning string | |
| 63 | 63 | ||
| 64 | 64 | // Temperature sets the sampling temperature. Nil = provider default. | |
| @@ -296,6 +296,23 @@ func (r *Runtime) SetModel(model string) error { | |||
| 296 | 296 | return nil | |
| 297 | 297 | } | |
| 298 | 298 | ||
| 299 | + | // SetReasoning changes the thinking level used by subsequent prompts. | |
| 300 | + | func (r *Runtime) SetReasoning(level string) error { | |
| 301 | + | r.mu.Lock() | |
| 302 | + | defer r.mu.Unlock() | |
| 303 | + | if r.agent == nil { | |
| 304 | + | return fmt.Errorf("sdk: no agent") | |
| 305 | + | } | |
| 306 | + | reasoning := provider.NormalizeReasoning(level) | |
| 307 | + | switch reasoning { | |
| 308 | + | case "", "minimum", "low", "medium", "high", "xhigh", "max": | |
| 309 | + | r.agent.Reasoning = reasoning | |
| 310 | + | return nil | |
| 311 | + | default: | |
| 312 | + | return fmt.Errorf("sdk: reasoning must be off|minimum|low|medium|high|xhigh|max") | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | ||
| 299 | 316 | // State returns a snapshot of the runtime's current state. Useful for | |
| 300 | 317 | // driving UIs. | |
| 301 | 318 | func (r *Runtime) State() State { | |
| @@ -304,6 +321,7 @@ func (r *Runtime) State() State { | |||
| 304 | 321 | return State{ | |
| 305 | 322 | Provider: r.provider, | |
| 306 | 323 | Model: r.model, | |
| 324 | + | Reasoning: r.agent.Reasoning, | |
| 307 | 325 | CWD: r.cwd, | |
| 308 | 326 | Busy: r.activeCancel != nil, | |
| 309 | 327 | MessageCount: len(r.Messages()), | |
| @@ -58,7 +58,7 @@ type Config struct { | |||||
| 58 | AppendSystemPrompt []string | 58 | AppendSystemPrompt []string | ||
| 59 | 59 | ||||
| 60 | // Reasoning sets the reasoning effort for models that support it | 60 | // Reasoning sets the reasoning effort for models that support it | ||
| 61 | - | // ("low", "medium", "high"). Empty = no reasoning. | 61 | + | // ("minimum", "low", "medium", "high", "xhigh", "max"). Empty disables it. |
| 62 | Reasoning string | 62 | Reasoning string | ||
| 63 | 63 | ||||
| 64 | // Temperature sets the sampling temperature. Nil = provider default. | 64 | // Temperature sets the sampling temperature. Nil = provider default. | ||
| @@ -296,6 +296,23 @@ func (r *Runtime) SetModel(model string) error { | |||||
| 296 | return nil | 296 | return nil | ||
| 297 | } | 297 | } | ||
| 298 | 298 | ||||
| 299 | + | // SetReasoning changes the thinking level used by subsequent prompts. | |||
| 300 | + | func (r *Runtime) SetReasoning(level string) error { | |||
| 301 | + | r.mu.Lock() | |||
| 302 | + | defer r.mu.Unlock() | |||
| 303 | + | if r.agent == nil { | |||
| 304 | + | return fmt.Errorf("sdk: no agent") | |||
| 305 | + | } | |||
| 306 | + | reasoning := provider.NormalizeReasoning(level) | |||
| 307 | + | switch reasoning { | |||
| 308 | + | case "", "minimum", "low", "medium", "high", "xhigh", "max": | |||
| 309 | + | r.agent.Reasoning = reasoning | |||
| 310 | + | return nil | |||
| 311 | + | default: | |||
| 312 | + | return fmt.Errorf("sdk: reasoning must be off|minimum|low|medium|high|xhigh|max") | |||
| 313 | + | } | |||
| 314 | + | } | |||
| 315 | + | ||||
| 299 | // State returns a snapshot of the runtime's current state. Useful for | 316 | // State returns a snapshot of the runtime's current state. Useful for | ||
| 300 | // driving UIs. | 317 | // driving UIs. | ||
| 301 | func (r *Runtime) State() State { | 318 | func (r *Runtime) State() State { | ||
| @@ -304,6 +321,7 @@ func (r *Runtime) State() State { | |||||
| 304 | return State{ | 321 | return State{ | ||
| 305 | Provider: r.provider, | 322 | Provider: r.provider, | ||
| 306 | Model: r.model, | 323 | Model: r.model, | ||
| 324 | + | Reasoning: r.agent.Reasoning, | |||
| 307 | CWD: r.cwd, | 325 | CWD: r.cwd, | ||
| 308 | Busy: r.activeCancel != nil, | 326 | Busy: r.activeCancel != nil, | ||
| 309 | MessageCount: len(r.Messages()), | 327 | MessageCount: len(r.Messages()), | ||
M packages/agent/sdk/types.go +1 -0
| @@ -94,6 +94,7 @@ type Usage struct { | |||
| 94 | 94 | type State struct { | |
| 95 | 95 | Provider string `json:"provider"` | |
| 96 | 96 | Model string `json:"model"` | |
| 97 | + | Reasoning string `json:"reasoning"` | |
| 97 | 98 | CWD string `json:"cwd"` | |
| 98 | 99 | Busy bool `json:"busy"` | |
| 99 | 100 | MessageCount int `json:"message_count"` | |
| @@ -94,6 +94,7 @@ type Usage struct { | |||||
| 94 | type State struct { | 94 | type State struct { | ||
| 95 | Provider string `json:"provider"` | 95 | Provider string `json:"provider"` | ||
| 96 | Model string `json:"model"` | 96 | Model string `json:"model"` | ||
| 97 | + | Reasoning string `json:"reasoning"` | |||
| 97 | CWD string `json:"cwd"` | 98 | CWD string `json:"cwd"` | ||
| 98 | Busy bool `json:"busy"` | 99 | Busy bool `json:"busy"` | ||
| 99 | MessageCount int `json:"message_count"` | 100 | MessageCount int `json:"message_count"` | ||
M packages/provider/gemini.go +1 -1
| @@ -413,7 +413,7 @@ func geminiThinkingConfig(modelID, level string) *gemThinkingConfig { | |||
| 413 | 413 | } else { | |
| 414 | 414 | lvl = "MEDIUM" | |
| 415 | 415 | } | |
| 416 | - | case "high", "maximum": | |
| 416 | + | case "high", "xhigh", "max": | |
| 417 | 417 | lvl = "HIGH" | |
| 418 | 418 | default: | |
| 419 | 419 | return nil | |
| @@ -413,7 +413,7 @@ func geminiThinkingConfig(modelID, level string) *gemThinkingConfig { | |||||
| 413 | } else { | 413 | } else { | ||
| 414 | lvl = "MEDIUM" | 414 | lvl = "MEDIUM" | ||
| 415 | } | 415 | } | ||
| 416 | - | case "high", "maximum": | 416 | + | case "high", "xhigh", "max": |
| 417 | lvl = "HIGH" | 417 | lvl = "HIGH" | ||
| 418 | default: | 418 | default: | ||
| 419 | return nil | 419 | return nil | ||
M packages/provider/openai_codex.go +1 -1
| @@ -174,7 +174,7 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||
| 174 | 174 | Include: []string{"reasoning.encrypted_content"}, | |
| 175 | 175 | } | |
| 176 | 176 | if m.Reasoning { | |
| 177 | - | if effort := OpenAICodexReasoningEffort(req.Reasoning); effort != "" { | |
| 177 | + | if effort := OpenAICodexReasoningEffort(req.Reasoning, req.Model); effort != "" { | |
| 178 | 178 | body.Reasoning = &codexReasoningConfig{Effort: effort} | |
| 179 | 179 | } | |
| 180 | 180 | } | |
| @@ -174,7 +174,7 @@ func (c *codexClient) buildRequest(req Request) (*codexRequest, error) { | |||||
| 174 | Include: []string{"reasoning.encrypted_content"}, | 174 | Include: []string{"reasoning.encrypted_content"}, | ||
| 175 | } | 175 | } | ||
| 176 | if m.Reasoning { | 176 | if m.Reasoning { | ||
| 177 | - | if effort := OpenAICodexReasoningEffort(req.Reasoning); effort != "" { | 177 | + | if effort := OpenAICodexReasoningEffort(req.Reasoning, req.Model); effort != "" { |
| 178 | body.Reasoning = &codexReasoningConfig{Effort: effort} | 178 | body.Reasoning = &codexReasoningConfig{Effort: effort} | ||
| 179 | } | 179 | } | ||
| 180 | } | 180 | } | ||
M packages/provider/openai_codex_test.go +11 -0
| @@ -114,6 +114,17 @@ func TestCodexPreviewModelUsesCodexCLIShape(t *testing.T) { | |||
| 114 | 114 | } | |
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | + | func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) { | |
| 118 | + | c := NewOpenAICodex("token", "acct", "").(*codexClient) | |
| 119 | + | wire, err := c.buildRequest(Request{Model: "gpt-5.6-sol", Reasoning: "max"}) | |
| 120 | + | if err != nil { | |
| 121 | + | t.Fatal(err) | |
| 122 | + | } | |
| 123 | + | if wire.Reasoning == nil || wire.Reasoning.Effort != "max" { | |
| 124 | + | t.Fatalf("reasoning = %+v", wire.Reasoning) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 117 | 128 | func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) { | |
| 118 | 129 | named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient) | |
| 119 | 130 | c := named.inner.(*codexClient) | |
| @@ -114,6 +114,17 @@ func TestCodexPreviewModelUsesCodexCLIShape(t *testing.T) { | |||||
| 114 | } | 114 | } | ||
| 115 | } | 115 | } | ||
| 116 | 116 | ||||
| 117 | + | func TestGPT56UsesNativeMaxReasoningEffort(t *testing.T) { | |||
| 118 | + | c := NewOpenAICodex("token", "acct", "").(*codexClient) | |||
| 119 | + | wire, err := c.buildRequest(Request{Model: "gpt-5.6-sol", Reasoning: "max"}) | |||
| 120 | + | if err != nil { | |||
| 121 | + | t.Fatal(err) | |||
| 122 | + | } | |||
| 123 | + | if wire.Reasoning == nil || wire.Reasoning.Effort != "max" { | |||
| 124 | + | t.Fatalf("reasoning = %+v", wire.Reasoning) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 117 | func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) { | 128 | func TestOpenAIGPT56DoesNotUseCodexCLIRouting(t *testing.T) { | ||
| 118 | named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient) | 129 | named := NewOpenAIResponsesNamed("token", "https://example.test/v1/responses", "openai").(*renamedClient) | ||
| 119 | c := named.inner.(*codexClient) | 130 | c := named.inner.(*codexClient) | ||
M packages/provider/provider.go +3 -4
| @@ -216,10 +216,9 @@ type Request struct { | |||
| 216 | 216 | Tools []Tool | |
| 217 | 217 | MaxTokens int | |
| 218 | 218 | Temperature *float32 | |
| 219 | - | // Reasoning is "", "minimum", "low", "medium", "high", or "maximum". | |
| 220 | - | // Empty disables reasoning. Budget-based providers map these to roughly | |
| 221 | - | // 1k/2k/8k/16k/32k thinking tokens; effort-based providers map them onto | |
| 222 | - | // their closest supported reasoning_effort values. | |
| 219 | + | // Reasoning is "", "minimum", "low", "medium", "high", "xhigh", or | |
| 220 | + | // "max". Empty disables reasoning. The max tier is sent natively only to | |
| 221 | + | // models that support it and clamped elsewhere. | |
| 223 | 222 | Reasoning string | |
| 224 | 223 | } | |
| 225 | 224 | ||
| @@ -216,10 +216,9 @@ type Request struct { | |||||
| 216 | Tools []Tool | 216 | Tools []Tool | ||
| 217 | MaxTokens int | 217 | MaxTokens int | ||
| 218 | Temperature *float32 | 218 | Temperature *float32 | ||
| 219 | - | // Reasoning is "", "minimum", "low", "medium", "high", or "maximum". | 219 | + | // Reasoning is "", "minimum", "low", "medium", "high", "xhigh", or |
| 220 | - | // Empty disables reasoning. Budget-based providers map these to roughly | 220 | + | // "max". Empty disables reasoning. The max tier is sent natively only to |
| 221 | - | // 1k/2k/8k/16k/32k thinking tokens; effort-based providers map them onto | 221 | + | // models that support it and clamped elsewhere. |
| 222 | - | // their closest supported reasoning_effort values. | |||
| 223 | Reasoning string | 222 | Reasoning string | ||
| 224 | } | 223 | } | ||
| 225 | 224 | ||||
M packages/provider/provider_test.go +13 -0
| @@ -118,6 +118,19 @@ func TestAnthropicAdaptiveThinking(t *testing.T) { | |||
| 118 | 118 | t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig) | |
| 119 | 119 | } | |
| 120 | 120 | ||
| 121 | + | // max is a separate native tier above xhigh on adaptive models. | |
| 122 | + | wire, err = c.buildRequest(Request{ | |
| 123 | + | Model: "claude-opus-4-8", | |
| 124 | + | Reasoning: "max", | |
| 125 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |
| 126 | + | }) | |
| 127 | + | if err != nil { | |
| 128 | + | t.Fatal(err) | |
| 129 | + | } | |
| 130 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != "max" { | |
| 131 | + | t.Fatalf("want effort=max, got %+v", wire.OutputConfig) | |
| 132 | + | } | |
| 133 | + | ||
| 121 | 134 | // Sonnet 5 -> adaptive thinking, effort set, no budget, no temperature. | |
| 122 | 135 | wire, err = c.buildRequest(Request{ | |
| 123 | 136 | Model: "claude-sonnet-5", | |
| @@ -118,6 +118,19 @@ func TestAnthropicAdaptiveThinking(t *testing.T) { | |||||
| 118 | t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig) | 118 | t.Fatalf("want effort=xhigh, got %+v", wire.OutputConfig) | ||
| 119 | } | 119 | } | ||
| 120 | 120 | ||||
| 121 | + | // max is a separate native tier above xhigh on adaptive models. | |||
| 122 | + | wire, err = c.buildRequest(Request{ | |||
| 123 | + | Model: "claude-opus-4-8", | |||
| 124 | + | Reasoning: "max", | |||
| 125 | + | Messages: []Message{{Role: RoleUser, Content: []Content{TextBlock{Text: "hi"}}}}, | |||
| 126 | + | }) | |||
| 127 | + | if err != nil { | |||
| 128 | + | t.Fatal(err) | |||
| 129 | + | } | |||
| 130 | + | if wire.OutputConfig == nil || wire.OutputConfig.Effort != "max" { | |||
| 131 | + | t.Fatalf("want effort=max, got %+v", wire.OutputConfig) | |||
| 132 | + | } | |||
| 133 | + | ||||
| 121 | // Sonnet 5 -> adaptive thinking, effort set, no budget, no temperature. | 134 | // Sonnet 5 -> adaptive thinking, effort set, no budget, no temperature. | ||
| 122 | wire, err = c.buildRequest(Request{ | 135 | wire, err = c.buildRequest(Request{ | ||
| 123 | Model: "claude-sonnet-5", | 136 | Model: "claude-sonnet-5", | ||
M packages/provider/reasoning.go +31 -28
| @@ -3,7 +3,8 @@ package provider | |||
| 3 | 3 | import "strings" | |
| 4 | 4 | ||
| 5 | 5 | // NormalizeReasoning canonicalizes zot's user-facing thinking levels. | |
| 6 | - | // Empty string means reasoning/thinking is disabled. | |
| 6 | + | // Empty string means reasoning/thinking is disabled. "maximum" remains | |
| 7 | + | // an alias for xhigh; "max" is the separate opt-in tier above it. | |
| 7 | 8 | func NormalizeReasoning(level string) string { | |
| 8 | 9 | switch strings.ToLower(strings.TrimSpace(level)) { | |
| 9 | 10 | case "", "off", "none", "no", "false", "disabled": | |
| @@ -16,8 +17,10 @@ func NormalizeReasoning(level string) string { | |||
| 16 | 17 | return "medium" | |
| 17 | 18 | case "hi", "high": | |
| 18 | 19 | return "high" | |
| 19 | - | case "max", "maximum": | |
| 20 | - | return "maximum" | |
| 20 | + | case "xhigh", "maximum": | |
| 21 | + | return "xhigh" | |
| 22 | + | case "max": | |
| 23 | + | return "max" | |
| 21 | 24 | default: | |
| 22 | 25 | return strings.ToLower(strings.TrimSpace(level)) | |
| 23 | 26 | } | |
| @@ -35,7 +38,7 @@ func ReasoningBudget(level string) int { | |||
| 35 | 38 | return 8192 | |
| 36 | 39 | case "high": | |
| 37 | 40 | return 16384 | |
| 38 | - | case "maximum": | |
| 41 | + | case "xhigh", "max": | |
| 39 | 42 | return 32768 | |
| 40 | 43 | default: | |
| 41 | 44 | return 0 | |
| @@ -43,10 +46,8 @@ func ReasoningBudget(level string) int { | |||
| 43 | 46 | } | |
| 44 | 47 | ||
| 45 | 48 | // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the | |
| 46 | - | // effort enum used by Anthropic's adaptive-thinking models (Opus 4.7+). | |
| 47 | - | // These models reject explicit thinking budgets; thinking depth is | |
| 48 | - | // controlled by output_config.effort instead. Returns "" when reasoning | |
| 49 | - | // is disabled. | |
| 49 | + | // effort enum used by adaptive-thinking models. These models reject explicit | |
| 50 | + | // thinking budgets; reasoning depth is controlled by output_config.effort. | |
| 50 | 51 | func AnthropicAdaptiveEffort(level string) string { | |
| 51 | 52 | switch NormalizeReasoning(level) { | |
| 52 | 53 | case "minimum", "low": | |
| @@ -55,38 +56,34 @@ func AnthropicAdaptiveEffort(level string) string { | |||
| 55 | 56 | return "medium" | |
| 56 | 57 | case "high": | |
| 57 | 58 | return "high" | |
| 58 | - | case "maximum": | |
| 59 | + | case "xhigh": | |
| 59 | 60 | return "xhigh" | |
| 61 | + | case "max": | |
| 62 | + | return "max" | |
| 60 | 63 | default: | |
| 61 | 64 | return "" | |
| 62 | 65 | } | |
| 63 | 66 | } | |
| 64 | 67 | ||
| 65 | - | // OpenAIReasoningEffort maps zot's six-level setting onto the effort enum | |
| 66 | - | // accepted by OpenAI-compatible chat-completions endpoints. | |
| 68 | + | // OpenAIReasoningEffort maps zot's thinking setting onto the effort enum | |
| 69 | + | // accepted by generic OpenAI-compatible chat-completions endpoints. | |
| 67 | 70 | func OpenAIReasoningEffort(level string) string { | |
| 68 | 71 | switch NormalizeReasoning(level) { | |
| 69 | 72 | case "minimum", "low": | |
| 70 | - | // Many OpenAI-compatible endpoints only accept low/medium/high. | |
| 71 | - | // Use low for zot's minimum instead of the newer minimal enum. | |
| 73 | + | // Many compatible endpoints only accept low/medium/high. | |
| 72 | 74 | return "low" | |
| 73 | 75 | case "medium": | |
| 74 | 76 | return "medium" | |
| 75 | - | case "high", "maximum": | |
| 77 | + | case "high", "xhigh", "max": | |
| 76 | 78 | return "high" | |
| 77 | 79 | default: | |
| 78 | 80 | return "" | |
| 79 | 81 | } | |
| 80 | 82 | } | |
| 81 | 83 | ||
| 82 | - | // OpenAICompatAnthropicEffort maps zot's user-facing thinking levels | |
| 83 | - | // onto reasoning_effort values when an adaptive-thinking Anthropic | |
| 84 | - | // model (Opus 4.7+) is served over the OpenAI-compatible chat- | |
| 85 | - | // completions wire (openrouter, opencode, ...). Differs from | |
| 86 | - | // OpenAIReasoningEffort only at the top: zot's "maximum" maps to | |
| 87 | - | // "xhigh" instead of being clamped to "high", so the model's full | |
| 88 | - | // adaptive-thinking ceiling is preserved when reachable through a | |
| 89 | - | // gateway that accepts the effort knob. | |
| 84 | + | // OpenAICompatAnthropicEffort maps zot's thinking setting when an adaptive | |
| 85 | + | // Anthropic model is served over an OpenAI-compatible chat-completions wire. | |
| 86 | + | // Adaptive models accept native xhigh and max effort values. | |
| 90 | 87 | func OpenAICompatAnthropicEffort(level string) string { | |
| 91 | 88 | switch NormalizeReasoning(level) { | |
| 92 | 89 | case "minimum", "low": | |
| @@ -95,17 +92,18 @@ func OpenAICompatAnthropicEffort(level string) string { | |||
| 95 | 92 | return "medium" | |
| 96 | 93 | case "high": | |
| 97 | 94 | return "high" | |
| 98 | - | case "maximum": | |
| 95 | + | case "xhigh": | |
| 99 | 96 | return "xhigh" | |
| 97 | + | case "max": | |
| 98 | + | return "max" | |
| 100 | 99 | default: | |
| 101 | 100 | return "" | |
| 102 | 101 | } | |
| 103 | 102 | } | |
| 104 | 103 | ||
| 105 | - | // OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex | |
| 106 | - | // Responses backend enum. That backend rejects "minimal" and uses | |
| 107 | - | // "xhigh" for the highest tier on recent GPT-5.x models. | |
| 108 | - | func OpenAICodexReasoningEffort(level string) string { | |
| 104 | + | // OpenAICodexReasoningEffort maps zot levels onto the Responses API effort | |
| 105 | + | // enum. GPT-5.6 supports native max; other models clamp max to xhigh. | |
| 106 | + | func OpenAICodexReasoningEffort(level, model string) string { | |
| 109 | 107 | switch NormalizeReasoning(level) { | |
| 110 | 108 | case "minimum", "low": | |
| 111 | 109 | return "low" | |
| @@ -113,7 +111,12 @@ func OpenAICodexReasoningEffort(level string) string { | |||
| 113 | 111 | return "medium" | |
| 114 | 112 | case "high": | |
| 115 | 113 | return "high" | |
| 116 | - | case "maximum": | |
| 114 | + | case "xhigh": | |
| 115 | + | return "xhigh" | |
| 116 | + | case "max": | |
| 117 | + | if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") { | |
| 118 | + | return "max" | |
| 119 | + | } | |
| 117 | 120 | return "xhigh" | |
| 118 | 121 | default: | |
| 119 | 122 | return "" | |
| @@ -3,7 +3,8 @@ package provider | |||||
| 3 | import "strings" | 3 | import "strings" | ||
| 4 | 4 | ||||
| 5 | // NormalizeReasoning canonicalizes zot's user-facing thinking levels. | 5 | // NormalizeReasoning canonicalizes zot's user-facing thinking levels. | ||
| 6 | - | // Empty string means reasoning/thinking is disabled. | 6 | + | // Empty string means reasoning/thinking is disabled. "maximum" remains |
| 7 | + | // an alias for xhigh; "max" is the separate opt-in tier above it. | |||
| 7 | func NormalizeReasoning(level string) string { | 8 | func NormalizeReasoning(level string) string { | ||
| 8 | switch strings.ToLower(strings.TrimSpace(level)) { | 9 | switch strings.ToLower(strings.TrimSpace(level)) { | ||
| 9 | case "", "off", "none", "no", "false", "disabled": | 10 | case "", "off", "none", "no", "false", "disabled": | ||
| @@ -16,8 +17,10 @@ func NormalizeReasoning(level string) string { | |||||
| 16 | return "medium" | 17 | return "medium" | ||
| 17 | case "hi", "high": | 18 | case "hi", "high": | ||
| 18 | return "high" | 19 | return "high" | ||
| 19 | - | case "max", "maximum": | 20 | + | case "xhigh", "maximum": |
| 20 | - | return "maximum" | 21 | + | return "xhigh" |
| 22 | + | case "max": | |||
| 23 | + | return "max" | |||
| 21 | default: | 24 | default: | ||
| 22 | return strings.ToLower(strings.TrimSpace(level)) | 25 | return strings.ToLower(strings.TrimSpace(level)) | ||
| 23 | } | 26 | } | ||
| @@ -35,7 +38,7 @@ func ReasoningBudget(level string) int { | |||||
| 35 | return 8192 | 38 | return 8192 | ||
| 36 | case "high": | 39 | case "high": | ||
| 37 | return 16384 | 40 | return 16384 | ||
| 38 | - | case "maximum": | 41 | + | case "xhigh", "max": |
| 39 | return 32768 | 42 | return 32768 | ||
| 40 | default: | 43 | default: | ||
| 41 | return 0 | 44 | return 0 | ||
| @@ -43,10 +46,8 @@ func ReasoningBudget(level string) int { | |||||
| 43 | } | 46 | } | ||
| 44 | 47 | ||||
| 45 | // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the | 48 | // AnthropicAdaptiveEffort maps zot's user-facing thinking levels onto the | ||
| 46 | - | // effort enum used by Anthropic's adaptive-thinking models (Opus 4.7+). | 49 | + | // effort enum used by adaptive-thinking models. These models reject explicit |
| 47 | - | // These models reject explicit thinking budgets; thinking depth is | 50 | + | // thinking budgets; reasoning depth is controlled by output_config.effort. |
| 48 | - | // controlled by output_config.effort instead. Returns "" when reasoning | |||
| 49 | - | // is disabled. | |||
| 50 | func AnthropicAdaptiveEffort(level string) string { | 51 | func AnthropicAdaptiveEffort(level string) string { | ||
| 51 | switch NormalizeReasoning(level) { | 52 | switch NormalizeReasoning(level) { | ||
| 52 | case "minimum", "low": | 53 | case "minimum", "low": | ||
| @@ -55,38 +56,34 @@ func AnthropicAdaptiveEffort(level string) string { | |||||
| 55 | return "medium" | 56 | return "medium" | ||
| 56 | case "high": | 57 | case "high": | ||
| 57 | return "high" | 58 | return "high" | ||
| 58 | - | case "maximum": | 59 | + | case "xhigh": |
| 59 | return "xhigh" | 60 | return "xhigh" | ||
| 61 | + | case "max": | |||
| 62 | + | return "max" | |||
| 60 | default: | 63 | default: | ||
| 61 | return "" | 64 | return "" | ||
| 62 | } | 65 | } | ||
| 63 | } | 66 | } | ||
| 64 | 67 | ||||
| 65 | - | // OpenAIReasoningEffort maps zot's six-level setting onto the effort enum | 68 | + | // OpenAIReasoningEffort maps zot's thinking setting onto the effort enum |
| 66 | - | // accepted by OpenAI-compatible chat-completions endpoints. | 69 | + | // accepted by generic OpenAI-compatible chat-completions endpoints. |
| 67 | func OpenAIReasoningEffort(level string) string { | 70 | func OpenAIReasoningEffort(level string) string { | ||
| 68 | switch NormalizeReasoning(level) { | 71 | switch NormalizeReasoning(level) { | ||
| 69 | case "minimum", "low": | 72 | case "minimum", "low": | ||
| 70 | - | // Many OpenAI-compatible endpoints only accept low/medium/high. | 73 | + | // Many compatible endpoints only accept low/medium/high. |
| 71 | - | // Use low for zot's minimum instead of the newer minimal enum. | |||
| 72 | return "low" | 74 | return "low" | ||
| 73 | case "medium": | 75 | case "medium": | ||
| 74 | return "medium" | 76 | return "medium" | ||
| 75 | - | case "high", "maximum": | 77 | + | case "high", "xhigh", "max": |
| 76 | return "high" | 78 | return "high" | ||
| 77 | default: | 79 | default: | ||
| 78 | return "" | 80 | return "" | ||
| 79 | } | 81 | } | ||
| 80 | } | 82 | } | ||
| 81 | 83 | ||||
| 82 | - | // OpenAICompatAnthropicEffort maps zot's user-facing thinking levels | 84 | + | // OpenAICompatAnthropicEffort maps zot's thinking setting when an adaptive |
| 83 | - | // onto reasoning_effort values when an adaptive-thinking Anthropic | 85 | + | // Anthropic model is served over an OpenAI-compatible chat-completions wire. |
| 84 | - | // model (Opus 4.7+) is served over the OpenAI-compatible chat- | 86 | + | // Adaptive models accept native xhigh and max effort values. |
| 85 | - | // completions wire (openrouter, opencode, ...). Differs from | |||
| 86 | - | // OpenAIReasoningEffort only at the top: zot's "maximum" maps to | |||
| 87 | - | // "xhigh" instead of being clamped to "high", so the model's full | |||
| 88 | - | // adaptive-thinking ceiling is preserved when reachable through a | |||
| 89 | - | // gateway that accepts the effort knob. | |||
| 90 | func OpenAICompatAnthropicEffort(level string) string { | 87 | func OpenAICompatAnthropicEffort(level string) string { | ||
| 91 | switch NormalizeReasoning(level) { | 88 | switch NormalizeReasoning(level) { | ||
| 92 | case "minimum", "low": | 89 | case "minimum", "low": | ||
| @@ -95,17 +92,18 @@ func OpenAICompatAnthropicEffort(level string) string { | |||||
| 95 | return "medium" | 92 | return "medium" | ||
| 96 | case "high": | 93 | case "high": | ||
| 97 | return "high" | 94 | return "high" | ||
| 98 | - | case "maximum": | 95 | + | case "xhigh": |
| 99 | return "xhigh" | 96 | return "xhigh" | ||
| 97 | + | case "max": | |||
| 98 | + | return "max" | |||
| 100 | default: | 99 | default: | ||
| 101 | return "" | 100 | return "" | ||
| 102 | } | 101 | } | ||
| 103 | } | 102 | } | ||
| 104 | 103 | ||||
| 105 | - | // OpenAICodexReasoningEffort maps zot levels onto the ChatGPT/Codex | 104 | + | // OpenAICodexReasoningEffort maps zot levels onto the Responses API effort |
| 106 | - | // Responses backend enum. That backend rejects "minimal" and uses | 105 | + | // enum. GPT-5.6 supports native max; other models clamp max to xhigh. |
| 107 | - | // "xhigh" for the highest tier on recent GPT-5.x models. | 106 | + | func OpenAICodexReasoningEffort(level, model string) string { |
| 108 | - | func OpenAICodexReasoningEffort(level string) string { | |||
| 109 | switch NormalizeReasoning(level) { | 107 | switch NormalizeReasoning(level) { | ||
| 110 | case "minimum", "low": | 108 | case "minimum", "low": | ||
| 111 | return "low" | 109 | return "low" | ||
| @@ -113,7 +111,12 @@ func OpenAICodexReasoningEffort(level string) string { | |||||
| 113 | return "medium" | 111 | return "medium" | ||
| 114 | case "high": | 112 | case "high": | ||
| 115 | return "high" | 113 | return "high" | ||
| 116 | - | case "maximum": | 114 | + | case "xhigh": |
| 115 | + | return "xhigh" | |||
| 116 | + | case "max": | |||
| 117 | + | if strings.HasPrefix(strings.ToLower(model), "gpt-5.6-") { | |||
| 118 | + | return "max" | |||
| 119 | + | } | |||
| 117 | return "xhigh" | 120 | return "xhigh" | ||
| 118 | default: | 121 | default: | ||
| 119 | return "" | 122 | return "" | ||
M packages/provider/reasoning_test.go +13 -10
| @@ -5,20 +5,23 @@ import "testing" | |||
| 5 | 5 | func TestReasoningEffortMappings(t *testing.T) { | |
| 6 | 6 | cases := []struct { | |
| 7 | 7 | level string | |
| 8 | + | model string | |
| 8 | 9 | openai string | |
| 9 | 10 | anthCompat string | |
| 10 | 11 | codex string | |
| 11 | 12 | budget int | |
| 12 | 13 | normalized string | |
| 13 | 14 | }{ | |
| 14 | - | {"off", "", "", "", 0, ""}, | |
| 15 | - | {"minimum", "low", "low", "low", 1024, "minimum"}, | |
| 16 | - | {"minimal", "low", "low", "low", 1024, "minimum"}, | |
| 17 | - | {"low", "low", "low", "low", 2048, "low"}, | |
| 18 | - | {"medium", "medium", "medium", "medium", 8192, "medium"}, | |
| 19 | - | {"high", "high", "high", "high", 16384, "high"}, | |
| 20 | - | {"maximum", "high", "xhigh", "xhigh", 32768, "maximum"}, | |
| 21 | - | {"max", "high", "xhigh", "xhigh", 32768, "maximum"}, | |
| 15 | + | {"off", "gpt-5.6-sol", "", "", "", 0, ""}, | |
| 16 | + | {"minimum", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"}, | |
| 17 | + | {"minimal", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"}, | |
| 18 | + | {"low", "gpt-5.6-sol", "low", "low", "low", 2048, "low"}, | |
| 19 | + | {"medium", "gpt-5.6-sol", "medium", "medium", "medium", 8192, "medium"}, | |
| 20 | + | {"high", "gpt-5.6-sol", "high", "high", "high", 16384, "high"}, | |
| 21 | + | {"maximum", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, | |
| 22 | + | {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, | |
| 23 | + | {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"}, | |
| 24 | + | {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"}, | |
| 22 | 25 | } | |
| 23 | 26 | for _, tc := range cases { | |
| 24 | 27 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | |
| @@ -30,8 +33,8 @@ func TestReasoningEffortMappings(t *testing.T) { | |||
| 30 | 33 | if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat { | |
| 31 | 34 | t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat) | |
| 32 | 35 | } | |
| 33 | - | if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex { | |
| 34 | - | t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex) | |
| 36 | + | if got := OpenAICodexReasoningEffort(tc.level, tc.model); got != tc.codex { | |
| 37 | + | t.Errorf("OpenAICodexReasoningEffort(%q, %q)=%q want %q", tc.level, tc.model, got, tc.codex) | |
| 35 | 38 | } | |
| 36 | 39 | if got := ReasoningBudget(tc.level); got != tc.budget { | |
| 37 | 40 | t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget) | |
| @@ -5,20 +5,23 @@ import "testing" | |||||
| 5 | func TestReasoningEffortMappings(t *testing.T) { | 5 | func TestReasoningEffortMappings(t *testing.T) { | ||
| 6 | cases := []struct { | 6 | cases := []struct { | ||
| 7 | level string | 7 | level string | ||
| 8 | + | model string | |||
| 8 | openai string | 9 | openai string | ||
| 9 | anthCompat string | 10 | anthCompat string | ||
| 10 | codex string | 11 | codex string | ||
| 11 | budget int | 12 | budget int | ||
| 12 | normalized string | 13 | normalized string | ||
| 13 | }{ | 14 | }{ | ||
| 14 | - | {"off", "", "", "", 0, ""}, | 15 | + | {"off", "gpt-5.6-sol", "", "", "", 0, ""}, |
| 15 | - | {"minimum", "low", "low", "low", 1024, "minimum"}, | 16 | + | {"minimum", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"}, |
| 16 | - | {"minimal", "low", "low", "low", 1024, "minimum"}, | 17 | + | {"minimal", "gpt-5.6-sol", "low", "low", "low", 1024, "minimum"}, |
| 17 | - | {"low", "low", "low", "low", 2048, "low"}, | 18 | + | {"low", "gpt-5.6-sol", "low", "low", "low", 2048, "low"}, |
| 18 | - | {"medium", "medium", "medium", "medium", 8192, "medium"}, | 19 | + | {"medium", "gpt-5.6-sol", "medium", "medium", "medium", 8192, "medium"}, |
| 19 | - | {"high", "high", "high", "high", 16384, "high"}, | 20 | + | {"high", "gpt-5.6-sol", "high", "high", "high", 16384, "high"}, |
| 20 | - | {"maximum", "high", "xhigh", "xhigh", 32768, "maximum"}, | 21 | + | {"maximum", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, |
| 21 | - | {"max", "high", "xhigh", "xhigh", 32768, "maximum"}, | 22 | + | {"xhigh", "gpt-5.6-sol", "high", "xhigh", "xhigh", 32768, "xhigh"}, |
| 23 | + | {"max", "gpt-5.6-sol", "high", "max", "max", 32768, "max"}, | |||
| 24 | + | {"max", "gpt-5.5", "high", "max", "xhigh", 32768, "max"}, | |||
| 22 | } | 25 | } | ||
| 23 | for _, tc := range cases { | 26 | for _, tc := range cases { | ||
| 24 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | 27 | if got := NormalizeReasoning(tc.level); got != tc.normalized { | ||
| @@ -30,8 +33,8 @@ func TestReasoningEffortMappings(t *testing.T) { | |||||
| 30 | if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat { | 33 | if got := OpenAICompatAnthropicEffort(tc.level); got != tc.anthCompat { | ||
| 31 | t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat) | 34 | t.Errorf("OpenAICompatAnthropicEffort(%q)=%q want %q", tc.level, got, tc.anthCompat) | ||
| 32 | } | 35 | } | ||
| 33 | - | if got := OpenAICodexReasoningEffort(tc.level); got != tc.codex { | 36 | + | if got := OpenAICodexReasoningEffort(tc.level, tc.model); got != tc.codex { |
| 34 | - | t.Errorf("OpenAICodexReasoningEffort(%q)=%q want %q", tc.level, got, tc.codex) | 37 | + | t.Errorf("OpenAICodexReasoningEffort(%q, %q)=%q want %q", tc.level, tc.model, got, tc.codex) |
| 35 | } | 38 | } | ||
| 36 | if got := ReasoningBudget(tc.level); got != tc.budget { | 39 | if got := ReasoningBudget(tc.level); got != tc.budget { | ||
| 37 | t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget) | 40 | t.Errorf("ReasoningBudget(%q)=%d want %d", tc.level, got, tc.budget) | ||
M packages/tui/statusbar_test.go +16 -2
| @@ -85,12 +85,26 @@ func TestStatusBarThinkingLevelBetweenModelAndStats(t *testing.T) { | |||
| 85 | 85 | } | |
| 86 | 86 | } | |
| 87 | 87 | ||
| 88 | + | func TestStatusBarUsesThinkingMaxThemeColor(t *testing.T) { | |
| 89 | + | th := Dark | |
| 90 | + | th.ThinkingMax = 201 | |
| 91 | + | lines := StatusBar(StatusBarParams{ | |
| 92 | + | Theme: th, Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", Cols: 200, | |
| 93 | + | }) | |
| 94 | + | if len(lines) != 1 { | |
| 95 | + | t.Fatalf("lines = %d, want 1", len(lines)) | |
| 96 | + | } | |
| 97 | + | if !strings.Contains(lines[0], "\x1b[38;5;201m") || !strings.Contains(stripANSI(lines[0]), "thinking: max") { | |
| 98 | + | t.Fatalf("max thinking style missing: %q", lines[0]) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 88 | 102 | func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) { | |
| 89 | 103 | lines := StatusBar(StatusBarParams{ | |
| 90 | 104 | Theme: Dark, | |
| 91 | 105 | Provider: "openai-codex", | |
| 92 | 106 | Model: "gpt-5.5", | |
| 93 | - | Reasoning: "maximum", | |
| 107 | + | Reasoning: "xhigh", | |
| 94 | 108 | CWD: "/tmp/x", | |
| 95 | 109 | Usage: provider.Usage{ | |
| 96 | 110 | CostUSD: 0, | |
| @@ -107,7 +121,7 @@ func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) { | |||
| 107 | 121 | for i, line := range lines { | |
| 108 | 122 | plain[i] = stripANSI(line) | |
| 109 | 123 | } | |
| 110 | - | if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: maximum") { | |
| 124 | + | if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: xhigh") { | |
| 111 | 125 | t.Fatalf("line 1 should contain model and thinking level, got %q", plain[0]) | |
| 112 | 126 | } | |
| 113 | 127 | if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "thinking level") { | |
| @@ -85,12 +85,26 @@ func TestStatusBarThinkingLevelBetweenModelAndStats(t *testing.T) { | |||||
| 85 | } | 85 | } | ||
| 86 | } | 86 | } | ||
| 87 | 87 | ||||
| 88 | + | func TestStatusBarUsesThinkingMaxThemeColor(t *testing.T) { | |||
| 89 | + | th := Dark | |||
| 90 | + | th.ThinkingMax = 201 | |||
| 91 | + | lines := StatusBar(StatusBarParams{ | |||
| 92 | + | Theme: th, Provider: "openai", Model: "gpt-5.6-sol", Reasoning: "max", Cols: 200, | |||
| 93 | + | }) | |||
| 94 | + | if len(lines) != 1 { | |||
| 95 | + | t.Fatalf("lines = %d, want 1", len(lines)) | |||
| 96 | + | } | |||
| 97 | + | if !strings.Contains(lines[0], "\x1b[38;5;201m") || !strings.Contains(stripANSI(lines[0]), "thinking: max") { | |||
| 98 | + | t.Fatalf("max thinking style missing: %q", lines[0]) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 88 | func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) { | 102 | func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) { | ||
| 89 | lines := StatusBar(StatusBarParams{ | 103 | lines := StatusBar(StatusBarParams{ | ||
| 90 | Theme: Dark, | 104 | Theme: Dark, | ||
| 91 | Provider: "openai-codex", | 105 | Provider: "openai-codex", | ||
| 92 | Model: "gpt-5.5", | 106 | Model: "gpt-5.5", | ||
| 93 | - | Reasoning: "maximum", | 107 | + | Reasoning: "xhigh", |
| 94 | CWD: "/tmp/x", | 108 | CWD: "/tmp/x", | ||
| 95 | Usage: provider.Usage{ | 109 | Usage: provider.Usage{ | ||
| 96 | CostUSD: 0, | 110 | CostUSD: 0, | ||
| @@ -107,7 +121,7 @@ func TestStatusBarNarrowKeepsModelAndThinkingTogetherWhenTheyFit(t *testing.T) { | |||||
| 107 | for i, line := range lines { | 121 | for i, line := range lines { | ||
| 108 | plain[i] = stripANSI(line) | 122 | plain[i] = stripANSI(line) | ||
| 109 | } | 123 | } | ||
| 110 | - | if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: maximum") { | 124 | + | if !strings.Contains(plain[0], "(openai-codex) gpt-5.5 thinking: xhigh") { |
| 111 | t.Fatalf("line 1 should contain model and thinking level, got %q", plain[0]) | 125 | t.Fatalf("line 1 should contain model and thinking level, got %q", plain[0]) | ||
| 112 | } | 126 | } | ||
| 113 | if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "thinking level") { | 127 | if !strings.Contains(plain[1], "$0.000 (sub)") || strings.Contains(plain[1], "thinking level") { | ||
M packages/tui/theme.go +3 -0
| @@ -49,6 +49,7 @@ type Theme struct { | |||
| 49 | 49 | Error int | |
| 50 | 50 | Warning int | |
| 51 | 51 | Spinner int // spinner + funny working line | |
| 52 | + | ThinkingMax int // status color for the opt-in max thinking level | |
| 52 | 53 | SelectionBG int // background for highlighted rows | |
| 53 | 54 | SelectionFG int // foreground for highlighted rows | |
| 54 | 55 | ||
| @@ -159,6 +160,7 @@ var Dark = Theme{ | |||
| 159 | 160 | Error: 203, | |
| 160 | 161 | Warning: 214, | |
| 161 | 162 | Spinner: 183, // soft purple | |
| 163 | + | ThinkingMax: 207, // vivid magenta | |
| 162 | 164 | SelectionBG: 24, // deep blue background | |
| 163 | 165 | SelectionFG: 231, // near-white foreground | |
| 164 | 166 | SpinnerFrames: defaultSpinnerFrames, | |
| @@ -181,6 +183,7 @@ var Light = Theme{ | |||
| 181 | 183 | Error: 160, | |
| 182 | 184 | Warning: 166, | |
| 183 | 185 | Spinner: 91, // purple | |
| 186 | + | ThinkingMax: 127, // deep magenta | |
| 184 | 187 | SelectionBG: 153, // light blue | |
| 185 | 188 | SelectionFG: 232, // near-black | |
| 186 | 189 | SpinnerFrames: defaultSpinnerFrames, | |
| @@ -49,6 +49,7 @@ type Theme struct { | |||||
| 49 | Error int | 49 | Error int | ||
| 50 | Warning int | 50 | Warning int | ||
| 51 | Spinner int // spinner + funny working line | 51 | Spinner int // spinner + funny working line | ||
| 52 | + | ThinkingMax int // status color for the opt-in max thinking level | |||
| 52 | SelectionBG int // background for highlighted rows | 53 | SelectionBG int // background for highlighted rows | ||
| 53 | SelectionFG int // foreground for highlighted rows | 54 | SelectionFG int // foreground for highlighted rows | ||
| 54 | 55 | ||||
| @@ -159,6 +160,7 @@ var Dark = Theme{ | |||||
| 159 | Error: 203, | 160 | Error: 203, | ||
| 160 | Warning: 214, | 161 | Warning: 214, | ||
| 161 | Spinner: 183, // soft purple | 162 | Spinner: 183, // soft purple | ||
| 163 | + | ThinkingMax: 207, // vivid magenta | |||
| 162 | SelectionBG: 24, // deep blue background | 164 | SelectionBG: 24, // deep blue background | ||
| 163 | SelectionFG: 231, // near-white foreground | 165 | SelectionFG: 231, // near-white foreground | ||
| 164 | SpinnerFrames: defaultSpinnerFrames, | 166 | SpinnerFrames: defaultSpinnerFrames, | ||
| @@ -181,6 +183,7 @@ var Light = Theme{ | |||||
| 181 | Error: 160, | 183 | Error: 160, | ||
| 182 | Warning: 166, | 184 | Warning: 166, | ||
| 183 | Spinner: 91, // purple | 185 | Spinner: 91, // purple | ||
| 186 | + | ThinkingMax: 127, // deep magenta | |||
| 184 | SelectionBG: 153, // light blue | 187 | SelectionBG: 153, // light blue | ||
| 185 | SelectionFG: 232, // near-black | 188 | SelectionFG: 232, // near-black | ||
| 186 | SpinnerFrames: defaultSpinnerFrames, | 189 | SpinnerFrames: defaultSpinnerFrames, | ||
M packages/tui/theme_loader.go +7 -0
| @@ -92,6 +92,8 @@ type ThemeOverrides struct { | |||
| 92 | 92 | Error *int `json:"error,omitempty"` | |
| 93 | 93 | Warning *int `json:"warning,omitempty"` | |
| 94 | 94 | Spinner *int `json:"spinner,omitempty"` | |
| 95 | + | ThinkingMax *int `json:"thinking_max,omitempty"` | |
| 96 | + | ThinkingMaxCamel *int `json:"thinkingMax,omitempty"` | |
| 95 | 97 | SelectionBG *int `json:"selection_bg,omitempty"` | |
| 96 | 98 | SelectionFG *int `json:"selection_fg,omitempty"` | |
| 97 | 99 | SpinnerFrames []string `json:"spinner_frames,omitempty"` | |
| @@ -383,6 +385,11 @@ func applyThemeOverrides(th Theme, o ThemeOverrides) Theme { | |||
| 383 | 385 | if o.Spinner != nil { | |
| 384 | 386 | th.Spinner = *o.Spinner | |
| 385 | 387 | } | |
| 388 | + | if o.ThinkingMax != nil { | |
| 389 | + | th.ThinkingMax = *o.ThinkingMax | |
| 390 | + | } else if o.ThinkingMaxCamel != nil { | |
| 391 | + | th.ThinkingMax = *o.ThinkingMaxCamel | |
| 392 | + | } | |
| 386 | 393 | if o.SelectionBG != nil { | |
| 387 | 394 | th.SelectionBG = *o.SelectionBG | |
| 388 | 395 | } | |
| @@ -92,6 +92,8 @@ type ThemeOverrides struct { | |||||
| 92 | Error *int `json:"error,omitempty"` | 92 | Error *int `json:"error,omitempty"` | ||
| 93 | Warning *int `json:"warning,omitempty"` | 93 | Warning *int `json:"warning,omitempty"` | ||
| 94 | Spinner *int `json:"spinner,omitempty"` | 94 | Spinner *int `json:"spinner,omitempty"` | ||
| 95 | + | ThinkingMax *int `json:"thinking_max,omitempty"` | |||
| 96 | + | ThinkingMaxCamel *int `json:"thinkingMax,omitempty"` | |||
| 95 | SelectionBG *int `json:"selection_bg,omitempty"` | 97 | SelectionBG *int `json:"selection_bg,omitempty"` | ||
| 96 | SelectionFG *int `json:"selection_fg,omitempty"` | 98 | SelectionFG *int `json:"selection_fg,omitempty"` | ||
| 97 | SpinnerFrames []string `json:"spinner_frames,omitempty"` | 99 | SpinnerFrames []string `json:"spinner_frames,omitempty"` | ||
| @@ -383,6 +385,11 @@ func applyThemeOverrides(th Theme, o ThemeOverrides) Theme { | |||||
| 383 | if o.Spinner != nil { | 385 | if o.Spinner != nil { | ||
| 384 | th.Spinner = *o.Spinner | 386 | th.Spinner = *o.Spinner | ||
| 385 | } | 387 | } | ||
| 388 | + | if o.ThinkingMax != nil { | |||
| 389 | + | th.ThinkingMax = *o.ThinkingMax | |||
| 390 | + | } else if o.ThinkingMaxCamel != nil { | |||
| 391 | + | th.ThinkingMax = *o.ThinkingMaxCamel | |||
| 392 | + | } | |||
| 386 | if o.SelectionBG != nil { | 393 | if o.SelectionBG != nil { | ||
| 387 | th.SelectionBG = *o.SelectionBG | 394 | th.SelectionBG = *o.SelectionBG | ||
| 388 | } | 395 | } | ||
M packages/tui/theme_loader_test.go +19 -0
| @@ -34,6 +34,25 @@ func TestLoadThemeAllowsPartialColorOverrides(t *testing.T) { | |||
| 34 | 34 | } | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | + | func TestLoadThemeAllowsThinkingMaxOverride(t *testing.T) { | |
| 38 | + | home := t.TempDir() | |
| 39 | + | if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil { | |
| 40 | + | t.Fatal(err) | |
| 41 | + | } | |
| 42 | + | path := filepath.Join(home, "themes", "thinking.json") | |
| 43 | + | if err := os.WriteFile(path, []byte(`{"colors":{"dark":{"thinkingMax":201}}}`), 0o644); err != nil { | |
| 44 | + | t.Fatal(err) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | th, _, err := LoadThemeFromHome(home, "thinking", Dark) | |
| 48 | + | if err != nil { | |
| 49 | + | t.Fatal(err) | |
| 50 | + | } | |
| 51 | + | if th.ThinkingMax != 201 { | |
| 52 | + | t.Fatalf("thinking max = %d, want 201", th.ThinkingMax) | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | ||
| 37 | 56 | func TestLoadThemeAllowsSpinnerOnlyTopLevelOverrides(t *testing.T) { | |
| 38 | 57 | home := t.TempDir() | |
| 39 | 58 | if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil { | |
| @@ -34,6 +34,25 @@ func TestLoadThemeAllowsPartialColorOverrides(t *testing.T) { | |||||
| 34 | } | 34 | } | ||
| 35 | } | 35 | } | ||
| 36 | 36 | ||||
| 37 | + | func TestLoadThemeAllowsThinkingMaxOverride(t *testing.T) { | |||
| 38 | + | home := t.TempDir() | |||
| 39 | + | if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil { | |||
| 40 | + | t.Fatal(err) | |||
| 41 | + | } | |||
| 42 | + | path := filepath.Join(home, "themes", "thinking.json") | |||
| 43 | + | if err := os.WriteFile(path, []byte(`{"colors":{"dark":{"thinkingMax":201}}}`), 0o644); err != nil { | |||
| 44 | + | t.Fatal(err) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | th, _, err := LoadThemeFromHome(home, "thinking", Dark) | |||
| 48 | + | if err != nil { | |||
| 49 | + | t.Fatal(err) | |||
| 50 | + | } | |||
| 51 | + | if th.ThinkingMax != 201 { | |||
| 52 | + | t.Fatalf("thinking max = %d, want 201", th.ThinkingMax) | |||
| 53 | + | } | |||
| 54 | + | } | |||
| 55 | + | ||||
| 37 | func TestLoadThemeAllowsSpinnerOnlyTopLevelOverrides(t *testing.T) { | 56 | func TestLoadThemeAllowsSpinnerOnlyTopLevelOverrides(t *testing.T) { | ||
| 38 | home := t.TempDir() | 57 | home := t.TempDir() | ||
| 39 | if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil { | 58 | if err := os.MkdirAll(filepath.Join(home, "themes"), 0o755); err != nil { | ||
M packages/tui/view.go +17 -7
| @@ -2453,8 +2453,8 @@ func StatusBar(p StatusBarParams) []string { | |||
| 2453 | 2453 | leftBuilder.WriteString(th.FG256(th.Muted, left)) | |
| 2454 | 2454 | if middle != "" { | |
| 2455 | 2455 | leftBuilder.WriteString(pad) | |
| 2456 | - | // `middle` already has colorized context segments; wrap the rest in muted. | |
| 2457 | - | leftBuilder.WriteString(th.FG256(th.Muted, middle)) | |
| 2456 | + | // Highlight the opt-in max tier; other status information stays muted. | |
| 2457 | + | leftBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle)) | |
| 2458 | 2458 | } | |
| 2459 | 2459 | ||
| 2460 | 2460 | cwd := shortenHome(p.CWD) | |
| @@ -2489,7 +2489,7 @@ func StatusBar(p StatusBarParams) []string { | |||
| 2489 | 2489 | infoBuilder.WriteString(modelLine) | |
| 2490 | 2490 | if middle != "" { | |
| 2491 | 2491 | infoBuilder.WriteString(pad) | |
| 2492 | - | infoBuilder.WriteString(th.FG256(th.Muted, middle)) | |
| 2492 | + | infoBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle)) | |
| 2493 | 2493 | } | |
| 2494 | 2494 | lines = append(lines, infoBuilder.String()) | |
| 2495 | 2495 | } | |
| @@ -2533,11 +2533,12 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking | |||
| 2533 | 2533 | } | |
| 2534 | 2534 | ||
| 2535 | 2535 | modelThinkingPlain := pad + modelText + pad + thinkingText | |
| 2536 | + | thinkingColor := thinkingStatusColor(th, thinkingText) | |
| 2536 | 2537 | if cols <= 0 || visibleWidth(modelThinkingPlain) <= cols { | |
| 2537 | - | lines = append(lines, pad+th.FG256(th.Muted, modelText+pad+thinkingText)) | |
| 2538 | + | lines = append(lines, pad+th.FG256(thinkingColor, modelText+pad+thinkingText)) | |
| 2538 | 2539 | } else { | |
| 2539 | 2540 | lines = append(lines, modelLine) | |
| 2540 | - | lines = append(lines, pad+th.FG256(th.Muted, thinkingText)) | |
| 2541 | + | lines = append(lines, pad+th.FG256(thinkingColor, thinkingText)) | |
| 2541 | 2542 | } | |
| 2542 | 2543 | if statsText != "" { | |
| 2543 | 2544 | lines = append(lines, pad+th.FG256(th.Muted, statsText)) | |
| @@ -2545,14 +2546,23 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking | |||
| 2545 | 2546 | return lines | |
| 2546 | 2547 | } | |
| 2547 | 2548 | ||
| 2549 | + | func thinkingStatusColor(th Theme, thinkingText string) int { | |
| 2550 | + | if strings.HasSuffix(thinkingText, ": max") { | |
| 2551 | + | return th.ThinkingMax | |
| 2552 | + | } | |
| 2553 | + | return th.Muted | |
| 2554 | + | } | |
| 2555 | + | ||
| 2548 | 2556 | func thinkingLevelLabel(level string) string { | |
| 2549 | 2557 | switch strings.ToLower(strings.TrimSpace(level)) { | |
| 2550 | 2558 | case "", "off", "none", "no", "false", "disabled": | |
| 2551 | 2559 | return "" | |
| 2552 | 2560 | case "minimum", "minimal", "min": | |
| 2553 | 2561 | return "minimal" | |
| 2554 | - | case "maximum", "max", "xhigh": | |
| 2555 | - | return "maximum" | |
| 2562 | + | case "maximum", "xhigh": | |
| 2563 | + | return "xhigh" | |
| 2564 | + | case "max": | |
| 2565 | + | return "max" | |
| 2556 | 2566 | default: | |
| 2557 | 2567 | return strings.ToLower(strings.TrimSpace(level)) | |
| 2558 | 2568 | } | |
| @@ -2453,8 +2453,8 @@ func StatusBar(p StatusBarParams) []string { | |||||
| 2453 | leftBuilder.WriteString(th.FG256(th.Muted, left)) | 2453 | leftBuilder.WriteString(th.FG256(th.Muted, left)) | ||
| 2454 | if middle != "" { | 2454 | if middle != "" { | ||
| 2455 | leftBuilder.WriteString(pad) | 2455 | leftBuilder.WriteString(pad) | ||
| 2456 | - | // `middle` already has colorized context segments; wrap the rest in muted. | 2456 | + | // Highlight the opt-in max tier; other status information stays muted. |
| 2457 | - | leftBuilder.WriteString(th.FG256(th.Muted, middle)) | 2457 | + | leftBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle)) |
| 2458 | } | 2458 | } | ||
| 2459 | 2459 | ||||
| 2460 | cwd := shortenHome(p.CWD) | 2460 | cwd := shortenHome(p.CWD) | ||
| @@ -2489,7 +2489,7 @@ func StatusBar(p StatusBarParams) []string { | |||||
| 2489 | infoBuilder.WriteString(modelLine) | 2489 | infoBuilder.WriteString(modelLine) | ||
| 2490 | if middle != "" { | 2490 | if middle != "" { | ||
| 2491 | infoBuilder.WriteString(pad) | 2491 | infoBuilder.WriteString(pad) | ||
| 2492 | - | infoBuilder.WriteString(th.FG256(th.Muted, middle)) | 2492 | + | infoBuilder.WriteString(th.FG256(thinkingStatusColor(th, thinkingText), middle)) |
| 2493 | } | 2493 | } | ||
| 2494 | lines = append(lines, infoBuilder.String()) | 2494 | lines = append(lines, infoBuilder.String()) | ||
| 2495 | } | 2495 | } | ||
| @@ -2533,11 +2533,12 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking | |||||
| 2533 | } | 2533 | } | ||
| 2534 | 2534 | ||||
| 2535 | modelThinkingPlain := pad + modelText + pad + thinkingText | 2535 | modelThinkingPlain := pad + modelText + pad + thinkingText | ||
| 2536 | + | thinkingColor := thinkingStatusColor(th, thinkingText) | |||
| 2536 | if cols <= 0 || visibleWidth(modelThinkingPlain) <= cols { | 2537 | if cols <= 0 || visibleWidth(modelThinkingPlain) <= cols { | ||
| 2537 | - | lines = append(lines, pad+th.FG256(th.Muted, modelText+pad+thinkingText)) | 2538 | + | lines = append(lines, pad+th.FG256(thinkingColor, modelText+pad+thinkingText)) |
| 2538 | } else { | 2539 | } else { | ||
| 2539 | lines = append(lines, modelLine) | 2540 | lines = append(lines, modelLine) | ||
| 2540 | - | lines = append(lines, pad+th.FG256(th.Muted, thinkingText)) | 2541 | + | lines = append(lines, pad+th.FG256(thinkingColor, thinkingText)) |
| 2541 | } | 2542 | } | ||
| 2542 | if statsText != "" { | 2543 | if statsText != "" { | ||
| 2543 | lines = append(lines, pad+th.FG256(th.Muted, statsText)) | 2544 | lines = append(lines, pad+th.FG256(th.Muted, statsText)) | ||
| @@ -2545,14 +2546,23 @@ func appendWrappedStatusLines(lines []string, th Theme, pad, modelText, thinking | |||||
| 2545 | return lines | 2546 | return lines | ||
| 2546 | } | 2547 | } | ||
| 2547 | 2548 | ||||
| 2549 | + | func thinkingStatusColor(th Theme, thinkingText string) int { | |||
| 2550 | + | if strings.HasSuffix(thinkingText, ": max") { | |||
| 2551 | + | return th.ThinkingMax | |||
| 2552 | + | } | |||
| 2553 | + | return th.Muted | |||
| 2554 | + | } | |||
| 2555 | + | ||||
| 2548 | func thinkingLevelLabel(level string) string { | 2556 | func thinkingLevelLabel(level string) string { | ||
| 2549 | switch strings.ToLower(strings.TrimSpace(level)) { | 2557 | switch strings.ToLower(strings.TrimSpace(level)) { | ||
| 2550 | case "", "off", "none", "no", "false", "disabled": | 2558 | case "", "off", "none", "no", "false", "disabled": | ||
| 2551 | return "" | 2559 | return "" | ||
| 2552 | case "minimum", "minimal", "min": | 2560 | case "minimum", "minimal", "min": | ||
| 2553 | return "minimal" | 2561 | return "minimal" | ||
| 2554 | - | case "maximum", "max", "xhigh": | 2562 | + | case "maximum", "xhigh": |
| 2555 | - | return "maximum" | 2563 | + | return "xhigh" |
| 2564 | + | case "max": | |||
| 2565 | + | return "max" | |||
| 2556 | default: | 2566 | default: | ||
| 2557 | return strings.ToLower(strings.TrimSpace(level)) | 2567 | return strings.ToLower(strings.TrimSpace(level)) | ||
| 2558 | } | 2568 | } | ||