1pub mod streaming {
5 use std::str::FromStr;
8
9 use serde::Deserialize;
10
11 use crate::{chat::ServiceTier, errors::OapiError};
12
13 #[derive(Debug, Deserialize, Clone)]
14 pub struct ChatCompletionChunk {
15 pub id: String,
17 pub choices: Vec<CompletionChunkChoice>,
21 pub created: u64,
24 pub model: String,
26 pub object: ChatCompletionChunkObject,
28 pub service_tier: Option<ServiceTier>,
32 pub system_fingerprint: Option<String>,
36 pub usage: Option<CompletionUsage>,
44 }
45
46 #[derive(Debug, Deserialize, Clone)]
47 pub enum ChatCompletionChunkObject {
48 #[serde(rename = "chat.completion.chunk")]
49 ChatCompletionChunk,
50 }
51
52 #[derive(Debug, Deserialize, Clone)]
53 pub struct CompletionChunkChoice {
54 pub delta: ChoiceDelta,
56 pub index: u32,
58 pub logprobs: Option<ChoiceLogprobs>,
60 pub finish_reason: Option<FinishReason>,
68 }
69
70 #[derive(Debug, Deserialize, Clone)]
71 #[serde(rename_all = "snake_case")]
72 pub enum FinishReason {
73 Length,
75 Stop,
77 ContentFilter,
79 FunctionCall,
81 ToolCalls,
83 InsufficientSystemResource,
85 }
86
87 #[derive(Debug, Deserialize, Clone)]
88 pub struct ChoiceDelta {
89 #[serde(flatten)]
91 pub content: Option<CompletionContent>,
92 pub function_call: Option<ChoiceDeltaFunctionCall>,
97 pub refusal: Option<String>,
99 pub role: Option<CompletionRole>,
101 pub tool_calls: Option<Vec<ChoiceDeltaToolCall>>,
103 }
104
105 #[derive(Debug, Deserialize, Clone)]
106 pub struct ChoiceDeltaToolCallFunction {
107 pub arguments: Option<String>,
112 pub name: Option<String>,
114 }
115
116 #[derive(Debug, Deserialize, Clone)]
117 pub struct ChoiceDeltaFunctionCall {
118 pub arguments: Option<String>,
123 pub name: Option<String>,
125 }
126
127 #[derive(Debug, Deserialize, Clone)]
128 pub struct ChoiceDeltaToolCall {
129 pub index: usize,
131 pub id: Option<String>,
133 pub function: Option<ChoiceDeltaToolCallFunction>,
135 #[serde(rename = "type")]
137 pub type_: Option<ChoiceDeltaToolCallType>,
138 }
139
140 #[derive(Debug, Deserialize, Clone)]
141 #[serde(rename_all = "snake_case")]
142 pub enum ChoiceDeltaToolCallType {
143 Function,
144 }
145
146 #[derive(Debug, Deserialize, Clone)]
147 #[serde(rename_all = "snake_case")]
148 pub enum CompletionRole {
149 Assistant,
150 Developer,
151 System,
152 Tool,
153 User,
154 }
155
156 #[derive(Debug, Deserialize, Clone)]
157 #[serde(rename_all = "snake_case")]
158 pub enum CompletionContent {
159 Content(String),
160 ReasoningContent(String),
162 }
163
164 #[derive(Debug, Deserialize, Clone)]
165 #[serde(rename_all = "snake_case")]
166 pub enum ChoiceLogprobs {
167 Content(Vec<LogprobeContent>),
168 ReasoningContent(Vec<LogprobeContent>),
170 }
171
172 #[derive(Debug, Deserialize, Clone)]
174 pub struct LogprobeContent {
175 pub token: String,
176 pub logprob: f32,
177 pub bytes: Option<Vec<u8>>,
178 pub top_logprobs: Vec<TopLogprob>,
179 }
180
181 #[derive(Debug, Deserialize, Clone)]
184 pub struct TopLogprob {
185 pub token: String,
186 pub logprob: f32,
187 pub bytes: Option<Vec<u8>>,
188 }
189
190 #[derive(Debug, Deserialize, Clone)]
191 pub struct CompletionUsage {
192 pub completion_tokens: usize,
194 pub prompt_tokens: usize,
196
197 pub prompt_cache_hit_tokens: Option<usize>,
200 pub prompt_cache_miss_tokens: Option<usize>,
202
203 pub total_tokens: usize,
205 pub completion_tokens_details: Option<CompletionTokensDetails>,
207 pub prompt_tokens_details: Option<PromptTokensDetails>,
209 }
210
211 #[derive(Debug, Deserialize, Clone)]
212 pub struct CompletionTokensDetails {
213 pub accepted_prediction_tokens: Option<usize>,
216 pub audio_tokens: Option<usize>,
218 pub reasoning_tokens: Option<usize>,
220 pub rejected_prediction_tokens: Option<usize>,
225 }
226
227 #[derive(Debug, Deserialize, Clone)]
228 pub struct PromptTokensDetails {
229 pub audio_tokens: Option<usize>,
231 pub cached_tokens: Option<usize>,
233 }
234
235 impl FromStr for ChatCompletionChunk {
236 type Err = crate::errors::OapiError;
237
238 fn from_str(content: &str) -> Result<Self, Self::Err> {
239 let parse_result: Result<ChatCompletionChunk, _> = serde_json::from_str(content)
240 .map_err(|e| OapiError::DeserializationError(e.to_string()));
241 parse_result
242 }
243 }
244
245 #[cfg(test)]
246 mod test {
247 use super::*;
248
249 #[test]
250 fn streaming_example_deepseek() {
251 let streams = vec![
252 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}"#,
253 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"}"#,
254 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"}"#,
255 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"}"#,
256 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"}"#,
257 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"}"#,
258 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"}"#,
259 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"}"#,
260 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"}"#,
261 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"}"#,
262 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}}"#,
263 ];
264
265 for stream in streams {
266 let parsed = ChatCompletionChunk::from_str(stream);
267 match parsed {
268 Ok(completion) => {
269 println!("Deserialized: {:#?}", completion);
270 }
271 Err(e) => {
272 panic!("Failed to deserialize {}: {}", stream, e);
273 }
274 }
275 }
276 }
277
278 #[test]
279 fn streaming_example_qwen() {
280 let streams = vec![
281 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}"#,
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":null,"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":[{"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}"#,
286 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}"#,
287 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}"#,
288 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}"#,
289 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}"#,
290 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}}}"#,
291 ];
292
293 for stream in streams {
294 let parsed = ChatCompletionChunk::from_str(stream);
295 match parsed {
296 Ok(completion) => {
297 println!("Deserialized: {:#?}", completion);
298 }
299 Err(e) => {
300 panic!("Failed to deserialize {}: {}", stream, e);
301 }
302 }
303 }
304 }
305 }
306}
307
308pub mod no_streaming {
309 pub type ChatCompletion = crate::chat::ChatCompletion;
314}