Skip to content

Commit 944db73

Browse files
authored
Merge pull request #14 from gitcommit90/fix/context-failure-status
Keep context failures from poisoning account pools
2 parents 9a77ce2 + d56e5eb commit 944db73

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

src/lib/router.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ function classifyFailure(status, errorText) {
225225
status === 429 ||
226226
/rate[ _-]?limit|too many requests|quota|usage[ _-]?limit|resource[ _-]?exhaust|capacity|overload/.test(text);
227227
if (quota) return { eligible: true, kind: "quota", defaultCooldownMs: COOLDOWN_MS.quota };
228+
const request =
229+
/context[ _-]?window|context[ _-]?length[ _-]?exceed|maximum[ _-]?context[ _-]?length|input.{0,80}(?:too[ _-]?long|too[ _-]?large|exceed.{0,40}(?:context|token))|request[ _-]?too[ _-]?large/.test(
230+
text
231+
);
232+
if (request) return { eligible: false, kind: "request", defaultCooldownMs: 0 };
228233
const capability =
229234
(status === 400 || status === 404 || status === 422) &&
230235
(/(?:unsupported|invalid|unknown)[ _-]?model|model[ _-]?(?:not[ _-]?found|unsupported|unavailable)/.test(
@@ -322,13 +327,19 @@ function parseEarlyResponsesFailure(text, response) {
322327
/usage[ _-]?limit|rate[ _-]?limit|quota|resource[ _-]?exhaust|insufficient[ _-]?quota/i.test(
323328
signature
324329
);
330+
const request =
331+
/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(
332+
signature
333+
);
325334
const reportedStatus = Number(error?.status ?? data?.status);
326335
const status =
327336
Number.isInteger(reportedStatus) && reportedStatus >= 400 && reportedStatus <= 599
328337
? reportedStatus
329338
: quota
330339
? 429
331-
: 502;
340+
: request
341+
? 400
342+
: 502;
332343
const message =
333344
error?.message || data?.message || error?.code || error?.type || "Upstream stream failed";
334345
return {

tests/router-fallback.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,79 @@ describe("provider failure classification", () => {
126126
kind: "request",
127127
defaultCooldownMs: 0,
128128
});
129+
assert.deepEqual(
130+
classifyFailure(
131+
502,
132+
"Your input exceeds the context window of this model. Please adjust your input and try again."
133+
),
134+
{ eligible: false, kind: "request", defaultCooldownMs: 0 }
135+
);
129136
});
130137
});
131138

132139
describe("same-provider OAuth account fallback", () => {
140+
it("does not turn context failures into cooldowns or a terminal 429", async () => {
141+
const store = createStore(tmpConfig());
142+
const quotaLock = {
143+
until: Date.now() + 60 * 60_000,
144+
status: 429,
145+
kind: "quota",
146+
reason: "usage limit reached",
147+
};
148+
store.seed({
149+
providers: [
150+
chatgptAccount("prov_a", "token-a", 100),
151+
chatgptAccount("prov_b", "token-b", 200),
152+
chatgptAccount("prov_c", "token-c", 300, { modelLocks: { "*": quotaLock } }),
153+
],
154+
});
155+
const calls = [];
156+
const logger = captureLogger();
157+
const router = createRouter({
158+
store,
159+
logger,
160+
fetchImpl: async (_url, options) => {
161+
calls.push(authToken(options));
162+
return new Response(
163+
[
164+
"event: response.failed",
165+
`data: ${JSON.stringify({
166+
type: "response.failed",
167+
response: {
168+
error: {
169+
message:
170+
"Your input exceeds the context window of this model. Please adjust your input and try again.",
171+
},
172+
},
173+
})}`,
174+
"",
175+
].join("\n"),
176+
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
177+
);
178+
},
179+
});
180+
181+
const result = await router.chatCompletions({
182+
body: {
183+
model: "chatgpt/gpt-5.4",
184+
messages: [{ role: "user", content: "oversized request" }],
185+
stream: true,
186+
},
187+
});
188+
189+
assert.equal(result.ok, false);
190+
assert.equal(result.status, 400);
191+
assert.deepEqual(calls, ["token-a", "token-b"]);
192+
assert.match(result.error.error.message, /ChatGPT \(oauth1\) \[400\]/);
193+
assert.match(result.error.error.message, /ChatGPT \(oauth2\) \[400\]/);
194+
assert.match(result.error.error.message, /ChatGPT \(oauth3\) \[429\]/);
195+
const providers = store.load().providers;
196+
assert.equal(providers.find((provider) => provider.id === "prov_a").modelLocks?.["gpt-5.4"], undefined);
197+
assert.equal(providers.find((provider) => provider.id === "prov_b").modelLocks?.["gpt-5.4"], undefined);
198+
assert.deepEqual(providers.find((provider) => provider.id === "prov_c").modelLocks?.["*"], quotaLock);
199+
assert.equal(logger.entries.find((entry) => entry.meta?.event === "accounts_exhausted").meta.status, 400);
200+
});
201+
133202
it("assigns monotonic oauth aliases and advertises only shared model ids", () => {
134203
const configPath = tmpConfig();
135204
const store = createStore(configPath);

0 commit comments

Comments
 (0)