Skip to main content

openai_protocol/
chat.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use validator::Validate;
6
7use super::{
8    common::{
9        default_true, deserialize_null_as_false, validate_stop, ChatLogProbs, ContentPart,
10        Function, FunctionCall, FunctionChoice, GenerationRequest, ResponseFormat, StreamOptions,
11        StringOrArray, Tool, ToolCall, ToolCallDelta, ToolChoice, ToolChoiceValue, ToolReference,
12        Usage,
13    },
14    sampling_params::{validate_top_k_value, validate_top_p_value},
15};
16use crate::{
17    builders::{ChatCompletionResponseBuilder, ChatCompletionStreamResponseBuilder},
18    validated::Normalizable,
19};
20
21// ============================================================================
22// Chat Messages
23// ============================================================================
24
25#[serde_with::skip_serializing_none]
26#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
27#[serde(tag = "role")]
28pub enum ChatMessage {
29    #[serde(rename = "system")]
30    System {
31        content: MessageContent,
32        name: Option<String>,
33    },
34    #[serde(rename = "user")]
35    User {
36        content: MessageContent,
37        name: Option<String>,
38    },
39    #[serde(rename = "assistant")]
40    Assistant {
41        content: Option<MessageContent>,
42        name: Option<String>,
43        tool_calls: Option<Vec<ToolCall>>,
44        /// Reasoning content for O1-style models (SGLang extension)
45        reasoning_content: Option<String>,
46    },
47    #[serde(rename = "tool")]
48    Tool {
49        content: MessageContent,
50        tool_call_id: String,
51    },
52    #[serde(rename = "function")]
53    Function { content: String, name: String },
54    #[serde(rename = "developer")]
55    Developer {
56        content: MessageContent,
57        tools: Option<Vec<Tool>>,
58        name: Option<String>,
59    },
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
63#[serde(untagged)]
64pub enum MessageContent {
65    Text(String),
66    Parts(Vec<ContentPart>),
67}
68
69impl MessageContent {
70    /// Returns the text content, cloning only when necessary.
71    /// For simple text, returns a clone of the string.
72    /// For parts, concatenates text parts with spaces.
73    /// Optimized to avoid intermediate Vec allocation.
74    pub fn to_simple_string(&self) -> String {
75        match self {
76            MessageContent::Text(text) => text.clone(),
77            MessageContent::Parts(parts) => {
78                // Use fold to build string directly without intermediate Vec allocation
79                let mut result = String::new();
80                let mut first = true;
81                for part in parts {
82                    if let ContentPart::Text { text } = part {
83                        if !first {
84                            result.push(' ');
85                        }
86                        result.push_str(text);
87                        first = false;
88                    }
89                }
90                result
91            }
92        }
93    }
94
95    /// Appends text content directly to a buffer, avoiding intermediate allocations.
96    /// Returns true if any content was appended.
97    #[inline]
98    pub fn append_text_to(&self, buffer: &mut String) -> bool {
99        match self {
100            MessageContent::Text(text) => {
101                if text.is_empty() {
102                    false
103                } else {
104                    buffer.push_str(text);
105                    true
106                }
107            }
108            MessageContent::Parts(parts) => {
109                let mut appended = false;
110                for part in parts {
111                    if let ContentPart::Text { text } = part {
112                        if !text.is_empty() {
113                            if appended {
114                                buffer.push(' ');
115                            }
116                            buffer.push_str(text);
117                            appended = true;
118                        }
119                    }
120                }
121                appended
122            }
123        }
124    }
125
126    /// Returns true if this content contains any non-empty text.
127    #[inline]
128    pub fn has_text(&self) -> bool {
129        match self {
130            MessageContent::Text(text) => !text.is_empty(),
131            MessageContent::Parts(parts) => parts
132                .iter()
133                .any(|part| matches!(part, ContentPart::Text { text } if !text.is_empty())),
134        }
135    }
136}
137
138// ============================================================================
139// Chat Completion Request
140// ============================================================================
141
142#[serde_with::skip_serializing_none]
143#[derive(Debug, Clone, Deserialize, Serialize, Default, Validate, schemars::JsonSchema)]
144#[validate(schema(function = "validate_chat_cross_parameters"))]
145pub struct ChatCompletionRequest {
146    /// A list of messages comprising the conversation so far
147    #[validate(custom(function = "validate_messages"))]
148    pub messages: Vec<ChatMessage>,
149
150    /// ID of the model to use
151    pub model: String,
152
153    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far
154    #[validate(range(min = -2.0, max = 2.0))]
155    pub frequency_penalty: Option<f32>,
156
157    /// Deprecated: Replaced by tool_choice
158    #[deprecated(note = "Use tool_choice instead")]
159    pub function_call: Option<FunctionCall>,
160
161    /// Deprecated: Replaced by tools
162    #[deprecated(note = "Use tools instead")]
163    pub functions: Option<Vec<Function>>,
164
165    /// Modify the likelihood of specified tokens appearing in the completion
166    pub logit_bias: Option<HashMap<String, f32>>,
167
168    /// Whether to return log probabilities of the output tokens
169    #[serde(default, deserialize_with = "deserialize_null_as_false")]
170    pub logprobs: bool,
171
172    /// Deprecated: Replaced by max_completion_tokens
173    #[deprecated(note = "Use max_completion_tokens instead")]
174    #[validate(range(min = 1))]
175    pub max_tokens: Option<u32>,
176
177    /// An upper bound for the number of tokens that can be generated for a completion
178    #[validate(range(min = 1))]
179    pub max_completion_tokens: Option<u32>,
180
181    /// Developer-defined tags and values used for filtering completions in the dashboard
182    pub metadata: Option<HashMap<String, String>>,
183
184    /// Output types that you would like the model to generate for this request
185    pub modalities: Option<Vec<String>>,
186
187    /// How many chat completion choices to generate for each input message
188    #[validate(range(min = 1, max = 10))]
189    pub n: Option<u32>,
190
191    /// Whether to enable parallel function calling during tool use
192    pub parallel_tool_calls: Option<bool>,
193
194    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far
195    #[validate(range(min = -2.0, max = 2.0))]
196    pub presence_penalty: Option<f32>,
197
198    /// Cache key for prompts (beta feature)
199    pub prompt_cache_key: Option<String>,
200
201    /// Effort level for reasoning models (low, medium, high)
202    pub reasoning_effort: Option<String>,
203
204    /// An object specifying the format that the model must output
205    pub response_format: Option<ResponseFormat>,
206
207    /// Safety identifier for content moderation
208    pub safety_identifier: Option<String>,
209
210    /// Deprecated: This feature is in Legacy mode
211    #[deprecated(note = "This feature is in Legacy mode")]
212    pub seed: Option<i64>,
213
214    /// The service tier to use for this request
215    pub service_tier: Option<String>,
216
217    /// Up to 4 sequences where the API will stop generating further tokens
218    #[validate(custom(function = "validate_stop"))]
219    pub stop: Option<StringOrArray>,
220
221    /// If set, partial message deltas will be sent
222    #[serde(default, deserialize_with = "deserialize_null_as_false")]
223    pub stream: bool,
224
225    /// Options for streaming response
226    pub stream_options: Option<StreamOptions>,
227
228    /// What sampling temperature to use, between 0 and 2
229    #[validate(range(min = 0.0, max = 2.0))]
230    pub temperature: Option<f32>,
231
232    /// Controls which (if any) tool is called by the model
233    pub tool_choice: Option<ToolChoice>,
234
235    /// A list of tools the model may call
236    pub tools: Option<Vec<Tool>>,
237
238    /// An integer between 0 and 20 specifying the number of most likely tokens to return
239    #[validate(range(min = 0, max = 20))]
240    pub top_logprobs: Option<u32>,
241
242    /// An alternative to sampling with temperature
243    #[validate(custom(function = "validate_top_p_value"))]
244    pub top_p: Option<f32>,
245
246    /// Verbosity level for debugging
247    pub verbosity: Option<i32>,
248
249    // =============================================================================
250    // Engine-Specific Sampling Parameters
251    // =============================================================================
252    // These parameters are extensions beyond the OpenAI API specification and
253    // control model generation behavior in engine-specific ways.
254    // =============================================================================
255    /// Top-k sampling parameter (-1 to disable)
256    #[validate(custom(function = "validate_top_k_value"))]
257    pub top_k: Option<i32>,
258
259    /// Min-p nucleus sampling parameter
260    #[validate(range(min = 0.0, max = 1.0))]
261    pub min_p: Option<f32>,
262
263    /// Minimum number of tokens to generate
264    #[validate(range(min = 1))]
265    pub min_tokens: Option<u32>,
266
267    /// Repetition penalty for reducing repetitive text
268    #[validate(range(min = 0.0, max = 2.0))]
269    pub repetition_penalty: Option<f32>,
270
271    /// Regex constraint for output generation
272    pub regex: Option<String>,
273
274    /// EBNF grammar constraint for structured output
275    pub ebnf: Option<String>,
276
277    /// Specific token IDs to use as stop conditions
278    pub stop_token_ids: Option<Vec<u32>>,
279
280    /// Skip trimming stop tokens from output
281    #[serde(default)]
282    pub no_stop_trim: bool,
283
284    /// Ignore end-of-sequence tokens during generation
285    #[serde(default)]
286    pub ignore_eos: bool,
287
288    /// Continue generating from final assistant message
289    #[serde(default)]
290    pub continue_final_message: bool,
291
292    /// Skip special tokens during detokenization
293    #[serde(default = "default_true")]
294    pub skip_special_tokens: bool,
295
296    /// Path to LoRA adapter(s) for model customization
297    pub lora_path: Option<String>,
298
299    /// Session parameters for continual prompting
300    pub session_params: Option<HashMap<String, Value>>,
301
302    /// Separate reasoning content from final answer (O1-style models)
303    #[serde(default = "default_true")]
304    pub separate_reasoning: bool,
305
306    /// Stream reasoning tokens during generation
307    #[serde(default = "default_true")]
308    pub stream_reasoning: bool,
309
310    /// Chat template kwargs
311    pub chat_template_kwargs: Option<HashMap<String, Value>>,
312
313    /// Return model hidden states
314    #[serde(default)]
315    pub return_hidden_states: bool,
316
317    /// Random seed for sampling for deterministic outputs
318    pub sampling_seed: Option<u64>,
319
320    /// Additional fields not explicitly defined above (e.g. engine-specific parameters)
321    #[serde(flatten)]
322    pub other: Map<String, Value>,
323}
324
325// ============================================================================
326// Validation Functions
327// ============================================================================
328
329/// Validates messages array is not empty and has valid content
330fn validate_messages(messages: &[ChatMessage]) -> Result<(), validator::ValidationError> {
331    if messages.is_empty() {
332        return Err(validator::ValidationError::new("messages cannot be empty"));
333    }
334
335    for msg in messages {
336        if let ChatMessage::User { content, .. } = msg {
337            match content {
338                MessageContent::Text(text) if text.is_empty() => {
339                    return Err(validator::ValidationError::new(
340                        "message content cannot be empty",
341                    ));
342                }
343                MessageContent::Parts(parts) if parts.is_empty() => {
344                    return Err(validator::ValidationError::new(
345                        "message content parts cannot be empty",
346                    ));
347                }
348                _ => {}
349            }
350        }
351    }
352    Ok(())
353}
354
355/// Schema-level validation for cross-field dependencies
356fn validate_chat_cross_parameters(
357    req: &ChatCompletionRequest,
358) -> Result<(), validator::ValidationError> {
359    // 1. Validate logprobs dependency
360    if req.top_logprobs.is_some() && !req.logprobs {
361        let mut e = validator::ValidationError::new("top_logprobs_requires_logprobs");
362        e.message = Some("top_logprobs is only allowed when logprobs is enabled".into());
363        return Err(e);
364    }
365
366    // 2. Validate stream_options dependency
367    if req.stream_options.is_some() && !req.stream {
368        let mut e = validator::ValidationError::new("stream_options_requires_stream");
369        e.message =
370            Some("The 'stream_options' parameter is only allowed when 'stream' is enabled".into());
371        return Err(e);
372    }
373
374    // 3. Validate token limits - min <= max
375    if let (Some(min), Some(max)) = (req.min_tokens, req.max_completion_tokens) {
376        if min > max {
377            let mut e = validator::ValidationError::new("min_tokens_exceeds_max");
378            e.message = Some("min_tokens cannot exceed max_tokens/max_completion_tokens".into());
379            return Err(e);
380        }
381    }
382
383    // 4. Validate structured output conflicts
384    let has_json_format = matches!(
385        req.response_format,
386        Some(ResponseFormat::JsonObject | ResponseFormat::JsonSchema { .. })
387    );
388
389    if has_json_format && req.regex.is_some() {
390        let mut e = validator::ValidationError::new("regex_conflicts_with_json");
391        e.message = Some("cannot use regex constraint with JSON response format".into());
392        return Err(e);
393    }
394
395    if has_json_format && req.ebnf.is_some() {
396        let mut e = validator::ValidationError::new("ebnf_conflicts_with_json");
397        e.message = Some("cannot use EBNF constraint with JSON response format".into());
398        return Err(e);
399    }
400
401    // 5. Validate mutually exclusive structured output constraints
402    let constraint_count = [
403        req.regex.is_some(),
404        req.ebnf.is_some(),
405        matches!(req.response_format, Some(ResponseFormat::JsonSchema { .. })),
406    ]
407    .iter()
408    .filter(|&&x| x)
409    .count();
410
411    if constraint_count > 1 {
412        let mut e = validator::ValidationError::new("multiple_constraints");
413        e.message = Some("only one structured output constraint (regex, ebnf, or json_schema) can be active at a time".into());
414        return Err(e);
415    }
416
417    // 6. Validate response format JSON schema name
418    if let Some(ResponseFormat::JsonSchema { json_schema }) = &req.response_format {
419        if json_schema.name.is_empty() {
420            let mut e = validator::ValidationError::new("json_schema_name_empty");
421            e.message = Some("JSON schema name cannot be empty".into());
422            return Err(e);
423        }
424    }
425
426    // 7. Validate tool_choice requires tools (except for "none")
427    if let Some(ref tool_choice) = req.tool_choice {
428        let has_tools = req.tools.as_ref().is_some_and(|t| !t.is_empty());
429
430        // Check if tool_choice is anything other than "none"
431        let is_some_choice = !matches!(tool_choice, ToolChoice::Value(ToolChoiceValue::None));
432
433        if is_some_choice && !has_tools {
434            let mut e = validator::ValidationError::new("tool_choice_requires_tools");
435            e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
436            return Err(e);
437        }
438
439        // Additional validation when tools are present
440        if let Some(tools) = req.tools.as_ref().filter(|t| !t.is_empty()) {
441            match tool_choice {
442                ToolChoice::Function { function, .. } => {
443                    // Validate that the specified function name exists in tools
444                    let function_exists = tools.iter().any(|tool| {
445                        tool.tool_type == "function" && tool.function.name == function.name
446                    });
447
448                    if !function_exists {
449                        let mut e =
450                            validator::ValidationError::new("tool_choice_function_not_found");
451                        e.message = Some(
452                            format!(
453                            "Invalid value for 'tool_choice': function '{}' not found in 'tools'.",
454                            function.name
455                        )
456                            .into(),
457                        );
458                        return Err(e);
459                    }
460                }
461                ToolChoice::AllowedTools {
462                    mode,
463                    tools: allowed_tools,
464                    ..
465                } => {
466                    // Validate mode is "auto" or "required"
467                    if mode != "auto" && mode != "required" {
468                        let mut e = validator::ValidationError::new("tool_choice_invalid_mode");
469                        e.message = Some(format!(
470                            "Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
471                        ).into());
472                        return Err(e);
473                    }
474
475                    // Validate that all ToolReferences are Function type (Chat API only supports function tools)
476                    for tool_ref in allowed_tools {
477                        match tool_ref {
478                            ToolReference::Function { name } => {
479                                // Validate that the function exists in tools array
480                                let tool_exists = tools.iter().any(|tool| {
481                                    tool.tool_type == "function" && tool.function.name == *name
482                                });
483
484                                if !tool_exists {
485                                    let mut e = validator::ValidationError::new(
486                                        "tool_choice_tool_not_found",
487                                    );
488                                    e.message = Some(
489                                        format!(
490                                            "Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
491                                        )
492                                        .into(),
493                                    );
494                                    return Err(e);
495                                }
496                            }
497                            _ => {
498                                // Chat Completion API only supports function tools in tool_choice
499                                let mut e = validator::ValidationError::new(
500                                    "tool_choice_invalid_tool_type",
501                                );
502                                e.message = Some(
503                                    format!(
504                                        "Invalid value for 'tool_choice.tools': Chat Completion API only supports function tools, got '{}'.",
505                                        tool_ref.identifier()
506                                    )
507                                    .into(),
508                                );
509                                return Err(e);
510                            }
511                        }
512                    }
513                }
514                ToolChoice::Value(_) => {}
515            }
516        }
517    }
518
519    Ok(())
520}
521
522// ============================================================================
523// Normalizable Implementation
524// ============================================================================
525
526impl Normalizable for ChatCompletionRequest {
527    /// Normalize the request by applying migrations and defaults:
528    /// 1. Migrate deprecated fields to their replacements
529    /// 2. Clear deprecated fields and log warnings
530    /// 3. Apply OpenAI defaults for tool_choice
531    fn normalize(&mut self) {
532        // Migrate deprecated max_tokens → max_completion_tokens
533        #[expect(deprecated)]
534        if self.max_completion_tokens.is_none() && self.max_tokens.is_some() {
535            self.max_completion_tokens = self.max_tokens;
536            self.max_tokens = None; // Clear deprecated field
537        }
538
539        // Migrate deprecated functions → tools
540        #[expect(deprecated)]
541        if self.tools.is_none() && self.functions.is_some() {
542            tracing::warn!("functions is deprecated, use tools instead");
543            self.tools = self.functions.as_ref().map(|functions| {
544                functions
545                    .iter()
546                    .map(|func| Tool {
547                        tool_type: "function".to_string(),
548                        function: func.clone(),
549                    })
550                    .collect()
551            });
552            self.functions = None; // Clear deprecated field
553        }
554
555        // Migrate deprecated function_call → tool_choice
556        #[expect(deprecated)]
557        if self.tool_choice.is_none() && self.function_call.is_some() {
558            tracing::warn!("function_call is deprecated, use tool_choice instead");
559            self.tool_choice = self.function_call.as_ref().map(|fc| match fc {
560                FunctionCall::None => ToolChoice::Value(ToolChoiceValue::None),
561                FunctionCall::Auto => ToolChoice::Value(ToolChoiceValue::Auto),
562                FunctionCall::Function { name } => ToolChoice::Function {
563                    tool_type: "function".to_string(),
564                    function: FunctionChoice { name: name.clone() },
565                },
566            });
567            self.function_call = None; // Clear deprecated field
568        }
569
570        // Apply tool_choice defaults
571        if self.tool_choice.is_none() {
572            if let Some(tools) = &self.tools {
573                let choice_value = if tools.is_empty() {
574                    ToolChoiceValue::None
575                } else {
576                    ToolChoiceValue::Auto
577                };
578                self.tool_choice = Some(ToolChoice::Value(choice_value));
579            }
580            // If tools is None, leave tool_choice as None (don't set it)
581        }
582    }
583}
584
585// ============================================================================
586// GenerationRequest Trait Implementation
587// ============================================================================
588
589impl GenerationRequest for ChatCompletionRequest {
590    fn is_stream(&self) -> bool {
591        self.stream
592    }
593
594    fn get_model(&self) -> Option<&str> {
595        Some(&self.model)
596    }
597
598    fn extract_text_for_routing(&self) -> String {
599        // Extract text from messages for routing decisions
600        // Use a single buffer to avoid intermediate Vec<String> allocations
601        let mut buffer = String::new();
602        let mut has_content = false;
603
604        for msg in &self.messages {
605            match msg {
606                ChatMessage::System { content, .. }
607                | ChatMessage::User { content, .. }
608                | ChatMessage::Tool { content, .. }
609                | ChatMessage::Developer { content, .. } => {
610                    if has_content && content.has_text() {
611                        buffer.push(' ');
612                    }
613                    if content.append_text_to(&mut buffer) {
614                        has_content = true;
615                    }
616                }
617                ChatMessage::Assistant {
618                    content,
619                    reasoning_content,
620                    ..
621                } => {
622                    // Append main content
623                    if let Some(c) = content {
624                        if has_content && c.has_text() {
625                            buffer.push(' ');
626                        }
627                        if c.append_text_to(&mut buffer) {
628                            has_content = true;
629                        }
630                    }
631                    // Append reasoning content
632                    if let Some(reasoning) = reasoning_content {
633                        if !reasoning.is_empty() {
634                            if has_content {
635                                buffer.push(' ');
636                            }
637                            buffer.push_str(reasoning);
638                            has_content = true;
639                        }
640                    }
641                }
642                ChatMessage::Function { content, .. } => {
643                    if !content.is_empty() {
644                        if has_content {
645                            buffer.push(' ');
646                        }
647                        buffer.push_str(content);
648                        has_content = true;
649                    }
650                }
651            }
652        }
653
654        buffer
655    }
656}
657
658// ============================================================================
659// Response Types
660// ============================================================================
661
662#[serde_with::skip_serializing_none]
663#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
664pub struct ChatCompletionResponse {
665    pub id: String,
666    pub object: String, // "chat.completion"
667    pub created: u64,
668    pub model: String,
669    pub choices: Vec<ChatChoice>,
670    pub usage: Option<Usage>,
671    pub system_fingerprint: Option<String>,
672}
673
674impl ChatCompletionResponse {
675    /// Create a new builder for ChatCompletionResponse
676    pub fn builder(
677        id: impl Into<String>,
678        model: impl Into<String>,
679    ) -> ChatCompletionResponseBuilder {
680        ChatCompletionResponseBuilder::new(id, model)
681    }
682}
683
684/// Response message structure for ChatCompletionResponse (different from request ChatMessage)
685#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
686pub struct ChatCompletionMessage {
687    pub role: String, // Always "assistant" for responses
688    #[serde(skip_serializing_if = "Option::is_none")]
689    pub content: Option<String>,
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub tool_calls: Option<Vec<ToolCall>>,
692    pub reasoning_content: Option<String>,
693    // Note: function_call is deprecated and not included
694    // Note: refusal, annotations, audio are not added yet
695}
696
697#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
698pub struct ChatChoice {
699    pub index: u32,
700    pub message: ChatCompletionMessage,
701    #[serde(skip_serializing_if = "Option::is_none")]
702    pub logprobs: Option<ChatLogProbs>,
703    pub finish_reason: Option<String>, // "stop", "length", "tool_calls", "content_filter", "function_call"
704    /// Information about which stop condition was matched
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub matched_stop: Option<Value>, // Can be string or integer
707    /// Hidden states from the model (SGLang extension)
708    #[serde(skip_serializing_if = "Option::is_none")]
709    pub hidden_states: Option<Vec<f32>>,
710}
711
712#[serde_with::skip_serializing_none]
713#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
714pub struct ChatCompletionStreamResponse {
715    pub id: String,
716    pub object: String, // "chat.completion.chunk"
717    pub created: u64,
718    pub model: String,
719    pub system_fingerprint: Option<String>,
720    pub choices: Vec<ChatStreamChoice>,
721    pub usage: Option<Usage>,
722}
723
724impl ChatCompletionStreamResponse {
725    /// Create a new builder for ChatCompletionStreamResponse
726    pub fn builder(
727        id: impl Into<String>,
728        model: impl Into<String>,
729    ) -> ChatCompletionStreamResponseBuilder {
730        ChatCompletionStreamResponseBuilder::new(id, model)
731    }
732}
733
734/// Delta structure for streaming chat completion responses
735#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
736pub struct ChatMessageDelta {
737    #[serde(skip_serializing_if = "Option::is_none")]
738    pub role: Option<String>,
739    #[serde(skip_serializing_if = "Option::is_none")]
740    pub content: Option<String>,
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub tool_calls: Option<Vec<ToolCallDelta>>,
743    pub reasoning_content: Option<String>,
744}
745
746#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
747pub struct ChatStreamChoice {
748    pub index: u32,
749    pub delta: ChatMessageDelta,
750    pub logprobs: Option<ChatLogProbs>,
751    pub finish_reason: Option<String>,
752    #[serde(skip_serializing_if = "Option::is_none")]
753    pub matched_stop: Option<Value>,
754}