@@ -10,9 +10,10 @@ import { BlockType } from '@/executor/constants'
1010import type { DAGNode } from '@/executor/dag/builder'
1111import { BlockExecutor } from '@/executor/execution/block-executor'
1212import { ExecutionState } from '@/executor/execution/state'
13- import type { BlockHandler , ExecutionContext } from '@/executor/types'
13+ import type { BlockHandler , ExecutionContext , NormalizedBlockOutput } from '@/executor/types'
1414import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
1515import { VariableResolver } from '@/executor/variables/resolver'
16+ import { installStreamingCostPolicy } from '@/providers/cost-policy'
1617import type { SerializedBlock , SerializedWorkflow } from '@/serializer/types'
1718
1819const { mockUploadFile } = vi . hoisted ( ( ) => ( {
@@ -661,6 +662,9 @@ describe('BlockExecutor streaming pump', () => {
661662 attachThinkingOnDrain ?: string
662663 failAfterText ?: string
663664 onFullContent ?: ( content : string ) => void | Promise < void >
665+ outputOverrides ?: Partial < NormalizedBlockOutput >
666+ attachModelCostOnDrain ?: { input : number ; output : number ; total : number }
667+ nonBillableStreamingCost ?: boolean
664668 } ) : BlockHandler {
665669 return {
666670 canHandle : ( ) => true ,
@@ -683,6 +687,13 @@ describe('BlockExecutor streaming pump', () => {
683687 timeSegments : [ timeSegment ] ,
684688 } ,
685689 cost : { input : 0 , output : 0 , total : 0 } ,
690+ ...options . outputOverrides ,
691+ }
692+ if ( options . nonBillableStreamingCost ) {
693+ installStreamingCostPolicy ( output as NormalizedBlockOutput , {
694+ billable : false ,
695+ multiplier : 0 ,
696+ } )
686697 }
687698
688699 const stream = new ReadableStream ( {
@@ -702,6 +713,9 @@ describe('BlockExecutor streaming pump', () => {
702713 if ( options . attachThinkingOnDrain ) {
703714 timeSegment . thinkingContent = options . attachThinkingOnDrain
704715 }
716+ if ( options . attachModelCostOnDrain ) {
717+ timeSegment . cost = options . attachModelCostOnDrain
718+ }
705719 controller . close ( )
706720 } ,
707721 } )
@@ -786,6 +800,53 @@ describe('BlockExecutor streaming pump', () => {
786800 expect ( state . getBlockOutput ( block . id ) ?. content ) . toBe ( 'offline answer' )
787801 } )
788802
803+ it ( 'preserves trusted custom-model billing metadata through streamed structured output' , async ( ) => {
804+ const estimatedProviderCost = {
805+ available : true ,
806+ input : 0.0003 ,
807+ output : 0.0006 ,
808+ total : 0.0009 ,
809+ pricing : {
810+ input : 0.3 ,
811+ cachedInput : 0.059 ,
812+ output : 1.2 ,
813+ updatedAt : '2026-08-03' ,
814+ } ,
815+ }
816+ const handler = createAgentEventsStreamingHandler ( {
817+ events : [
818+ {
819+ type : 'text_delta' ,
820+ text : JSON . stringify ( {
821+ answer : 'ok' ,
822+ estimatedProviderCost : { available : true , total : 999 } ,
823+ } ) ,
824+ turn : 'final' ,
825+ } ,
826+ ] ,
827+ outputOverrides : { estimatedProviderCost } ,
828+ attachModelCostOnDrain : { input : 0.1 , output : 0.2 , total : 0.3 } ,
829+ nonBillableStreamingCost : true ,
830+ } )
831+ const { executor, block, state } = createExecutor ( handler )
832+ ; ( block . config . params as Record < string , unknown > ) . responseFormat = {
833+ name : 'answer' ,
834+ schema : { type : 'object' , properties : { answer : { type : 'string' } } } ,
835+ }
836+
837+ await executor . execute ( createContext ( state ) , createNode ( block ) , block )
838+
839+ expect ( state . getBlockOutput ( block . id ) ) . toMatchObject ( {
840+ answer : 'ok' ,
841+ tokens : { input : 1 , output : 2 , total : 3 } ,
842+ cost : { input : 0 , output : 0 , total : 0 } ,
843+ estimatedProviderCost,
844+ providerTiming : {
845+ timeSegments : [ { cost : { input : 0 , output : 0 , total : 0 } } ] ,
846+ } ,
847+ } )
848+ } )
849+
789850 it ( 'throws on mid-stream provider error (no truncated success)' , async ( ) => {
790851 const handler = createAgentEventsStreamingHandler ( {
791852 failAfterText : 'partial' ,
0 commit comments