@@ -52,7 +52,7 @@ function getAvianModelId(openrouterModel: string): string {
5252 return AVIAN_MODEL_MAP [ openrouterModel ] ?? openrouterModel
5353}
5454
55- type StreamState = { responseText : string ; reasoningText : string ; ttftMs : number | null }
55+ type StreamState = { responseText : string ; reasoningText : string ; ttftMs : number | null ; billedAlready : boolean }
5656
5757type LineResult = {
5858 state : StreamState
@@ -232,7 +232,7 @@ export async function handleAvianStream({
232232 }
233233
234234 let heartbeatInterval : NodeJS . Timeout
235- let state : StreamState = { responseText : '' , reasoningText : '' , ttftMs : null }
235+ let state : StreamState = { responseText : '' , reasoningText : '' , ttftMs : null , billedAlready : false }
236236 let clientDisconnected = false
237237
238238 const stream = new ReadableStream ( {
@@ -421,6 +421,12 @@ async function handleLine({
421421 return { state : result . state , billedCredits : result . billedCredits , patchedLine }
422422}
423423
424+ function isFinalChunk ( data : Record < string , unknown > ) : boolean {
425+ const choices = data . choices as Array < Record < string , unknown > > | undefined
426+ if ( ! choices || choices . length === 0 ) return true
427+ return choices . some ( c => c . finish_reason != null )
428+ }
429+
424430async function handleResponse ( {
425431 userId,
426432 stripeCustomerId,
@@ -454,13 +460,22 @@ async function handleResponse({
454460} ) : Promise < { state : StreamState ; billedCredits ?: number } > {
455461 state = handleStreamChunk ( { data, state, startTime, logger, userId, agentId, model : originalModel } )
456462
457- if ( 'error' in data || ! data . usage ) {
463+ // Some providers send cumulative usage on EVERY chunk (not just the final one),
464+ // so we must only bill once on the final chunk to avoid charging N times.
465+ if ( 'error' in data || ! data . usage || state . billedAlready || ! isFinalChunk ( data ) ) {
466+ // Strip usage from non-final chunks and duplicate final chunks
467+ // so the SDK doesn't see multiple usage objects
468+ if ( data . usage && ( ! isFinalChunk ( data ) || state . billedAlready ) ) {
469+ delete data . usage
470+ }
458471 return { state }
459472 }
460473
461474 const usageData = extractUsageAndCost ( data . usage as Record < string , unknown > , avianModelId )
462475 const messageId = typeof data . id === 'string' ? data . id : 'unknown'
463476
477+ state . billedAlready = true
478+
464479 insertMessageToBigQuery ( {
465480 messageId,
466481 userId,
0 commit comments