Skip to main content

smos_application/types/
chat_response.rs

1//! OpenAI-compatible chat-completion response envelope.
2//!
3//! Two flavours coexist on the same upstream:
4//! - `NonStreaming` is the full buffered JSON response (callers parse it).
5//! - `Streaming` is an opaque byte stream (callers pass it through as SSE).
6//!
7//! We deliberately do *not* model the JSON shape: the proxy forwards it
8//! verbatim, and OpenAI may evolve it independently of this crate.
9
10use bytes::Bytes;
11use futures::Stream;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::errors::UpstreamError;
16
17/// Chat-completion response — buffered JSON or byte stream.
18///
19/// A manual `Debug` impl is required because `dyn Stream` has no `Debug`;
20/// the streaming arm is rendered with a placeholder that still identifies
21/// the variant.
22pub enum ChatResponse {
23    /// Full buffered body for non-streaming calls.
24    NonStreaming(Value),
25
26    /// Raw byte stream for streaming calls. The boxed trait object keeps the
27    /// enum cheap to move and decouples callers from the concrete HTTP body
28    /// type produced by the adapter.
29    Streaming(Box<dyn Stream<Item = Result<Bytes, UpstreamError>> + Send + Unpin>),
30}
31
32impl std::fmt::Debug for ChatResponse {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            ChatResponse::NonStreaming(v) => f.debug_tuple("NonStreaming").field(v).finish(),
36            ChatResponse::Streaming(_) => {
37                f.debug_tuple("Streaming").field(&"<byte stream>").finish()
38            }
39        }
40    }
41}
42
43// `NonStreaming` arm implements `Serialize` so callers that need to forward
44// the response can do so without re-matching. The streaming arm is not
45// serialisable by definition; we expose a helper instead.
46impl Serialize for ChatResponse {
47    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48    where
49        S: serde::Serializer,
50    {
51        match self {
52            ChatResponse::NonStreaming(v) => v.serialize(serializer),
53            ChatResponse::Streaming(_) => Err(serde::ser::Error::custom(
54                "cannot serialize a streaming ChatResponse; drain the stream first",
55            )),
56        }
57    }
58}
59
60// `NonStreaming` arm deserialises directly; there is no wire representation
61// for the streaming arm, so we only support the buffered shape here.
62impl<'de> Deserialize<'de> for ChatResponse {
63    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64    where
65        D: serde::Deserializer<'de>,
66    {
67        Value::deserialize(deserializer).map(ChatResponse::NonStreaming)
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use futures::stream;
75    use serde_json::json;
76
77    #[test]
78    fn non_streaming_serialises_inner_value() {
79        let resp = ChatResponse::NonStreaming(json!({"choices": []}));
80        let s = serde_json::to_string(&resp).unwrap();
81        assert!(s.contains("choices"));
82    }
83
84    #[test]
85    fn streaming_arm_is_not_serialisable() {
86        let stream: Box<dyn Stream<Item = Result<Bytes, UpstreamError>> + Send + Unpin> =
87            Box::new(stream::iter(vec![Ok(Bytes::from_static(b"chunk"))]));
88        let resp = ChatResponse::Streaming(stream);
89        let result = serde_json::to_string(&resp);
90        assert!(result.is_err());
91    }
92
93    #[test]
94    fn non_streaming_roundtrips_through_serde() {
95        let resp = ChatResponse::NonStreaming(json!({"a": 1}));
96        let json_str = serde_json::to_string(&resp).unwrap();
97        let back: ChatResponse = serde_json::from_str(&json_str).unwrap();
98        match back {
99            ChatResponse::NonStreaming(v) => assert_eq!(v["a"], 1),
100            ChatResponse::Streaming(_) => panic!("expected NonStreaming"),
101        }
102    }
103}