@@ -26,6 +26,12 @@ import {
2626 normalizeClientMetricObservations ,
2727 type ClientAttemptTelemetry
2828} from './benchmark-client-metrics.js' ;
29+ import {
30+ classifyStreamSemanticEvent ,
31+ createStreamTimingTracker ,
32+ type StreamTimingTelemetry ,
33+ type StreamTimingTracker
34+ } from './benchmark-stream-timing.js' ;
2935
3036const ENGINE_VERSION = 'benchmark-runner-v1' ;
3137const DEFAULT_TIMEOUT_MS = 30_000 ;
@@ -856,17 +862,14 @@ export function parseOpenAiSseStream(raw: string): StreamNormalization {
856862 events
857863 ) ;
858864 }
865+ if ( jsonRecord ) {
866+ content . push ( ...classifyStreamSemanticEvent ( 'openai_chat' , jsonRecord , event . event ) . text_fragments ) ;
867+ }
859868 const choices = Array . isArray ( jsonRecord ?. choices ) ? jsonRecord . choices : [ ] ;
860869 for ( const choice of choices ) {
861870 const choiceRecord = objectValue ( choice ) ;
862871 const delta = objectValue ( choiceRecord ?. delta ) ;
863872 const message = objectValue ( choiceRecord ?. message ) ;
864- const text = typeof delta ?. content === 'string'
865- ? delta . content
866- : typeof message ?. content === 'string' ? message . content : null ;
867- if ( text !== null ) {
868- content . push ( text ) ;
869- }
870873 const streamedCalls = Array . isArray ( delta ?. tool_calls )
871874 ? delta . tool_calls
872875 : Array . isArray ( message ?. tool_calls ) ? message . tool_calls : [ ] ;
@@ -933,13 +936,10 @@ export function parseOllamaJsonlStream(raw: string): StreamNormalization {
933936 events
934937 ) ;
935938 }
936- const message = objectValue ( record ?. message ) ;
937- const part = typeof message ?. content === 'string'
938- ? message . content
939- : typeof record ?. response === 'string' ? record . response : null ;
940- if ( part !== null ) {
941- content . push ( part ) ;
939+ if ( record ) {
940+ content . push ( ...classifyStreamSemanticEvent ( 'ollama_chat' , record ) . text_fragments ) ;
942941 }
942+ const message = objectValue ( record ?. message ) ;
943943 const calls = Array . isArray ( message ?. tool_calls ) ? message . tool_calls : [ ] ;
944944 for ( const call of calls ) {
945945 const callRecord = objectValue ( call ) ;
@@ -1006,6 +1006,9 @@ export function parseAnthropicSseStream(raw: string): StreamNormalization {
10061006 events
10071007 ) ;
10081008 }
1009+ if ( record ) {
1010+ content . push ( ...classifyStreamSemanticEvent ( 'anthropic_messages' , record , event . event ) . text_fragments ) ;
1011+ }
10091012 if ( eventType === 'message_start' ) {
10101013 const message = objectValue ( record ?. message ) ;
10111014 if ( message ) finalMetadata = message ;
@@ -1021,15 +1024,12 @@ export function parseAnthropicSseStream(raw: string): StreamNormalization {
10211024 initialInput : block . input ,
10221025 partialJson : ''
10231026 } ) ;
1024- if ( type === 'text' && typeof block . text === 'string' ) content . push ( block . text ) ;
10251027 }
10261028 } else if ( eventType === 'content_block_delta' ) {
10271029 const blockIndex = numberAt ( record , 'index' ) ;
10281030 const delta = objectValue ( record ?. delta ) ;
10291031 const block = blockIndex === null ? undefined : blocks . get ( blockIndex ) ;
1030- if ( delta ?. type === 'text_delta' && typeof delta . text === 'string' ) {
1031- content . push ( delta . text ) ;
1032- } else if ( block && delta ?. type === 'input_json_delta' && typeof delta . partial_json === 'string' ) {
1032+ if ( block && delta ?. type === 'input_json_delta' && typeof delta . partial_json === 'string' ) {
10331033 block . partialJson += delta . partial_json ;
10341034 }
10351035 } else if ( eventType === 'content_block_stop' ) {
@@ -1103,14 +1103,16 @@ export function parseGeminiSseStream(raw: string): StreamNormalization {
11031103 events
11041104 ) ;
11051105 }
1106+ if ( record ) {
1107+ content . push ( ...classifyStreamSemanticEvent ( 'gemini_generate_content' , record , event . event ) . text_fragments ) ;
1108+ }
11061109 const candidates = Array . isArray ( record ?. candidates ) ? record . candidates : [ ] ;
11071110 for ( const candidate of candidates ) {
11081111 const candidateRecord = objectValue ( candidate ) ;
11091112 const candidateContent = objectValue ( candidateRecord ?. content ) ;
11101113 const parts = Array . isArray ( candidateContent ?. parts ) ? candidateContent . parts : [ ] ;
11111114 for ( const part of parts ) {
11121115 const partRecord = objectValue ( part ) ;
1113- if ( typeof partRecord ?. text === 'string' ) content . push ( partRecord . text ) ;
11141116 const functionCall = objectValue ( partRecord ?. functionCall ) ;
11151117 const name = textFromValue ( functionCall ?. name ) ;
11161118 if ( functionCall && ! name ) {
@@ -1307,6 +1309,17 @@ function roundMilliseconds(value: number): number {
13071309 return Math . round ( value * 1000 ) / 1000 ;
13081310}
13091311
1312+ function emptyStreamTimingTelemetry ( ) : StreamTimingTelemetry {
1313+ return {
1314+ first_output_at_ms : null ,
1315+ first_tool_call_at_ms : null ,
1316+ tool_calls_ready_at_ms : null ,
1317+ last_output_at_ms : null ,
1318+ tool_call_started : false ,
1319+ tool_call_error : null
1320+ } ;
1321+ }
1322+
13101323async function executeItem (
13111324 instantiation : Record < string , unknown > ,
13121325 stage : BenchmarkStage ,
@@ -1353,6 +1366,7 @@ async function executeItem(
13531366 for ( let attempt = 1 ; attempt <= maxAttempts ; attempt += 1 ) {
13541367 const attemptStartedAtMs = performance . now ( ) ;
13551368 let firstChunkAtMs : number | null = null ;
1369+ let streamTimingTracker : StreamTimingTracker | null = null ;
13561370 const controller = new AbortController ( ) ;
13571371 const timer = setTimeout ( ( ) => controller . abort ( ) , timeoutMs ) ;
13581372 try {
@@ -1364,6 +1378,9 @@ async function executeItem(
13641378 } ) ;
13651379 let responseText = '' ;
13661380 const effectiveStreaming = streaming && response . ok ;
1381+ if ( effectiveStreaming && isProviderProtocol ( operationSpec ?. protocol ) ) {
1382+ streamTimingTracker = createStreamTimingTracker ( operationSpec . protocol , attemptStartedAtMs ) ;
1383+ }
13671384 firstTokenMs = null ;
13681385 if ( effectiveStreaming && response . body ) {
13691386 const reader = response . body . getReader ( ) ;
@@ -1379,9 +1396,15 @@ async function executeItem(
13791396 firstChunkAtMs = performance . now ( ) ;
13801397 firstTokenMs = roundMilliseconds ( firstChunkAtMs - operationStartedAtMs ) ;
13811398 }
1382- responseText += decoder . decode ( value , { stream : true } ) ;
1399+ const receivedAtMs = performance . now ( ) ;
1400+ const decodedChunk = decoder . decode ( value , { stream : true } ) ;
1401+ responseText += decodedChunk ;
1402+ streamTimingTracker ?. push ( decodedChunk , receivedAtMs ) ;
13831403 }
1384- responseText += decoder . decode ( ) ;
1404+ const finalDecodedChunk = decoder . decode ( ) ;
1405+ responseText += finalDecodedChunk ;
1406+ streamTimingTracker ?. push ( finalDecodedChunk , performance . now ( ) ) ;
1407+ streamTimingTracker ?. finish ( performance . now ( ) ) ;
13851408 } else {
13861409 responseText = await response . text ( ) ;
13871410 }
@@ -1422,10 +1445,12 @@ async function executeItem(
14221445 stream_format : error . format
14231446 } ;
14241447 attemptErrors . push ( issue ) ;
1448+ const streamTiming = streamTimingTracker ?. snapshot ( ) ?? emptyStreamTimingTelemetry ( ) ;
14251449 clientAttempts . push ( {
14261450 started_at_ms : attemptStartedAtMs ,
14271451 ended_at_ms : attemptEndedAtMs ,
14281452 first_chunk_at_ms : firstChunkAtMs ,
1453+ ...streamTiming ,
14291454 request_succeeded : response . ok ,
14301455 timed_out : false ,
14311456 response_normalization_succeeded : false ,
@@ -1505,10 +1530,12 @@ async function executeItem(
15051530 } ;
15061531 }
15071532 const errorCode = response . ok ? null : `http_${ response . status } ` ;
1533+ const streamTiming = streamTimingTracker ?. snapshot ( ) ?? emptyStreamTimingTelemetry ( ) ;
15081534 clientAttempts . push ( {
15091535 started_at_ms : attemptStartedAtMs ,
15101536 ended_at_ms : attemptEndedAtMs ,
15111537 first_chunk_at_ms : firstChunkAtMs ,
1538+ ...streamTiming ,
15121539 request_succeeded : response . ok ,
15131540 timed_out : false ,
15141541 response_normalization_succeeded : response . ok ,
@@ -1626,6 +1653,7 @@ async function executeItem(
16261653 } catch ( error ) {
16271654 const err = error as Error ;
16281655 const attemptEndedAtMs = performance . now ( ) ;
1656+ const streamTiming = streamTimingTracker ?. snapshot ( ) ?? emptyStreamTimingTelemetry ( ) ;
16291657 const issue = {
16301658 code : err . name === 'AbortError' ? 'timeout' : 'connection_error' ,
16311659 message : err . name === 'AbortError' ? `Benchmark request timed out after ${ timeoutMs } ms` : err . message ,
@@ -1641,6 +1669,7 @@ async function executeItem(
16411669 started_at_ms : attemptStartedAtMs ,
16421670 ended_at_ms : attemptEndedAtMs ,
16431671 first_chunk_at_ms : firstChunkAtMs ,
1672+ ...streamTiming ,
16441673 request_succeeded : false ,
16451674 timed_out : err . name === 'AbortError' ,
16461675 response_normalization_succeeded : null ,
0 commit comments