use std::pin::Pin;
use bytes::Bytes;
use futures_core::Stream;
use futures_util::StreamExt;
use reqwest::Method;
use reqwest::header::HeaderMap;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::client::generate::{ChatRequest, GenerateRequest};
use crate::client::{Client, StreamAttempt};
use crate::error::{Error, Result, TransportErrorKind};
use crate::http::{HttpResponse, PreparedRequest, headers};
use crate::retry::{Decision, RequestOptions, RetryPolicy, RetryState};
use crate::types::{ChatCompletionChunk, GenerateChunk};
use crate::wire::sse::{self, LineDecoder};
pub type ChunkStream<T> = Pin<Box<dyn Stream<Item = Result<T>> + Send>>;
impl GenerateRequest {
pub fn stream(self) -> Result<ChunkStream<GenerateChunk>> {
let body = self.body(true)?;
let request = self
.client
.sse_request(&self.path(), &body, &self.options)?;
Ok(Box::pin(sse_stream(
self.client,
request,
RetryPolicy::STREAM,
self.model,
self.options,
)))
}
}
impl ChatRequest {
pub fn stream(self) -> Result<ChunkStream<ChatCompletionChunk>> {
let body = self.body(true)?;
let request = self
.client
.sse_request("/v1/chat/completions", &body, &self.options)?;
Ok(Box::pin(sse_stream(
self.client,
request,
RetryPolicy::STREAM,
self.model,
self.options,
)))
}
}
impl Client {
fn sse_request(
&self,
path: &str,
body: &Value,
options: &RequestOptions,
) -> Result<PreparedRequest> {
let routing = self.routing(options.gpu.as_deref());
let encoded = serde_json::to_vec(body)
.map_err(|err| Error::invalid(format!("could not encode the request body: {err}")))?;
Ok(self
.request(Method::POST, path)?
.sse_headers()
.maybe_header(headers::MACHINE_PROFILE, routing.profile.as_deref())
.maybe_header(headers::POOL, routing.pool.as_deref())
.body(encoded))
}
}
fn as_capacity_response(code: &str, message: &str, headers: &HeaderMap) -> HttpResponse {
let mut response_headers = headers.clone();
response_headers.insert(
reqwest::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
HttpResponse {
status: 503,
headers: response_headers,
body: Bytes::from(
serde_json::json!({"error": {"code": code, "message": message}}).to_string(),
),
}
}
#[derive(Debug)]
enum Event<T> {
Chunk(T),
Done,
Capacity {
code: String,
message: String,
},
}
fn decode_event<T: DeserializeOwned>(payload: &str) -> Result<Event<T>> {
if payload == sse::DONE {
return Ok(Event::Done);
}
let value: Value = serde_json::from_str(payload)
.map_err(|err| Error::decode(format!("Malformed SSE chunk from server: {err}")))?;
if let Some((code, message)) = sse::chunk_error(&value) {
return Ok(Event::Capacity { code, message });
}
serde_json::from_value(value)
.map(Event::Chunk)
.map_err(|err| Error::decode(format!("Unexpected SSE chunk shape: {err}")))
}
fn sse_stream<T: DeserializeOwned + Send>(
client: Client,
request: PreparedRequest,
policy: RetryPolicy,
model: String,
options: RequestOptions,
) -> impl Stream<Item = Result<T>> {
async_stream::try_stream! {
let mut state = RetryState::new(policy, &options, Some(&model));
let mut yielded = false;
loop {
let StreamAttempt { response, headers } = client.send_streaming(&request, &mut state).await?;
let mut body = response.bytes_stream();
let mut decoder = LineDecoder::default();
let mut reconnect_after = None;
'attempt: while let Some(chunk) = body.next().await {
let chunk = chunk.map_err(|error| {
Error::connection(
TransportErrorKind::MidFlight,
format!("Connection lost during stream: {error}"),
error,
)
})?;
for line in decoder.push(&chunk) {
let Some(payload) = sse::data_payload(&line) else { continue };
match decode_event::<T>(payload)? {
Event::Chunk(value) => {
yielded = true;
yield value;
}
Event::Done => return,
Event::Capacity { code, message } => {
let terminal = || Error::Server {
message: message.clone(),
code: Some(code.clone()),
status: 503,
request: None,
};
if yielded {
Err(terminal())?;
}
match state.on_response(&as_capacity_response(&code, &message, &headers))? {
Decision::Retry(delay) => {
reconnect_after = Some(delay);
break 'attempt;
}
Decision::Accept => Err(terminal())?,
}
}
}
}
}
if reconnect_after.is_none()
&& let Some(line) = decoder.finish()
&& let Some(payload) = sse::data_payload(&line)
{
match decode_event::<T>(payload)? {
Event::Chunk(value) => yield value,
Event::Done | Event::Capacity { .. } => return,
}
}
match reconnect_after {
Some(delay) => tokio::time::sleep(delay).await,
None => return,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ChatMessage;
#[test]
fn decodes_payloads_sentinels_and_errors() {
let chunk: Event<ChatCompletionChunk> =
decode_event(r#"{"id": "c", "choices": [{"index": 0, "delta": {"content": "hi"}}]}"#)
.unwrap();
match chunk {
Event::Chunk(chunk) => assert_eq!(chunk.delta(), Some("hi")),
_ => panic!("expected a chunk"),
}
assert!(matches!(
decode_event::<ChatCompletionChunk>("[DONE]").unwrap(),
Event::Done
));
let capacity: Event<ChatCompletionChunk> =
decode_event(r#"{"error": {"code": "RESOURCE_EXHAUSTED", "message": "oom"}}"#).unwrap();
match capacity {
Event::Capacity { code, message } => {
assert_eq!(code, "RESOURCE_EXHAUSTED");
assert_eq!(message, "oom");
}
_ => panic!("expected a capacity signal"),
}
}
#[test]
fn malformed_json_is_a_decode_error() {
let err = decode_event::<ChatCompletionChunk>("{not json").unwrap_err();
assert!(err.to_string().contains("Malformed SSE chunk"), "{err}");
}
#[test]
fn the_synthesized_capacity_response_keeps_the_connections_retry_hint() {
let mut headers = HeaderMap::new();
headers.insert(reqwest::header::RETRY_AFTER, "3".parse().unwrap());
let response = as_capacity_response("MODEL_LOADING", "loading", &headers);
assert_eq!(
crate::retry::backoff::retry_after(&response.headers),
Some(std::time::Duration::from_secs(3))
);
}
#[test]
fn the_synthesized_capacity_response_round_trips_through_the_error_reader() {
let response = as_capacity_response("MODEL_LOADING", "still loading", &HeaderMap::new());
assert_eq!(response.status, 503);
let envelope = crate::wire::parse_envelope(&response);
assert_eq!(envelope.code.as_deref(), Some("MODEL_LOADING"));
assert_eq!(envelope.message.as_deref(), Some("still loading"));
}
#[tokio::test]
async fn a_stream_against_an_unreachable_server_fails_rather_than_hanging() {
let client = Client::builder("http://127.0.0.1:1")
.timeout(std::time::Duration::from_millis(200))
.wait_for_capacity(false)
.build()
.unwrap();
let mut stream = client
.chat("m", [ChatMessage::user("hi")])
.stream()
.unwrap();
let first = stream
.next()
.await
.expect("the stream must yield a failure");
assert!(matches!(first, Err(Error::Connection { .. })), "{first:?}");
}
}