| |
| |
| |
| |
|
|
| 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' |
|
|
| |
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| export function isRateLimitErrorMessage(text: string): boolean { |
| return RATE_LIMIT_ERROR_PREFIXES.some(prefix => text.startsWith(prefix)) |
| } |
|
|
| export type RateLimitMessage = { |
| message: string |
| severity: 'error' | 'warning' |
| } |
|
|
| |
| |
| |
| |
| export function getRateLimitMessage( |
| limits: ClaudeAILimits, |
| model: string, |
| ): RateLimitMessage | null { |
| |
| |
| if (limits.isUsingOverage) { |
| |
| if (limits.overageStatus === 'allowed_warning') { |
| return { |
| message: "You're close to your extra usage spending limit", |
| severity: 'warning', |
| } |
| } |
| return null |
| } |
|
|
| |
| if (limits.status === 'rejected') { |
| return { message: getLimitReachedText(limits, model), severity: 'error' } |
| } |
|
|
| |
| if (limits.status === 'allowed_warning') { |
| |
| |
| |
| const WARNING_THRESHOLD = 0.7 |
| if ( |
| limits.utilization !== undefined && |
| limits.utilization < WARNING_THRESHOLD |
| ) { |
| return null |
| } |
|
|
| |
| |
| 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' } |
| } |
| } |
|
|
| |
| return null |
| } |
|
|
| |
| |
| |
| |
| export function getRateLimitErrorMessage( |
| limits: ClaudeAILimits, |
| model: string, |
| ): string | null { |
| const message = getRateLimitMessage(limits, model) |
|
|
| |
| if (message && message.severity === 'error') { |
| return message.message |
| } |
|
|
| return null |
| } |
|
|
| |
| |
| |
| |
| export function getRateLimitWarning( |
| limits: ClaudeAILimits, |
| model: string, |
| ): string | null { |
| const message = getRateLimitMessage(limits, model) |
|
|
| |
| if (message && message.severity === 'warning') { |
| return message.message |
| } |
|
|
| |
| 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 (limits.overageStatus === 'rejected') { |
| |
| let overageResetMessage = '' |
| if (resetsAt && limits.overageResetsAt) { |
| |
| 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' |
| |
| 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 |
| } |
|
|
| |
| const used = limits.utilization |
| ? Math.floor(limits.utilization * 100) |
| : undefined |
| const resetTime = limits.resetsAt |
| ? formatResetTime(limits.resetsAt, true) |
| : undefined |
|
|
| |
| 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') { |
| |
| limitName += ' limit' |
| } |
|
|
| if (resetTime) { |
| const base = `Approaching ${limitName} 路 resets ${resetTime}` |
| return upsell ? `${base} 路 ${upsell}` : base |
| } |
|
|
| const base = `Approaching ${limitName}` |
| return upsell ? `${base} 路 ${upsell}` : base |
| } |
|
|
| |
| |
| |
| |
| |
| function getWarningUpsellText( |
| rateLimitType: ClaudeAILimits['rateLimitType'], |
| ): string | null { |
| const subscriptionType = getSubscriptionType() |
| const hasExtraUsageEnabled = |
| getOauthAccountInfo()?.hasExtraUsageEnabled === true |
|
|
| |
| if (rateLimitType === 'five_hour') { |
| |
| |
| if (subscriptionType === 'team' || subscriptionType === 'enterprise') { |
| if (!hasExtraUsageEnabled && isOverageProvisioningAllowed()) { |
| return '/extra-usage to request more' |
| } |
| |
| return null |
| } |
|
|
| |
| if (subscriptionType === 'pro' || subscriptionType === 'max') { |
| return '/upgrade to keep using Claude Code' |
| } |
| } |
|
|
| |
| if (rateLimitType === 'overage') { |
| if (subscriptionType === 'team' || subscriptionType === 'enterprise') { |
| if (!hasExtraUsageEnabled && isOverageProvisioningAllowed()) { |
| return '/extra-usage to request more' |
| } |
| } |
| } |
|
|
| |
| return null |
| } |
|
|
| |
| |
| |
| |
| 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' |
| |
| 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 { |
| |
| 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}` |
| } |
|
|