Files
2026-07-13 13:39:12 +08:00

287 lines
9.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rate-limit-manager-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const resilienceSettings = await import("../../src/lib/resilience/settings.ts");
const rateLimitManager = await import("../../open-sse/services/rateLimitManager.ts");
const accountFallback = await import("../../open-sse/services/accountFallback.ts");
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function flushBackgroundWork() {
await wait(50);
await new Promise((resolve) => setImmediate(resolve));
}
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.afterEach(async () => {
await rateLimitManager.__resetRateLimitManagerForTests();
await flushBackgroundWork();
});
test.after(async () => {
await rateLimitManager.__resetRateLimitManagerForTests();
await flushBackgroundWork();
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("rate limit manager bypasses disabled connections and exposes inactive status", async () => {
const result = await rateLimitManager.withRateLimit("openai", "disabled-conn", null, async () => {
return "bypassed";
});
assert.equal(result, "bypassed");
assert.deepEqual(rateLimitManager.getRateLimitStatus("openai", "disabled-conn"), {
enabled: false,
active: false,
queued: 0,
running: 0,
});
assert.deepEqual(rateLimitManager.getAllRateLimitStatus(), {});
});
test("rate limit manager handles soft over-limit warnings and normal header learning", async () => {
rateLimitManager.enableRateLimitProtection("conn-over-limit");
rateLimitManager.updateFromHeaders(
"openai",
"conn-over-limit",
{ "x-ratelimit-over-limit": "yes" },
200
);
const softStatus = rateLimitManager.getRateLimitStatus("openai", "conn-over-limit");
assert.equal(softStatus.enabled, true);
assert.equal(softStatus.active, true);
rateLimitManager.enableRateLimitProtection("conn-low-remaining");
rateLimitManager.updateFromHeaders(
"openai",
"conn-low-remaining",
{
"x-ratelimit-limit-requests": "100",
"x-ratelimit-remaining-requests": "5",
"x-ratelimit-reset-requests": "30s",
},
200
);
await rateLimitManager.__flushLearnedLimitsForTests();
const learnedLimits = rateLimitManager.getLearnedLimits();
const learnedEntry = learnedLimits["openai:conn-low-remaining"];
assert.equal(learnedEntry.provider, "openai");
assert.equal(learnedEntry.connectionId, "conn-low-remaining");
assert.equal(learnedEntry.limit, 100);
assert.equal(learnedEntry.remaining, 5);
assert.ok(learnedEntry.minTime > 0);
rateLimitManager.enableRateLimitProtection("conn-high-remaining");
rateLimitManager.updateFromHeaders(
"claude",
"conn-high-remaining",
{
get(name) {
const map = {
"anthropic-ratelimit-requests-limit": "100",
"anthropic-ratelimit-requests-remaining": "70",
"anthropic-ratelimit-requests-reset": new Date(Date.now() + 30_000).toISOString(),
};
return map[name] ?? null;
},
},
200
);
await rateLimitManager.__flushLearnedLimitsForTests();
const allStatuses = rateLimitManager.getAllRateLimitStatus();
assert.ok(allStatuses["openai:conn-over-limit"]);
assert.ok(allStatuses["openai:conn-low-remaining"]);
assert.ok(allStatuses["claude:conn-high-remaining"]);
});
test("rate limit manager handles 429 limiter teardown and disable cleanup", async () => {
rateLimitManager.enableRateLimitProtection("conn-429");
rateLimitManager.updateFromHeaders("openai", "conn-429", { "retry-after": "1s" }, 429, "gpt-4o");
await wait(25);
assert.equal(rateLimitManager.getRateLimitStatus("openai", "conn-429").active, false);
rateLimitManager.enableRateLimitProtection("conn-disable");
rateLimitManager.updateFromHeaders(
"gemini",
"conn-disable",
{
"x-ratelimit-limit-requests": "60",
"x-ratelimit-remaining-requests": "4",
"x-ratelimit-reset-requests": "10s",
},
200,
"gemini-2.5-flash"
);
await rateLimitManager.__flushLearnedLimitsForTests();
assert.ok(rateLimitManager.getAllRateLimitStatus()["gemini:conn-disable:gemini-2.5-flash"]);
rateLimitManager.disableRateLimitProtection("conn-disable");
assert.equal(rateLimitManager.isRateLimitEnabled("conn-disable"), false);
assert.equal(rateLimitManager.getRateLimitStatus("gemini", "conn-disable").active, false);
});
test("rate limit manager uses model-scoped limiter keys for GitHub Copilot (#1624)", async () => {
rateLimitManager.enableRateLimitProtection("conn-github");
rateLimitManager.updateFromHeaders(
"github",
"conn-github",
{
"x-ratelimit-limit-requests": "50",
"x-ratelimit-remaining-requests": "3",
"x-ratelimit-reset-requests": "15s",
},
200,
"gpt-5.1-codex-max"
);
await rateLimitManager.__flushLearnedLimitsForTests();
// GitHub should use model-scoped key: github:conn-github:gpt-5.1-codex-max
const allStatuses = rateLimitManager.getAllRateLimitStatus();
assert.ok(
allStatuses["github:conn-github:gpt-5.1-codex-max"],
"GitHub limiter key should be model-scoped (github:conn:model)"
);
// Verify the limiter state is model-scoped via test helper
const limiterState = await rateLimitManager.__getLimiterStateForTests(
"github",
"conn-github",
"gpt-5.1-codex-max"
);
assert.equal(limiterState?.key, "github:conn-github:gpt-5.1-codex-max");
});
test("rate limit manager parses retry hints from response bodies and locks models", async () => {
rateLimitManager.enableRateLimitProtection("conn-body");
rateLimitManager.updateFromResponseBody(
"openai",
"conn-body",
{
error: {
details: [{ retryDelay: "2s" }],
message: "Please retry later",
},
},
429,
"gpt-4o"
);
assert.equal(accountFallback.getModelLockoutInfo("openai", "conn-body", "gpt-4o"), null);
const limiterState = await rateLimitManager.__getLimiterStateForTests(
"openai",
"conn-body",
"gpt-4o"
);
assert.equal(limiterState?.key, "openai:conn-body");
assert.equal(rateLimitManager.getRateLimitStatus("openai", "conn-body").active, true);
rateLimitManager.updateFromResponseBody(
"openai",
"conn-body",
JSON.stringify({ error: { type: "rate_limit_error" } }),
429,
null
);
assert.equal(rateLimitManager.getRateLimitStatus("openai", "conn-body").active, true);
});
test("RATE_LIMIT_AUTO_ENABLE env var overrides dashboard auto-enable setting", async () => {
const conn = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "Env Override",
apiKey: "sk-env",
isActive: true,
});
// Dashboard says auto-enable on, but env says off → off wins
const original = process.env.RATE_LIMIT_AUTO_ENABLE;
process.env.RATE_LIMIT_AUTO_ENABLE = "false";
try {
await rateLimitManager.initializeRateLimits();
assert.equal(rateLimitManager.isRateLimitEnabled(conn.id), false);
} finally {
if (original === undefined) delete process.env.RATE_LIMIT_AUTO_ENABLE;
else process.env.RATE_LIMIT_AUTO_ENABLE = original;
}
// Reset and verify the opposite: env=true forces on even when dashboard would be off
await rateLimitManager.__resetRateLimitManagerForTests();
process.env.RATE_LIMIT_AUTO_ENABLE = "true";
try {
await rateLimitManager.applyRequestQueueSettings({
...resilienceSettings.DEFAULT_RESILIENCE_SETTINGS.requestQueue,
autoEnableApiKeyProviders: false,
});
assert.equal(rateLimitManager.isRateLimitEnabled(conn.id), true);
} finally {
if (original === undefined) delete process.env.RATE_LIMIT_AUTO_ENABLE;
else process.env.RATE_LIMIT_AUTO_ENABLE = original;
}
});
test("rate limit manager recomputes auto-enabled API key connections when queue settings change", async () => {
const autoConnection = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "Auto OpenAI",
apiKey: "sk-auto",
isActive: true,
});
const explicitConnection = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "Explicit OpenAI",
apiKey: "sk-explicit",
isActive: true,
rateLimitProtection: true,
});
await rateLimitManager.initializeRateLimits();
assert.equal(rateLimitManager.isRateLimitEnabled(autoConnection.id), true);
assert.equal(rateLimitManager.isRateLimitEnabled(explicitConnection.id), true);
assert.ok(rateLimitManager.getAllRateLimitStatus()[`openai:${autoConnection.id}`]);
await rateLimitManager.applyRequestQueueSettings({
...resilienceSettings.DEFAULT_RESILIENCE_SETTINGS.requestQueue,
autoEnableApiKeyProviders: false,
});
assert.equal(rateLimitManager.isRateLimitEnabled(autoConnection.id), false);
assert.equal(rateLimitManager.isRateLimitEnabled(explicitConnection.id), true);
assert.equal(rateLimitManager.getAllRateLimitStatus()[`openai:${autoConnection.id}`], undefined);
await rateLimitManager.applyRequestQueueSettings({
...resilienceSettings.DEFAULT_RESILIENCE_SETTINGS.requestQueue,
autoEnableApiKeyProviders: true,
});
assert.equal(rateLimitManager.isRateLimitEnabled(autoConnection.id), true);
assert.equal(rateLimitManager.isRateLimitEnabled(explicitConnection.id), true);
assert.ok(rateLimitManager.getAllRateLimitStatus()[`openai:${autoConnection.id}`]);
});