fix: preserve status model alias display

This commit is contained in:
Shakker
2026-06-29 00:37:24 +01:00
parent 19cac35a06
commit 1d4e7899a4
5 changed files with 189 additions and 23 deletions

View File

@@ -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 = {

View File

@@ -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();
});

View File

@@ -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,
};

View File

@@ -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",
}),
);
});
});

View File

@@ -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,
});