1pub mod streaming {
2 use std::str::FromStr;
3
4 use serde::Deserialize;
5
6 use crate::{chat::ServiceTier, errors::OapiError};
7
8 #[derive(Debug, Deserialize, Clone)]
9 pub struct ChatCompletionChunk {
10 pub id: String,
12 pub choices: Vec<CompletionChunkChoice>,
16 pub created: u64,
19 pub model: String,
21 pub object: ChatCompletionChunkObject,
23 pub service_tier: Option<ServiceTier>,
27 pub system_fingerprint: Option<String>,
31 pub usage: Option<CompletionUsage>,
39 }
40
41 #[derive(Debug, Deserialize, Clone)]
42 pub enum ChatCompletionChunkObject {
43 #[serde(rename = "chat.completion.chunk")]
44 ChatCompletionChunk,
45 }
46
47 #[derive(Debug, Deserialize, Clone)]
48 pub struct CompletionChunkChoice {
49 pub delta: ChoiceDelta,
51 pub index: u32,
53 pub logprobs: Option<ChoiceLogprobs>,
55 pub finish_reason: Option<FinishReason>,
63 }
64
65 #[derive(Debug, Deserialize, Clone)]
66 #[serde(rename_all = "snake_case")]
67 pub enum FinishReason {
68 Length,
70 Stop,
72 ContentFilter,
74 FunctionCall,
76 ToolCalls,
78 InsufficientSystemResource,
80 }
81
82 #[derive(Debug, Deserialize, Clone)]
83 pub struct ChoiceDelta {
84 #[serde(flatten)]
86 pub content: Option<CompletionContent>,
87 pub function_call: Option<ChoiceDeltaFunctionCall>,
92 pub refusal: Option<String>,
94 pub role: Option<CompletionRole>,
96 pub tool_calls: Option<Vec<ChoiceDeltaToolCall>>,
98 }
99
100 #[derive(Debug, Deserialize, Clone)]
101 pub struct ChoiceDeltaToolCallFunction {
102 pub arguments: Option<String>,
107 pub name: Option<String>,
109 }
110
111 #[derive(Debug, Deserialize, Clone)]
112 pub struct ChoiceDeltaFunctionCall {
113 pub arguments: Option<String>,
118 pub name: Option<String>,
120 }
121
122 #[derive(Debug, Deserialize, Clone)]
123 pub struct ChoiceDeltaToolCall {
124 pub index: usize,
126 pub id: Option<String>,
128 pub function: Option<ChoiceDeltaToolCallFunction>,
130 #[serde(rename = "type")]
132 pub type_: Option<ChoiceDeltaToolCallType>,
133 }
134
135 #[derive(Debug, Deserialize, Clone)]
136 #[serde(rename_all = "snake_case")]
137 pub enum ChoiceDeltaToolCallType {
138 Function,
139 }
140
141 #[derive(Debug, Deserialize, Clone)]
142 #[serde(rename_all = "snake_case")]
143 pub enum CompletionRole {
144 Assistant,
145 Developer,
146 System,
147 Tool,
148 User,
149 }
150
151 #[derive(Debug, Deserialize, Clone)]
152 #[serde(rename_all = "snake_case")]
153 pub enum CompletionContent {
154 Content(String),
155 ReasoningContent(String),
157 }
158
159 #[derive(Debug, Deserialize, Clone)]
160 #[serde(rename_all = "snake_case")]
161 pub enum ChoiceLogprobs {
162 Content(Vec<LogprobeContent>),
163 ReasoningContent(Vec<LogprobeContent>),
165 }
166
167 #[derive(Debug, Deserialize, Clone)]
169 pub struct LogprobeContent {
170 pub token: String,
171 pub logprob: f32,
172 pub bytes: Option<Vec<u8>>,
173 pub top_logprobs: Vec<TopLogprob>,
174 }
175
176 #[derive(Debug, Deserialize, Clone)]
179 pub struct TopLogprob {
180 pub token: String,
181 pub logprob: f32,
182 pub bytes: Option<Vec<u8>>,
183 }
184
185 #[derive(Debug, Deserialize, Clone)]
186 pub struct CompletionUsage {
187 pub completion_tokens: usize,
189 pub prompt_tokens: usize,
191
192 pub prompt_cache_hit_tokens: Option<usize>,
195 pub prompt_cache_miss_tokens: Option<usize>,
197
198 pub total_tokens: usize,
200 pub completion_tokens_details: Option<CompletionTokensDetails>,
202 pub prompt_tokens_details: Option<PromptTokensDetails>,
204 }
205
206 #[derive(Debug, Deserialize, Clone)]
207 pub struct CompletionTokensDetails {
208 pub accepted_prediction_tokens: Option<usize>,
211 pub audio_tokens: Option<usize>,
213 pub reasoning_tokens: Option<usize>,
215 pub rejected_prediction_tokens: Option<usize>,
220 }
221
222 #[derive(Debug, Deserialize, Clone)]
223 pub struct PromptTokensDetails {
224 pub audio_tokens: Option<usize>,
226 pub cached_tokens: Option<usize>,
228 }
229
230 impl FromStr for ChatCompletionChunk {
231 type Err = crate::errors::OapiError;
232
233 fn from_str(content: &str) -> Result<Self, Self::Err> {
234 let parse_result: Result<ChatCompletionChunk, _> = serde_json::from_str(content)
235 .map_err(|e| OapiError::DeserializationError(e.to_string()));
236 parse_result
237 }
238 }
239
240 #[cfg(test)]
241 mod test {
242 use super::*;
243
244 #[test]
245 fn streaming_example_deepseek() {
246 let streams = vec![
247 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}"#,
248 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"}"#,
249 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"}"#,
250 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"}"#,
251 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"}"#,
252 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"}"#,
253 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"}"#,
254 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"}"#,
255 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"}"#,
256 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"}"#,
257 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}}"#,
258 ];
259
260 for stream in streams {
261 let parsed = ChatCompletionChunk::from_str(stream);
262 match parsed {
263 Ok(completion) => {
264 println!("Deserialized: {:#?}", completion);
265 }
266 Err(e) => {
267 panic!("Failed to deserialize {}: {}", stream, e);
268 }
269 }
270 }
271 }
272
273 #[test]
274 fn streaming_example_qwen() {
275 let streams = vec![
276 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}"#,
277 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}"#,
278 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}"#,
279 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}"#,
280 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}"#,
281 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}"#,
282 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}"#,
283 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}"#,
284 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}"#,
285 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}}}"#,
286 ];
287
288 for stream in streams {
289 let parsed = ChatCompletionChunk::from_str(stream);
290 match parsed {
291 Ok(completion) => {
292 println!("Deserialized: {:#?}", completion);
293 }
294 Err(e) => {
295 panic!("Failed to deserialize {}: {}", stream, e);
296 }
297 }
298 }
299 }
300 }
301}
302
303pub mod no_streaming {
304 pub type ChatCompletion = crate::chat::ChatCompletion;
305}