From d56e5eb44682a1b0e634b76a01d725b0daf7473d Mon Sep 17 00:00:00 2001 From: Joseph Yaksich <294273268+gitcommit90@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:50:54 +0000 Subject: [PATCH] Keep context failures from poisoning account pools --- src/lib/router.js | 13 ++++++- tests/router-fallback.test.js | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/lib/router.js b/src/lib/router.js index 869bcab..ca785b8 100644 --- a/src/lib/router.js +++ b/src/lib/router.js @@ -225,6 +225,11 @@ function classifyFailure(status, errorText) { status === 429 || /rate[ _-]?limit|too many requests|quota|usage[ _-]?limit|resource[ _-]?exhaust|capacity|overload/.test(text); if (quota) return { eligible: true, kind: "quota", defaultCooldownMs: COOLDOWN_MS.quota }; + const request = + /context[ _-]?window|context[ _-]?length[ _-]?exceed|maximum[ _-]?context[ _-]?length|input.{0,80}(?:too[ _-]?long|too[ _-]?large|exceed.{0,40}(?:context|token))|request[ _-]?too[ _-]?large/.test( + text + ); + if (request) return { eligible: false, kind: "request", defaultCooldownMs: 0 }; const capability = (status === 400 || status === 404 || status === 422) && (/(?:unsupported|invalid|unknown)[ _-]?model|model[ _-]?(?:not[ _-]?found|unsupported|unavailable)/.test( @@ -322,13 +327,19 @@ function parseEarlyResponsesFailure(text, response) { /usage[ _-]?limit|rate[ _-]?limit|quota|resource[ _-]?exhaust|insufficient[ _-]?quota/i.test( signature ); + const request = + /context[ _-]?window|context[ _-]?length[ _-]?exceed|maximum[ _-]?context[ _-]?length|input.{0,80}(?:too[ _-]?long|too[ _-]?large|exceed.{0,40}(?:context|token))|request[ _-]?too[ _-]?large/i.test( + signature + ); const reportedStatus = Number(error?.status ?? data?.status); const status = Number.isInteger(reportedStatus) && reportedStatus >= 400 && reportedStatus <= 599 ? reportedStatus : quota ? 429 - : 502; + : request + ? 400 + : 502; const message = error?.message || data?.message || error?.code || error?.type || "Upstream stream failed"; return { diff --git a/tests/router-fallback.test.js b/tests/router-fallback.test.js index 52a6680..23b36d8 100644 --- a/tests/router-fallback.test.js +++ b/tests/router-fallback.test.js @@ -126,10 +126,79 @@ describe("provider failure classification", () => { kind: "request", defaultCooldownMs: 0, }); + assert.deepEqual( + classifyFailure( + 502, + "Your input exceeds the context window of this model. Please adjust your input and try again." + ), + { eligible: false, kind: "request", defaultCooldownMs: 0 } + ); }); }); describe("same-provider OAuth account fallback", () => { + it("does not turn context failures into cooldowns or a terminal 429", async () => { + const store = createStore(tmpConfig()); + const quotaLock = { + until: Date.now() + 60 * 60_000, + status: 429, + kind: "quota", + reason: "usage limit reached", + }; + store.seed({ + providers: [ + chatgptAccount("prov_a", "token-a", 100), + chatgptAccount("prov_b", "token-b", 200), + chatgptAccount("prov_c", "token-c", 300, { modelLocks: { "*": quotaLock } }), + ], + }); + const calls = []; + const logger = captureLogger(); + const router = createRouter({ + store, + logger, + fetchImpl: async (_url, options) => { + calls.push(authToken(options)); + return new Response( + [ + "event: response.failed", + `data: ${JSON.stringify({ + type: "response.failed", + response: { + error: { + message: + "Your input exceeds the context window of this model. Please adjust your input and try again.", + }, + }, + })}`, + "", + ].join("\n"), + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ); + }, + }); + + const result = await router.chatCompletions({ + body: { + model: "chatgpt/gpt-5.4", + messages: [{ role: "user", content: "oversized request" }], + stream: true, + }, + }); + + assert.equal(result.ok, false); + assert.equal(result.status, 400); + assert.deepEqual(calls, ["token-a", "token-b"]); + assert.match(result.error.error.message, /ChatGPT \(oauth1\) \[400\]/); + assert.match(result.error.error.message, /ChatGPT \(oauth2\) \[400\]/); + assert.match(result.error.error.message, /ChatGPT \(oauth3\) \[429\]/); + const providers = store.load().providers; + assert.equal(providers.find((provider) => provider.id === "prov_a").modelLocks?.["gpt-5.4"], undefined); + assert.equal(providers.find((provider) => provider.id === "prov_b").modelLocks?.["gpt-5.4"], undefined); + assert.deepEqual(providers.find((provider) => provider.id === "prov_c").modelLocks?.["*"], quotaLock); + assert.equal(logger.entries.find((entry) => entry.meta?.event === "accounts_exhausted").meta.status, 400); + }); + it("assigns monotonic oauth aliases and advertises only shared model ids", () => { const configPath = tmpConfig(); const store = createStore(configPath);