fix: classify invalid-model fallback errors (#50028)

Merged via squash.

Prepared head SHA: 04b13e09e1
Co-authored-by: xiwuqi <64734786+xiwuqi@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
xiwuqi
2026-04-14 12:32:29 -05:00
committed by GitHub
parent 66e06b50ba
commit 7fbd31818b
5 changed files with 53 additions and 1 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
- Gateway/auth: resolve the active gateway bearer per-request on the HTTP server and the HTTP upgrade handler via `getResolvedAuth()`, mirroring the WebSocket path, so a secret rotated through `secrets.reload` or config hot-reload stops authenticating on `/v1/*`, `/tools/invoke`, plugin HTTP routes, and the canvas upgrade path immediately instead of remaining valid on HTTP until gateway restart. (#66651) Thanks @mmaps.
- Agents/compaction: cap the compaction reserve-token floor to the model context window so small-context local models (e.g. Ollama with 16K tokens) no longer trigger context-overflow errors or infinite compaction loops on every prompt. (#65671) Thanks @openperf.
- Agents/OpenAI Responses: classify the exact `Unknown error (no error details in response)` transport failure as failover reason `unknown` so assistant/model fallback still runs for that no-details failure path. (#65254) Thanks @OpenCodeEngineer.
- Models/probe: surface invalid-model probe failures as `format` instead of `unknown` in `models list --probe`, and lock the invalid-model fallback path in with regression coverage. (#50028) Thanks @xiwuqi.
## 2026.4.14

View File

@@ -334,6 +334,26 @@ describe("failover-error", () => {
).toEqual({ kind: "context_overflow" });
});
it("treats invalid-model HTTP 400 payloads as model_not_found instead of format", () => {
expect(
resolveFailoverReasonFromError({
message: "openrouter/__invalid_test_model__ is not a valid model ID",
}),
).toBe("model_not_found");
expect(
resolveFailoverReasonFromError({
status: 400,
message: "HTTP 400: openrouter/__invalid_test_model__ is not a valid model ID",
}),
).toBe("model_not_found");
expect(
resolveFailoverReasonFromError({
status: 422,
message: "invalid model: openrouter/__invalid_test_model__",
}),
).toBe("model_not_found");
});
it("treats HTTP 422 as format error", () => {
expect(
resolveFailoverReasonFromError({

View File

@@ -591,6 +591,33 @@ describe("runWithModelFallback", () => {
expect(run.mock.calls[1]?.[1]).toBe("gpt-4.1-mini");
});
it("records invalid-model HTTP 400 responses as model_not_found during fallback", async () => {
const cfg = makeCfg();
const run = vi
.fn()
.mockRejectedValueOnce(
Object.assign(
new Error("HTTP 400: openrouter/__invalid_test_model__ is not a valid model ID"),
{ status: 400 },
),
)
.mockResolvedValueOnce("ok");
const result = await runWithModelFallback({
cfg,
provider: "openrouter",
model: "__invalid_test_model__",
run,
});
expect(result.result).toBe("ok");
expect(run).toHaveBeenCalledTimes(2);
expect(result.attempts).toHaveLength(1);
expect(result.attempts[0]?.reason).toBe("model_not_found");
expect(run.mock.calls[1]?.[0]).toBe("openai");
expect(run.mock.calls[1]?.[1]).toBe("gpt-4.1-mini");
});
it("warns when falling back due to model_not_found", async () => {
setLoggerOverride({ level: "silent", consoleLevel: "warn" });
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});

View File

@@ -27,12 +27,13 @@ describe("mapFailoverReasonToProbeStatus", () => {
expect(mapFailoverReasonToProbeStatus("overloaded")).toBe("rate_limit");
expect(mapFailoverReasonToProbeStatus("billing")).toBe("billing");
expect(mapFailoverReasonToProbeStatus("timeout")).toBe("timeout");
expect(mapFailoverReasonToProbeStatus("model_not_found")).toBe("format");
expect(mapFailoverReasonToProbeStatus("format")).toBe("format");
});
it("falls back to unknown for unrecognized values", () => {
expect(mapFailoverReasonToProbeStatus(undefined)).toBe("unknown");
expect(mapFailoverReasonToProbeStatus(null)).toBe("unknown");
expect(mapFailoverReasonToProbeStatus("model_not_found")).toBe("unknown");
expect(mapFailoverReasonToProbeStatus("something_else")).toBe("unknown");
});
});

View File

@@ -121,6 +121,9 @@ export function mapFailoverReasonToProbeStatus(reason?: string | null): AuthProb
if (reason === "timeout") {
return "timeout";
}
if (reason === "model_not_found") {
return "format";
}
if (reason === "format") {
return "format";
}