use crate::canonical::CanonChunk;
use crate::error::ProxyError;
#[cfg(feature = "axum")]
use futures::Stream;
pub struct OpenAiFramer {
first_pending: bool,
done_sent: bool,
id: String,
model: String,
created: u64,
include_usage: bool,
}
impl OpenAiFramer {
pub fn new(id: String, model: String, created: u64, include_usage: bool) -> Self {
Self {
first_pending: true,
done_sent: false,
id,
model,
created,
include_usage,
}
}
pub fn frame_chunk(&mut self, chunk: &CanonChunk) -> Vec<String> {
let mut bytes = String::new();
if self.first_pending {
self.first_pending = false;
let role = serde_json::json!({
"id": self.id, "object": "chat.completion.chunk", "created": self.created,
"model": self.model,
"choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": null}]
});
bytes.push_str(&format!("data: {role}\n\n"));
}
if let Some(json) = chunk.to_sse_json(&self.id, &self.model, self.created, false) {
bytes.push_str(&format!("data: {json}\n\n"));
}
if self.include_usage
&& chunk.usage.is_some()
&& let Some(json) = chunk.to_sse_json(&self.id, &self.model, self.created, true)
{
bytes.push_str(&format!("data: {json}\n\n"));
}
if bytes.is_empty() {
Vec::new()
} else {
vec![bytes]
}
}
pub fn frame_error(&mut self, e: &ProxyError) -> Vec<String> {
self.done_sent = true;
let msg = match e {
ProxyError::Upstream { status, .. } => {
format!("upstream returned status {status}")
}
other => other.to_string(),
};
let frame = serde_json::json!({"error": {"message": msg, "type": "server_error"}});
vec![format!("data: {frame}\n\ndata: [DONE]\n\n")]
}
pub fn finish(&mut self) -> Vec<String> {
if self.done_sent {
Vec::new()
} else {
vec!["data: [DONE]\n\n".into()]
}
}
}
#[cfg(feature = "axum")]
pub fn openai_stream_response<S>(
inner: S,
id: String,
model: String,
created: u64,
include_usage: bool,
) -> axum::response::Response
where
S: Stream<Item = Result<CanonChunk, ProxyError>> + Unpin + Send + 'static,
{
let framer = std::sync::Arc::new(std::sync::Mutex::new(OpenAiFramer::new(
id,
model,
created,
include_usage,
)));
let framer_done = framer.clone();
crate::dialect::sse::sse_response(
inner,
move |out, item| {
let mut f = framer.lock().unwrap();
match item {
Ok(chunk) => {
for seg in f.frame_chunk(&chunk) {
out.push(seg);
}
}
Err(e) => {
for seg in f.frame_error(&e) {
out.push(seg);
}
}
}
},
move |out| {
let mut f = framer_done.lock().unwrap();
for seg in f.finish() {
out.push(seg);
}
},
)
}
#[cfg(all(test, feature = "axum"))]
mod tests {
use super::*;
use crate::canonical::Usage;
fn stream_chunks(
chunks: Vec<Result<CanonChunk, ProxyError>>,
) -> futures::stream::BoxStream<'static, Result<CanonChunk, ProxyError>> {
Box::pin(futures::stream::iter(chunks))
}
fn chunk(text: &str) -> CanonChunk {
CanonChunk {
delta_text: text.into(),
..Default::default()
}
}
async fn collect(resp: axum::response::Response) -> String {
let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
.await
.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn usage_chunk_only_when_client_opted_in() {
let mk = || {
vec![
Ok(chunk("hi")),
Ok(CanonChunk {
finish_reason: Some("stop".into()),
usage: Some(Usage {
prompt_tokens: 5,
completion_tokens: 2,
cached_read_tokens: 0,
cache_write_tokens: 0,
reasoning_tokens: None,
}),
..chunk("")
}),
]
};
let off = openai_stream_response(
stream_chunks(mk()),
"chatcmpl-1".into(),
"m".into(),
0,
false,
);
let s = collect(off).await;
assert!(
!s.contains("\"usage\""),
"usage leaked without include_usage: {s}"
);
assert!(s.contains("data: [DONE]"));
let on = openai_stream_response(
stream_chunks(mk()),
"chatcmpl-1".into(),
"m".into(),
0,
true,
);
let s = collect(on).await;
assert!(
s.contains("\"prompt_tokens\":5"),
"usage missing with include_usage: {s}"
);
}
#[tokio::test]
async fn first_chunk_carries_role_and_error_frame_terminates() {
let s = collect(openai_stream_response(
stream_chunks(vec![Err(ProxyError::upstream(
502,
"secret upstream body".into(),
))]),
"chatcmpl-1".into(),
"m".into(),
0,
false,
))
.await;
assert!(s.contains("\"type\":\"server_error\""), "{s}");
assert!(!s.contains("secret upstream body"), "{s}");
assert!(s.ends_with("data: [DONE]\n\n"), "{s}");
}
#[tokio::test]
async fn first_chunk_carries_role_preamble_before_data() {
let s = collect(openai_stream_response(
stream_chunks(vec![Ok(chunk("hello"))]),
"chatcmpl-1".into(),
"m".into(),
0,
false,
))
.await;
let role = s
.find("\"delta\":{\"role\":\"assistant\"}")
.expect("role preamble missing");
let data = s.find("hello").expect("data frame missing");
assert!(role < data, "role preamble must precede data frames: {s}");
assert_eq!(
s.matches("\"delta\":{\"role\":\"assistant\"}").count(),
1,
"exactly one role preamble: {s}"
);
}
}