From 76ed274aad868c7a453eb52373628c37dbac122f Mon Sep 17 00:00:00 2001 From: Ayane Date: Tue, 17 Feb 2026 18:09:05 +0800 Subject: [PATCH] fix(agents): trigger model failover on connection-refused and network-unreachable errors Previously, only ETIMEDOUT / ESOCKETTIMEDOUT / ECONNRESET / ECONNABORTED were recognised as failover-worthy network errors. Connection-level failures such as ECONNREFUSED (server down), ENETUNREACH / EHOSTUNREACH (network disconnected), ENETRESET, and EAI_AGAIN (DNS failure) were treated as unknown errors and did not advance the fallback chain. This is particularly impactful when a local fallback model (e.g. Ollama) is configured: if the remote provider is unreachable due to a network outage, the gateway should fall back to the local model instead of returning an error to the user. Add the missing error codes to resolveFailoverReasonFromError() and corresponding e2e tests. Closes #18868 --- src/agents/failover-error.ts | 14 +++++++++++- src/agents/model-fallback.test.ts | 36 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index ee287d79484..5b3884b29f2 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -183,7 +183,19 @@ export function resolveFailoverReasonFromError(err: unknown): FailoverReason | n } const code = (getErrorCode(err) ?? "").toUpperCase(); - if (["ETIMEDOUT", "ESOCKETTIMEDOUT", "ECONNRESET", "ECONNABORTED"].includes(code)) { + if ( + [ + "ETIMEDOUT", + "ESOCKETTIMEDOUT", + "ECONNRESET", + "ECONNABORTED", + "ECONNREFUSED", + "ENETUNREACH", + "EHOSTUNREACH", + "ENETRESET", + "EAI_AGAIN", + ].includes(code) + ) { return "timeout"; } if (isTimeoutError(err)) { diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index cd0217faafc..14b705fffbd 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -751,6 +751,42 @@ describe("runWithModelFallback", () => { }); }); + it("falls back on ECONNREFUSED (local server down or remote unreachable)", async () => { + await expectFallsBackToHaiku({ + provider: "openai", + model: "gpt-4.1-mini", + firstError: Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:11434"), { + code: "ECONNREFUSED", + }), + }); + }); + + it("falls back on ENETUNREACH (network disconnected)", async () => { + await expectFallsBackToHaiku({ + provider: "openai", + model: "gpt-4.1-mini", + firstError: Object.assign(new Error("connect ENETUNREACH"), { code: "ENETUNREACH" }), + }); + }); + + it("falls back on EHOSTUNREACH (host unreachable)", async () => { + await expectFallsBackToHaiku({ + provider: "openai", + model: "gpt-4.1-mini", + firstError: Object.assign(new Error("connect EHOSTUNREACH"), { code: "EHOSTUNREACH" }), + }); + }); + + it("falls back on EAI_AGAIN (DNS resolution failure)", async () => { + await expectFallsBackToHaiku({ + provider: "openai", + model: "gpt-4.1-mini", + firstError: Object.assign(new Error("getaddrinfo EAI_AGAIN api.openai.com"), { + code: "EAI_AGAIN", + }), + }); + }); + it("falls back on provider abort errors with request-aborted messages", async () => { await expectFallsBackToHaiku({ provider: "openai",