Remove internal-only bundled skills and mock helpers (#376)
* Remove internal-only bundled skills and mock rate-limit behavior This takes the next planned Phase C-lite slice by deleting bundled skills that only ever registered for internal users and replacing the internal mock rate-limit helper with a stable no-op external stub. The external build keeps the same behavior while removing a concentrated block of USER_TYPE-gated dead code. Constraint: Limit this PR to isolated internal-only helpers and avoid bridge, oauth, or rebrand behavior Rejected: Broad USER_TYPE cleanup across mixed runtime surfaces | too risky for the next medium-sized PR Confidence: high Scope-risk: moderate Reversibility: clean Directive: The next cleanup pass should continue with similarly isolated USER_TYPE helpers before touching main.tsx or protocol-heavy code Tested: bun run build Tested: bun run smoke Tested: bun run verify:privacy Tested: bun run test:provider Tested: bun run test:provider-recommendation Not-tested: Full repo typecheck (upstream baseline remains noisy) * Align internal-only helper removal with remaining user guidance This follow-up fixes the mock billing stub to be a true no-op and removes stale user-facing references to /verify and /skillify from the same PR. It also leaves a clearer paper trail for review: the deleted verify skill was explicitly ant-gated before removal, and the remaining mock helper callers still resolve to safe no-op returns in the external build. Constraint: Keep the PR focused on consistency fixes and reviewer-requested evidence, not new cleanup scope Rejected: Leave stale guidance for a later PR | would make this branch internally inconsistent after skill removal Confidence: high Scope-risk: narrow Reversibility: clean Directive: When deleting gated features, always sweep user guidance and coordinator prompts in the same pass Tested: bun run build Tested: bun run smoke Tested: bun run verify:privacy Tested: bun run test:provider Tested: bun run test:provider-recommendation Not-tested: Full repo typecheck (upstream baseline remains noisy; changed-file scan still shows only pre-existing tipRegistry errors outside edited lines) * Clarify generic workflow wording after skill removal This removes the last generic verification-skill wording that could still be read as pointing at a deleted bundled command. The guidance now talks about project workflows rather than a specific bundled verify skill. Constraint: Keep the follow-up limited to reviewer-facing wording cleanup on the same PR Rejected: Leave generic wording as-is | still too easy to misread after the explicit /verify references were removed Confidence: high Scope-risk: narrow Reversibility: clean Directive: When removing bundled commands, scrub both explicit and generic references in the same branch Tested: bun run build Tested: bun run smoke Not-tested: Additional checks unchanged by wording-only follow-up --------- Co-authored-by: anandh8x <test@example.com>
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
// Mock rate limits for testing [internal-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.
|
||||
// The external build keeps this module as a stable no-op surface so imports
|
||||
// remain valid without exposing internal-only rate-limit simulation behavior.
|
||||
|
||||
import type { SubscriptionType } from '../services/oauth/types.js'
|
||||
import { setMockBillingAccessOverride } from '../utils/billing.js'
|
||||
import type { OverageDisabledReason } from './claudeAiLimits.js'
|
||||
|
||||
type SubscriptionType = string
|
||||
|
||||
type MockHeaders = {
|
||||
'anthropic-ratelimit-unified-status'?:
|
||||
| 'allowed'
|
||||
@@ -29,7 +27,6 @@ type MockHeaders = {
|
||||
'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
|
||||
@@ -79,679 +76,53 @@ export type MockScenario =
|
||||
| '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
|
||||
}
|
||||
_key: MockHeaderKey,
|
||||
_value: string | undefined,
|
||||
): void {}
|
||||
|
||||
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
|
||||
}
|
||||
_type: 'five_hour' | 'seven_day' | 'seven_day_opus' | 'seven_day_sonnet',
|
||||
_hoursFromNow: number,
|
||||
): void {}
|
||||
|
||||
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
|
||||
}
|
||||
_claimAbbrev: '5h' | '7d' | 'overage',
|
||||
_utilization: number,
|
||||
_hoursFromNow?: number,
|
||||
): void {}
|
||||
|
||||
mockEnabled = true
|
||||
export function clearMockEarlyWarning(): void {}
|
||||
|
||||
// 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 setMockRateLimitScenario(_scenario: MockScenario): void {}
|
||||
|
||||
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
|
||||
return null
|
||||
}
|
||||
|
||||
export function getMockHeaders(): MockHeaders | null {
|
||||
if (
|
||||
!mockEnabled ||
|
||||
process.env.USER_TYPE !== 'ant' ||
|
||||
Object.keys(mockHeaders).length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return mockHeaders
|
||||
return null
|
||||
}
|
||||
|
||||
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')
|
||||
return 'No mock headers active (using real limits)'
|
||||
}
|
||||
|
||||
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
|
||||
return headers
|
||||
}
|
||||
|
||||
// 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)
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -802,81 +173,28 @@ export function getScenarioDescription(scenario: MockScenario): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Mock subscription type management
|
||||
export function setMockSubscriptionType(
|
||||
subscriptionType: SubscriptionType | null,
|
||||
): void {
|
||||
if (process.env.USER_TYPE !== 'ant') {
|
||||
return
|
||||
}
|
||||
mockEnabled = true
|
||||
mockSubscriptionType = subscriptionType
|
||||
}
|
||||
_subscriptionType: SubscriptionType | null,
|
||||
): void {}
|
||||
|
||||
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
|
||||
return null
|
||||
}
|
||||
|
||||
// Export a function that checks if we should use mock subscription
|
||||
export function shouldUseMockSubscription(): boolean {
|
||||
return (
|
||||
mockEnabled &&
|
||||
mockSubscriptionType !== null &&
|
||||
process.env.USER_TYPE === 'ant'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// 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)
|
||||
export function setMockBillingAccess(_hasAccess: boolean | null): void {
|
||||
// External build: internal mock billing access overrides are disabled.
|
||||
}
|
||||
|
||||
// Mock fast mode rate limit handling
|
||||
export function isMockFastModeRateLimitScenario(): boolean {
|
||||
return mockFastModeRateLimitDurationMs !== null
|
||||
return false
|
||||
}
|
||||
|
||||
export function checkMockFastModeRateLimit(
|
||||
isFastModeActive?: boolean,
|
||||
_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
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user