rateLimitMessages.ts
services/rateLimitMessages.ts
345
Lines
10858
Bytes
7
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with integrations. It contains 345 lines, 4 detected imports, and 7 detected exports.
Important relationships
Detected exports
RATE_LIMIT_ERROR_PREFIXESisRateLimitErrorMessageRateLimitMessagegetRateLimitMessagegetRateLimitErrorMessagegetRateLimitWarninggetUsingOverageText
Keywords
limitslimitmessagelimitnamesubscriptiontypewarningratelimittypemodelresettimeupsell
Detected imports
../utils/auth.js../utils/billing.js../utils/format.js./claudeAiLimits.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
/**
* 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}`
}