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        /// vLLM: the prompt's token IDs after chat-template rendering. Sent
71        /// on the first chunk only.
72        #[cfg(feature = "vllm")]
73        pub prompt_token_ids: Option<Vec<u32>>,
74        /// vLLM: the fully rendered prompt text. Only sent on the first
75        /// chunk, and only when the request set `vllm_chat.return_prompt_text`.
76        #[cfg(feature = "vllm")]
77        pub prompt_text: Option<String>,
78
79        /// Z.ai / GLM: the request identifier, echoing the request's
80        /// `request_id` or the one GLM generated. Not part of the OpenAI chunk
81        /// schema.
82        #[cfg(feature = "zai")]
83        pub request_id: Option<String>,
84    }
85
86    crate::wire_string_enum! {
87        /// The object type, which is always `chat.completion.chunk`.
88        pub enum ChatCompletionChunkObject {
89            ChatCompletionChunk => "chat.completion.chunk",
90        }
91    }
92
93    #[derive(Debug, Deserialize, Serialize, Clone)]
94    pub struct CompletionChunkChoice {
95        /// A chat completion delta generated by streamed model responses.
96        pub delta: ChoiceDelta,
97        /// The index of the choice in the list of choices.
98        pub index: u32,
99        /// Log probability information for the choice.
100        pub logprobs: Option<ChoiceLogprobs>,
101        /// The reason the model stopped generating tokens.
102        ///
103        /// This will be `stop` if the model hit a natural stop point or a provided stop
104        /// sequence, `length` if the maximum number of tokens specified in the request was
105        /// reached, `content_filter` if content was omitted due to a flag from our content
106        /// filters, `tool_calls` if the model called a tool, or `function_call`
107        /// (deprecated) if the model called a function.
108        pub finish_reason: Option<FinishReason>,
109
110        /// vLLM: which terminator ended generation — the matched stop
111        /// string, or the matched token ID. Not part of the OpenAI chunk
112        /// schema.
113        #[cfg(feature = "vllm")]
114        pub stop_reason: Option<crate::vllm::StopReason>,
115        /// vLLM: the generated token IDs for this chunk. Only set when the
116        /// request set `vllm_chat.return_token_ids`.
117        #[cfg(feature = "vllm")]
118        pub token_ids: Option<Vec<u32>>,
119    }
120
121    pub use crate::chat::FinishReason;
122
123    #[derive(Debug, Deserialize, Serialize, Clone)]
124    pub struct ChoiceDelta {
125        /// The contents of the chunk message.
126        pub content: Option<String>,
127        /// The reasoning contents of the chunk message. Only present for
128        /// thinking models (DeepSeek, Qwen3, and other reasoning models
129        /// served by OpenAI-compatible backends).
130        #[cfg(feature = "reasoning")]
131        pub reasoning_content: Option<String>,
132        /// vLLM: the reasoning contents of the chunk message, under the key
133        /// vLLM actually streams.
134        ///
135        /// vLLM serializes the chain of thought as `reasoning`, not
136        /// `reasoning_content`, so for a vLLM backend the field above stays
137        /// `None`. [`ChatCompletionAccumulator`](crate::chat::create::accumulator::ChatCompletionAccumulator)
138        /// accumulates both and exposes this one through its `reasoning`
139        /// accessor.
140        #[cfg(feature = "vllm")]
141        pub reasoning: Option<String>,
142        /// Deprecated and replaced by `tool_calls`.
143        ///
144        /// The name and arguments of a function that should be called, as generated by the
145        /// model.
146        pub function_call: Option<ChoiceDeltaFunctionCall>,
147        /// The refusal message generated by the model.
148        pub refusal: Option<String>,
149        /// The role of the author of this message.
150        pub role: Option<CompletionRole>,
151        /// A list of tool calls generated by the model, such as function calls.
152        pub tool_calls: Option<Vec<ChoiceDeltaToolCall>>,
153        /// Annotations for the chunk message, such as URL citations emitted
154        /// by search deployments.
155        ///
156        /// Not part of the official OpenAI chunk schema, but Azure OpenAI
157        /// ("on your data" / search deployments) streams `annotations`
158        /// inside `delta`, and dropping them silently loses the citations.
159        pub annotations: Option<Vec<crate::chat::Annotation>>,
160        /// Data about a streamed audio response from the model.
161        ///
162        /// Not part of the official OpenAI chunk schema, but audio-capable
163        /// Azure OpenAI deployments stream `audio` inside `delta`.
164        pub audio: Option<crate::chat::ChatCompletionAudio>,
165    }
166
167    #[derive(Debug, Deserialize, Serialize, Clone)]
168    pub struct ChoiceDeltaToolCallFunction {
169        /// The arguments to call the function with, as generated by the model in JSON
170        /// format. Note that the model does not always generate valid JSON, and may
171        /// hallucinate parameters not defined by your function schema. Validate the
172        /// arguments in your code before calling your function.
173        pub arguments: Option<String>,
174        /// The name of the function to call.
175        pub name: Option<String>,
176    }
177
178    #[derive(Debug, Deserialize, Serialize, Clone)]
179    pub struct ChoiceDeltaFunctionCall {
180        /// The arguments to call the function with, as generated by the model in JSON
181        /// format. Note that the model does not always generate valid JSON, and may
182        /// hallucinate parameters not defined by your function schema. Validate the
183        /// arguments in your code before calling your function.
184        pub arguments: Option<String>,
185        /// The name of the function to call.
186        pub name: Option<String>,
187    }
188
189    #[derive(Debug, Deserialize, Serialize, Clone)]
190    pub struct ChoiceDeltaToolCall {
191        /// The index of the tool call in the list of tool calls.
192        pub index: u32,
193        /// The ID of the tool call.
194        pub id: Option<String>,
195        /// The function that the model called.
196        pub function: Option<ChoiceDeltaToolCallFunction>,
197        /// The type of the tool. Currently, only `function` is supported.
198        #[serde(rename = "type")]
199        pub type_: Option<ChoiceDeltaToolCallType>,
200    }
201
202    crate::wire_string_enum! {
203        /// The type of a streamed tool call.
204        pub enum ChoiceDeltaToolCallType {
205            /// The tool call invokes a function.
206            Function => "function",
207            /// The tool call invokes a custom tool.
208            Custom => "custom",
209        }
210    }
211
212    pub use crate::chat::Role as CompletionRole;
213
214    /// Log probability information for a choice.
215    #[derive(Debug, Deserialize, Serialize, Clone)]
216    pub struct ChoiceLogprobs {
217        /// A list of message content tokens with log probability information.
218        pub content: Option<Vec<LogprobeContent>>,
219        /// A list of reasoning content tokens with log probability
220        /// information. Only present for thinking models.
221        #[cfg(feature = "reasoning")]
222        pub reasoning_content: Option<Vec<LogprobeContent>>,
223        /// A list of message refusal tokens with log probability information.
224        pub refusal: Option<Vec<LogprobeContent>>,
225    }
226
227    /// A list of message content tokens with log probability information.
228    #[derive(Debug, Deserialize, Serialize, Clone)]
229    pub struct LogprobeContent {
230        pub token: String,
231        pub logprob: f32,
232        pub bytes: Option<Vec<u8>>,
233        pub top_logprobs: Vec<TopLogprob>,
234    }
235
236    /// List of the most likely tokens and their log probability, at this
237    /// token position. In rare cases, there may be fewer than the number of requested top_logprobs returned.
238    #[derive(Debug, Deserialize, Serialize, Clone)]
239    pub struct TopLogprob {
240        pub token: String,
241        pub logprob: f32,
242        pub bytes: Option<Vec<u8>>,
243    }
244
245    pub use crate::chat::{CompletionTokensDetails, CompletionUsage, PromptTokensDetails};
246
247    crate::impl_from_str!(ChatCompletionChunk);
248
249    #[cfg(test)]
250    mod test {
251        use std::str::FromStr;
252
253        use super::*;
254
255        #[test]
256        fn streaming_example_deepseek() {
257            let streams = vec![
258                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}"#,
259                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"}"#,
260                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"}"#,
261                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"}"#,
262                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"}"#,
263                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"}"#,
264                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"}"#,
265                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"}"#,
266                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"}"#,
267                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"}"#,
268                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}}"#,
269            ];
270
271            for stream in streams {
272                let parsed = ChatCompletionChunk::from_str(stream);
273                match parsed {
274                    Ok(completion) => {
275                        println!("Deserialized: {:#?}", completion);
276                    }
277                    Err(e) => {
278                        panic!("Failed to deserialize {}: {}", stream, e);
279                    }
280                }
281            }
282        }
283
284        #[test]
285        fn streaming_example_qwen() {
286            let streams = vec![
287                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}"#,
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":null,"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":[{"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}"#,
291                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}"#,
292                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}"#,
293                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}"#,
294                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}"#,
295                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}"#,
296                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}"#,
297                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}}}"#,
298            ];
299
300            for stream in streams {
301                let parsed = ChatCompletionChunk::from_str(stream);
302                match parsed {
303                    Ok(completion) => {
304                        println!("Deserialized: {:#?}", completion);
305                    }
306                    Err(e) => {
307                        panic!("Failed to deserialize {}: {}", stream, e);
308                    }
309                }
310            }
311        }
312
313        /// Azure OpenAI streams `annotations` (URL citations from "on your
314        /// data" deployments) and `audio` inside `delta`, outside the
315        /// official chunk schema; they must deserialize instead of being
316        /// dropped.
317        #[test]
318        fn streaming_example_azure_annotations_and_audio() {
319            let chunk = ChatCompletionChunk::from_str(
320                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"}"#,
321            )
322            .expect("chunk with annotations and audio must deserialize");
323
324            let delta = &chunk.choices[0].delta;
325            let annotations = delta.annotations.as_ref().expect("annotations");
326            assert_eq!(annotations.len(), 1);
327            assert_eq!(annotations[0].url_citation.title, "Azure Docs");
328            assert_eq!(
329                annotations[0].url_citation.url,
330                "https://learn.microsoft.com/azure"
331            );
332
333            let audio = delta.audio.as_ref().expect("audio");
334            assert_eq!(audio.id, "audio_abc");
335            assert_eq!(audio.transcript, "Hello");
336        }
337
338        /// vLLM-style backends terminate the stream with a usage-only chunk
339        /// whose `choices` is `null`. The chunk must parse, yield no
340        /// choices, and keep the usage statistics.
341        #[test]
342        fn usage_chunk_with_null_choices_parses() {
343            let parsed = ChatCompletionChunk::from_str(
344                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}}"#,
345            )
346            .expect("usage chunk with null choices must deserialize");
347            assert!(parsed.choices.is_empty());
348            assert_eq!(parsed.usage.expect("usage").total_tokens, 39);
349        }
350
351        #[test]
352        fn usage_chunk_with_missing_choices_parses() {
353            let parsed = ChatCompletionChunk::from_str(
354                r#"{"id":"chatcmpl-1","created":1735113344,"model":"qwen-plus","object":"chat.completion.chunk","usage":{"completion_tokens":1,"prompt_tokens":2,"total_tokens":3}}"#,
355            )
356            .expect("usage chunk without a choices key must deserialize");
357            assert!(parsed.choices.is_empty());
358        }
359
360        /// Gateways invent finish reasons (`eos`, provider-specific codes).
361        /// They must not kill the chunk, and must round-trip unchanged.
362        #[test]
363        fn unknown_finish_reason_is_preserved() {
364            let parsed = ChatCompletionChunk::from_str(
365                r#"{"id":"1","choices":[{"index":0,"delta":{},"finish_reason":"eos"}],"created":1,"model":"m","object":"chat.completion.chunk"}"#,
366            )
367            .expect("chunk with unknown finish_reason must deserialize");
368
369            let finish_reason = parsed.choices[0]
370                .finish_reason
371                .as_ref()
372                .expect("finish_reason");
373            assert_eq!(finish_reason.as_str(), "eos");
374            assert_eq!(finish_reason.to_string(), "eos");
375            assert_eq!(
376                serde_json::to_value(finish_reason).unwrap(),
377                serde_json::json!("eos")
378            );
379        }
380
381        #[test]
382        fn unknown_role_is_preserved() {
383            let parsed = ChatCompletionChunk::from_str(
384                r#"{"id":"1","choices":[{"index":0,"delta":{"role":"model","content":"hi"},"finish_reason":null}],"created":1,"model":"m","object":"chat.completion.chunk"}"#,
385            )
386            .expect("chunk with unknown role must deserialize");
387            let role = parsed.choices[0].delta.role.as_ref().expect("role");
388            assert_eq!(role.as_str(), "model");
389        }
390
391        #[test]
392        fn missing_or_unknown_object_field_parses() {
393            let missing =
394                ChatCompletionChunk::from_str(r#"{"id":"1","choices":[],"created":1,"model":"m"}"#)
395                    .expect("chunk without object must deserialize");
396            assert!(missing.object.is_none());
397
398            let weird = ChatCompletionChunk::from_str(
399                r#"{"id":"1","choices":[],"created":1,"model":"m","object":"vendor.custom.chunk"}"#,
400            )
401            .expect("chunk with unknown object must deserialize");
402            assert_eq!(
403                weird.object.expect("object").as_str(),
404                "vendor.custom.chunk"
405            );
406        }
407
408        /// Responses must serialize back out for proxying/logging.
409        #[test]
410        fn chunk_round_trips_through_json() {
411            let parsed = ChatCompletionChunk::from_str(
412                r#"{"id":"1","choices":[{"index":0,"delta":{"role":"assistant","content":"Hi"},"finish_reason":null}],"created":1,"model":"m","object":"chat.completion.chunk"}"#,
413            )
414            .expect("chunk must deserialize");
415
416            let json = serde_json::to_value(&parsed).unwrap();
417            assert_eq!(json["object"], "chat.completion.chunk");
418            assert_eq!(json["choices"][0]["delta"]["content"], "Hi");
419
420            let reparsed = serde_json::from_value::<ChatCompletionChunk>(json).unwrap();
421            assert_eq!(reparsed.id, parsed.id);
422            assert_eq!(
423                reparsed.choices[0].delta.content,
424                parsed.choices[0].delta.content
425            );
426        }
427    }
428}
429
430pub mod no_streaming {
431    //! Non-streaming chat completion response.
432
433    /// Alias for `crate::chat::ChatCompletion`, which is shared
434    /// by many other modules. This alias is for compatibility.
435    pub type ChatCompletion = crate::chat::ChatCompletion;
436}