fix(openai): correctly parse Responses API tool call events
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

- The Responses API does not use 'response.item.delta' for tool calls.
- It uses 'response.output_item.added' to initialize the function call.
- It uses 'response.function_call_arguments.delta' for the payload stream.
- Updated the streaming parser to catch these events and correctly yield ToolCallDelta objects.
- This restores proper streaming of tool calls back to the client.
This commit is contained in:
2026-03-18 16:13:13 +00:00
parent 24a898c9a7
commit 79dc8fe409

View File

@@ -757,15 +757,11 @@ impl super::Provider for OpenAIProvider {
content_buffer.push_str(delta); content_buffer.push_str(delta);
} }
} }
"response.item.delta" => { "response.output_item.added" => {
if let Some(delta) = chunk.get("delta") { if let Some(item) = chunk.get("item") {
let t = delta.get("type").and_then(|v| v.as_str()).unwrap_or(""); if item.get("type").and_then(|v| v.as_str()) == Some("function_call") {
if t == "function_call" { let call_id = item.get("call_id").and_then(|v| v.as_str());
let call_id = delta.get("call_id") let name = item.get("name").and_then(|v| v.as_str());
.or_else(|| chunk.get("item_id"))
.and_then(|v| v.as_str());
let name = delta.get("name").and_then(|v| v.as_str());
let arguments = delta.get("arguments").and_then(|v| v.as_str());
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: chunk.get("output_index").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
@@ -773,17 +769,26 @@ impl super::Provider for OpenAIProvider {
call_type: Some("function".to_string()), call_type: Some("function".to_string()),
function: Some(crate::models::FunctionCallDelta { function: Some(crate::models::FunctionCallDelta {
name: name.map(|s| s.to_string()), name: name.map(|s| s.to_string()),
arguments: arguments.map(|s| s.to_string()), arguments: Some("".to_string()), // Start with empty arguments
}), }),
}]); }]);
} else if t == "message" {
if let Some(text) = delta.get("text").and_then(|v| v.as_str()) {
content_buffer.push_str(text);
} }
} }
} }
"response.function_call_arguments.delta" => {
if let Some(delta) = chunk.get("delta").and_then(|v| v.as_str()) {
tool_calls = Some(vec![crate::models::ToolCallDelta {
index: chunk.get("output_index").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
id: None,
call_type: None,
function: Some(crate::models::FunctionCallDelta {
name: None,
arguments: Some(delta.to_string()),
}),
}]);
} }
"response.output_text.done" | "response.item.done" | "response.done" => { }
"response.output_text.done" | "response.item.done" | "response.completed" => {
finish_reason = Some("stop".to_string()); finish_reason = Some("stop".to_string());
} }
_ => {} _ => {}
@@ -890,12 +895,18 @@ impl super::Provider for OpenAIProvider {
Ok(_) => continue, Ok(_) => continue,
Err(reqwest_eventsource::Error::StreamEnded) => break, Err(reqwest_eventsource::Error::StreamEnded) => break,
Err(e) => { Err(e) => {
tracing::error!("Responses stream encountered an error: {}", e);
// Attempt to probe for the actual error body // Attempt to probe for the actual error body
let mut probe_body_no_stream = probe_body.clone();
if let Some(obj) = probe_body_no_stream.as_object_mut() {
obj.remove("stream");
}
let probe_resp = probe_client let probe_resp = probe_client
.post(&url) .post(&url)
.header("Authorization", format!("Bearer {}", api_key)) .header("Authorization", format!("Bearer {}", api_key))
.header("Accept", "application/json") .header("Accept", "application/json")
.json(&probe_body) .json(&probe_body_no_stream)
.send() .send()
.await; .await;