From 4de457cc5ebc86b073748c2f183d23e386582905 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Wed, 18 Mar 2026 18:26:27 +0000 Subject: [PATCH] fix(openai): correctly map tool_call indexes in Responses API stream - The OpenAI Responses API uses 'output_index' to identify items in the response. - If a response starts with text (output_index 0) followed by a tool call (output_index 1), the standard Chat Completions streaming format requires the first tool call to have index 0. - Previously, the proxy was passing output_index (1) as the tool_call index, causing client-side SDKs to fail parsing the stream and silently drop the tool calls. - Implemented a local mapping within the stream to ensure tool_call indexes are always dense and start at 0. --- src/providers/openai.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/providers/openai.rs b/src/providers/openai.rs index ecb6300f..240ea6b1 100644 --- a/src/providers/openai.rs +++ b/src/providers/openai.rs @@ -731,6 +731,8 @@ impl super::Provider for OpenAIProvider { let mut es = es; let mut content_buffer = String::new(); let mut has_tool_calls = false; + let mut tool_index_map = std::collections::HashMap::::new(); + let mut next_tool_index = 0u32; while let Some(event) = es.next().await { match event { @@ -765,8 +767,15 @@ impl super::Provider for OpenAIProvider { let call_id = item.get("call_id").and_then(|v| v.as_str()); let name = item.get("name").and_then(|v| v.as_str()); + let out_idx = chunk.get("output_index").and_then(|v| v.as_u64()).unwrap_or(0) as u32; + let tc_idx = *tool_index_map.entry(out_idx).or_insert_with(|| { + let i = next_tool_index; + next_tool_index += 1; + i + }); + tool_calls = Some(vec![crate::models::ToolCallDelta { - index: chunk.get("output_index").and_then(|v| v.as_u64()).unwrap_or(0) as u32, + index: tc_idx, id: call_id.map(|s| s.to_string()), call_type: Some("function".to_string()), function: Some(crate::models::FunctionCallDelta { @@ -780,8 +789,16 @@ impl super::Provider for OpenAIProvider { "response.function_call_arguments.delta" => { if let Some(delta) = chunk.get("delta").and_then(|v| v.as_str()) { has_tool_calls = true; + + let out_idx = chunk.get("output_index").and_then(|v| v.as_u64()).unwrap_or(0) as u32; + let tc_idx = *tool_index_map.entry(out_idx).or_insert_with(|| { + let i = next_tool_index; + next_tool_index += 1; + i + }); + tool_calls = Some(vec![crate::models::ToolCallDelta { - index: chunk.get("output_index").and_then(|v| v.as_u64()).unwrap_or(0) as u32, + index: tc_idx, id: None, call_type: None, function: Some(crate::models::FunctionCallDelta {