Skip to main content

zai_rs/model/
chat_stream_response.rs

1//! # Streaming Response Types for Chat API Models
2//!
3//! This module defines the data structures used for processing streaming
4//! responses from chat completion APIs. These types are specifically designed
5//! to handle Server-Sent Events (SSE) data chunks where responses arrive
6//! incrementally.
7//!
8//! ## Key Differences from Standard Responses
9//!
10//! Unlike regular chat completion responses, streaming responses:
11//! - Contain `delta` fields instead of complete `message` objects
12//! - Arrive as multiple chunks over time
13//! - Include partial content that gets assembled client-side
14//! - May contain reasoning content for models with thinking capabilities
15//!
16//! ## Streaming Protocol
17//!
18//! The streaming implementation expects SSE-formatted data with:
19//! - `data: ` prefixed lines containing JSON chunks
20//! - `[DONE]` marker to signal stream completion
21//! - Optional usage statistics on the final chunk
22//!
23//! ## Decoding a chunk
24//!
25//! ```
26//! use zai_rs::model::chat_stream_response::ChatStreamResponse;
27//!
28//! let chunk: ChatStreamResponse = serde_json::from_str(
29//!     r#"{"id":null,"choices":[{"index":0,"delta":{"content":"Hello"}}]}"#,
30//! )?;
31//! assert_eq!(chunk.choices[0].delta.as_ref().unwrap().content.as_deref(), Some("Hello"));
32//! # Ok::<(), serde_json::Error>(())
33//! ```
34
35use serde::{Deserialize, Serialize};
36
37/// Represents a single streaming chunk from the chat API.
38///
39/// This struct contains a portion of the complete response that arrives
40/// as part of an SSE stream. Multiple chunks are typically received
41/// and assembled to form the complete response.
42///
43/// ## Fields
44///
45/// - `id` - Unique identifier for the streaming session (optional)
46/// - `created` - Unix timestamp when the chunk was created (optional)
47/// - `model` - Name of the model generating the response (optional)
48/// - `choices` - Array of streaming choices, usually containing one item
49/// - `usage` - Token usage statistics, typically only on final chunk
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ChatStreamResponse {
52    /// Unique identifier for the streaming session.
53    ///
54    /// May be a string or number in the wire format, converted to
55    /// `Option<String>`.
56    #[serde(
57        default,
58        skip_serializing_if = "Option::is_none",
59        deserialize_with = "super::serde_helpers::optional_string_from_number_or_string"
60    )]
61    pub id: Option<String>,
62
63    /// Unix timestamp indicating when the chunk was created.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub created: Option<u64>,
66
67    /// Name of the AI model generating the response.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub model: Option<String>,
70
71    /// Array of streaming choices, typically containing one item per chunk.
72    ///
73    /// Each choice contains a delta with partial content updates.
74    pub choices: Vec<StreamChoice>,
75
76    /// Token usage statistics.
77    ///
78    /// This field typically appears only on the final chunk of the stream,
79    /// providing information about prompt and completion token counts.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub usage: Option<crate::model::chat_base_response::Usage>,
82}
83
84/// Represents a single choice within a streaming response chunk.
85///
86/// Each choice contains a delta with incremental content updates and
87/// metadata about the generation process.
88///
89/// ## Fields
90///
91/// - `index` - Position of this choice in the results array
92/// - `delta` - Partial content update for this choice
93/// - `finish_reason` - Reason why generation stopped (on final chunk)
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct StreamChoice {
96    /// Index position of this choice in the results array.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub index: Option<i32>,
99
100    /// Delta payload containing partial content updates.
101    ///
102    /// This field contains the incremental content that should be
103    /// appended to the accumulated response.
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub delta: Option<Delta>,
106
107    /// Reason why the generation process finished.
108    ///
109    /// This field typically appears only on the final chunk of a choice,
110    /// indicating why generation stopped (e.g., "stop", "length", etc.).
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub finish_reason: Option<String>,
113}
114
115/// Represents incremental content updates in streaming responses.
116///
117/// The delta contains partial content that should be appended to the
118/// accumulated response. Different fields may be present depending on
119/// the chunk type and model capabilities.
120///
121/// ## Fields
122///
123/// - `role` - Message role, typically "assistant" on first chunk
124/// - `content` - Partial text content to append
125/// - `reasoning_content` - Reasoning traces for thinking models
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Delta {
128    /// Role of the message sender.
129    ///
130    /// Typically "assistant" on the first chunk of a response,
131    /// may be omitted on subsequent chunks.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub role: Option<String>,
134
135    /// Partial text content that should be appended to the response.
136    ///
137    /// This field contains the incremental text content for the current chunk.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub content: Option<String>,
140
141    /// Reasoning content for models with thinking capabilities.
142    ///
143    /// This field contains step-by-step reasoning traces when the model
144    /// is operating in thinking mode with reasoning enabled.
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub reasoning_content: Option<String>,
147
148    /// Streaming tool call payload for tool invocation.
149    ///
150    /// When `tool_stream` is enabled, the provider emits partially populated
151    /// tool calls whose names and JSON-string arguments can arrive in separate
152    /// chunks.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub tool_calls: Option<Vec<StreamToolCall>>,
155}
156
157/// One incremental function call in a streaming delta.
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct StreamToolCall {
160    /// Position of this call in the assistant's tool-call list.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub index: Option<i32>,
163    /// Tool-call identifier, usually present in the first delta for a call.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub id: Option<String>,
166    /// Tool kind; the frozen streaming schema currently defines only function.
167    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
168    pub type_: Option<StreamToolCallType>,
169    /// Incremental function payload.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub function: Option<StreamToolFunction>,
172}
173
174/// Tool kind accepted by the frozen chat-stream schema.
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "lowercase")]
177pub enum StreamToolCallType {
178    /// A function invocation.
179    Function,
180}
181
182/// Incremental function fields carried by a streaming tool call.
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct StreamToolFunction {
185    /// Function name; omitted after the initial delta.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub name: Option<String>,
188    /// JSON argument fragment; omitted when this delta carries other metadata.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub arguments: Option<String>,
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn chunk_accepts_an_omitted_id() {
199        let chunk: ChatStreamResponse = serde_json::from_str(r#"{"choices":[]}"#).unwrap();
200        assert!(chunk.id.is_none());
201    }
202
203    #[test]
204    fn incremental_tool_function_fields_may_arrive_separately() {
205        let chunk: ChatStreamResponse = serde_json::from_value(serde_json::json!({
206            "choices": [{
207                "delta": {
208                    "tool_calls": [
209                        {"index": 0, "type": "function", "function": {"name": "lookup"}},
210                        {"index": 0, "function": {"arguments": "{\"city\":"}}
211                    ]
212                }
213            }]
214        }))
215        .unwrap();
216        let calls = chunk.choices[0]
217            .delta
218            .as_ref()
219            .unwrap()
220            .tool_calls
221            .as_ref()
222            .unwrap();
223        assert_eq!(
224            calls[0].function.as_ref().unwrap().name.as_deref(),
225            Some("lookup")
226        );
227        assert!(calls[1].function.as_ref().unwrap().name.is_none());
228    }
229}