From e4adb0b0e313bfe13170abfac4205ec5a43c5d8a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Apr 2026 06:16:10 +0100 Subject: [PATCH] fix: hide adaptive think option for GPT models --- CHANGELOG.md | 1 + docs/concepts/model-providers.md | 2 + docs/plugins/architecture.md | 25 +++++++------ docs/plugins/sdk-provider-plugins.md | 21 ++++++----- docs/tools/thinking.md | 6 ++- extensions/amazon-bedrock/index.test.ts | 6 +++ .../amazon-bedrock/register.sync.runtime.ts | 1 + extensions/anthropic/index.test.ts | 12 ++++++ extensions/anthropic/register.runtime.ts | 5 +++ .../native-command.think-autocomplete.test.ts | 6 +++ .../reply/directive-handling.model.test.ts | 2 +- src/auto-reply/thinking.shared.ts | 2 +- src/auto-reply/thinking.test.ts | 23 +++++++++++- src/auto-reply/thinking.ts | 37 ++++++++++++++----- src/plugins/provider-runtime.ts | 10 +++++ src/plugins/provider-thinking.ts | 7 ++++ src/plugins/provider-thinking.types.ts | 4 +- src/plugins/types.ts | 6 +++ 18 files changed, 136 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index febfe0cabb0..f9d5cc1f401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - fix(qqbot): add SSRF guard to direct-upload URL paths in uploadC2CMedia and uploadGroupMedia [AI-assisted]. (#69595) Thanks @pgondhi987. - 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. - 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/model-providers.md b/docs/concepts/model-providers.md index ec81192abd2..f2f9dfc22fc 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -43,6 +43,7 @@ For model selection rules, see [/concepts/models](/concepts/models). `matchesContextOverflowError`, `classifyFailoverReason`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, `supportsXHighThinking`, + `supportsAdaptiveThinking`, `resolveDefaultThinkingLevel`, `applyConfigDefaults`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`, and `onModelSelected`. @@ -133,6 +134,7 @@ Typical split: discovery and config merging - `isBinaryThinking`: provider owns binary on/off thinking UX - `supportsXHighThinking`: provider opts selected models into `xhigh` +- `supportsAdaptiveThinking`: provider opts selected models into `adaptive` - `resolveDefaultThinkingLevel`: provider owns default `/think` policy for a model family - `applyConfigDefaults`: provider applies provider-specific global defaults diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index c336f92787c..57cd3673f22 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -658,7 +658,7 @@ Provider plugins now have two layers: `buildAuthDoctorHint`, `matchesContextOverflowError`, `classifyFailoverReason`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, - `isBinaryThinking`, `supportsXHighThinking`, + `isBinaryThinking`, `supportsXHighThinking`, `supportsAdaptiveThinking`, `resolveDefaultThinkingLevel`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`, `createEmbeddingProvider`, `buildReplayPolicy`, @@ -724,16 +724,17 @@ The "When to use" column is the quick decision guide. | 32 | `augmentModelCatalog` | Synthetic/final catalog rows appended after discovery | Provider needs synthetic forward-compat rows in `models list` and pickers | | 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 | `resolveDefaultThinkingLevel` | Default `/think` level for a specific model family | Provider owns default `/think` policy for a model family | -| 36 | `isModernModelRef` | Modern-model matcher for live profile filters and smoke selection | Provider owns live/smoke preferred-model matching | -| 37 | `prepareRuntimeAuth` | Exchange a configured credential into the actual runtime token/key just before inference | Provider needs a token exchange or short-lived request credential | -| 38 | `resolveUsageAuth` | Resolve usage/billing credentials for `/usage` and related status surfaces | Provider needs custom usage/quota token parsing or a different usage credential | -| 39 | `fetchUsageSnapshot` | Fetch and normalize provider-specific usage/quota snapshots after auth is resolved | Provider needs a provider-specific usage endpoint or payload parser | -| 40 | `createEmbeddingProvider` | Build a provider-owned embedding adapter for memory/search | Memory embedding behavior belongs with the provider plugin | -| 41 | `buildReplayPolicy` | Return a replay policy controlling transcript handling for the provider | Provider needs custom transcript policy (for example, thinking-block stripping) | -| 42 | `sanitizeReplayHistory` | Rewrite replay history after generic transcript cleanup | Provider needs provider-specific replay rewrites beyond shared compaction helpers | -| 43 | `validateReplayTurns` | Final replay-turn validation or reshaping before the embedded runner | Provider transport needs stricter turn validation after generic sanitation | -| 44 | `onModelSelected` | Run provider-owned post-selection side effects | Provider needs telemetry or provider-owned state when a model becomes active | +| 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 | `normalizeModelId`, `normalizeTransport`, and `normalizeConfig` first check the matched provider plugin, then fall through other hook-capable provider plugins @@ -805,7 +806,7 @@ api.registerProvider({ - Anthropic uses `resolveDynamicModel`, `capabilities`, `buildAuthDoctorHint`, `resolveUsageAuth`, `fetchUsageSnapshot`, `isCacheTtlEligible`, - `resolveDefaultThinkingLevel`, `applyConfigDefaults`, `isModernModelRef`, + `supportsAdaptiveThinking`, `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 b55847ef5e1..7cc4c0f5eb0 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -535,16 +535,17 @@ API key auth, and dynamic model resolution. | 31 | `augmentModelCatalog` | Synthetic forward-compat rows | | 32 | `isBinaryThinking` | Binary thinking on/off | | 33 | `supportsXHighThinking` | `xhigh` reasoning support | - | 34 | `resolveDefaultThinkingLevel` | Default `/think` policy | - | 35 | `isModernModelRef` | Live/smoke model matching | - | 36 | `prepareRuntimeAuth` | Token exchange before inference | - | 37 | `resolveUsageAuth` | Custom usage credential parsing | - | 38 | `fetchUsageSnapshot` | Custom usage endpoint | - | 39 | `createEmbeddingProvider` | Provider-owned embedding adapter for memory/search | - | 40 | `buildReplayPolicy` | Custom transcript replay/compaction policy | - | 41 | `sanitizeReplayHistory` | Provider-specific replay rewrites after generic cleanup | - | 42 | `validateReplayTurns` | Strict replay-turn validation before the embedded runner | - | 43 | `onModelSelected` | Post-selection callback (e.g. telemetry) | + | 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) | Prompt tuning note: diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index 160bfa6d0d3..414914e201b 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -16,10 +16,11 @@ title: "Thinking Levels" - 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 Anthropic Claude 4.6 and Opus 4.7) + - adaptive → provider-managed adaptive thinking (supported for Claude 4.6 on Anthropic/Bedrock and Anthropic Claude Opus 4.7) - `x-high`, `x_high`, `extra-high`, `extra high`, and `extra_high` map to `xhigh`. - `highest`, `max` map 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. - 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. @@ -109,7 +110,8 @@ title: "Thinking Levels" - Picking another level writes the session override immediately via `sessions.patch`; it does not wait for the next send and it is not a one-shot `thinkingOnce` override. - The first option is always `Default ()`, where the resolved default comes from the active session model: `adaptive` for Claude 4.6 on Anthropic, `off` for Anthropic Claude Opus 4.7 unless configured, `low` for other reasoning-capable models, `off` otherwise. - The picker stays provider-aware: - - most providers show `off | minimal | low | medium | high | adaptive` + - 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` - 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/amazon-bedrock/index.test.ts b/extensions/amazon-bedrock/index.test.ts index a4b541e17ce..435e2904b23 100644 --- a/extensions/amazon-bedrock/index.test.ts +++ b/extensions/amazon-bedrock/index.test.ts @@ -107,6 +107,12 @@ describe("amazon-bedrock provider plugin", () => { modelId: "amazon.nova-micro-v1:0", } as never), ).toBeUndefined(); + expect( + provider.supportsAdaptiveThinking?.({ + provider: "amazon-bedrock", + modelId: "us.anthropic.claude-opus-4-6-v1", + } as never), + ).toBe(true); }); it("owns Anthropic-style replay policy for Claude Bedrock models", async () => { diff --git a/extensions/amazon-bedrock/register.sync.runtime.ts b/extensions/amazon-bedrock/register.sync.runtime.ts index a141ffd3444..bfbb3023206 100644 --- a/extensions/amazon-bedrock/register.sync.runtime.ts +++ b/extensions/amazon-bedrock/register.sync.runtime.ts @@ -191,6 +191,7 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void { } return undefined; }, + supportsAdaptiveThinking: ({ modelId }) => claude46ModelRe.test(modelId.trim()), resolveDefaultThinkingLevel: ({ modelId }) => claude46ModelRe.test(modelId.trim()) ? "adaptive" : undefined, }); diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index daaa429d0de..25949a0ddfd 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.supportsAdaptiveThinking?.({ + provider: "anthropic", + modelId: "claude-opus-4-7", + } as never), + ).toBe(true); + expect( + provider.supportsAdaptiveThinking?.({ + provider: "anthropic", + modelId: "claude-opus-4-6", + } as never), + ).toBe(true); }); it("resolves claude-cli synthetic oauth auth", async () => { diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index ae8125528bd..d9277a5861b 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -278,6 +278,10 @@ function isAnthropicOpus47Model(modelId: string): boolean { ); } +function supportsAnthropicAdaptiveThinking(modelId: string): boolean { + return shouldUseAnthropicAdaptiveThinkingDefault(modelId) || isAnthropicOpus47Model(modelId); +} + function matchesAnthropicModernModel(modelId: string): boolean { const lower = normalizeLowercaseStringOrEmpty(modelId); return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix)); @@ -490,6 +494,7 @@ export function buildAnthropicProvider(): ProviderPlugin { isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId), resolveReasoningOutputMode: () => "native", supportsXHighThinking: ({ modelId }) => isAnthropicOpus47Model(modelId), + supportsAdaptiveThinking: ({ modelId }) => supportsAnthropicAdaptiveThinking(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 a1887c94808..efae65bf36a 100644 --- a/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts +++ b/extensions/discord/src/monitor/native-command.think-autocomplete.test.ts @@ -34,6 +34,7 @@ const resolveConfiguredBindingRouteMock = vi.hoisted(() => vi.fn(() => createUnboundConfiguredRouteResult()), ); const providerThinkingMocks = vi.hoisted(() => ({ + resolveProviderAdaptiveThinking: vi.fn(), resolveProviderBinaryThinking: vi.fn(), resolveProviderDefaultThinkingLevel: vi.fn(), resolveProviderXHighThinking: vi.fn(), @@ -127,6 +128,7 @@ let resolveDiscordNativeChoiceContext: typeof import("./native-command-ui.js").r async function loadDiscordThinkAutocompleteModulesForTest() { vi.resetModules(); vi.doMock("../../../../src/plugins/provider-thinking.js", () => ({ + resolveProviderAdaptiveThinking: providerThinkingMocks.resolveProviderAdaptiveThinking, resolveProviderBinaryThinking: providerThinkingMocks.resolveProviderBinaryThinking, resolveProviderDefaultThinkingLevel: providerThinkingMocks.resolveProviderDefaultThinkingLevel, resolveProviderXHighThinking: providerThinkingMocks.resolveProviderXHighThinking, @@ -143,6 +145,7 @@ async function loadDiscordThinkAutocompleteModulesForTest() { describe("discord native /think autocomplete", () => { beforeAll(async () => { providerThinkingMocks.resolveProviderBinaryThinking.mockReturnValue(undefined); + providerThinkingMocks.resolveProviderAdaptiveThinking.mockReturnValue(undefined); providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined); providerThinkingMocks.resolveProviderXHighThinking.mockImplementation(({ provider, context }) => provider === "openai-codex" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId) @@ -170,6 +173,8 @@ describe("discord native /think autocomplete", () => { resolveConfiguredBindingRouteMock.mockReturnValue(createUnboundConfiguredRouteResult()); providerThinkingMocks.resolveProviderBinaryThinking.mockReset(); providerThinkingMocks.resolveProviderBinaryThinking.mockReturnValue(undefined); + providerThinkingMocks.resolveProviderAdaptiveThinking.mockReset(); + providerThinkingMocks.resolveProviderAdaptiveThinking.mockReturnValue(undefined); providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReset(); providerThinkingMocks.resolveProviderDefaultThinkingLevel.mockReturnValue(undefined); providerThinkingMocks.resolveProviderXHighThinking.mockReset(); @@ -258,6 +263,7 @@ describe("discord native /think autocomplete", () => { }); const values = choices.map((choice) => choice.value); expect(values).toContain("xhigh"); + expect(values).not.toContain("adaptive"); }); it("falls back when a configured binding is unavailable", async () => { diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 923ea65b5bc..26226d92031 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -811,7 +811,7 @@ describe("handleDirectiveOnly model persist behavior (fixes #1435)", () => { ); expect(result?.text).toContain("Current thinking level: low"); - expect(result?.text).toContain("Options: off, minimal, low, medium, high, adaptive."); + expect(result?.text).toContain("Options: off, minimal, low, medium, high."); }); it("persists verbose on and off directives", async () => { diff --git a/src/auto-reply/thinking.shared.ts b/src/auto-reply/thinking.shared.ts index 0c2492f5be5..3de1f05bf2d 100644 --- a/src/auto-reply/thinking.shared.ts +++ b/src/auto-reply/thinking.shared.ts @@ -20,7 +20,7 @@ export type ThinkingCatalogEntry = { reasoning?: boolean; }; -const BASE_THINKING_LEVELS: ThinkLevel[] = ["off", "minimal", "low", "medium", "high", "adaptive"]; +const BASE_THINKING_LEVELS: ThinkLevel[] = ["off", "minimal", "low", "medium", "high"]; const NO_THINKING_LEVELS: ThinkLevel[] = [...BASE_THINKING_LEVELS]; export function isBinaryThinkingProvider(provider?: string | null): boolean { diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index 88b66383a44..3f99ecf4cff 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const providerRuntimeMocks = vi.hoisted(() => ({ + resolveProviderAdaptiveThinking: vi.fn(), resolveProviderBinaryThinking: vi.fn(), resolveProviderDefaultThinkingLevel: vi.fn(), resolveProviderXHighThinking: vi.fn(), @@ -15,6 +16,7 @@ let resolveThinkingDefaultForModel: typeof import("./thinking.js").resolveThinki async function loadFreshThinkingModuleForTest() { vi.resetModules(); vi.doMock("../plugins/provider-thinking.js", () => ({ + resolveProviderAdaptiveThinking: providerRuntimeMocks.resolveProviderAdaptiveThinking, resolveProviderBinaryThinking: providerRuntimeMocks.resolveProviderBinaryThinking, resolveProviderDefaultThinkingLevel: providerRuntimeMocks.resolveProviderDefaultThinkingLevel, resolveProviderXHighThinking: providerRuntimeMocks.resolveProviderXHighThinking, @@ -23,6 +25,8 @@ async function loadFreshThinkingModuleForTest() { } beforeEach(async () => { + providerRuntimeMocks.resolveProviderAdaptiveThinking.mockReset(); + providerRuntimeMocks.resolveProviderAdaptiveThinking.mockReturnValue(undefined); providerRuntimeMocks.resolveProviderBinaryThinking.mockReset(); providerRuntimeMocks.resolveProviderBinaryThinking.mockReturnValue(undefined); providerRuntimeMocks.resolveProviderDefaultThinkingLevel.mockReset(); @@ -113,8 +117,23 @@ describe("listThinkingLevels", () => { expect(listThinkingLevels(undefined, "gpt-4.1-mini")).not.toContain("xhigh"); }); - it("always includes adaptive", () => { - expect(listThinkingLevels(undefined, "gpt-4.1-mini")).toContain("adaptive"); + it("uses provider runtime hooks for adaptive support", () => { + providerRuntimeMocks.resolveProviderAdaptiveThinking.mockReturnValue(true); + + expect(listThinkingLevels("demo", "demo-model")).toContain("adaptive"); + }); + + 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"); + }); + + it("includes adaptive for provider-advertised models", () => { + providerRuntimeMocks.resolveProviderAdaptiveThinking.mockImplementation( + ({ provider, context }) => + provider === "anthropic" && context.modelId === "claude-opus-4-6" ? true : undefined, + ); + expect(listThinkingLevels("anthropic", "claude-opus-4-6")).toContain("adaptive"); }); }); diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index f9edd5006ba..9d760fe7623 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -1,7 +1,5 @@ import { normalizeProviderId } from "../agents/provider-id.js"; import { - formatThinkingLevels as formatThinkingLevelsFallback, - listThinkingLevelLabels as listThinkingLevelLabelsFallback, listThinkingLevels as listThinkingLevelsFallback, resolveThinkingDefaultForModel as resolveThinkingDefaultForModelFallback, } from "./thinking.shared.js"; @@ -31,6 +29,7 @@ export type { VerboseLevel, } from "./thinking.shared.js"; import { + resolveProviderAdaptiveThinking, resolveProviderBinaryThinking, resolveProviderDefaultThinkingLevel, resolveProviderXHighThinking, @@ -82,10 +81,33 @@ export function supportsXHighThinking(provider?: string | null, model?: string | return false; } +export function supportsAdaptiveThinking(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 = resolveProviderAdaptiveThinking({ + 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)) { - levels.splice(levels.length - 1, 0, "xhigh"); + levels.push("xhigh"); + } + if (supportsAdaptiveThinking(provider, model)) { + levels.push("adaptive"); } return levels; } @@ -94,10 +116,7 @@ export function listThinkingLevelLabels(provider?: string | null, model?: string if (isBinaryThinkingProvider(provider, model)) { return ["off", "on"]; } - if (supportsXHighThinking(provider, model)) { - return listThinkingLevels(provider, model); - } - return listThinkingLevelLabelsFallback(provider, model); + return listThinkingLevels(provider, model); } export function formatThinkingLevels( @@ -105,9 +124,7 @@ export function formatThinkingLevels( model?: string | null, separator = ", ", ): string { - return supportsXHighThinking(provider, model) - ? listThinkingLevelLabels(provider, model).join(separator) - : formatThinkingLevelsFallback(provider, model, separator); + return listThinkingLevelLabels(provider, model).join(separator); } export function resolveThinkingDefaultForModel(params: { diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 9f4e0138af8..59490d5f832 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -640,6 +640,16 @@ export function resolveProviderXHighThinking(params: { return resolveProviderRuntimePlugin(params)?.supportsXHighThinking?.(params.context); } +export function resolveProviderAdaptiveThinking(params: { + provider: string; + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; + context: ProviderThinkingPolicyContext; +}) { + return resolveProviderRuntimePlugin(params)?.supportsAdaptiveThinking?.(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 8e5874eb57f..c88eea714ea 100644 --- a/src/plugins/provider-thinking.ts +++ b/src/plugins/provider-thinking.ts @@ -8,6 +8,7 @@ type ThinkingProviderPlugin = { id: string; aliases?: string[]; isBinaryThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; + supportsAdaptiveThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; supportsXHighThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; resolveDefaultThinkingLevel?: ( ctx: ProviderDefaultThinkingPolicyContext, @@ -61,6 +62,12 @@ export function resolveProviderXHighThinking( return resolveActiveThinkingProvider(params.provider)?.supportsXHighThinking?.(params.context); } +export function resolveProviderAdaptiveThinking( + params: ThinkingHookParams, +) { + return resolveActiveThinkingProvider(params.provider)?.supportsAdaptiveThinking?.(params.context); +} + export function resolveProviderDefaultThinkingLevel( params: ThinkingHookParams, ) { diff --git a/src/plugins/provider-thinking.types.ts b/src/plugins/provider-thinking.types.ts index f68f9ded1b8..06574e9aa6a 100644 --- a/src/plugins/provider-thinking.types.ts +++ b/src/plugins/provider-thinking.types.ts @@ -2,8 +2,8 @@ * Provider-owned thinking policy input. * * Used by shared `/think`, ACP controls, and directive parsing to ask a - * provider whether a model supports special reasoning UX such as xhigh or a - * binary on/off toggle. + * provider whether a model supports special reasoning UX such as adaptive, + * xhigh, or a binary on/off toggle. */ export type ProviderThinkingPolicyContext = { provider: string; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 34dd566d5ab..f25ead05127 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1407,6 +1407,12 @@ export type ProviderPlugin = { * Return true only for models that should expose the `xhigh` thinking level. */ supportsXHighThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; + /** + * Provider-owned adaptive thinking support. + * + * Return true only for models that should expose the `adaptive` thinking level. + */ + supportsAdaptiveThinking?: (ctx: ProviderThinkingPolicyContext) => boolean | undefined; /** * Provider-owned default thinking level. *