From 66e8b114b9716ac564afcbe8074e6119b3b1605e Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Wed, 18 Mar 2026 18:05:37 +0000 Subject: [PATCH] fix(openai): split embedded tool_calls into standard chunk format - Standard OpenAI clients expect tool_calls to be streamed as two parts: 1. Initialization chunk containing 'id', 'type', and 'name', with empty 'arguments'. 2. Payload chunk(s) containing 'arguments', with 'id', 'type', and 'name' omitted. - Previously, the proxy was yielding all fields in a single chunk when parsing the custom 'tool_uses' JSON from gpt-5.4, causing strict clients like opencode to fail silently when delegating parallel tasks. - The proxy now splits the extracted JSON into the correct two-chunk sequence, restoring subagent compatibility. --- src/providers/openai.rs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/providers/openai.rs b/src/providers/openai.rs index ff2de314..ecb6300f 100644 --- a/src/providers/openai.rs +++ b/src/providers/openai.rs @@ -819,15 +819,37 @@ impl super::Provider for OpenAIProvider { }; } - // Yield the tool calls - // ... (rest of tool call yielding unchanged) - let deltas: Vec = embedded_calls.into_iter().enumerate().map(|(idx, tc)| { + // Yield the tool calls in two chunks to mimic standard streaming behavior + // Chunk 1: Initialization (id, name) + let init_deltas: Vec = embedded_calls.iter().enumerate().map(|(idx, tc)| { crate::models::ToolCallDelta { index: idx as u32, - id: Some(tc.id), + id: Some(tc.id.clone()), call_type: Some("function".to_string()), function: Some(crate::models::FunctionCallDelta { - name: Some(tc.function.name), + name: Some(tc.function.name.clone()), + arguments: Some("".to_string()), + }), + } + }).collect(); + + yield ProviderStreamChunk { + content: String::new(), + reasoning_content: None, + finish_reason: None, + tool_calls: Some(init_deltas), + model: model.clone(), + usage: None, + }; + + // Chunk 2: Payload (arguments) + let arg_deltas: Vec = embedded_calls.into_iter().enumerate().map(|(idx, tc)| { + crate::models::ToolCallDelta { + index: idx as u32, + id: None, + call_type: None, + function: Some(crate::models::FunctionCallDelta { + name: None, arguments: Some(tc.function.arguments), }), } @@ -837,7 +859,7 @@ impl super::Provider for OpenAIProvider { content: String::new(), reasoning_content: None, finish_reason: None, - tool_calls: Some(deltas), + tool_calls: Some(arg_deltas), model: model.clone(), usage: None, };