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}