Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions tests/router-fallback.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading