mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 05:45:04 +03:00
fix(rate-limit): replace unsupported Bottleneck maxWait with job-level expiration (#1694)
Bottleneck v2.19.5 does not support a `maxWait` limiter/constructor option — it was silently ignored, causing queued jobs to wait indefinitely when no 429 response triggered the drop mechanism. Replace with Bottleneck's supported `expiration` job-schedule option which rejects any job that waits+executes longer than maxWaitMs. Also log expiration rejections so they are observable in production.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
- **fix(codex):** prevent unexpected protocol leakage and fabricated instructions on bare chat completion requests without tools (#1686)
|
||||
- **fix(executors):** truncate tools array to 128 items max in GitHub Copilot and OpenCode executors to mitigate 400 Bad Request errors from upstream (#1687)
|
||||
- **fix:** add body-read timeout to prevent stuck pending requests (#1680)
|
||||
- **fix(rate-limit):** replace unsupported Bottleneck `maxWait` option with job-level `expiration` to prevent indefinite queue stalls (#1694)
|
||||
- **fix:** combo retry loop stops immediately on client disconnect (499) (#1681)
|
||||
- **fix(search):** support optional bearer auth for SearXNG (#1683)
|
||||
- **fix(vision):** respect native GPT vision support — prevents VisionBridge from intercepting models that already handle images natively (#1678)
|
||||
|
||||
@@ -30,7 +30,6 @@ interface LearnedLimitEntry {
|
||||
interface LimiterUpdateSettings {
|
||||
maxConcurrent?: number | null;
|
||||
minTime: number;
|
||||
maxWait?: number | null;
|
||||
reservoir?: number | null;
|
||||
reservoirRefreshAmount?: number | null;
|
||||
reservoirRefreshInterval?: number | null;
|
||||
@@ -76,7 +75,6 @@ function buildLimiterDefaults() {
|
||||
reservoir: currentRequestQueueSettings.requestsPerMinute,
|
||||
reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute,
|
||||
reservoirRefreshInterval: 60 * 1000,
|
||||
maxWait: currentRequestQueueSettings.maxWaitMs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,7 +83,6 @@ function updateAllLimiterSettings() {
|
||||
limiter.updateSettings({
|
||||
maxConcurrent: currentRequestQueueSettings.concurrentRequests,
|
||||
minTime: currentRequestQueueSettings.minTimeBetweenRequestsMs,
|
||||
maxWait: currentRequestQueueSettings.maxWaitMs,
|
||||
reservoir: currentRequestQueueSettings.requestsPerMinute,
|
||||
reservoirRefreshAmount: currentRequestQueueSettings.requestsPerMinute,
|
||||
reservoirRefreshInterval: 60 * 1000,
|
||||
@@ -285,7 +282,21 @@ export async function withRateLimit(provider, connectionId, model, fn) {
|
||||
}
|
||||
|
||||
const limiter = getLimiter(provider, connectionId, model);
|
||||
return limiter.schedule(fn);
|
||||
const maxWaitMs = currentRequestQueueSettings.maxWaitMs;
|
||||
const scheduleOpts = maxWaitMs && maxWaitMs > 0 ? { expiration: maxWaitMs } : {};
|
||||
try {
|
||||
return await limiter.schedule(scheduleOpts, fn);
|
||||
} catch (err) {
|
||||
// Bottleneck throws when a job exceeds its expiration timeout.
|
||||
// Surface as a clear rate-limit timeout so callers can fallback.
|
||||
if (err?.message?.includes("This job timed out")) {
|
||||
const key = getLimiterKey(provider, connectionId, model);
|
||||
console.log(
|
||||
`⏰ [RATE-LIMIT] ${key} — job expired after ${Math.ceil((maxWaitMs || 0) / 1000)}s in queue, dropping`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Header Parsing ──────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user