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