use futures_util::StreamExt;
use serde::Serialize;
use serde_json::{Map, Value};
use tokio::sync::mpsc;
use crate::error::parse_stream_error;
use crate::sse::{SseParser, SseToken};
use crate::{Client, Error};
const OPENAI_PATH: &str = "/v1/chat/completions";
const MESSAGES_PATH: &str = "/v1/messages";
const COUNT_TOKENS_PATH: &str = "/v1/messages/count_tokens";
const ANTHROPIC_HEADERS: &[(&str, &str)] = &[("anthropic-version", "2023-06-01")];
fn prepare(params: impl Serialize, endpoint: &str) -> Result<Map<String, Value>, Error> {
let value = serde_json::to_value(params)
.map_err(|e| Error::sdk(format!("failed to serialize {endpoint} params: {e}")))?;
let body = match value {
Value::Object(map) => map,
_ => {
return Err(Error::client_validation(format!(
"{endpoint} params must serialize to a JSON object"
)))
}
};
for field in ["model", "messages"] {
if matches!(body.get(field), None | Some(Value::Null)) {
return Err(Error::client_validation(format!(
"{field} is required for {endpoint}"
)));
}
}
Ok(body)
}
fn reject_stream_flag(body: &Map<String, Value>, endpoint: &str) -> Result<(), Error> {
if body.get("stream") == Some(&Value::Bool(true)) {
return Err(Error::client_validation(format!(
"params set stream=true — use create_stream() for streaming {endpoint} requests"
)));
}
Ok(())
}
pub struct ChatCompletions {
client: Client,
}
impl ChatCompletions {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
pub async fn create(&self, params: impl Serialize) -> Result<Value, Error> {
let body = prepare(params, OPENAI_PATH)?;
reject_stream_flag(&body, OPENAI_PATH)?;
self.client
.post_passthrough_json(OPENAI_PATH, &Value::Object(body), &[])
.await
}
pub async fn create_stream(&self, params: impl Serialize) -> Result<RawEventStream, Error> {
let mut body = prepare(params, OPENAI_PATH)?;
body.insert("stream".into(), Value::Bool(true));
let resp = self
.client
.post_passthrough_stream(OPENAI_PATH, &Value::Object(body), &[])
.await?;
Ok(spawn_raw_event_driver(resp, RawStreamKind::OpenAi))
}
}
pub struct Messages {
client: Client,
}
impl Messages {
pub(crate) fn new(client: Client) -> Self {
Self { client }
}
pub async fn create(&self, params: impl Serialize) -> Result<Value, Error> {
let body = prepare(params, MESSAGES_PATH)?;
reject_stream_flag(&body, MESSAGES_PATH)?;
self.client
.post_passthrough_json(MESSAGES_PATH, &Value::Object(body), ANTHROPIC_HEADERS)
.await
}
pub async fn create_stream(&self, params: impl Serialize) -> Result<RawEventStream, Error> {
let mut body = prepare(params, MESSAGES_PATH)?;
body.insert("stream".into(), Value::Bool(true));
let resp = self
.client
.post_passthrough_stream(MESSAGES_PATH, &Value::Object(body), ANTHROPIC_HEADERS)
.await?;
Ok(spawn_raw_event_driver(resp, RawStreamKind::Anthropic))
}
pub async fn count_tokens(&self, params: impl Serialize) -> Result<Value, Error> {
let body = prepare(params, COUNT_TOKENS_PATH)?;
self.client
.post_passthrough_json(COUNT_TOKENS_PATH, &Value::Object(body), ANTHROPIC_HEADERS)
.await
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RawStreamKind {
OpenAi,
Anthropic,
}
pub struct RawEventStream {
rx: mpsc::Receiver<Result<Value, Error>>,
}
impl RawEventStream {
pub async fn recv(&mut self) -> Result<Option<Value>, Error> {
match self.rx.recv().await {
Some(Ok(event)) => Ok(Some(event)),
Some(Err(e)) => Err(e),
None => Ok(None),
}
}
}
fn spawn_raw_event_driver(response: reqwest::Response, kind: RawStreamKind) -> RawEventStream {
let (tx, rx) = mpsc::channel::<Result<Value, Error>>(64);
tokio::spawn(async move {
let mut byte_stream = response.bytes_stream();
let mut parser = SseParser::new();
let mut done = false; let mut saw_message_stop = false; let mut aborted = false; let mut eof = false;
'read: while !done && !eof {
match byte_stream.next().await {
Some(Ok(chunk)) => {
parser.push(&chunk);
if parser.overflowed() {
let _ = tx
.send(Err(Error::model_stream_error(
"SSE line exceeds the 32 MiB limit",
)))
.await;
aborted = true;
break 'read;
}
}
Some(Err(e)) => {
let _ = tx
.send(Err(Error::model_stream_error(format!(
"stream read error: {e}"
))))
.await;
aborted = true;
break 'read;
}
None => {
parser.flush();
eof = true;
}
}
for token in parser.drain() {
let payload = match token {
SseToken::Done => match kind {
RawStreamKind::OpenAi => {
done = true;
break;
}
RawStreamKind::Anthropic => continue,
},
SseToken::Data(payload) => payload,
};
let Ok(event) = serde_json::from_str::<Value>(&payload) else {
continue;
};
if !event.is_object() {
continue;
}
let is_error = match kind {
RawStreamKind::OpenAi => event.get("error").is_some(),
RawStreamKind::Anthropic => {
event.get("type").and_then(Value::as_str) == Some("error")
}
};
if is_error {
let _ = tx.send(Err(parse_stream_error(&payload))).await;
aborted = true;
break 'read;
}
if kind == RawStreamKind::Anthropic
&& event.get("type").and_then(Value::as_str) == Some("message_stop")
{
saw_message_stop = true;
}
if tx.send(Ok(event)).await.is_err() {
aborted = true;
break 'read;
}
}
}
let healthy = match kind {
RawStreamKind::OpenAi => done,
RawStreamKind::Anthropic => saw_message_stop,
};
if !healthy && !aborted {
let message = match kind {
RawStreamKind::OpenAi => "connection closed before [DONE]",
RawStreamKind::Anthropic => "stream ended unexpectedly before message_stop",
};
let _ = tx.send(Err(Error::model_stream_error(message))).await;
}
});
RawEventStream { rx }
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn prepare_accepts_object_with_required_fields() {
let body = prepare(
json!({"model": "m", "messages": [], "custom_field": {"x": 1}}),
OPENAI_PATH,
)
.unwrap();
assert_eq!(body["custom_field"], json!({"x": 1}));
}
#[test]
fn prepare_rejects_non_object_params() {
let err = prepare(json!(["not", "an", "object"]), OPENAI_PATH).unwrap_err();
assert!(matches!(err, Error::Validation(_)));
}
#[test]
fn prepare_rejects_missing_model() {
let err = prepare(json!({"messages": []}), MESSAGES_PATH).unwrap_err();
match err {
Error::Validation(d) => assert!(d.message.contains("model")),
other => panic!("expected Validation, got {other:?}"),
}
}
#[test]
fn prepare_rejects_null_messages() {
let err = prepare(json!({"model": "m", "messages": null}), OPENAI_PATH).unwrap_err();
match err {
Error::Validation(d) => assert!(d.message.contains("messages")),
other => panic!("expected Validation, got {other:?}"),
}
}
#[test]
fn reject_stream_flag_only_fires_on_true() {
let on = prepare(
json!({"model": "m", "messages": [], "stream": true}),
OPENAI_PATH,
)
.unwrap();
assert!(reject_stream_flag(&on, OPENAI_PATH).is_err());
let off = prepare(
json!({"model": "m", "messages": [], "stream": false}),
OPENAI_PATH,
)
.unwrap();
assert!(reject_stream_flag(&off, OPENAI_PATH).is_ok());
}
}