openai_interface/chat/response/
streaming.rs

1use serde::Deserialize;
2
3use crate::errors::ResponseError;
4
5#[derive(Debug, Deserialize)]
6pub struct Completion {
7    pub id: String,
8    pub choices: Vec<CompletionChoice>,
9    pub created: u64,
10    pub model: String,
11    pub object: String,
12    pub usage: Option<CompletionUsage>,
13}
14
15#[derive(Debug, Deserialize)]
16pub struct CompletionChoice {
17    pub delta: CompletionDelta,
18    pub index: u32,
19    pub logprobs: Option<ChoiceLogprobs>,
20    pub finish_reason: Option<FinishReason>,
21}
22
23#[derive(Debug, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum FinishReason {
26    Length,
27    Stop,
28    ContentFilter,
29    ToolCalls,
30    InsufficientSystemResource,
31}
32
33#[derive(Debug, Deserialize)]
34pub struct CompletionDelta {
35    #[serde(flatten)]
36    pub content: CompletionContent,
37    pub role: Option<CompletionRole>,
38}
39
40#[derive(Debug, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum CompletionRole {
43    User,
44    Assistant,
45    System,
46    Tool,
47}
48
49#[derive(Debug, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum CompletionContent {
52    Content(String),
53    ReasoningContent(String),
54}
55
56#[derive(Debug, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum ChoiceLogprobs {
59    Content(Vec<LogprobeContent>),
60    ReasoningContent(Vec<LogprobeContent>),
61}
62
63#[derive(Debug, Deserialize)]
64pub struct LogprobeContent {
65    pub token: String,
66    pub logprob: f32,
67    pub bytes: Option<Vec<u8>>,
68    pub top_logprobs: Vec<TopLogprob>,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct TopLogprob {
73    pub token: String,
74    pub logprob: f32,
75    pub bytes: Option<Vec<u8>>,
76}
77
78#[derive(Debug, Deserialize)]
79pub struct CompletionUsage {
80    pub completion_tokens: usize,
81    pub prompt_tokens: usize,
82    pub total_tokens: usize,
83}
84
85impl Completion {
86    pub fn parse_string(content: &str) -> Result<Self, crate::errors::ResponseError> {
87        let parse_result: Result<Completion, _> = serde_json::from_str(content)
88            .map_err(|e| ResponseError::DeserializationError(e.to_string()));
89        parse_result
90    }
91}
92
93#[cfg(test)]
94mod test {
95    use super::*;
96
97    #[test]
98    fn streaming_example_deepseek() {
99        let streams = vec![
100            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}"#,
101            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"}"#,
102            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"}"#,
103            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"}"#,
104            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"}"#,
105            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"}"#,
106            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"}"#,
107            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"}"#,
108            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"}"#,
109            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"}"#,
110            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}}"#,
111        ];
112
113        for stream in streams {
114            let parsed = Completion::parse_string(stream);
115            match parsed {
116                Ok(completion) => {
117                    println!("Deserialized: {:#?}", completion);
118                }
119                Err(e) => {
120                    panic!("Failed to deserialize {}: {}", stream, e);
121                }
122            }
123        }
124    }
125
126    #[test]
127    fn streaming_example_qwen() {
128        let streams = vec![
129            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}"#,
130            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}"#,
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":"stop","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":[],"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}}}"#,
139        ];
140
141        for stream in streams {
142            let parsed = Completion::parse_string(stream);
143            match parsed {
144                Ok(completion) => {
145                    println!("Deserialized: {:#?}", completion);
146                }
147                Err(e) => {
148                    panic!("Failed to deserialize {}: {}", stream, e);
149                }
150            }
151        }
152    }
153}