@@ -7,6 +7,7 @@ import { contentPartsToText } from "./image";
77function messagesToChatFormat ( parsed : OcxParsedRequest ) : unknown [ ] {
88 const out : unknown [ ] = [ ] ;
99 const { context, options } = parsed ;
10+ let pendingToolCallIds = new Set < string > ( ) ;
1011
1112 if ( context . systemPrompt && context . systemPrompt . length > 0 ) {
1213 // Codex sends its GPT-5 identity prompt for EVERY model (the per-model catalog
@@ -39,6 +40,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
3940 out . push ( { role : "user" , content : chatParts } ) ;
4041 }
4142 }
43+ pendingToolCallIds = new Set ( ) ;
4244 break ;
4345 }
4446 case "assistant" : {
@@ -61,14 +63,33 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
6163 // like DeepSeek reject an assistant message with neither content nor tool_calls.
6264 if ( chatMsg . content === undefined && chatMsg . tool_calls === undefined ) break ;
6365 out . push ( chatMsg ) ;
66+ pendingToolCallIds = new Set ( toolCalls . map ( tc => tc . id ) . filter ( Boolean ) ) ;
6467 break ;
6568 }
6669 case "toolResult" : {
70+ let toolCallId = msg . toolCallId ;
71+ if ( ! toolCallId ) toolCallId = `call_orphan_${ out . length } ` ;
72+ if ( ! pendingToolCallIds . has ( toolCallId ) ) {
73+ // WS turns can arrive with only tool outputs; chat-completions providers reject a bare
74+ // role:"tool" message unless an assistant tool_call with the same id immediately precedes it.
75+ const name = safeToolName ( msg . toolName ) ;
76+ out . push ( {
77+ role : "assistant" ,
78+ content : null ,
79+ tool_calls : [ {
80+ id : toolCallId ,
81+ type : "function" ,
82+ function : { name, arguments : "{}" } ,
83+ } ] ,
84+ } ) ;
85+ pendingToolCallIds = new Set ( [ toolCallId ] ) ;
86+ }
6787 out . push ( {
6888 role : "tool" ,
69- tool_call_id : msg . toolCallId ,
89+ tool_call_id : toolCallId ,
7090 content : contentPartsToText ( msg . content ) ,
7191 } ) ;
92+ pendingToolCallIds . delete ( toolCallId ) ;
7293 break ;
7394 }
7495 }
@@ -77,6 +98,12 @@ function messagesToChatFormat(parsed: OcxParsedRequest): unknown[] {
7798 return out ;
7899}
79100
101+ function safeToolName ( name : string | undefined ) : string {
102+ const raw = name && name . trim ( ) . length > 0 ? name : "tool_result" ;
103+ const sanitized = raw . replace ( / [ ^ A - Z a - z 0 - 9 _ - ] / g, "_" ) ;
104+ return sanitized . length > 0 ? sanitized : "tool_result" ;
105+ }
106+
80107function toolsToChatFormat ( parsed : OcxParsedRequest ) : unknown [ ] | undefined {
81108 if ( ! parsed . context . tools || parsed . context . tools . length === 0 ) return undefined ;
82109 return parsed . context . tools . map ( t => ( {
0 commit comments