Skip to main content

openai_interface/chat/create/
response.rs

1//! This module provides structures for streaming and non-streaming
2//! chat completion responses.
3
4pub mod streaming {
5    //! Streaming chat completion response.
6
7    use serde::{Deserialize, Serialize};
8
9    use crate::chat::ServiceTier;
10
11    /// Deserializes a possibly-null, possibly-missing list as an empty
12    /// `Vec`. Some OpenAI-compatible backends (e.g. vLLM) terminate the
13    /// stream with a usage-only chunk whose `choices` is `null`; without
14    /// this, the chunk — and with it the only copy of the usage statistics —
15    /// would fail to deserialize.
16    fn null_to_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
17    where
18        D: serde::Deserializer<'de>,
19        T: Deserialize<'de>,
20    {
21        Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
22    }
23
24    #[derive(Debug, Deserialize, Serialize, Clone)]
25    pub struct ChatCompletionChunk {
26        /// A unique identifier for the chat completion.
27        pub id: String,
28        /// A list of chat completion choices. Can be more than one
29        /// if `n` is greater than 1. Empty for the final usage-only chunk
30        /// (see `stream_options: {"include_usage": true}`); some backends
31        /// send that chunk with `"choices": null` or omit the key entirely,
32        /// which deserializes as an empty list too.
33        #[serde(default, deserialize_with = "null_to_empty_vec")]
34        pub choices: Vec<CompletionChunkChoice>,
35        /// The Unix timestamp (in seconds) of when the chat completion was created.
36        /// Each chunk has the same timestamp.
37        pub created: u64,
38        /// The model used for the chat completion.
39        pub model: String,
40        /// The object type, which is always `chat.completion.chunk`.
41        ///
42        /// `Some` only when the backend sends a recognized value; some
43        /// non-OpenAI gateways omit or repurpose the field.
44        pub object: Option<ChatCompletionChunkObject>,
45        /// Specifies the processing type used for serving the request.
46        ///
47        /// When the `service_tier` parameter is set, the response body will include the
48        /// `service_tier` value based on the processing mode actually used to serve the
49        /// request. This response value may be different from the value set in the
50        /// request parameter.
51        pub service_tier: Option<ServiceTier>,
52        /// This fingerprint represents the backend configuration that the model runs with.
53        /// Can be used in conjunction with the `seed` request parameter to understand when
54        /// backend changes have been made that might impact determinism.
55        pub system_fingerprint: Option<String>,
56        /// An optional field that will only be present when you set
57        /// `stream_options: {"include_usage": true}` in your request. When present, it
58        /// contains a null value **except for the last chunk** which contains the token
59        /// usage statistics for the entire request.
60        ///
61        /// **NOTE:** If the stream is interrupted or cancelled, you may not receive the
62        /// final usage chunk which contains the total token usage for the request.
63        pub usage: Option<CompletionUsage>,
64        /// Moderation results for the request input and generated output.
65        ///
66        /// Present on the moderation chunk when moderated completions are
67        /// requested via the `moderation` request parameter.
68        pub moderation: Option<crate::chat::ChatModeration>,
69    }
70
71    crate::wire_string_enum! {
72        /// The object type, which is always `chat.completion.chunk`.
73        pub enum ChatCompletionChunkObject {
74            ChatCompletionChunk => "chat.completion.chunk",
75        }
76    }
77
78    #[derive(Debug, Deserialize, Serialize, Clone)]
79    pub struct CompletionChunkChoice {
80        /// A chat completion delta generated by streamed model responses.
81        pub delta: ChoiceDelta,
82        /// The index of the choice in the list of choices.
83        pub index: u32,
84        /// Log probability information for the choice.
85        pub logprobs: Option<ChoiceLogprobs>,
86        /// The reason the model stopped generating tokens.
87        ///
88        /// This will be `stop` if the model hit a natural stop point or a provided stop
89        /// sequence, `length` if the maximum number of tokens specified in the request was
90        /// reached, `content_filter` if content was omitted due to a flag from our content
91        /// filters, `tool_calls` if the model called a tool, or `function_call`
92        /// (deprecated) if the model called a function.
93        pub finish_reason: Option<FinishReason>,
94    }
95
96    pub use crate::chat::FinishReason;
97
98    #[derive(Debug, Deserialize, Serialize, Clone)]
99    pub struct ChoiceDelta {
100        /// The contents of the chunk message.
101        pub content: Option<String>,
102        /// The reasoning contents of the chunk message. Only present for
103        /// thinking models (DeepSeek, Qwen3, and other reasoning models
104        /// served by OpenAI-compatible backends).
105        #[cfg(feature = "reasoning")]
106        pub reasoning_content: Option<String>,
107        /// Deprecated and replaced by `tool_calls`.
108        ///
109        /// The name and arguments of a function that should be called, as generated by the
110        /// model.
111        pub function_call: Option<ChoiceDeltaFunctionCall>,
112        /// The refusal message generated by the model.
113        pub refusal: Option<String>,
114        /// The role of the author of this message.
115        pub role: Option<CompletionRole>,
116        /// A list of tool calls generated by the model, such as function calls.
117        pub tool_calls: Option<Vec<ChoiceDeltaToolCall>>,
118        /// Annotations for the chunk message, such as URL citations emitted
119        /// by search deployments.
120        ///
121        /// Not part of the official OpenAI chunk schema, but Azure OpenAI
122        /// ("on your data" / search deployments) streams `annotations`
123        /// inside `delta`, and dropping them silently loses the citations.
124        pub annotations: Option<Vec<crate::chat::Annotation>>,
125        /// Data about a streamed audio response from the model.
126        ///
127        /// Not part of the official OpenAI chunk schema, but audio-capable
128        /// Azure OpenAI deployments stream `audio` inside `delta`.
129        pub audio: Option<crate::chat::ChatCompletionAudio>,
130    }
131
132    #[derive(Debug, Deserialize, Serialize, Clone)]
133    pub struct ChoiceDeltaToolCallFunction {
134        /// The arguments to call the function with, as generated by the model in JSON
135        /// format. Note that the model does not always generate valid JSON, and may
136        /// hallucinate parameters not defined by your function schema. Validate the
137        /// arguments in your code before calling your function.
138        pub arguments: Option<String>,
139        /// The name of the function to call.
140        pub name: Option<String>,
141    }
142
143    #[derive(Debug, Deserialize, Serialize, Clone)]
144    pub struct ChoiceDeltaFunctionCall {
145        /// The arguments to call the function with, as generated by the model in JSON
146        /// format. Note that the model does not always generate valid JSON, and may
147        /// hallucinate parameters not defined by your function schema. Validate the
148        /// arguments in your code before calling your function.
149        pub arguments: Option<String>,
150        /// The name of the function to call.
151        pub name: Option<String>,
152    }
153
154    #[derive(Debug, Deserialize, Serialize, Clone)]
155    pub struct ChoiceDeltaToolCall {
156        /// The index of the tool call in the list of tool calls.
157        pub index: u32,
158        /// The ID of the tool call.
159        pub id: Option<String>,
160        /// The function that the model called.
161        pub function: Option<ChoiceDeltaToolCallFunction>,
162        /// The type of the tool. Currently, only `function` is supported.
163        #[serde(rename = "type")]
164        pub type_: Option<ChoiceDeltaToolCallType>,
165    }
166
167    crate::wire_string_enum! {
168        /// The type of a streamed tool call.
169        pub enum ChoiceDeltaToolCallType {
170            /// The tool call invokes a function.
171            Function => "function",
172            /// The tool call invokes a custom tool.
173            Custom => "custom",
174        }
175    }
176
177    pub use crate::chat::Role as CompletionRole;
178
179    /// Log probability information for a choice.
180    #[derive(Debug, Deserialize, Serialize, Clone)]
181    pub struct ChoiceLogprobs {
182        /// A list of message content tokens with log probability information.
183        pub content: Option<Vec<LogprobeContent>>,
184        /// A list of reasoning content tokens with log probability
185        /// information. Only present for thinking models.
186        #[cfg(feature = "reasoning")]
187        pub reasoning_content: Option<Vec<LogprobeContent>>,
188        /// A list of message refusal tokens with log probability information.
189        pub refusal: Option<Vec<LogprobeContent>>,
190    }
191
192    /// A list of message content tokens with log probability information.
193    #[derive(Debug, Deserialize, Serialize, Clone)]
194    pub struct LogprobeContent {
195        pub token: String,
196        pub logprob: f32,
197        pub bytes: Option<Vec<u8>>,
198        pub top_logprobs: Vec<TopLogprob>,
199    }
200
201    /// List of the most likely tokens and their log probability, at this
202    /// token position. In rare cases, there may be fewer than the number of requested top_logprobs returned.
203    #[derive(Debug, Deserialize, Serialize, Clone)]
204    pub struct TopLogprob {
205        pub token: String,
206        pub logprob: f32,
207        pub bytes: Option<Vec<u8>>,
208    }
209
210    pub use crate::chat::{CompletionTokensDetails, CompletionUsage, PromptTokensDetails};
211
212    crate::impl_from_str!(ChatCompletionChunk);
213
214    #[cfg(test)]
215    mod test {
216        use std::str::FromStr;
217
218        use super::*;
219
220        #[test]
221        fn streaming_example_deepseek() {
222            let streams = vec![
223                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}"#,
224                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"}"#,
225                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"}"#,
226                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"}"#,
227                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"}"#,
228                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"}"#,
229                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"}"#,
230                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"}"#,
231                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"}"#,
232                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"}"#,
233                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}}"#,
234            ];
235
236            for stream in streams {
237                let parsed = ChatCompletionChunk::from_str(stream);
238                match parsed {
239                    Ok(completion) => {
240                        println!("Deserialized: {:#?}", completion);
241                    }
242                    Err(e) => {
243                        panic!("Failed to deserialize {}: {}", stream, e);
244                    }
245                }
246            }
247        }
248
249        #[test]
250        fn streaming_example_qwen() {
251            let streams = vec![
252                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}"#,
253                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}"#,
254                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}"#,
255                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}"#,
256                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}"#,
257                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}"#,
258                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}"#,
259                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}"#,
260                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}"#,
261                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}"#,
262                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}}}"#,
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        /// Azure OpenAI streams `annotations` (URL citations from "on your
279        /// data" deployments) and `audio` inside `delta`, outside the
280        /// official chunk schema; they must deserialize instead of being
281        /// dropped.
282        #[test]
283        fn streaming_example_azure_annotations_and_audio() {
284            let chunk = ChatCompletionChunk::from_str(
285                r#"{"id":"chatcmpl-abc","choices":[{"delta":{"content":"According to the doc","annotations":[{"type":"url_citation","url_citation":{"start_index":0,"end_index":20,"title":"Azure Docs","url":"https://learn.microsoft.com/azure"}}],"audio":{"id":"audio_abc","data":"SGVsbG8=","expires_at":1735113344,"transcript":"Hello"}},"finish_reason":null,"index":0,"logprobs":null}],"created":1735113344,"model":"gpt-4o","object":"chat.completion.chunk","system_fingerprint":"fp_abc"}"#,
286            )
287            .expect("chunk with annotations and audio must deserialize");
288
289            let delta = &chunk.choices[0].delta;
290            let annotations = delta.annotations.as_ref().expect("annotations");
291            assert_eq!(annotations.len(), 1);
292            assert_eq!(annotations[0].url_citation.title, "Azure Docs");
293            assert_eq!(
294                annotations[0].url_citation.url,
295                "https://learn.microsoft.com/azure"
296            );
297
298            let audio = delta.audio.as_ref().expect("audio");
299            assert_eq!(audio.id, "audio_abc");
300            assert_eq!(audio.transcript, "Hello");
301        }
302
303        /// vLLM-style backends terminate the stream with a usage-only chunk
304        /// whose `choices` is `null`. The chunk must parse, yield no
305        /// choices, and keep the usage statistics.
306        #[test]
307        fn usage_chunk_with_null_choices_parses() {
308            let parsed = ChatCompletionChunk::from_str(
309                r#"{"id":"chatcmpl-1","choices":null,"created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","usage":{"completion_tokens":17,"prompt_tokens":22,"total_tokens":39}}"#,
310            )
311            .expect("usage chunk with null choices must deserialize");
312            assert!(parsed.choices.is_empty());
313            assert_eq!(parsed.usage.expect("usage").total_tokens, 39);
314        }
315
316        #[test]
317        fn usage_chunk_with_missing_choices_parses() {
318            let parsed = ChatCompletionChunk::from_str(
319                r#"{"id":"chatcmpl-1","created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","usage":{"completion_tokens":1,"prompt_tokens":2,"total_tokens":3}}"#,
320            )
321            .expect("usage chunk without a choices key must deserialize");
322            assert!(parsed.choices.is_empty());
323        }
324
325        /// Gateways invent finish reasons (`eos`, provider-specific codes).
326        /// They must not kill the chunk, and must round-trip unchanged.
327        #[test]
328        fn unknown_finish_reason_is_preserved() {
329            let parsed = ChatCompletionChunk::from_str(
330                r#"{"id":"1","choices":[{"index":0,"delta":{},"finish_reason":"eos"}],"created":1,"model":"m","object":"chat.completion.chunk"}"#,
331            )
332            .expect("chunk with unknown finish_reason must deserialize");
333
334            let finish_reason = parsed.choices[0]
335                .finish_reason
336                .as_ref()
337                .expect("finish_reason");
338            assert_eq!(finish_reason.as_str(), "eos");
339            assert_eq!(finish_reason.to_string(), "eos");
340            assert_eq!(
341                serde_json::to_value(finish_reason).unwrap(),
342                serde_json::json!("eos")
343            );
344        }
345
346        #[test]
347        fn unknown_role_is_preserved() {
348            let parsed = ChatCompletionChunk::from_str(
349                r#"{"id":"1","choices":[{"index":0,"delta":{"role":"model","content":"hi"},"finish_reason":null}],"created":1,"model":"m","object":"chat.completion.chunk"}"#,
350            )
351            .expect("chunk with unknown role must deserialize");
352            let role = parsed.choices[0].delta.role.as_ref().expect("role");
353            assert_eq!(role.as_str(), "model");
354        }
355
356        #[test]
357        fn missing_or_unknown_object_field_parses() {
358            let missing =
359                ChatCompletionChunk::from_str(r#"{"id":"1","choices":[],"created":1,"model":"m"}"#)
360                    .expect("chunk without object must deserialize");
361            assert!(missing.object.is_none());
362
363            let weird = ChatCompletionChunk::from_str(
364                r#"{"id":"1","choices":[],"created":1,"model":"m","object":"vendor.custom.chunk"}"#,
365            )
366            .expect("chunk with unknown object must deserialize");
367            assert_eq!(
368                weird.object.expect("object").as_str(),
369                "vendor.custom.chunk"
370            );
371        }
372
373        /// Responses must serialize back out for proxying/logging.
374        #[test]
375        fn chunk_round_trips_through_json() {
376            let parsed = ChatCompletionChunk::from_str(
377                r#"{"id":"1","choices":[{"index":0,"delta":{"role":"assistant","content":"Hi"},"finish_reason":null}],"created":1,"model":"m","object":"chat.completion.chunk"}"#,
378            )
379            .expect("chunk must deserialize");
380
381            let json = serde_json::to_value(&parsed).unwrap();
382            assert_eq!(json["object"], "chat.completion.chunk");
383            assert_eq!(json["choices"][0]["delta"]["content"], "Hi");
384
385            let reparsed = serde_json::from_value::<ChatCompletionChunk>(json).unwrap();
386            assert_eq!(reparsed.id, parsed.id);
387            assert_eq!(
388                reparsed.choices[0].delta.content,
389                parsed.choices[0].delta.content
390            );
391        }
392    }
393}
394
395pub mod no_streaming {
396    //! Non-streaming chat completion response.
397
398    /// Alias for `crate::chat::ChatCompletion`, which is shared
399    /// by many other modules. This alias is for compatibility.
400    pub type ChatCompletion = crate::chat::ChatCompletion;
401}