Files
diegosouzapw--omniroute/tests/integration/combo-provider-exhaustion.test.ts
2026-07-13 13:39:12 +08:00

659 lines
21 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
const harness = await createChatPipelineHarness("combo-provider-exhaustion");
const {
buildClaudeResponse,
buildRequest,
combosDb,
handleChat,
resetStorage,
seedConnection,
settingsDb,
} = harness;
const providersDb = await import("../../src/lib/db/providers.ts");
function toPlainHeaders(headers: any): Record<string, string> {
if (!headers) return {};
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
return Object.fromEntries(
Object.entries(headers).map(([key, value]) => [key, value == null ? "" : String(value)])
);
}
test.beforeEach(async () => {
await resetStorage();
});
test.afterEach(async () => {
await resetStorage();
});
test.after(async () => {
await harness.cleanup();
});
test("fast-skip on quota-exhausted 429: first same-provider target causes remaining same-provider targets to be skipped (#1731)", async () => {
await seedConnection("openai", {
apiKey: "sk-openai-quota-exhausted",
});
await seedConnection("anthropic", {
apiKey: "sk-anthropic-quota-exhausted",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
// Combo with two openai targets and one anthropic
await combosDb.createCombo({
name: "quota-exhausted-combo",
strategy: "priority",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: [
"openai/gpt-4o-mini",
"openai/gpt-3.5-turbo", // Same provider as first target
"anthropic/claude-3-5-sonnet-20241022",
],
});
let openaiCalls = 0;
let anthropicCalls = 0;
let callSequence: string[] = [];
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const apiKeyHeader = headers["x-api-key"] ?? headers["X-Api-Key"];
if (authHeader === "Bearer sk-openai-quota-exhausted") {
openaiCalls += 1;
callSequence.push("openai");
// Return quota exhausted on all openai calls
return new Response(JSON.stringify({ error: { message: "Subscription quota exceeded" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
if (
apiKeyHeader === "sk-anthropic-quota-exhausted" ||
authHeader === "Bearer sk-anthropic-quota-exhausted"
) {
anthropicCalls += 1;
callSequence.push("anthropic");
return buildClaudeResponse("anthropic fallback success");
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
const response = await handleChat(
buildRequest({
body: {
model: "quota-exhausted-combo",
stream: false,
messages: [{ role: "user", content: "test quota exhaustion skip" }],
},
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200, "should return 200");
assert.equal(
body.choices[0].message.content,
"anthropic fallback success",
"should fallback to anthropic"
);
// The key assertion: openai should only be called once (not twice for both gpt-4o-mini and gpt-3.5-turbo)
assert.equal(
openaiCalls,
1,
`openai should be called only once, but was called ${openaiCalls} times. Call sequence: ${callSequence.join(" -> ")}`
);
assert.equal(anthropicCalls, 1, "anthropic should be called once");
});
test("fast-skip on credits-exhausted 429: same-provider targets are skipped (#1731)", async () => {
await seedConnection("openai", {
apiKey: "sk-openai-credits-exhausted",
});
await seedConnection("anthropic", {
apiKey: "sk-anthropic-credits-exhausted",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
await combosDb.createCombo({
name: "credits-exhausted-combo",
strategy: "priority",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: ["openai/gpt-4o-mini", "openai/gpt-3.5-turbo", "anthropic/claude-3-5-sonnet-20241022"],
});
let openaiCalls = 0;
let anthropicCalls = 0;
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const apiKeyHeader = headers["x-api-key"] ?? headers["X-Api-Key"];
if (authHeader === "Bearer sk-openai-credits-exhausted") {
openaiCalls += 1;
if (openaiCalls === 1) {
return new Response(
JSON.stringify({ error: { message: "You exceeded your current usage quota" } }),
{
status: 429,
headers: { "Content-Type": "application/json" },
}
);
}
throw new Error("Second openai call should have been skipped!");
}
if (
apiKeyHeader === "sk-anthropic-credits-exhausted" ||
authHeader === "Bearer sk-anthropic-credits-exhausted"
) {
anthropicCalls += 1;
return buildClaudeResponse("anthropic handled credits exhaustion");
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
const response = await handleChat(
buildRequest({
body: {
model: "credits-exhausted-combo",
stream: false,
messages: [{ role: "user", content: "test credits exhaustion" }],
},
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.choices[0].message.content, "anthropic handled credits exhaustion");
assert.equal(openaiCalls, 1, "openai should only be attempted once");
assert.equal(anthropicCalls, 1, "anthropic should be attempted once");
});
test("no skip on transient 429: plain rate-limit does not skip same-provider targets (#1731)", async () => {
await seedConnection("openai", {
apiKey: "sk-openai-transient-429",
});
await seedConnection("anthropic", {
apiKey: "sk-anthropic-transient-429",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
await combosDb.createCombo({
name: "transient-429-combo",
strategy: "priority",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: ["openai/gpt-4o-mini", "openai/gpt-3.5-turbo", "anthropic/claude-3-5-sonnet-20241022"],
});
let openaiCalls = 0;
let anthropicCalls = 0;
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const apiKeyHeader = headers["x-api-key"] ?? headers["X-Api-Key"];
if (authHeader === "Bearer sk-openai-transient-429") {
openaiCalls += 1;
// Return plain 429 without quota exhaustion signals
return new Response(JSON.stringify({ error: { message: "Too many requests" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
if (
apiKeyHeader === "sk-anthropic-transient-429" ||
authHeader === "Bearer sk-anthropic-transient-429"
) {
anthropicCalls += 1;
return buildClaudeResponse("anthropic recovered from transient 429");
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
const response = await handleChat(
buildRequest({
body: {
model: "transient-429-combo",
stream: false,
messages: [{ role: "user", content: "test transient 429" }],
},
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.choices[0].message.content, "anthropic recovered from transient 429");
// Transient 429 should still try both openai targets (retries), then move to anthropic
assert.ok(openaiCalls >= 2, "openai should be attempted multiple times for transient 429");
assert.equal(anthropicCalls, 1);
});
test("cross-provider not affected: different providers both return 429, both are still attempted (#1731)", async () => {
await seedConnection("openai", {
apiKey: "sk-openai-cross-provider",
});
await seedConnection("anthropic", {
apiKey: "sk-anthropic-cross-provider",
});
await seedConnection("claude", {
apiKey: "sk-claude-cross-provider",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
await combosDb.createCombo({
name: "cross-provider-combo",
strategy: "priority",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: [
"openai/gpt-4o-mini",
"anthropic/claude-3-5-sonnet-20241022",
"claude/claude-3-5-sonnet-20241022",
],
});
let openaiCalls = 0;
let anthropicCalls = 0;
let claudeCalls = 0;
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const apiKeyHeader = headers["x-api-key"] ?? headers["X-Api-Key"];
if (authHeader === "Bearer sk-openai-cross-provider") {
openaiCalls += 1;
return new Response(JSON.stringify({ error: { message: "Rate limited" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
if (
apiKeyHeader === "sk-anthropic-cross-provider" ||
authHeader === "Bearer sk-anthropic-cross-provider"
) {
anthropicCalls += 1;
return new Response(JSON.stringify({ error: { message: "Rate limited" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
if (
apiKeyHeader === "sk-claude-cross-provider" ||
authHeader === "Bearer sk-claude-cross-provider"
) {
claudeCalls += 1;
return buildClaudeResponse("claude succeeded after other providers failed");
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
const response = await handleChat(
buildRequest({
body: {
model: "cross-provider-combo",
stream: false,
messages: [{ role: "user", content: "test cross provider" }],
},
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.choices[0].message.content, "claude succeeded after other providers failed");
// Each different provider should be attempted
assert.equal(openaiCalls, 1);
assert.equal(anthropicCalls, 1);
assert.equal(claudeCalls, 1);
});
test("exhaustion does not persist across requests: second request starts fresh (#1731)", async () => {
// The fast-skip exhaustion sets are request-scoped. The connection-level
// cooldown written by the first 429 is a separate, legitimate layer — clear
// it between requests so this test only observes the request-scoped state.
const openaiConn = (await seedConnection("openai", {
apiKey: "sk-openai-persistence-test",
})) as any;
await seedConnection("anthropic", {
apiKey: "sk-anthropic-persistence-test",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
await combosDb.createCombo({
name: "persistence-test-combo",
strategy: "priority",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: ["openai/gpt-4o-mini", "openai/gpt-3.5-turbo", "anthropic/claude-3-5-sonnet-20241022"],
});
let requestCount = 0;
let openaiCalls = 0;
let anthropicCalls = 0;
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const apiKeyHeader = headers["x-api-key"] ?? headers["X-Api-Key"];
if (authHeader === "Bearer sk-openai-persistence-test") {
openaiCalls += 1;
// First request: openai fails with quota exhaustion
if (requestCount === 0) {
return new Response(JSON.stringify({ error: { message: "Subscription quota exceeded" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
// Second request: openai succeeds
return new Response(JSON.stringify({ choices: [{ message: { content: "openai ok" } }] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (
apiKeyHeader === "sk-anthropic-persistence-test" ||
authHeader === "Bearer sk-anthropic-persistence-test"
) {
anthropicCalls += 1;
return buildClaudeResponse("anthropic handled first request");
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
// First request: openai exhausted -> anthropic succeeds
requestCount = 0;
const response1 = await handleChat(
buildRequest({
body: {
model: "persistence-test-combo",
stream: false,
messages: [{ role: "user", content: "first request" }],
},
})
);
const body1 = (await response1.json()) as any;
assert.equal(response1.status, 200);
assert.equal(body1.choices[0].message.content, "anthropic handled first request");
assert.equal(openaiCalls, 1, "first request: openai called once");
assert.equal(anthropicCalls, 1, "first request: anthropic called once");
// Clear the connection-level cooldown left by the first 429 — we are testing
// the request-scoped exhaustion sets, not the (persistent-by-design) cooldown.
await providersDb.updateProviderConnection(openaiConn.id, {
rateLimitedUntil: null,
testStatus: "active",
lastError: null,
lastErrorType: null,
errorCode: null,
backoffLevel: 0,
});
// Second request: exhaustedProviders should be reset, openai should be tried again
requestCount = 1;
const response2 = await handleChat(
buildRequest({
body: {
model: "persistence-test-combo",
stream: false,
messages: [{ role: "user", content: "second request" }],
},
})
);
const body2 = (await response2.json()) as any;
assert.equal(response2.status, 200);
assert.equal(body2.choices[0].message.content, "openai ok", "second request should try openai");
assert.equal(openaiCalls, 2, "second request: openai should be called again");
assert.equal(anthropicCalls, 1, "second request: anthropic should not be called");
});
test("round-robin path fast-skip: round-robin combo also skips exhausted provider targets (#1731)", async () => {
await seedConnection("openai", {
apiKey: "sk-openai-rr-exhaustion",
});
await seedConnection("anthropic", {
apiKey: "sk-anthropic-rr-exhaustion",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
await combosDb.createCombo({
name: "rr-exhaustion-combo",
strategy: "round-robin",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: ["openai/gpt-4o-mini", "openai/gpt-3.5-turbo", "anthropic/claude-3-5-sonnet-20241022"],
});
let openaiCalls = 0;
let anthropicCalls = 0;
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const apiKeyHeader = headers["x-api-key"] ?? headers["X-Api-Key"];
if (authHeader === "Bearer sk-openai-rr-exhaustion") {
openaiCalls += 1;
if (openaiCalls === 1) {
return new Response(JSON.stringify({ error: { message: "Daily quota exceeded" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
throw new Error("Second openai call should have been skipped!");
}
if (
apiKeyHeader === "sk-anthropic-rr-exhaustion" ||
authHeader === "Bearer sk-anthropic-rr-exhaustion"
) {
anthropicCalls += 1;
return buildClaudeResponse("anthropic handled round-robin exhaustion");
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
const response = await handleChat(
buildRequest({
body: {
model: "rr-exhaustion-combo",
stream: false,
messages: [{ role: "user", content: "test round-robin exhaustion" }],
},
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.choices[0].message.content, "anthropic handled round-robin exhaustion");
assert.equal(openaiCalls, 1, "round-robin should skip second openai target");
assert.equal(anthropicCalls, 1);
});
test("allow rate-limited connections after transient 429 on subsequent targets in same combo", async () => {
const now = Date.now();
await seedConnection("openai", {
name: "openai-rate-limited-reused",
apiKey: "sk-openai-rate-limited-reused",
rateLimitedUntil: new Date(now + 60000).toISOString(),
});
await seedConnection("openai", {
name: "openai-fresh-429",
apiKey: "sk-openai-fresh-429",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
await combosDb.createCombo({
name: "rate-limit-reuse-combo",
strategy: "priority",
config: { maxRetries: 0, retryDelayMs: 0, fallbackDelayMs: 0 },
models: [
"openai/gpt-4o-mini",
"openai/gpt-3.5-turbo",
],
});
let openaiCalls = 0;
let usedApiKeys: string[] = [];
globalThis.fetch = async (_url: string, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
openaiCalls += 1;
if (authHeader === "Bearer sk-openai-fresh-429") {
usedApiKeys.push("fresh");
return new Response(JSON.stringify({ error: { message: "Too many requests" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
if (authHeader === "Bearer sk-openai-rate-limited-reused") {
usedApiKeys.push("rate-limited");
return new Response(
JSON.stringify({ choices: [{ message: { content: "rate limited reuse success" } }] }),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
const response = await handleChat(
buildRequest({
body: {
model: "rate-limit-reuse-combo",
stream: false,
messages: [{ role: "user", content: "test rate limit reuse" }],
},
})
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.choices[0].message.content, "rate limited reuse success");
assert.equal(openaiCalls, 2);
assert.deepEqual(usedApiKeys, ["fresh", "rate-limited"]);
});
test("emergency fallback never sends the failing provider's credentials to another provider's endpoint", async () => {
// Single-model request (no combo). On a quota/billing 429 the emergency
// fallback redirects to nvidia/openai/gpt-oss-120b. The hop must go through
// credential selection for the EMERGENCY provider — it must never re-send
// the exhausted provider's API key to a different provider's URL (the old
// executor-level hop shipped the OpenAI key to integrate.api.nvidia.com).
await seedConnection("openai", {
apiKey: "sk-openai-emergency-leak-guard",
});
await settingsDb.updateSettings({
requestRetry: 0,
maxRetryIntervalSec: 0,
});
const originalRetryDelay = harness.BaseExecutor.RETRY_CONFIG.delayMs;
harness.BaseExecutor.RETRY_CONFIG.delayMs = 0;
const upstreamCalls: Array<{ host: string; usedOpenAIKey: boolean }> = [];
globalThis.fetch = async (url: any, init: any = {}) => {
const headers = toPlainHeaders(init.headers);
const authHeader = headers.authorization ?? headers.Authorization;
const host = new URL(typeof url === "string" ? url : String(url)).host;
const usedOpenAIKey = authHeader === "Bearer sk-openai-emergency-leak-guard";
upstreamCalls.push({ host, usedOpenAIKey });
if (usedOpenAIKey) {
return new Response(JSON.stringify({ error: { message: "Subscription quota exceeded" } }), {
status: 429,
headers: { "Content-Type": "application/json" },
});
}
throw new Error(`unexpected upstream headers: ${JSON.stringify(headers)}`);
};
try {
const response = await handleChat(
buildRequest({
body: {
model: "openai/gpt-4o-mini",
stream: false,
messages: [{ role: "user", content: "test emergency credential leak guard" }],
},
})
);
assert.notEqual(response.status, 200, "request should fail (no fallback available)");
} finally {
harness.BaseExecutor.RETRY_CONFIG.delayMs = originalRetryDelay;
}
// Precise host match (not substring) — `includes("openai.com")` would wrongly
// accept a leak host like `openai.com.evil.com` (CodeQL js/incomplete-url-substring-sanitization).
const isOpenAIHost = (h: string) => {
const host = h.split(":")[0].toLowerCase();
return host === "openai.com" || host.endsWith(".openai.com");
};
const leaks = upstreamCalls.filter((call) => call.usedOpenAIKey && !isOpenAIHost(call.host));
assert.deepEqual(
leaks,
[],
`the OpenAI key must never be sent to a non-OpenAI host: ${JSON.stringify(upstreamCalls)}`
);
assert.ok(
upstreamCalls.every((call) => call.usedOpenAIKey),
`no unauthenticated emergency call should reach upstream either: ${JSON.stringify(upstreamCalls)}`
);
});