init: add source code from src.zip
This commit is contained in:
882
src/services/mockRateLimits.ts
Normal file
882
src/services/mockRateLimits.ts
Normal file
@@ -0,0 +1,882 @@
|
||||
// Mock rate limits for testing [ANT-ONLY]
|
||||
// This allows testing various rate limit scenarios without hitting actual limits
|
||||
//
|
||||
// ⚠️ WARNING: This is for internal testing/demo purposes only!
|
||||
// The mock headers may not exactly match the API specification or real-world behavior.
|
||||
// Always validate against actual API responses before relying on this for production features.
|
||||
|
||||
import type { SubscriptionType } from '../services/oauth/types.js'
|
||||
import { setMockBillingAccessOverride } from '../utils/billing.js'
|
||||
import type { OverageDisabledReason } from './claudeAiLimits.js'
|
||||
|
||||
type MockHeaders = {
|
||||
'anthropic-ratelimit-unified-status'?:
|
||||
| 'allowed'
|
||||
| 'allowed_warning'
|
||||
| 'rejected'
|
||||
'anthropic-ratelimit-unified-reset'?: string
|
||||
'anthropic-ratelimit-unified-representative-claim'?:
|
||||
| 'five_hour'
|
||||
| 'seven_day'
|
||||
| 'seven_day_opus'
|
||||
| 'seven_day_sonnet'
|
||||
'anthropic-ratelimit-unified-overage-status'?:
|
||||
| 'allowed'
|
||||
| 'allowed_warning'
|
||||
| 'rejected'
|
||||
'anthropic-ratelimit-unified-overage-reset'?: string
|
||||
'anthropic-ratelimit-unified-overage-disabled-reason'?: OverageDisabledReason
|
||||
'anthropic-ratelimit-unified-fallback'?: 'available'
|
||||
'anthropic-ratelimit-unified-fallback-percentage'?: string
|
||||
'retry-after'?: string
|
||||
// Early warning utilization headers
|
||||
'anthropic-ratelimit-unified-5h-utilization'?: string
|
||||
'anthropic-ratelimit-unified-5h-reset'?: string
|
||||
'anthropic-ratelimit-unified-5h-surpassed-threshold'?: string
|
||||
'anthropic-ratelimit-unified-7d-utilization'?: string
|
||||
'anthropic-ratelimit-unified-7d-reset'?: string
|
||||
'anthropic-ratelimit-unified-7d-surpassed-threshold'?: string
|
||||
'anthropic-ratelimit-unified-overage-utilization'?: string
|
||||
'anthropic-ratelimit-unified-overage-surpassed-threshold'?: string
|
||||
}
|
||||
|
||||
export type MockHeaderKey =
|
||||
| 'status'
|
||||
| 'reset'
|
||||
| 'claim'
|
||||
| 'overage-status'
|
||||
| 'overage-reset'
|
||||
| 'overage-disabled-reason'
|
||||
| 'fallback'
|
||||
| 'fallback-percentage'
|
||||
| 'retry-after'
|
||||
| '5h-utilization'
|
||||
| '5h-reset'
|
||||
| '5h-surpassed-threshold'
|
||||
| '7d-utilization'
|
||||
| '7d-reset'
|
||||
| '7d-surpassed-threshold'
|
||||
|
||||
export type MockScenario =
|
||||
| 'normal'
|
||||
| 'session-limit-reached'
|
||||
| 'approaching-weekly-limit'
|
||||
| 'weekly-limit-reached'
|
||||
| 'overage-active'
|
||||
| 'overage-warning'
|
||||
| 'overage-exhausted'
|
||||
| 'out-of-credits'
|
||||
| 'org-zero-credit-limit'
|
||||
| 'org-spend-cap-hit'
|
||||
| 'member-zero-credit-limit'
|
||||
| 'seat-tier-zero-credit-limit'
|
||||
| 'opus-limit'
|
||||
| 'opus-warning'
|
||||
| 'sonnet-limit'
|
||||
| 'sonnet-warning'
|
||||
| 'fast-mode-limit'
|
||||
| 'fast-mode-short-limit'
|
||||
| 'extra-usage-required'
|
||||
| 'clear'
|
||||
|
||||
let mockHeaders: MockHeaders = {}
|
||||
let mockEnabled = false
|
||||
let mockHeaderless429Message: string | null = null
|
||||
let mockSubscriptionType: SubscriptionType | null = null
|
||||
let mockFastModeRateLimitDurationMs: number | null = null
|
||||
let mockFastModeRateLimitExpiresAt: number | null = null
|
||||
// Default subscription type for mock testing
|
||||
const DEFAULT_MOCK_SUBSCRIPTION: SubscriptionType = 'max'
|
||||
|
||||
// Track individual exceeded limits with their reset times
|
||||
type ExceededLimit = {
|
||||
type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet'
|
||||
resetsAt: number // Unix timestamp
|
||||
}
|
||||
|
||||
let exceededLimits: ExceededLimit[] = []
|
||||
|
||||
// New approach: Toggle individual headers
|
||||
export function setMockHeader(
|
||||
key: MockHeaderKey,
|
||||
value: string | undefined,
|
||||
): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
|
||||
mockEnabled = true
|
||||
|
||||
// Special case for retry-after which doesn't have the prefix
|
||||
const fullKey = (
|
||||
key === 'retry-after' ? 'retry-after' : `anthropic-ratelimit-unified-${key}`
|
||||
) as keyof MockHeaders
|
||||
|
||||
if (value === undefined || value === 'clear') {
|
||||
delete mockHeaders[fullKey]
|
||||
if (key === 'claim') {
|
||||
exceededLimits = []
|
||||
}
|
||||
// Update retry-after if status changed
|
||||
if (key === 'status' || key === 'overage-status') {
|
||||
updateRetryAfter()
|
||||
}
|
||||
return
|
||||
} else {
|
||||
// Handle special cases for reset times
|
||||
if (key === 'reset' || key === 'overage-reset') {
|
||||
// If user provides a number, treat it as hours from now
|
||||
const hours = Number(value)
|
||||
if (!isNaN(hours)) {
|
||||
value = String(Math.floor(Date.now() / 1000) + hours * 3600)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle claims - add to exceeded limits
|
||||
if (key === 'claim') {
|
||||
const validClaims = [
|
||||
'five_hour',
|
||||
'seven_day',
|
||||
'seven_day_opus',
|
||||
'seven_day_sonnet',
|
||||
]
|
||||
if (validClaims.includes(value)) {
|
||||
// Determine reset time based on claim type
|
||||
let resetsAt: number
|
||||
if (value === 'five_hour') {
|
||||
resetsAt = Math.floor(Date.now() / 1000) + 5 * 3600
|
||||
} else if (
|
||||
value === 'seven_day' ||
|
||||
value === 'seven_day_opus' ||
|
||||
value === 'seven_day_sonnet'
|
||||
) {
|
||||
resetsAt = Math.floor(Date.now() / 1000) + 7 * 24 * 3600
|
||||
} else {
|
||||
resetsAt = Math.floor(Date.now() / 1000) + 3600
|
||||
}
|
||||
|
||||
// Add to exceeded limits (remove if already exists)
|
||||
exceededLimits = exceededLimits.filter(l => l.type !== value)
|
||||
exceededLimits.push({ type: value as ExceededLimit['type'], resetsAt })
|
||||
|
||||
// Set the representative claim (furthest reset time)
|
||||
updateRepresentativeClaim()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Widen to a string-valued record so dynamic key assignment is allowed.
|
||||
// MockHeaders values are string-literal unions; assigning a raw user-input
|
||||
// string requires widening, but this is mock/test code so it's acceptable.
|
||||
const headers: Partial<Record<keyof MockHeaders, string>> = mockHeaders
|
||||
headers[fullKey] = value
|
||||
|
||||
// Update retry-after if status changed
|
||||
if (key === 'status' || key === 'overage-status') {
|
||||
updateRetryAfter()
|
||||
}
|
||||
}
|
||||
|
||||
// If all headers are cleared, disable mocking
|
||||
if (Object.keys(mockHeaders).length === 0) {
|
||||
mockEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to update retry-after based on current state
|
||||
function updateRetryAfter(): void {
|
||||
const status = mockHeaders['anthropic-ratelimit-unified-status']
|
||||
const overageStatus =
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status']
|
||||
const reset = mockHeaders['anthropic-ratelimit-unified-reset']
|
||||
|
||||
if (
|
||||
status === 'rejected' &&
|
||||
(!overageStatus || overageStatus === 'rejected') &&
|
||||
reset
|
||||
) {
|
||||
// Calculate seconds until reset
|
||||
const resetTimestamp = Number(reset)
|
||||
const secondsUntilReset = Math.max(
|
||||
0,
|
||||
resetTimestamp - Math.floor(Date.now() / 1000),
|
||||
)
|
||||
mockHeaders['retry-after'] = String(secondsUntilReset)
|
||||
} else {
|
||||
delete mockHeaders['retry-after']
|
||||
}
|
||||
}
|
||||
|
||||
// Update the representative claim based on exceeded limits
|
||||
function updateRepresentativeClaim(): void {
|
||||
if (exceededLimits.length === 0) {
|
||||
delete mockHeaders['anthropic-ratelimit-unified-representative-claim']
|
||||
delete mockHeaders['anthropic-ratelimit-unified-reset']
|
||||
delete mockHeaders['retry-after']
|
||||
return
|
||||
}
|
||||
|
||||
// Find the limit with the furthest reset time
|
||||
const furthest = exceededLimits.reduce((prev, curr) =>
|
||||
curr.resetsAt > prev.resetsAt ? curr : prev,
|
||||
)
|
||||
|
||||
// Set the representative claim (appears for both warning and rejected)
|
||||
mockHeaders['anthropic-ratelimit-unified-representative-claim'] =
|
||||
furthest.type
|
||||
mockHeaders['anthropic-ratelimit-unified-reset'] = String(furthest.resetsAt)
|
||||
|
||||
// Add retry-after if rejected and no overage available
|
||||
if (mockHeaders['anthropic-ratelimit-unified-status'] === 'rejected') {
|
||||
const overageStatus =
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status']
|
||||
if (!overageStatus || overageStatus === 'rejected') {
|
||||
// Calculate seconds until reset
|
||||
const secondsUntilReset = Math.max(
|
||||
0,
|
||||
furthest.resetsAt - Math.floor(Date.now() / 1000),
|
||||
)
|
||||
mockHeaders['retry-after'] = String(secondsUntilReset)
|
||||
} else {
|
||||
// Overage is available, no retry-after
|
||||
delete mockHeaders['retry-after']
|
||||
}
|
||||
} else {
|
||||
delete mockHeaders['retry-after']
|
||||
}
|
||||
}
|
||||
|
||||
// Add function to add exceeded limit with custom reset time
|
||||
export function addExceededLimit(
|
||||
type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet',
|
||||
hoursFromNow: number,
|
||||
): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
|
||||
mockEnabled = true
|
||||
const resetsAt = Math.floor(Date.now() / 1000) + hoursFromNow * 3600
|
||||
|
||||
// Remove existing limit of same type
|
||||
exceededLimits = exceededLimits.filter(l => l.type !== type)
|
||||
exceededLimits.push({ type, resetsAt })
|
||||
|
||||
// Update status to rejected if we have exceeded limits
|
||||
if (exceededLimits.length > 0) {
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
}
|
||||
|
||||
updateRepresentativeClaim()
|
||||
}
|
||||
|
||||
// Set mock early warning utilization for time-relative thresholds
|
||||
// claimAbbrev: '5h' or '7d'
|
||||
// utilization: 0-1 (e.g., 0.92 for 92% used)
|
||||
// hoursFromNow: hours until reset (default: 4 for 5h, 120 for 7d)
|
||||
export function setMockEarlyWarning(
|
||||
claimAbbrev: '5h' | '7d' | 'overage',
|
||||
utilization: number,
|
||||
hoursFromNow?: number,
|
||||
): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
|
||||
mockEnabled = true
|
||||
|
||||
// Clear ALL early warning headers first (5h is checked before 7d, so we need
|
||||
// to clear 5h headers when testing 7d to avoid 5h taking priority)
|
||||
clearMockEarlyWarning()
|
||||
|
||||
// Default hours based on claim type (early in window to trigger warning)
|
||||
const defaultHours = claimAbbrev === '5h' ? 4 : 5 * 24
|
||||
const hours = hoursFromNow ?? defaultHours
|
||||
const resetsAt = Math.floor(Date.now() / 1000) + hours * 3600
|
||||
|
||||
mockHeaders[`anthropic-ratelimit-unified-${claimAbbrev}-utilization`] =
|
||||
String(utilization)
|
||||
mockHeaders[`anthropic-ratelimit-unified-${claimAbbrev}-reset`] =
|
||||
String(resetsAt)
|
||||
// Set the surpassed-threshold header to trigger early warning
|
||||
mockHeaders[
|
||||
`anthropic-ratelimit-unified-${claimAbbrev}-surpassed-threshold`
|
||||
] = String(utilization)
|
||||
|
||||
// Set status to allowed so early warning logic can upgrade it
|
||||
if (!mockHeaders['anthropic-ratelimit-unified-status']) {
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'allowed'
|
||||
}
|
||||
}
|
||||
|
||||
// Clear mock early warning headers
|
||||
export function clearMockEarlyWarning(): void {
|
||||
delete mockHeaders['anthropic-ratelimit-unified-5h-utilization']
|
||||
delete mockHeaders['anthropic-ratelimit-unified-5h-reset']
|
||||
delete mockHeaders['anthropic-ratelimit-unified-5h-surpassed-threshold']
|
||||
delete mockHeaders['anthropic-ratelimit-unified-7d-utilization']
|
||||
delete mockHeaders['anthropic-ratelimit-unified-7d-reset']
|
||||
delete mockHeaders['anthropic-ratelimit-unified-7d-surpassed-threshold']
|
||||
}
|
||||
|
||||
export function setMockRateLimitScenario(scenario: MockScenario): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
|
||||
if (scenario === 'clear') {
|
||||
mockHeaders = {}
|
||||
mockHeaderless429Message = null
|
||||
mockEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
mockEnabled = true
|
||||
|
||||
// Set reset times for demos
|
||||
const fiveHoursFromNow = Math.floor(Date.now() / 1000) + 5 * 3600
|
||||
const sevenDaysFromNow = Math.floor(Date.now() / 1000) + 7 * 24 * 3600
|
||||
|
||||
// Clear existing headers
|
||||
mockHeaders = {}
|
||||
mockHeaderless429Message = null
|
||||
|
||||
// Only clear exceeded limits for scenarios that explicitly set them
|
||||
// Overage scenarios should preserve existing exceeded limits
|
||||
const preserveExceededLimits = [
|
||||
'overage-active',
|
||||
'overage-warning',
|
||||
'overage-exhausted',
|
||||
].includes(scenario)
|
||||
if (!preserveExceededLimits) {
|
||||
exceededLimits = []
|
||||
}
|
||||
|
||||
switch (scenario) {
|
||||
case 'normal':
|
||||
mockHeaders = {
|
||||
'anthropic-ratelimit-unified-status': 'allowed',
|
||||
'anthropic-ratelimit-unified-reset': String(fiveHoursFromNow),
|
||||
}
|
||||
break
|
||||
|
||||
case 'session-limit-reached':
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
break
|
||||
|
||||
case 'approaching-weekly-limit':
|
||||
mockHeaders = {
|
||||
'anthropic-ratelimit-unified-status': 'allowed_warning',
|
||||
'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow),
|
||||
'anthropic-ratelimit-unified-representative-claim': 'seven_day',
|
||||
}
|
||||
break
|
||||
|
||||
case 'weekly-limit-reached':
|
||||
exceededLimits = [{ type: 'seven_day', resetsAt: sevenDaysFromNow }]
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
break
|
||||
|
||||
case 'overage-active': {
|
||||
// If no limits have been exceeded yet, default to 5-hour
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'allowed'
|
||||
// Set overage reset time (monthly)
|
||||
const endOfMonthActive = new Date()
|
||||
endOfMonthActive.setMonth(endOfMonthActive.getMonth() + 1, 1)
|
||||
endOfMonthActive.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonthActive.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'overage-warning': {
|
||||
// If no limits have been exceeded yet, default to 5-hour
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] =
|
||||
'allowed_warning'
|
||||
// Overage typically resets monthly, but for demo let's say end of month
|
||||
const endOfMonth = new Date()
|
||||
endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1)
|
||||
endOfMonth.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonth.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'overage-exhausted': {
|
||||
// If no limits have been exceeded yet, default to 5-hour
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected'
|
||||
// Both subscription and overage are exhausted
|
||||
// Subscription resets based on the exceeded limit, overage resets monthly
|
||||
const endOfMonthExhausted = new Date()
|
||||
endOfMonthExhausted.setMonth(endOfMonthExhausted.getMonth() + 1, 1)
|
||||
endOfMonthExhausted.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonthExhausted.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'out-of-credits': {
|
||||
// Out of credits - subscription limit hit, overage rejected due to insufficient credits
|
||||
// (wallet is empty)
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] =
|
||||
'out_of_credits'
|
||||
const endOfMonth = new Date()
|
||||
endOfMonth.setMonth(endOfMonth.getMonth() + 1, 1)
|
||||
endOfMonth.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonth.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'org-zero-credit-limit': {
|
||||
// Org service has zero credit limit - admin set org-level spend cap to $0
|
||||
// Non-admin Team/Enterprise users should not see "Request extra usage" option
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] =
|
||||
'org_service_zero_credit_limit'
|
||||
const endOfMonthZero = new Date()
|
||||
endOfMonthZero.setMonth(endOfMonthZero.getMonth() + 1, 1)
|
||||
endOfMonthZero.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonthZero.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'org-spend-cap-hit': {
|
||||
// Org spend cap hit for the month - org overages temporarily disabled
|
||||
// Non-admin Team/Enterprise users should not see "Request extra usage" option
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] =
|
||||
'org_level_disabled_until'
|
||||
const endOfMonthHit = new Date()
|
||||
endOfMonthHit.setMonth(endOfMonthHit.getMonth() + 1, 1)
|
||||
endOfMonthHit.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonthHit.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'member-zero-credit-limit': {
|
||||
// Member has zero credit limit - admin set this user's individual limit to $0
|
||||
// Non-admin Team/Enterprise users SHOULD see "Request extra usage" (admin can allocate more)
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] =
|
||||
'member_zero_credit_limit'
|
||||
const endOfMonthMember = new Date()
|
||||
endOfMonthMember.setMonth(endOfMonthMember.getMonth() + 1, 1)
|
||||
endOfMonthMember.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonthMember.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'seat-tier-zero-credit-limit': {
|
||||
// Seat tier has zero credit limit - admin set this seat tier's limit to $0
|
||||
// Non-admin Team/Enterprise users SHOULD see "Request extra usage" (admin can allocate more)
|
||||
if (exceededLimits.length === 0) {
|
||||
exceededLimits = [{ type: 'five_hour', resetsAt: fiveHoursFromNow }]
|
||||
}
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-status'] = 'rejected'
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-disabled-reason'] =
|
||||
'seat_tier_zero_credit_limit'
|
||||
const endOfMonthSeatTier = new Date()
|
||||
endOfMonthSeatTier.setMonth(endOfMonthSeatTier.getMonth() + 1, 1)
|
||||
endOfMonthSeatTier.setHours(0, 0, 0, 0)
|
||||
mockHeaders['anthropic-ratelimit-unified-overage-reset'] = String(
|
||||
Math.floor(endOfMonthSeatTier.getTime() / 1000),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'opus-limit': {
|
||||
exceededLimits = [{ type: 'seven_day_opus', resetsAt: sevenDaysFromNow }]
|
||||
updateRepresentativeClaim()
|
||||
// Always send 429 rejected status - the error handler will decide whether
|
||||
// to show an error or return NO_RESPONSE_REQUESTED based on fallback eligibility
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
break
|
||||
}
|
||||
|
||||
case 'opus-warning': {
|
||||
mockHeaders = {
|
||||
'anthropic-ratelimit-unified-status': 'allowed_warning',
|
||||
'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow),
|
||||
'anthropic-ratelimit-unified-representative-claim': 'seven_day_opus',
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'sonnet-limit': {
|
||||
exceededLimits = [
|
||||
{ type: 'seven_day_sonnet', resetsAt: sevenDaysFromNow },
|
||||
]
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
break
|
||||
}
|
||||
|
||||
case 'sonnet-warning': {
|
||||
mockHeaders = {
|
||||
'anthropic-ratelimit-unified-status': 'allowed_warning',
|
||||
'anthropic-ratelimit-unified-reset': String(sevenDaysFromNow),
|
||||
'anthropic-ratelimit-unified-representative-claim': 'seven_day_sonnet',
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'fast-mode-limit': {
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
// Duration in ms (> 20s threshold to trigger cooldown)
|
||||
mockFastModeRateLimitDurationMs = 10 * 60 * 1000
|
||||
break
|
||||
}
|
||||
|
||||
case 'fast-mode-short-limit': {
|
||||
updateRepresentativeClaim()
|
||||
mockHeaders['anthropic-ratelimit-unified-status'] = 'rejected'
|
||||
// Duration in ms (< 20s threshold, won't trigger cooldown)
|
||||
mockFastModeRateLimitDurationMs = 10 * 1000
|
||||
break
|
||||
}
|
||||
|
||||
case 'extra-usage-required': {
|
||||
// Headerless 429 — exercises the entitlement-rejection path in errors.ts
|
||||
mockHeaderless429Message =
|
||||
'Extra usage is required for long context requests.'
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export function getMockHeaderless429Message(): string | null {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return null
|
||||
}
|
||||
// Env var path for -p / SDK testing where slash commands aren't available
|
||||
if (process.env.CLAUDE_MOCK_HEADERLESS_429) {
|
||||
return process.env.CLAUDE_MOCK_HEADERLESS_429
|
||||
}
|
||||
if (!mockEnabled) {
|
||||
return null
|
||||
}
|
||||
return mockHeaderless429Message
|
||||
}
|
||||
|
||||
export function getMockHeaders(): MockHeaders | null {
|
||||
if (
|
||||
!mockEnabled ||
|
||||
process.env.USER_TYPE !== 'ant' ||
|
||||
Object.keys(mockHeaders).length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return mockHeaders
|
||||
}
|
||||
|
||||
export function getMockStatus(): string {
|
||||
if (
|
||||
!mockEnabled ||
|
||||
(Object.keys(mockHeaders).length === 0 && !mockSubscriptionType)
|
||||
) {
|
||||
return 'No mock headers active (using real limits)'
|
||||
}
|
||||
|
||||
const lines: string[] = []
|
||||
lines.push('Active mock headers:')
|
||||
|
||||
// Show subscription type - either explicitly set or default
|
||||
const effectiveSubscription =
|
||||
mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION
|
||||
if (mockSubscriptionType) {
|
||||
lines.push(` Subscription Type: ${mockSubscriptionType} (explicitly set)`)
|
||||
} else {
|
||||
lines.push(` Subscription Type: ${effectiveSubscription} (default)`)
|
||||
}
|
||||
|
||||
Object.entries(mockHeaders).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// Format the header name nicely
|
||||
const formattedKey = key
|
||||
.replace('anthropic-ratelimit-unified-', '')
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, c => c.toUpperCase())
|
||||
|
||||
// Format timestamps as human-readable
|
||||
if (key.includes('reset') && value) {
|
||||
const timestamp = Number(value)
|
||||
const date = new Date(timestamp * 1000)
|
||||
lines.push(` ${formattedKey}: ${value} (${date.toLocaleString()})`)
|
||||
} else {
|
||||
lines.push(` ${formattedKey}: ${value}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Show exceeded limits if any
|
||||
if (exceededLimits.length > 0) {
|
||||
lines.push('\nExceeded limits (contributing to representative claim):')
|
||||
exceededLimits.forEach(limit => {
|
||||
const date = new Date(limit.resetsAt * 1000)
|
||||
lines.push(` ${limit.type}: resets at ${date.toLocaleString()}`)
|
||||
})
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function clearMockHeaders(): void {
|
||||
mockHeaders = {}
|
||||
exceededLimits = []
|
||||
mockSubscriptionType = null
|
||||
mockFastModeRateLimitDurationMs = null
|
||||
mockFastModeRateLimitExpiresAt = null
|
||||
mockHeaderless429Message = null
|
||||
setMockBillingAccessOverride(null)
|
||||
mockEnabled = false
|
||||
}
|
||||
|
||||
export function applyMockHeaders(
|
||||
headers: globalThis.Headers,
|
||||
): globalThis.Headers {
|
||||
const mock = getMockHeaders()
|
||||
if (!mock) {
|
||||
return headers
|
||||
}
|
||||
|
||||
// Create a new Headers object with original headers
|
||||
// eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
|
||||
const newHeaders = new globalThis.Headers(headers)
|
||||
|
||||
// Apply mock headers (overwriting originals)
|
||||
Object.entries(mock).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
newHeaders.set(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
return newHeaders
|
||||
}
|
||||
|
||||
// Check if we should process rate limits even without subscription
|
||||
// This is for Ant employees testing with mocks
|
||||
export function shouldProcessMockLimits(): boolean {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return false
|
||||
}
|
||||
return mockEnabled || Boolean(process.env.CLAUDE_MOCK_HEADERLESS_429)
|
||||
}
|
||||
|
||||
export function getCurrentMockScenario(): MockScenario | null {
|
||||
if (!mockEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Reverse lookup the scenario from current headers
|
||||
if (!mockHeaders) return null
|
||||
|
||||
const status = mockHeaders['anthropic-ratelimit-unified-status']
|
||||
const overage = mockHeaders['anthropic-ratelimit-unified-overage-status']
|
||||
const claim = mockHeaders['anthropic-ratelimit-unified-representative-claim']
|
||||
|
||||
if (claim === 'seven_day_opus') {
|
||||
return status === 'rejected' ? 'opus-limit' : 'opus-warning'
|
||||
}
|
||||
|
||||
if (claim === 'seven_day_sonnet') {
|
||||
return status === 'rejected' ? 'sonnet-limit' : 'sonnet-warning'
|
||||
}
|
||||
|
||||
if (overage === 'rejected') return 'overage-exhausted'
|
||||
if (overage === 'allowed_warning') return 'overage-warning'
|
||||
if (overage === 'allowed') return 'overage-active'
|
||||
|
||||
if (status === 'rejected') {
|
||||
if (claim === 'five_hour') return 'session-limit-reached'
|
||||
if (claim === 'seven_day') return 'weekly-limit-reached'
|
||||
}
|
||||
|
||||
if (status === 'allowed_warning') {
|
||||
if (claim === 'seven_day') return 'approaching-weekly-limit'
|
||||
}
|
||||
|
||||
if (status === 'allowed') return 'normal'
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getScenarioDescription(scenario: MockScenario): string {
|
||||
switch (scenario) {
|
||||
case 'normal':
|
||||
return 'Normal usage, no limits'
|
||||
case 'session-limit-reached':
|
||||
return 'Session rate limit exceeded'
|
||||
case 'approaching-weekly-limit':
|
||||
return 'Approaching weekly aggregate limit'
|
||||
case 'weekly-limit-reached':
|
||||
return 'Weekly aggregate limit exceeded'
|
||||
case 'overage-active':
|
||||
return 'Using extra usage (overage active)'
|
||||
case 'overage-warning':
|
||||
return 'Approaching extra usage limit'
|
||||
case 'overage-exhausted':
|
||||
return 'Both subscription and extra usage limits exhausted'
|
||||
case 'out-of-credits':
|
||||
return 'Out of extra usage credits (wallet empty)'
|
||||
case 'org-zero-credit-limit':
|
||||
return 'Org spend cap is zero (no extra usage budget)'
|
||||
case 'org-spend-cap-hit':
|
||||
return 'Org spend cap hit for the month'
|
||||
case 'member-zero-credit-limit':
|
||||
return 'Member limit is zero (admin can allocate more)'
|
||||
case 'seat-tier-zero-credit-limit':
|
||||
return 'Seat tier limit is zero (admin can allocate more)'
|
||||
case 'opus-limit':
|
||||
return 'Opus limit reached'
|
||||
case 'opus-warning':
|
||||
return 'Approaching Opus limit'
|
||||
case 'sonnet-limit':
|
||||
return 'Sonnet limit reached'
|
||||
case 'sonnet-warning':
|
||||
return 'Approaching Sonnet limit'
|
||||
case 'fast-mode-limit':
|
||||
return 'Fast mode rate limit'
|
||||
case 'fast-mode-short-limit':
|
||||
return 'Fast mode rate limit (short)'
|
||||
case 'extra-usage-required':
|
||||
return 'Headerless 429: Extra usage required for 1M context'
|
||||
case 'clear':
|
||||
return 'Clear mock headers (use real limits)'
|
||||
default:
|
||||
return 'Unknown scenario'
|
||||
}
|
||||
}
|
||||
|
||||
// Mock subscription type management
|
||||
export function setMockSubscriptionType(
|
||||
subscriptionType: SubscriptionType | null,
|
||||
): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
mockEnabled = true
|
||||
mockSubscriptionType = subscriptionType
|
||||
}
|
||||
|
||||
export function getMockSubscriptionType(): SubscriptionType | null {
|
||||
if (!mockEnabled || process.env.USER_TYPE !== 'ant') {
|
||||
return null
|
||||
}
|
||||
// Return the explicitly set subscription type, or default to 'max'
|
||||
return mockSubscriptionType || DEFAULT_MOCK_SUBSCRIPTION
|
||||
}
|
||||
|
||||
// Export a function that checks if we should use mock subscription
|
||||
export function shouldUseMockSubscription(): boolean {
|
||||
return (
|
||||
mockEnabled &&
|
||||
mockSubscriptionType !== null &&
|
||||
process.env.USER_TYPE === 'ant'
|
||||
)
|
||||
}
|
||||
|
||||
// Mock billing access (admin vs non-admin)
|
||||
export function setMockBillingAccess(hasAccess: boolean | null): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
mockEnabled = true
|
||||
setMockBillingAccessOverride(hasAccess)
|
||||
}
|
||||
|
||||
// Mock fast mode rate limit handling
|
||||
export function isMockFastModeRateLimitScenario(): boolean {
|
||||
return mockFastModeRateLimitDurationMs !== null
|
||||
}
|
||||
|
||||
export function checkMockFastModeRateLimit(
|
||||
isFastModeActive?: boolean,
|
||||
): MockHeaders | null {
|
||||
if (mockFastModeRateLimitDurationMs === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Only throw when fast mode is active
|
||||
if (!isFastModeActive) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if the rate limit has expired
|
||||
if (
|
||||
mockFastModeRateLimitExpiresAt !== null &&
|
||||
Date.now() >= mockFastModeRateLimitExpiresAt
|
||||
) {
|
||||
clearMockHeaders()
|
||||
return null
|
||||
}
|
||||
|
||||
// Set expiry on first error (not when scenario is configured)
|
||||
if (mockFastModeRateLimitExpiresAt === null) {
|
||||
mockFastModeRateLimitExpiresAt =
|
||||
Date.now() + mockFastModeRateLimitDurationMs
|
||||
}
|
||||
|
||||
// Compute dynamic retry-after based on remaining time
|
||||
const remainingMs = mockFastModeRateLimitExpiresAt - Date.now()
|
||||
const headersToSend = { ...mockHeaders }
|
||||
headersToSend['retry-after'] = String(
|
||||
Math.max(1, Math.ceil(remainingMs / 1000)),
|
||||
)
|
||||
|
||||
return headersToSend
|
||||
}
|
||||
Reference in New Issue
Block a user