1+ import { PROFIT_MARGIN } from '@codebuff/common/constants/limits'
12import { getErrorObject } from '@codebuff/common/util/error'
23import { env } from '@codebuff/internal/env'
34
@@ -17,6 +18,13 @@ import type { InsertMessageBigqueryFn } from '@codebuff/common/types/contracts/b
1718import type { Logger } from '@codebuff/common/types/contracts/logger'
1819
1920type StreamState = { responseText : string ; reasoningText : string }
21+
22+ /** Result from processing a line, including optional billed credits for final chunk */
23+ type LineResult = {
24+ state : StreamState
25+ billedCredits ?: number
26+ }
27+
2028function createOpenRouterRequest ( params : {
2129 body : any
2230 openrouterApiKey : string | null
@@ -143,8 +151,8 @@ export async function handleOpenRouterNonStream({
143151 logger . error ( { error } , 'Failed to insert message into BigQuery' )
144152 } )
145153
146- // Consume credits
147- await consumeCreditsForMessage ( {
154+ // Consume credits and get the actual billed amount
155+ const billedCredits = await consumeCreditsForMessage ( {
148156 messageId : firstData . id ,
149157 userId,
150158 stripeCustomerId,
@@ -175,7 +183,9 @@ export async function handleOpenRouterNonStream({
175183 completion_tokens : aggregatedUsage . outputTokens ,
176184 total_tokens :
177185 aggregatedUsage . inputTokens + aggregatedUsage . outputTokens ,
178- cost : aggregatedUsage . cost ,
186+ // Overwrite cost so SDK calculates exact credits we charged
187+ cost : creditsToFakeCost ( billedCredits ) ,
188+ cost_details : { upstream_inference_cost : 0 } ,
179189 } ,
180190 }
181191 }
@@ -211,8 +221,8 @@ export async function handleOpenRouterNonStream({
211221 logger . error ( { error } , 'Failed to insert message into BigQuery' )
212222 } )
213223
214- // Consume credits
215- await consumeCreditsForMessage ( {
224+ // Consume credits and get the actual billed amount
225+ const billedCredits = await consumeCreditsForMessage ( {
216226 messageId : data . id ,
217227 userId,
218228 stripeCustomerId,
@@ -228,6 +238,12 @@ export async function handleOpenRouterNonStream({
228238 logger,
229239 } )
230240
241+ // Overwrite cost so SDK calculates exact credits we charged
242+ if ( data . usage ) {
243+ data . usage . cost = creditsToFakeCost ( billedCredits )
244+ data . usage . cost_details = { upstream_inference_cost : 0 }
245+ }
246+
231247 return data
232248}
233249
@@ -323,7 +339,7 @@ export async function handleOpenRouterStream({
323339 const line = buffer . slice ( 0 , lineEnd + 1 )
324340 buffer = buffer . slice ( lineEnd + 1 )
325341
326- state = await handleLine ( {
342+ const lineResult = await handleLine ( {
327343 userId,
328344 stripeCustomerId,
329345 agentId,
@@ -337,10 +353,15 @@ export async function handleOpenRouterStream({
337353 logger,
338354 insertMessage : insertMessageBigquery ,
339355 } )
356+ state = lineResult . state
340357
341358 if ( ! clientDisconnected ) {
342359 try {
343- controller . enqueue ( new TextEncoder ( ) . encode ( line ) )
360+ // Overwrite cost in final chunk so SDK calculates exact credits we charged
361+ const lineToSend = lineResult . billedCredits !== undefined
362+ ? overwriteCostWithBilledCredits ( line , lineResult . billedCredits )
363+ : line
364+ controller . enqueue ( new TextEncoder ( ) . encode ( lineToSend ) )
344365 } catch ( error ) {
345366 logger . warn (
346367 'Client disconnected during stream, continuing for billing' ,
@@ -413,14 +434,14 @@ async function handleLine({
413434 state : StreamState
414435 logger : Logger
415436 insertMessage : InsertMessageBigqueryFn
416- } ) : Promise < StreamState > {
437+ } ) : Promise < LineResult > {
417438 if ( ! line . startsWith ( 'data: ' ) ) {
418- return state
439+ return { state }
419440 }
420441
421442 const raw = line . slice ( 'data: ' . length )
422443 if ( raw === '[DONE]\n' ) {
423- return state
444+ return { state }
424445 }
425446
426447 // Parse the string into an object
@@ -432,7 +453,7 @@ async function handleLine({
432453 { error : getErrorObject ( error , { includeRawError : true } ) } ,
433454 'Received non-JSON OpenRouter response' ,
434455 )
435- return state
456+ return { state }
436457 }
437458
438459 // Extract usage
@@ -442,10 +463,10 @@ async function handleLine({
442463 { error : getErrorObject ( parsed . error , { includeRawError : true } ) } ,
443464 'Unable to parse OpenRouter response' ,
444465 )
445- return state
466+ return { state }
446467 }
447468
448- return await handleResponse ( {
469+ return handleResponse ( {
449470 userId,
450471 stripeCustomerId,
451472 agentId,
@@ -487,7 +508,7 @@ async function handleResponse({
487508 state : StreamState
488509 logger : Logger
489510 insertMessage : InsertMessageBigqueryFn
490- } ) : Promise < StreamState > {
511+ } ) : Promise < LineResult > {
491512 const model = 'model' in data ? data . model : undefined
492513 state = await handleStreamChunk ( {
493514 data,
@@ -500,7 +521,7 @@ async function handleResponse({
500521
501522 if ( 'error' in data || ! data . usage ) {
502523 // Stream not finished
503- return state
524+ return { state }
504525 }
505526
506527 const usageData = extractUsageAndCost ( data . usage )
@@ -520,7 +541,8 @@ async function handleResponse({
520541 logger . error ( { error } , 'Failed to insert message into BigQuery' )
521542 } )
522543
523- await consumeCreditsForMessage ( {
544+ // Consume credits and get the actual billed amount
545+ const billedCredits = await consumeCreditsForMessage ( {
524546 messageId : data . id ,
525547 userId,
526548 stripeCustomerId,
@@ -536,7 +558,7 @@ async function handleResponse({
536558 logger,
537559 } )
538560
539- return state
561+ return { state, billedCredits }
540562}
541563
542564async function handleStreamChunk ( {
@@ -727,3 +749,41 @@ async function parseOpenRouterError(
727749 }
728750 return new OpenRouterError ( response . status , response . statusText , errorBody )
729751}
752+
753+ /**
754+ * Convert credits (integer cents) back to a cost value that will result in the same
755+ * credits when the SDK applies its formula: credits = Math.round(cost * (1 + PROFIT_MARGIN) * 100)
756+ */
757+ function creditsToFakeCost ( credits : number ) : number {
758+ return credits / ( ( 1 + PROFIT_MARGIN ) * 100 )
759+ }
760+
761+ /**
762+ * Overwrite the cost field in the final SSE chunk to reflect actual billed credits.
763+ * This ensures the SDK calculates the exact credits value we stored in the database,
764+ * making the server the single source of truth for credit tracking.
765+ */
766+ function overwriteCostWithBilledCredits ( line : string , billedCredits : number ) : string {
767+ if ( ! line . startsWith ( 'data: ' ) ) {
768+ return line
769+ }
770+
771+ const raw = line . slice ( 'data: ' . length )
772+ if ( raw === '[DONE]\n' || raw === '[DONE]' ) {
773+ return line
774+ }
775+
776+ try {
777+ const obj = JSON . parse ( raw )
778+ // Only modify if there's usage data (final chunk)
779+ if ( obj . usage ) {
780+ obj . usage . cost = creditsToFakeCost ( billedCredits )
781+ obj . usage . cost_details = { upstream_inference_cost : 0 }
782+ return `data: ${ JSON . stringify ( obj ) } \n`
783+ }
784+ } catch {
785+ // If parsing fails, return original line
786+ }
787+
788+ return line
789+ }
0 commit comments