feat(auto-reply): add model fallback lifecycle visibility in status, verbose logs, and WebUI (#20704)

This commit is contained in:
Josh Avant
2026-02-19 14:33:02 -08:00
committed by GitHub
parent 6cdcb5904d
commit c2876b69fb
24 changed files with 1855 additions and 55 deletions

View File

@@ -40,6 +40,8 @@ import {
type ChatCommandDefinition,
} from "./commands-registry.js";
import type { CommandCategory } from "./commands-registry.types.js";
import { resolveActiveFallbackState } from "./fallback-state.js";
import { formatProviderModelRef, resolveSelectedAndActiveModel } from "./model-runtime.js";
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js";
type AgentDefaults = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>;
@@ -72,6 +74,7 @@ type StatusArgs = {
resolvedReasoning?: ReasoningLevel;
resolvedElevated?: ElevatedLevel;
modelAuth?: string;
activeModelAuth?: string;
usageLine?: string;
timeLine?: string;
queue?: QueueStatus;
@@ -339,12 +342,19 @@ export function buildStatusMessage(args: StatusArgs): string {
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
const provider = entry?.providerOverride ?? resolved.provider ?? DEFAULT_PROVIDER;
let model = entry?.modelOverride ?? resolved.model ?? DEFAULT_MODEL;
const selectedProvider = entry?.providerOverride ?? resolved.provider ?? DEFAULT_PROVIDER;
const selectedModel = entry?.modelOverride ?? resolved.model ?? DEFAULT_MODEL;
const modelRefs = resolveSelectedAndActiveModel({
selectedProvider,
selectedModel,
sessionEntry: entry,
});
let activeProvider = modelRefs.active.provider;
let activeModel = modelRefs.active.model;
let contextTokens =
entry?.contextTokens ??
args.agent?.contextTokens ??
lookupContextTokens(model) ??
lookupContextTokens(activeModel) ??
DEFAULT_CONTEXT_TOKENS;
let inputTokens = entry?.inputTokens;
@@ -366,8 +376,18 @@ export function buildStatusMessage(args: StatusArgs): string {
if (!totalTokens || totalTokens === 0 || candidate > totalTokens) {
totalTokens = candidate;
}
if (!model) {
model = logUsage.model ?? model;
if (!entry?.model && logUsage.model) {
const slashIndex = logUsage.model.indexOf("/");
if (slashIndex > 0) {
const provider = logUsage.model.slice(0, slashIndex).trim();
const model = logUsage.model.slice(slashIndex + 1).trim();
if (provider && model) {
activeProvider = provider;
activeModel = model;
}
} else {
activeModel = logUsage.model;
}
}
if (!contextTokens && logUsage.model) {
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
@@ -440,14 +460,21 @@ export function buildStatusMessage(args: StatusArgs): string {
];
const activationLine = activationParts.filter(Boolean).join(" · ");
const authMode = resolveModelAuthMode(provider, args.config);
const authLabelValue =
args.modelAuth ?? (authMode && authMode !== "unknown" ? authMode : undefined);
const showCost = authLabelValue === "api-key" || authLabelValue === "mixed";
const activeAuthMode = resolveModelAuthMode(activeProvider, args.config);
const selectedAuthLabelValue =
args.modelAuth ??
(() => {
const selectedAuthMode = resolveModelAuthMode(selectedProvider, args.config);
return selectedAuthMode && selectedAuthMode !== "unknown" ? selectedAuthMode : undefined;
})();
const activeAuthLabelValue =
args.activeModelAuth ??
(activeAuthMode && activeAuthMode !== "unknown" ? activeAuthMode : undefined);
const showCost = activeAuthLabelValue === "api-key" || activeAuthLabelValue === "mixed";
const costConfig = showCost
? resolveModelCostConfig({
provider,
model,
provider: activeProvider,
model: activeModel,
config: args.config,
})
: undefined;
@@ -464,9 +491,21 @@ export function buildStatusMessage(args: StatusArgs): string {
: undefined;
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
const modelLabel = model ? `${provider}/${model}` : "unknown";
const authLabel = authLabelValue ? ` · 🔑 ${authLabelValue}` : "";
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
const selectedModelLabel = modelRefs.selected.label || "unknown";
const activeModelLabel = formatProviderModelRef(activeProvider, activeModel) || "unknown";
const fallbackState = resolveActiveFallbackState({
selectedModelRef: selectedModelLabel,
activeModelRef: activeModelLabel,
state: entry,
});
const selectedAuthLabel = selectedAuthLabelValue ? ` · 🔑 ${selectedAuthLabelValue}` : "";
const modelLine = `🧠 Model: ${selectedModelLabel}${selectedAuthLabel}`;
const showFallbackAuth = activeAuthLabelValue && activeAuthLabelValue !== selectedAuthLabelValue;
const fallbackLine = fallbackState.active
? `↪️ Fallback: ${activeModelLabel}${
showFallbackAuth ? ` · 🔑 ${activeAuthLabelValue}` : ""
} (${fallbackState.reason ?? "selected model unavailable"})`
: null;
const commit = resolveCommitHash();
const versionLine = `🦞 OpenClaw ${VERSION}${commit ? ` (${commit})` : ""}`;
const usagePair = formatUsagePair(inputTokens, outputTokens);
@@ -480,6 +519,7 @@ export function buildStatusMessage(args: StatusArgs): string {
versionLine,
args.timeLine,
modelLine,
fallbackLine,
usageCostLine,
`📚 ${contextLine}`,
mediaLine,