Skip to content

Stabilization and better retry - #3

Open
yhurynovich wants to merge 26 commits into
nexon33:mainfrom
yhurynovich:main
Open

Stabilization and better retry#3
yhurynovich wants to merge 26 commits into
nexon33:mainfrom
yhurynovich:main

Conversation

@yhurynovich

Copy link
Copy Markdown

Main Proxy Server & System Fixes Documentation

Overview of Changes & Fixes

server.js - Main Proxy Server

Area Original Fixed
Startup Fire-and-forget initializeKeys().catch() Blocking await initializeKeys() - server won't accept requests until keys loaded
Multiple Keys Only reads single OPENROUTER_API_KEYS Splits comma-separated OPENROUTER_API_KEYS into multiple keys
Axios Config No timeout, no trust_env 120s timeout + trust_env: false (ignores system proxy)
Non-streaming Response Returns response.data directly (even if error in body) Validates response body for error.message with NVIDIA patterns → throws to trigger retry
Streaming Response Pipes chunks directly to client Buffers & inspects chunks for error.message with NVIDIA patterns → throws to trigger retry
Error Handling Streaming: ends stream immediately, no retry Both streaming & non-streaming: detects NVIDIA rate limit → waits 1 second → retries up to 3 times with rotated key
NVIDIA Detection None Case-insensitive patterns: Upstream error from Nvidia + ResourceExhausted / rate limit / limit reached

services/KeyManager.js - Key Rotation Logic

Area Original Fixed
NVIDIA Detection Only ResourceExhausted Multiple patterns: ResourceExhausted OR rate limit OR limit reached
Rate Limit Handling Only HTTP 429 HTTP 429 OR NVIDIA error in response body
Key Rotation Sets cooldown on rate limit Sets cooldown + immediately rotates to next key
Failure Tracking Increments on ALL errors Only increments on non-rate-limit errors (prevents deactivating keys for transient NVIDIA limits)
Logging Basic Logs isNvidia: true for debugging

services/utils/sanitize.js - Log Redaction

Area Original Fixed
Fields Redacted Only Authorization header + apiKey in body All common API key fields: Authorization, x-api-key, api_key, key in body

models/ApiKey.js - Identified Issues

Issue Status
No file locking for concurrent access Known limitation - JSON file corruption possible under high concurrency
Reads entire file on every query Known limitation - inefficient for many keys

Key Behavioral Changes

  • ORIGINAL:
    $$\text{Request} \longrightarrow \text{NVIDIA returns 200 with error in body} \longrightarrow \text{Client sees error}$$

  • FIXED:
    $$\text{Request} \longrightarrow \text{Detect NVIDIA error} \longrightarrow \text{Rotate key} \longrightarrow \text{Wait 1s} \longrightarrow \text{Retry (}\times 3\text{)} \longrightarrow \text{Success}$$

Summary: The proxy now transparently handles NVIDIA rate limits — clients never see the "ResourceExhausted" error.


NVIDIA Error Formats Handled

The proxy inspects and detects errors matching these structures (e.g. from system logs):

In SSE Stream Chunk

{
  "error": {
    "message": "Upstream error from Nvidia: ResourceExhausted: Worker local total request limit reached (32/32)"
  }
}

In Non-Streaming Response

{
  "error": {
    "message": "Upstream error from Nvidia: ResourceExhausted: Worker local total request limit reached (32/32)"
  }
}

Environment Variables Supported

Multiple Keys (Comma-Separated)

OPENROUTER_API_KEYS="key1,key2,key3"

Optional Configuration

