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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Api>;
|
||||
@@ -28,6 +29,13 @@ type CommitModelRegistry = ModelLookupRegistry &
|
||||
getApiKey: (model: Model<Api>) => Promise<string | undefined>;
|
||||
};
|
||||
|
||||
// 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `:<level>` 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<Api>[],
|
||||
): { models: Model<Api>[]; 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<Api>): 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<Api> | 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<Api> | 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<Api>; thinkingLevel?: ThinkingLevel; explicitThinkingLevel: boolean } {
|
||||
): { model?: Model<Api>; 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<Api>;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
thinkingLevel?: ConfiguredThinkingLevel;
|
||||
explicitThinkingLevel: boolean;
|
||||
authFallbackUsed: boolean;
|
||||
}> {
|
||||
@@ -1207,7 +1224,7 @@ export function resolveRoleSelection(
|
||||
roles: readonly string[],
|
||||
settings: Settings,
|
||||
availableModels: Model<Api>[],
|
||||
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
||||
): { model: Model<Api>; 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<Api>[],
|
||||
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
||||
): { model: Model<Api>; 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<Api> | undefined;
|
||||
selector?: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
thinkingLevel?: ConfiguredThinkingLevel;
|
||||
warning: string | undefined;
|
||||
error: string | undefined;
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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({});
|
||||
|
||||
Reference in New Issue
Block a user