@@ -65,6 +65,7 @@ import {
6565 OpenRouterError ,
6666} from '@/llm-api/openrouter'
6767import { extractApiKeyFromHeader } from '@/util/auth'
68+ import { withDefaultProperties } from '@codebuff/common/analytics'
6869import { checkFreeModeRateLimit } from './free-mode-rate-limiter'
6970
7071const FREE_MODE_ALLOWED_COUNTRIES = new Set ( [
@@ -148,7 +149,6 @@ export async function postChatCompletions(params: {
148149 req,
149150 getUserInfoFromApiKey,
150151 loggerWithContext,
151- trackEvent,
152152 getUserUsageData,
153153 getAgentRunFromId,
154154 fetch,
@@ -157,6 +157,7 @@ export async function postChatCompletions(params: {
157157 getUserPreferences,
158158 } = params
159159 let { logger } = params
160+ let { trackEvent } = params
160161
161162 try {
162163 // Parse request body
@@ -182,6 +183,12 @@ export async function postChatCompletions(params: {
182183 const bodyStream = typedBody . stream ?? false
183184 const runId = typedBody . codebuff_metadata ?. run_id
184185
186+ // Check if the request is in FREE mode (costs 0 credits for allowed agent+model combos)
187+ const costMode = typedBody . codebuff_metadata ?. cost_mode
188+ const isFreeModeRequest = isFreeMode ( costMode )
189+
190+ trackEvent = withDefaultProperties ( trackEvent , { freebuff : isFreeModeRequest } )
191+
185192 // Extract and validate API key
186193 const apiKey = extractApiKeyFromHeader ( req )
187194 if ( ! apiKey ) {
@@ -249,10 +256,6 @@ export async function postChatCompletions(params: {
249256 logger,
250257 } )
251258
252- // Check if the request is in FREE mode (costs 0 credits for allowed agent+model combos)
253- const costMode = typedBody . codebuff_metadata ?. cost_mode
254- const isFreeModeRequest = isFreeMode ( costMode )
255-
256259 // For free mode requests, check if user is in US or Canada
257260 if ( isFreeModeRequest ) {
258261 const countryCode = getCountryCode ( req )
@@ -288,35 +291,6 @@ export async function postChatCompletions(params: {
288291 )
289292 }
290293
291- // Rate limit free mode requests
292- const rateLimitResult = checkFreeModeRateLimit ( userId )
293- if ( rateLimitResult . limited ) {
294- const retryAfterSeconds = Math . ceil ( rateLimitResult . retryAfterMs / 1000 )
295- const resetTime = new Date ( Date . now ( ) + rateLimitResult . retryAfterMs ) . toISOString ( )
296- const resetCountdown = formatQuotaResetCountdown ( resetTime )
297-
298- trackEvent ( {
299- event : AnalyticsEvent . CHAT_COMPLETIONS_VALIDATION_ERROR ,
300- userId,
301- properties : {
302- error : 'free_mode_rate_limited' ,
303- windowName : rateLimitResult . windowName ,
304- retryAfterSeconds,
305- } ,
306- logger,
307- } )
308-
309- return NextResponse . json (
310- {
311- error : 'free_mode_rate_limited' ,
312- message : `Free mode rate limit exceeded (${ rateLimitResult . windowName } limit). Try again ${ resetCountdown } .` ,
313- } ,
314- {
315- status : 429 ,
316- headers : { 'Retry-After' : String ( retryAfterSeconds ) } ,
317- } ,
318- )
319- }
320294 }
321295
322296 // Extract and validate agent run ID
@@ -377,6 +351,38 @@ export async function postChatCompletions(params: {
377351 )
378352 }
379353
354+ // Rate limit free mode requests (after validation so invalid requests don't consume quota)
355+ if ( isFreeModeRequest ) {
356+ const rateLimitResult = checkFreeModeRateLimit ( userId )
357+ if ( rateLimitResult . limited ) {
358+ const retryAfterSeconds = Math . ceil ( rateLimitResult . retryAfterMs / 1000 )
359+ const resetTime = new Date ( Date . now ( ) + rateLimitResult . retryAfterMs ) . toISOString ( )
360+ const resetCountdown = formatQuotaResetCountdown ( resetTime )
361+
362+ trackEvent ( {
363+ event : AnalyticsEvent . CHAT_COMPLETIONS_VALIDATION_ERROR ,
364+ userId,
365+ properties : {
366+ error : 'free_mode_rate_limited' ,
367+ windowName : rateLimitResult . windowName ,
368+ retryAfterSeconds,
369+ } ,
370+ logger,
371+ } )
372+
373+ return NextResponse . json (
374+ {
375+ error : 'free_mode_rate_limited' ,
376+ message : `Free mode rate limit exceeded (${ rateLimitResult . windowName } limit). Try again ${ resetCountdown } .` ,
377+ } ,
378+ {
379+ status : 429 ,
380+ headers : { 'Retry-After' : String ( retryAfterSeconds ) } ,
381+ } ,
382+ )
383+ }
384+ }
385+
380386 // For subscribers, ensure a block grant exists before processing the request.
381387 // This is done AFTER validation so malformed requests don't start a new 5-hour block.
382388 // When the function is provided, always include subscription credits in the balance:
0 commit comments