From 1962816c1ea78cfb2176469250ec6104aa772a0e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 20 Jul 2026 22:52:06 +0200 Subject: [PATCH] Allow turning thinking off (#2257) * feat(providers): allow turning thinking off Expose Off only for models that support disabled thinking while preserving Low as the default. * fix(providers): reset unsupported thinking on model change Keep the selected thinking option only when the new model advertises it; otherwise use that model's default and persist the runtime change. * fix(providers): isolate custom model capabilities Use strict manifest identity when reconciling thinking options so provider-prefixed custom models cannot inherit capabilities they do not advertise. * fix(providers): clear thinking with default model * fix(providers): validate disabled thinking support * fix(providers): validate initial thinking config * refactor(providers): centralize thinking capability * fix(server): order config mutation events --- .../src/server/agent/agent-manager.test.ts | 48 +++++++++ .../server/src/server/agent/agent-manager.ts | 23 +++++ .../agent/providers/claude/agent.test.ts | 98 ++++++++++++++++++- .../server/agent/providers/claude/agent.ts | 59 ++++++++++- .../agent/providers/claude/model-manifest.ts | 55 ++++++++++- .../agent/providers/claude/models.test.ts | 23 +++++ 6 files changed, 296 insertions(+), 10 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 6edd2db6d..74d3a32f3 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -3103,6 +3103,53 @@ test("persists live mode, model, and thinking changes without an external snapsh expect(persisted?.runtimeInfo?.model).toBe("gpt-5.4"); }); +test("later explicit config mutations win over events emitted by earlier mutations", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-config-mutation-order-")); + class ConfigMutationSession extends TestAgentSession { + async setModel(): Promise { + this.pushEvent({ + type: "timeline", + provider: "codex", + item: { type: "assistant_message", text: "model changed" }, + }); + this.pushEvent({ + type: "thinking_option_changed", + provider: "codex", + thinkingOptionId: "low", + }); + } + + async setThinkingOption(): Promise {} + } + class ConfigMutationClient extends TestAgentClient { + override async createSession(config: AgentSessionConfig): Promise { + return new ConfigMutationSession(config); + } + } + + const manager = new AgentManager({ + clients: { codex: new ConfigMutationClient() }, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000134", + }); + const snapshot = await manager.createAgent( + { + provider: "codex", + cwd: workdir, + model: "gpt-5.2-codex", + thinkingOptionId: "off", + }, + undefined, + { workspaceId: undefined }, + ); + + await manager.setAgentModel(snapshot.id, "gpt-5.4"); + await manager.setAgentThinkingOption(snapshot.id, "high"); + await manager.flush(); + + expect(manager.getAgent(snapshot.id)?.config.thinkingOptionId).toBe("high"); +}); + test("session config drift events update state through the stream channel", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-session-config-events-")); let capturedSession: TestAgentSession | null = null; @@ -3171,6 +3218,7 @@ test("session config drift events update state through the stream channel", asyn const agent = manager.getAgent(snapshot.id); expect(agent?.currentModeId).toBe("build"); + expect(agent?.config.thinkingOptionId).toBe("high"); expect(agent?.availableModes).toEqual([ { id: "plan", label: "Plan" }, { id: "build", label: "Build" }, diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 0bcbc2ca8..32931c097 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1579,6 +1579,7 @@ export class AgentManager { async setAgentMode(agentId: string, modeId: string): Promise { const agent = this.requireSessionAgent(agentId); const notice = (await agent.session.setMode(modeId)) ?? null; + await this.drainSessionEvents(agentId); const currentMode = (await agent.session.getCurrentMode()) ?? modeId; agent.config.modeId = currentMode ?? undefined; agent.currentModeId = currentMode; @@ -1599,6 +1600,7 @@ export class AgentManager { if (agent.session.setModel) { await agent.session.setModel(normalizedModelId); } + await this.drainSessionEvents(agentId); agent.config.model = normalizedModelId ?? undefined; if (agent.runtimeInfo) { @@ -1622,6 +1624,7 @@ export class AgentManager { if (agent.session.setThinkingOption) { notice = (await agent.session.setThinkingOption(normalizedThinkingOptionId)) ?? null; } + await this.drainSessionEvents(agentId); agent.config.thinkingOptionId = normalizedThinkingOptionId ?? undefined; if (agent.runtimeInfo) { @@ -1643,6 +1646,7 @@ export class AgentManager { } await agent.session.setFeature(featureId, value); + await this.drainSessionEvents(agentId); agent.config.featureValues = { ...agent.config.featureValues, [featureId]: value }; this.touchUpdatedAt(agent); this.emitState(agent); @@ -3009,6 +3013,24 @@ export class AgentManager { }); } + /** + * Provider mutations may synchronously emit config events that are processed through the + * asynchronous session queue. Apply those events before committing the mutation's explicit + * manager state so call order remains authoritative. + */ + private async drainSessionEvents(agentId: string): Promise { + while (true) { + const tail = this.sessionEventTails.get(agentId); + if (!tail) { + return; + } + await tail; + if (this.sessionEventTails.get(agentId) === tail) { + return; + } + } + } + private async dispatchSessionEvent( agent: ActiveManagedAgent, event: AgentStreamEvent, @@ -3431,6 +3453,7 @@ export class AgentManager { this.emitState(agent); return undefined; case "thinking_option_changed": + agent.config.thinkingOptionId = event.thinkingOptionId ?? undefined; if (agent.runtimeInfo) { agent.runtimeInfo = { ...agent.runtimeInfo, diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index bd535ac13..fc0a5d3ba 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -583,8 +583,12 @@ describe("ClaudeAgentSession features", () => { }; }, }; - const queryFactory = vi.fn(() => queryMock); - return { queryFactory, queryMock }; + const launches: Array<{ options: Record }> = []; + const queryFactory = vi.fn((input) => { + launches.push(input); + return queryMock; + }); + return { queryFactory, queryMock, launches }; } test("lists fast mode only for supported Opus models", async () => { @@ -686,6 +690,96 @@ describe("ClaudeAgentSession features", () => { await session.close(); }); + test("turns Claude thinking off without retaining an effort level", async () => { + const { queryFactory, launches } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-sonnet-5", + thinkingOptionId: "off", + }); + + await expect(session.startTurn("hello")).resolves.toEqual({ + turnId: expect.stringMatching(/^foreground-turn-/), + }); + + expect(launches[0]?.options.thinking).toEqual({ type: "disabled" }); + expect(launches[0]?.options).not.toHaveProperty("effort"); + + await session.close(); + }); + + test.each([ + ["supported model", "claude-opus-4-8", { type: "disabled" }, undefined], + ["unsupported model", "claude-fable-5", { type: "adaptive" }, "low"], + ["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined], + ["provider default", null, undefined, undefined], + ])("reconciles Off when switching to a %s", async (_label, modelId, thinking, effort) => { + const { queryFactory, launches } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-sonnet-5", + thinkingOptionId: "off", + }); + + await session.setModel?.(modelId); + await session.startTurn("hello"); + + expect(launches.at(-1)?.options.thinking).toEqual(thinking); + expect(launches.at(-1)?.options.effort).toBe(effort); + + await session.close(); + }); + + test("rejects disabled thinking when the active model does not support it", async () => { + const { queryFactory } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-fable-5", + }); + + await expect(session.setThinkingOption?.("off")).rejects.toThrow( + "Thinking option 'off' is not available for model 'claude-fable-5'", + ); + + await session.close(); + }); + + test("rejects an initial disabled-thinking config for an unsupported model", async () => { + const { queryFactory } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + + await expect( + client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-fable-5", + thinkingOptionId: "off", + }), + ).rejects.toThrow("Thinking option 'off' is not available for model 'claude-fable-5'"); + }); + test("returns a next-turn notice when changing Claude thinking during an active turn", async () => { const { queryFactory } = createQueryMock(); const client = new ClaudeAgentClient({ diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 6497a9e96..675e9561b 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -34,7 +34,11 @@ import { getClaudeModelsWithSettings, normalizeClaudeRuntimeModelId, } from "./models.js"; -import { CLAUDE_ULTRACODE_THINKING_OPTION_ID } from "./model-manifest.js"; +import { + CLAUDE_DISABLED_THINKING_OPTION_ID, + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + resolveClaudeDisabledThinkingForModel, +} from "./model-manifest.js"; import { parsePartialJsonObject } from "./partial-json.js"; import { ClaudeSidechainTracker } from "./sidechain-tracker.js"; import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js"; @@ -375,7 +379,10 @@ interface ClaudeAgentSessionOptions { } type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max"; -type ClaudeThinkingOption = ClaudeThinkingEffort | typeof CLAUDE_ULTRACODE_THINKING_OPTION_ID; +type ClaudeThinkingOption = + | ClaudeThinkingEffort + | typeof CLAUDE_DISABLED_THINKING_OPTION_ID + | typeof CLAUDE_ULTRACODE_THINKING_OPTION_ID; function resolvePathEnvKey(): "Path" | "PATH" | null { if (process.env["Path"] !== undefined) return "Path"; @@ -423,7 +430,26 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau } function isClaudeThinkingOption(value: string | null | undefined): value is ClaudeThinkingOption { - return value === CLAUDE_ULTRACODE_THINKING_OPTION_ID || isClaudeThinkingEffort(value); + return ( + value === CLAUDE_DISABLED_THINKING_OPTION_ID || + value === CLAUDE_ULTRACODE_THINKING_OPTION_ID || + isClaudeThinkingEffort(value) + ); +} + +function assertClaudeThinkingOptionSupported( + modelId: string | null | undefined, + thinkingOptionId: string | null | undefined, +): void { + if ( + thinkingOptionId !== CLAUDE_DISABLED_THINKING_OPTION_ID || + resolveClaudeDisabledThinkingForModel(modelId).supported + ) { + return; + } + throw new Error( + `Thinking option '${thinkingOptionId}' is not available for model '${modelId ?? "default"}'`, + ); } interface ClaudeOptionsLogSummary { @@ -1946,6 +1972,7 @@ class ClaudeAgentSession implements AgentSession { constructor(config: ClaudeAgentConfig, options: ClaudeAgentSessionOptions) { this.config = config; + assertClaudeThinkingOptionSupported(config.model, config.thinkingOptionId); this.launchEnv = options.launchEnv; this.agentId = options.agentId; this.defaults = options.defaults; @@ -2203,6 +2230,7 @@ class ClaudeAgentSession implements AgentSession { const activeQuery = await this.ensureQuery(); await activeQuery.setModel(normalizedModelId ?? undefined); this.config.model = normalizedModelId ?? undefined; + this.reconcileThinkingOptionForModel(normalizedModelId); if (!claudeModelSupportsFastMode(this.config.model) && this.config.featureValues?.fast_mode) { await this.applyFastModeFeature(false, activeQuery); } @@ -2216,6 +2244,26 @@ class ClaudeAgentSession implements AgentSession { this.persistence = null; } + private reconcileThinkingOptionForModel(modelId: string | null): void { + const thinkingOptionId = this.config.thinkingOptionId; + if (thinkingOptionId !== CLAUDE_DISABLED_THINKING_OPTION_ID) { + return; + } + + const resolution = resolveClaudeDisabledThinkingForModel(modelId); + if (resolution.supported) { + return; + } + + this.config.thinkingOptionId = resolution.fallbackThinkingOptionId; + this.queryRestartNeeded = true; + this.pushEvent({ + type: "thinking_option_changed", + provider: "claude", + thinkingOptionId: this.config.thinkingOptionId ?? null, + }); + } + async setThinkingOption(thinkingOptionId: string | null): Promise { const normalizedThinkingOptionId = typeof thinkingOptionId === "string" && thinkingOptionId.trim().length > 0 @@ -2225,6 +2273,7 @@ class ClaudeAgentSession implements AgentSession { if (!normalizedThinkingOptionId || normalizedThinkingOptionId === "default") { this.config.thinkingOptionId = undefined; } else if (isClaudeThinkingOption(normalizedThinkingOptionId)) { + assertClaudeThinkingOptionSupported(this.config.model, normalizedThinkingOptionId); this.config.thinkingOptionId = normalizedThinkingOptionId; } else { throw new Error(`Unknown thinking option: ${normalizedThinkingOptionId}`); @@ -2899,6 +2948,10 @@ class ClaudeAgentSession implements AgentSession { this.config.thinkingOptionId && this.config.thinkingOptionId !== "default" ? this.config.thinkingOptionId : undefined; + assertClaudeThinkingOptionSupported(this.config.model, thinkingOptionId); + if (thinkingOptionId === CLAUDE_DISABLED_THINKING_OPTION_ID) { + return { thinking: { type: "disabled" }, effort: undefined, ultracode: false }; + } if (thinkingOptionId === CLAUDE_ULTRACODE_THINKING_OPTION_ID) { return { thinking: { type: "adaptive" }, effort: "xhigh", ultracode: true }; } diff --git a/packages/server/src/server/agent/providers/claude/model-manifest.ts b/packages/server/src/server/agent/providers/claude/model-manifest.ts index e8262f4cf..88c469ad3 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -9,6 +9,7 @@ interface ClaudeModelManifestEntry { isDefault?: boolean; contextWindowMaxTokens?: number; effortLevels?: readonly ClaudeEffortLevel[]; + supportsThinkingDisabled?: boolean; supportsFastMode?: boolean; } @@ -25,6 +26,7 @@ const CLAUDE_EFFORT_LABELS = { max: "Max", } as const satisfies Record; +export const CLAUDE_DISABLED_THINKING_OPTION_ID = "off"; export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode"; export const CLAUDE_MODEL_MANIFEST = [ @@ -41,6 +43,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Opus 4.8 with 1M context window", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsThinkingDisabled: true, supportsFastMode: true, }, { @@ -50,6 +53,7 @@ export const CLAUDE_MODEL_MANIFEST = [ isDefault: true, contextWindowMaxTokens: 200_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsThinkingDisabled: true, supportsFastMode: true, }, { @@ -58,6 +62,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Sonnet 5 · Best for everyday tasks", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsThinkingDisabled: true, }, { id: "claude-opus-4-7[1m]", @@ -65,6 +70,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Opus 4.7 with 1M context window", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsThinkingDisabled: true, supportsFastMode: true, }, { @@ -73,6 +79,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Opus 4.7 · Previous release", contextWindowMaxTokens: 200_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsThinkingDisabled: true, supportsFastMode: true, }, { @@ -81,6 +88,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Opus 4.6 with 1M context window", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.standard, + supportsThinkingDisabled: true, supportsFastMode: true, }, { @@ -89,6 +97,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Opus 4.6 · Most capable for complex work", contextWindowMaxTokens: 200_000, effortLevels: CLAUDE_EFFORT_LEVELS.standard, + supportsThinkingDisabled: true, supportsFastMode: true, }, { @@ -97,6 +106,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Sonnet 4.6 with 1M context window", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.standard, + supportsThinkingDisabled: true, }, { id: "claude-sonnet-4-6", @@ -104,6 +114,7 @@ export const CLAUDE_MODEL_MANIFEST = [ description: "Sonnet 4.6 · Best for everyday tasks", contextWindowMaxTokens: 200_000, effortLevels: CLAUDE_EFFORT_LEVELS.standard, + supportsThinkingDisabled: true, }, { id: "claude-haiku-4-5", @@ -115,15 +126,19 @@ export const CLAUDE_MODEL_MANIFEST = [ function buildThinkingOptions( effortLevels: readonly ClaudeEffortLevel[] | undefined, + supportsThinkingDisabled: boolean, ): AgentSelectOption[] | undefined { if (!effortLevels) { return undefined; } - const options: AgentSelectOption[] = effortLevels.map((id) => ({ - id, - label: CLAUDE_EFFORT_LABELS[id], - })); + const options: AgentSelectOption[] = [ + ...(supportsThinkingDisabled ? [{ id: CLAUDE_DISABLED_THINKING_OPTION_ID, label: "Off" }] : []), + ...effortLevels.map((id) => ({ + id, + label: CLAUDE_EFFORT_LABELS[id], + })), + ]; if (effortLevels.includes("xhigh")) { options.push({ id: CLAUDE_ULTRACODE_THINKING_OPTION_ID, label: "Ultra Code" }); @@ -136,6 +151,7 @@ export function getClaudeManifestModels(): AgentModelDefinition[] { return CLAUDE_MODEL_MANIFEST.map((model) => { const thinkingOptions = buildThinkingOptions( "effortLevels" in model ? model.effortLevels : undefined, + "supportsThinkingDisabled" in model && model.supportsThinkingDisabled, ); return { provider: "claude", @@ -146,11 +162,40 @@ export function getClaudeManifestModels(): AgentModelDefinition[] { ...(model.contextWindowMaxTokens !== undefined ? { contextWindowMaxTokens: model.contextWindowMaxTokens } : {}), - ...(thinkingOptions ? { thinkingOptions } : {}), + ...(thinkingOptions + ? { + thinkingOptions, + defaultThinkingOptionId: "effortLevels" in model ? model.effortLevels?.[0] : undefined, + } + : {}), }; }); } +export interface ClaudeDisabledThinkingResolution { + supported: boolean; + fallbackThinkingOptionId: string | undefined; +} + +/** + * Resolve the disabled-thinking capability from the curated manifest only. Runtime/provider + * model aliases intentionally do not inherit this capability. + */ +export function resolveClaudeDisabledThinkingForModel( + modelId: string | null | undefined, +): ClaudeDisabledThinkingResolution { + const normalizedModelId = normalizeClaudeManifestModelId(modelId); + const model = normalizedModelId + ? CLAUDE_MODEL_MANIFEST.find((candidate) => candidate.id === normalizedModelId) + : undefined; + return { + supported: + !!model && "supportsThinkingDisabled" in model && model.supportsThinkingDisabled === true, + fallbackThinkingOptionId: + model && "effortLevels" in model ? model.effortLevels?.[0] : undefined, + }; +} + export function isClaudeManifestModelId(modelId: string): boolean { return CLAUDE_MODEL_MANIFEST.some((model) => model.id === modelId); } diff --git a/packages/server/src/server/agent/providers/claude/models.test.ts b/packages/server/src/server/agent/providers/claude/models.test.ts index 3a922b5b1..a62784494 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -6,9 +6,11 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestLogger } from "../../../../test-utils/test-logger.js"; import { ClaudeAgentClient } from "./agent.js"; import { + CLAUDE_DISABLED_THINKING_OPTION_ID, CLAUDE_ULTRACODE_THINKING_OPTION_ID, claudeManifestModelSupportsFastMode, normalizeClaudeManifestModelId, + resolveClaudeDisabledThinkingForModel, } from "./model-manifest.js"; import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js"; @@ -87,6 +89,7 @@ describe("getClaudeModels", () => { const models = new Map(getClaudeModels().map((model) => [model.id, model])); expect(models.get("claude-sonnet-5")?.thinkingOptions?.map((option) => option.id)).toEqual([ + CLAUDE_DISABLED_THINKING_OPTION_ID, "low", "medium", "high", @@ -100,8 +103,10 @@ describe("getClaudeModels", () => { ?.thinkingOptions?.find((option) => option.id === CLAUDE_ULTRACODE_THINKING_OPTION_ID) ?.label, ).toBe("Ultra Code"); + expect(models.get("claude-sonnet-5")?.defaultThinkingOptionId).toBe("low"); expect(models.get("claude-opus-4-7")?.thinkingOptions?.map((option) => option.id)).toEqual([ + CLAUDE_DISABLED_THINKING_OPTION_ID, "low", "medium", "high", @@ -110,14 +115,32 @@ describe("getClaudeModels", () => { CLAUDE_ULTRACODE_THINKING_OPTION_ID, ]); expect(models.get("claude-sonnet-4-6")?.thinkingOptions?.map((option) => option.id)).toEqual([ + CLAUDE_DISABLED_THINKING_OPTION_ID, "low", "medium", "high", "max", ]); + expect(models.get("claude-fable-5")?.thinkingOptions?.map((option) => option.id)).not.toContain( + CLAUDE_DISABLED_THINKING_OPTION_ID, + ); expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined(); }); + it.each([ + ["claude-sonnet-5", true, "low"], + ["claude-sonnet-5-20260101", true, "low"], + ["claude-fable-5", false, "low"], + ["claude-haiku-4-5", false, undefined], + ["openrouter/anthropic/claude-opus-4-8", false, undefined], + [null, false, undefined], + ])("resolves disabled thinking for model %s", (modelId, supported, fallbackThinkingOptionId) => { + expect(resolveClaudeDisabledThinkingForModel(modelId)).toEqual({ + supported, + fallbackThinkingOptionId, + }); + }); + it("returns fresh copies each call", () => { const a = getClaudeModels(); const b = getClaudeModels();