PORT=3000
HTTP_REFERER=http://localhost:3000
SITE_NAME=OpenRouterProxy

 🔴 Critical Bugs Fixed

 ┌─┬────────────┬───────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────┐
 │#│File        │Bug                                                            │Fix                                                                         │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │1│server.js   │Race condition - Server accepts requests before keys           │Changed to await initializeKeys() (blocking)                                │
 │ │            │initialized                                                    │                                                                            │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │2│server.js   │Multiple keys in env var not supported                         │Now splits comma-separated OPENROUTER_API_KEYS                              │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │3│server.js   │Streaming error handling broken - return in catch didn't exit  │Added explicit return to exit function after streaming error                │
 │ │            │while loop                                                     │                                                                            │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │4│server.js   │No axios timeout - Requests could hang indefinitely            │Added 120s timeout + trust_env: false                                       │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │5│KeyManager. │NVIDIA rate limits not detected - Error is in response body,   │Added isNvidiaRateLimitError() detecting "Upstream error from Nvidia:       │
 │ │js          │not HTTP 429                                                   │ResourceExhausted"                                                          │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │6│KeyManager. │No retry delay for NVIDIA                                      │Added 1-second wait before retry on NVIDIA rate limits                      │
 │ │js          │                                                               │                                                                            │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │7│KeyManager. │Failure count increments on ALL errors (including 4xx)         │Now only rate limits trigger rotation; other errors tracked but don't auto- │
 │ │js          │                                                               │deactivate                                                                  │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │8│sanitize.js │Insufficient redaction - Missed x-api-key, api_key, key in body│Extended to redact all common API key fields                                │
 ├─┼────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────┤
 │9│server.js   │Models endpoint missing timeout                                │Added 30s timeout + trust_env: false                                        │
 └─┴────────────┴───────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────┘

 🔧 Key Improvements

 1. NVIDIA-specific handling: Detects "Upstream error from Nvidia: ResourceExhausted: Worker local total request limit reached (32/32)" in response body,
 waits 1 second, then retries with new key
 2. Proper startup blocking: Server won't accept requests until at least one API key is loaded
3. Robust streaming error handling: Ensures streaming responses are properly terminated on errors
4. Network resilience: Timeouts prevent hanging connections; trust_env: false ignores system proxy settings

📁 Files Modified
- server.js - Main proxy logic (startup, timeouts, retries, streaming)
- services/KeyManager.js - Key rotation, NVIDIA rate limit detection
- services/utils/sanitize.js - Enhanced API key redaction

The server is now production-ready with proper error handling, retry logic, and NVIDIA-specific rate limit handling. Run node add-key.js to add your
OpenRouter API keys, then node server.js to start.
  📝 How It Works Now

  1. Request fails → NVIDIA returns 200 OK with error in body: "Upstream error from Nvidia: ResourceExhausted..."
  2. KeyManager.markKeyError() → Detects NVIDIA rate limit via isNvidiaRateLimitError() → rotates key
  3. server.js catch block → Detects same error → waits 1 second → retries with new key (up to 3 retries)
  4. Streaming → Same logic applies, retries on rate limits
How It Works Now:

1. Request sent with Key A
2. NVIDIA returns 200 OK with body: "Upstream error from Nvidia: ResourceExhausted..."
3. KeyManager.markKeyError() detects NVIDIA rate limit → marks Key A in cooldown
4. server.js catch block detects same error → waits 1 second
5. Retry with Key B (rotated by KeyManager)
6. Success! Client gets response, no error
 Fixes Applied (server.js)

 ┌───────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
 │Scenario       │Fix                                                                                                                                        │
 ├───────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
 │Non-streaming  │After axios.post(), validate response.data.error.message. If NVIDIA rate limit → throw error with isNvidiaRateLimit=true to trigger retry  │
 ├───────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
 │Streaming      │Stream handler reads chunks, detects NVIDIA error in stream data → throws error with isNvidiaRateLimit=true                                │
 ├───────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
 │Both           │Catch block detects error.isNvidiaRateLimit, calls keyManager.markKeyError() (rotates key), waits 1 second, retries (max 3x)               │
 └───────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

 KeyManager.js (already had)
 - isNvidiaRateLimitError() - detects NVIDIA error in response body
 - markKeyError() - rotates key immediately on NVIDIA rate limit
 - 1-minute cooldown for rate-limited keys

 Result

 Request with Key A
   → NVIDIA returns 200 OK with "ResourceExhausted" error in body
   → Response validation catches it → throws error
   → Catch block: markKeyError() rotates to Key B, wait 1s
   → Retry with Key B
   → Success! Client gets clean response

 The "Upstream error from Nvidia: ResourceExhausted" error should now be completely transparent to clients - the proxy handles retries and key rotation
 automatically.
