diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index ea26bc9983..6adfe42c0b 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -140,9 +140,7 @@ function resolveMinTime(override: number | undefined | null): number { // Resolve a maxConcurrent override. 0 or missing means "effectively infinite". function resolveMaxConcurrent(override: number | undefined | null): number { - return typeof override === "number" && override > 0 - ? override - : EFFECTIVELY_INFINITE_CONCURRENCY; + return typeof override === "number" && override > 0 ? override : EFFECTIVELY_INFINITE_CONCURRENCY; } function buildLimiterDefaults() { @@ -159,14 +157,9 @@ function buildLimiterDefaults() { } function updateAllLimiterSettings() { + const defaults = buildLimiterDefaults(); for (const limiter of limiters.values()) { - limiter.updateSettings({ - maxConcurrent: currentRequestQueueSettings.concurrentRequests, - minTime: currentRequestQueueSettings.minTimeBetweenRequestsMs, - reservoir: currentRequestQueueSettings.requestsPerMinute, - reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute, - reservoirRefreshInterval: 60 * 1000, - }); + limiter.updateSettings(defaults); } } @@ -234,7 +227,9 @@ function watchdogTick() { limiters.delete(key); lastDispatchAt.delete(key); limiterLastUsed.delete(key); - logRateLimit(`🧹 [RATE-LIMIT] Evicting idle limiter: ${key} (inactive for ${Math.round((now - lastUsed) / 1000)}s)`); + logRateLimit( + `🧹 [RATE-LIMIT] Evicting idle limiter: ${key} (inactive for ${Math.round((now - lastUsed) / 1000)}s)` + ); trackAsyncOperation(limiter.disconnect()); } } @@ -535,7 +530,12 @@ export async function withRateLimit(provider, connectionId, model, fn, signal = // Proactive sliding-window fallback for header-less providers with a declared cap // (Fase 8.2). No-op unless PROVIDER_DEFAULT_RATE_LIMITS has an entry for `provider`. - await awaitProviderDefaultSlot(provider, connectionId, signal, currentRequestQueueSettings.maxWaitMs); + await awaitProviderDefaultSlot( + provider, + connectionId, + signal, + currentRequestQueueSettings.maxWaitMs + ); const limiter = getLimiter(provider, connectionId, model); const maxWaitMs = currentRequestQueueSettings.maxWaitMs; diff --git a/tests/unit/limiter-lifecycle.test.ts b/tests/unit/limiter-lifecycle.test.ts index c7d2609cc6..7d71dbbabf 100644 --- a/tests/unit/limiter-lifecycle.test.ts +++ b/tests/unit/limiter-lifecycle.test.ts @@ -202,3 +202,35 @@ test("after 429 teardown, next withRateLimit must get a fresh limiter and succee ); assert.equal(result, "post-429", "post-429 request must return its value"); }); + +test("request queue refresh treats zero limits as unbounded for existing limiters", async () => { + await flushBackgroundWork(); + + const provider = "openai"; + const connectionId = "lifecycle-test-conn-d"; + + rateLimitManager.enableRateLimitProtection(connectionId); + assert.equal( + await rateLimitManager.withRateLimit( + provider, + connectionId, + null, + async () => "before-refresh" + ), + "before-refresh" + ); + + await rateLimitManager.applyRequestQueueSettings({ + enabled: true, + autoEnableApiKeyProviders: false, + maxWaitMs: 100, + requestsPerMinute: 0, + concurrentRequests: 0, + minTimeBetweenRequestsMs: 0, + }); + + assert.equal( + await rateLimitManager.withRateLimit(provider, connectionId, null, async () => "after-refresh"), + "after-refresh" + ); +});