Compare commits

..

2 Commits

Author SHA1 Message Date
57aa0aa70e fix(openai): unify tool call indexing for both standard and embedded calls
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled
- Sequential next_tool_index is now used for both Responses API 'function_call' events and the proxy's 'tool_uses' JSON extraction.
- This ensures tool_calls arrays in the stream always start at index 0 and are dense, even if standard and embedded calls were somehow mixed.
- Fixed 'payload_idx' logic to correctly align argument chunks with their initialization chunks.
2026-03-18 18:31:24 +00:00
4de457cc5e 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.
2026-03-18 18:26:27 +00:00

View File

@@ -731,6 +731,8 @@ impl super::Provider for OpenAIProvider {
let mut es = es; let mut es = es;
let mut content_buffer = String::new(); let mut content_buffer = String::new();
let mut has_tool_calls = false; let mut has_tool_calls = false;
let mut tool_index_map = std::collections::HashMap::<u32, u32>::new();
let mut next_tool_index = 0u32;
while let Some(event) = es.next().await { while let Some(event) = es.next().await {
match event { 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 call_id = item.get("call_id").and_then(|v| v.as_str());
let name = item.get("name").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 { 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()), id: call_id.map(|s| s.to_string()),
call_type: Some("function".to_string()), call_type: Some("function".to_string()),
function: Some(crate::models::FunctionCallDelta { function: Some(crate::models::FunctionCallDelta {
@@ -780,8 +789,16 @@ impl super::Provider for OpenAIProvider {
"response.function_call_arguments.delta" => { "response.function_call_arguments.delta" => {
if let Some(delta) = chunk.get("delta").and_then(|v| v.as_str()) { if let Some(delta) = chunk.get("delta").and_then(|v| v.as_str()) {
has_tool_calls = true; 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 { 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, id: None,
call_type: None, call_type: None,
function: Some(crate::models::FunctionCallDelta { function: Some(crate::models::FunctionCallDelta {
@@ -821,9 +838,11 @@ impl super::Provider for OpenAIProvider {
// Yield the tool calls in two chunks to mimic standard streaming behavior // Yield the tool calls in two chunks to mimic standard streaming behavior
// Chunk 1: Initialization (id, name) // Chunk 1: Initialization (id, name)
let init_deltas: Vec<crate::models::ToolCallDelta> = embedded_calls.iter().enumerate().map(|(idx, tc)| { let init_deltas: Vec<crate::models::ToolCallDelta> = embedded_calls.iter().map(|tc| {
let tc_idx = next_tool_index;
next_tool_index += 1;
crate::models::ToolCallDelta { crate::models::ToolCallDelta {
index: idx as u32, index: tc_idx,
id: Some(tc.id.clone()), id: Some(tc.id.clone()),
call_type: Some("function".to_string()), call_type: Some("function".to_string()),
function: Some(crate::models::FunctionCallDelta { function: Some(crate::models::FunctionCallDelta {
@@ -843,9 +862,13 @@ impl super::Provider for OpenAIProvider {
}; };
// Chunk 2: Payload (arguments) // Chunk 2: Payload (arguments)
let arg_deltas: Vec<crate::models::ToolCallDelta> = embedded_calls.into_iter().enumerate().map(|(idx, tc)| { // Reset temp index for payload chunk
let mut payload_idx = next_tool_index - embedded_calls.len() as u32;
let arg_deltas: Vec<crate::models::ToolCallDelta> = embedded_calls.into_iter().map(|tc| {
let tc_idx = payload_idx;
payload_idx += 1;
crate::models::ToolCallDelta { crate::models::ToolCallDelta {
index: idx as u32, index: tc_idx,
id: None, id: None,
call_type: None, call_type: None,
function: Some(crate::models::FunctionCallDelta { function: Some(crate::models::FunctionCallDelta {