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>
125 lines
4.3 KiB
JavaScript
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 }; |