use std::collections::HashSet;
use serde_json::Value;
use tokio_stream::StreamExt;
use super::wire;
use super::{parse::response_to_completion, wire::ResponsesRequest};
use crate::completion::{Completion, CompletionDelta};
use crate::http::HttpFailure;
use crate::openai::{OpenAiModelConfig, classify};
pub(crate) async fn send_once_streaming(
http: &reqwest::Client,
config: &OpenAiModelConfig,
request: &ResponsesRequest,
freeform_names: &HashSet<String>,
on_delta: &mut (dyn FnMut(CompletionDelta) + Send),
) -> Result<Completion, HttpFailure> {
let url = format!("{}/v1/responses", config.base_url);
let mut builder = http.post(&url).bearer_auth(&config.bearer).json(request);
for (name, value) in &config.extra_headers {
builder = builder.header(name, value);
}
let response = builder
.send()
.await
.map_err(|e| HttpFailure::transport(e.to_string()))?;
let status = response.status();
let retry_after = response
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(crate::http::parse_retry_after);
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
let body: crate::openai::OpenAiErrorBody = match serde_json::from_str(&text) {
Ok(body) => body,
Err(_) => crate::openai::OpenAiErrorBody {
error: crate::openai::OpenAiErrorDetail {
code: None,
r#type: None,
message: text,
},
},
};
return Err(classify(status.as_u16(), retry_after, &body));
}
let mut stream = response.bytes_stream();
let mut buf = String::new();
while let Some(chunk) = stream.next().await {
let bytes = chunk.map_err(|e| HttpFailure::transport(format!("stream read: {e}")))?;
buf.push_str(&String::from_utf8_lossy(&bytes));
if let Some(value) = drain_frames(&mut buf, on_delta) {
return finish(value, freeform_names, retry_after);
}
}
Err(HttpFailure::transport(
"stream closed before response.completed".to_string(),
))
}
fn drain_frames(
buf: &mut String,
on_delta: &mut (dyn FnMut(CompletionDelta) + Send),
) -> Option<Value> {
while let Some(pos) = buf.find("\n\n") {
let frame: String = buf.drain(..pos + 2).collect();
let Some(data) = sse_data(&frame) else {
continue;
};
if data == "[DONE]" {
continue;
}
let Ok(event) = serde_json::from_str::<StreamEvent>(&data) else {
continue;
};
let mut terminal = None;
if handle(event, on_delta, &mut terminal) {
return terminal;
}
}
None
}
fn finish(
value: Value,
freeform_names: &HashSet<String>,
retry_after: Option<std::time::Duration>,
) -> Result<Completion, HttpFailure> {
let parsed: wire::ResponsesResponse = serde_json::from_value(value)
.map_err(|e| HttpFailure::decode(format!("terminal stream response: {e}")))?;
response_to_completion(parsed, freeform_names).map_err(|error| HttpFailure {
error,
force_terminal: false,
retry_after,
})
}
fn handle(
event: StreamEvent,
on_delta: &mut (dyn FnMut(CompletionDelta) + Send),
final_response: &mut Option<Value>,
) -> bool {
match event.kind.as_str() {
"response.output_text.delta" => {
if let Some(delta) = event.delta {
on_delta(CompletionDelta::Text(delta));
}
}
"response.reasoning_summary_text.delta" | "response.reasoning_text.delta" => {
if let Some(delta) = event.delta {
on_delta(CompletionDelta::Thinking(delta));
}
}
"response.function_call_arguments.delta" => {
if let Some(delta) = event.delta {
on_delta(CompletionDelta::ToolArgs(delta));
}
}
"response.output_item.added" => {
if let Some(item) = &event.item
&& item.get("type").and_then(Value::as_str) == Some("function_call")
{
let id = item
.get("call_id")
.or_else(|| item.get("id"))
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let name = item
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
on_delta(CompletionDelta::ToolUseStart { id, name });
}
}
"response.completed" | "response.incomplete" | "response.failed" => {
*final_response = event.response;
return true;
}
_ => {}
}
false
}
#[derive(serde::Deserialize)]
struct StreamEvent {
#[serde(rename = "type")]
kind: String,
#[serde(default)]
delta: Option<String>,
#[serde(default)]
item: Option<Value>,
#[serde(default)]
response: Option<Value>,
}
fn sse_data(frame: &str) -> Option<String> {
let mut data = String::new();
let mut has = false;
for line in frame.lines() {
if let Some(rest) = line.strip_prefix("data:") {
if has {
data.push('\n');
}
data.push_str(rest.strip_prefix(' ').unwrap_or(rest));
has = true;
}
}
has.then_some(data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::completion::CompletionDelta;
use locode_protocol::ContentBlock;
use serde_json::json;
fn completed_response() -> Value {
json!({
"status": "completed",
"output": [
{"type": "message", "content": [{"type": "output_text", "text": "Hello world"}]},
{"type": "function_call", "call_id": "call_1", "name": "get_weather",
"arguments": "{\"city\":\"SF\"}"}
],
"usage": {"input_tokens": 10, "output_tokens": 5}
})
}
fn transcript() -> String {
let events = [
json!({"type": "response.created"}),
json!({"type": "response.output_item.added",
"item": {"type": "function_call", "call_id": "call_1", "name": "get_weather"}}),
json!({"type": "response.output_text.delta", "delta": "Hello "}),
json!({"type": "response.output_text.delta", "delta": "world"}),
json!({"type": "response.function_call_arguments.delta", "delta": "{\"city\":"}),
json!({"type": "response.function_call_arguments.delta", "delta": "\"SF\"}"}),
json!({"type": "response.output_item.done",
"item": {"type": "function_call", "call_id": "call_1", "name": "get_weather",
"arguments": "{\"city\":\"SF\"}"}}),
json!({"type": "response.completed", "response": completed_response()}),
];
let mut out = String::new();
for e in events {
out.push_str("event: x\ndata: ");
out.push_str(&serde_json::to_string(&e).unwrap());
out.push_str("\n\n");
}
out
}
#[test]
fn stream_assembly_is_byte_identical_and_emits_deltas() {
let names = HashSet::new();
let expected = response_to_completion(
serde_json::from_value(completed_response()).unwrap(),
&names,
)
.unwrap();
for chunk_size in [1usize, 3, 7, 50, 100_000] {
let full = transcript();
let mut buf = String::new();
let mut deltas = Vec::new();
let mut got = None;
for chunk in full.as_bytes().chunks(chunk_size) {
buf.push_str(&String::from_utf8_lossy(chunk));
if let Some(value) = drain_frames(&mut buf, &mut |d| deltas.push(d)) {
got = Some(finish(value, &names, None).unwrap());
break;
}
}
assert_eq!(got.unwrap(), expected, "chunk_size {chunk_size}");
assert_eq!(
deltas,
vec![
CompletionDelta::ToolUseStart {
id: "call_1".into(),
name: "get_weather".into()
},
CompletionDelta::Text("Hello ".into()),
CompletionDelta::Text("world".into()),
CompletionDelta::ToolArgs("{\"city\":".into()),
CompletionDelta::ToolArgs("\"SF\"}".into()),
],
"chunk_size {chunk_size}"
);
}
assert!(
matches!(&expected.content[0], ContentBlock::Text { text } if text == "Hello world")
);
assert!(
matches!(&expected.content[1], ContentBlock::ToolUse { name, .. } if name == "get_weather")
);
}
#[test]
fn sse_data_extracts_payload() {
assert_eq!(
sse_data("event: x\ndata: {\"a\":1}\n"),
Some("{\"a\":1}".to_string())
);
assert_eq!(sse_data(": comment\n"), None);
}
#[test]
fn failed_terminal_event_maps_to_provider_error() {
let failed = json!({"status": "failed", "error": {"code": "rate_limit_exceeded", "message": "slow"}});
let err = finish(failed, &HashSet::new(), None).unwrap_err();
assert!(matches!(
err.error,
crate::provider::ProviderError::RateLimited { .. }
));
}
#[test]
fn no_terminal_event_yields_none_then_truncation() {
let mut buf =
String::from("data: {\"type\":\"response.output_text.delta\",\"delta\":\"hi\"}\n\n");
let mut deltas = Vec::new();
assert!(drain_frames(&mut buf, &mut |d| deltas.push(d)).is_none());
assert_eq!(deltas, vec![CompletionDelta::Text("hi".into())]);
}
#[tokio::test]
#[ignore = "hits the live API; needs an openai-responses model in LOCODE_MODEL"]
async fn live_smoke_responses_streaming() {
use crate::provider::Provider;
use crate::request::{CacheHint, ConversationRequest, SamplingArgs};
use locode_protocol::{Message, Role};
let mut provider = super::super::OpenAiResponsesProvider::from_env().expect("from_env");
if let Ok(model) = std::env::var("LOCODE_MODEL") {
provider.config_mut().model = model;
}
let request = ConversationRequest {
messages: vec![Message {
role: Role::User,
content: vec![ContentBlock::Text {
text: "Reply with exactly: streaming works".into(),
}],
}],
tools: vec![],
sampling_args: SamplingArgs::default(),
cache_hint: CacheHint::default(),
};
let mut deltas = Vec::new();
let completion = provider
.stream(&request, &mut |d| deltas.push(d))
.await
.expect("live stream ok");
let joined: String = deltas
.iter()
.filter_map(|d| match d {
CompletionDelta::Text(t) => Some(t.as_str()),
_ => None,
})
.collect();
assert_eq!(Some(joined), completion.text());
}
}