@@ -86,6 +86,8 @@ function extractTextContent(content: string | ResponsesContentPart[] | undefined
8686
8787export function responsesInputToMessages ( req : ResponsesRequest ) : ChatMessage [ ] {
8888 const messages : ChatMessage [ ] = [ ] ;
89+ // Track item_reference placeholders so we can upgrade or clean them up
90+ const itemReferencePlaceholders = new WeakSet < ChatMessage > ( ) ;
8991
9092 // instructions field → system message
9193 if ( req . instructions ) {
@@ -120,15 +122,85 @@ export function responsesInputToMessages(req: ResponsesRequest): ChatMessage[] {
120122 ] ,
121123 } ) ;
122124 } else if ( item . type === "function_call_output" ) {
125+ // Bug 1 fix: If there's no preceding assistant message with a matching
126+ // tool_call for this call_id, synthesize one. This happens when the AI SDK
127+ // sends [user, item_reference, function_call_output] — the item_reference
128+ // placeholder (see below) has no tool_calls, so we need a real assistant
129+ // message with the tool_call for turnIndex counting.
130+ const hasMatchingToolCall = messages . some (
131+ ( m ) => m . role === "assistant" && m . tool_calls ?. some ( ( tc ) => tc . id === item . call_id ) ,
132+ ) ;
133+ if ( ! hasMatchingToolCall ) {
134+ // Check if the last message is an item_reference placeholder — if so,
135+ // upgrade it to carry the tool_call instead of synthesizing a duplicate.
136+ const lastMsg = messages [ messages . length - 1 ] ;
137+ if (
138+ lastMsg &&
139+ lastMsg . role === "assistant" &&
140+ itemReferencePlaceholders . has ( lastMsg ) &&
141+ ! lastMsg . tool_calls
142+ ) {
143+ lastMsg . content = null ;
144+ lastMsg . tool_calls = [
145+ {
146+ id : item . call_id ?? generateToolCallId ( ) ,
147+ type : "function" ,
148+ function : { name : "" , arguments : "" } ,
149+ } ,
150+ ] ;
151+ itemReferencePlaceholders . delete ( lastMsg ) ;
152+ } else {
153+ // Multi-fco case: look for a recent assistant with tool_calls that
154+ // belongs to the same turn. After the first fco upgrades a placeholder,
155+ // subsequent fco's see [assistant(call_A), tool(call_A)] — the last
156+ // assistant with tool_calls (right before the trailing tool messages)
157+ // is the correct target.
158+ let appended = false ;
159+ for ( let k = messages . length - 1 ; k >= 0 ; k -- ) {
160+ const m = messages [ k ] ;
161+ if ( m . role === "assistant" && m . tool_calls ) {
162+ m . tool_calls . push ( {
163+ id : item . call_id ?? generateToolCallId ( ) ,
164+ type : "function" ,
165+ function : { name : "" , arguments : "" } ,
166+ } ) ;
167+ appended = true ;
168+ break ;
169+ }
170+ // Stop scanning if we hit a user message — different turn
171+ if ( m . role === "user" ) break ;
172+ }
173+ if ( ! appended ) {
174+ messages . push ( {
175+ role : "assistant" ,
176+ content : null ,
177+ tool_calls : [
178+ {
179+ id : item . call_id ?? generateToolCallId ( ) ,
180+ type : "function" ,
181+ function : { name : "" , arguments : "" } ,
182+ } ,
183+ ] ,
184+ } ) ;
185+ }
186+ }
187+ }
123188 messages . push ( {
124189 role : "tool" ,
125190 content : item . output ?? "" ,
126191 tool_call_id : item . call_id ,
127192 } ) ;
193+ } else if ( item . type === "item_reference" ) {
194+ // Bug 6 fix: item_reference items represent prior assistant turns (text
195+ // or function_call). Push a placeholder so they count in assistantCount.
196+ // If a subsequent function_call_output arrives, the handler above will
197+ // upgrade this placeholder to carry tool_calls (avoiding double-count).
198+ const placeholder : ChatMessage = { role : "assistant" , content : "" } ;
199+ itemReferencePlaceholders . add ( placeholder ) ;
200+ messages . push ( placeholder ) ;
128201 } else {
129- // Skip item_reference, local_shell_call, mcp_list_tools, etc. — not needed
130- // for fixture matching. Logging is not threaded into this pure conversion
131- // function; callers can inspect the returned messages if needed.
202+ // Skip local_shell_call, mcp_list_tools, etc. — not needed for fixture
203+ // matching.
132204 }
133205 }
134206
@@ -370,6 +442,7 @@ function buildReasoningStreamEvents(
370442
371443 events . push ( {
372444 type : "response.reasoning_summary_part.added" ,
445+ item_id : reasoningId ,
373446 output_index : 0 ,
374447 summary_index : 0 ,
375448 part : { type : "summary_text" , text : "" } ,
@@ -388,13 +461,15 @@ function buildReasoningStreamEvents(
388461
389462 events . push ( {
390463 type : "response.reasoning_summary_text.done" ,
464+ item_id : reasoningId ,
391465 output_index : 0 ,
392466 summary_index : 0 ,
393467 text : reasoning ,
394468 } ) ;
395469
396470 events . push ( {
397471 type : "response.reasoning_summary_part.done" ,
472+ item_id : reasoningId ,
398473 output_index : 0 ,
399474 summary_index : 0 ,
400475 part : { type : "summary_text" , text : reasoning } ,
@@ -430,7 +505,7 @@ function buildWebSearchStreamEvents(
430505 type : "web_search_call" ,
431506 id : searchId ,
432507 status : "in_progress" ,
433- action : { query : queries [ i ] } ,
508+ action : { type : "search" , query : queries [ i ] } ,
434509 } ,
435510 } ) ;
436511
@@ -441,7 +516,7 @@ function buildWebSearchStreamEvents(
441516 type : "web_search_call" ,
442517 id : searchId ,
443518 status : "completed" ,
444- action : { query : queries [ i ] } ,
519+ action : { type : "search" , query : queries [ i ] } ,
445520 } ,
446521 } ) ;
447522 }
@@ -545,7 +620,7 @@ function buildMessageOutputEvents(
545620 type : "response.content_part.added" ,
546621 output_index : outputIndex ,
547622 content_index : 0 ,
548- part : { type : "output_text" , text : "" } ,
623+ part : { type : "output_text" , text : "" , annotations : [ ] } ,
549624 } ) ;
550625
551626 for ( let i = 0 ; i < content . length ; i += chunkSize ) {
@@ -568,15 +643,15 @@ function buildMessageOutputEvents(
568643 type : "response.content_part.done" ,
569644 output_index : outputIndex ,
570645 content_index : 0 ,
571- part : { type : "output_text" , text : content } ,
646+ part : { type : "output_text" , text : content , annotations : [ ] } ,
572647 } ) ;
573648
574649 const msgItem = {
575650 type : "message" ,
576651 id : msgId ,
577652 status : "completed" ,
578653 role : "assistant" ,
579- content : [ { type : "output_text" , text : content } ] ,
654+ content : [ { type : "output_text" , text : content , annotations : [ ] } ] ,
580655 } ;
581656
582657 events . push ( { type : "response.output_item.done" , output_index : outputIndex , item : msgItem } ) ;
@@ -603,7 +678,7 @@ function buildOutputPrefix(content: string, reasoning?: string, webSearches?: st
603678 type : "web_search_call" ,
604679 id : generateId ( "ws" ) ,
605680 status : "completed" ,
606- action : { query } ,
681+ action : { type : "search" , query } ,
607682 } ) ;
608683 }
609684 }
@@ -613,7 +688,7 @@ function buildOutputPrefix(content: string, reasoning?: string, webSearches?: st
613688 id : itemId ( ) ,
614689 status : "completed" ,
615690 role : "assistant" ,
616- content : [ { type : "output_text" , text : content } ] ,
691+ content : [ { type : "output_text" , text : content , annotations : [ ] } ] ,
617692 } ) ;
618693
619694 return output ;
@@ -869,7 +944,14 @@ export async function handleResponses(
869944 ) ;
870945
871946 if ( fixture ) {
947+ defaults . logger . debug (
948+ `Responses fixture matched for ${ req . method ?? "POST" } ${ req . url ?? "/v1/responses" } ` ,
949+ ) ;
872950 journal . incrementFixtureMatchCount ( fixture , fixtures , testId ) ;
951+ } else {
952+ defaults . logger . debug (
953+ `No responses fixture matched for ${ req . method ?? "POST" } ${ req . url ?? "/v1/responses" } ` ,
954+ ) ;
873955 }
874956
875957 if (
0 commit comments