From cbd3a8cb87da2e24b4998c19c6a6f9db4f3335d1 Mon Sep 17 00:00:00 2001 From: Fero Date: Wed, 7 Jan 2026 18:11:03 +0100 Subject: [PATCH] fix: use defaultThinkingLevel from settings when enabledModels lacks explicit suffix (#540) When enabledModels is configured without thinking level suffixes (e.g., 'claude-opus-4-5' instead of 'claude-opus-4-5:high'), the scoped model's default 'off' thinking level was overriding defaultThinkingLevel from settings. Now thinkingLevel in ScopedModel is optional (undefined means 'not explicitly specified'). When passing to SDK, undefined values are filled with defaultThinkingLevel from settings. --- packages/coding-agent/CHANGELOG.md | 4 ++ .../coding-agent/src/core/model-resolver.ts | 23 ++++++----- packages/coding-agent/src/main.ts | 24 ++++++++--- .../coding-agent/test/model-resolver.test.ts | 40 +++++++++---------- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 92561529..145af044 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -11,6 +11,10 @@ - Extension UI dialogs (`ctx.ui.select()`, `ctx.ui.confirm()`, `ctx.ui.input()`) now support a `timeout` option that auto-dismisses the dialog with a live countdown display. Simpler alternative to `AbortSignal` for timed dialogs. - Extensions can now provide custom editor components via `ctx.ui.setEditorComponent((tui, theme, keybindings) => ...)`. Extend `CustomEditor` for full app keybinding support (escape, ctrl+d, model switching, etc.). See `examples/extensions/modal-editor.ts`, `examples/extensions/rainbow-editor.ts`, and `docs/tui.md` Pattern 7. +### Fixed + +- Default thinking level from settings now applies correctly when `enabledModels` is configured. Previously, models without explicit thinking level suffixes (e.g., `claude-opus-4-5` instead of `claude-opus-4-5:high`) would override `defaultThinkingLevel` with "off" + ## [0.37.8] - 2026-01-07 ## [0.37.7] - 2026-01-07 diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 98c8fd60..b00e213a 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -29,7 +29,8 @@ export const defaultModelPerProvider: Record = { export interface ScopedModel { model: Model; - thinkingLevel: ThinkingLevel; + /** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */ + thinkingLevel?: ThinkingLevel; } /** @@ -98,7 +99,8 @@ function tryMatchModel(modelPattern: string, availableModels: Model[]): Mod export interface ParsedModelResult { model: Model | undefined; - thinkingLevel: ThinkingLevel; + /** Thinking level if explicitly specified in pattern, undefined otherwise */ + thinkingLevel?: ThinkingLevel; warning: string | undefined; } @@ -119,14 +121,14 @@ export function parseModelPattern(pattern: string, availableModels: Model[] // Try exact match first const exactMatch = tryMatchModel(pattern, availableModels); if (exactMatch) { - return { model: exactMatch, thinkingLevel: "off", warning: undefined }; + return { model: exactMatch, thinkingLevel: undefined, warning: undefined }; } // No match - try splitting on last colon if present const lastColonIndex = pattern.lastIndexOf(":"); if (lastColonIndex === -1) { // No colons, pattern simply doesn't match any model - return { model: undefined, thinkingLevel: "off", warning: undefined }; + return { model: undefined, thinkingLevel: undefined, warning: undefined }; } const prefix = pattern.substring(0, lastColonIndex); @@ -137,22 +139,21 @@ export function parseModelPattern(pattern: string, availableModels: Model[] const result = parseModelPattern(prefix, availableModels); if (result.model) { // Only use this thinking level if no warning from inner recursion - // (if there was an invalid suffix deeper, we already have "off") return { model: result.model, - thinkingLevel: result.warning ? "off" : suffix, + thinkingLevel: result.warning ? undefined : suffix, warning: result.warning, }; } return result; } else { - // Invalid suffix - recurse on prefix with "off" and warn + // Invalid suffix - recurse on prefix and warn const result = parseModelPattern(prefix, availableModels); if (result.model) { return { model: result.model, - thinkingLevel: "off", - warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using "off" instead.`, + thinkingLevel: undefined, + warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`, }; } return result; @@ -180,7 +181,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model // Extract optional thinking level suffix (e.g., "provider/*:high") const colonIdx = pattern.lastIndexOf(":"); let globPattern = pattern; - let thinkingLevel: ThinkingLevel = "off"; + let thinkingLevel: ThinkingLevel | undefined; if (colonIdx !== -1) { const suffix = pattern.substring(colonIdx + 1); @@ -282,7 +283,7 @@ export async function findInitialModel(options: { if (scopedModels.length > 0 && !isContinuing) { return { model: scopedModels[0].model, - thinkingLevel: scopedModels[0].thinkingLevel, + thinkingLevel: scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? "off", fallbackMessage: undefined, }; } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index da8d0e6c..a05c46a8 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -235,6 +235,7 @@ function buildSessionOptions( scopedModels: ScopedModel[], sessionManager: SessionManager | undefined, modelRegistry: ModelRegistry, + settingsManager: SettingsManager, preloadedExtensions?: LoadedExtension[], ): CreateAgentSessionOptions { const options: CreateAgentSessionOptions = {}; @@ -261,15 +262,21 @@ function buildSessionOptions( } // Thinking level + // Only use scoped model's thinking level if it was explicitly specified (e.g., "model:high") + // Otherwise, let the SDK use defaultThinkingLevel from settings if (parsed.thinking) { options.thinkingLevel = parsed.thinking; - } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) { + } else if (scopedModels.length > 0 && scopedModels[0].thinkingLevel && !parsed.continue && !parsed.resume) { options.thinkingLevel = scopedModels[0].thinkingLevel; } - // Scoped models for Ctrl+P cycling + // Scoped models for Ctrl+P cycling - fill in default thinking level for models without explicit level if (scopedModels.length > 0) { - options.scopedModels = scopedModels; + const defaultThinkingLevel = settingsManager.getDefaultThinkingLevel() ?? "off"; + options.scopedModels = scopedModels.map((sm) => ({ + model: sm.model, + thinkingLevel: sm.thinkingLevel ?? defaultThinkingLevel, + })); } // API key from CLI - set in authStorage @@ -423,7 +430,14 @@ export async function main(args: string[]) { sessionManager = SessionManager.open(selectedPath); } - const sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, loadedExtensions); + const sessionOptions = buildSessionOptions( + parsed, + scopedModels, + sessionManager, + modelRegistry, + settingsManager, + loadedExtensions, + ); sessionOptions.authStorage = authStorage; sessionOptions.modelRegistry = modelRegistry; sessionOptions.eventBus = eventBus; @@ -471,7 +485,7 @@ export async function main(args: string[]) { if (scopedModels.length > 0) { const modelList = scopedModels .map((sm) => { - const thinkingStr = sm.thinkingLevel !== "off" ? `:${sm.thinkingLevel}` : ""; + const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; return `${sm.model.id}${thinkingStr}`; }) .join(", "); diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 0b7b47ca..fdef4160 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -62,24 +62,24 @@ const allModels = [...mockModels, ...mockOpenRouterModels]; describe("parseModelPattern", () => { describe("simple patterns without colons", () => { - test("exact match returns model with off thinking level", () => { + test("exact match returns model with undefined thinking level", () => { const result = parseModelPattern("claude-sonnet-4-5", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); - test("partial match returns best model", () => { + test("partial match returns best model with undefined thinking level", () => { const result = parseModelPattern("sonnet", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); - test("no match returns null model", () => { + test("no match returns undefined model and thinking level", () => { const result = parseModelPattern("nonexistent", allModels); expect(result.model).toBeUndefined(); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); }); @@ -110,27 +110,27 @@ describe("parseModelPattern", () => { }); describe("patterns with invalid thinking levels", () => { - test("sonnet:random returns sonnet with off and warning", () => { + test("sonnet:random returns sonnet with undefined thinking level and warning", () => { const result = parseModelPattern("sonnet:random", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); - test("gpt-4o:invalid returns gpt-4o with off and warning", () => { + test("gpt-4o:invalid returns gpt-4o with undefined thinking level and warning", () => { const result = parseModelPattern("gpt-4o:invalid", allModels); expect(result.model?.id).toBe("gpt-4o"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); }); }); describe("OpenRouter models with colons in IDs", () => { - test("qwen3-coder:exacto matches the model with off", () => { + test("qwen3-coder:exacto matches the model with undefined thinking level", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); @@ -138,7 +138,7 @@ describe("parseModelPattern", () => { const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.model?.provider).toBe("openrouter"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); @@ -157,27 +157,27 @@ describe("parseModelPattern", () => { expect(result.warning).toBeUndefined(); }); - test("gpt-4o:extended matches the extended model", () => { + test("gpt-4o:extended matches the extended model with undefined thinking level", () => { const result = parseModelPattern("openai/gpt-4o:extended", allModels); expect(result.model?.id).toBe("openai/gpt-4o:extended"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); }); describe("invalid thinking levels with OpenRouter models", () => { - test("qwen3-coder:exacto:random returns model with off and warning", () => { + test("qwen3-coder:exacto:random returns model with undefined thinking level and warning", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:random", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); - test("qwen3-coder:exacto:high:random returns model with off and warning", () => { + test("qwen3-coder:exacto:high:random returns model with undefined thinking level and warning", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:high:random", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); @@ -188,7 +188,7 @@ describe("parseModelPattern", () => { // Empty string is included in all model IDs, so partial matching finds a match const result = parseModelPattern("", allModels); expect(result.model).not.toBeNull(); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); }); test("pattern ending with colon treats empty suffix as invalid", () => {