Files
antigravity-claude-proxy/src/config.js
Badri Narayanan S 5a85f0cfcc feat: comprehensive rate limit handling overhaul (inspired by opencode-antigravity-auth)
This commit addresses "Max retries exceeded" errors during stress testing where
all accounts would become exhausted simultaneously due to short per-second rate
limits triggering cascading failures.

## Rate Limit Parser (`rate-limit-parser.js`)
- Remove 2s buffer enforcement that caused cascading failures when API returned
  short reset times (200-600ms). Now adds 200ms buffer for sub-500ms resets
- Add `parseRateLimitReason()` for smart backoff based on error type:
  QUOTA_EXHAUSTED, RATE_LIMIT_EXCEEDED, MODEL_CAPACITY_EXHAUSTED, SERVER_ERROR

## Message/Streaming Handlers
- Add per-account+model rate limit state tracking with exponential backoff
- For short rate limits (< 1 second), wait and retry on same account instead
  of switching - prevents thundering herd when all accounts hit per-second limits
- Add throttle wait support for fallback modes (emergency/lastResort)
- Add `calculateSmartBackoff()` with progressive tiers by error type

## HybridStrategy (`hybrid-strategy.js`)
- Refactor `#getCandidates()` to return 4 fallback levels:
  - `normal`: All filters pass (health, tokens, quota)
  - `quota`: Bypass critical quota check
  - `emergency`: Bypass health check when ALL accounts unhealthy
  - `lastResort`: Bypass BOTH health AND token bucket checks
- Add throttle wait times: 500ms for lastResort, 250ms for emergency
- Fix LRU calculation to use seconds (matches opencode-antigravity-auth)

## Health Tracker
- Increase `recoveryPerHour` from 2 to 10 for faster recovery (1 hour vs 5 hours)

## Account Manager
- Add consecutive failure tracking: `getConsecutiveFailures()`,
  `incrementConsecutiveFailures()`, `resetConsecutiveFailures()`
- Add cooldown mechanism separate from rate limits with `CooldownReason`
- Reset consecutive failures on successful request

## Base Strategy
- Add `isAccountCoolingDown()` check in `isAccountUsable()`

## Constants
- Replace fixed `CAPACITY_RETRY_DELAY_MS` with progressive `CAPACITY_BACKOFF_TIERS_MS`
- Add `BACKOFF_BY_ERROR_TYPE` for smart backoff
- Add `QUOTA_EXHAUSTED_BACKOFF_TIERS_MS` for progressive quota backoff
- Add `MIN_BACKOFF_MS` floor to prevent "Available in 0s" loops
- Increase `MAX_CAPACITY_RETRIES` from 3 to 5
- Reduce `RATE_LIMIT_DEDUP_WINDOW_MS` from 5s to 2s

## Frontend
- Remove `capacityRetryDelayMs` config (replaced by progressive tiers)
- Update default `maxCapacityRetries` display from 3 to 5

## Testing
- Add `tests/stress-test.cjs` for concurrent request stress testing

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-24 22:43:53 +05:30

125 lines
4.3 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import os from 'os';
import { logger } from './utils/logger.js';
// Default config
const DEFAULT_CONFIG = {
apiKey: '',
webuiPassword: '',
debug: false,
logLevel: 'info',
maxRetries: 5,
retryBaseMs: 1000,
retryMaxMs: 30000,
persistTokenCache: false,
defaultCooldownMs: 10000, // 10 seconds
maxWaitBeforeErrorMs: 120000, // 2 minutes
maxAccounts: 10, // Maximum number of accounts allowed
// Rate limit handling (matches opencode-antigravity-auth)
rateLimitDedupWindowMs: 2000, // 2 seconds - prevents concurrent retry storms
maxConsecutiveFailures: 3, // Before applying extended cooldown
extendedCooldownMs: 60000, // 1 minute extended cooldown
maxCapacityRetries: 5, // Max retries for capacity exhaustion
modelMapping: {},
// Account selection strategy configuration
accountSelection: {
strategy: 'hybrid', // 'sticky' | 'round-robin' | 'hybrid'
// Hybrid strategy tuning (optional - sensible defaults)
healthScore: {
initial: 70, // Starting score for new accounts
successReward: 1, // Points on successful request
rateLimitPenalty: -10, // Points on rate limit
failurePenalty: -20, // Points on other failures
recoveryPerHour: 2, // Passive recovery rate
minUsable: 50, // Minimum score to be selected
maxScore: 100 // Maximum score cap
},
tokenBucket: {
maxTokens: 50, // Maximum token capacity
tokensPerMinute: 6, // Regeneration rate
initialTokens: 50 // Starting tokens
},
quota: {
lowThreshold: 0.10, // 10% - reduce score
criticalThreshold: 0.05, // 5% - exclude from candidates
staleMs: 300000 // 5 min - max age of quota data to trust
}
}
};
// Config locations
const HOME_DIR = os.homedir();
const CONFIG_DIR = path.join(HOME_DIR, '.config', 'antigravity-proxy');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
// Ensure config dir exists
if (!fs.existsSync(CONFIG_DIR)) {
try {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
} catch (err) {
// Ignore
}
}
// Load config
let config = { ...DEFAULT_CONFIG };
function loadConfig() {
try {
// Env vars take precedence for initial defaults, but file overrides them if present?
// Usually Env > File > Default.
if (fs.existsSync(CONFIG_FILE)) {
const fileContent = fs.readFileSync(CONFIG_FILE, 'utf8');
const userConfig = JSON.parse(fileContent);
config = { ...DEFAULT_CONFIG, ...userConfig };
} else {
// Try looking in current dir for config.json as fallback
const localConfigPath = path.resolve('config.json');
if (fs.existsSync(localConfigPath)) {
const fileContent = fs.readFileSync(localConfigPath, 'utf8');
const userConfig = JSON.parse(fileContent);
config = { ...DEFAULT_CONFIG, ...userConfig };
}
}
// Environment overrides
if (process.env.API_KEY) config.apiKey = process.env.API_KEY;
if (process.env.WEBUI_PASSWORD) config.webuiPassword = process.env.WEBUI_PASSWORD;
if (process.env.DEBUG === 'true') config.debug = true;
} catch (error) {
logger.error('[Config] Error loading config:', error);
}
}
// Initial load
loadConfig();
export function getPublicConfig() {
// Create a deep copy and redact sensitive fields
const publicConfig = JSON.parse(JSON.stringify(config));
// Redact sensitive values
if (publicConfig.webuiPassword) publicConfig.webuiPassword = '********';
if (publicConfig.apiKey) publicConfig.apiKey = '********';
return publicConfig;
}
export function saveConfig(updates) {
try {
// Apply updates
config = { ...config, ...updates };
// Save to disk
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
return true;
} catch (error) {
logger.error('[Config] Failed to save config:', error);
return false;
}
}
export { config };