@@ -204,29 +204,11 @@ export async function postChatCompletions(params: {
204204 const costMode = typedBody . codebuff_metadata ?. cost_mode
205205 const isFreeModeRequest = isFreeMode ( costMode )
206206
207- // Check user credits (skip for FREE mode since those requests cost 0 credits )
207+ // Fetch user credit data early (actual credit check happens after subscription block grant logic )
208208 const {
209209 balance : { totalRemaining } ,
210210 nextQuotaReset,
211211 } = await getUserUsageData ( { userId, logger } )
212- if ( totalRemaining <= 0 && ! isFreeModeRequest ) {
213- trackEvent ( {
214- event : AnalyticsEvent . CHAT_COMPLETIONS_INSUFFICIENT_CREDITS ,
215- userId,
216- properties : {
217- totalRemaining,
218- nextQuotaReset,
219- } ,
220- logger,
221- } )
222- const resetCountdown = formatQuotaResetCountdown ( nextQuotaReset )
223- return NextResponse . json (
224- {
225- message : `Out of credits. Please add credits at ${ env . NEXT_PUBLIC_CODEBUFF_APP_URL } /usage. Your free credits reset ${ resetCountdown } .` ,
226- } ,
227- { status : 402 } ,
228- )
229- }
230212
231213 // Extract and validate agent run ID
232214 const runIdFromBody = typedBody . codebuff_metadata ?. run_id
@@ -288,6 +270,7 @@ export async function postChatCompletions(params: {
288270
289271 // For subscribers, ensure a block grant exists before processing the request.
290272 // This is done AFTER validation so malformed requests don't start a new 5-hour block.
273+ let subscriberHasAvailableCredits = false
291274 if ( ensureSubscriberBlockGrant ) {
292275 try {
293276 const blockGrantResult = await ensureSubscriberBlockGrant ( { userId, logger } )
@@ -328,17 +311,42 @@ export async function postChatCompletions(params: {
328311 { userId, limitType : isWeeklyLimitError ( blockGrantResult ) ? 'weekly' : 'session' } ,
329312 'Subscriber hit limit, falling back to a-la-carte credits' ,
330313 )
314+ } else if ( blockGrantResult ) {
315+ subscriberHasAvailableCredits = true
331316 }
332317 } catch ( error ) {
333318 logger . error (
334319 { error : getErrorObject ( error ) , userId } ,
335320 'Error ensuring subscription block grant' ,
336321 )
337- // Fail open: if we can't check the subscription status, allow the request to proceed
338- // This is intentional - we prefer to allow requests rather than block legitimate users
322+ // Fail open: if we can't check the subscription status, allow the request to proceed.
323+ // Assume the user may be a subscriber so the credit check below doesn't reject them.
324+ subscriberHasAvailableCredits = true
339325 }
340326 }
341327
328+ // Credit check: reject if user has no a-la-carte credits AND is not covered by subscription.
329+ // Subscribers with available block grant credits bypass this check since their
330+ // subscription credits are excluded from totalRemaining (isPersonalContext: true).
331+ if ( totalRemaining <= 0 && ! isFreeModeRequest && ! subscriberHasAvailableCredits ) {
332+ trackEvent ( {
333+ event : AnalyticsEvent . CHAT_COMPLETIONS_INSUFFICIENT_CREDITS ,
334+ userId,
335+ properties : {
336+ totalRemaining,
337+ nextQuotaReset,
338+ } ,
339+ logger,
340+ } )
341+ const resetCountdown = formatQuotaResetCountdown ( nextQuotaReset )
342+ return NextResponse . json (
343+ {
344+ message : `Out of credits. Please add credits at ${ env . NEXT_PUBLIC_CODEBUFF_APP_URL } /usage. Your free credits reset ${ resetCountdown } .` ,
345+ } ,
346+ { status : 402 } ,
347+ )
348+ }
349+
342350 const openrouterApiKey = req . headers . get ( BYOK_OPENROUTER_HEADER )
343351
344352 // Handle streaming vs non-streaming
0 commit comments