From 1d4e7899a47f4483d29848eac02e1fb89de2a2ee Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 29 Jun 2026 00:37:24 +0100 Subject: [PATCH] fix: preserve status model alias display --- .../src/reasoning-lane-coordinator.ts | 12 +--- ...atus.summary.runtime.normalization.test.ts | 18 +++++ src/commands/status.summary.runtime.ts | 68 +++++++++++++++++- src/commands/status.summary.test.ts | 69 +++++++++++++++++++ src/commands/status.summary.ts | 45 +++++++++--- 5 files changed, 189 insertions(+), 23 deletions(-) diff --git a/extensions/telegram/src/reasoning-lane-coordinator.ts b/extensions/telegram/src/reasoning-lane-coordinator.ts index 1bcd19c5e9d..6d2d12fc565 100644 --- a/extensions/telegram/src/reasoning-lane-coordinator.ts +++ b/extensions/telegram/src/reasoning-lane-coordinator.ts @@ -94,17 +94,7 @@ export function splitTelegramReasoningText( const taggedReasoning = extractThinkingFromTaggedStreamOutsideCode(text); const strippedAnswer = stripReasoningTagsFromText(text, { mode: "strict", trim: "both" }); - if (isReasoning === true) { - return { reasoningText: formatReasoningMessage(taggedReasoning || strippedAnswer || text) }; - } - - if (!taggedReasoning && strippedAnswer === text) { - return { answerText: text }; - } - - const reasoningText = taggedReasoning ? formatReasoningMessage(taggedReasoning) : undefined; - const answerText = strippedAnswer || undefined; - return { reasoningText, answerText }; + return { reasoningText: formatReasoningMessage(taggedReasoning || strippedAnswer || text) }; } type BufferedFinalAnswer = { diff --git a/src/commands/status.summary.runtime.normalization.test.ts b/src/commands/status.summary.runtime.normalization.test.ts index 985dd36825b..81e95be8d6b 100644 --- a/src/commands/status.summary.runtime.normalization.test.ts +++ b/src/commands/status.summary.runtime.normalization.test.ts @@ -94,6 +94,24 @@ describe("statusSummaryRuntime configured model normalization", () => { model: "opus-4.6", }); + expect( + statusSummaryRuntime.resolveStatusModelComparisonLabel({ + provider: "anthropic", + model: "opus-4.6", + defaultProvider: "anthropic", + }), + ).toBe("anthropic/claude-opus-4-6"); + expect( + statusSummaryRuntime.resolveStatusModelLookupRef({ + provider: "anthropic", + model: "opus-4.6", + defaultProvider: "anthropic", + }), + ).toEqual({ + provider: "anthropic", + model: "claude-opus-4-6", + }); + expect(normalizeProviderModelIdWithManifestMock).not.toHaveBeenCalled(); expect(normalizeProviderModelIdWithRuntimeMock).not.toHaveBeenCalled(); }); diff --git a/src/commands/status.summary.runtime.ts b/src/commands/status.summary.runtime.ts index 37cefd054b3..4efed516dbc 100644 --- a/src/commands/status.summary.runtime.ts +++ b/src/commands/status.summary.runtime.ts @@ -101,6 +101,55 @@ function resolveConfiguredStatusModelRef(params: { return { provider: params.defaultProvider, model: params.defaultModel }; } +function resolveProviderlessPersistedStatusModelRef(params: { + defaultProvider: string; + provider?: unknown; + model?: unknown; +}): { provider: string; model: string } | null { + const provider = normalizeOptionalString(params.provider); + const model = normalizeOptionalString(params.model); + if ( + !model || + provider || + model.includes("/") || + normalizeLowercaseStringOrEmpty(model) === "openrouter:auto" + ) { + return null; + } + // Status rows report the persisted session text. Shared ref parsing still + // canonicalizes provider-local aliases, which would rewrite this display. + return { provider: params.defaultProvider, model }; +} + +function resolveStatusModelLookupRef(params: { + provider?: unknown; + model?: unknown; + defaultProvider?: unknown; +}): { provider: string; model: string } | null { + const provider = normalizeOptionalString(params.provider); + const model = normalizeOptionalString(params.model); + if (!model) { + return null; + } + const defaultProvider = + normalizeOptionalString(params.defaultProvider) ?? provider ?? DEFAULT_PROVIDER; + const raw = provider ? `${provider}/${model}` : model; + const parsed = parseModelRef(raw, defaultProvider, { + allowManifestNormalization: false, + allowPluginNormalization: false, + }); + return parsed ?? { provider: provider ?? defaultProvider, model }; +} + +function resolveStatusModelComparisonLabel(params: { + provider?: unknown; + model?: unknown; + defaultProvider?: unknown; +}): string | null { + const ref = resolveStatusModelLookupRef(params); + return ref ? `${ref.provider}/${ref.model}` : null; +} + function resolveSessionModelRef( cfg: OpenClawConfig, entry?: @@ -114,10 +163,25 @@ function resolveSessionModelRef( defaultModel: DEFAULT_MODEL, agentId, }); + const defaultProvider = resolved.provider || DEFAULT_PROVIDER; + const providerlessPersisted = + resolveProviderlessPersistedStatusModelRef({ + defaultProvider, + provider: entry?.providerOverride, + model: entry?.modelOverride, + }) ?? + resolveProviderlessPersistedStatusModelRef({ + defaultProvider, + provider: entry?.modelProvider, + model: entry?.model, + }); + if (providerlessPersisted) { + return providerlessPersisted; + } return ( // Persisted selected model or overrides describe the active session, not just current config. resolvePersistedSelectedModelRef({ - defaultProvider: resolved.provider || DEFAULT_PROVIDER, + defaultProvider, runtimeProvider: entry?.modelProvider, runtimeModel: entry?.model, overrideProvider: entry?.providerOverride, @@ -171,4 +235,6 @@ export const statusSummaryRuntime = { resolveSessionModelRef, resolveSessionRuntimeLabel, resolveConfiguredStatusModelRef, + resolveStatusModelLookupRef, + resolveStatusModelComparisonLabel, }; diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts index 5702a69cb2e..c36b639e4cf 100644 --- a/src/commands/status.summary.test.ts +++ b/src/commands/status.summary.test.ts @@ -80,6 +80,19 @@ vi.mock("./status.summary.runtime.js", () => ({ model: "gpt-5.5", })), resolveSessionRuntimeLabel: vi.fn(() => "OpenClaw Default"), + resolveStatusModelLookupRef: vi.fn(({ provider, model }) => + typeof model === "string" && model.length > 0 + ? { + provider: typeof provider === "string" && provider.length > 0 ? provider : "openai", + model, + } + : null, + ), + resolveStatusModelComparisonLabel: vi.fn(({ provider, model }) => + typeof model === "string" && model.length > 0 + ? `${typeof provider === "string" && provider.length > 0 ? provider : "openai"}/${model}` + : null, + ), resolveContextTokensForModel: vi.fn(() => 200_000), waitForContextWindowCacheLoad: vi.fn(async () => "idle" as const), }, @@ -721,4 +734,60 @@ describe("getStatusSummary", () => { expect(summary.sessions.recent[0]?.selectedModel).toBe("openai/gpt-5.5-codex"); expect(summary.sessions.recent[0]?.modelSelectionReason).toBeNull(); }); + + it("does not mark provider-local model aliases as pinned mismatches", async () => { + vi.mocked(statusSummaryRuntime.resolveConfiguredStatusModelRef).mockReturnValue({ + provider: "anthropic", + model: "claude-opus-4-8", + }); + vi.mocked(statusSummaryRuntime.resolveSessionModelRef).mockReturnValue({ + provider: "anthropic", + model: "opus", + }); + vi.mocked(statusSummaryRuntime.resolveStatusModelComparisonLabel).mockImplementation( + ({ provider, model }) => { + if (provider === "anthropic" && model === "opus") { + return "anthropic/claude-opus-4-8"; + } + return typeof model === "string" && model.length > 0 + ? `${typeof provider === "string" && provider.length > 0 ? provider : "openai"}/${model}` + : null; + }, + ); + vi.mocked(statusSummaryRuntime.resolveStatusModelLookupRef).mockImplementation( + ({ provider, model }) => { + if (provider === "anthropic" && model === "opus") { + return { provider: "anthropic", model: "claude-opus-4-8" }; + } + return typeof model === "string" && model.length > 0 + ? { + provider: typeof provider === "string" && provider.length > 0 ? provider : "openai", + model, + } + : null; + }, + ); + statusSummaryMocks.listSessionEntries.mockReturnValue( + toSessionEntrySummaries({ + "agent:main:main": { + sessionId: "session-1", + updatedAt: Date.now(), + modelOverride: "opus", + modelOverrideSource: "user", + }, + }), + ); + + const summary = await getStatusSummary(); + + expect(summary.sessions.recent[0]?.configuredModel).toBe("anthropic/claude-opus-4-8"); + expect(summary.sessions.recent[0]?.selectedModel).toBe("anthropic/opus"); + expect(summary.sessions.recent[0]?.modelSelectionReason).toBeNull(); + expect(statusSummaryRuntime.resolveSessionRuntimeLabel).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "anthropic", + model: "claude-opus-4-8", + }), + ); + }); }); diff --git a/src/commands/status.summary.ts b/src/commands/status.summary.ts index 219a7ac735c..fe485c5be1d 100644 --- a/src/commands/status.summary.ts +++ b/src/commands/status.summary.ts @@ -254,6 +254,8 @@ export async function getStatusSummary( resolveContextTokensForModel, resolveSessionRuntimeLabel, resolveSessionModelRef, + resolveStatusModelComparisonLabel, + resolveStatusModelLookupRef, waitForContextWindowCacheLoad, } = await loadStatusSummaryRuntimeModule(); const cfg = options.config ?? getRuntimeConfig(); @@ -399,29 +401,50 @@ export async function getStatusSummary( const configuredSessionModelLabel = `${configuredForSession.provider ?? DEFAULT_PROVIDER}/${configuredSessionModel}`; const resolvedModel = resolveSessionModelRef(cfg, entry, opts.agentIdOverride); const model = resolvedModel.model ?? configuredSessionModel ?? null; + const lookupModel = + resolveStatusModelLookupRef({ + provider: resolvedModel.provider, + model, + defaultProvider: configuredForSession.provider ?? DEFAULT_PROVIDER, + }) ?? resolvedModel; + const lookupModelId = lookupModel.model ?? model; const modelContext = await resolveStaticModelContext( - resolvedModel.provider, - model ?? undefined, + lookupModel.provider, + lookupModelId ?? undefined, ); const selectedModelLabel = resolvedModel.provider && model ? `${resolvedModel.provider}/${model}` : model; + const configuredSessionModelComparisonLabel = resolveStatusModelComparisonLabel({ + provider: configuredForSession.provider ?? DEFAULT_PROVIDER, + model: configuredSessionModel, + defaultProvider: DEFAULT_PROVIDER, + }); + const selectedModelComparisonLabel = resolveStatusModelComparisonLabel({ + provider: resolvedModel.provider, + model, + defaultProvider: configuredForSession.provider ?? DEFAULT_PROVIDER, + }); const modelSelectionDiffers = - selectedModelLabel != null && - selectedModelLabel !== configuredSessionModelLabel && - !areRuntimeModelRefsEquivalent(selectedModelLabel, configuredSessionModelLabel) && + selectedModelComparisonLabel != null && + configuredSessionModelComparisonLabel != null && + selectedModelComparisonLabel !== configuredSessionModelComparisonLabel && + !areRuntimeModelRefsEquivalent( + selectedModelComparisonLabel, + configuredSessionModelComparisonLabel, + ) && hasUserPinnedModelSelection(entry); // Session rows show the live selected model but warn only for user-pinned differences. const contextTokens = resolveContextTokensForModel({ cfg, sourceCfg: contextSourceConfig, - provider: resolvedModel.provider, - model, + provider: lookupModel.provider, + model: lookupModelId, ...modelContext, contextTokensOverride: resolveTrustedSessionContextTokens({ entry, - provider: resolvedModel.provider, - model, + provider: lookupModel.provider, + model: lookupModelId, }), fallbackContextTokens: configContextTokens ?? undefined, allowAsyncLoad: false, @@ -438,8 +461,8 @@ export async function getStatusSummary( const runtime = resolveSessionRuntimeLabel({ cfg, entry, - provider: resolvedModel.provider, - model: model ?? "", + provider: lookupModel.provider, + model: lookupModelId ?? "", agentId, sessionKey: key, });