source dump of claude code
at main 344 lines 11 kB view raw
1/** 2 * Centralized rate limit message generation 3 * Single source of truth for all rate limit-related messages 4 */ 5 6import { 7 getOauthAccountInfo, 8 getSubscriptionType, 9 isOverageProvisioningAllowed, 10} from '../utils/auth.js' 11import { hasClaudeAiBillingAccess } from '../utils/billing.js' 12import { formatResetTime } from '../utils/format.js' 13import type { ClaudeAILimits } from './claudeAiLimits.js' 14 15const FEEDBACK_CHANNEL_ANT = '#briarpatch-cc' 16 17/** 18 * All possible rate limit error message prefixes 19 * Export this to avoid fragile string matching in UI components 20 */ 21export const RATE_LIMIT_ERROR_PREFIXES = [ 22 "You've hit your", 23 "You've used", 24 "You're now using extra usage", 25 "You're close to", 26 "You're out of extra usage", 27] as const 28 29/** 30 * Check if a message is a rate limit error 31 */ 32export function isRateLimitErrorMessage(text: string): boolean { 33 return RATE_LIMIT_ERROR_PREFIXES.some(prefix => text.startsWith(prefix)) 34} 35 36export type RateLimitMessage = { 37 message: string 38 severity: 'error' | 'warning' 39} 40 41/** 42 * Get the appropriate rate limit message based on limit state 43 * Returns null if no message should be shown 44 */ 45export function getRateLimitMessage( 46 limits: ClaudeAILimits, 47 model: string, 48): RateLimitMessage | null { 49 // Check overage scenarios first (when subscription is rejected but overage is available) 50 // getUsingOverageText is rendered separately from warning. 51 if (limits.isUsingOverage) { 52 // Show warning if approaching overage spending limit 53 if (limits.overageStatus === 'allowed_warning') { 54 return { 55 message: "You're close to your extra usage spending limit", 56 severity: 'warning', 57 } 58 } 59 return null 60 } 61 62 // ERROR STATES - when limits are rejected 63 if (limits.status === 'rejected') { 64 return { message: getLimitReachedText(limits, model), severity: 'error' } 65 } 66 67 // WARNING STATES - when approaching limits with early warning 68 if (limits.status === 'allowed_warning') { 69 // Only show warnings when utilization is above threshold (70%) 70 // This prevents false warnings after week reset when API may send 71 // allowed_warning with stale data at low usage levels 72 const WARNING_THRESHOLD = 0.7 73 if ( 74 limits.utilization !== undefined && 75 limits.utilization < WARNING_THRESHOLD 76 ) { 77 return null 78 } 79 80 // Don't warn non-billing Team/Enterprise users about approaching plan limits 81 // if overages are enabled - they'll seamlessly roll into overage 82 const subscriptionType = getSubscriptionType() 83 const isTeamOrEnterprise = 84 subscriptionType === 'team' || subscriptionType === 'enterprise' 85 const hasExtraUsageEnabled = 86 getOauthAccountInfo()?.hasExtraUsageEnabled === true 87 88 if ( 89 isTeamOrEnterprise && 90 hasExtraUsageEnabled && 91 !hasClaudeAiBillingAccess() 92 ) { 93 return null 94 } 95 96 const text = getEarlyWarningText(limits) 97 if (text) { 98 return { message: text, severity: 'warning' } 99 } 100 } 101 102 // No message needed 103 return null 104} 105 106/** 107 * Get error message for API errors (used in errors.ts) 108 * Returns the message string or null if no error message should be shown 109 */ 110export function getRateLimitErrorMessage( 111 limits: ClaudeAILimits, 112 model: string, 113): string | null { 114 const message = getRateLimitMessage(limits, model) 115 116 // Only return error messages, not warnings 117 if (message && message.severity === 'error') { 118 return message.message 119 } 120 121 return null 122} 123 124/** 125 * Get warning message for UI footer 126 * Returns the warning message string or null if no warning should be shown 127 */ 128export function getRateLimitWarning( 129 limits: ClaudeAILimits, 130 model: string, 131): string | null { 132 const message = getRateLimitMessage(limits, model) 133 134 // Only return warnings for the footer - errors are shown in AssistantTextMessages 135 if (message && message.severity === 'warning') { 136 return message.message 137 } 138 139 // Don't show errors in the footer 140 return null 141} 142 143function getLimitReachedText(limits: ClaudeAILimits, model: string): string { 144 const resetsAt = limits.resetsAt 145 const resetTime = resetsAt ? formatResetTime(resetsAt, true) : undefined 146 const overageResetTime = limits.overageResetsAt 147 ? formatResetTime(limits.overageResetsAt, true) 148 : undefined 149 const resetMessage = resetTime ? ` · resets ${resetTime}` : '' 150 151 // if BOTH subscription (checked before this method) and overage are exhausted 152 if (limits.overageStatus === 'rejected') { 153 // Show the earliest reset time to indicate when user can resume 154 let overageResetMessage = '' 155 if (resetsAt && limits.overageResetsAt) { 156 // Both timestamps present - use the earlier one 157 if (resetsAt < limits.overageResetsAt) { 158 overageResetMessage = ` · resets ${resetTime}` 159 } else { 160 overageResetMessage = ` · resets ${overageResetTime}` 161 } 162 } else if (resetTime) { 163 overageResetMessage = ` · resets ${resetTime}` 164 } else if (overageResetTime) { 165 overageResetMessage = ` · resets ${overageResetTime}` 166 } 167 168 if (limits.overageDisabledReason === 'out_of_credits') { 169 return `You're out of extra usage${overageResetMessage}` 170 } 171 172 return formatLimitReachedText('limit', overageResetMessage, model) 173 } 174 175 if (limits.rateLimitType === 'seven_day_sonnet') { 176 const subscriptionType = getSubscriptionType() 177 const isProOrEnterprise = 178 subscriptionType === 'pro' || subscriptionType === 'enterprise' 179 // For pro and enterprise, Sonnet limit is the same as weekly 180 const limit = isProOrEnterprise ? 'weekly limit' : 'Sonnet limit' 181 return formatLimitReachedText(limit, resetMessage, model) 182 } 183 184 if (limits.rateLimitType === 'seven_day_opus') { 185 return formatLimitReachedText('Opus limit', resetMessage, model) 186 } 187 188 if (limits.rateLimitType === 'seven_day') { 189 return formatLimitReachedText('weekly limit', resetMessage, model) 190 } 191 192 if (limits.rateLimitType === 'five_hour') { 193 return formatLimitReachedText('session limit', resetMessage, model) 194 } 195 196 return formatLimitReachedText('usage limit', resetMessage, model) 197} 198 199function getEarlyWarningText(limits: ClaudeAILimits): string | null { 200 let limitName: string | null = null 201 switch (limits.rateLimitType) { 202 case 'seven_day': 203 limitName = 'weekly limit' 204 break 205 case 'five_hour': 206 limitName = 'session limit' 207 break 208 case 'seven_day_opus': 209 limitName = 'Opus limit' 210 break 211 case 'seven_day_sonnet': 212 limitName = 'Sonnet limit' 213 break 214 case 'overage': 215 limitName = 'extra usage' 216 break 217 case undefined: 218 return null 219 } 220 221 // utilization and resetsAt should be defined since early warning is calculated with them 222 const used = limits.utilization 223 ? Math.floor(limits.utilization * 100) 224 : undefined 225 const resetTime = limits.resetsAt 226 ? formatResetTime(limits.resetsAt, true) 227 : undefined 228 229 // Get upsell command based on subscription type and limit type 230 const upsell = getWarningUpsellText(limits.rateLimitType) 231 232 if (used && resetTime) { 233 const base = `You've used ${used}% of your ${limitName} · resets ${resetTime}` 234 return upsell ? `${base} · ${upsell}` : base 235 } 236 237 if (used) { 238 const base = `You've used ${used}% of your ${limitName}` 239 return upsell ? `${base} · ${upsell}` : base 240 } 241 242 if (limits.rateLimitType === 'overage') { 243 // For the "Approaching <x>" verbiage, "extra usage limit" makes more sense than "extra usage" 244 limitName += ' limit' 245 } 246 247 if (resetTime) { 248 const base = `Approaching ${limitName} · resets ${resetTime}` 249 return upsell ? `${base} · ${upsell}` : base 250 } 251 252 const base = `Approaching ${limitName}` 253 return upsell ? `${base} · ${upsell}` : base 254} 255 256/** 257 * Get the upsell command text for warning messages based on subscription and limit type. 258 * Returns null if no upsell should be shown. 259 * Only used for warnings because actual rate limit hits will see an interactive menu of options. 260 */ 261function getWarningUpsellText( 262 rateLimitType: ClaudeAILimits['rateLimitType'], 263): string | null { 264 const subscriptionType = getSubscriptionType() 265 const hasExtraUsageEnabled = 266 getOauthAccountInfo()?.hasExtraUsageEnabled === true 267 268 // 5-hour session limit warning 269 if (rateLimitType === 'five_hour') { 270 // Teams/Enterprise with overages disabled: prompt to request extra usage 271 // Only show if overage provisioning is allowed for this org type (e.g., not AWS marketplace) 272 if (subscriptionType === 'team' || subscriptionType === 'enterprise') { 273 if (!hasExtraUsageEnabled && isOverageProvisioningAllowed()) { 274 return '/extra-usage to request more' 275 } 276 // Teams/Enterprise with overages enabled or unsupported billing type don't need upsell 277 return null 278 } 279 280 // Pro/Max users: prompt to upgrade 281 if (subscriptionType === 'pro' || subscriptionType === 'max') { 282 return '/upgrade to keep using Claude Code' 283 } 284 } 285 286 // Overage warning (approaching spending limit) 287 if (rateLimitType === 'overage') { 288 if (subscriptionType === 'team' || subscriptionType === 'enterprise') { 289 if (!hasExtraUsageEnabled && isOverageProvisioningAllowed()) { 290 return '/extra-usage to request more' 291 } 292 } 293 } 294 295 // Weekly limit warnings don't show upsell per spec 296 return null 297} 298 299/** 300 * Get notification text for overage mode transitions 301 * Used for transient notifications when entering overage mode 302 */ 303export function getUsingOverageText(limits: ClaudeAILimits): string { 304 const resetTime = limits.resetsAt 305 ? formatResetTime(limits.resetsAt, true) 306 : '' 307 308 let limitName = '' 309 if (limits.rateLimitType === 'five_hour') { 310 limitName = 'session limit' 311 } else if (limits.rateLimitType === 'seven_day') { 312 limitName = 'weekly limit' 313 } else if (limits.rateLimitType === 'seven_day_opus') { 314 limitName = 'Opus limit' 315 } else if (limits.rateLimitType === 'seven_day_sonnet') { 316 const subscriptionType = getSubscriptionType() 317 const isProOrEnterprise = 318 subscriptionType === 'pro' || subscriptionType === 'enterprise' 319 // For pro and enterprise, Sonnet limit is the same as weekly 320 limitName = isProOrEnterprise ? 'weekly limit' : 'Sonnet limit' 321 } 322 323 if (!limitName) { 324 return 'Now using extra usage' 325 } 326 327 const resetMessage = resetTime 328 ? ` · Your ${limitName} resets ${resetTime}` 329 : '' 330 return `You're now using extra usage${resetMessage}` 331} 332 333function formatLimitReachedText( 334 limit: string, 335 resetMessage: string, 336 _model: string, 337): string { 338 // Enhanced messaging for Ant users 339 if (process.env.USER_TYPE === 'ant') { 340 return `You've hit your ${limit}${resetMessage}. If you have feedback about this limit, post in ${FEEDBACK_CHANNEL_ANT}. You can reset your limits with /reset-limits` 341 } 342 343 return `You've hit your ${limit}${resetMessage}` 344}