@@ -27,6 +27,107 @@ import { getByokOpenrouterApiKeyFromEnv } from '../env'
2727
2828import type { LanguageModel } from 'ai'
2929
30+ // ============================================================================
31+ // Claude OAuth Rate Limit Cache
32+ // ============================================================================
33+
34+ /** Timestamp (ms) when Claude OAuth rate limit expires, or null if not rate-limited */
35+ let claudeOAuthRateLimitedUntil : number | null = null
36+
37+ /**
38+ * Mark Claude OAuth as rate-limited. Subsequent requests will skip Claude OAuth
39+ * and use Codebuff backend until the reset time.
40+ * @param resetAt - When the rate limit resets. If not provided, guesses 5 minutes from now.
41+ */
42+ export function markClaudeOAuthRateLimited ( resetAt ?: Date ) : void {
43+ const fiveMinutesFromNow = Date . now ( ) + 5 * 60 * 1000
44+ claudeOAuthRateLimitedUntil = resetAt ? resetAt . getTime ( ) : fiveMinutesFromNow
45+ }
46+
47+ /**
48+ * Check if Claude OAuth is currently rate-limited.
49+ * Returns true if rate-limited and reset time hasn't passed.
50+ */
51+ export function isClaudeOAuthRateLimited ( ) : boolean {
52+ if ( claudeOAuthRateLimitedUntil === null ) {
53+ return false
54+ }
55+ if ( Date . now ( ) >= claudeOAuthRateLimitedUntil ) {
56+ // Rate limit expired, clear the cache
57+ claudeOAuthRateLimitedUntil = null
58+ return false
59+ }
60+ return true
61+ }
62+
63+ /**
64+ * Reset the Claude OAuth rate limit cache.
65+ * Call this when user reconnects their Claude subscription.
66+ */
67+ export function resetClaudeOAuthRateLimit ( ) : void {
68+ claudeOAuthRateLimitedUntil = null
69+ }
70+
71+ // ============================================================================
72+ // Claude OAuth Quota Fetching
73+ // ============================================================================
74+
75+ interface ClaudeQuotaWindow {
76+ utilization : number
77+ resets_at : string | null
78+ }
79+
80+ interface ClaudeQuotaResponse {
81+ five_hour : ClaudeQuotaWindow | null
82+ seven_day : ClaudeQuotaWindow | null
83+ seven_day_oauth_apps : ClaudeQuotaWindow | null
84+ seven_day_opus : ClaudeQuotaWindow | null
85+ }
86+
87+ /**
88+ * Fetch the rate limit reset time from Anthropic's quota API.
89+ * Returns the earliest reset time (whichever limit is more restrictive).
90+ * Returns null if fetch fails or no reset time is available.
91+ */
92+ export async function fetchClaudeOAuthResetTime ( accessToken : string ) : Promise < Date | null > {
93+ try {
94+ const response = await fetch ( 'https://api.anthropic.com/api/oauth/usage' , {
95+ method : 'GET' ,
96+ headers : {
97+ Authorization : `Bearer ${ accessToken } ` ,
98+ Accept : 'application/json' ,
99+ 'Content-Type' : 'application/json' ,
100+ 'anthropic-version' : '2023-06-01' ,
101+ 'anthropic-beta' : 'oauth-2025-04-20,claude-code-20250219' ,
102+ } ,
103+ } )
104+
105+ if ( ! response . ok ) {
106+ return null
107+ }
108+
109+ const data = ( await response . json ( ) ) as ClaudeQuotaResponse
110+
111+ // Parse reset times
112+ const fiveHour = data . five_hour
113+ const sevenDay = data . seven_day
114+
115+ const fiveHourRemaining = fiveHour ? Math . max ( 0 , 100 - fiveHour . utilization ) : 100
116+ const sevenDayRemaining = sevenDay ? Math . max ( 0 , 100 - sevenDay . utilization ) : 100
117+
118+ // Return the reset time for whichever limit is more restrictive (lower remaining)
119+ if ( fiveHourRemaining <= sevenDayRemaining && fiveHour ?. resets_at ) {
120+ return new Date ( fiveHour . resets_at )
121+ } else if ( sevenDay ?. resets_at ) {
122+ return new Date ( sevenDay . resets_at )
123+ }
124+
125+ return null
126+ } catch {
127+ return null
128+ }
129+ }
130+
30131/**
31132 * Parameters for requesting a model.
32133 */
@@ -69,7 +170,8 @@ export async function getModelForRequest(params: ModelRequestParams): Promise<Mo
69170 const { apiKey, model, skipClaudeOAuth } = params
70171
71172 // Check if we should use Claude OAuth direct
72- if ( ! skipClaudeOAuth && isClaudeModel ( model ) ) {
173+ // Skip if explicitly requested, if rate-limited, or if not a Claude model
174+ if ( ! skipClaudeOAuth && ! isClaudeOAuthRateLimited ( ) && isClaudeModel ( model ) ) {
73175 // Get valid credentials (will refresh if needed)
74176 const claudeOAuthCredentials = await getValidClaudeOAuthCredentials ( )
75177 if ( claudeOAuthCredentials ) {
0 commit comments