From f704d0c1d075f8e668110cd8d190bff1f176a089 Mon Sep 17 00:00:00 2001 From: "Andrew B." <37745667+AndrianBalanescu@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:05:51 -0500 Subject: [PATCH] fix(providers): classify 404 as MODEL_NOT_FOUND to stop retry storm (#6829) Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first); the author's chatCore/errorClassifier deltas were re-applied cleanly onto the release tip. Co-authored-by: Andrian B. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../6829-classify-404-model-not-found.md | 1 + open-sse/handlers/chatCore.ts | 10 ++++++++++ open-sse/services/errorClassifier.ts | 10 ++++++++++ tests/unit/error-classifier.test.ts | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+) create mode 100644 changelog.d/fixes/6829-classify-404-model-not-found.md diff --git a/changelog.d/fixes/6829-classify-404-model-not-found.md b/changelog.d/fixes/6829-classify-404-model-not-found.md new file mode 100644 index 0000000000..77a741377c --- /dev/null +++ b/changelog.d/fixes/6829-classify-404-model-not-found.md @@ -0,0 +1 @@ +- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu). diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 676292a4c5..8319e5ba80 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -3306,6 +3306,16 @@ export async function handleChatCore({ console.warn( `[provider] Node ${errorConnectionId} project routing error (${statusCode}) — not banning` ); + } else if (errorType === PROVIDER_ERROR_TYPES.MODEL_NOT_FOUND) { + // 404 — model/endpoint does not exist upstream. Lock the model so the + // retry/backoff loop stops hammering the dead endpoint (which would + // otherwise degenerate into a 429 rate-limit storm). Connection stays + // active since only the specific model is unavailable. (#6827) + const notFoundCooldownMs = COOLDOWN_MS.notFound; + lockModel(provider, errorConnectionId, currentModel, "model_not_found", notFoundCooldownMs); + console.warn( + `[provider] Node ${errorConnectionId} model not found (${statusCode}) for ${currentModel} - locking model for ${Math.ceil(notFoundCooldownMs / 1000)}s (connection stays active)` + ); } } catch { // Best-effort state update; request flow should continue with fallback handling. diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index f68bebea3d..3d978fec4c 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -76,6 +76,7 @@ export const PROVIDER_ERROR_TYPES = { CONTEXT_OVERFLOW: "context_overflow", OAUTH_INVALID_TOKEN: "oauth_invalid_token", EMPTY_CONTENT: "empty_content", + MODEL_NOT_FOUND: "model_not_found", }; export const CONTEXT_OVERFLOW_SIGNALS = [ @@ -144,6 +145,15 @@ export function classifyProviderError( return PROVIDER_ERROR_TYPES.RATE_LIMITED; } + // 404 — model or endpoint not found. Without classification the error + // falls through to `return null`, so no cooldown/lockout is applied and the + // retry/backoff loop keeps hammering the dead endpoint until the upstream + // rate-limits it (404 + 429 storm). Classify as MODEL_NOT_FOUND so the model + // gets locked via the cooldown layer and retries stop. (#6827) + if (statusCode === 404) { + return PROVIDER_ERROR_TYPES.MODEL_NOT_FOUND; + } + if (statusCode === 401) { if (oauthInvalid) { return PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN; diff --git a/tests/unit/error-classifier.test.ts b/tests/unit/error-classifier.test.ts index c3a2ea1589..ef9b740955 100644 --- a/tests/unit/error-classifier.test.ts +++ b/tests/unit/error-classifier.test.ts @@ -120,3 +120,22 @@ test("classifyProviderError: OAuth provider 429 with daily quota signal => QUOTA ); assert.equal(result, PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED); }); + +// #6827 — 404 must be classified as MODEL_NOT_FOUND, not fall through to null. +// Without this, no cooldown/lockout is applied and the retry loop keeps hitting +// the dead endpoint until the upstream rate-limits it (404 + 429 storm). +test("classifyProviderError: 404 => MODEL_NOT_FOUND", () => { + const result = classifyProviderError(404, { + error: { message: "model v0-1.5-md not found" }, + }); + assert.equal(result, PROVIDER_ERROR_TYPES.MODEL_NOT_FOUND); +}); + +test("classifyProviderError: 404 with provider => MODEL_NOT_FOUND", () => { + const result = classifyProviderError( + 404, + { error: { message: "Not Found" } }, + "v0-vercel" + ); + assert.equal(result, PROVIDER_ERROR_TYPES.MODEL_NOT_FOUND); +});