openai_interface/chat/
response.rs

1pub mod streaming {
2    use serde::Deserialize;
3
4    use crate::errors::ResponseError;
5
6    #[derive(Debug, Deserialize)]
7    pub struct Completion {
8        pub id: String,
9        pub choices: Vec<CompletionChoice>,
10        pub created: u64,
11        pub model: String,
12        pub object: String,
13        pub usage: Option<CompletionUsage>,
14    }
15
16    #[derive(Debug, Deserialize)]
17    pub struct CompletionChoice {
18        pub delta: CompletionDelta,
19        pub index: u32,
20        pub logprobs: Option<ChoiceLogprobs>,
21        pub finish_reason: Option<FinishReason>,
22    }
23
24    #[derive(Debug, Deserialize)]
25    #[serde(rename_all = "snake_case")]
26    pub enum FinishReason {
27        Length,
28        Stop,
29        ContentFilter,
30        ToolCalls,
31        InsufficientSystemResource,
32    }
33
34    #[derive(Debug, Deserialize)]
35    pub struct CompletionDelta {
36        #[serde(flatten)]
37        pub content: CompletionContent,
38        pub role: Option<CompletionRole>,
39    }
40
41    #[derive(Debug, Deserialize)]
42    #[serde(rename_all = "snake_case")]
43    pub enum CompletionRole {
44        User,
45        Assistant,
46        System,
47        Tool,
48    }
49
50    #[derive(Debug, Deserialize)]
51    #[serde(rename_all = "snake_case")]
52    pub enum CompletionContent {
53        Content(String),
54        ReasoningContent(String),
55    }
56
57    #[derive(Debug, Deserialize)]
58    #[serde(rename_all = "snake_case")]
59    pub enum ChoiceLogprobs {
60        Content(Vec<LogprobeContent>),
61        ReasoningContent(Vec<LogprobeContent>),
62    }
63
64    #[derive(Debug, Deserialize)]
65    pub struct LogprobeContent {
66        pub token: String,
67        pub logprob: f32,
68        pub bytes: Option<Vec<u8>>,
69        pub top_logprobs: Vec<TopLogprob>,
70    }
71
72    #[derive(Debug, Deserialize)]
73    pub struct TopLogprob {
74        pub token: String,
75        pub logprob: f32,
76        pub bytes: Option<Vec<u8>>,
77    }
78
79    #[derive(Debug, Deserialize)]
80    pub struct CompletionUsage {
81        pub completion_tokens: usize,
82        pub prompt_tokens: usize,
83        pub total_tokens: usize,
84    }
85
86    impl Completion {
87        pub fn parse_string(content: &str) -> Result<Self, crate::errors::ResponseError> {
88            let parse_result: Result<Completion, _> = serde_json::from_str(content)
89                .map_err(|e| ResponseError::DeserializationError(e.to_string()));
90            parse_result
91        }
92    }
93
94    #[cfg(test)]
95    mod test {
96        use super::*;
97
98        #[test]
99        fn streaming_example_deepseek() {
100            let streams = vec![
101                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}"#,
102                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"}"#,
103                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"}"#,
104                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"}"#,
105                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"}"#,
106                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"}"#,
107                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"}"#,
108                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"}"#,
109                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"}"#,
110                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"}"#,
111                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}}"#,
112            ];
113
114            for stream in streams {
115                let parsed = Completion::parse_string(stream);
116                match parsed {
117                    Ok(completion) => {
118                        println!("Deserialized: {:#?}", completion);
119                    }
120                    Err(e) => {
121                        panic!("Failed to deserialize {}: {}", stream, e);
122                    }
123                }
124            }
125        }
126
127        #[test]
128        fn streaming_example_qwen() {
129            let streams = vec![
130                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}"#,
131                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}"#,
132                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}"#,
133                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}"#,
134                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}"#,
135                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}"#,
136                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}"#,
137                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}"#,
138                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}"#,
139                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}}}"#,
140            ];
141
142            for stream in streams {
143                let parsed = Completion::parse_string(stream);
144                match parsed {
145                    Ok(completion) => {
146                        println!("Deserialized: {:#?}", completion);
147                    }
148                    Err(e) => {
149                        panic!("Failed to deserialize {}: {}", stream, e);
150                    }
151                }
152            }
153        }
154    }
155}
156
157pub mod no_streaming {
158    use serde::Deserialize;
159
160    use crate::errors::ResponseError;
161
162    #[derive(Debug, Deserialize)]
163    pub struct Completion {
164        pub id: String,
165        pub choices: Vec<ResponseChoice>,
166        pub created: u64,
167        pub model: String,
168        pub system_fingerprint: String,
169        pub object: String,
170        pub usage: CompletionUsage,
171    }
172
173    #[derive(Debug, Deserialize)]
174    pub struct ResponseChoice {
175        pub finish_reason: FinishReason,
176        pub index: usize,
177        pub message: ResponseMessage,
178        pub logprobs: Option<ResponseLogprobs>,
179    }
180
181    #[derive(Debug, Deserialize, PartialEq)]
182    #[serde(rename_all = "snake_case")]
183    pub enum FinishReason {
184        Length,
185        Stop,
186        ContentFilter,
187        InsufficientSystemResource,
188    }
189
190    #[derive(Debug, Deserialize)]
191    pub struct ResponseMessage {
192        /// This shall always be ResponseRole::Assistant
193        pub role: ResponseRole,
194        pub content: Option<String>,
195        pub reasoning_content: Option<String>,
196        /// Tool calls deserialization is not supported yet.
197        pub tool_calls: Option<String>,
198    }
199
200    #[derive(Debug, Deserialize)]
201    #[serde(rename_all = "snake_case")]
202    pub enum ResponseRole {
203        User,
204        Assistant,
205        System,
206        Tool,
207    }
208
209    #[derive(Debug, Deserialize)]
210    pub struct ResponseLogprobs {
211        pub content: Vec<LogProb>,
212        pub reasoning_content: Vec<LogProb>,
213    }
214
215    #[derive(Debug, Deserialize)]
216    pub struct LogProb {
217        pub token: String,
218        pub logprob: f32,
219        pub bytes: Option<Vec<u8>>,
220        pub top_logprobs: Vec<TopLogprob>,
221    }
222
223    #[derive(Debug, Deserialize)]
224    pub struct TopLogprob {
225        pub token: String,
226        pub logprob: f32,
227        pub bytes: Option<Vec<u8>>,
228    }
229
230    #[derive(Debug, Deserialize)]
231    pub struct CompletionUsage {
232        pub completion_tokens: usize,
233        pub prompt_tokens: usize,
234
235        // These two fields seem to be DeepSeek specific.
236        pub prompt_cache_hit_tokens: Option<usize>,
237        pub prompt_cache_miss_tokens: Option<usize>,
238
239        pub total_tokens: usize,
240        pub completion_tokens_details: Option<CompletionTokensDetails>,
241    }
242
243    #[derive(Debug, Deserialize)]
244    pub struct CompletionTokensDetails {
245        pub reasoning_tokens: usize,
246    }
247
248    impl Completion {
249        pub fn parse_string(content: &str) -> Result<Self, crate::errors::ResponseError> {
250            let parse_result: Result<Completion, _> = serde_json::from_str(content)
251                .map_err(|e| ResponseError::DeserializationError(e.to_string()));
252            parse_result
253        }
254    }
255
256    #[cfg(test)]
257    mod test {
258
259        #[test]
260        fn no_streaming_example_deepseek() {
261            let json = r#"{
262              "id": "30f6413a-a827-4cf3-9898-f13a8634b798",
263              "object": "chat.completion",
264              "created": 1757944111,
265              "model": "deepseek-chat",
266              "choices": [
267                {
268                  "index": 0,
269                  "message": {
270                    "role": "assistant",
271                    "content": "Hello! How can I help you today? 😊"
272                  },
273                  "logprobs": null,
274                  "finish_reason": "stop"
275                }
276              ],
277              "usage": {
278                "prompt_tokens": 10,
279                "completion_tokens": 11,
280                "total_tokens": 21,
281                "prompt_tokens_details": {
282                  "cached_tokens": 0
283                },
284                "prompt_cache_hit_tokens": 0,
285                "prompt_cache_miss_tokens": 10
286              },
287              "system_fingerprint": "fp_08f168e49b_prod0820_fp8_kvcache"
288            }"#;
289
290            let parsed = super::Completion::parse_string(json);
291            match parsed {
292                Ok(_) => {}
293                Err(e) => {
294                    panic!("Failed to deserialize: {}", e);
295                }
296            }
297        }
298    }
299}