From 6ce17db11aa1aa4fd27f4317d5f8a2586d153c14 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Apr 2026 06:47:06 +0100 Subject: [PATCH] fix: gate max thinking by model support --- CHANGELOG.md | 1 + docs/concepts/active-memory.md | 2 +- docs/concepts/model-providers.md | 3 +- docs/gateway/configuration-reference.md | 2 +- docs/plugins/architecture.md | 24 +++---- docs/plugins/sdk-provider-plugins.md | 21 ++++--- docs/providers/mistral.md | 2 +- docs/tools/thinking.md | 9 ++- extensions/active-memory/index.ts | 6 +- extensions/anthropic/index.test.ts | 12 ++++ extensions/anthropic/register.runtime.ts | 1 + .../native-command.think-autocomplete.test.ts | 63 +++++++++++++++++++ extensions/kimi-coding/stream.ts | 10 ++- extensions/llm-task/src/llm-task-tool.ts | 13 +++- extensions/mistral/api.ts | 1 + extensions/ollama/src/stream.ts | 2 +- extensions/qa-lab/src/cli.runtime.ts | 6 +- extensions/qa-lab/src/cli.ts | 2 +- extensions/qa-lab/src/qa-thinking.ts | 13 +++- .../agent-command.live-model-switch.test.ts | 1 + src/agents/agent-command.ts | 22 +++++++ src/agents/model-selection.ts | 10 ++- src/agents/model-thinking-default.ts | 5 +- .../proxy-stream-wrappers.ts | 3 + src/agents/pi-embedded-runner/utils.ts | 3 + .../reply/directive-handling.impl.ts | 54 +++++++++++++--- src/auto-reply/reply/get-reply-run.ts | 27 +++++++- src/auto-reply/thinking.shared.ts | 21 +++++-- src/auto-reply/thinking.test.ts | 19 ++++++ src/auto-reply/thinking.ts | 56 +++++++++++++++++ src/config/schema.base.generated.ts | 6 +- src/config/types.agent-defaults.ts | 2 +- src/config/types.agents.ts | 2 +- src/config/zod-schema.agent-defaults.ts | 1 + src/config/zod-schema.agent-runtime.ts | 2 +- src/cron/isolated-agent/run.runtime.ts | 6 +- src/cron/isolated-agent/run.test-harness.ts | 3 + src/cron/isolated-agent/run.ts | 14 +++++ src/gateway/sessions-patch.ts | 10 +++ src/plugin-sdk/llm-task.ts | 1 + src/plugin-sdk/provider-stream-shared.ts | 4 ++ src/plugins/provider-runtime.ts | 10 +++ src/plugins/provider-thinking.ts | 19 +++++- src/plugins/provider-thinking.types.ts | 2 +- src/plugins/types.ts | 18 +++++- ui/src/ui/chat/session-controls.ts | 2 +- .../chat/slash-command-executor.node.test.ts | 2 +- ui/src/ui/chat/slash-command-executor.ts | 4 +- ui/src/ui/thinking.ts | 61 +++++++++++++++--- 49 files changed, 510 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9d5cc1f401..d3ce51793bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai - fix(gateway): enforce allowRequestSessionKey gate on template-rendered mapping sessionKeys. (#69381) Thanks @pgondhi987. - Webchat/images: treat inline image attachments as media for empty-turn gating while still ignoring metadata-only blank turns. (#69474) Thanks @Jaswir. - Discord/think: only show `adaptive` in `/think` autocomplete for provider/model pairs that actually support provider-managed adaptive thinking, so GPT/OpenAI models no longer advertise an Anthropic-only option. +- Thinking: only expose `max` for models that explicitly support provider max reasoning, and remap stored `max` settings to the largest supported thinking mode when users switch to another model. - OpenAI/Responses: resolve `/think` levels against each GPT model's supported reasoning efforts so `/think off` no longer becomes high reasoning or sends unsupported `reasoning.effort: "none"` payloads. - Lobster/TaskFlow: allow managed approval resumes to use `approvalId` without a resume token, and persist that id in approval wait state. (#69559) Thanks @kirkluokun. - Plugins/startup: install bundled runtime dependencies into each plugin's own runtime directory, reuse source-checkout repair caches after rebuilds, and log only packages that were actually installed so repeated Gateway starts stay quiet once deps are present. diff --git a/docs/concepts/active-memory.md b/docs/concepts/active-memory.md index 4ebc1b61fa4..21573231510 100644 --- a/docs/concepts/active-memory.md +++ b/docs/concepts/active-memory.md @@ -625,7 +625,7 @@ The most important fields are: | `config.model` | `string` | Optional blocking memory sub-agent model ref; when unset, active memory uses the current session model | | `config.queryMode` | `"message" \| "recent" \| "full"` | Controls how much conversation the blocking memory sub-agent sees | | `config.promptStyle` | `"balanced" \| "strict" \| "contextual" \| "recall-heavy" \| "precision-heavy" \| "preference-only"` | Controls how eager or strict the blocking memory sub-agent is when deciding whether to return memory | -| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed | +| `config.thinking` | `"off" \| "minimal" \| "low" \| "medium" \| "high" \| "xhigh" \| "adaptive" \| "max"` | Advanced thinking override for the blocking memory sub-agent; default `off` for speed | | `config.promptOverride` | `string` | Advanced full prompt replacement; not recommended for normal use | | `config.promptAppend` | `string` | Advanced extra instructions appended to the default or overridden prompt | | `config.timeoutMs` | `number` | Hard timeout for the blocking memory sub-agent, capped at 120000 ms | diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index f2f9dfc22fc..e84ab1d5cd0 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -43,7 +43,7 @@ For model selection rules, see [/concepts/models](/concepts/models). `matchesContextOverflowError`, `classifyFailoverReason`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, `supportsXHighThinking`, - `supportsAdaptiveThinking`, + `supportsAdaptiveThinking`, `supportsMaxThinking`, `resolveDefaultThinkingLevel`, `applyConfigDefaults`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`, and `onModelSelected`. @@ -135,6 +135,7 @@ Typical split: - `isBinaryThinking`: provider owns binary on/off thinking UX - `supportsXHighThinking`: provider opts selected models into `xhigh` - `supportsAdaptiveThinking`: provider opts selected models into `adaptive` +- `supportsMaxThinking`: provider opts selected models into `max` - `resolveDefaultThinkingLevel`: provider owns default `/think` policy for a model family - `applyConfigDefaults`: provider applies provider-specific global defaults diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index dd1c9343f59..83a2dffa976 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -1791,7 +1791,7 @@ scripts/sandbox-browser-setup.sh # optional browser image - `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks). Cron jobs that only override `primary` still inherit default fallbacks unless you set `fallbacks: []`. - `params`: per-agent stream params merged over the selected model entry in `agents.defaults.models`. Use this for agent-specific overrides like `cacheRetention`, `temperature`, or `maxTokens` without duplicating the whole model catalog. - `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills. -- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. +- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. - `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Applies when no per-message or session reasoning override is set. - `fastModeDefault`: optional per-agent default for fast mode (`true | false`). Applies when no per-message or session fast-mode override is set. - `embeddedHarness`: optional per-agent low-level harness policy override. Use `{ runtime: "codex", fallback: "none" }` to make one agent Codex-only while other agents keep the default PI fallback. diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 57cd3673f22..8ecd3320e7a 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -659,6 +659,7 @@ Provider plugins now have two layers: `classifyFailoverReason`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, `supportsXHighThinking`, `supportsAdaptiveThinking`, + `supportsMaxThinking`, `resolveDefaultThinkingLevel`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`, `createEmbeddingProvider`, `buildReplayPolicy`, @@ -725,16 +726,17 @@ The "When to use" column is the quick decision guide. | 33 | `isBinaryThinking` | On/off reasoning toggle for binary-thinking providers | Provider exposes only binary thinking on/off | | 34 | `supportsXHighThinking` | `xhigh` reasoning support for selected models | Provider wants `xhigh` on only a subset of models | | 35 | `supportsAdaptiveThinking` | `adaptive` thinking support for selected models | Provider wants `adaptive` shown only for models with provider-managed adaptive thinking | -| 36 | `resolveDefaultThinkingLevel` | Default `/think` level for a specific model family | Provider owns default `/think` policy for a model family | -| 37 | `isModernModelRef` | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching | -| 38 | `prepareRuntimeAuth` | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential | -| 39 | `resolveUsageAuth` | Resolve usage/billing credentials for `/usage` and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential | -| 40 | `fetchUsageSnapshot` | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser | -| 41 | `createEmbeddingProvider` | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin | -| 42 | `buildReplayPolicy` | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) | -| 43 | `sanitizeReplayHistory` | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers | -| 44 | `validateReplayTurns` | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation | -| 45 | `onModelSelected` | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active | +| 36 | `supportsMaxThinking` | `max` reasoning support for selected models | Provider wants `max` shown only for models with provider max thinking | +| 37 | `resolveDefaultThinkingLevel` | Default `/think` level for a specific model family | Provider owns default `/think` policy for a model family | +| 38 | `isModernModelRef` | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching | +| 39 | `prepareRuntimeAuth` | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential | +| 40 | `resolveUsageAuth` | Resolve usage/billing credentials for `/usage` and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential | +| 41 | `fetchUsageSnapshot` | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser | +| 42 | `createEmbeddingProvider` | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin | +| 43 | `buildReplayPolicy` | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) | +| 44 | `sanitizeReplayHistory` | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers | +| 45 | `validateReplayTurns` | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation | +| 46 | `onModelSelected` | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active | `normalizeModelId`, `normalizeTransport`, and `normalizeConfig` first check the matched provider plugin, then fall through other hook-capable provider plugins @@ -806,7 +808,7 @@ api.registerProvider({ - Anthropic uses `resolveDynamicModel`, `capabilities`, `buildAuthDoctorHint`, `resolveUsageAuth`, `fetchUsageSnapshot`, `isCacheTtlEligible`, - `supportsAdaptiveThinking`, `resolveDefaultThinkingLevel`, `applyConfigDefaults`, `isModernModelRef`, + `supportsAdaptiveThinking`, `supportsMaxThinking`, `resolveDefaultThinkingLevel`, `applyConfigDefaults`, `isModernModelRef`, and `wrapStreamFn` because it owns Claude 4.6 forward-compat, provider-family hints, auth repair guidance, usage endpoint integration, prompt-cache eligibility, auth-aware config defaults, Claude diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index 7cc4c0f5eb0..5fe8e2ebb97 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -536,16 +536,17 @@ API key auth, and dynamic model resolution. | 32 | `isBinaryThinking` | Binary thinking on/off | | 33 | `supportsXHighThinking` | `xhigh` reasoning support | | 34 | `supportsAdaptiveThinking` | Adaptive thinking support | - | 35 | `resolveDefaultThinkingLevel` | Default `/think` policy | - | 36 | `isModernModelRef` | Live/smoke model matching | - | 37 | `prepareRuntimeAuth` | Token exchange before inference | - | 38 | `resolveUsageAuth` | Custom usage credential parsing | - | 39 | `fetchUsageSnapshot` | Custom usage endpoint | - | 40 | `createEmbeddingProvider` | Provider-owned embedding adapter for memory/search | - | 41 | `buildReplayPolicy` | Custom transcript replay/compaction policy | - | 42 | `sanitizeReplayHistory` | Provider-specific replay rewrites after generic cleanup | - | 43 | `validateReplayTurns` | Strict replay-turn validation before the embedded runner | - | 44 | `onModelSelected` | Post-selection callback (e.g. telemetry) | + | 35 | `supportsMaxThinking` | `max` reasoning support | + | 36 | `resolveDefaultThinkingLevel` | Default `/think` policy | + | 37 | `isModernModelRef` | Live/smoke model matching | + | 38 | `prepareRuntimeAuth` | Token exchange before inference | + | 39 | `resolveUsageAuth` | Custom usage credential parsing | + | 40 | `fetchUsageSnapshot` | Custom usage endpoint | + | 41 | `createEmbeddingProvider` | Provider-owned embedding adapter for memory/search | + | 42 | `buildReplayPolicy` | Custom transcript replay/compaction policy | + | 43 | `sanitizeReplayHistory` | Provider-specific replay rewrites after generic cleanup | + | 44 | `validateReplayTurns` | Strict replay-turn validation before the embedded runner | + | 45 | `onModelSelected` | Post-selection callback (e.g. telemetry) | Prompt tuning note: diff --git a/docs/providers/mistral.md b/docs/providers/mistral.md index f4241ad14b1..81211a49e34 100644 --- a/docs/providers/mistral.md +++ b/docs/providers/mistral.md @@ -95,7 +95,7 @@ The media transcription path uses `/v1/audio/transcriptions`. The default audio | OpenClaw thinking level | Mistral `reasoning_effort` | | ------------------------------------------------ | -------------------------- | | **off** / **minimal** | `none` | - | **low** / **medium** / **high** / **xhigh** / **adaptive** | `high` | + | **low** / **medium** / **high** / **xhigh** / **adaptive** / **max** | `high` | Other bundled Mistral catalog models do not use this parameter. Keep using `magistral-*` models when you want Mistral's native reasoning-first behavior. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 414914e201b..c6cab59ddd9 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -10,20 +10,23 @@ title: "Thinking Levels" ## What it does - Inline directive in any inbound body: `/t `, `/think:`, or `/thinking `. -- Levels (aliases): `off | minimal | low | medium | high | xhigh | adaptive` +- Levels (aliases): `off | minimal | low | medium | high | xhigh | adaptive | max` - minimal → “think” - low → “think hard” - medium → “think harder” - high → “ultrathink” (max budget) - xhigh → “ultrathink+” (GPT-5.2 + Codex models and Anthropic Claude Opus 4.7 effort) - adaptive → provider-managed adaptive thinking (supported for Claude 4.6 on Anthropic/Bedrock and Anthropic Claude Opus 4.7) + - max → provider max reasoning (currently Anthropic Claude Opus 4.7) - `x-high`, `x_high`, `extra-high`, `extra high`, and `extra_high` map to `xhigh`. - - `highest`, `max` map to `high`. + - `highest` maps to `high`. - Provider notes: - `adaptive` is only advertised in native command menus and pickers for providers/models that declare adaptive thinking support. It remains accepted as a typed directive for compatibility with existing configs and aliases. + - `max` is only advertised in native command menus and pickers for providers/models that declare max thinking support. Existing stored `max` settings are remapped to the largest supported level for the selected model when the model does not support `max`. - Anthropic Claude 4.6 models default to `adaptive` when no explicit thinking level is set. - Anthropic Claude Opus 4.7 does not default to adaptive thinking. Its API effort default remains provider-owned unless you explicitly set a thinking level. - Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting. + - Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path. - OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value. - MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format. - Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`). @@ -112,6 +115,6 @@ title: "Thinking Levels" - The picker stays provider-aware: - most providers show `off | minimal | low | medium | high` - Anthropic/Bedrock Claude 4.6 shows `off | minimal | low | medium | high | adaptive` - - Anthropic Claude Opus 4.7 shows `off | minimal | low | medium | high | xhigh | adaptive` + - Anthropic Claude Opus 4.7 shows `off | minimal | low | medium | high | xhigh | adaptive | max` - Z.AI shows binary `off | on` - `/think:` still works and updates the same stored session level, so chat directives and the picker stay in sync. diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 73f333b2d7d..5cecfaf14e0 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -216,7 +216,8 @@ type ActiveMemoryThinkingLevel = | "medium" | "high" | "xhigh" - | "adaptive"; + | "adaptive" + | "max"; type ActiveMemoryPromptStyle = | "balanced" | "strict" @@ -698,7 +699,8 @@ function resolveThinkingLevel(thinking: unknown): ActiveMemoryThinkingLevel { thinking === "medium" || thinking === "high" || thinking === "xhigh" || - thinking === "adaptive" + thinking === "adaptive" || + thinking === "max" ) { return thinking; } diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index 25949a0ddfd..fe4453c9044 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -218,6 +218,18 @@ describe("anthropic provider replay hooks", () => { modelId: "claude-opus-4-6", } as never), ).toBe(false); + expect( + provider.supportsMaxThinking?.({ + provider: "anthropic", + modelId: "claude-opus-4-7", + } as never), + ).toBe(true); + expect( + provider.supportsMaxThinking?.({ + provider: "anthropic", + modelId: "claude-opus-4-6", + } as never), + ).toBe(false); expect( provider.supportsAdaptiveThinking?.({ provider: "anthropic", diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index d9277a5861b..e695f2870cd 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -495,6 +495,7 @@ export function buildAnthropicProvider(): ProviderPlugin { resolveReasoningOutputMode: () => "native", supportsXHighThinking: ({ modelId }) => isAnthropicOpus47Model(modelId), supportsAdaptiveThinking: ({ modelId }) => supportsAnthropicAdaptiveThinking(modelId), + supportsMaxThinking: ({ modelId }) => isAnthropicOpus47Model(modelId), wrapStreamFn: wrapAnthropicProviderStream, resolveDefaultThinkingLevel: ({ modelId }) => isAnthropicOpus47Model(modelId) diff --git a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts index efae65bf36a..ea9013a5b77 100644 --- a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts +++ b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts @@ -37,6 +37,7 @@ const providerThinkingMocks = vi.hoisted(() => ({ resolveProviderAdaptiveThinking: vi.fn(), resolveProviderBinaryThinking: vi.fn(), resolveProviderDefaultThinkingLevel: vi.fn(), + resolveProviderMaxThinking: vi.fn(), resolveProviderXHighThinking: vi.fn(), })); const buildModelsProviderDataMock = vi.hoisted(() => vi.fn()); @@ -131,6 +132,7 @@ async function loadDiscordThinkAutocompleteModulesForTest() { resolveProviderAdaptiveThinking: providerThinkingMocks.resolveProviderAdaptiveThinking, resolveProviderBinaryThinking: providerThinkingMocks.resolveProviderBinaryThinking, resolveProviderDefaultThinkingLevel: providerThinkingMocks.resolveProviderDefaultThinkingLevel, + resolveProviderMaxThinking: providerThinkingMocks.resolveProviderMaxThinking, resolveProviderXHighThinking: providerThinkingMocks.resolveProviderXHighThinking, })); const commandAuth = await import("openclaw/plugin-sdk/command-auth"); @@ -147,6 +149,7 @@ describe("discord native /think autocomplete", () => { providerThinkingMocks.resolveProviderBinaryThinking.mockReturnValue(undefined); providerThinkingMocks.resolveProviderAdaptiveThinking.mockReturnValue(undefined); providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined); + providerThinkingMocks.resolveProviderMaxThinking.mockReturnValue(undefined); providerThinkingMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) => provider === "openai-codex" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId) ? true @@ -177,6 +180,10 @@ describe("discord native /think autocomplete", () => { providerThinkingMocks.resolveProviderAdaptiveThinking.mockReturnValue(undefined); providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReset(); providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined); + providerThinkingMocks.resolveProviderMaxThinking.mockReset(); + providerThinkingMocks.resolveProviderMaxThinking.mockImplementation(({ provider, context }) => + provider === "anthropic" && context.modelId === "claude-opus-4-7" ? true : undefined, + ); providerThinkingMocks.resolveProviderXHighThinking.mockReset(); providerThinkingMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) => provider === "openai-codex" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId) @@ -263,9 +270,65 @@ describe("discord native /think autocomplete", () => { }); const values = choices.map((choice) => choice.value); expect(values).toContain("xhigh"); + expect(values).not.toContain("max"); expect(values).not.toContain("adaptive"); }); + it("includes max only for provider-advertised models", async () => { + fs.writeFileSync( + STORE_PATH, + JSON.stringify({ + [SESSION_KEY]: { + updatedAt: Date.now(), + providerOverride: "anthropic", + modelOverride: "claude-opus-4-7", + }, + }), + "utf8", + ); + const cfg = createConfig(); + resolveConfiguredBindingRouteMock.mockImplementation(createConfiguredRouteResult); + const interaction = { + options: { + getFocused: () => ({ value: "ma" }), + }, + respond: async (_choices: Array<{ name: string; value: string }>) => {}, + rawData: { + member: { roles: [] }, + }, + channel: { id: "C1", type: ChannelType.GuildText }, + user: { id: "U1" }, + guild: { id: "G1" }, + client: {}, + } as unknown as AutocompleteInteraction & { + respond: (choices: Array<{ name: string; value: string }>) => Promise; + }; + + const context = await resolveDiscordNativeChoiceContext({ + interaction, + cfg, + accountId: "default", + threadBindings: createNoopThreadBindingManager("default"), + }); + const command = findCommandByNativeName("think", "discord"); + const levelArg = command?.args?.find((entry) => entry.name === "level"); + expect(command).toBeTruthy(); + expect(levelArg).toBeTruthy(); + if (!command || !levelArg) { + return; + } + + const choices = resolveCommandArgChoices({ + command, + arg: levelArg, + cfg, + provider: context?.provider, + model: context?.model, + }); + const values = choices.map((choice) => choice.value); + expect(values).toContain("max"); + }); + it("falls back when a configured binding is unavailable", async () => { const cfg = createConfig(); resolveConfiguredBindingRouteMock.mockImplementation(createConfiguredRouteResult); diff --git a/extensions/kimi-coding/stream.ts b/extensions/kimi-coding/stream.ts index a1f495d45e3..6fa8ec30c1e 100644 --- a/extensions/kimi-coding/stream.ts +++ b/extensions/kimi-coding/stream.ts @@ -21,7 +21,15 @@ type KimiToolCallBlock = { }; type KimiThinkingType = "enabled" | "disabled"; -type KimiThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; +type KimiThinkingLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "adaptive" + | "max"; function normalizeKimiThinkingType(value: unknown): KimiThinkingType | undefined { if (typeof value === "boolean") { diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 01e17ddc74d..4e08a6f0245 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -7,6 +7,7 @@ import { formatXHighModelHint, normalizeThinkLevel, resolvePreferredOpenClawTmpDir, + resolveSupportedThinkingLevel, supportsXHighThinking, } from "../api.js"; import type { OpenClawPluginApi } from "../api.js"; @@ -61,7 +62,7 @@ type LlmTaskParams = { }; const INVALID_THINKING_LEVELS_HINT = - "off, minimal, low, medium, high, adaptive, and xhigh where supported"; + "off, minimal, low, medium, high, adaptive, xhigh where supported, and max where supported"; export function createLlmTaskTool(api: OpenClawPluginApi) { return { @@ -143,9 +144,17 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { `Invalid thinking level "${thinkingRaw}". Use one of: ${INVALID_THINKING_LEVELS_HINT}.`, ); } + let resolvedThinkLevel = thinkLevel; if (thinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) { throw new Error(`Thinking level "xhigh" is only supported for ${formatXHighModelHint()}.`); } + if (thinkLevel === "max") { + resolvedThinkLevel = resolveSupportedThinkingLevel({ + provider, + model, + level: thinkLevel, + }); + } const timeoutMs = (typeof params.timeoutMs === "number" && params.timeoutMs > 0 @@ -204,7 +213,7 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { model, authProfileId, authProfileIdSource: authProfileId ? "user" : "auto", - thinkLevel, + thinkLevel: resolvedThinkLevel, streamParams, disableTools: true, }); diff --git a/extensions/mistral/api.ts b/extensions/mistral/api.ts index 068a53dbbbc..529f8776f97 100644 --- a/extensions/mistral/api.ts +++ b/extensions/mistral/api.ts @@ -28,6 +28,7 @@ export const MISTRAL_SMALL_LATEST_REASONING_EFFORT_MAP: Record = high: "high", xhigh: "high", adaptive: "high", + max: "high", }; export const MISTRAL_SMALL_LATEST_ID = "mistral-small-latest"; diff --git a/extensions/ollama/src/stream.ts b/extensions/ollama/src/stream.ts index e0d36013362..aea126f6b53 100644 --- a/extensions/ollama/src/stream.ts +++ b/extensions/ollama/src/stream.ts @@ -202,7 +202,7 @@ export function createConfiguredOllamaCompatStreamWrapper( if (ctx.thinkingLevel === "off") { streamFn = createOllamaThinkingWrapper(streamFn, false); } else if (ctx.thinkingLevel) { - // Any non-off ThinkLevel (minimal, low, medium, high, xhigh, adaptive) + // Any non-off ThinkLevel (minimal, low, medium, high, xhigh, adaptive, max) // should enable Ollama's native thinking mode. streamFn = createOllamaThinkingWrapper(streamFn, true); } diff --git a/extensions/qa-lab/src/cli.runtime.ts b/extensions/qa-lab/src/cli.runtime.ts index 7d2de4464cc..859620fdd1a 100644 --- a/extensions/qa-lab/src/cli.runtime.ts +++ b/extensions/qa-lab/src/cli.runtime.ts @@ -73,7 +73,9 @@ function parseQaThinkingLevel( } const normalized = normalizeQaThinkingLevel(value); if (!normalized) { - throw new Error(`${label} must be one of off, minimal, low, medium, high, xhigh, adaptive`); + throw new Error( + `${label} must be one of off, minimal, low, medium, high, xhigh, adaptive, max`, + ); } return normalized; } @@ -238,7 +240,7 @@ function parseQaModelSpecs(label: string, entries: readonly string[] | undefined const thinkingDefault = parseQaThinkingLevel(`${label} thinking`, value); if (!thinkingDefault) { throw new Error( - `${label} thinking must be one of off, minimal, low, medium, high, xhigh, adaptive`, + `${label} thinking must be one of off, minimal, low, medium, high, xhigh, adaptive, max`, ); } options.thinkingDefault = thinkingDefault; diff --git a/extensions/qa-lab/src/cli.ts b/extensions/qa-lab/src/cli.ts index e08d759ecc0..3b015272645 100644 --- a/extensions/qa-lab/src/cli.ts +++ b/extensions/qa-lab/src/cli.ts @@ -339,7 +339,7 @@ export function registerQaLabCli(program: Command) { .option("--fast", "Enable provider fast mode for all candidate runs") .option( "--thinking ", - "Candidate thinking default: off|minimal|low|medium|high|xhigh|adaptive", + "Candidate thinking default: off|minimal|low|medium|high|xhigh|adaptive|max", ) .option( "--model-thinking ", diff --git a/extensions/qa-lab/src/qa-thinking.ts b/extensions/qa-lab/src/qa-thinking.ts index 12e109a7ca8..74527ebc089 100644 --- a/extensions/qa-lab/src/qa-thinking.ts +++ b/extensions/qa-lab/src/qa-thinking.ts @@ -1,4 +1,12 @@ -export type QaThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; +export type QaThinkingLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "adaptive" + | "max"; export function normalizeQaThinkingLevel(input: unknown): QaThinkingLevel | undefined { const value = typeof input === "string" ? input.trim().toLowerCase() : ""; @@ -24,5 +32,8 @@ export function normalizeQaThinkingLevel(input: unknown): QaThinkingLevel | unde if (collapsed === "adaptive" || collapsed === "auto") { return "adaptive"; } + if (collapsed === "max") { + return "max"; + } return undefined; } diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index fbd3d739e9e..e2692266239 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -90,6 +90,7 @@ vi.mock("../auto-reply/thinking.js", () => ({ formatXHighModelHint: () => "model-x", normalizeThinkLevel: (v?: string) => v || undefined, normalizeVerboseLevel: (v?: string) => v || undefined, + resolveSupportedThinkingLevel: ({ level }: { level?: string }) => level, supportsXHighThinking: () => false, })); diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index b405dd7b6f8..de53aa580ab 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -3,6 +3,7 @@ import { formatXHighModelHint, normalizeThinkLevel, normalizeVerboseLevel, + resolveSupportedThinkingLevel, supportsXHighThinking, type VerboseLevel, } from "../auto-reply/thinking.js"; @@ -800,6 +801,27 @@ async function agentCommandInternal( }); } } + if (resolvedThinkLevel === "max") { + const fallbackThinkLevel = resolveSupportedThinkingLevel({ + provider, + model, + level: resolvedThinkLevel, + }); + if (fallbackThinkLevel !== resolvedThinkLevel) { + resolvedThinkLevel = fallbackThinkLevel; + if (sessionEntry && sessionStore && sessionKey && sessionEntry.thinkingLevel === "max") { + const entry = sessionEntry; + entry.thinkingLevel = fallbackThinkLevel; + entry.updatedAt = Date.now(); + await persistSessionEntry({ + sessionStore, + sessionKey, + storePath, + entry, + }); + } + } + } const { resolveSessionTranscriptFile } = await loadTranscriptResolveRuntime(); let sessionFile: string | undefined; if (sessionStore && sessionKey) { diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index f36ad7af3db..73c4f8734d4 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -43,7 +43,15 @@ import { export type { ModelAliasIndex, ModelRef, ModelRefStatus }; -export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; +export type ThinkLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "adaptive" + | "max"; export { buildConfiguredAllowlistKeys, diff --git a/src/agents/model-thinking-default.ts b/src/agents/model-thinking-default.ts index 35db5ca1935..d06e91c82fa 100644 --- a/src/agents/model-thinking-default.ts +++ b/src/agents/model-thinking-default.ts @@ -8,7 +8,7 @@ import type { ModelCatalogEntry } from "./model-catalog.types.js"; import { legacyModelKey, modelKey, normalizeProviderId } from "./model-selection-normalize.js"; import { normalizeModelSelection } from "./model-selection-resolve.js"; -type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; +type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | "max"; export function resolveThinkingDefault(params: { cfg: OpenClawConfig; @@ -46,7 +46,8 @@ export function resolveThinkingDefault(params: { perModelThinking === "medium" || perModelThinking === "high" || perModelThinking === "xhigh" || - perModelThinking === "adaptive" + perModelThinking === "adaptive" || + perModelThinking === "max" ) { return perModelThinking; } diff --git a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts index 32f7faf4d0e..cfb8f8d3629 100644 --- a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts @@ -26,6 +26,9 @@ function mapThinkingLevelToOpenRouterReasoningEffort( if (thinkingLevel === "adaptive") { return "medium"; } + if (thinkingLevel === "max") { + return "xhigh"; + } return thinkingLevel; } diff --git a/src/agents/pi-embedded-runner/utils.ts b/src/agents/pi-embedded-runner/utils.ts index f6ac79ecd47..e55e6103a11 100644 --- a/src/agents/pi-embedded-runner/utils.ts +++ b/src/agents/pi-embedded-runner/utils.ts @@ -6,6 +6,9 @@ export function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { if (!level) { return "off"; } + if (level === "max") { + return "xhigh"; + } // "adaptive" maps to "medium" at the pi-agent-core layer. The Pi SDK // provider then translates this to `thinking.type: "adaptive"` with // `output_config.effort: "medium"` for models that support it (Opus 4.6, diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 9c755f4d0a0..ce2e4be35f5 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -8,7 +8,12 @@ import { enqueueSystemEvent } from "../../infra/system-events.js"; import { applyTraceOverride, applyVerboseOverride } from "../../sessions/level-overrides.js"; import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; -import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js"; +import { + formatThinkingLevels, + formatXHighModelHint, + resolveSupportedThinkingLevel, + supportsXHighThinking, +} from "../thinking.js"; import type { ReplyPayload } from "../types.js"; import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js"; import { maybeHandleModelDirectiveInfo } from "./directive-handling.model.js"; @@ -294,13 +299,33 @@ export async function handleDirectiveOnly( }; } + const resolvedDirectiveThinkLevel = + directives.hasThinkDirective && directives.thinkLevel + ? resolveSupportedThinkingLevel({ + provider: resolvedProvider, + model: resolvedModel, + level: directives.thinkLevel, + }) + : directives.thinkLevel; const nextThinkLevel = directives.hasThinkDirective - ? directives.thinkLevel + ? resolvedDirectiveThinkLevel : ((sessionEntry?.thinkingLevel as ThinkLevel | undefined) ?? currentThinkLevel); const shouldDowngradeXHigh = !directives.hasThinkDirective && nextThinkLevel === "xhigh" && !supportsXHighThinking(resolvedProvider, resolvedModel); + const remappedMaxThinkLevel = + nextThinkLevel === "max" + ? resolveSupportedThinkingLevel({ + provider: resolvedProvider, + model: resolvedModel, + level: nextThinkLevel, + }) + : undefined; + const shouldRemapMax = + nextThinkLevel === "max" && + remappedMaxThinkLevel !== undefined && + remappedMaxThinkLevel !== "max"; const prevElevatedLevel = currentElevatedLevel ?? @@ -326,7 +351,8 @@ export async function handleDirectiveOnly( (directives.hasExecDirective && directives.hasExecOptions && allowInternalExecPersistence) || Boolean(modelSelection) || directives.hasQueueDirective || - shouldDowngradeXHigh; + shouldDowngradeXHigh || + shouldRemapMax; const fastModeChanged = directives.hasFastDirective && directives.fastMode !== undefined && @@ -334,8 +360,8 @@ export async function handleDirectiveOnly( let reasoningChanged = directives.hasReasoningDirective && directives.reasoningLevel !== undefined; if (shouldPersistSessionEntry) { - if (directives.hasThinkDirective && directives.thinkLevel) { - sessionEntry.thinkingLevel = directives.thinkLevel; + if (directives.hasThinkDirective && directives.thinkLevel && resolvedDirectiveThinkLevel) { + sessionEntry.thinkingLevel = resolvedDirectiveThinkLevel; } if (directives.hasFastDirective && directives.fastMode !== undefined) { sessionEntry.fastMode = directives.fastMode; @@ -343,6 +369,9 @@ export async function handleDirectiveOnly( if (shouldDowngradeXHigh) { sessionEntry.thinkingLevel = "high"; } + if (shouldRemapMax && remappedMaxThinkLevel) { + sessionEntry.thinkingLevel = remappedMaxThinkLevel; + } if ( directives.hasVerboseDirective && directives.verboseLevel && @@ -452,11 +481,17 @@ export async function handleDirectiveOnly( const parts: string[] = []; if (directives.hasThinkDirective && directives.thinkLevel) { + const displayedThinkLevel = resolvedDirectiveThinkLevel ?? directives.thinkLevel; parts.push( - directives.thinkLevel === "off" + displayedThinkLevel === "off" ? "Thinking disabled." - : `Thinking level set to ${directives.thinkLevel}.`, + : `Thinking level set to ${displayedThinkLevel}.`, ); + if (directives.thinkLevel === "max" && displayedThinkLevel !== "max") { + parts.push( + `max not supported for ${resolvedProvider}/${resolvedModel}; using ${displayedThinkLevel}.`, + ); + } } if (directives.hasFastDirective && directives.fastMode !== undefined) { parts.push( @@ -543,6 +578,11 @@ export async function handleDirectiveOnly( `Thinking level set to high (xhigh not supported for ${resolvedProvider}/${resolvedModel}).`, ); } + if (!directives.hasThinkDirective && shouldRemapMax && remappedMaxThinkLevel) { + parts.push( + `Thinking level set to ${remappedMaxThinkLevel} (max not supported for ${resolvedProvider}/${resolvedModel}).`, + ); + } if (modelSelection) { const label = `${modelSelection.provider}/${modelSelection.model}`; const labelWithAlias = modelSelection.alias ? `${modelSelection.alias} (${label})` : label; diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index a58d1a5b988..8a4114692c6 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -30,6 +30,7 @@ import { formatXHighModelHint, normalizeThinkLevel, type ReasoningLevel, + resolveSupportedThinkingLevel, supportsXHighThinking, type ThinkLevel, type VerboseLevel, @@ -413,7 +414,10 @@ export async function runPreparedReply( if (!resolvedThinkLevel && prefixedBodyBase) { const parts = prefixedBodyBase.split(/\s+/); const maybeLevel = normalizeThinkLevel(parts[0]); - if (maybeLevel && (maybeLevel !== "xhigh" || supportsXHighThinking(provider, model))) { + if ( + maybeLevel && + (maybeLevel === "max" || maybeLevel !== "xhigh" || supportsXHighThinking(provider, model)) + ) { resolvedThinkLevel = maybeLevel; prefixedBodyBase = parts.slice(1).join(" ").trim(); } @@ -483,6 +487,27 @@ export async function runPreparedReply( if (!resolvedThinkLevel) { resolvedThinkLevel = await modelState.resolveDefaultThinkingLevel(); } + if (resolvedThinkLevel === "max") { + const fallbackThinkLevel = resolveSupportedThinkingLevel({ + provider, + model, + level: resolvedThinkLevel, + }); + if (fallbackThinkLevel !== resolvedThinkLevel) { + resolvedThinkLevel = fallbackThinkLevel; + if (sessionEntry && sessionStore && sessionKey && sessionEntry.thinkingLevel === "max") { + sessionEntry.thinkingLevel = fallbackThinkLevel; + sessionEntry.updatedAt = Date.now(); + sessionStore[sessionKey] = sessionEntry; + if (storePath) { + const { updateSessionStore } = await loadSessionStoreRuntime(); + await updateSessionStore(storePath, (store) => { + store[sessionKey] = sessionEntry; + }); + } + } + } + } if (resolvedThinkLevel === "xhigh" && !supportsXHighThinking(provider, model)) { const explicitThink = directives.hasThinkDirective && directives.thinkLevel !== undefined; if (explicitThink) { diff --git a/src/auto-reply/thinking.shared.ts b/src/auto-reply/thinking.shared.ts index 3de1f05bf2d..c9f81357623 100644 --- a/src/auto-reply/thinking.shared.ts +++ b/src/auto-reply/thinking.shared.ts @@ -6,7 +6,15 @@ import { export { normalizeFastMode }; -export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; +export type ThinkLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "adaptive" + | "max"; export type VerboseLevel = "off" | "on" | "full"; export type TraceLevel = "off" | "on" | "raw"; export type NoticeLevel = "off" | "on" | "full"; @@ -38,6 +46,9 @@ export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined if (collapsed === "adaptive" || collapsed === "auto") { return "adaptive"; } + if (collapsed === "max") { + return "max"; + } if (collapsed === "xhigh" || collapsed === "extrahigh") { return "xhigh"; } @@ -56,9 +67,7 @@ export function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) { return "medium"; } - if ( - ["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest", "max"].includes(key) - ) { + if (["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest"].includes(key)) { return "high"; } if (["think"].includes(key)) { @@ -93,6 +102,10 @@ export function formatXHighModelHint(): string { return "provider models that advertise xhigh reasoning"; } +export function formatMaxModelHint(): string { + return "provider models that advertise max reasoning"; +} + export function resolveThinkingDefaultForModel(params: { provider: string; model: string; diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index 3f99ecf4cff..1decc557815 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -4,6 +4,7 @@ const providerRuntimeMocks = vi.hoisted(() => ({ resolveProviderAdaptiveThinking: vi.fn(), resolveProviderBinaryThinking: vi.fn(), resolveProviderDefaultThinkingLevel: vi.fn(), + resolveProviderMaxThinking: vi.fn(), resolveProviderXHighThinking: vi.fn(), })); @@ -19,6 +20,7 @@ async function loadFreshThinkingModuleForTest() { resolveProviderAdaptiveThinking: providerRuntimeMocks.resolveProviderAdaptiveThinking, resolveProviderBinaryThinking: providerRuntimeMocks.resolveProviderBinaryThinking, resolveProviderDefaultThinkingLevel: providerRuntimeMocks.resolveProviderDefaultThinkingLevel, + resolveProviderMaxThinking: providerRuntimeMocks.resolveProviderMaxThinking, resolveProviderXHighThinking: providerRuntimeMocks.resolveProviderXHighThinking, })); return await import("./thinking.js"); @@ -31,6 +33,8 @@ beforeEach(async () => { providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(undefined); providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReset(); providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined); + providerRuntimeMocks.resolveProviderMaxThinking.mockReset(); + providerRuntimeMocks.resolveProviderMaxThinking.mockReturnValue(undefined); providerRuntimeMocks.resolveProviderXHighThinking.mockReset(); providerRuntimeMocks.resolveProviderXHighThinking.mockReturnValue(undefined); @@ -76,6 +80,11 @@ describe("normalizeThinkLevel", () => { expect(normalizeThinkLevel("auto")).toBe("adaptive"); expect(normalizeThinkLevel("Adaptive")).toBe("adaptive"); }); + + it("accepts max as its own level", () => { + expect(normalizeThinkLevel("max")).toBe("max"); + expect(normalizeThinkLevel("MAX")).toBe("max"); + }); }); describe("listThinkingLevels", () => { @@ -123,6 +132,16 @@ describe("listThinkingLevels", () => { expect(listThinkingLevels("demo", "demo-model")).toContain("adaptive"); }); + it("uses provider runtime hooks for max support", () => { + providerRuntimeMocks.resolveProviderMaxThinking.mockReturnValue(true); + + expect(listThinkingLevels("demo", "demo-model")).toContain("max"); + }); + + it("does not include max without provider support", () => { + expect(listThinkingLevels("openai", "gpt-5.4")).not.toContain("max"); + }); + it("does not include adaptive without provider support", () => { expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain("adaptive"); expect(listThinkingLevels("openai", "gpt-5.4")).not.toContain("adaptive"); diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index 9d760fe7623..4f593913796 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -32,6 +32,7 @@ import { resolveProviderAdaptiveThinking, resolveProviderBinaryThinking, resolveProviderDefaultThinkingLevel, + resolveProviderMaxThinking, resolveProviderXHighThinking, } from "../plugins/provider-thinking.js"; import { @@ -101,6 +102,26 @@ export function supportsAdaptiveThinking(provider?: string | null, model?: strin return pluginDecision === true; } +export function supportsMaxThinking(provider?: string | null, model?: string | null): boolean { + const modelKey = normalizeOptionalLowercaseString(model); + if (!modelKey) { + return false; + } + const providerRaw = normalizeOptionalString(provider); + const providerKey = providerRaw ? normalizeProviderId(providerRaw) : ""; + if (!providerKey) { + return false; + } + const pluginDecision = resolveProviderMaxThinking({ + provider: providerKey, + context: { + provider: providerKey, + modelId: modelKey, + }, + }); + return pluginDecision === true; +} + export function listThinkingLevels(provider?: string | null, model?: string | null): ThinkLevel[] { const levels = listThinkingLevelsFallback(provider, model); if (supportsXHighThinking(provider, model)) { @@ -109,6 +130,9 @@ export function listThinkingLevels(provider?: string | null, model?: string | nu if (supportsAdaptiveThinking(provider, model)) { levels.push("adaptive"); } + if (supportsMaxThinking(provider, model)) { + levels.push("max"); + } return levels; } @@ -149,3 +173,35 @@ export function resolveThinkingDefaultForModel(params: { } return resolveThinkingDefaultForModelFallback(params); } + +export function resolveLargestSupportedThinkingLevel( + provider?: string | null, + model?: string | null, +): ThinkLevel { + if (isBinaryThinkingProvider(provider, model)) { + return "low"; + } + if (supportsMaxThinking(provider, model)) { + return "max"; + } + if (supportsXHighThinking(provider, model)) { + return "xhigh"; + } + if (supportsAdaptiveThinking(provider, model)) { + return "adaptive"; + } + return "high"; +} + +export function resolveSupportedThinkingLevel(params: { + provider?: string | null; + model?: string | null; + level: ThinkLevel; +}): ThinkLevel { + if (params.level !== "max") { + return params.level; + } + return supportsMaxThinking(params.provider, params.model) + ? "max" + : resolveLargestSupportedThinkingLevel(params.provider, params.model); +} diff --git a/src/config/schema.base.generated.ts b/src/config/schema.base.generated.ts index 657fbe2b6a4..59ad7eeb339 100644 --- a/src/config/schema.base.generated.ts +++ b/src/config/schema.base.generated.ts @@ -4773,6 +4773,10 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { type: "string", const: "adaptive", }, + { + type: "string", + const: "max", + }, ], }, verboseDefault: { @@ -5743,7 +5747,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = { }, thinkingDefault: { type: "string", - enum: ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"], + enum: ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive", "max"], title: "Agent Thinking Default", description: "Optional per-agent default thinking level. Overrides agents.defaults.thinkingDefault for this agent when no per-message or session override is set.", diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index c7ef65f4c3c..04c57f90658 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -281,7 +281,7 @@ export type AgentDefaultsConfig = { /** Vector memory search configuration (per-agent overrides supported). */ memorySearch?: MemorySearchConfig; /** Default thinking level when no /think directive is present. */ - thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; + thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | "max"; /** Default verbose level when no /verbose directive is present. */ verboseDefault?: "off" | "on" | "full"; /** Default elevated level when no /elevated directive is present. */ diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index ddfa362b617..23b69d93c2a 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -79,7 +79,7 @@ export type AgentConfig = { embeddedHarness?: AgentEmbeddedHarnessConfig; model?: AgentModelConfig; /** Optional per-agent default thinking level (overrides agents.defaults.thinkingDefault). */ - thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive"; + thinkingDefault?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | "max"; /** Optional per-agent default verbosity level. */ verboseDefault?: "off" | "on" | "full"; /** Optional per-agent default reasoning visibility. */ diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 28370af8784..165ae53427d 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -217,6 +217,7 @@ export const AgentDefaultsSchema = z z.literal("high"), z.literal("xhigh"), z.literal("adaptive"), + z.literal("max"), ]) .optional(), verboseDefault: z.union([z.literal("off"), z.literal("on"), z.literal("full")]).optional(), diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index c620c46c800..5401abeb342 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -819,7 +819,7 @@ export const AgentEntrySchema = z embeddedHarness: AgentEmbeddedHarnessSchema, model: AgentModelSchema.optional(), thinkingDefault: z - .enum(["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"]) + .enum(["off", "minimal", "low", "medium", "high", "xhigh", "adaptive", "max"]) .optional(), reasoningDefault: z.enum(["on", "off", "stream"]).optional(), fastModeDefault: z.boolean().optional(), diff --git a/src/cron/isolated-agent/run.runtime.ts b/src/cron/isolated-agent/run.runtime.ts index 471034d1900..8d10c0842fd 100644 --- a/src/cron/isolated-agent/run.runtime.ts +++ b/src/cron/isolated-agent/run.runtime.ts @@ -12,7 +12,11 @@ export { resolveThinkingDefault } from "../../agents/model-thinking-default.js"; export { resolveAgentTimeoutMs } from "../../agents/timeout.js"; export { deriveSessionTotalTokens, hasNonzeroUsage } from "../../agents/usage.js"; export { DEFAULT_IDENTITY_FILENAME, ensureAgentWorkspace } from "../../agents/workspace.js"; -export { normalizeThinkLevel, supportsXHighThinking } from "../../auto-reply/thinking.js"; +export { + normalizeThinkLevel, + resolveSupportedThinkingLevel, + supportsXHighThinking, +} from "../../auto-reply/thinking.js"; export { resolveSessionTranscriptPath } from "../../config/sessions/paths.js"; export { setSessionRuntimeModel } from "../../config/sessions/types.js"; export { logWarn } from "../../logger.js"; diff --git a/src/cron/isolated-agent/run.test-harness.ts b/src/cron/isolated-agent/run.test-harness.ts index eada1c3bf92..d9a048ab7e4 100644 --- a/src/cron/isolated-agent/run.test-harness.ts +++ b/src/cron/isolated-agent/run.test-harness.ts @@ -78,6 +78,7 @@ const hasNonzeroUsageMock = createMock(); const ensureAgentWorkspaceMock = createMock(); const normalizeThinkLevelMock = createMock(); const normalizeVerboseLevelMock = createMock(); +const resolveSupportedThinkingLevelMock = createMock(); const supportsXHighThinkingMock = createMock(); const resolveSessionTranscriptPathMock = createMock(); const setSessionRuntimeModelMock = createMock(); @@ -109,6 +110,7 @@ vi.mock("./run.runtime.js", () => ({ DEFAULT_IDENTITY_FILENAME: "IDENTITY.md", ensureAgentWorkspace: ensureAgentWorkspaceMock, normalizeThinkLevel: normalizeThinkLevelMock, + resolveSupportedThinkingLevel: resolveSupportedThinkingLevelMock, supportsXHighThinking: supportsXHighThinkingMock, resolveSessionTranscriptPath: resolveSessionTranscriptPathMock, setSessionRuntimeModel: setSessionRuntimeModelMock, @@ -306,6 +308,7 @@ function resetRunConfigMocks(): void { hasNonzeroUsageMock.mockReturnValue(true); ensureAgentWorkspaceMock.mockResolvedValue({ dir: "/tmp/workspace" }); normalizeThinkLevelMock.mockImplementation((value: unknown) => value); + resolveSupportedThinkingLevelMock.mockImplementation(({ level }: { level?: unknown }) => level); supportsXHighThinkingMock.mockReturnValue(false); buildSafeExternalPromptMock.mockImplementation( ({ message }: { message?: string }) => message ?? "", diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index db930248ccb..54a003b57c3 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -39,6 +39,7 @@ import { resolveCronStyleNow, resolveDefaultAgentId, resolveHookExternalContentSource, + resolveSupportedThinkingLevel, resolveSessionTranscriptPath, resolveThinkingDefault, setSessionRuntimeModel, @@ -388,6 +389,19 @@ async function prepareCronRunContext(params: { ); thinkLevel = "high"; } + if (thinkLevel === "max") { + const fallbackThinkLevel = resolveSupportedThinkingLevel({ + provider, + model, + level: thinkLevel, + }); + if (fallbackThinkLevel !== thinkLevel) { + logWarn( + `[cron:${input.job.id}] Thinking level "max" is not supported for ${provider}/${model}; downgrading to "${fallbackThinkLevel}".`, + ); + thinkLevel = fallbackThinkLevel; + } + } const timeoutMs = resolveAgentTimeoutMs({ cfg: cfgWithAgentDefaults, diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index 66b821172d1..ad08c477164 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -15,6 +15,7 @@ import { normalizeReasoningLevel, normalizeThinkLevel, normalizeUsageDisplay, + resolveSupportedThinkingLevel, supportsXHighThinking, } from "../auto-reply/thinking.js"; import type { SessionEntry } from "../config/sessions.js"; @@ -444,6 +445,15 @@ export async function applySessionsPatchToStore(params: { next.thinkingLevel = "high"; } } + if (next.thinkingLevel === "max") { + const effectiveProvider = next.providerOverride ?? resolvedDefault.provider; + const effectiveModel = next.modelOverride ?? resolvedDefault.model; + next.thinkingLevel = resolveSupportedThinkingLevel({ + provider: effectiveProvider, + model: effectiveModel, + level: next.thinkingLevel, + }); + } if ("sendPolicy" in patch) { const raw = patch.sendPolicy; diff --git a/src/plugin-sdk/llm-task.ts b/src/plugin-sdk/llm-task.ts index 01cd7e2342e..9bca014680b 100644 --- a/src/plugin-sdk/llm-task.ts +++ b/src/plugin-sdk/llm-task.ts @@ -7,6 +7,7 @@ export { formatThinkingLevels, formatXHighModelHint, normalizeThinkLevel, + resolveSupportedThinkingLevel, supportsXHighThinking, } from "../auto-reply/thinking.js"; export type { AnyAgentTool, OpenClawPluginApi } from "../plugins/types.js"; diff --git a/src/plugin-sdk/provider-stream-shared.ts b/src/plugin-sdk/provider-stream-shared.ts index 8feafccdc9c..18858e726a7 100644 --- a/src/plugin-sdk/provider-stream-shared.ts +++ b/src/plugin-sdk/provider-stream-shared.ts @@ -149,6 +149,7 @@ export type GoogleThinkingInputLevel = | "medium" | "adaptive" | "high" + | "max" | "xhigh"; // Gemini 2.5 Pro only works in thinking mode and rejects thinkingBudget=0 with @@ -188,6 +189,7 @@ export function resolveGoogleGemini3ThinkingLevel(params: { case "medium": case "adaptive": case "high": + case "max": case "xhigh": return "HIGH"; } @@ -209,6 +211,7 @@ export function resolveGoogleGemini3ThinkingLevel(params: { case "adaptive": return "MEDIUM"; case "high": + case "max": case "xhigh": return "HIGH"; } @@ -258,6 +261,7 @@ function mapThinkLevelToGemma4ThinkingLevel( case "medium": case "adaptive": case "high": + case "max": case "xhigh": return "HIGH"; default: diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 59490d5f832..595413db57f 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -650,6 +650,16 @@ export function resolveProviderAdaptiveThinking(params: { return resolveProviderRuntimePlugin(params)?.supportsAdaptiveThinking?.(params.context); } +export function resolveProviderMaxThinking(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + context: ProviderThinkingPolicyContext; +}) { + return resolveProviderRuntimePlugin(params)?.supportsMaxThinking?.(params.context); +} + export function resolveProviderDefaultThinkingLevel(params: { provider: string; config?: OpenClawConfig; diff --git a/src/plugins/provider-thinking.ts b/src/plugins/provider-thinking.ts index c88eea714ea..2833cf04a1c 100644 --- a/src/plugins/provider-thinking.ts +++ b/src/plugins/provider-thinking.ts @@ -9,10 +9,21 @@ type ThinkingProviderPlugin = { aliases?: string[]; isBinaryThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; supportsAdaptiveThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; + supportsMaxThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; supportsXHighThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; resolveDefaultThinkingLevel?: ( ctx: ProviderDefaultThinkingPolicyContext, - ) => "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | null | undefined; + ) => + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "adaptive" + | "max" + | null + | undefined; }; const PLUGIN_REGISTRY_STATE = Symbol.for("openclaw.pluginRegistryState"); @@ -68,6 +79,12 @@ export function resolveProviderAdaptiveThinking( return resolveActiveThinkingProvider(params.provider)?.supportsAdaptiveThinking?.(params.context); } +export function resolveProviderMaxThinking( + params: ThinkingHookParams, +) { + return resolveActiveThinkingProvider(params.provider)?.supportsMaxThinking?.(params.context); +} + export function resolveProviderDefaultThinkingLevel( params: ThinkingHookParams, ) { diff --git a/src/plugins/provider-thinking.types.ts b/src/plugins/provider-thinking.types.ts index 06574e9aa6a..128d1b291c6 100644 --- a/src/plugins/provider-thinking.types.ts +++ b/src/plugins/provider-thinking.types.ts @@ -3,7 +3,7 @@ * * Used by shared `/think`, ACP controls, and directive parsing to ask a * provider whether a model supports special reasoning UX such as adaptive, - * xhigh, or a binary on/off toggle. + * xhigh, max, or a binary on/off toggle. */ export type ProviderThinkingPolicyContext = { provider: string; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index f25ead05127..832002dfb87 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1413,6 +1413,12 @@ export type ProviderPlugin = { * Return true only for models that should expose the `adaptive` thinking level. */ supportsAdaptiveThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; + /** + * Provider-owned max thinking support. + * + * Return true only for models that should expose the `max` thinking level. + */ + supportsMaxThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; /** * Provider-owned default thinking level. * @@ -1421,7 +1427,17 @@ export type ProviderPlugin = { */ resolveDefaultThinkingLevel?: ( ctx: ProviderDefaultThinkingPolicyContext, - ) => "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive" | null | undefined; + ) => + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh" + | "adaptive" + | "max" + | null + | undefined; /** * Provider-owned system-prompt contribution. * diff --git a/ui/src/ui/chat/session-controls.ts b/ui/src/ui/chat/session-controls.ts index 42fcb41b679..e832fe4d8ff 100644 --- a/ui/src/ui/chat/session-controls.ts +++ b/ui/src/ui/chat/session-controls.ts @@ -160,7 +160,7 @@ function buildThinkingOptions( ); }; - for (const label of listThinkingLevelLabels(provider)) { + for (const label of listThinkingLevelLabels(provider, model)) { const normalized = normalizeThinkLevel(label) ?? normalizeLowercaseStringOrEmpty(label); addOption(normalized); } diff --git a/ui/src/ui/chat/slash-command-executor.node.test.ts b/ui/src/ui/chat/slash-command-executor.node.test.ts index a16d83c6adc..55ce77ad140 100644 --- a/ui/src/ui/chat/slash-command-executor.node.test.ts +++ b/ui/src/ui/chat/slash-command-executor.node.test.ts @@ -520,7 +520,7 @@ describe("executeSlashCommand directives", () => { ); expect(result.content).toBe( - "Current thinking level: low.\nOptions: off, minimal, low, medium, high, adaptive.", + "Current thinking level: low.\nOptions: off, minimal, low, medium, high.", ); expect(request).toHaveBeenNthCalledWith(1, "sessions.list", {}); expect(request).toHaveBeenNthCalledWith(2, "models.list", {}); diff --git a/ui/src/ui/chat/slash-command-executor.ts b/ui/src/ui/chat/slash-command-executor.ts index 9fb4b14a58e..f0e12a22ae1 100644 --- a/ui/src/ui/chat/slash-command-executor.ts +++ b/ui/src/ui/chat/slash-command-executor.ts @@ -258,7 +258,7 @@ async function executeThink( return { content: formatDirectiveOptions( `Current thinking level: ${resolveCurrentThinkingLevel(session, models)}.`, - formatThinkingLevels(session?.modelProvider), + formatThinkingLevels(session?.modelProvider, session?.model), ), }; } catch (err) { @@ -271,7 +271,7 @@ async function executeThink( try { const session = await loadCurrentSession(client, sessionKey); return { - content: `Unrecognized thinking level "${rawLevel}". Valid levels: ${formatThinkingLevels(session?.modelProvider)}.`, + content: `Unrecognized thinking level "${rawLevel}". Valid levels: ${formatThinkingLevels(session?.modelProvider, session?.model)}.`, }; } catch (err) { return { content: `Failed to validate thinking level: ${String(err)}` }; diff --git a/ui/src/ui/thinking.ts b/ui/src/ui/thinking.ts index 1e16c2bc1d0..19b0fcaaef5 100644 --- a/ui/src/ui/thinking.ts +++ b/ui/src/ui/thinking.ts @@ -6,10 +6,13 @@ export type ThinkingCatalogEntry = { reasoning?: boolean; }; -const BASE_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "adaptive"] as const; +const BASE_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"] as const; const BINARY_THINKING_LEVELS = ["off", "on"] as const; const ANTHROPIC_CLAUDE_46_MODEL_RE = /^claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i; +const ANTHROPIC_OPUS_47_MODEL_RE = /^claude-opus-4(?:\.|-)7(?:$|[-.])/i; const AMAZON_BEDROCK_CLAUDE_46_MODEL_RE = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i; +const OPENAI_XHIGH_MODEL_RE = + /^(?:gpt-5\.[2-9](?:\.\d+)?|gpt-5\.[2-9](?:\.\d+)?-pro|gpt-5\.\d+-codex|gpt-5\.\d+-codex-spark|gpt-5\.1-codex|gpt-5\.2-codex)(?:$|-)/i; export function normalizeThinkingProviderId(provider?: string | null): string { if (!provider) { @@ -38,6 +41,9 @@ export function normalizeThinkLevel(raw?: string | null): string | undefined { if (collapsed === "adaptive" || collapsed === "auto") { return "adaptive"; } + if (collapsed === "max") { + return "max"; + } if (collapsed === "xhigh" || collapsed === "extrahigh") { return "xhigh"; } @@ -56,9 +62,7 @@ export function normalizeThinkLevel(raw?: string | null): string | undefined { if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) { return "medium"; } - if ( - ["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest", "max"].includes(key) - ) { + if (["high", "ultra", "ultrathink", "think-hard", "thinkhardest", "highest"].includes(key)) { return "high"; } if (key === "think") { @@ -67,12 +71,53 @@ export function normalizeThinkLevel(raw?: string | null): string | undefined { return undefined; } -export function listThinkingLevelLabels(provider?: string | null): readonly string[] { - return isBinaryThinkingProvider(provider) ? BINARY_THINKING_LEVELS : BASE_THINKING_LEVELS; +function supportsAdaptiveThinking(provider?: string | null, model?: string | null): boolean { + const normalizedProvider = normalizeThinkingProviderId(provider); + const modelId = model?.trim() ?? ""; + if (normalizedProvider === "anthropic") { + return ANTHROPIC_CLAUDE_46_MODEL_RE.test(modelId) || ANTHROPIC_OPUS_47_MODEL_RE.test(modelId); + } + if (normalizedProvider === "amazon-bedrock") { + return AMAZON_BEDROCK_CLAUDE_46_MODEL_RE.test(modelId); + } + return false; } -export function formatThinkingLevels(provider?: string | null): string { - return listThinkingLevelLabels(provider).join(", "); +function supportsXHighThinking(provider?: string | null, model?: string | null): boolean { + const normalizedProvider = normalizeThinkingProviderId(provider); + const modelId = model?.trim() ?? ""; + if (normalizedProvider === "anthropic") { + return ANTHROPIC_OPUS_47_MODEL_RE.test(modelId); + } + if (["openai", "openai-codex", "github-copilot", "codex"].includes(normalizedProvider)) { + return OPENAI_XHIGH_MODEL_RE.test(modelId); + } + return false; +} + +function supportsMaxThinking(provider?: string | null, model?: string | null): boolean { + return normalizeThinkingProviderId(provider) === "anthropic" + ? ANTHROPIC_OPUS_47_MODEL_RE.test(model?.trim() ?? "") + : false; +} + +export function listThinkingLevelLabels( + provider?: string | null, + model?: string | null, +): readonly string[] { + if (isBinaryThinkingProvider(provider)) { + return BINARY_THINKING_LEVELS; + } + return [ + ...BASE_THINKING_LEVELS, + ...(supportsXHighThinking(provider, model) ? ["xhigh"] : []), + ...(supportsAdaptiveThinking(provider, model) ? ["adaptive"] : []), + ...(supportsMaxThinking(provider, model) ? ["max"] : []), + ]; +} + +export function formatThinkingLevels(provider?: string | null, model?: string | null): string { + return listThinkingLevelLabels(provider, model).join(", "); } export function resolveThinkingDefaultForModel(params: {