forked from sanbuphy/learn-coding-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrateLimitMessages.ts
More file actions
344 lines (298 loc) · 10.6 KB
/
Copy pathrateLimitMessages.ts
File metadata and controls
344 lines (298 loc) · 10.6 KB
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
/**
* 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}`
}