The error appears as a JSON object inside the SSE stream chunk with error.message containing the NVIDIA rate limit message.

Fixes Applied:

┌──────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│File                          │Fix                                                                                                                         │
├──────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│server.js (streaming handler) │Buffer chunks, detect error.message with NVIDIA patterns in stream, throw error with isNvidiaRateLimit=true to trigger retry│
├──────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│server.js (non-streaming)     │Check response.data.error.message for NVIDIA patterns, throw to trigger retry                                               │
├──────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│server.js (catch block)       │Detect NVIDIA errors from both stream and response, wait 1 second, retry up to 3 times                                      │
├──────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│KeyManager.js                 │Enhanced isNvidiaRateLimitError() with broader patterns (rate limit, limit reached)                                         │
├──────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│server.js (startup)           │Blocking await initializeKeys() - no requests accepted until keys loaded                                                    │
├──────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│server.js (axios)             │120s timeout + trust_env: false                                                                                             │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

NVIDIA Detection Patterns (case-insensitive):
- Upstream error from Nvidia + ResourceExhausted
- Upstream error from Nvidia + rate limit
- Upstream error from Nvidia + limit reached

How It Works Now:
1. Request with Key A → NVIDIA returns SSE stream with error chunk containing error.message: "Upstream error from Nvidia: ResourceExhausted..."
2. Stream handler detects this in chunks, throws error with isNvidiaRateLimit=true
3. Catch block calls markKeyError() → KeyManager rotates to Key B + marks Key A in cooldown
4. Waits 1 second → Retries with Key B (up to 3 retries)
5. Client receives clean response - no error visible

The fix handles the exact format from your logs where the error is embedded in SSE stream chunks.
3. Enhanced rate limit error message in KeyManager.js - When all keys are exhausted, rotateKey() now calculates the minimum wait time across all keys'
rateLimitResetAt timestamps and includes it in the error message (e.g., "try again in ~30 seconds" or "try again in ~2 minute(s)").
 Done. Here's what was added:

 New reactivateAllKeys() method (lines 234-261):
 - Fetches all keys (including inactive ones)
 - Reactivates any key that is either:
   - Marked as inactive (isActive: false)
   - Has a rate limit cooldown (rateLimitResetAt set)
 - Resets their failureCount to 0 and clears rateLimitResetAt
 - Logs the bulk reactivation event
 - Returns true if any keys were reactivated

 Updated rotateKey() logic (lines 34-40):
 - When no available keys are found, it first calls reactivateAllKeys()
 - If reactivation succeeds, it recursively calls rotateKey() again to pick a fresh key
 - Only if reactivation fails (no keys to reactivate) does it return the "No available API keys" error with estimated wait time

 Now when all keys are exhausted, the system will:
 1. Attempt to reactivate all keys (giving them a fresh chance)
 2. If successful, immediately retry with a fresh key
 3. Only return an error with wait time estimate if reactivation also fails
Critical Bugs Fixed (7/7)

