From cad7e7f9e1eef2ff20a3533d3be5026de5a5dfa4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 19:46:56 -0700 Subject: [PATCH] fix: session model selection replaced by response aliases and fallback wins (#122910) * fix(agents): keep session model selection canonical Preserve configured session model identity across response aliases and turn-local fallbacks. * fix(gateway): align session model projection consumers * test(gateway): mark session event model selection --- .../agent-command.live-model-switch.test.ts | 3 + src/agents/command/post-run.ts | 2 + src/agents/command/session-store.ts | 9 ++- .../run/terminal-preparation.test.ts | 6 +- .../run/terminal-preparation.ts | 5 +- .../reply/agent-runner-result-accounting.ts | 3 +- .../agent-runner.runreplyagent.e2e.test.ts | 20 +++--- .../server.chat.gateway-server-chat-b.test.ts | 4 +- .../server.sessions.list-changed.test.ts | 10 ++- .../server.sessions.thinking-e2e.test.ts | 63 +++++++++++++++++++ src/gateway/session-message-events.test.ts | 2 + src/gateway/session-utils-projection.ts | 7 +-- src/gateway/session-utils-row.ts | 33 +++------- src/gateway/session-utils-search.ts | 15 +---- src/gateway/session-utils.search.test.ts | 25 +++++--- src/gateway/session-utils.test.ts | 16 +++-- 16 files changed, 143 insertions(+), 80 deletions(-) diff --git a/src/agents/agent-command.live-model-switch.test.ts b/src/agents/agent-command.live-model-switch.test.ts index cd059949228c..a022be7f2e66 100644 --- a/src/agents/agent-command.live-model-switch.test.ts +++ b/src/agents/agent-command.live-model-switch.test.ts @@ -2445,6 +2445,9 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => { entry: expect.objectContaining({ thinkingLevel: "max" }), }), ); + expectRecordFields(mockCallArg(state.updateSessionStoreAfterAgentRunMock), { + preserveRuntimeModel: true, + }); }); it("recomputes a model-derived thinking default for each fallback candidate", async () => { diff --git a/src/agents/command/post-run.ts b/src/agents/command/post-run.ts index 130d23575e21..2b1e8ff0e6b6 100644 --- a/src/agents/command/post-run.ts +++ b/src/agents/command/post-run.ts @@ -165,6 +165,8 @@ export async function finalizeEmbeddedAgentCommand(params: { touchActivity: !isHeartbeatLifecycleRun && !params.opts.internalEvents?.length, preserveRuntimeModel: fallbackExhausted || + fallbackProvider !== provider || + fallbackModel !== model || isHeartbeatLifecycleRun || params.preserveUserFacingSessionModelState, preserveUserFacingSessionModelState: params.preserveUserFacingSessionModelState, diff --git a/src/agents/command/session-store.ts b/src/agents/command/session-store.ts index 6ae227e6b2a1..121eb2fe1b66 100644 --- a/src/agents/command/session-store.ts +++ b/src/agents/command/session-store.ts @@ -66,8 +66,8 @@ export async function updateSessionStoreAfterAgentRun(params: { /** * When true, preserve the pre-existing runtime model fields (model, * modelProvider, contextTokens) on the session entry instead of overwriting - * them with the model used by this run. Used for heartbeat turns so the - * heartbeat model does not "bleed" into the main session's perceived state. + * them with the model used by this run. Used for turn-local fallback and + * heartbeat runs so their model does not bleed into the session selection. */ preserveRuntimeModel?: boolean; preserveUserFacingSessionModelState?: boolean; @@ -146,9 +146,8 @@ export async function updateSessionStoreAfterAgentRun(params: { ); } if (preserveRuntimeModel) { - // Keep the pre-existing runtime model and context window so a background - // heartbeat turn using a different model does not bleed into the main - // session's perceived state. + // Keep the pre-existing runtime model and context window so a turn-local + // model does not bleed into the session's perceived selection. if (entry.model) { // Prior runtime model exists: preserve its contextTokens. When missing, // leave contextTokens unset rather than falling back to the heartbeat diff --git a/src/agents/embedded-agent-runner/run/terminal-preparation.test.ts b/src/agents/embedded-agent-runner/run/terminal-preparation.test.ts index 89888064dd20..928467d87c38 100644 --- a/src/agents/embedded-agent-runner/run/terminal-preparation.test.ts +++ b/src/agents/embedded-agent-runner/run/terminal-preparation.test.ts @@ -251,7 +251,7 @@ describe("prepareEmbeddedRunTerminal run stats", () => { expect(prepared.agentMeta).not.toHaveProperty("costUsd"); }); - it("builds exact terminal model and successful-tool evidence", async () => { + it("keeps response identity in the terminal receipt without replacing the run model", async () => { const prepared = await prepareStats({ responseModel: "cost-model-rerouted", attempt: { @@ -276,7 +276,7 @@ describe("prepareEmbeddedRunTerminal run stats", () => { requested: { provider: "cost-test-provider", model: "cost-model" }, effective: { provider: "cost-test-provider", - model: "cost-model-rerouted", + model: "cost-model", responseModel: "cost-model-rerouted", }, successfulToolNames: ["exec", "read", "Zeta", "alpha", "zeta"], @@ -285,5 +285,7 @@ describe("prepareEmbeddedRunTerminal run stats", () => { expect( (prepared.agentMeta as { terminalReceipt?: Record }).terminalReceipt, ).not.toHaveProperty("terminalDisposition"); + expect(prepared.agentMeta.model).toBe("cost-model"); + expect(prepared.reportedModelRef.model).toBe("cost-model"); }); }); diff --git a/src/agents/embedded-agent-runner/run/terminal-preparation.ts b/src/agents/embedded-agent-runner/run/terminal-preparation.ts index a44bc5eb267b..e6e3f10f7137 100644 --- a/src/agents/embedded-agent-runner/run/terminal-preparation.ts +++ b/src/agents/embedded-agent-runner/run/terminal-preparation.ts @@ -75,13 +75,12 @@ export function prepareEmbeddedRunTerminal(input: { latestUsage: terminalAssistant?.usage as UsageLike | undefined, lastRunPromptUsage: input.lastRunPromptUsage, }); - const resolvedModelRef = resolveReportedModelRef({ + const reportedModelRef = resolveReportedModelRef({ provider: input.provider, model: input.model, assistant: terminalAssistant, }); - const responseModel = terminalAssistant?.responseModel?.trim() || resolvedModelRef.model; - const reportedModelRef = { ...resolvedModelRef, model: responseModel }; + const responseModel = terminalAssistant?.responseModel?.trim() || reportedModelRef.model; const finalAssistantStopReason = (terminalAssistant?.stopReason ?? "").trim().toLowerCase(); const terminalAssistantCanOwnFinalText = finalAssistantStopReason !== "error" && finalAssistantStopReason !== "aborted"; diff --git a/src/auto-reply/reply/agent-runner-result-accounting.ts b/src/auto-reply/reply/agent-runner-result-accounting.ts index 0163a44e0be6..9aa2119b91af 100644 --- a/src/auto-reply/reply/agent-runner-result-accounting.ts +++ b/src/auto-reply/reply/agent-runner-result-accounting.ts @@ -253,7 +253,8 @@ export async function accountAgentTurn(context: AgentTurnAccountingContext) { compactionTokensAfter: runResult.meta?.agentMeta?.compactionTokensAfter, promptTokens, isHeartbeat, - preserveRuntimeModel: fallbackExhausted, + preserveRuntimeModel: + fallbackExhausted || fallbackTransition.nextState.selectedModel !== undefined, preserveUserFacingSessionModelState: preserveUserFacingSessionState, modelUsed, providerUsed, diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index 7ec9d3dd2d67..60a4141a13c6 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -3439,8 +3439,10 @@ describe("runReplyAgent typing (heartbeat)", () => { for (const testCase of cases) { const sessionEntry = makeSessionEntry({ providerOverride: "openai", - modelOverride: "gpt-5.6-luna", + modelOverride: "gpt-5.6-sol", modelOverrideSource: "user", + modelProvider: "openai", + model: "gpt-5.6-sol", }); await replaceSessionEntry({ storePath, sessionKey: "main" }, sessionEntry); const sessionStore = { main: sessionEntry }; @@ -3451,10 +3453,10 @@ describe("runReplyAgent typing (heartbeat)", () => { vi.spyOn(modelFallbackModule, "runWithModelFallback").mockImplementationOnce(async (args) => { const { run, onFallbackStep } = args; expect(args.provider, testCase.name).toBe("openai"); - expect(args.model, testCase.name).toBe("gpt-5.6-luna"); + expect(args.model, testCase.name).toBe("gpt-5.6-sol"); await onFallbackStep?.({ fallbackStepType: "fallback_step", - fallbackStepFromModel: "openai/gpt-5.6-luna", + fallbackStepFromModel: "openai/gpt-5.6-sol", fallbackStepToModel: "deepinfra/moonshotai/Kimi-K2.5", fallbackStepFromFailureReason: "rate_limit", fallbackStepFinalOutcome: "succeeded", @@ -3467,7 +3469,7 @@ describe("runReplyAgent typing (heartbeat)", () => { attempts: [ { provider: "openai", - model: "gpt-5.6-luna", + model: "gpt-5.6-sol", error: "Provider openai is in cooldown (all profiles unavailable)", reason: "rate_limit", }, @@ -3481,7 +3483,7 @@ describe("runReplyAgent typing (heartbeat)", () => { sessionStore, sessionKey: "main", storePath, - runOverrides: { provider: "openai", model: "gpt-5.6-luna" }, + runOverrides: { provider: "openai", model: "gpt-5.6-sol" }, }); const phases: string[] = []; const off = onAgentEvent((evt) => { @@ -3499,11 +3501,11 @@ describe("runReplyAgent typing (heartbeat)", () => { expect(payload.text, testCase.name).toContain("Model Fallback:"); expect(payload.text, testCase.name).toContain("deepinfra/moonshotai/Kimi-K2.5"); expect(stored.providerOverride, testCase.name).toBe("openai"); - expect(stored.modelOverride, testCase.name).toBe("gpt-5.6-luna"); + expect(stored.modelOverride, testCase.name).toBe("gpt-5.6-sol"); expect(stored.modelOverrideSource, testCase.name).toBe("user"); - expect(stored.modelProvider, testCase.name).toBe("deepinfra"); - expect(stored.model, testCase.name).toBe("moonshotai/Kimi-K2.5"); - expect(stored.fallbackNotice?.selectedModel, testCase.name).toBe("openai/gpt-5.6-luna"); + expect(stored.modelProvider, testCase.name).toBe("openai"); + expect(stored.model, testCase.name).toBe("gpt-5.6-sol"); + expect(stored.fallbackNotice?.selectedModel, testCase.name).toBe("openai/gpt-5.6-sol"); expect(stored.fallbackNotice?.activeModel, testCase.name).toBe( "deepinfra/moonshotai/Kimi-K2.5", ); diff --git a/src/gateway/server.chat.gateway-server-chat-b.test.ts b/src/gateway/server.chat.gateway-server-chat-b.test.ts index 427ca56b881b..64a750dc05a4 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.test.ts @@ -903,13 +903,15 @@ describe("gateway server chat", () => { } }); - test("chat.history exposes persisted and synthetic session metadata for startup hydration", async () => { + test("chat.history exposes selected and synthetic session metadata for startup hydration", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); await createSessionDir(); const updatedAt = Date.now(); await writeStoredMainSession({ updatedAt, + providerOverride: "openai", + modelOverride: "gpt-5", modelProvider: "openai", model: "gpt-5", contextTokens: 128_000, diff --git a/src/gateway/server.sessions.list-changed.test.ts b/src/gateway/server.sessions.list-changed.test.ts index 5a2af4a1b625..321ed642a193 100644 --- a/src/gateway/server.sessions.list-changed.test.ts +++ b/src/gateway/server.sessions.list-changed.test.ts @@ -261,7 +261,7 @@ async function expectListedSessionActiveRun( expect(session.activeRunIds).toEqual(expected ? ["run-1"] : undefined); } -test("sessions.list keeps bulk rows lightweight and uses persisted model fields", async () => { +test("sessions.list keeps bulk rows lightweight and uses selected model fields", async () => { const { storePath } = await createSessionStoreDir(); testState.agentConfig = { models: { @@ -273,6 +273,8 @@ test("sessions.list keeps bulk rows lightweight and uses persisted model fields" main: sessionStoreEntry("sess-parent"), "dashboard:child": sessionStoreEntry("sess-child", { updatedAt: Date.now() - 1_000, + providerOverride: "anthropic", + modelOverride: "test-model-without-catalog-context", modelProvider: "anthropic", model: "test-model-without-catalog-context", modelSelectionLocked: true, @@ -351,13 +353,15 @@ test.each([ ["my-ngc", "deepseek-ai/deepseek-v4-pro"], ["my-ngc:nvidia", "nvidia/nemotron-3-ultra-550b-a55b"], ])( - "sessions.list preserves custom provider %s and nested models over WebSocket", + "sessions.list preserves selected custom provider %s and nested models over WebSocket", async (provider, model) => { const { storePath } = await createSessionStoreDir(); await writeSessionStore({ entries: { main: sessionStoreEntry("sess-parent"), "dashboard:child": sessionStoreEntry("sess-custom-provider", { + providerOverride: provider, + modelOverride: model, modelProvider: provider, model, parentSessionKey: "agent:main:main", @@ -857,6 +861,8 @@ test("sessions.changed mutation events include live usage metadata", async () => await writeSessionStore({ entries: { main: sessionStoreEntry("sess-main", { + providerOverride: "openai", + modelOverride: "gpt-5.3-codex-spark", modelProvider: "openai", model: "gpt-5.3-codex-spark", contextTokens: 123_456, diff --git a/src/gateway/server.sessions.thinking-e2e.test.ts b/src/gateway/server.sessions.thinking-e2e.test.ts index e5b9d2ff91b0..b1b26ff7b168 100644 --- a/src/gateway/server.sessions.thinking-e2e.test.ts +++ b/src/gateway/server.sessions.thinking-e2e.test.ts @@ -12,6 +12,7 @@ import { expect, test, vi } from "vitest"; import { formatThinkingLevels } from "../auto-reply/thinking.js"; import { testState, writeSessionStore } from "./test-helpers.js"; import { + directSessionReq, setupGatewaySessionsHandlerTestHarness, getGatewayConfigModule, getSessionsHandlers, @@ -77,6 +78,7 @@ type ThinkingSession = { key: string; modelProvider?: string; model?: string; + agentRuntime?: { id?: string }; thinkingLevels?: Array<{ label: string }>; thinkingOptions?: string[]; }; @@ -91,6 +93,8 @@ async function listMainSessionWithThinking(params: { primaryModel: string; sessionModelProvider: string; sessionModel: string; + agentRuntime?: "codex" | "openclaw"; + selectedByOverride?: boolean; readPreparedGatewayModelCatalog?: () => Promise< Array<{ provider: string; @@ -104,12 +108,25 @@ async function listMainSessionWithThinking(params: { await createSessionStoreDir(); testState.agentConfig = { model: { primary: params.primaryModel }, + ...(params.agentRuntime + ? { + models: { + [params.primaryModel]: { agentRuntime: { id: params.agentRuntime } }, + }, + } + : {}), }; await writeSessionStore({ entries: { main: sessionStoreEntry("sess-main", { modelProvider: params.sessionModelProvider, model: params.sessionModel, + ...(params.selectedByOverride === false + ? {} + : { + providerOverride: params.sessionModelProvider, + modelOverride: params.sessionModel, + }), }), }, }); @@ -211,3 +228,49 @@ test("e2e #76482: session matching default model inherits default thinking level expect(resolved).toContain("off"); expect(resolved).toContain("high"); }); + +test("session rows keep the selected Codex Sol model when runtime metadata contains a response family", async () => { + const loadSolCatalog = async () => [ + { + provider: "openai", + id: "gpt-5.6-sol", + name: "GPT-5.6-Sol", + reasoning: true, + compat: { + supportedReasoningEfforts: ["low", "medium", "high", "xhigh", "max", "ultra"], + }, + }, + ]; + const { session } = await listMainSessionWithThinking({ + reqId: "req-e2e-codex-sol-family", + primaryModel: "openai/gpt-5.6-sol", + sessionModelProvider: "openai", + sessionModel: "gpt-5.6", + agentRuntime: "codex", + selectedByOverride: false, + readPreparedGatewayModelCatalog: loadSolCatalog, + }); + + expect(session).toMatchObject({ + modelProvider: "openai", + model: "gpt-5.6-sol", + }); + expect(session?.agentRuntime?.id).toBe("codex"); + expect(session?.thinkingOptions).toContain("max"); + + const patchResponse = await directSessionReq( + "sessions.patch", + { key: "main", thinkingLevel: "max" }, + { context: { loadGatewayModelCatalog: loadSolCatalog } }, + ); + expect(patchResponse.ok, patchResponse.error?.message).toBe(true); + expect(patchResponse.error).toBeUndefined(); + expect(patchResponse.payload).toMatchObject({ + ok: true, + resolved: { + modelProvider: "openai", + model: "gpt-5.6-sol", + thinkingLevel: "max", + }, + }); +}); diff --git a/src/gateway/session-message-events.test.ts b/src/gateway/session-message-events.test.ts index 094ea73ef0a2..6da9a686d0aa 100644 --- a/src/gateway/session-message-events.test.ts +++ b/src/gateway/session-message-events.test.ts @@ -1753,6 +1753,8 @@ describe("session.message websocket events", () => { main: { sessionId: "sess-main", updatedAt: Date.now(), + providerOverride: "openai", + modelOverride: "gpt-5.4", modelProvider: "openai", model: "gpt-5.4", contextTokens: 123_456, diff --git a/src/gateway/session-utils-projection.ts b/src/gateway/session-utils-projection.ts index c9aea3e65f90..a85f66731a40 100644 --- a/src/gateway/session-utils-projection.ts +++ b/src/gateway/session-utils-projection.ts @@ -87,14 +87,11 @@ export function resolveSessionSelectedModelRef(params: { agentId: string; rowContext?: SessionListRowContext; allowPluginNormalization?: boolean; -}): ReturnType | null { +}): ReturnType { const override = normalizeStoredOverrideModel({ providerOverride: params.entry?.providerOverride, modelOverride: params.entry?.modelOverride, }); - if (!override.modelOverride) { - return null; - } if (!params.rowContext) { return resolveSessionModelRef(params.cfg, params.entry, params.agentId, { allowPluginNormalization: params.allowPluginNormalization, @@ -103,7 +100,7 @@ export function resolveSessionSelectedModelRef(params: { const key = [ normalizeAgentId(params.agentId), override.providerOverride ?? "", - override.modelOverride, + override.modelOverride ?? "", ].join("\0"); const cached = params.rowContext.selectedModelByOverrideRef.get(key); if (cached) { diff --git a/src/gateway/session-utils-row.ts b/src/gateway/session-utils-row.ts index 98829a8603ef..5e4a1fd7b31e 100644 --- a/src/gateway/session-utils-row.ts +++ b/src/gateway/session-utils-row.ts @@ -234,8 +234,6 @@ export function buildGatewaySessionRow(params: { subagentRun?.model, { allowPluginNormalization: !lightweight }, ); - const runtimeModelPresent = - Boolean(entry?.model?.trim()) || Boolean(entry?.modelProvider?.trim()); const freshSessionTotalTokens = asNonNegativeFiniteNumber(resolveFreshSessionTotalTokens(entry)); const needsTranscriptTotalTokens = freshSessionTotalTokens === undefined; const needsTranscriptContextTokens = resolvePositiveNumber(entry?.contextTokens) === undefined; @@ -263,23 +261,6 @@ export function buildGatewaySessionRow(params: { agentId: sessionAgentId, }) : null; - const preferLiveSubagentModelIdentity = - Boolean(subagentRun?.model?.trim()) && subagentStatus === "running"; - const shouldUseTranscriptModelIdentity = - runtimeModelPresent && - !preferLiveSubagentModelIdentity && - (needsTranscriptTotalTokens || needsTranscriptContextTokens); - const resolvedModelIdentity = { - provider: resolvedModel.provider, - model: resolvedModel.model ?? DEFAULT_MODEL, - }; - const modelIdentity = shouldUseTranscriptModelIdentity - ? { - provider: transcriptUsage?.modelProvider ?? resolvedModelIdentity.provider, - model: transcriptUsage?.model ?? resolvedModelIdentity.model, - } - : resolvedModelIdentity; - const { provider: modelProvider, model } = modelIdentity; const totalTokens = freshSessionTotalTokens ?? asNonNegativeFiniteNumber(transcriptUsage?.totalTokens); const totalTokensFresh = @@ -314,15 +295,15 @@ export function buildGatewaySessionRow(params: { const latestCompactionCheckpoint = buildCompactionCheckpointPreview( resolveLatestCompactionCheckpoint(compactionCheckpoints), ); - const selectedOrRuntimeModelProvider = selectedModel?.provider ?? modelProvider; - const selectedOrRuntimeModel = selectedModel?.model ?? model; + const selectedModelProvider = selectedModel.provider; + const selectedModelId = selectedModel.model; const rowModelIdentity = lightweight - ? { provider: selectedOrRuntimeModelProvider, model: selectedOrRuntimeModel } + ? { provider: selectedModelProvider, model: selectedModelId } : resolveSessionDisplayModelIdentityRefCached({ cfg, agentId: sessionAgentId, - provider: selectedOrRuntimeModelProvider, - model: selectedOrRuntimeModel, + provider: selectedModelProvider, + model: selectedModelId, rowContext: params.rowContext, }); const rowModelProvider = rowModelIdentity.provider; @@ -394,8 +375,8 @@ export function buildGatewaySessionRow(params: { }); const fastModeState = resolveFastModeState({ cfg, - provider: selectedOrRuntimeModelProvider ?? DEFAULT_PROVIDER, - model: selectedOrRuntimeModel ?? DEFAULT_MODEL, + provider: selectedModelProvider, + model: selectedModelId, agentId: sessionAgentId, sessionEntry: entry?.fastMode !== undefined diff --git a/src/gateway/session-utils-search.ts b/src/gateway/session-utils-search.ts index e74f5a436ef7..edd7050ffd8a 100644 --- a/src/gateway/session-utils-search.ts +++ b/src/gateway/session-utils-search.ts @@ -2,7 +2,6 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "@openclaw/normalization-core/string-coerce"; -import { DEFAULT_MODEL } from "../agents/defaults.js"; import type { ModelCatalogEntry } from "../agents/model-catalog.js"; import { resolveSessionModelIdentityRef } from "../agents/session-model-ref.js"; import { getSessionDisplaySubagentRunByChildSessionKey } from "../agents/subagents/registry/subagent-registry-read.js"; @@ -128,17 +127,11 @@ export function resolveSessionListSearchModelFields(params: { subagentRun?.model, { allowPluginNormalization: false }, ); - const modelIdentity = { - provider: resolvedModel.provider, - model: resolvedModel.model ?? DEFAULT_MODEL, - }; - const selectedOrRuntimeModelProvider = selectedModel?.provider ?? modelIdentity.provider; - const selectedOrRuntimeModel = selectedModel?.model ?? modelIdentity.model; const displayModelIdentity = resolveSessionDisplayModelIdentityRefCached({ cfg: params.cfg, agentId, - provider: selectedOrRuntimeModelProvider, - model: selectedOrRuntimeModel, + provider: selectedModel.provider, + model: selectedModel.model, rowContext: params.rowContext, }); const fields: Array = []; @@ -147,9 +140,7 @@ export function resolveSessionListSearchModelFields(params: { model: params.entry?.model, }); addSessionListSearchModelFields(fields, resolvedModel); - if (selectedModel) { - addSessionListSearchModelFields(fields, selectedModel); - } + addSessionListSearchModelFields(fields, selectedModel); addSessionListSearchModelFields(fields, displayModelIdentity); return fields; } diff --git a/src/gateway/session-utils.search.test.ts b/src/gateway/session-utils.search.test.ts index 8be88ac690a0..a8478304f43d 100644 --- a/src/gateway/session-utils.search.test.ts +++ b/src/gateway/session-utils.search.test.ts @@ -96,7 +96,7 @@ describe("filterAndSortSessionEntries search", () => { } }); - test("filters by displayed provider and model identity", () => { + test("filters by selected and stored provider and model identity", () => { const now = Date.now(); const cfg = createModelDefaultsConfig("anthropic/claude-sonnet-4-6"); const store: Record = { @@ -121,12 +121,21 @@ describe("filterAndSortSessionEntries search", () => { } as SessionEntry, }; const cases = [ - { search: "anthropic", expectedKey: "agent:main:inherited-default" }, - { search: "claude-sonnet", expectedKey: "agent:main:inherited-default" }, - { search: "anthropic/claude-sonnet", expectedKey: "agent:main:inherited-default" }, - { search: "openai/gpt-5.5", expectedKey: "agent:main:override" }, - { search: "gemini-3.1", expectedKey: "agent:main:runtime" }, - { search: "google/gemini", expectedKey: "agent:main:runtime" }, + { + search: "anthropic", + expectedKeys: ["agent:main:inherited-default", "agent:main:runtime"], + }, + { + search: "claude-sonnet", + expectedKeys: ["agent:main:inherited-default", "agent:main:runtime"], + }, + { + search: "anthropic/claude-sonnet", + expectedKeys: ["agent:main:inherited-default", "agent:main:runtime"], + }, + { search: "openai/gpt-5.5", expectedKeys: ["agent:main:override"] }, + { search: "gemini-3.1", expectedKeys: ["agent:main:runtime"] }, + { search: "google/gemini", expectedKeys: ["agent:main:runtime"] }, ] as const; for (const testCase of cases) { @@ -137,7 +146,7 @@ describe("filterAndSortSessionEntries search", () => { opts: { search: testCase.search }, now, }), - ).toEqual([testCase.expectedKey]); + ).toEqual(testCase.expectedKeys); } }); diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index a8093f98c334..2357850fa858 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -916,12 +916,16 @@ describe("gateway session utils", () => { store: { upper: { sessionId: "upper", + providerOverride: "custom", + modelOverride: "CaseModel", modelProvider: "custom", model: "CaseModel", updatedAt: 2, } satisfies SessionEntry, lower: { sessionId: "lower", + providerOverride: "custom", + modelOverride: "casemodel", modelProvider: "custom", model: "casemodel", updatedAt: 1, @@ -3359,7 +3363,7 @@ describe("listSessionsFromStore selected model display", () => { }); }); - test("infers canonical provider for bare CLI models before default-provider fallback", () => { + test("ignores bare CLI runtime metadata when the selected default differs", () => { const cfg = createModelDefaultsConfig({ primary: "openai/gpt-5.4", models: { @@ -3382,8 +3386,8 @@ describe("listSessionsFromStore selected model display", () => { opts: {}, }); - expect(result.sessions[0]?.modelProvider).toBe("anthropic"); - expect(result.sessions[0]?.model).toBe("claude-opus-4-7"); + expect(result.sessions[0]?.modelProvider).toBe("openai"); + expect(result.sessions[0]?.model).toBe("gpt-5.4"); }); test("uses qualified selected defaults for rows without runtime model metadata", () => { @@ -3435,7 +3439,7 @@ describe("listSessionsFromStore selected model display", () => { ]); }); - test("uses persisted runtime model metadata before selected defaults", () => { + test("uses selected defaults before persisted runtime model metadata", () => { const cfg = { agents: { defaults: { model: { primary: "openai/gpt-5.4" } }, @@ -3457,8 +3461,8 @@ describe("listSessionsFromStore selected model display", () => { opts: {}, }); - expect(result.sessions[0]?.modelProvider).toBe("openai"); - expect(result.sessions[0]?.model).toBe("gpt-5.5"); + expect(result.sessions[0]?.modelProvider).toBe("anthropic"); + expect(result.sessions[0]?.model).toBe("claude-sonnet-4-6"); }); test("uses complete model overrides without default-model fallback", () => {