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}`]); });