fix(status): show configured fallback models in /status output (#33111)

Merged via squash.

Prepared head SHA: 5e590aa68c
Co-authored-by: AnCoSONG <32268203+AnCoSONG@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
Justin Song
2026-04-09 17:13:09 +08:00
committed by GitHub
parent 905e56d191
commit 1b24560392
5 changed files with 188 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
- Config validation: surface the actual offending field for strict-schema union failures in bindings, including top-level unexpected keys on the matching ACP branch. (#40841) Thanks @Hollychou924.
- QQBot/security: replace raw `fetch()` in the image-size probe with SSRF-guarded `fetchRemoteMedia`, fix `resolveRepoRoot()` to walk up to `.git` instead of hardcoding two parent levels, and refresh the raw-fetch allowlist to match the corrected scan. (#63495) Thanks @dims.
- Cron/scheduling: treat `nextRunAtMs <= 0` as invalid across cron update, maintenance, timer, and stale-delivery paths so corrupted zero timestamps self-heal instead of causing immediate runs or skipped deliveries. (#63507) Thanks @WarrenJones.
- Status: show configured fallback models in `/status` and shared session status cards so per-agent fallback configuration is visible before a live failover happens. (#33111) Thanks @AnCoSONG.
## 2026.4.9

View File

@@ -73,4 +73,139 @@ describe("buildStatusReply", () => {
expect(reply?.text).toContain("Think: xhigh");
});
it("shows per-agent fallback overrides in the status card", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
fallbacks: ["google/gemini-2.5-flash"],
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildStatusReply({
cfg,
command: {
isAuthorizedSender: true,
channel: "whatsapp",
} as never,
sessionKey: "agent:kira:main",
provider: "openai",
model: "gpt-5.4",
contextTokens: 0,
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false,
defaultGroupActivation: () => "mention",
});
expect(reply?.text).toContain("Fallbacks: google/gemini-2.5-flash");
expect(reply?.text).not.toContain("Fallbacks: anthropic/claude-sonnet-4-6");
});
it("keeps default fallback config when the agent has no explicit fallback override", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildStatusReply({
cfg,
command: {
isAuthorizedSender: true,
channel: "whatsapp",
} as never,
sessionKey: "agent:kira:main",
provider: "openai",
model: "gpt-5.4",
contextTokens: 0,
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false,
defaultGroupActivation: () => "mention",
});
expect(reply?.text).toContain("Fallbacks: anthropic/claude-sonnet-4-6");
});
it("treats an explicit empty per-agent fallback override as disabling inherited fallbacks", async () => {
const cfg = {
session: { mainKey: "main", scope: "per-sender" },
agents: {
defaults: {
model: {
primary: "openai/gpt-5.4",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
list: [
{
id: "kira",
model: {
primary: "openai/gpt-5.4",
fallbacks: [],
},
},
],
},
channels: {
whatsapp: { allowFrom: ["*"] },
},
} as OpenClawConfig;
const reply = await buildStatusReply({
cfg,
command: {
isAuthorizedSender: true,
channel: "whatsapp",
} as never,
sessionKey: "agent:kira:main",
provider: "openai",
model: "gpt-5.4",
contextTokens: 0,
resolvedVerboseLevel: "off",
resolvedReasoningLevel: "off",
resolveDefaultThinkingLevel: async () => undefined,
isGroup: false,
defaultGroupActivation: () => "mention",
});
expect(reply?.text).not.toContain("Fallbacks:");
});
});

View File

@@ -3,6 +3,7 @@ import {
resolveAgentDir,
resolveDefaultAgentId,
resolveSessionAgentId,
resolveAgentModelFallbacksOverride,
} from "../../agents/agent-scope.js";
import { resolveFastModeState } from "../../agents/fast-mode.js";
import { resolveModelAuthLabel } from "../../agents/model-auth-label.js";
@@ -295,6 +296,7 @@ export async function buildStatusText(params: {
agentId: statusAgentId,
sessionEntry,
}).enabled;
const agentFallbacksOverride = resolveAgentModelFallbacksOverride(cfg, statusAgentId);
const statusText = buildStatusMessage({
config: cfg,
agent: {
@@ -302,6 +304,7 @@ export async function buildStatusText(params: {
model: {
...toAgentModelListLike(agentDefaults.model),
primary: params.primaryModelLabelOverride ?? `${provider}/${model}`,
...(agentFallbacksOverride === undefined ? {} : { fallbacks: agentFallbacksOverride }),
},
...(typeof contextTokens === "number" && contextTokens > 0 ? { contextTokens } : {}),
thinkingDefault: agentConfig?.thinkingDefault ?? agentDefaults.thinkingDefault,

View File

@@ -794,6 +794,41 @@ describe("buildStatusMessage", () => {
expect(normalized).not.toContain("Fallback:");
});
it("shows configured fallback models when provided", () => {
const text = buildStatusMessage({
agent: {
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["google/gemini-2.5-flash", "openai/gpt-5-mini"],
},
},
sessionEntry: { sessionId: "fb1", updatedAt: 0 },
sessionKey: "agent:main:main",
sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 },
modelAuth: "api-key",
});
const normalized = normalizeTestText(text);
expect(normalized).toContain("Fallbacks: google/gemini-2.5-flash, openai/gpt-5-mini");
});
it("omits configured fallbacks line when no fallbacks provided", () => {
const text = buildStatusMessage({
agent: {
model: "anthropic/claude-opus-4-6",
},
sessionEntry: { sessionId: "fb2", updatedAt: 0 },
sessionKey: "agent:main:main",
sessionScope: "per-sender",
queue: { mode: "collect", depth: 0 },
modelAuth: "api-key",
});
const normalized = normalizeTestText(text);
expect(normalized).not.toContain("Fallbacks:");
});
it("keeps provider prefix from configured model", () => {
const text = buildStatusMessage({
agent: {

View File

@@ -789,6 +789,19 @@ export function buildStatusMessage(args: StatusArgs): string {
})();
const modelNote = channelModelNote ? ` · ${channelModelNote}` : "";
const modelLine = `🧠 Model: ${selectedModelLabel}${selectedAuthLabel}${modelNote}`;
// Show configured fallback models (from agent model config)
const configuredFallbacks = (() => {
const modelConfig = args.agent?.model;
if (typeof modelConfig === "object" && modelConfig && Array.isArray(modelConfig.fallbacks)) {
return modelConfig.fallbacks;
}
return undefined;
})();
const configuredFallbacksLine = configuredFallbacks?.length
? `🔄 Fallbacks: ${configuredFallbacks.join(", ")}`
: null;
const showFallbackAuth = activeAuthLabelValue && activeAuthLabelValue !== selectedAuthLabelValue;
const fallbackLine = fallbackState.active
? `↪️ Fallback: ${activeModelLabel}${
@@ -809,6 +822,7 @@ export function buildStatusMessage(args: StatusArgs): string {
versionLine,
args.timeLine,
modelLine,
configuredFallbacksLine,
fallbackLine,
usageCostLine,
cacheLine,