/** * Facade for rate limit header processing * This isolates mock logic from production code */ import { APIError } from '@anthropic-ai/sdk' import { applyMockHeaders, checkMockFastModeRateLimit, getMockHeaderless429Message, getMockHeaders, isMockFastModeRateLimitScenario, shouldProcessMockLimits, } from './mockRateLimits.js' /** * Process headers, applying mocks if /mock-limits command is active */ export function processRateLimitHeaders( headers: globalThis.Headers, ): globalThis.Headers { // Only apply mocks for Ant employees using /mock-limits command if (shouldProcessMockLimits()) { return applyMockHeaders(headers) } return headers } /** * Check if we should process rate limits (either real subscriber or /mock-limits command) */ export function shouldProcessRateLimits(isSubscriber: boolean): boolean { return isSubscriber || shouldProcessMockLimits() } /** * Check if mock rate limits should throw a 429 error * Returns the error to throw, or null if no error should be thrown * @param currentModel The model being used for the current request * @param isFastModeActive Whether fast mode is currently active (for fast-mode-only mocks) */ export function checkMockRateLimitError( currentModel: string, isFastModeActive?: boolean, ): APIError | null { if (!shouldProcessMockLimits()) { return null } const headerlessMessage = getMockHeaderless429Message() if (headerlessMessage) { return new APIError( 429, { error: { type: 'rate_limit_error', message: headerlessMessage } }, headerlessMessage, // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins new globalThis.Headers(), ) } const mockHeaders = getMockHeaders() if (!mockHeaders) { return null } // Check if we should throw a 429 error // Only throw if: // 1. Status is rejected AND // 2. Either no overage headers OR overage is also rejected // 3. For Opus-specific limits, only throw if actually using an Opus model const status = mockHeaders['anthropic-ratelimit-unified-status'] const overageStatus = mockHeaders['anthropic-ratelimit-unified-overage-status'] const rateLimitType = mockHeaders['anthropic-ratelimit-unified-representative-claim'] // Check if this is an Opus-specific rate limit const isOpusLimit = rateLimitType === 'seven_day_opus' // Check if current model is an Opus model (handles all variants including aliases) const isUsingOpus = currentModel.includes('opus') // For Opus limits, only throw 429 if actually using Opus // This simulates the real API behavior where fallback to Sonnet succeeds if (isOpusLimit && !isUsingOpus) { return null } // Check for mock fast mode rate limits (handles expiry, countdown, etc.) if (isMockFastModeRateLimitScenario()) { const fastModeHeaders = checkMockFastModeRateLimit(isFastModeActive) if (fastModeHeaders === null) { return null } // Create a mock 429 error with the fast mode headers const error = new APIError( 429, { error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } }, 'Rate limit exceeded', // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins new globalThis.Headers( Object.entries(fastModeHeaders).filter(([_, v]) => v !== undefined) as [ string, string, ][], ), ) return error } const shouldThrow429 = status === 'rejected' && (!overageStatus || overageStatus === 'rejected') if (shouldThrow429) { // Create a mock 429 error with the appropriate headers const error = new APIError( 429, { error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } }, 'Rate limit exceeded', // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins new globalThis.Headers( Object.entries(mockHeaders).filter(([_, v]) => v !== undefined) as [ string, string, ][], ), ) return error } return null } /** * Check if this is a mock 429 error that shouldn't be retried */ export function isMockRateLimitError(error: APIError): boolean { return shouldProcessMockLimits() && error.status === 429 } /** * Check if /mock-limits command is currently active (for UI purposes) */ export { shouldProcessMockLimits }