@@ -357,21 +357,23 @@ export class ChatCompletionDriver extends PuterDriver {
357357 const init = res . init_chat_stream ;
358358 const cleanup = res . finally_fn ;
359359
360- // Intercept `chatStream.end(usage)` so we can fire the same
361- // complete + cost-calculated events the non-streaming branch
362- // emits. Providers always terminate streams via `.end(usage)`;
363- // if they skip it, we just lose the cost event (no worse than
364- // not emitting).
360+ // Intercept `chatStream.end(usage)` to fire complete + cost events
361+ // (mirrors the non-streaming branch). Clone usage so providers that
362+ // meter after this call (e.g. Claude) don't pick up `usd_cents`.
365363 const originalEnd = chatStream . end . bind ( chatStream ) ;
366364 chatStream . end = ( usage ?: Record < string , number > ) => {
365+ const enrichedUsage = usage ? { ...usage } : usage ;
366+ if ( enrichedUsage ) {
367+ this . #injectUsdCents( enrichedUsage , model ) ;
368+ }
367369 this . clients . event . emit (
368370 'ai.prompt.complete' ,
369371 {
370372 username,
371373 completionId,
372374 intended_service : intendedProvider ,
373375 parameters : args ,
374- result : { usage, stream : true } ,
376+ result : { usage : enrichedUsage , stream : true } ,
375377 model_used : model . id ,
376378 service_used : model . provider ,
377379 } ,
@@ -386,7 +388,7 @@ export class ChatCompletionDriver extends PuterDriver {
386388 intendedProvider,
387389 } ) ;
388390 }
389- return originalEnd ( usage ! ) ;
391+ return originalEnd ( enrichedUsage ! ) ;
390392 } ;
391393
392394 // Fire-and-forget — the stream writes happen async while the
@@ -435,6 +437,7 @@ export class ChatCompletionDriver extends PuterDriver {
435437 ) ;
436438
437439 if ( 'usage' in res && res . usage ) {
440+ this . #injectUsdCents( res . usage , model ) ;
438441 this . #emitCostCalculated( {
439442 completionId,
440443 username,
@@ -461,6 +464,116 @@ export class ChatCompletionDriver extends PuterDriver {
461464 return { ...res , via_ai_chat_service : true } ;
462465 }
463466
467+ // Compute per-token cost in microcents (1 cent = 1_000_000 microCents).
468+ // Shape-agnostic: multiplies every usage key by its matching rate in
469+ // `model.costs`. Returns `null` when cost data is unavailable.
470+ #computeCost(
471+ usage : Record < string , number > ,
472+ model : IChatModel ,
473+ ) : {
474+ inputKey : string ;
475+ outputKey : string ;
476+ inputTokens : number ;
477+ outputTokens : number ;
478+ inputMicroCents : number ;
479+ outputMicroCents : number ;
480+ totalMicroCents : number ;
481+ } | null {
482+ const inputKey =
483+ ( model . input_cost_key as string | undefined ) ?? 'input_tokens' ;
484+ const outputKey =
485+ ( model . output_cost_key as string | undefined ) ?? 'output_tokens' ;
486+
487+ const costs = model . costs ;
488+ if ( ! costs ) return null ;
489+
490+ const outputRateRaw = costs [ outputKey ] ;
491+ const outputRate =
492+ typeof outputRateRaw === 'number' && Number . isFinite ( outputRateRaw )
493+ ? outputRateRaw
494+ : undefined ;
495+
496+ const isOutputKey = ( key : string ) =>
497+ key === outputKey ||
498+ key === 'output_tokens' ||
499+ key === 'completion_tokens' ||
500+ key === 'thinking_tokens' ;
501+
502+ let inputMicroCents = 0 ;
503+ let outputMicroCents = 0 ;
504+ let sawAnyRate = false ;
505+
506+ for ( const [ key , rawAmount ] of Object . entries ( usage ) ) {
507+ if ( typeof rawAmount !== 'number' || ! Number . isFinite ( rawAmount ) ) {
508+ continue ;
509+ }
510+
511+ if ( key === 'usd_cents' ) continue ;
512+ if ( key === 'tokens' ) continue ;
513+
514+ // thinking_tokens → output rate fallback
515+ let rate = costs [ key ] ;
516+ if ( typeof rate !== 'number' || ! Number . isFinite ( rate ) ) {
517+ if ( key === 'thinking_tokens' && outputRate !== undefined ) {
518+ rate = outputRate ;
519+ } else {
520+ continue ;
521+ }
522+ }
523+
524+ const sub = rawAmount * rate ;
525+ sawAnyRate = true ;
526+ if ( isOutputKey ( key ) ) {
527+ outputMicroCents += sub ;
528+ } else {
529+ inputMicroCents += sub ;
530+ }
531+ }
532+
533+ if ( ! sawAnyRate ) return null ;
534+
535+ inputMicroCents = Math . max ( 0 , Math . round ( inputMicroCents ) ) ;
536+ outputMicroCents = Math . max ( 0 , Math . round ( outputMicroCents ) ) ;
537+
538+ const inputTokens = Number (
539+ usage [ inputKey ] ?? usage . prompt_tokens ?? usage . input_tokens ?? 0 ,
540+ ) ;
541+ const outputTokens = Number (
542+ usage [ outputKey ] ??
543+ usage . completion_tokens ??
544+ usage . output_tokens ??
545+ 0 ,
546+ ) ;
547+
548+ return {
549+ inputKey,
550+ outputKey,
551+ inputTokens,
552+ outputTokens,
553+ inputMicroCents,
554+ outputMicroCents,
555+ totalMicroCents : inputMicroCents + outputMicroCents ,
556+ } ;
557+ }
558+
559+ // Add `usd_cents` to the usage object. Skips if the provider already
560+ // set an authoritative value (e.g. OpenRouter's `usage.cost`).
561+ // Sets `null` when cost data is unavailable for the model.
562+ #injectUsdCents( usage : Record < string , number > , model : IChatModel ) : void {
563+ if (
564+ typeof usage . usd_cents === 'number' &&
565+ Number . isFinite ( usage . usd_cents )
566+ ) {
567+ return ;
568+ }
569+ const cost = this . #computeCost( usage , model ) ;
570+ if ( ! cost ) {
571+ ( usage as Record < string , number | null > ) . usd_cents = null ;
572+ return ;
573+ }
574+ usage . usd_cents = cost . totalMicroCents / 1_000_000 ;
575+ }
576+
464577 // Compute per-token cost in microcents using the model's cost map,
465578 // then emit `ai.prompt.cost-calculated` for listeners that persist
466579 // billing/abuse rows keyed on the completion id.
@@ -474,23 +587,15 @@ export class ChatCompletionDriver extends PuterDriver {
474587 const { completionId, username, usage, model, intendedProvider } =
475588 params ;
476589
590+ const cost = this . #computeCost( usage , model ) ;
477591 const inputKey =
478592 ( model . input_cost_key as string | undefined ) ?? 'input_tokens' ;
479593 const outputKey =
480594 ( model . output_cost_key as string | undefined ) ?? 'output_tokens' ;
481- const inputCostPer = Number ( model . costs ?. [ inputKey ] ?? 0 ) ;
482- const outputCostPer = Number ( model . costs ?. [ outputKey ] ?? 0 ) ;
483- const inputTokens = Number (
484- usage [ inputKey ] ?? usage . prompt_tokens ?? usage . input_tokens ?? 0 ,
485- ) ;
486- const outputTokens = Number (
487- usage [ outputKey ] ??
488- usage . completion_tokens ??
489- usage . output_tokens ??
490- 0 ,
491- ) ;
492- const inputUcents = Math . round ( inputTokens * inputCostPer ) ;
493- const outputUcents = Math . round ( outputTokens * outputCostPer ) ;
595+ const inputTokens = cost ?. inputTokens ?? 0 ;
596+ const outputTokens = cost ?. outputTokens ?? 0 ;
597+ const inputMicroCents = cost ?. inputMicroCents ?? 0 ;
598+ const outputMicroCents = cost ?. outputMicroCents ?? 0 ;
494599
495600 this . clients . event . emit (
496601 'ai.prompt.cost-calculated' ,
@@ -500,9 +605,9 @@ export class ChatCompletionDriver extends PuterDriver {
500605 usage,
501606 input_tokens : inputTokens ,
502607 output_tokens : outputTokens ,
503- input_ucents : inputUcents ,
504- output_ucents : outputUcents ,
505- total_ucents : inputUcents + outputUcents ,
608+ input_ucents : inputMicroCents ,
609+ output_ucents : outputMicroCents ,
610+ total_ucents : inputMicroCents + outputMicroCents ,
506611 costs_currency : model . costs_currency ,
507612 model_used : model . id ,
508613 service_used : model . provider ,
0 commit comments