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//! ## Usage
24//!
25//! ```rust,ignore
26//! let mut client = ChatCompletion::new(model, messages, api_key).enable_stream();
27//! client.stream_for_each(|chunk| async move {
28//!     if let Some(delta) = &chunk.choices[0].delta {
29//!         if let Some(content) = &delta.content {
30//!             print!("{}", content);
31//!         }
32//!     }
33//!     Ok(())
34//! }).await?;
35//! ```
36
37use serde::{Deserialize, Deserializer, Serialize};
38
39/// Custom deserializer that accepts strings or numbers, converting to
40/// Option<String>.
41///
42/// This helper function handles the wire format flexibility where IDs may be
43/// transmitted as either strings or numbers, normalizing them to
44/// Option<String>.
45///
46/// ## Supported Formats
47///
48/// - `null` → `None`
49/// - `"string_id"` → `Some("string_id")`
50/// - `123` → `Some("123")`
51/// - Other types → deserialization error
52fn de_opt_string_from_number_or_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
53where
54    D: Deserializer<'de>,
55{
56    let v = serde_json::Value::deserialize(deserializer)?;
57    match v {
58        serde_json::Value::Null => Ok(None),
59        serde_json::Value::String(s) => Ok(Some(s)),
60        serde_json::Value::Number(n) => Ok(Some(n.to_string())),
61        other => Err(serde::de::Error::custom(format!(
62            "expected string or number, got {}",
63            other
64        ))),
65    }
66}
67
68/// Represents a single streaming chunk from the chat API.
69///
70/// This struct contains a portion of the complete response that arrives
71/// as part of an SSE stream. Multiple chunks are typically received
72/// and assembled to form the complete response.
73///
74/// ## Fields
75///
76/// - `id` - Unique identifier for the streaming session (optional)
77/// - `created` - Unix timestamp when the chunk was created (optional)
78/// - `model` - Name of the model generating the response (optional)
79/// - `choices` - Array of streaming choices, usually containing one item
80/// - `usage` - Token usage statistics, typically only on final chunk
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ChatStreamResponse {
83    /// Unique identifier for the streaming session.
84    ///
85    /// May be a string or number in the wire format, converted to
86    /// `Option<String>`.
87    #[serde(
88        skip_serializing_if = "Option::is_none",
89        deserialize_with = "de_opt_string_from_number_or_string"
90    )]
91    pub id: Option<String>,
92
93    /// Unix timestamp indicating when the chunk was created.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub created: Option<u64>,
96
97    /// Name of the AI model generating the response.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub model: Option<String>,
100
101    /// Array of streaming choices, typically containing one item per chunk.
102    ///
103    /// Each choice contains a delta with partial content updates.
104    pub choices: Vec<StreamChoice>,
105
106    /// Token usage statistics.
107    ///
108    /// This field typically appears only on the final chunk of the stream,
109    /// providing information about prompt and completion token counts.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub usage: Option<crate::model::chat_base_response::Usage>,
112}
113
114/// Represents a single choice within a streaming response chunk.
115///
116/// Each choice contains a delta with incremental content updates and
117/// metadata about the generation process.
118///
119/// ## Fields
120///
121/// - `index` - Position of this choice in the results array
122/// - `delta` - Partial content update for this choice
123/// - `finish_reason` - Reason why generation stopped (on final chunk)
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct StreamChoice {
126    /// Index position of this choice in the results array.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub index: Option<i32>,
129
130    /// Delta payload containing partial content updates.
131    ///
132    /// This field contains the incremental content that should be
133    /// appended to the accumulated response.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub delta: Option<Delta>,
136
137    /// Reason why the generation process finished.
138    ///
139    /// This field typically appears only on the final chunk of a choice,
140    /// indicating why generation stopped (e.g., "stop", "length", etc.).
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub finish_reason: Option<String>,
143}
144
145/// Represents incremental content updates in streaming responses.
146///
147/// The delta contains partial content that should be appended to the
148/// accumulated response. Different fields may be present depending on
149/// the chunk type and model capabilities.
150///
151/// ## Fields
152///
153/// - `role` - Message role, typically "assistant" on first chunk
154/// - `content` - Partial text content to append
155/// - `reasoning_content` - Reasoning traces for thinking models
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Delta {
158    /// Role of the message sender.
159    ///
160    /// Typically "assistant" on the first chunk of a response,
161    /// may be omitted on subsequent chunks.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub role: Option<String>,
164
165    /// Partial text content that should be appended to the response.
166    ///
167    /// This field contains the incremental text content for the current chunk.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub content: Option<String>,
170
171    /// Reasoning content for models with thinking capabilities.
172    ///
173    /// This field contains step-by-step reasoning traces when the model
174    /// is operating in thinking mode with reasoning enabled.
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub reasoning_content: Option<String>,
177
178    /// Streaming tool call payload for tool invocation.
179    ///
180    /// When `tool_stream` is enabled and the model emits tool calling
181    /// information, providers often stream this as an array of objects with
182    /// partial fields. Use a flexible Value here to accept
183    /// strings/arrays/objects without failing deserialization on type
184    /// mismatch across increments.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub tool_calls: Option<Vec<crate::model::chat_base_response::ToolCallMessage>>,
187}