From 95ee120a91f91ac0392f65bea3fdbb02dc4a5a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=88=E5=AE=9A?= Date: Tue, 14 Apr 2026 02:53:55 +0800 Subject: [PATCH] fix: classify openrouter json 404 model errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the stale branch on top of current `main` and preserves the original issue as regression coverage for the exact OpenRouter JSON 404 payload from #51571. No production behavior changes are introduced here; current `main` already classifies this payload as `model_not_found`, and this merge locks that in across the shared matcher, failover classifier, and fallback loop. Co-authored-by: 屈定 Co-authored-by: Altay --- src/agents/failover-error.test.ts | 20 ++++++++++++++++++++ src/agents/live-model-errors.test.ts | 7 +++++++ src/agents/model-fallback.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index 1a8406c0229..e5f2b6633f4 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -19,6 +19,8 @@ const GEMINI_RESOURCE_EXHAUSTED_MESSAGE = "RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. check quota)."; // OpenRouter 402 billing example: https://openrouter.ai/docs/api-reference/errors const OPENROUTER_CREDITS_MESSAGE = "Payment Required: insufficient credits"; +const OPENROUTER_MODEL_NOT_FOUND_PAYLOAD = + '{"error":{"message":"Healer Alpha was a stealth model revealed on March 18th as an early testing version of MiMo-V2-Omni. Find it here: https://openrouter.ai/xiaomi/mimo-v2-omni","code":404},"user_id":"user_33GTyP8uDSYYbaeBO48AGHXyuMC"}'; const TOGETHER_MONTHLY_SPEND_CAP_MESSAGE = "The account associated with this API key has reached its maximum allowed monthly spending limit."; // Issue-backed Anthropic/OpenAI-compatible insufficient_quota payload under HTTP 400: @@ -195,6 +197,14 @@ describe("failover-error", () => { ).toBe("model_not_found"); }); + it("classifies JSON-wrapped OpenRouter stealth-model 404s as model_not_found", () => { + expect( + resolveFailoverReasonFromError({ + message: OPENROUTER_MODEL_NOT_FOUND_PAYLOAD, + }), + ).toBe("model_not_found"); + }); + it("classifies generic model-does-not-exist messages as model_not_found", () => { expect( resolveFailoverReasonFromError({ @@ -569,6 +579,16 @@ describe("failover-error", () => { expect(err?.model).toBe("claude-opus-4-6"); }); + it("coerces JSON-wrapped OpenRouter stealth-model 404s into FailoverError", () => { + const err = coerceToFailoverError(OPENROUTER_MODEL_NOT_FOUND_PAYLOAD, { + provider: "openrouter", + model: "openrouter/healer-alpha", + }); + + expect(err?.reason).toBe("model_not_found"); + expect(err?.status).toBe(404); + }); + it("maps overloaded to a 503 fallback status", () => { expect(resolveFailoverStatus("overloaded")).toBe(503); }); diff --git a/src/agents/live-model-errors.test.ts b/src/agents/live-model-errors.test.ts index c18f3d8c7df..8a5b97cbc7f 100644 --- a/src/agents/live-model-errors.test.ts +++ b/src/agents/live-model-errors.test.ts @@ -6,10 +6,14 @@ import { describe("live model error helpers", () => { it("detects generic model-not-found messages", () => { + const openRouterJson404Payload = + '{"error":{"message":"Healer Alpha was a stealth model revealed on March 18th as an early testing version of MiMo-V2-Omni. Find it here: https://openrouter.ai/xiaomi/mimo-v2-omni","code":404},"user_id":"user_33GTyP8uDSYYbaeBO48AGHXyuMC"}'; + expect(isModelNotFoundErrorMessage("Model not found: openai/gpt-6")).toBe(true); expect(isModelNotFoundErrorMessage("model_not_found")).toBe(true); expect(isModelNotFoundErrorMessage("The model gpt-foo does not exist.")).toBe(true); expect(isModelNotFoundErrorMessage('{"code":404,"message":"model not found"}')).toBe(true); + expect(isModelNotFoundErrorMessage(openRouterJson404Payload)).toBe(true); expect(isModelNotFoundErrorMessage("model: MiniMax-M2.7-highspeed not found")).toBe(true); expect( isModelNotFoundErrorMessage("404 No endpoints found for deepseek/deepseek-r1:free."), @@ -32,6 +36,9 @@ describe("live model error helpers", () => { expect( isModelNotFoundErrorMessage("The deployment does not exist or you do not have access."), ).toBe(false); + expect(isModelNotFoundErrorMessage('{"error":{"message":"Resource missing","code":404}}')).toBe( + false, + ); expect(isModelNotFoundErrorMessage("request ended without sending any chunks")).toBe(false); }); diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 69f26985e91..e2141e518c9 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -22,6 +22,8 @@ vi.mock("../plugins/provider-runtime.js", () => ({ })); const makeCfg = makeModelFallbackCfg; +const OPENROUTER_MODEL_NOT_FOUND_PAYLOAD = + '{"error":{"message":"Healer Alpha was a stealth model revealed on March 18th as an early testing version of MiMo-V2-Omni. Find it here: https://openrouter.ai/xiaomi/mimo-v2-omni","code":404},"user_id":"user_33GTyP8uDSYYbaeBO48AGHXyuMC"}'; function makeFallbacksOnlyCfg(): OpenClawConfig { return { @@ -569,6 +571,26 @@ describe("runWithModelFallback", () => { expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); }); + it("falls back on JSON-wrapped OpenRouter stealth-model 404s", async () => { + const cfg = makeCfg(); + const run = vi + .fn() + .mockRejectedValueOnce(new Error(OPENROUTER_MODEL_NOT_FOUND_PAYLOAD)) + .mockResolvedValueOnce("ok"); + + const result = await runWithModelFallback({ + cfg, + provider: "openrouter", + model: "openrouter/healer-alpha", + run, + }); + + expect(result.result).toBe("ok"); + expect(run).toHaveBeenCalledTimes(2); + 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(() => {});