1pub mod streaming {
5 use serde::Deserialize;
8
9 use crate::chat::ServiceTier;
10
11 #[derive(Debug, Deserialize, Clone)]
12 pub struct ChatCompletionChunk {
13 pub id: String,
15 pub choices: Vec<CompletionChunkChoice>,
19 pub created: u64,
22 pub model: String,
24 pub object: ChatCompletionChunkObject,
26 pub service_tier: Option<ServiceTier>,
33 pub system_fingerprint: Option<String>,
37 pub usage: Option<CompletionUsage>,
45 pub moderation: Option<crate::chat::ChatModeration>,
50 }
51
52 #[derive(Debug, Deserialize, Clone)]
53 pub enum ChatCompletionChunkObject {
54 #[serde(rename = "chat.completion.chunk")]
55 ChatCompletionChunk,
56 }
57
58 #[derive(Debug, Deserialize, Clone)]
59 pub struct CompletionChunkChoice {
60 pub delta: ChoiceDelta,
62 pub index: u32,
64 pub logprobs: Option<ChoiceLogprobs>,
66 pub finish_reason: Option<FinishReason>,
74 }
75
76 #[derive(Debug, Deserialize, Clone)]
77 #[serde(rename_all = "snake_case")]
78 pub enum FinishReason {
79 Length,
81 Stop,
83 ContentFilter,
85 FunctionCall,
87 ToolCalls,
89 #[cfg(feature = "deepseek")]
92 InsufficientSystemResource,
93 }
94
95 #[derive(Debug, Deserialize, Clone)]
96 pub struct ChoiceDelta {
97 pub content: Option<String>,
99 #[cfg(feature = "deepseek")]
102 pub reasoning_content: Option<String>,
103 pub function_call: Option<ChoiceDeltaFunctionCall>,
108 pub refusal: Option<String>,
110 pub role: Option<CompletionRole>,
112 pub tool_calls: Option<Vec<ChoiceDeltaToolCall>>,
114 #[cfg(feature = "azure")]
123 pub annotations: Option<Vec<crate::chat::Annotation>>,
124 #[cfg(feature = "azure")]
131 pub audio: Option<crate::chat::ChatCompletionAudio>,
132 }
133
134 #[derive(Debug, Deserialize, Clone)]
135 pub struct ChoiceDeltaToolCallFunction {
136 pub arguments: Option<String>,
141 pub name: Option<String>,
143 }
144
145 #[derive(Debug, Deserialize, Clone)]
146 pub struct ChoiceDeltaFunctionCall {
147 pub arguments: Option<String>,
152 pub name: Option<String>,
154 }
155
156 #[derive(Debug, Deserialize, Clone)]
157 pub struct ChoiceDeltaToolCall {
158 pub index: usize,
160 pub id: Option<String>,
162 pub function: Option<ChoiceDeltaToolCallFunction>,
164 #[serde(rename = "type")]
166 pub type_: Option<ChoiceDeltaToolCallType>,
167 }
168
169 #[derive(Debug, Deserialize, Clone)]
170 #[serde(rename_all = "snake_case")]
171 pub enum ChoiceDeltaToolCallType {
172 Function,
173 }
174
175 #[derive(Debug, Deserialize, Clone)]
176 #[serde(rename_all = "snake_case")]
177 pub enum CompletionRole {
178 Assistant,
179 Developer,
180 System,
181 Tool,
182 User,
183 }
184
185 #[derive(Debug, Deserialize, Clone)]
187 pub struct ChoiceLogprobs {
188 pub content: Option<Vec<LogprobeContent>>,
190 #[cfg(feature = "deepseek")]
193 pub reasoning_content: Option<Vec<LogprobeContent>>,
194 pub refusal: Option<Vec<LogprobeContent>>,
196 }
197
198 #[derive(Debug, Deserialize, Clone)]
200 pub struct LogprobeContent {
201 pub token: String,
202 pub logprob: f32,
203 pub bytes: Option<Vec<u8>>,
204 pub top_logprobs: Vec<TopLogprob>,
205 }
206
207 #[derive(Debug, Deserialize, Clone)]
210 pub struct TopLogprob {
211 pub token: String,
212 pub logprob: f32,
213 pub bytes: Option<Vec<u8>>,
214 }
215
216 #[derive(Debug, Deserialize, Clone)]
217 pub struct CompletionUsage {
218 pub completion_tokens: usize,
220 pub prompt_tokens: usize,
222
223 #[cfg(feature = "deepseek")]
225 pub prompt_cache_hit_tokens: Option<usize>,
226 #[cfg(feature = "deepseek")]
228 pub prompt_cache_miss_tokens: Option<usize>,
229
230 pub total_tokens: usize,
232 pub completion_tokens_details: Option<CompletionTokensDetails>,
234 pub prompt_tokens_details: Option<PromptTokensDetails>,
236 }
237
238 #[derive(Debug, Deserialize, Clone)]
239 pub struct CompletionTokensDetails {
240 pub accepted_prediction_tokens: Option<usize>,
243 pub audio_tokens: Option<usize>,
245 pub reasoning_tokens: Option<usize>,
247 pub rejected_prediction_tokens: Option<usize>,
252 }
253
254 #[derive(Debug, Deserialize, Clone)]
255 pub struct PromptTokensDetails {
256 pub audio_tokens: Option<usize>,
258 pub cached_tokens: Option<usize>,
260 }
261
262 crate::impl_from_str!(ChatCompletionChunk);
263
264 #[cfg(test)]
265 mod test {
266 use std::str::FromStr;
267
268 use super::*;
269
270 #[test]
271 fn streaming_example_deepseek() {
272 let streams = vec![
273 r#"{"id": "1f633d8bfc032625086f14113c411638", "choices": [{"index": 0, "delta": {"content": "", "role": "assistant"}, "finish_reason": null, "logprobs": null}], "created": 1718345013, "model": "deepseek-chat", "system_fingerprint": "fp_a49d71b8a1", "object": "chat.completion.chunk", "usage": null}"#,
274 r#"{"choices": [{"delta": {"content": "Hello", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
275 r#"{"choices": [{"delta": {"content": "!", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
276 r#"{"choices": [{"delta": {"content": " How", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
277 r#"{"choices": [{"delta": {"content": " can", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
278 r#"{"choices": [{"delta": {"content": " I", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
279 r#"{"choices": [{"delta": {"content": " assist", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
280 r#"{"choices": [{"delta": {"content": " you", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
281 r#"{"choices": [{"delta": {"content": " today", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
282 r#"{"choices": [{"delta": {"content": "?", "role": "assistant"}, "finish_reason": null, "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1"}"#,
283 r#"{"choices": [{"delta": {"content": "", "role": null}, "finish_reason": "stop", "index": 0, "logprobs": null}], "created": 1718345013, "id": "1f633d8bfc032625086f14113c411638", "model": "deepseek-chat", "object": "chat.completion.chunk", "system_fingerprint": "fp_a49d71b8a1", "usage": {"completion_tokens": 9, "prompt_tokens": 17, "total_tokens": 26}}"#,
284 ];
285
286 for stream in streams {
287 let parsed = ChatCompletionChunk::from_str(stream);
288 match parsed {
289 Ok(completion) => {
290 println!("Deserialized: {:#?}", completion);
291 }
292 Err(e) => {
293 panic!("Failed to deserialize {}: {}", stream, e);
294 }
295 }
296 }
297 }
298
299 #[test]
300 fn streaming_example_qwen() {
301 let streams = vec![
302 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"","function_call":null,"refusal":null,"role":"assistant","tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
303 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"我是","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
304 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"来自","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
305 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"阿里","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
306 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"云的超大规模","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
307 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"语言","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
308 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"模型","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
309 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"。","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
310 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"问。","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
311 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[{"delta":{"content":"","function_call":null,"refusal":null,"role":null,"tool_calls":null},"finish_reason":"stop","index":0,"logprobs":null}],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":null}"#,
312 r#"{"id":"chatcmpl-e30f5ae7-3063-93c4-90fe-beb5f900bd57","choices":[],"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","service_tier":null,"system_fingerprint":null,"usage":{"completion_tokens":17,"prompt_tokens":22,"total_tokens":39,"completion_tokens_details":null,"prompt_tokens_details":{"audio_tokens":null,"cached_tokens":0}}}"#,
313 ];
314
315 for stream in streams {
316 let parsed = ChatCompletionChunk::from_str(stream);
317 match parsed {
318 Ok(completion) => {
319 println!("Deserialized: {:#?}", completion);
320 }
321 Err(e) => {
322 panic!("Failed to deserialize {}: {}", stream, e);
323 }
324 }
325 }
326 }
327
328 #[cfg(feature = "azure")]
333 #[test]
334 fn streaming_example_azure_annotations_and_audio() {
335 let chunk = ChatCompletionChunk::from_str(
336 r#"{"id":"chatcmpl-abc","choices":[{"delta":{"content":"According to the doc","annotations":[{"type":"url_citation","url_citation":{"start_index":0,"end_index":20,"title":"Azure Docs","url":"https://learn.microsoft.com/azure"}}],"audio":{"id":"audio_abc","data":"SGVsbG8=","expires_at":1735113344,"transcript":"Hello"}},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"gpt-4o","object":"chat.completion.chunk","system_fingerprint":"fp_abc"}"#,
337 )
338 .expect("chunk with annotations and audio must deserialize");
339
340 let delta = &chunk.choices[0].delta;
341 let annotations = delta.annotations.as_ref().expect("annotations");
342 assert_eq!(annotations.len(), 1);
343 assert_eq!(annotations[0].url_citation.title, "Azure Docs");
344 assert_eq!(
345 annotations[0].url_citation.url,
346 "https://learn.microsoft.com/azure"
347 );
348
349 let audio = delta.audio.as_ref().expect("audio");
350 assert_eq!(audio.id, "audio_abc");
351 assert_eq!(audio.transcript, "Hello");
352 }
353 }
354}
355
356pub mod no_streaming {
357 pub type ChatCompletion = crate::chat::ChatCompletion;
362}