┌───┬────────────────────────────────────────────────────────┬──────────────────────────────┬───────────────────────────────────────────────────────────────┐
│#  │Bug                                                     │File                          │Fix                                                            │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│1  │ApiKey.js#filterKey - inverted query logic              │models/ApiKey.js:18-33        │Fixed $lte condition to only match actual dates, not null      │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│2  │ApiKey.js#writeKeys - non-atomic writes                 │models/ApiKey.js:81-83        │Write to temp file then rename (atomic on POSIX)               │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│3  │KeyManager.js reactivateAllKeys - resets failureCount   │services/KeyManager.js:272    │Graduated reset: failureCount = Math.max(0, failureCount - 2)  │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│4  │KeyManager.js rotateKey - infinite recursion            │services/KeyManager.js:17-22  │Added depth parameter with max 3 recursion limit               │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│5  │sanitize.js - JSON.stringify fails on circular refs     │services/utils/sanitize.js:2  │Uses structuredClone() with JSON fallback                      │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│6  │logger.js res.json override - crashes if response sent  │services/logger.js:125        │Checks res.headersSent before logging                          │
├───┼────────────────────────────────────────────────────────┼──────────────────────────────┼───────────────────────────────────────────────────────────────┤
│7  │server.js - no request body validation                  │server.js:150-161             │Validates model and messages array                             │
└───┴────────────────────────────────────────────────────────┴──────────────────────────────┴───────────────────────────────────────────────────────────────┘
Logging Configuration Added

Config file: config/logger.json

{
  "logLevel": "warning",
  "console": { "enabled": true, "colorize": true },
  "file": { "enabled": true, "directory": "logs", "maxSize": "20m", "maxFiles": "14d" },
  "categories": { "request": "warning", "error": "error", "key": "info", "stream": "info" }
}

Levels: error < warning < info < debug

Default behavior (logLevel: "warning"):
- [ERROR] - Always shown (errors, exceptions)
- [WARNING] - Shown (deprecated features, potential issues)
- [INFO] - Hidden by default (requests, responses, key rotations)
- [DEBUG] - Hidden (detailed debugging)

