openai_interface/chat/
response.rs

1pub mod streaming {
2    use std::str::FromStr;
3
4    use serde::Deserialize;
5
6    use crate::errors::ResponseError;
7
8    #[derive(Debug, Deserialize)]
9    pub struct ChatCompletion {
10        /// A unique identifier for the chat completion.
11        pub id: String,
12        /// A list of chat completion choices. Can be more than one
13        /// if `n` is greater than 1.
14        pub choices: Vec<CompletionChoice>,
15        /// The Unix timestamp (in seconds) of when the chat completion was created.
16        pub created: u64,
17        /// The model used for the chat completion.
18        pub model: String,
19        /// The object type, which is always `chat.completion`
20        pub object: String,
21        pub usage: Option<CompletionUsage>,
22    }
23
24    #[derive(Debug, Deserialize)]
25    pub struct CompletionChoice {
26        pub delta: CompletionDelta,
27        pub index: u32,
28        pub logprobs: Option<ChoiceLogprobs>,
29        pub finish_reason: Option<FinishReason>,
30    }
31
32    #[derive(Debug, Deserialize)]
33    #[serde(rename_all = "snake_case")]
34    pub enum FinishReason {
35        Length,
36        Stop,
37        ContentFilter,
38        ToolCalls,
39        InsufficientSystemResource,
40    }
41
42    #[derive(Debug, Deserialize)]
43    pub struct CompletionDelta {
44        #[serde(flatten)]
45        pub content: CompletionContent,
46        pub role: Option<CompletionRole>,
47    }
48
49    #[derive(Debug, Deserialize)]
50    #[serde(rename_all = "snake_case")]
51    pub enum CompletionRole {
52        User,
53        Assistant,
54        System,
55        Tool,
56    }
57
58    #[derive(Debug, Deserialize)]
59    #[serde(rename_all = "snake_case")]
60    pub enum CompletionContent {
61        Content(String),
62        ReasoningContent(String),
63    }
64
65    #[derive(Debug, Deserialize)]
66    #[serde(rename_all = "snake_case")]
67    pub enum ChoiceLogprobs {
68        Content(Vec<LogprobeContent>),
69        ReasoningContent(Vec<LogprobeContent>),
70    }
71
72    #[derive(Debug, Deserialize)]
73    pub struct LogprobeContent {
74        pub token: String,
75        pub logprob: f32,
76        pub bytes: Option<Vec<u8>>,
77        pub top_logprobs: Vec<TopLogprob>,
78    }
79
80    #[derive(Debug, Deserialize)]
81    pub struct TopLogprob {
82        pub token: String,
83        pub logprob: f32,
84        pub bytes: Option<Vec<u8>>,
85    }
86
87    #[derive(Debug, Deserialize)]
88    pub struct CompletionUsage {
89        pub completion_tokens: usize,
90        pub prompt_tokens: usize,
91        pub total_tokens: usize,
92    }
93
94    impl FromStr for ChatCompletion {
95        type Err = crate::errors::ResponseError;
96
97        fn from_str(content: &str) -> Result<Self, Self::Err> {
98            let parse_result: Result<ChatCompletion, _> = serde_json::from_str(content)
99                .map_err(|e| ResponseError::DeserializationError(e.to_string()));
100            parse_result
101        }
102    }
103
104    #[cfg(test)]
105    mod test {
106        use super::*;
107
108        #[test]
109        fn streaming_example_deepseek() {
110            let streams = vec![
111                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}"#,
112                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"}"#,
113                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"}"#,
114                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"}"#,
115                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"}"#,
116                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"}"#,
117                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"}"#,
118                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"}"#,
119                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"}"#,
120                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"}"#,
121                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}}"#,
122            ];
123
124            for stream in streams {
125                let parsed = ChatCompletion::from_str(stream);
126                match parsed {
127                    Ok(completion) => {
128                        println!("Deserialized: {:#?}", completion);
129                    }
130                    Err(e) => {
131                        panic!("Failed to deserialize {}: {}", stream, e);
132                    }
133                }
134            }
135        }
136
137        #[test]
138        fn streaming_example_qwen() {
139            let streams = vec![
140                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}"#,
141                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}"#,
142                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}"#,
143                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}"#,
144                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}"#,
145                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}"#,
146                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}"#,
147                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}"#,
148                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}"#,
149                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}}}"#,
150            ];
151
152            for stream in streams {
153                let parsed = ChatCompletion::from_str(stream);
154                match parsed {
155                    Ok(completion) => {
156                        println!("Deserialized: {:#?}", completion);
157                    }
158                    Err(e) => {
159                        panic!("Failed to deserialize {}: {}", stream, e);
160                    }
161                }
162            }
163        }
164    }
165}
166
167pub mod no_streaming {
168    use std::str::FromStr;
169
170    use serde::Deserialize;
171
172    use crate::errors::ResponseError;
173
174    #[derive(Debug, Deserialize)]
175    pub struct ChatCompletion {
176        /// A unique identifier for the chat completion.
177        pub id: String,
178        /// A list of chat completion choices. Can be more than one
179        /// if `n` is greater than 1.
180        pub choices: Vec<Choice>,
181        /// The Unix timestamp (in seconds) of when the chat completion was created.
182        pub created: u64,
183        /// The model used for the chat completion.
184        pub model: String,
185        /// Specifies the processing type used for serving the request.
186        ///
187        /// - If set to 'auto', then the request will be processed with the service tier
188        ///   configured in the Project settings. Unless otherwise configured, the Project
189        ///   will use 'default'.
190        /// - If set to 'default', then the request will be processed with the standard
191        ///   pricing and performance for the selected model.
192        /// - If set to '[flex](https://platform.openai.com/docs/guides/flex-processing)' or
193        ///   '[priority](https://openai.com/api-priority-processing/)', then the request
194        ///   will be processed with the corresponding service tier.
195        /// - When not set, the default behavior is 'auto'.
196        ///
197        /// When the `service_tier` parameter is set, the response body will include the
198        /// `service_tier` value based on the processing mode actually used to serve the
199        /// request. This response value may be different from the value set in the
200        /// parameter.
201        pub service_tier: Option<ServiceTier>,
202        /// The system fingerprint used for the chat completion.
203        /// Can be used in conjunction with the `seed` request parameter to understand when
204        /// backend changes have been made that might impact determinism.
205        pub system_fingerprint: Option<String>,
206        /// The object type, which is always `chat.completion`.
207        pub object: ChatCompletionObject,
208        /// Usage statistics for the completion request.
209        pub usage: Option<CompletionUsage>,
210    }
211
212    #[derive(Debug, Deserialize)]
213    #[serde(rename_all = "lowercase")]
214    pub enum ServiceTier {
215        Auto,
216        Default,
217        Flex,
218        Scale,
219        Priority,
220    }
221
222    /// The object type, which is always `chat.completion`.
223    #[derive(Debug, Deserialize)]
224    pub enum ChatCompletionObject {
225        /// The object type is always `chat.completion`.
226        #[serde(rename = "chat.completion")]
227        ChatCompletion,
228    }
229
230    #[derive(Debug, Deserialize)]
231    pub struct Choice {
232        /// The reason the model stopped generating tokens.
233        ///
234        /// This will be `stop` if the model hit a natural stop point or a provided stop
235        /// sequence, `length` if the maximum number of tokens specified in the request was
236        /// reached, `content_filter` if content was omitted due to a flag from our content
237        /// filters, `tool_calls` if the model called a tool, or `function_call`
238        /// (deprecated) if the model called a function.
239        pub finish_reason: FinishReason,
240        /// The index of the choice in the list of choices.
241        pub index: usize,
242        /// Log probability information for the choice.
243        pub logprobs: Option<ChoiceLogprobs>,
244        /// A chat completion message generated by the model.
245        pub message: ChatCompletionMessage,
246    }
247
248    #[derive(Debug, Deserialize, PartialEq)]
249    #[serde(rename_all = "snake_case")]
250    pub enum FinishReason {
251        Length,
252        Stop,
253        ToolCalls,
254        FunctionCall,
255        ContentFilter,
256        /// This choice can only be found in the manual of DeepSeek
257        InsufficientSystemResource,
258    }
259
260    /// Fields that are not supported yet:
261    /// - _audio_: If the audio output modality is requested, this object contains
262    /// data about the audio response from the model.
263    /// [Learn more from OpenAI](https://platform.openai.com/docs/guides/audio).
264    #[derive(Debug, Deserialize)]
265    pub struct ChatCompletionMessage {
266        /// The role of the author of this message. This shall always
267        /// be ResponseRole::Assistant
268        pub role: ResponseRole,
269        /// The contents of the message.
270        pub content: Option<String>,
271        pub reasoning_content: Option<String>,
272        /// The tool calls generated by the model, such as function calls.
273        /// Tool calls deserialization is not supported yet.
274        pub tool_calls: Option<Vec<ChatCompletionMessageToolCall>>,
275    }
276
277    #[derive(Debug, Deserialize)]
278    #[serde(tag = "type", rename_all = "snake_case")]
279    pub enum ChatCompletionMessageToolCall {
280        /// The type of the tool. Currently, only `function` is supported.
281        /// The field { type = "function" } is added automatically.
282        Function {
283            /// The ID of the tool call.
284            id: String,
285            /// The function that the model called.
286            function: String, // function type
287        },
288        /// The type of the tool. Always `custom`.
289        /// The field { type = "custom" } is added automatically.
290        Custom {
291            /// The id of the tool call.
292            id: String,
293            /// The custom tool that the model called.
294            custom: MessageToolCallCustom,
295        },
296    }
297
298    #[derive(Debug, Deserialize)]
299    pub struct MessageToolCallCustom {
300        /// The input for the custom tool call generated by the model.
301        pub input: String,
302        /// The name of the custom tool to call.
303        pub name: String,
304    }
305
306    #[derive(Debug, Deserialize)]
307    pub struct MessageToolCallFunction {
308        /// The arguments to call the function with, as generated by the model in JSON
309        /// format. Note that the model does not always generate valid JSON, and may
310        /// hallucinate parameters not defined by your function schema. Validate the
311        /// arguments in your code before calling your function.
312        pub arguments: String,
313        /// The name of the function to call.
314        pub name: String,
315    }
316
317    #[derive(Debug, Deserialize)]
318    #[serde(rename_all = "snake_case")]
319    pub enum ResponseRole {
320        /// The role of the response message is always assistant.
321        Assistant,
322    }
323
324    #[derive(Debug, Deserialize)]
325    pub struct ChoiceLogprobs {
326        /// A list of message content tokens with log probability information.
327        pub content: Option<Vec<TokenLogProb>>,
328        /// Only found in DeepSeek's manual.
329        pub reasoning_content: Option<Vec<TokenLogProb>>,
330        /// A list of message refusal tokens with log probability information.
331        pub refusal: Option<Vec<TokenLogProb>>,
332    }
333
334    #[derive(Debug, Deserialize)]
335    pub struct TokenLogProb {
336        /// The token.
337        pub token: String,
338        /// The log probability of this token, if it is within the top 20 most likely
339        /// tokens. Otherwise, the value `-9999.0` is used to signify that the token is very
340        /// unlikely.
341        pub logprob: f32,
342        /// A list of integers representing the UTF-8 bytes representation of the token.
343        ///
344        /// Useful in instances where characters are represented by multiple tokens and
345        /// their byte representations must be combined to generate the correct text
346        /// representation. Can be `null` if there is no bytes representation for the token.
347        pub bytes: Option<Vec<u8>>,
348        /// List of the most likely tokens and their log probability, at this token
349        /// position. In rare cases, there may be fewer than the number of requested
350        /// `top_logprobs` returned.
351        pub top_logprobs: Vec<TopLogprob>,
352    }
353
354    #[derive(Debug, Deserialize)]
355    pub struct TopLogprob {
356        /// The token.
357        pub token: String,
358        /// A list of integers representing the UTF-8 bytes representation of the token.
359        ///
360        /// Useful in instances where characters are represented by multiple tokens and
361        /// their byte representations must be combined to generate the correct text
362        /// representation. Can be `null` if there is no bytes representation for the token.
363        pub logprob: f32,
364        /// List of the most likely tokens and their log probability, at this token
365        /// position. In rare cases, there may be fewer than the number of requested
366        /// `top_logprobs` returned.
367        pub bytes: Option<Vec<u8>>,
368    }
369
370    #[derive(Debug, Deserialize)]
371    pub struct CompletionUsage {
372        /// Number of tokens in the generated completion.
373        pub completion_tokens: usize,
374        /// Number of tokens in the prompt.
375        pub prompt_tokens: usize,
376
377        // These two fields seem to be DeepSeek specific.
378        /// Number of tokens in the prompt that hits the context cache.
379        pub prompt_cache_hit_tokens: Option<usize>,
380        /// Number of tokens in the prompt that misses the context cache.
381        pub prompt_cache_miss_tokens: Option<usize>,
382
383        /// Total number of tokens used in the request (prompt + completion).
384        pub total_tokens: usize,
385        /// Breakdown of tokens used in a completion.
386        pub completion_tokens_details: Option<CompletionTokensDetails>,
387        /// Breakdown of tokens used in the prompt.
388        pub prompt_tokens_details: Option<PromptTokensDetails>,
389    }
390
391    #[derive(Debug, Deserialize)]
392    pub struct CompletionTokensDetails {
393        /// When using Predicted Outputs, the number of tokens in the prediction that
394        /// appeared in the completion.
395        pub accepted_prediction_tokens: Option<usize>,
396        /// Audio input tokens generated by the model.
397        pub audio_tokens: Option<usize>,
398        /// Tokens generated by the model for reasoning.
399        pub reasoning_tokens: Option<usize>,
400        /// When using Predicted Outputs, the number of tokens in the prediction that did
401        /// not appear in the completion. However, like reasoning tokens, these tokens are
402        /// still counted in the total completion tokens for purposes of billing, output,
403        /// and context window limits.
404        pub rejected_prediction_tokens: Option<usize>,
405    }
406
407    #[derive(Debug, Deserialize)]
408    pub struct PromptTokensDetails {
409        /// Audio input tokens present in the prompt.
410        pub audio_tokens: Option<usize>,
411        /// Cached tokens present in the prompt.
412        pub cached_tokens: Option<usize>,
413    }
414
415    impl FromStr for ChatCompletion {
416        type Err = crate::errors::ResponseError;
417
418        fn from_str(content: &str) -> Result<Self, Self::Err> {
419            let parse_result: Result<ChatCompletion, _> = serde_json::from_str(content)
420                .map_err(|e| ResponseError::DeserializationError(e.to_string()));
421            parse_result
422        }
423    }
424
425    #[cfg(test)]
426    mod test {
427        use super::*;
428
429        #[test]
430        fn no_streaming_example_deepseek() {
431            let json = r#"{
432              "id": "30f6413a-a827-4cf3-9898-f13a8634b798",
433              "object": "chat.completion",
434              "created": 1757944111,
435              "model": "deepseek-chat",
436              "choices": [
437                {
438                  "index": 0,
439                  "message": {
440                    "role": "assistant",
441                    "content": "Hello! How can I help you today? 😊"
442                  },
443                  "logprobs": null,
444                  "finish_reason": "stop"
445                }
446              ],
447              "usage": {
448                "prompt_tokens": 10,
449                "completion_tokens": 11,
450                "total_tokens": 21,
451                "prompt_tokens_details": {
452                  "cached_tokens": 0
453                },
454                "prompt_cache_hit_tokens": 0,
455                "prompt_cache_miss_tokens": 10
456              },
457              "system_fingerprint": "fp_08f168e49b_prod0820_fp8_kvcache"
458            }"#;
459
460            let parsed = ChatCompletion::from_str(json);
461            match parsed {
462                Ok(_) => {}
463                Err(e) => {
464                    panic!("Failed to deserialize: {}", e);
465                }
466            }
467        }
468    }
469}