fix(openai): implement parsing for real-time Responses API streaming format
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

This commit is contained in:
2026-03-17 19:00:40 +00:00
parent 78fff61660
commit 649371154f

View File

@@ -474,8 +474,30 @@ impl super::Provider for OpenAIProvider {
} else { } else {
// Responses API specific parsing for streaming // Responses API specific parsing for streaming
let mut content = String::new(); let mut content = String::new();
let mut finish_reason = None;
// Check for output[0].content[0].text (similar to non-stream) let event_type = chunk.get("type").and_then(|v| v.as_str()).unwrap_or("");
match event_type {
"response.output_text.delta" => {
if let Some(delta) = chunk.get("delta").and_then(|v| v.as_str()) {
content.push_str(delta);
}
}
"response.output_text.done" => {
if let Some(text) = chunk.get("text").and_then(|v| v.as_str()) {
// Some implementations send the full text at the end
// We usually prefer deltas, but if we haven't seen them, this is the fallback.
// However, if we're already yielding deltas, we might not want this.
// For now, let's just use it as a signal that we're done.
finish_reason = Some("stop".to_string());
}
}
"response.done" => {
finish_reason = Some("stop".to_string());
}
_ => {
// Fallback to older nested structure if present
if let Some(output) = chunk.get("output").and_then(|o| o.as_array()) { if let Some(output) = chunk.get("output").and_then(|o| o.as_array()) {
for out in output { for out in output {
if let Some(contents) = out.get("content").and_then(|c| c.as_array()) { if let Some(contents) = out.get("content").and_then(|c| c.as_array()) {
@@ -489,29 +511,14 @@ impl super::Provider for OpenAIProvider {
} }
} }
} }
// Check for candidates[0].content.parts[0].text
if content.is_empty() {
if let Some(cands) = chunk.get("candidates").and_then(|c| c.as_array()) {
for c in cands {
if let Some(content_obj) = c.get("content") {
if let Some(parts) = content_obj.get("parts").and_then(|p| p.as_array()) {
for p in parts {
if let Some(t) = p.get("text").and_then(|v| v.as_str()) {
content.push_str(t);
}
}
}
}
}
} }
} }
if !content.is_empty() { if !content.is_empty() || finish_reason.is_some() {
yield ProviderStreamChunk { yield ProviderStreamChunk {
content, content,
reasoning_content: None, reasoning_content: None,
finish_reason: None, finish_reason,
tool_calls: None, tool_calls: None,
model: model.clone(), model: model.clone(),
usage: None, usage: None,