Skip to main content

gproxy_protocol/wire/openai/chat/
stream.rs

1//! Typed JSON records for Chat Completions streaming; `[DONE]` is SSE framing,
2//! not a JSON chunk. `StreamResponse` carries raw transport bytes separately.
3//!
4//! Verified against openai-python main on 2026-09-13:
5//! https://github.com/openai/openai-python/blob/main/src/openai/types/chat/chat_completion_chunk.py
6//! https://github.com/openai/openai-python/blob/main/src/openai/types/chat/chat_completion_token_logprob.py
7//! https://github.com/openai/openai-python/blob/main/src/openai/types/completion_usage.py
8//! The SDK chunk has function-call fragments (not non-streaming custom calls),
9//! five role values, moderation, and obfuscation. The chunk source last-change
10//! commit was f348ec87b934c98889102668913e0a3ae7fc303d. Referenced SDK token
11//! bytes are optional (unlike the local non-streaming document), and usage adds
12//! text/image details; these use dedicated chunk types here.
13//! Audio is not a declared delta
14//! field in this source; unknown extensions are retained in `rest`.
15
16use super::content::present_nullable;
17use super::request::GenerateContentRequest;
18use super::response::{FinishReason, ResponseServiceTier};
19use crate::connection::ByteStream;
20use crate::{Rest, WireResponse};
21use serde::{Deserialize, Serialize};
22
23pub type StreamRequest = GenerateContentRequest;
24pub type StreamResponse = WireResponse<ByteStream>;
25
26/// The `error` payload of a failed Chat SSE record, which has no chunk fields.
27#[derive(
28    Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::DeclaredFields,
29)]
30#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32pub struct StreamError {
33    pub message: String,
34    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
35    pub type_: Option<String>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub code: Option<serde_json::Value>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub param: Option<String>,
40    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
41    pub rest: Rest,
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
45#[serde(rename_all = "snake_case")]
46#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
47#[derive(gproxy_protocol_macros::DeclaredFields)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49pub struct ChatCompletionChunk {
50    #[serde(
51        default,
52        deserialize_with = "present_nullable",
53        skip_serializing_if = "Option::is_none"
54    )]
55    pub moderation: Option<Option<super::response::ModerationResponse>>,
56    pub id: String,
57    pub choices: Vec<StreamChoice>,
58    pub created: i64,
59    pub model: String,
60    pub object: ChunkObject,
61    #[serde(
62        default,
63        deserialize_with = "present_nullable",
64        skip_serializing_if = "Option::is_none"
65    )]
66    pub service_tier: Option<Option<ResponseServiceTier>>,
67    #[serde(
68        default,
69        deserialize_with = "present_nullable",
70        skip_serializing_if = "Option::is_none"
71    )]
72    pub system_fingerprint: Option<Option<String>>,
73    #[serde(
74        default,
75        deserialize_with = "present_nullable",
76        skip_serializing_if = "Option::is_none"
77    )]
78    pub usage: Option<Option<ChunkUsage>>,
79    #[serde(
80        default,
81        deserialize_with = "present_nullable",
82        skip_serializing_if = "Option::is_none"
83    )]
84    pub obfuscation: Option<Option<String>>,
85    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
86    pub rest: Rest,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
91#[derive(gproxy_protocol_macros::DeclaredFields)]
92#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
93pub enum ChunkObject {
94    #[serde(rename = "chat.completion.chunk")]
95    ChatCompletionChunk,
96}
97
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
99#[serde(rename_all = "snake_case")]
100#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
101#[derive(gproxy_protocol_macros::DeclaredFields)]
102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
103pub struct StreamChoice {
104    pub index: i64,
105    pub delta: Delta,
106    #[serde(
107        default,
108        deserialize_with = "present_nullable",
109        skip_serializing_if = "Option::is_none"
110    )]
111    pub logprobs: Option<Option<ChunkLogprobs>>,
112    #[serde(
113        default,
114        deserialize_with = "present_nullable",
115        skip_serializing_if = "Option::is_none"
116    )]
117    pub finish_reason: Option<Option<FinishReason>>,
118    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
119    pub rest: Rest,
120}
121
122#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
123#[serde(rename_all = "snake_case")]
124#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
125#[derive(gproxy_protocol_macros::DeclaredFields)]
126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127pub struct Delta {
128    #[serde(
129        default,
130        deserialize_with = "present_nullable",
131        skip_serializing_if = "Option::is_none"
132    )]
133    pub reasoning_details: Option<Option<Vec<super::reasoning::ReasoningDetail>>>,
134    /// Plain-text reasoning returned by compatible providers (for example DeepSeek).
135    #[serde(
136        default,
137        deserialize_with = "present_nullable",
138        skip_serializing_if = "Option::is_none"
139    )]
140    pub reasoning_content: Option<Option<String>>,
141    /// Alternative compatible-provider spelling. Kept separately on the wire.
142    #[serde(
143        default,
144        deserialize_with = "present_nullable",
145        skip_serializing_if = "Option::is_none"
146    )]
147    pub reasoning: Option<Option<String>>,
148    #[serde(
149        default,
150        deserialize_with = "present_nullable",
151        skip_serializing_if = "Option::is_none"
152    )]
153    pub role: Option<Option<DeltaRole>>,
154    #[serde(
155        default,
156        deserialize_with = "present_nullable",
157        skip_serializing_if = "Option::is_none"
158    )]
159    pub content: Option<Option<String>>,
160    #[serde(
161        default,
162        deserialize_with = "present_nullable",
163        skip_serializing_if = "Option::is_none"
164    )]
165    pub refusal: Option<Option<String>>,
166    #[serde(
167        default,
168        deserialize_with = "present_nullable",
169        skip_serializing_if = "Option::is_none"
170    )]
171    pub function_call: Option<Option<DeltaFunctionCall>>,
172    #[serde(
173        default,
174        deserialize_with = "present_nullable",
175        skip_serializing_if = "Option::is_none"
176    )]
177    pub tool_calls: Option<Option<Vec<DeltaToolCall>>>,
178    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
179    pub rest: Rest,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
184#[derive(gproxy_protocol_macros::DeclaredFields)]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186pub enum DeltaRole {
187    #[serde(rename = "developer")]
188    Developer,
189    #[serde(rename = "system")]
190    System,
191    #[serde(rename = "user")]
192    User,
193    #[serde(rename = "tool")]
194    Tool,
195    #[serde(rename = "assistant")]
196    Assistant,
197}
198
199#[derive(
200    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder,
201)]
202#[serde(rename_all = "snake_case")]
203#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
204#[derive(gproxy_protocol_macros::DeclaredFields)]
205#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
206pub struct DeltaFunctionCall {
207    #[serde(
208        default,
209        deserialize_with = "present_nullable",
210        skip_serializing_if = "Option::is_none"
211    )]
212    pub name: Option<Option<String>>,
213    #[serde(
214        default,
215        deserialize_with = "present_nullable",
216        skip_serializing_if = "Option::is_none"
217    )]
218    pub arguments: Option<Option<String>>,
219    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
220    pub rest: Rest,
221}
222
223#[derive(
224    Debug, Clone, PartialEq, Eq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder,
225)]
226#[serde(rename_all = "snake_case")]
227#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
228#[derive(gproxy_protocol_macros::DeclaredFields)]
229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
230pub struct DeltaToolCall {
231    pub index: i64,
232    #[serde(
233        default,
234        deserialize_with = "present_nullable",
235        skip_serializing_if = "Option::is_none"
236    )]
237    pub id: Option<Option<String>>,
238    #[serde(
239        default,
240        deserialize_with = "present_nullable",
241        skip_serializing_if = "Option::is_none",
242        rename = "type"
243    )]
244    pub type_: Option<Option<DeltaToolCallType>>,
245    #[serde(
246        default,
247        deserialize_with = "present_nullable",
248        skip_serializing_if = "Option::is_none"
249    )]
250    pub function: Option<Option<DeltaFunctionCall>>,
251    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
252    pub rest: Rest,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
257#[derive(gproxy_protocol_macros::DeclaredFields)]
258#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
259pub enum DeltaToolCallType {
260    #[serde(rename = "function")]
261    Function,
262}
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
265#[serde(rename_all = "snake_case")]
266#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
267#[derive(gproxy_protocol_macros::DeclaredFields)]
268#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
269pub struct ChunkLogprobs {
270    #[serde(
271        default,
272        deserialize_with = "present_nullable",
273        skip_serializing_if = "Option::is_none"
274    )]
275    pub content: Option<Option<Vec<ChunkTokenLogprob>>>,
276    #[serde(
277        default,
278        deserialize_with = "present_nullable",
279        skip_serializing_if = "Option::is_none"
280    )]
281    pub refusal: Option<Option<Vec<ChunkTokenLogprob>>>,
282    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
283    pub rest: Rest,
284}
285
286#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
287#[serde(rename_all = "snake_case")]
288#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
289#[derive(gproxy_protocol_macros::DeclaredFields)]
290#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
291pub struct ChunkTokenLogprob {
292    pub token: String,
293    #[serde(
294        default,
295        deserialize_with = "present_nullable",
296        skip_serializing_if = "Option::is_none"
297    )]
298    pub bytes: Option<Option<Vec<i64>>>,
299    pub logprob: f64,
300    pub top_logprobs: Vec<ChunkTopLogprob>,
301    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
302    pub rest: Rest,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
306#[serde(rename_all = "snake_case")]
307#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
308#[derive(gproxy_protocol_macros::DeclaredFields)]
309#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
310pub struct ChunkTopLogprob {
311    pub token: String,
312    #[serde(
313        default,
314        deserialize_with = "present_nullable",
315        skip_serializing_if = "Option::is_none"
316    )]
317    pub bytes: Option<Option<Vec<i64>>>,
318    pub logprob: f64,
319    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
320    pub rest: Rest,
321}
322
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
324#[serde(rename_all = "snake_case")]
325#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
326#[derive(gproxy_protocol_macros::DeclaredFields)]
327#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
328pub struct ChunkUsage {
329    pub completion_tokens: i64,
330    pub prompt_tokens: i64,
331    pub total_tokens: i64,
332    #[serde(
333        default,
334        deserialize_with = "present_nullable",
335        skip_serializing_if = "Option::is_none"
336    )]
337    pub completion_tokens_details: Option<Option<ChunkCompletionTokensDetails>>,
338    #[serde(
339        default,
340        deserialize_with = "present_nullable",
341        skip_serializing_if = "Option::is_none"
342    )]
343    pub prompt_tokens_details: Option<Option<ChunkPromptTokensDetails>>,
344    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
345    pub rest: Rest,
346}
347
348#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
349#[serde(rename_all = "snake_case")]
350#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
351#[derive(gproxy_protocol_macros::DeclaredFields)]
352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
353pub struct ChunkCompletionTokensDetails {
354    #[serde(
355        default,
356        deserialize_with = "present_nullable",
357        skip_serializing_if = "Option::is_none"
358    )]
359    pub accepted_prediction_tokens: Option<Option<i64>>,
360    #[serde(
361        default,
362        deserialize_with = "present_nullable",
363        skip_serializing_if = "Option::is_none"
364    )]
365    pub audio_tokens: Option<Option<i64>>,
366    #[serde(
367        default,
368        deserialize_with = "present_nullable",
369        skip_serializing_if = "Option::is_none"
370    )]
371    pub reasoning_tokens: Option<Option<i64>>,
372    #[serde(
373        default,
374        deserialize_with = "present_nullable",
375        skip_serializing_if = "Option::is_none"
376    )]
377    pub rejected_prediction_tokens: Option<Option<i64>>,
378    #[serde(
379        default,
380        deserialize_with = "present_nullable",
381        skip_serializing_if = "Option::is_none"
382    )]
383    pub text_tokens: Option<Option<i64>>,
384    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
385    pub rest: Rest,
386}
387
388#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, gproxy_protocol_macros::WireBuilder)]
389#[serde(rename_all = "snake_case")]
390#[cfg_attr(not(feature = "exhaustive"), non_exhaustive)]
391#[derive(gproxy_protocol_macros::DeclaredFields)]
392#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
393pub struct ChunkPromptTokensDetails {
394    #[serde(
395        default,
396        deserialize_with = "present_nullable",
397        skip_serializing_if = "Option::is_none"
398    )]
399    pub audio_tokens: Option<Option<i64>>,
400    #[serde(
401        default,
402        deserialize_with = "present_nullable",
403        skip_serializing_if = "Option::is_none"
404    )]
405    pub cache_write_tokens: Option<Option<i64>>,
406    #[serde(
407        default,
408        deserialize_with = "present_nullable",
409        skip_serializing_if = "Option::is_none"
410    )]
411    pub cached_tokens: Option<Option<i64>>,
412    #[serde(
413        default,
414        deserialize_with = "present_nullable",
415        skip_serializing_if = "Option::is_none"
416    )]
417    pub image_tokens: Option<Option<i64>>,
418    #[serde(
419        default,
420        deserialize_with = "present_nullable",
421        skip_serializing_if = "Option::is_none"
422    )]
423    pub text_tokens: Option<Option<i64>>,
424    #[serde(default, flatten, skip_serializing_if = "serde_json::Map::is_empty")]
425    pub rest: Rest,
426}