/ services / rateLimitMocking.ts
rateLimitMocking.ts
  1  /**
  2   * Facade for rate limit header processing
  3   * This isolates mock logic from production code
  4   */
  5  
  6  import { APIError } from '@anthropic-ai/sdk'
  7  import {
  8    applyMockHeaders,
  9    checkMockFastModeRateLimit,
 10    getMockHeaderless429Message,
 11    getMockHeaders,
 12    isMockFastModeRateLimitScenario,
 13    shouldProcessMockLimits,
 14  } from './mockRateLimits.js'
 15  
 16  /**
 17   * Process headers, applying mocks if /mock-limits command is active
 18   */
 19  export function processRateLimitHeaders(
 20    headers: globalThis.Headers,
 21  ): globalThis.Headers {
 22    // Only apply mocks for Ant employees using /mock-limits command
 23    if (shouldProcessMockLimits()) {
 24      return applyMockHeaders(headers)
 25    }
 26    return headers
 27  }
 28  
 29  /**
 30   * Check if we should process rate limits (either real subscriber or /mock-limits command)
 31   */
 32  export function shouldProcessRateLimits(isSubscriber: boolean): boolean {
 33    return isSubscriber || shouldProcessMockLimits()
 34  }
 35  
 36  /**
 37   * Check if mock rate limits should throw a 429 error
 38   * Returns the error to throw, or null if no error should be thrown
 39   * @param currentModel The model being used for the current request
 40   * @param isFastModeActive Whether fast mode is currently active (for fast-mode-only mocks)
 41   */
 42  export function checkMockRateLimitError(
 43    currentModel: string,
 44    isFastModeActive?: boolean,
 45  ): APIError | null {
 46    if (!shouldProcessMockLimits()) {
 47      return null
 48    }
 49  
 50    const headerlessMessage = getMockHeaderless429Message()
 51    if (headerlessMessage) {
 52      return new APIError(
 53        429,
 54        { error: { type: 'rate_limit_error', message: headerlessMessage } },
 55        headerlessMessage,
 56        // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
 57        new globalThis.Headers(),
 58      )
 59    }
 60  
 61    const mockHeaders = getMockHeaders()
 62    if (!mockHeaders) {
 63      return null
 64    }
 65  
 66    // Check if we should throw a 429 error
 67    // Only throw if:
 68    // 1. Status is rejected AND
 69    // 2. Either no overage headers OR overage is also rejected
 70    // 3. For Opus-specific limits, only throw if actually using an Opus model
 71    const status = mockHeaders['anthropic-ratelimit-unified-status']
 72    const overageStatus =
 73      mockHeaders['anthropic-ratelimit-unified-overage-status']
 74    const rateLimitType =
 75      mockHeaders['anthropic-ratelimit-unified-representative-claim']
 76  
 77    // Check if this is an Opus-specific rate limit
 78    const isOpusLimit = rateLimitType === 'seven_day_opus'
 79  
 80    // Check if current model is an Opus model (handles all variants including aliases)
 81    const isUsingOpus = currentModel.includes('opus')
 82  
 83    // For Opus limits, only throw 429 if actually using Opus
 84    // This simulates the real API behavior where fallback to Sonnet succeeds
 85    if (isOpusLimit && !isUsingOpus) {
 86      return null
 87    }
 88  
 89    // Check for mock fast mode rate limits (handles expiry, countdown, etc.)
 90    if (isMockFastModeRateLimitScenario()) {
 91      const fastModeHeaders = checkMockFastModeRateLimit(isFastModeActive)
 92      if (fastModeHeaders === null) {
 93        return null
 94      }
 95      // Create a mock 429 error with the fast mode headers
 96      const error = new APIError(
 97        429,
 98        { error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } },
 99        'Rate limit exceeded',
100        // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
101        new globalThis.Headers(
102          Object.entries(fastModeHeaders).filter(([_, v]) => v !== undefined) as [
103            string,
104            string,
105          ][],
106        ),
107      )
108      return error
109    }
110  
111    const shouldThrow429 =
112      status === 'rejected' && (!overageStatus || overageStatus === 'rejected')
113  
114    if (shouldThrow429) {
115      // Create a mock 429 error with the appropriate headers
116      const error = new APIError(
117        429,
118        { error: { type: 'rate_limit_error', message: 'Rate limit exceeded' } },
119        'Rate limit exceeded',
120        // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
121        new globalThis.Headers(
122          Object.entries(mockHeaders).filter(([_, v]) => v !== undefined) as [
123            string,
124            string,
125          ][],
126        ),
127      )
128      return error
129    }
130  
131    return null
132  }
133  
134  /**
135   * Check if this is a mock 429 error that shouldn't be retried
136   */
137  export function isMockRateLimitError(error: APIError): boolean {
138    return shouldProcessMockLimits() && error.status === 429
139  }
140  
141  /**
142   * Check if /mock-limits command is currently active (for UI purposes)
143   */
144  export { shouldProcessMockLimits }