Usage:
- Set logLevel: "info" to see requests/responses
- Set logLevel: "debug" for maximum verbosity
- Per-category control via categories object
 Problem: When a NVIDIA rate limit was detected in the SSE stream chunk (Upstream error from Nvidia: ResourceExhausted), the handleStreamingResponse function
 was throwing an error (throw new Error('NVIDIA rate limit in stream')) after some data had already been written to the response. This caused the server to
 crash with "Cannot set headers after they are sent" or similar errors.

 Root Cause: In server.js:162, the code was throwing an error to trigger retry logic, but this happened mid-stream after headers and data were already sent.

 Fix Applied: Modified handleStreamingResponse function (lines 102-181) to:
 1. Detect NVIDIA rate limit in SSE chunks (as before)
 2. Instead of throwing, write the error to the stream as a proper SSE error event
 3. End the response gracefully
 4. Return { rateLimitHandled: true } to indicate the error was handled
  Now when NVIDIA rate limits are detected, the logs will show:

  2026-07-29T17:27:18.192Z [ERROR] NVIDIA rate limit detected in SSE chunk {"context":"Stream","errorMessage":"Upstream error from Nvidia: Re
  Worker local total request limit reached (32/32)"}

  instead of the previous:

  [Stream] NVIDIA rate limit detected in SSE chunk: Upstream error from Nvidia: ResourceExhausted: Worker local total request limit reached (
  The timestamps use the ISO 8601 format (e.g., 2026-07-29T17:27:18.192Z) and include the log level [ERROR] as configured in the logger.
Critical Issues Fixed
1. AbortController wired to Axios - Created AbortController before axios call, passed signal: abortController.signal to axios config, passed controller to
handleStreamingResponse, aborts on client close/buffer overflow

High-Priority Issues Fixed
2. Admin endpoint dedicated rate limiting - Added adminLimiter (10 req/min) applied to /admin/keys endpoint
3. Header injection prevention - Added sanitizeHeaderValue() function that strips newlines/carriage returns and limits length to 500 chars
4. Plaintext key storage noted - Documented need for encryption at rest (future enhancement)

Medium-Priority Issues Fixed
4. CSP header removed - Removed unnecessary Content-Security-Policy header from API-only service
5. Redundant buffer processing - processBuffer() returns true on error, main loop breaks immediately, no redundant processing after error
6. Console.log replaced with structured logging - All console.log/warn calls replaced with logError/logKeyEvent

Code Style & Maintainability
7. Magic numbers replaced with named constants - Added UNIX_TIMESTAMP_THRESHOLD, MILLISECOND_THRESHOLD, PADDED_SECOND_THRESHOLD,
DEFAULT_RATE_LIMIT_WINDOW_MS in KeyManager config
8. Explicit Winston transports - Replaced fragile filename-based filtering with explicit transport instances
9. Streaming logs at debug level - Reduced I/O overhead by logging stream chunks at debug level

Edge Cases Handled
- Buffer overflow returns immediately after sending error
- Event size limit enforced per event
- Client disconnect aborts upstream request
- add-key.js exits with proper error code
- Duplicate error check removed
- Key initialization wrapped in try/catch with proper exit on failure
- Retry logic has proper backoff with NVIDIA-specific delays
Summary of Changes

Phase 1: Request Validation & Client Header Forwarding ✅
- OpenAI Chat Completions Request Validation (server.js:24-400+) - Full schema validation for:
  - Messages (roles: system/user/assistant/tool/function, content as string or array for multi-modal)
  - Tool/function calling (tools, tool_choice, function_call deprecated)
  - All optional parameters (temperature, top_p, max_tokens, stop, presence_penalty, frequency_penalty, logit_bias, user, seed, logprobs, top_logprobs,
response_format, n, stream)
- Client Header Forwarding - Forwards HTTP-Referer, Referer, X-Title from client to OpenRouter with env fallback

Phase 2: Model ID Normalization & Error Response Normalization ✅
- Model ID Normalization (server.js:22-120+) - Maps OpenRouter IDs to OpenAI-compatible format:
  - Direct mapping for 50+ models (OpenAI, Anthropic, Google, Meta, Mistral, Cohere, free models)
  - Fallback extraction from provider/model format
- Model Object Normalization - Ensures OpenAI format (id, object, created, owned_by, permission, root, parent)
- Error Response Normalization - Maps to OpenAI error format with type, message, param, code
- Streaming Error Normalization - SSE-formatted errors for streaming responses
- Applied to both /v1/chat/completions and /v1/models endpoints

Phase 3: Tool/Function Calling, Multi-Modal, Anthropic Endpoint ✅
- Tool/Function Calling Validation - Complete validation for tools, tool_choice, function_call (deprecated)
- Multi-Modal Support - Validates image_url content parts in messages
- Anthropic /v1/messages Endpoint (server.js:1035-1200+) - Format translation layer:
  - Transforms Anthropic format → OpenAI format for OpenRouter
  - Handles system message (separate in Anthropic)
  - Transforms OpenAI response → Anthropic format
  - Supports streaming (basic implementation)
  - Full retry logic with NVIDIA rate limit detection
- Response Format Support - json_object and json_schema validation already in place, passed through to OpenRouter

Session ID Management
- Already well-implemented with no cross-session leakage risk
- Dual UUID generation (middleware + handler) for observability
- Per-request AbortController for streaming isolation
Key New Features Documented:
1. Anthropic SDK Compatibility - /v1/messages with format translation
2. Auto Model ID Mapping - Fetches from OpenRouter on startup, zero maintenance
3. Full OpenAI Schema Validation - Tools, multi-modal, all parameters
4. Error Logs Always in Console - Dedicated error console transport
5. Client Header Forwarding - HTTP-Referer, X-Title passed to OpenRouter
 ✅ Model Object Normalization
 Outputs proper OpenAI format with all required fields:
 - id, object: "model", created, owned_by, permission[], root, parent

 ✅ Logger Functions
 - logError: Always appears in console (dedicated error transport) ✅
 - logInfo: Filtered by log level (expected behavior) ✅
 - No misuse of logError for informational events ✅

 ✅ Network Error Retry Logic
 - Detects: ECONNRESET, ETIMEDOUT, ECONNABORTED, ENOTFOUND, ENETUNREACH, EAI_AGAIN, EHOSTUNREACH, EPIPE, ECONNREFUSED ✅
 - Detects idle timeout in error messages ✅
 - Retries on both rate limits and network errors ✅
services/KeyManager.js
1. Increased MAX_ROTATION_DEPTH from 2 to 3 to allow more key rotation attempts
2. Added retry configuration constants for exponential backoff:
   - BASE_RETRY_DELAY_MS (default 1000ms)
   - MAX_RETRY_DELAY_MS (default 30000ms)
   - RETRY_JITTER_FACTOR (0.3)
3. Enhanced parseRateLimitReset() to handle more header formats:
   - Added support for x-rate-limit-reset, x-ratelimit-reset-after, rate-limit-reset
   - Added HTTP-date format parsing (e.g., "Wed, 21 Oct 2015 07:28:00 GMT")
   - Case-insensitive header matching
4. Improved isRateLimitError():
   - Added immediate check for HTTP status 429 (definitive rate limit indicator)
   - Added more Xiaomi MiMo specific patterns: capacity, busy, overload
5. Added calculateRetryDelay() method for exponential backoff with jitter

server.js
1. Fixed retry logic in all three endpoints (/v1/chat/completions, /v1/messages, /v1/models):
   - Now uses keyRateLimit return value from markKeyError() as primary indicator
   - Combines with isRateLimitFromResponse and isRateLimitFromError for robustness
   - Uses exponential backoff via keyManager.calculateRetryDelay(retryCount)
   - Added attempt counter in log messages (e.g., "attempt 2/3")
2. Improved streaming retry logic:
   - Allows retry on rate limits even if some data was sent (!streamDataSent || isRateLimit)
   - Resets streamDataSent = false on retry since it's a fresh connection
3. Enhanced network error detection across all endpoints with same patterns

The key fix was that markKeyError() returns true for rate limits (checking HTTP 429), but this return value was never used in the retry condition. Now it's
the primary indicator combined with other detection methods.
 Summary of Changes

 services/KeyManager.js
 1. Increased MAX_ROTATION_DEPTH from 2 to 3
 2. Added retry config: BASE_RETRY_DELAY_MS (1s), MAX_RETRY_DELAY_MS (30s), RETRY_JITTER_FACTOR (0.3)
 3. Enhanced parseRateLimitReset():
    - More header names (x-rate-limit-reset, x-ratelimit-reset-after, rate-limit-reset)
    - HTTP-date format support
    - Case-insensitive matching
 4. Improved isRateLimitError():
    - Immediate HTTP 429 check
    - More Xiaomi MiMo patterns: capacity, busy, overload
 5. Added calculateRetryDelay() for exponential backoff with jitter
 6. Error now includes minWaitMs when no keys available (NO_AVAILABLE_KEYS code)

 server.js
 1. All three endpoints now use keyRateLimit from markKeyError() as primary indicator
 2. Smart wait time: When NO_AVAILABLE_KEYS error, waits for minWaitMs (actual key reset time) instead of exponential backoff
 3. Increased retries: MAX_RETRIES 3→5, added MAX_RATE_LIMIT_RETRIES (default 10) for rate limit scenarios
 4. Better logging: Shows attempt number, wait reason (exponential backoff vs key reset time)
 5. Streaming retry: Allows retry on rate limits even if some data sent, resets streamDataSent on retry

 How it works now:

 Request → 429 → markKeyError() sets rateLimitResetAt
 Next request → rotateKey() finds all keys rate limited
 → Throws NO_AVAILABLE_KEYS with minWaitMs (earliest reset)
 → Server catches, waits minWaitMs, retries
 → Repeats until key available or max retries (10) reached
Server starts successfully. The fix addresses Cloudflare error 524 (100s timeout) by:

1. Added TOTAL_REQUEST_TIMEOUT_MS (default 90s) - tracks total elapsed time across all retries
2. Reduced AXIOS_TIMEOUT from 120s → 60s per request
3. Reduced MAX_RATE_LIMIT_RETRIES from 10 → 5
4. Added timeout checks in all 3 endpoints:
   - /v1/chat/completions
   - /v1/models
   - /v1/messages (Anthropic)

Now the proxy will return 504 Gateway Timeout after 90s total (well under Cloudflare's 100s limit) instead of stalling indefinitely.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant