@@ -18,7 +18,18 @@ import type {
1818 LoggerWithContextFn ,
1919} from '@codebuff/common/types/contracts/logger'
2020
21- import type { BlockGrantResult } from '@codebuff/billing/subscription'
21+ import type {
22+ BlockGrantResult ,
23+ } from '@codebuff/billing/subscription'
24+ import {
25+ isWeeklyLimitError ,
26+ isBlockExhaustedError ,
27+ } from '@codebuff/billing/subscription'
28+
29+ export type GetUserPreferencesFn = ( params : {
30+ userId : string
31+ logger : Logger
32+ } ) => Promise < { fallbackToALaCarte : boolean } >
2233import type { NextRequest } from 'next/server'
2334
2435import type { ChatCompletionRequestBody } from '@/llm-api/types'
@@ -81,6 +92,7 @@ export async function postChatCompletions(params: {
8192 fetch : typeof globalThis . fetch
8293 insertMessageBigquery : InsertMessageBigqueryFn
8394 ensureSubscriberBlockGrant ?: ( params : { userId : string ; logger : Logger } ) => Promise < BlockGrantResult | null >
95+ getUserPreferences ?: GetUserPreferencesFn
8496} ) {
8597 const {
8698 req,
@@ -92,6 +104,7 @@ export async function postChatCompletions(params: {
92104 fetch,
93105 insertMessageBigquery,
94106 ensureSubscriberBlockGrant,
107+ getUserPreferences,
95108 } = params
96109 let { logger } = params
97110
@@ -186,19 +199,6 @@ export async function postChatCompletions(params: {
186199 logger,
187200 } )
188201
189- // For subscribers, ensure a block grant exists before checking balance.
190- // This is done here because block grants should only start when the user begins working.
191- if ( ensureSubscriberBlockGrant ) {
192- try {
193- await ensureSubscriberBlockGrant ( { userId, logger } )
194- } catch ( error ) {
195- logger . error (
196- { error : getErrorObject ( error ) , userId } ,
197- 'Error ensuring subscription block grant' ,
198- )
199- }
200- }
201-
202202 // Check user credits
203203 const {
204204 balance : { totalRemaining } ,
@@ -281,6 +281,59 @@ export async function postChatCompletions(params: {
281281 )
282282 }
283283
284+ // For subscribers, ensure a block grant exists before processing the request.
285+ // This is done AFTER validation so malformed requests don't start a new 5-hour block.
286+ if ( ensureSubscriberBlockGrant ) {
287+ try {
288+ const blockGrantResult = await ensureSubscriberBlockGrant ( { userId, logger } )
289+
290+ // Check if user hit subscription limit and should be rate-limited
291+ if ( blockGrantResult && ( isWeeklyLimitError ( blockGrantResult ) || isBlockExhaustedError ( blockGrantResult ) ) ) {
292+ // Fetch user's preference for falling back to a-la-carte credits
293+ const preferences = getUserPreferences
294+ ? await getUserPreferences ( { userId, logger } )
295+ : { fallbackToALaCarte : true } // Default to allowing a-la-carte if no preference function
296+
297+ if ( ! preferences . fallbackToALaCarte ) {
298+ const resetTime = blockGrantResult . resetsAt
299+ const resetCountdown = formatQuotaResetCountdown ( resetTime . toISOString ( ) )
300+ const limitType = isWeeklyLimitError ( blockGrantResult ) ? 'weekly' : '5-hour session'
301+
302+ trackEvent ( {
303+ event : AnalyticsEvent . CHAT_COMPLETIONS_INSUFFICIENT_CREDITS ,
304+ userId,
305+ properties : {
306+ reason : 'subscription_limit_no_fallback' ,
307+ limitType,
308+ fallbackToALaCarte : false ,
309+ } ,
310+ logger,
311+ } )
312+
313+ return NextResponse . json (
314+ {
315+ error : 'rate_limit_exceeded' ,
316+ message : `Subscription ${ limitType } limit reached. Your limit resets ${ resetCountdown } . Enable "Continue with credits" in the CLI to use a-la-carte credits.` ,
317+ } ,
318+ { status : 429 } ,
319+ )
320+ }
321+ // If fallbackToALaCarte is true, continue to use a-la-carte credits
322+ logger . info (
323+ { userId, limitType : isWeeklyLimitError ( blockGrantResult ) ? 'weekly' : 'session' } ,
324+ 'Subscriber hit limit, falling back to a-la-carte credits' ,
325+ )
326+ }
327+ } catch ( error ) {
328+ logger . error (
329+ { error : getErrorObject ( error ) , userId } ,
330+ 'Error ensuring subscription block grant' ,
331+ )
332+ // Fail open: if we can't check the subscription status, allow the request to proceed
333+ // This is intentional - we prefer to allow requests rather than block legitimate users
334+ }
335+ }
336+
284337 const openrouterApiKey = req . headers . get ( BYOK_OPENROUTER_HEADER )
285338
286339 // Handle streaming vs non-streaming
0 commit comments