mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 17:43:05 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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:");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user