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