source dump of claude code
at main 882 lines 30 kB view raw
1// Mock rate limits for testing [ANT-ONLY] 2// This allows testing various rate limit scenarios without hitting actual limits 3// 4// ⚠️ WARNING: This is for internal testing/demo purposes only! 5// The mock headers may not exactly match the API specification or real-world behavior. 6// Always validate against actual API responses before relying on this for production features. 7 8import type { SubscriptionType } from '../services/oauth/types.js' 9import { setMockBillingAccessOverride } from '../utils/billing.js' 10import type { OverageDisabledReason } from './claudeAiLimits.js' 11 12type MockHeaders = { 13 'anthropic-ratelimit-unified-status'?: 14 | 'allowed' 15 | 'allowed_warning' 16 | 'rejected' 17 'anthropic-ratelimit-unified-reset'?: string 18 'anthropic-ratelimit-unified-representative-claim'?: 19 | 'five_hour' 20 | 'seven_day' 21 | 'seven_day_opus' 22 | 'seven_day_sonnet' 23 'anthropic-ratelimit-unified-overage-status'?: 24 | 'allowed' 25 | 'allowed_warning' 26 | 'rejected' 27 'anthropic-ratelimit-unified-overage-reset'?: string 28 'anthropic-ratelimit-unified-overage-disabled-reason'?: OverageDisabledReason 29 'anthropic-ratelimit-unified-fallback'?: 'available' 30 'anthropic-ratelimit-unified-fallback-percentage'?: string 31 'retry-after'?: string 32 // Early warning utilization headers 33 'anthropic-ratelimit-unified-5h-utilization'?: string 34 'anthropic-ratelimit-unified-5h-reset'?: string 35 'anthropic-ratelimit-unified-5h-surpassed-threshold'?: string 36 'anthropic-ratelimit-unified-7d-utilization'?: string 37 'anthropic-ratelimit-unified-7d-reset'?: string 38 'anthropic-ratelimit-unified-7d-surpassed-threshold'?: string 39 'anthropic-ratelimit-unified-overage-utilization'?: string 40 'anthropic-ratelimit-unified-overage-surpassed-threshold'?: string 41} 42 43export type MockHeaderKey = 44 | 'status' 45 | 'reset' 46 | 'claim' 47 | 'overage-status' 48 | 'overage-reset' 49 | 'overage-disabled-reason' 50 | 'fallback' 51 | 'fallback-percentage' 52 | 'retry-after' 53 | '5h-utilization' 54 | '5h-reset' 55 | '5h-surpassed-threshold' 56 | '7d-utilization' 57 | '7d-reset' 58 | '7d-surpassed-threshold' 59 60export type MockScenario = 61 | 'normal' 62 | 'session-limit-reached' 63 | 'approaching-weekly-limit' 64 | 'weekly-limit-reached' 65 | 'overage-active' 66 | 'overage-warning' 67 | 'overage-exhausted' 68 | 'out-of-credits' 69 | 'org-zero-credit-limit' 70 | 'org-spend-cap-hit' 71 | 'member-zero-credit-limit' 72 | 'seat-tier-zero-credit-limit' 73 | 'opus-limit' 74 | 'opus-warning' 75 | 'sonnet-limit' 76 | 'sonnet-warning' 77 | 'fast-mode-limit' 78 | 'fast-mode-short-limit' 79 | 'extra-usage-required' 80 | 'clear' 81 82let mockHeaders: MockHeaders = {} 83let mockEnabled = false 84let mockHeaderless429Message: string | null = null 85let mockSubscriptionType: SubscriptionType | null = null 86let mockFastModeRateLimitDurationMs: number | null = null 87let mockFastModeRateLimitExpiresAt: number | null = null 88// Default subscription type for mock testing 89const DEFAULT_MOCK_SUBSCRIPTION: SubscriptionType = 'max' 90 91// Track individual exceeded limits with their reset times 92type ExceededLimit = { 93 type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet' 94 resetsAt: number // Unix timestamp 95} 96 97let exceededLimits: ExceededLimit[] = [] 98 99// New approach: Toggle individual headers 100export function setMockHeader( 101 key: MockHeaderKey, 102 value: string | undefined, 103): void { 104 if (process.env.USER_TYPE !== 'ant') { 105 return 106 } 107 108 mockEnabled = true 109 110 // Special case for retry-after which doesn't have the prefix 111 const fullKey = ( 112 key === 'retry-after' ? 'retry-after' : `anthropic-ratelimit-unified-${key}` 113 ) as keyof MockHeaders 114 115 if (value === undefined || value === 'clear') { 116 delete mockHeaders[fullKey] 117 if (key === 'claim') { 118 exceededLimits = [] 119 } 120 // Update retry-after if status changed 121 if (key === 'status' || key === 'overage-status') { 122 updateRetryAfter() 123 } 124 return 125 } else { 126 // Handle special cases for reset times 127 if (key === 'reset' || key === 'overage-reset') { 128 // If user provides a number, treat it as hours from now 129 const hours = Number(value) 130 if (!isNaN(hours)) { 131 value = String(Math.floor(Date.now() / 1000) + hours * 3600) 132 } 133 } 134 135 // Handle claims - add to exceeded limits 136 if (key === 'claim') { 137 const validClaims = [ 138 'five_hour', 139 'seven_day', 140 'seven_day_opus', 141 'seven_day_sonnet', 142 ] 143 if (validClaims.includes(value)) { 144 // Determine reset time based on claim type 145 let resetsAt: number 146 if (value === 'five_hour') { 147 resetsAt = Math.floor(Date.now() / 1000) + 5 * 3600 148 } else if ( 149 value === 'seven_day' || 150 value === 'seven_day_opus' || 151 value === 'seven_day_sonnet' 152 ) { 153 resetsAt = Math.floor(Date.now() / 1000) + 7 * 24 * 3600 154 } else { 155 resetsAt = Math.floor(Date.now() / 1000) + 3600 156 } 157 158 // Add to exceeded limits (remove if already exists) 159 exceededLimits = exceededLimits.filter(l => l.type !== value) 160 exceededLimits.push({ type: value as ExceededLimit['type'], resetsAt }) 161 162 // Set the representative claim (furthest reset time) 163 updateRepresentativeClaim() 164 return 165 } 166 } 167 // Widen to a string-valued record so dynamic key assignment is allowed. 168 // MockHeaders values are string-literal unions; assigning a raw user-input 169 // string requires widening, but this is mock/test code so it's acceptable. 170 const headers: Partial<Record<keyof MockHeaders, string>> = mockHeaders 171 headers[fullKey] = value 172 173 // Update retry-after if status changed 174 if (key === 'status' || key === 'overage-status') { 175 updateRetryAfter() 176 } 177 } 178 179 // If all headers are cleared, disable mocking 180 if (Object.keys(mockHeaders).length === 0) { 181 mockEnabled = false 182 } 183} 184 185// Helper to update retry-after based on current state 186function updateRetryAfter(): void { 187 const status = mockHeaders['anthropic-ratelimit-unified-status'] 188 const overageStatus = 189 mockHeaders['anthropic-ratelimit-unified-overage-status'] 190 const reset = mockHeaders['anthropic-ratelimit-unified-reset'] 191 192 if ( 193 status === 'rejected' && 194 (!overageStatus || overageStatus === 'rejected') && 195 reset 196 ) { 197 // Calculate seconds until reset 198 const resetTimestamp = Number(reset) 199 const secondsUntilReset = Math.max( 200 0, 201 resetTimestamp - Math.floor(Date.now() / 1000), 202 ) 203 mockHeaders['retry-after'] = String(secondsUntilReset) 204 } else { 205 delete mockHeaders['retry-after'] 206 } 207} 208 209// Update the representative claim based on exceeded limits 210function updateRepresentativeClaim(): void { 211 if (exceededLimits.length === 0) { 212 delete mockHeaders['anthropic-ratelimit-unified-representative-claim'] 213 delete mockHeaders['anthropic-ratelimit-unified-reset'] 214 delete mockHeaders['retry-after'] 215 return 216 } 217 218 // Find the limit with the furthest reset time 219 const furthest = exceededLimits.reduce((prev, curr) => 220 curr.resetsAt > prev.resetsAt ? curr : prev, 221 ) 222 223 // Set the representative claim (appears for both warning and rejected) 224 mockHeaders['anthropic-ratelimit-unified-representative-claim'] = 225 furthest.type 226 mockHeaders['anthropic-ratelimit-unified-reset'] = String(furthest.resetsAt) 227 228 // Add retry-after if rejected and no overage available 229 if (mockHeaders['anthropic-ratelimit-unified-status'] === 'rejected') { 230 const overageStatus = 231 mockHeaders['anthropic-ratelimit-unified-overage-status'] 232 if (!overageStatus || overageStatus === 'rejected') { 233 // Calculate seconds until reset 234 const secondsUntilReset = Math.max( 235 0, 236 furthest.resetsAt - Math.floor(Date.now() / 1000), 237 ) 238 mockHeaders['retry-after'] = String(secondsUntilReset) 239 } else { 240 // Overage is available, no retry-after 241 delete mockHeaders['retry-after'] 242 } 243 } else { 244 delete mockHeaders['retry-after'] 245 } 246} 247 248// Add function to add exceeded limit with custom reset time 249export function addExceededLimit( 250 type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet', 251 hoursFromNow: number, 252): void { 253 if (process.env.USER_TYPE !== 'ant') { 254 return 255 } 256 257 mockEnabled = true 258 const resetsAt = Math.floor(Date.now() / 1000) + hoursFromNow * 3600 259 260 // Remove existing limit of same type 261 exceededLimits = exceededLimits.filter(l => l.type !== type) 262 exceededLimits.push({ type, resetsAt }) 263 264 // Update status to rejected if we have exceeded limits 265 if (exceededLimits.length > 0) { 266 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 267 } 268 269 updateRepresentativeClaim() 270} 271 272// Set mock early warning utilization for time-relative thresholds 273// claimAbbrev: '5h' or '7d' 274// utilization: 0-1 (e.g., 0.92 for 92% used) 275// hoursFromNow: hours until reset (default: 4 for 5h, 120 for 7d) 276export function setMockEarlyWarning( 277 claimAbbrev: '5h' | '7d' | 'overage', 278 utilization: number, 279 hoursFromNow?: number, 280): void { 281 if (process.env.USER_TYPE !== 'ant') { 282 return 283 } 284 285 mockEnabled = true 286 287 // Clear ALL early warning headers first (5h is checked before 7d, so we need 288 // to clear 5h headers when testing 7d to avoid 5h taking priority) 289 clearMockEarlyWarning() 290 291 // Default hours based on claim type (early in window to trigger warning) 292 const defaultHours = claimAbbrev === '5h' ? 4 : 5 * 24 293 const hours = hoursFromNow ?? defaultHours 294 const resetsAt = Math.floor(Date.now() / 1000) + hours * 3600 295 296 mockHeaders[`anthropic-ratelimit-unified-${claimAbbrev}-utilization`] = 297 String(utilization) 298 mockHeaders[`anthropic-ratelimit-unified-${claimAbbrev}-reset`] = 299 String(resetsAt) 300 // Set the surpassed-threshold header to trigger early warning 301 mockHeaders[ 302 `anthropic-ratelimit-unified-${claimAbbrev}-surpassed-threshold` 303 ] = String(utilization) 304 305 // Set status to allowed so early warning logic can upgrade it 306 if (!mockHeaders['anthropic-ratelimit-unified-status']) { 307 mockHeaders['anthropic-ratelimit-unified-status'] = 'allowed' 308 } 309} 310 311// Clear mock early warning headers 312export function clearMockEarlyWarning(): void { 313 delete mockHeaders['anthropic-ratelimit-unified-5h-utilization'] 314 delete mockHeaders['anthropic-ratelimit-unified-5h-reset'] 315 delete mockHeaders['anthropic-ratelimit-unified-5h-surpassed-threshold'] 316 delete mockHeaders['anthropic-ratelimit-unified-7d-utilization'] 317 delete mockHeaders['anthropic-ratelimit-unified-7d-reset'] 318 delete mockHeaders['anthropic-ratelimit-unified-7d-surpassed-threshold'] 319} 320 321export function setMockRateLimitScenario(scenario: MockScenario): void { 322 if (process.env.USER_TYPE !== 'ant') { 323 return 324 } 325 326 if (scenario === 'clear') { 327 mockHeaders = {} 328 mockHeaderless429Message = null 329 mockEnabled = false 330 return 331 } 332 333 mockEnabled = true 334 335 // Set reset times for demos 336 const fiveHoursFromNow = Math.floor(Date.now() / 1000) + 5 * 3600 337 const sevenDaysFromNow = Math.floor(Date.now() / 1000) + 7 * 24 * 3600 338 339 // Clear existing headers 340 mockHeaders = {} 341 mockHeaderless429Message = null 342 343 // Only clear exceeded limits for scenarios that explicitly set them 344 // Overage scenarios should preserve existing exceeded limits 345 const preserveExceededLimits = [ 346 'overage-active', 347 'overage-warning', 348 'overage-exhausted', 349 ].includes(scenario) 350 if (!preserveExceededLimits) { 351 exceededLimits = [] 352 } 353 354 switch (scenario) { 355 case 'normal': 356 mockHeaders = { 357 'anthropic-ratelimit-unified-status': 'allowed', 358 'anthropic-ratelimit-unified-reset': String(fiveHoursFromNow), 359 } 360 break 361 362 case 'session-limit-reached': 363 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 364 updateRepresentativeClaim() 365 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 366 break 367 368 case 'approaching-weekly-limit': 369 mockHeaders = { 370 'anthropic-ratelimit-unified-status': 'allowed_warning', 371 'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow), 372 'anthropic-ratelimit-unified-representative-claim': 'seven_day', 373 } 374 break 375 376 case 'weekly-limit-reached': 377 exceededLimits = [{ type: 'seven_day', resetsAt: sevenDaysFromNow }] 378 updateRepresentativeClaim() 379 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 380 break 381 382 case 'overage-active': { 383 // If no limits have been exceeded yet, default to 5-hour 384 if (exceededLimits.length === 0) { 385 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 386 } 387 updateRepresentativeClaim() 388 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 389 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'allowed' 390 // Set overage reset time (monthly) 391 const endOfMonthActive = new Date() 392 endOfMonthActive.setMonth(endOfMonthActive.getMonth() + 1, 1) 393 endOfMonthActive.setHours(0, 0, 0, 0) 394 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 395 Math.floor(endOfMonthActive.getTime() / 1000), 396 ) 397 break 398 } 399 400 case 'overage-warning': { 401 // If no limits have been exceeded yet, default to 5-hour 402 if (exceededLimits.length === 0) { 403 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 404 } 405 updateRepresentativeClaim() 406 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 407 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 408 'allowed_warning' 409 // Overage typically resets monthly, but for demo let's say end of month 410 const endOfMonth = new Date() 411 endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1) 412 endOfMonth.setHours(0, 0, 0, 0) 413 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 414 Math.floor(endOfMonth.getTime() / 1000), 415 ) 416 break 417 } 418 419 case 'overage-exhausted': { 420 // If no limits have been exceeded yet, default to 5-hour 421 if (exceededLimits.length === 0) { 422 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 423 } 424 updateRepresentativeClaim() 425 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 426 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' 427 // Both subscription and overage are exhausted 428 // Subscription resets based on the exceeded limit, overage resets monthly 429 const endOfMonthExhausted = new Date() 430 endOfMonthExhausted.setMonth(endOfMonthExhausted.getMonth() + 1, 1) 431 endOfMonthExhausted.setHours(0, 0, 0, 0) 432 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 433 Math.floor(endOfMonthExhausted.getTime() / 1000), 434 ) 435 break 436 } 437 438 case 'out-of-credits': { 439 // Out of credits - subscription limit hit, overage rejected due to insufficient credits 440 // (wallet is empty) 441 if (exceededLimits.length === 0) { 442 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 443 } 444 updateRepresentativeClaim() 445 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 446 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' 447 mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = 448 'out_of_credits' 449 const endOfMonth = new Date() 450 endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1) 451 endOfMonth.setHours(0, 0, 0, 0) 452 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 453 Math.floor(endOfMonth.getTime() / 1000), 454 ) 455 break 456 } 457 458 case 'org-zero-credit-limit': { 459 // Org service has zero credit limit - admin set org-level spend cap to $0 460 // Non-admin Team/Enterprise users should not see "Request extra usage" option 461 if (exceededLimits.length === 0) { 462 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 463 } 464 updateRepresentativeClaim() 465 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 466 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' 467 mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = 468 'org_service_zero_credit_limit' 469 const endOfMonthZero = new Date() 470 endOfMonthZero.setMonth(endOfMonthZero.getMonth() + 1, 1) 471 endOfMonthZero.setHours(0, 0, 0, 0) 472 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 473 Math.floor(endOfMonthZero.getTime() / 1000), 474 ) 475 break 476 } 477 478 case 'org-spend-cap-hit': { 479 // Org spend cap hit for the month - org overages temporarily disabled 480 // Non-admin Team/Enterprise users should not see "Request extra usage" option 481 if (exceededLimits.length === 0) { 482 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 483 } 484 updateRepresentativeClaim() 485 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 486 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' 487 mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = 488 'org_level_disabled_until' 489 const endOfMonthHit = new Date() 490 endOfMonthHit.setMonth(endOfMonthHit.getMonth() + 1, 1) 491 endOfMonthHit.setHours(0, 0, 0, 0) 492 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 493 Math.floor(endOfMonthHit.getTime() / 1000), 494 ) 495 break 496 } 497 498 case 'member-zero-credit-limit': { 499 // Member has zero credit limit - admin set this user's individual limit to $0 500 // Non-admin Team/Enterprise users SHOULD see "Request extra usage" (admin can allocate more) 501 if (exceededLimits.length === 0) { 502 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 503 } 504 updateRepresentativeClaim() 505 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 506 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' 507 mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = 508 'member_zero_credit_limit' 509 const endOfMonthMember = new Date() 510 endOfMonthMember.setMonth(endOfMonthMember.getMonth() + 1, 1) 511 endOfMonthMember.setHours(0, 0, 0, 0) 512 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 513 Math.floor(endOfMonthMember.getTime() / 1000), 514 ) 515 break 516 } 517 518 case 'seat-tier-zero-credit-limit': { 519 // Seat tier has zero credit limit - admin set this seat tier's limit to $0 520 // Non-admin Team/Enterprise users SHOULD see "Request extra usage" (admin can allocate more) 521 if (exceededLimits.length === 0) { 522 exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }] 523 } 524 updateRepresentativeClaim() 525 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 526 mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected' 527 mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] = 528 'seat_tier_zero_credit_limit' 529 const endOfMonthSeatTier = new Date() 530 endOfMonthSeatTier.setMonth(endOfMonthSeatTier.getMonth() + 1, 1) 531 endOfMonthSeatTier.setHours(0, 0, 0, 0) 532 mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String( 533 Math.floor(endOfMonthSeatTier.getTime() / 1000), 534 ) 535 break 536 } 537 538 case 'opus-limit': { 539 exceededLimits = [{ type: 'seven_day_opus', resetsAt: sevenDaysFromNow }] 540 updateRepresentativeClaim() 541 // Always send 429 rejected status - the error handler will decide whether 542 // to show an error or return NO_RESPONSE_REQUESTED based on fallback eligibility 543 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 544 break 545 } 546 547 case 'opus-warning': { 548 mockHeaders = { 549 'anthropic-ratelimit-unified-status': 'allowed_warning', 550 'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow), 551 'anthropic-ratelimit-unified-representative-claim': 'seven_day_opus', 552 } 553 break 554 } 555 556 case 'sonnet-limit': { 557 exceededLimits = [ 558 { type: 'seven_day_sonnet', resetsAt: sevenDaysFromNow }, 559 ] 560 updateRepresentativeClaim() 561 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 562 break 563 } 564 565 case 'sonnet-warning': { 566 mockHeaders = { 567 'anthropic-ratelimit-unified-status': 'allowed_warning', 568 'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow), 569 'anthropic-ratelimit-unified-representative-claim': 'seven_day_sonnet', 570 } 571 break 572 } 573 574 case 'fast-mode-limit': { 575 updateRepresentativeClaim() 576 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 577 // Duration in ms (> 20s threshold to trigger cooldown) 578 mockFastModeRateLimitDurationMs = 10 * 60 * 1000 579 break 580 } 581 582 case 'fast-mode-short-limit': { 583 updateRepresentativeClaim() 584 mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected' 585 // Duration in ms (< 20s threshold, won't trigger cooldown) 586 mockFastModeRateLimitDurationMs = 10 * 1000 587 break 588 } 589 590 case 'extra-usage-required': { 591 // Headerless 429 — exercises the entitlement-rejection path in errors.ts 592 mockHeaderless429Message = 593 'Extra usage is required for long context requests.' 594 break 595 } 596 597 default: 598 break 599 } 600} 601 602export function getMockHeaderless429Message(): string | null { 603 if (process.env.USER_TYPE !== 'ant') { 604 return null 605 } 606 // Env var path for -p / SDK testing where slash commands aren't available 607 if (process.env.CLAUDE_MOCK_HEADERLESS_429) { 608 return process.env.CLAUDE_MOCK_HEADERLESS_429 609 } 610 if (!mockEnabled) { 611 return null 612 } 613 return mockHeaderless429Message 614} 615 616export function getMockHeaders(): MockHeaders | null { 617 if ( 618 !mockEnabled || 619 process.env.USER_TYPE !== 'ant' || 620 Object.keys(mockHeaders).length === 0 621 ) { 622 return null 623 } 624 return mockHeaders 625} 626 627export function getMockStatus(): string { 628 if ( 629 !mockEnabled || 630 (Object.keys(mockHeaders).length === 0 && !mockSubscriptionType) 631 ) { 632 return 'No mock headers active (using real limits)' 633 } 634 635 const lines: string[] = [] 636 lines.push('Active mock headers:') 637 638 // Show subscription type - either explicitly set or default 639 const effectiveSubscription = 640 mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION 641 if (mockSubscriptionType) { 642 lines.push(` Subscription Type: ${mockSubscriptionType} (explicitly set)`) 643 } else { 644 lines.push(` Subscription Type: ${effectiveSubscription} (default)`) 645 } 646 647 Object.entries(mockHeaders).forEach(([key, value]) => { 648 if (value !== undefined) { 649 // Format the header name nicely 650 const formattedKey = key 651 .replace('anthropic-ratelimit-unified-', '') 652 .replace(/-/g, ' ') 653 .replace(/\b\w/g, c => c.toUpperCase()) 654 655 // Format timestamps as human-readable 656 if (key.includes('reset') && value) { 657 const timestamp = Number(value) 658 const date = new Date(timestamp * 1000) 659 lines.push(` ${formattedKey}: ${value} (${date.toLocaleString()})`) 660 } else { 661 lines.push(` ${formattedKey}: ${value}`) 662 } 663 } 664 }) 665 666 // Show exceeded limits if any 667 if (exceededLimits.length > 0) { 668 lines.push('\nExceeded limits (contributing to representative claim):') 669 exceededLimits.forEach(limit => { 670 const date = new Date(limit.resetsAt * 1000) 671 lines.push(` ${limit.type}: resets at ${date.toLocaleString()}`) 672 }) 673 } 674 675 return lines.join('\n') 676} 677 678export function clearMockHeaders(): void { 679 mockHeaders = {} 680 exceededLimits = [] 681 mockSubscriptionType = null 682 mockFastModeRateLimitDurationMs = null 683 mockFastModeRateLimitExpiresAt = null 684 mockHeaderless429Message = null 685 setMockBillingAccessOverride(null) 686 mockEnabled = false 687} 688 689export function applyMockHeaders( 690 headers: globalThis.Headers, 691): globalThis.Headers { 692 const mock = getMockHeaders() 693 if (!mock) { 694 return headers 695 } 696 697 // Create a new Headers object with original headers 698 // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins 699 const newHeaders = new globalThis.Headers(headers) 700 701 // Apply mock headers (overwriting originals) 702 Object.entries(mock).forEach(([key, value]) => { 703 if (value !== undefined) { 704 newHeaders.set(key, value) 705 } 706 }) 707 708 return newHeaders 709} 710 711// Check if we should process rate limits even without subscription 712// This is for Ant employees testing with mocks 713export function shouldProcessMockLimits(): boolean { 714 if (process.env.USER_TYPE !== 'ant') { 715 return false 716 } 717 return mockEnabled || Boolean(process.env.CLAUDE_MOCK_HEADERLESS_429) 718} 719 720export function getCurrentMockScenario(): MockScenario | null { 721 if (!mockEnabled) { 722 return null 723 } 724 725 // Reverse lookup the scenario from current headers 726 if (!mockHeaders) return null 727 728 const status = mockHeaders['anthropic-ratelimit-unified-status'] 729 const overage = mockHeaders['anthropic-ratelimit-unified-overage-status'] 730 const claim = mockHeaders['anthropic-ratelimit-unified-representative-claim'] 731 732 if (claim === 'seven_day_opus') { 733 return status === 'rejected' ? 'opus-limit' : 'opus-warning' 734 } 735 736 if (claim === 'seven_day_sonnet') { 737 return status === 'rejected' ? 'sonnet-limit' : 'sonnet-warning' 738 } 739 740 if (overage === 'rejected') return 'overage-exhausted' 741 if (overage === 'allowed_warning') return 'overage-warning' 742 if (overage === 'allowed') return 'overage-active' 743 744 if (status === 'rejected') { 745 if (claim === 'five_hour') return 'session-limit-reached' 746 if (claim === 'seven_day') return 'weekly-limit-reached' 747 } 748 749 if (status === 'allowed_warning') { 750 if (claim === 'seven_day') return 'approaching-weekly-limit' 751 } 752 753 if (status === 'allowed') return 'normal' 754 755 return null 756} 757 758export function getScenarioDescription(scenario: MockScenario): string { 759 switch (scenario) { 760 case 'normal': 761 return 'Normal usage, no limits' 762 case 'session-limit-reached': 763 return 'Session rate limit exceeded' 764 case 'approaching-weekly-limit': 765 return 'Approaching weekly aggregate limit' 766 case 'weekly-limit-reached': 767 return 'Weekly aggregate limit exceeded' 768 case 'overage-active': 769 return 'Using extra usage (overage active)' 770 case 'overage-warning': 771 return 'Approaching extra usage limit' 772 case 'overage-exhausted': 773 return 'Both subscription and extra usage limits exhausted' 774 case 'out-of-credits': 775 return 'Out of extra usage credits (wallet empty)' 776 case 'org-zero-credit-limit': 777 return 'Org spend cap is zero (no extra usage budget)' 778 case 'org-spend-cap-hit': 779 return 'Org spend cap hit for the month' 780 case 'member-zero-credit-limit': 781 return 'Member limit is zero (admin can allocate more)' 782 case 'seat-tier-zero-credit-limit': 783 return 'Seat tier limit is zero (admin can allocate more)' 784 case 'opus-limit': 785 return 'Opus limit reached' 786 case 'opus-warning': 787 return 'Approaching Opus limit' 788 case 'sonnet-limit': 789 return 'Sonnet limit reached' 790 case 'sonnet-warning': 791 return 'Approaching Sonnet limit' 792 case 'fast-mode-limit': 793 return 'Fast mode rate limit' 794 case 'fast-mode-short-limit': 795 return 'Fast mode rate limit (short)' 796 case 'extra-usage-required': 797 return 'Headerless 429: Extra usage required for 1M context' 798 case 'clear': 799 return 'Clear mock headers (use real limits)' 800 default: 801 return 'Unknown scenario' 802 } 803} 804 805// Mock subscription type management 806export function setMockSubscriptionType( 807 subscriptionType: SubscriptionType | null, 808): void { 809 if (process.env.USER_TYPE !== 'ant') { 810 return 811 } 812 mockEnabled = true 813 mockSubscriptionType = subscriptionType 814} 815 816export function getMockSubscriptionType(): SubscriptionType | null { 817 if (!mockEnabled || process.env.USER_TYPE !== 'ant') { 818 return null 819 } 820 // Return the explicitly set subscription type, or default to 'max' 821 return mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION 822} 823 824// Export a function that checks if we should use mock subscription 825export function shouldUseMockSubscription(): boolean { 826 return ( 827 mockEnabled && 828 mockSubscriptionType !== null && 829 process.env.USER_TYPE === 'ant' 830 ) 831} 832 833// Mock billing access (admin vs non-admin) 834export function setMockBillingAccess(hasAccess: boolean | null): void { 835 if (process.env.USER_TYPE !== 'ant') { 836 return 837 } 838 mockEnabled = true 839 setMockBillingAccessOverride(hasAccess) 840} 841 842// Mock fast mode rate limit handling 843export function isMockFastModeRateLimitScenario(): boolean { 844 return mockFastModeRateLimitDurationMs !== null 845} 846 847export function checkMockFastModeRateLimit( 848 isFastModeActive?: boolean, 849): MockHeaders | null { 850 if (mockFastModeRateLimitDurationMs === null) { 851 return null 852 } 853 854 // Only throw when fast mode is active 855 if (!isFastModeActive) { 856 return null 857 } 858 859 // Check if the rate limit has expired 860 if ( 861 mockFastModeRateLimitExpiresAt !== null && 862 Date.now() >= mockFastModeRateLimitExpiresAt 863 ) { 864 clearMockHeaders() 865 return null 866 } 867 868 // Set expiry on first error (not when scenario is configured) 869 if (mockFastModeRateLimitExpiresAt === null) { 870 mockFastModeRateLimitExpiresAt = 871 Date.now() + mockFastModeRateLimitDurationMs 872 } 873 874 // Compute dynamic retry-after based on remaining time 875 const remainingMs = mockFastModeRateLimitExpiresAt - Date.now() 876 const headersToSend = { ...mockHeaders } 877 headersToSend['retry-after'] = String( 878 Math.max(1, Math.ceil(remainingMs / 1000)), 879 ) 880 881 return headersToSend 882}