From a4ae4c130c49faad5dfd8c834ad9247b7741d7c6 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 1 Jul 2026 08:18:42 +0000 Subject: [PATCH] fix(coding-agent): preserved explicit :auto suffix in modelRoles The model selector's persistence path dropped the `:auto` selector when parsing role values, producing a warning ('Invalid thinking level "auto"') and rendering the badge as `inherit` instead of `auto`. Reload of the default role also lost the auto state whenever the role value carried an explicit `:auto` suffix instead of relying on `defaultThinkingLevel`. Widen the resolver chain (`parseThinkingSuffix`, `splitThinkingSuffix`, `parseModelString`, `parseModelPattern*`, `ResolvedModelRoleValue`, `ResolvedRoleModel`, `ResolveCliModelResult`) to carry the `AUTO_THINKING` sentinel end to end, and coerce it back to `undefined` at concrete-only boundaries (glob scope patterns, retry fallback, advisor, commit pipeline, guided-goal, bench). Regression tests cover: - `resolveModelRoleValue("provider/model:auto")` returns explicit auto without a warning. - `ModelSelector` renders `DEFAULT (auto)` and `SMOL (auto)` when the role value has `:auto`. - `cycleRoleModels` activates auto thinking on entering a `:auto` role. - Startup resume activates auto thinking when `modelRoles.default` carries `:auto`. Fixes #4128 --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/cli/bench-cli.ts | 7 +- .../src/commit/model-selection.ts | 12 +++- .../coding-agent/src/config/model-resolver.ts | 71 ++++++++++++------- .../coding-agent/src/goals/guided-setup.ts | 8 ++- .../src/modes/components/model-selector.ts | 2 +- packages/coding-agent/src/sdk.ts | 2 +- .../coding-agent/src/session/agent-session.ts | 8 +-- .../src/utils/commit-message-generator.ts | 7 +- .../agent-session-model-persistence.test.ts | 17 +++++ .../test/agent-session-role-thinking.test.ts | 19 +++++ .../coding-agent/test/model-resolver.test.ts | 19 +++++ ...model-selector-role-badge-thinking.test.ts | 41 +++++++++++ 13 files changed, 178 insertions(+), 39 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d1722f2ec..d67003188 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed explicit `provider/model:auto` entries in `modelRoles` collapsing to `inherit` in the model selector and losing their auto state on reload; the `:auto` selector is now preserved as an explicit thinking level end to end ([#4128](https://github.com/can1357/oh-my-pi/issues/4128)). + ## [16.2.12] - 2026-07-01 ### Breaking Changes diff --git a/packages/coding-agent/src/cli/bench-cli.ts b/packages/coding-agent/src/cli/bench-cli.ts index 29913adda..085271829 100644 --- a/packages/coding-agent/src/cli/bench-cli.ts +++ b/packages/coding-agent/src/cli/bench-cli.ts @@ -30,7 +30,7 @@ import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSetting import { Settings } from "../config/settings"; import benchPrompt from "../prompts/bench.md" with { type: "text" }; import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk"; -import { resolveThinkingLevelForModel, shouldDisableReasoning, toReasoningEffort } from "../thinking"; +import { AUTO_THINKING, resolveThinkingLevelForModel, shouldDisableReasoning, toReasoningEffort } from "../thinking"; const DEFAULT_RUNS = 10; const DEFAULT_PAR = 4; @@ -477,7 +477,10 @@ function resolveBenchModels( resolved.push({ selector, model, - thinking: resolveThinkingLevelForModel(model, result.thinkingLevel), + thinking: resolveThinkingLevelForModel( + model, + result.thinkingLevel === AUTO_THINKING ? undefined : result.thinkingLevel, + ), }); } if (errors.length > 0) { diff --git a/packages/coding-agent/src/commit/model-selection.ts b/packages/coding-agent/src/commit/model-selection.ts index db04a84a3..d4b499f44 100644 --- a/packages/coding-agent/src/commit/model-selection.ts +++ b/packages/coding-agent/src/commit/model-selection.ts @@ -11,6 +11,7 @@ import { import { MODEL_ROLE_IDS } from "../config/model-roles"; import type { Settings } from "../config/settings"; import MODEL_PRIO from "../priority.json" with { type: "json" }; +import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../thinking"; export interface ResolvedCommitModel { model: Model; @@ -28,6 +29,13 @@ type CommitModelRegistry = ModelLookupRegistry & getApiKey: (model: Model) => Promise; }; +// Commit-time inference is stateless: session-level auto classification isn't +// available, so an explicit `:auto` selector collapses to "no override" and +// the model's own default level fills in. +function coerceCommitThinkingLevel(level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined { + return level === AUTO_THINKING ? undefined : level; +} + export async function resolvePrimaryModel( override: string | undefined, settings: Settings, @@ -49,7 +57,7 @@ export async function resolvePrimaryModel( return { model, apiKey: modelRegistry.resolver(model), - thinkingLevel: resolved?.thinkingLevel, + thinkingLevel: coerceCommitThinkingLevel(resolved?.thinkingLevel), }; } @@ -67,7 +75,7 @@ export async function resolveSmolModel( return { model: resolvedSmol.model, apiKey: modelRegistry.resolver(resolvedSmol.model), - thinkingLevel: resolvedSmol.thinkingLevel, + thinkingLevel: coerceCommitThinkingLevel(resolvedSmol.thinkingLevel), }; } } diff --git a/packages/coding-agent/src/config/model-resolver.ts b/packages/coding-agent/src/config/model-resolver.ts index f9d57aad3..5089ec800 100644 --- a/packages/coding-agent/src/config/model-resolver.ts +++ b/packages/coding-agent/src/config/model-resolver.ts @@ -29,7 +29,12 @@ import { fuzzyMatch } from "@oh-my-pi/pi-tui"; import { logger } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import MODEL_PRIO from "../priority.json" with { type: "json" }; -import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking"; +import { + AUTO_THINKING, + type ConfiguredThinkingLevel, + parseThinkingLevel, + resolveThinkingLevelForModel, +} from "../thinking"; import { isAuthenticated, kNoAuth, type ModelRegistry } from "./model-registry"; import { MODEL_ROLE_IDS, type ModelRole } from "./model-roles"; import type { Settings } from "./settings"; @@ -81,7 +86,8 @@ interface ModelStringParseOptions extends ThinkingSuffixOptions { } const MAX_THINKING_SUFFIX_OPTIONS: ThinkingSuffixOptions = { allowMaxAlias: true }; -function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): ThinkingLevel | undefined { +function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): ConfiguredThinkingLevel | undefined { + if (value === AUTO_THINKING) return AUTO_THINKING; const level = parseThinkingLevel(value); if (level !== undefined) return level; return options?.allowMaxAlias === true && value === "max" ? ThinkingLevel.XHigh : undefined; @@ -90,17 +96,17 @@ function parseThinkingSuffix(value: string, options?: ThinkingSuffixOptions): Th /** * Split a trailing `:` thinking selector off a model pattern. * - * `level` is set only when the suffix parses as a valid thinking level, in - * which case `base` has the suffix stripped; otherwise `base` is the input. - * `minColonIndex` requires the colon to appear strictly after that index — - * role-alias callers pass `PREFIX_MODEL_ROLE.length` so the base is at least - * as long as the `pi/` prefix. + * `level` is set when the suffix parses as a concrete thinking level or the + * `auto` sentinel, in which case `base` has the suffix stripped; otherwise + * `base` is the input. `minColonIndex` requires the colon to appear strictly + * after that index — role-alias callers pass `PREFIX_MODEL_ROLE.length` so the + * base is at least as long as the `pi/` prefix. */ function splitThinkingSuffix( pattern: string, minColonIndex = -1, options?: ThinkingSuffixOptions, -): { base: string; level?: ThinkingLevel } { +): { base: string; level?: ConfiguredThinkingLevel } { const colonIdx = pattern.lastIndexOf(":"); if (colonIdx <= minColonIndex) return { base: pattern }; const level = parseThinkingSuffix(pattern.slice(colonIdx + 1), options); @@ -119,12 +125,20 @@ function resolveGlobScopePattern( pattern: string, availableModels: readonly Model[], ): { models: Model[]; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } { + // Glob scopes describe which models are enabled, not per-role thinking. + // Coerce the `auto` sentinel to a concrete-only view so scope callers stay + // typed on `ThinkingLevel` and `enabledModels: [\"openai/*:auto\"]` doesn't + // pin a stray per-model level. + const concrete = (level: ConfiguredThinkingLevel | undefined): ThinkingLevel | undefined => + level === AUTO_THINKING ? undefined : level; + const strictSuffix = splitThinkingSuffix(pattern); if (strictSuffix.level !== undefined) { + const thinkingLevel = concrete(strictSuffix.level); return { models: matchingGlobModels(strictSuffix.base, availableModels), - thinkingLevel: strictSuffix.level, - explicitThinkingLevel: true, + thinkingLevel, + explicitThinkingLevel: thinkingLevel !== undefined, }; } @@ -134,10 +148,11 @@ function resolveGlobScopePattern( if (literalMatches.length > 0) { return { models: literalMatches, thinkingLevel: undefined, explicitThinkingLevel: false }; } + const thinkingLevel = concrete(maxSuffix.level); return { models: matchingGlobModels(maxSuffix.base, availableModels), - thinkingLevel: maxSuffix.level, - explicitThinkingLevel: true, + thinkingLevel, + explicitThinkingLevel: thinkingLevel !== undefined, }; } @@ -155,7 +170,7 @@ function resolveGlobScopePattern( export function parseModelString( modelStr: string, options?: ModelStringParseOptions, -): { provider: string; id: string; thinkingLevel?: ThinkingLevel } | undefined { +): { provider: string; id: string; thinkingLevel?: ConfiguredThinkingLevel } | undefined { const slashIdx = modelStr.indexOf("/"); if (slashIdx <= 0) return undefined; const id = modelStr.slice(slashIdx + 1); @@ -208,7 +223,7 @@ export function formatModelStringWithRouting(model: Model): string { return upstream ? `${selector}@${upstream}` : selector; } -export function formatModelSelectorValue(selector: string, thinkingLevel: ThinkingLevel | undefined): string { +export function formatModelSelectorValue(selector: string, thinkingLevel: ConfiguredThinkingLevel | undefined): string { return thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit ? `${selector}:${thinkingLevel}` : selector; } @@ -711,7 +726,7 @@ function matchModel( export interface ParsedModelResult { model: Model | undefined; /** Thinking level if explicitly specified in pattern, undefined otherwise */ - thinkingLevel?: ThinkingLevel; + thinkingLevel?: ConfiguredThinkingLevel; /** Upstream provider slug from an `@upstream` routing selector, if present. */ upstream?: string; warning: string | undefined; @@ -989,7 +1004,7 @@ export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOp */ export interface ResolvedModelRoleValue { model: Model | undefined; - thinkingLevel?: ThinkingLevel; + thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean; warning: string | undefined; } @@ -1021,7 +1036,9 @@ export function resolveModelRoleValue( return { model: resolved.model, thinkingLevel: resolved.explicitThinkingLevel - ? (resolveThinkingLevelForModel(resolved.model, resolved.thinkingLevel) ?? resolved.thinkingLevel) + ? resolved.thinkingLevel === AUTO_THINKING + ? AUTO_THINKING + : (resolveThinkingLevelForModel(resolved.model, resolved.thinkingLevel) ?? resolved.thinkingLevel) : resolved.thinkingLevel, explicitThinkingLevel: resolved.explicitThinkingLevel, warning: resolved.warning, @@ -1048,7 +1065,7 @@ export function extractExplicitThinkingSelector( value: string | undefined, settings?: Settings, options?: ExplicitThinkingSelectorOptions, -): ThinkingLevel | undefined { +): ConfiguredThinkingLevel | undefined { if (!value) return undefined; const normalized = value.trim(); if (!normalized || normalized === DEFAULT_MODEL_ROLE) return undefined; @@ -1127,7 +1144,7 @@ export function resolveModelOverride( modelPatterns: string[], modelRegistry: ModelLookupRegistry, settings?: Settings, -): { model?: Model; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } { +): { model?: Model; thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean } { if (modelPatterns.length === 0) return { explicitThinkingLevel: false }; const availableModels = modelRegistry.getAvailable(); const matchPreferences = getModelMatchPreferences(settings); @@ -1171,7 +1188,7 @@ export async function resolveModelOverrideWithAuthFallback( settings?: Settings, ): Promise<{ model?: Model; - thinkingLevel?: ThinkingLevel; + thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean; authFallbackUsed: boolean; }> { @@ -1207,7 +1224,7 @@ export function resolveRoleSelection( roles: readonly string[], settings: Settings, availableModels: Model[], -): { model: Model; thinkingLevel?: ThinkingLevel } | undefined { +): { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined { const matchPreferences = getModelMatchPreferences(settings); for (const role of roles) { const resolved = resolveModelRoleValue(settings.getModelRole(role), availableModels, { @@ -1232,7 +1249,7 @@ export function resolveRoleSelection( export function resolveAdvisorRoleSelection( settings: Settings, availableModels: Model[], -): { model: Model; thinkingLevel?: ThinkingLevel } | undefined { +): { model: Model; thinkingLevel?: ConfiguredThinkingLevel } | undefined { const resolved = resolveModelRoleValue(`${PREFIX_MODEL_ROLE}advisor`, availableModels, { settings, matchPreferences: getModelMatchPreferences(settings), @@ -1307,7 +1324,13 @@ export async function resolveModelScope( continue; } - addScopedModel(model, thinkingLevel, explicitThinkingLevel); + // Scoped models (Ctrl+P cycling) carry concrete per-model overrides; + // `auto` lives on the session, so drop the sentinel here. + if (thinkingLevel === AUTO_THINKING) { + addScopedModel(model, undefined, false); + } else { + addScopedModel(model, thinkingLevel, explicitThinkingLevel); + } } return scopedModels; @@ -1393,7 +1416,7 @@ export function filterAvailableModelsByEnabledPatterns( export interface ResolveCliModelResult { model: Model | undefined; selector?: string; - thinkingLevel?: ThinkingLevel; + thinkingLevel?: ConfiguredThinkingLevel; warning: string | undefined; error: string | undefined; } diff --git a/packages/coding-agent/src/goals/guided-setup.ts b/packages/coding-agent/src/goals/guided-setup.ts index 83e937f01..8220cfaee 100644 --- a/packages/coding-agent/src/goals/guided-setup.ts +++ b/packages/coding-agent/src/goals/guided-setup.ts @@ -5,7 +5,7 @@ import { extractTextContent, extractToolCall, parseJsonPayload } from "../commit import guidedGoalInterviewPrompt from "../prompts/goals/guided-goal-interview.md" with { type: "text" }; import guidedGoalSystemPrompt from "../prompts/goals/guided-goal-system.md" with { type: "text" }; import type { AgentSession } from "../session/agent-session"; -import { shouldDisableReasoning, toReasoningEffort } from "../thinking"; +import { AUTO_THINKING, shouldDisableReasoning, toReasoningEffort } from "../thinking"; const RESPOND_TOOL_NAME = "respond"; @@ -102,8 +102,10 @@ export async function runGuidedGoalTurn( { apiKey: session.modelRegistry.resolver(resolved.model, session.sessionId), signal: options.signal, - reasoning: toReasoningEffort(resolved.thinkingLevel), - disableReasoning: shouldDisableReasoning(resolved.thinkingLevel), + reasoning: toReasoningEffort(resolved.thinkingLevel === AUTO_THINKING ? undefined : resolved.thinkingLevel), + disableReasoning: shouldDisableReasoning( + resolved.thinkingLevel === AUTO_THINKING ? undefined : resolved.thinkingLevel, + ), toolChoice: { type: "tool", name: RESPOND_TOOL_NAME }, }, { telemetry: resolveTelemetry(session.agent.telemetry, session.sessionId), oneshotKind: "guided_goal_setup" }, diff --git a/packages/coding-agent/src/modes/components/model-selector.ts b/packages/coding-agent/src/modes/components/model-selector.ts index 858711024..8e34aa5cf 100644 --- a/packages/coding-agent/src/modes/components/model-selector.ts +++ b/packages/coding-agent/src/modes/components/model-selector.ts @@ -912,7 +912,7 @@ export class ModelSelectorComponent extends Container { } #getResolvedRoleThinkingLevel( role: string, - resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ThinkingLevel }, + resolved: { explicitThinkingLevel: boolean; thinkingLevel?: ConfiguredThinkingLevel }, ): ConfiguredThinkingLevel { if (resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined) { return resolved.thinkingLevel; diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index aeaf8de90..ed87791e7 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1258,7 +1258,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} ? getRestorableSessionModels(existingSession.models, sessionManager.getLastModelChangeRole()) : []; let restoredSessionModelIndex = -1; - let restoredSessionThinkingLevel: ThinkingLevel | undefined; + let restoredSessionThinkingLevel: ConfiguredThinkingLevel | undefined; if (!hasExplicitModel && !model && sessionModelStrings.length > 0) { logger.time("restoreSessionModel", () => { let failedSessionModel: string | undefined; diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 0fa7704da..df79e418e 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -727,7 +727,7 @@ export interface RoleModelCycleResult { export interface ResolvedRoleModel { role: string; model: Model; - thinkingLevel?: ThinkingLevel; + thinkingLevel?: ConfiguredThinkingLevel; explicitThinkingLevel: boolean; } @@ -877,7 +877,7 @@ function parseRetryFallbackSelector( raw: trimmed, provider: parsed.provider, id: parsed.id, - thinkingLevel: parsed.thinkingLevel, + thinkingLevel: parsed.thinkingLevel === AUTO_THINKING ? undefined : parsed.thinkingLevel, }; } @@ -2102,7 +2102,7 @@ export class AgentSession { if (config.model) { const resolved = resolveModelOverride([config.model], this.#modelRegistry, this.settings); model = resolved.model; - thinkingLevel = resolved.thinkingLevel; + thinkingLevel = resolved.thinkingLevel === AUTO_THINKING ? undefined : resolved.thinkingLevel; if (!model) { this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor"); continue; @@ -2116,7 +2116,7 @@ export class AgentSession { continue; } model = sel.model; - thinkingLevel = sel.thinkingLevel; + thinkingLevel = sel.thinkingLevel === AUTO_THINKING ? undefined : sel.thinkingLevel; } const advisorModel = model; const advisorName = config.name; diff --git a/packages/coding-agent/src/utils/commit-message-generator.ts b/packages/coding-agent/src/utils/commit-message-generator.ts index bd0fc9f63..f78a26b63 100644 --- a/packages/coding-agent/src/utils/commit-message-generator.ts +++ b/packages/coding-agent/src/utils/commit-message-generator.ts @@ -12,7 +12,7 @@ import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model import type { Settings } from "../config/settings"; import MODEL_PRIO from "../priority.json" with { type: "json" }; import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" }; -import { toReasoningEffort } from "../thinking"; +import { AUTO_THINKING, toReasoningEffort } from "../thinking"; const COMMIT_SYSTEM_PROMPT = prompt.render(commitSystemPrompt); const MAX_DIFF_CHARS = 4000; @@ -56,7 +56,10 @@ function getSmolModelCandidates( settings, matchPreferences, }); - addCandidate(configuredSmol.model, configuredSmol.thinkingLevel); + addCandidate( + configuredSmol.model, + configuredSmol.thinkingLevel === AUTO_THINKING ? undefined : configuredSmol.thinkingLevel, + ); for (const pattern of MODEL_PRIO.smol) { const needle = pattern.toLowerCase(); diff --git a/packages/coding-agent/test/agent-session-model-persistence.test.ts b/packages/coding-agent/test/agent-session-model-persistence.test.ts index 8f8d590db..9e69f5349 100644 --- a/packages/coding-agent/test/agent-session-model-persistence.test.ts +++ b/packages/coding-agent/test/agent-session-model-persistence.test.ts @@ -11,6 +11,7 @@ import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage"; import { getRestorableSessionModels } from "@oh-my-pi/pi-coding-agent/session/session-context"; import { EPHEMERAL_MODEL_CHANGE_ROLE } from "@oh-my-pi/pi-coding-agent/session/session-entries"; import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager"; +import { AUTO_THINKING } from "@oh-my-pi/pi-coding-agent/thinking"; import { TempDir } from "@oh-my-pi/pi-utils"; describe("AgentSession model persistence", () => { @@ -408,6 +409,22 @@ describe("AgentSession model persistence", () => { expect(result.session.model?.id).toBe(temporaryModel.id); }); + it("activates auto thinking on startup resume when modelRoles.default carries an explicit :auto suffix", async () => { + const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5"); + const targetSessionFile = await writeRoleModelSession( + modelValue(defaultModel), + modelValue(defaultModel), + "default", + ); + const settings = Settings.isolated(); + settings.setModelRole("default", `${modelValue(defaultModel)}:auto`); + + const result = await createStartupResumeSession(targetSessionFile, settings); + + expect(result.session.model?.id).toBe(defaultModel.id); + expect(result.session.configuredThinkingLevel()).toBe(AUTO_THINKING); + }); + it("lists restorable temporary model before the default fallback", () => { expect( getRestorableSessionModels( diff --git a/packages/coding-agent/test/agent-session-role-thinking.test.ts b/packages/coding-agent/test/agent-session-role-thinking.test.ts index 12536f42b..4c28ce846 100644 --- a/packages/coding-agent/test/agent-session-role-thinking.test.ts +++ b/packages/coding-agent/test/agent-session-role-thinking.test.ts @@ -110,6 +110,25 @@ describe("AgentSession role model thinking behavior", () => { expect(session.thinkingLevel).toBe("off"); }); + it("activates auto thinking when cycling into a role whose value carries an explicit :auto suffix", async () => { + const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5"); + const smolModel = getAnthropicModelOrThrow("claude-sonnet-4-6"); + + await createSession({ + initialModelId: defaultModel.id, + initialThinkingLevel: Effort.High, + modelRoles: { + default: `${defaultModel.provider}/${defaultModel.id}`, + smol: `${smolModel.provider}/${smolModel.id}:auto`, + }, + }); + + const toSmol = await session.cycleRoleModels(["default", "smol"]); + expect(toSmol?.role).toBe("smol"); + expect(toSmol?.model.id).toBe(smolModel.id); + expect(session.configuredThinkingLevel()).toBe(AUTO_THINKING); + }); + it("preserves current thinking when switching into default/no-suffix role", async () => { const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5"); const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6"); diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index a4ef29a97..19f966784 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -611,6 +611,25 @@ describe("resolveModelRoleValue", () => { expect(result.thinkingLevel).toBe(Effort.High); expect(result.explicitThinkingLevel).toBe(true); }); + + test("preserves an explicit :auto suffix as an explicit thinking selector", () => { + const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:auto", allModels); + + expect(result.model?.provider).toBe("anthropic"); + expect(result.model?.id).toBe("claude-sonnet-4-5"); + expect(result.thinkingLevel).toBe("auto"); + expect(result.explicitThinkingLevel).toBe(true); + expect(result.warning).toBeUndefined(); + }); + + test("does not clamp :auto against the model's supported efforts", () => { + // claude-sonnet-4-5 caps at "high"; ensure auto isn't collapsed onto it + // by resolveThinkingLevelForModel. + const result = resolveModelRoleValue("anthropic/claude-sonnet-4-5:auto", allModels); + + expect(result.thinkingLevel).toBe("auto"); + expect(result.explicitThinkingLevel).toBe(true); + }); }); describe("resolveAgentModelPatterns", () => { test("falls back to the active session model when pi/task is unset", () => { diff --git a/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts b/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts index 14112339b..3e74dc631 100644 --- a/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts +++ b/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts @@ -174,6 +174,47 @@ describe("ModelSelector role badge thinking display", () => { expect(rendered).toContain("DEFAULT (auto)"); }); + test("renders DEFAULT (auto) when modelRoles.default carries an explicit :auto suffix", async () => { + installTestTheme(); + const model = getBundledModel("openai", "gpt-5.5"); + if (!model) throw new Error("Expected bundled model openai/gpt-5.5"); + + const settings = Settings.isolated({ + modelRoles: { + default: `${model.provider}/${model.id}:auto`, + }, + }); + + const selector = createSelector(model, settings); + await Bun.sleep(0); + installTestTheme(); + + const rendered = normalizeRenderedText(selector.render(220).join("\n")); + expect(rendered).toContain("DEFAULT (auto)"); + expect(rendered).not.toContain("DEFAULT (inherit)"); + }); + + test("renders SMOL (auto) when modelRoles.smol carries an explicit :auto suffix", async () => { + installTestTheme(); + const model = getBundledModel("openai", "gpt-5.5"); + if (!model) throw new Error("Expected bundled model openai/gpt-5.5"); + + const settings = Settings.isolated({ + modelRoles: { + default: `${model.provider}/${model.id}`, + smol: `${model.provider}/${model.id}:auto`, + }, + }); + + const selector = createSelector(model, settings); + await Bun.sleep(0); + installTestTheme(); + + const rendered = normalizeRenderedText(selector.render(220).join("\n")); + expect(rendered).toContain("SMOL (auto)"); + expect(rendered).not.toContain("SMOL (inherit)"); + }); + test("shows compact auto badges for unconfigured role defaults", async () => { installTestTheme(); const settings = Settings.isolated({});