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, is_false, is_true, validate_stop, ChatLogProbs,
10        ContentPart, Function, FunctionCall, FunctionChoice, GenerationRequest, ResponseFormat,
11        StreamOptions, StringOrArray, Tool, ToolCall, ToolCallDelta, ToolChoice, ToolChoiceValue,
12        ToolReference, 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    /// Whether to return audio output.
186    pub return_audio: Option<bool>,
187
188    /// How many chat completion choices to generate for each input message
189    #[validate(range(min = 1, max = 10))]
190    pub n: Option<u32>,
191
192    /// Whether to enable parallel function calling during tool use
193    pub parallel_tool_calls: Option<bool>,
194
195    /// Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far
196    #[validate(range(min = -2.0, max = 2.0))]
197    pub presence_penalty: Option<f32>,
198
199    /// Cache key for prompts (beta feature)
200    pub prompt_cache_key: Option<String>,
201
202    /// Effort level for reasoning models.
203    ///
204    /// OpenAI-compatible callers normally send a named string, while some
205    /// model integrations accept a numeric value. Keep the public Rust shape
206    /// as a string for compatibility, but accept either JSON representation at
207    /// the HTTP boundary; model-specific normalization happens in the gateway.
208    #[serde(default, deserialize_with = "deserialize_reasoning_effort")]
209    pub reasoning_effort: Option<String>,
210
211    /// An object specifying the format that the model must output
212    pub response_format: Option<ResponseFormat>,
213
214    /// Safety identifier for content moderation
215    pub safety_identifier: Option<String>,
216
217    /// Deprecated: This feature is in Legacy mode
218    #[deprecated(note = "This feature is in Legacy mode")]
219    pub seed: Option<i64>,
220
221    /// The service tier to use for this request
222    pub service_tier: Option<String>,
223
224    /// Up to 4 sequences where the API will stop generating further tokens
225    #[validate(custom(function = "validate_stop"))]
226    pub stop: Option<StringOrArray>,
227
228    /// If set, partial message deltas will be sent
229    #[serde(default, deserialize_with = "deserialize_null_as_false")]
230    pub stream: bool,
231
232    /// Options for streaming response
233    pub stream_options: Option<StreamOptions>,
234
235    /// What sampling temperature to use, between 0 and 2
236    #[validate(range(min = 0.0, max = 2.0))]
237    pub temperature: Option<f32>,
238
239    /// Controls which (if any) tool is called by the model
240    pub tool_choice: Option<ToolChoice>,
241
242    /// A list of tools the model may call
243    pub tools: Option<Vec<Tool>>,
244
245    /// An integer between 0 and 20 specifying the number of most likely tokens to return
246    #[validate(range(min = 0, max = 20))]
247    pub top_logprobs: Option<u32>,
248
249    /// An alternative to sampling with temperature
250    #[validate(custom(function = "validate_top_p_value"))]
251    pub top_p: Option<f32>,
252
253    /// Verbosity level for debugging
254    pub verbosity: Option<i32>,
255
256    // =============================================================================
257    // Engine-Specific Sampling Parameters
258    // =============================================================================
259    // These parameters are extensions beyond the OpenAI API specification and
260    // control model generation behavior in engine-specific ways.
261    // =============================================================================
262    /// Top-k sampling parameter (-1 to disable)
263    #[validate(custom(function = "validate_top_k_value"))]
264    pub top_k: Option<i32>,
265
266    /// Min-p nucleus sampling parameter
267    #[validate(range(min = 0.0, max = 1.0))]
268    pub min_p: Option<f32>,
269
270    /// Minimum number of tokens to generate
271    #[validate(range(min = 0))]
272    pub min_tokens: Option<u32>,
273
274    /// Repetition penalty for reducing repetitive text
275    #[validate(range(min = 0.0, max = 2.0))]
276    pub repetition_penalty: Option<f32>,
277
278    /// Regex constraint for output generation
279    pub regex: Option<String>,
280
281    /// EBNF grammar constraint for structured output
282    pub ebnf: Option<String>,
283
284    /// Specific token IDs to use as stop conditions
285    pub stop_token_ids: Option<Vec<u32>>,
286
287    /// Skip trimming stop tokens from output
288    #[serde(default, skip_serializing_if = "is_false")]
289    pub no_stop_trim: bool,
290
291    /// Ignore end-of-sequence tokens during generation
292    #[serde(default, skip_serializing_if = "is_false")]
293    pub ignore_eos: bool,
294
295    /// Continue generating from final assistant message
296    #[serde(default, skip_serializing_if = "is_false")]
297    pub continue_final_message: bool,
298
299    /// Skip special tokens during detokenization
300    #[serde(default = "default_true")]
301    pub skip_special_tokens: bool,
302
303    /// Path to LoRA adapter(s) for model customization
304    pub lora_path: Option<String>,
305
306    /// Session parameters for continual prompting
307    pub session_params: Option<HashMap<String, Value>>,
308
309    /// Separate reasoning content from final answer (O1-style models)
310    #[serde(default = "default_true", skip_serializing_if = "is_true")]
311    pub separate_reasoning: bool,
312
313    /// Stream reasoning tokens during generation
314    #[serde(default = "default_true", skip_serializing_if = "is_true")]
315    pub stream_reasoning: bool,
316
317    /// Chat template kwargs
318    pub chat_template_kwargs: Option<HashMap<String, Value>>,
319
320    /// Return model hidden states
321    #[serde(default, skip_serializing_if = "is_false")]
322    pub return_hidden_states: bool,
323
324    /// Random seed for sampling for deterministic outputs
325    pub sampling_seed: Option<u64>,
326
327    /// Request ID forwarded to the backend for log correlation (SGLang extension)
328    pub rid: Option<String>,
329
330    /// Additional fields not explicitly defined above (e.g. engine-specific parameters)
331    #[serde(flatten)]
332    pub other: Map<String, Value>,
333}
334
335/// Map an OpenAI `reasoning_effort` to a thinking on/off preference.
336///
337/// This is the protocol-level interpretation of "does the caller want
338/// reasoning?" — independent of any model/template. `reasoning_effort` is a
339/// *level* (`"low"`/`"medium"`/`"high"`) plus the vendor-extension `"none"`.
340///
341/// Both `"none"` and `"minimal"` map to thinking OFF (`Some(false)`).
342/// `"minimal"` is treated as an off-signal deliberately: templates that expose
343/// only a boolean thinking toggle (GLM/Qwen3) cannot do "a little" reasoning,
344/// so the lowest OpenAI level is the closest available "do not reason".
345/// Level values return `None` — no opinion, defer to the template default or an
346/// explicit thinking kwarg.
347pub fn thinking_from_reasoning_effort(reasoning_effort: Option<&str>) -> Option<bool> {
348    match reasoning_effort {
349        Some("none") | Some("minimal") => Some(false),
350        _ => None,
351    }
352}
353
354fn deserialize_reasoning_effort<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
355where
356    D: serde::Deserializer<'de>,
357{
358    let value = Option::<Value>::deserialize(deserializer)?;
359    match value {
360        None | Some(Value::Null) => Ok(None),
361        Some(Value::String(value)) => Ok(Some(value)),
362        Some(Value::Number(value)) => Ok(Some(value.to_string())),
363        Some(_) => Err(serde::de::Error::custom(
364            "reasoning_effort must be a string, number, or null",
365        )),
366    }
367}
368
369// ============================================================================
370// Validation Functions
371// ============================================================================
372
373/// Validates messages array is not empty and has valid content
374fn validate_messages(messages: &[ChatMessage]) -> Result<(), validator::ValidationError> {
375    if messages.is_empty() {
376        return Err(validator::ValidationError::new("messages cannot be empty"));
377    }
378
379    for msg in messages {
380        if let ChatMessage::User { content, .. } = msg {
381            match content {
382                MessageContent::Text(text) if text.is_empty() => {
383                    return Err(validator::ValidationError::new(
384                        "message content cannot be empty",
385                    ));
386                }
387                MessageContent::Parts(parts) if parts.is_empty() => {
388                    return Err(validator::ValidationError::new(
389                        "message content parts cannot be empty",
390                    ));
391                }
392                _ => {}
393            }
394        }
395    }
396    Ok(())
397}
398
399/// Schema-level validation for cross-field dependencies
400fn validate_chat_cross_parameters(
401    req: &ChatCompletionRequest,
402) -> Result<(), validator::ValidationError> {
403    // 1. Validate logprobs dependency
404    if req.top_logprobs.is_some() && !req.logprobs {
405        let mut e = validator::ValidationError::new("top_logprobs_requires_logprobs");
406        e.message = Some("top_logprobs is only allowed when logprobs is enabled".into());
407        return Err(e);
408    }
409
410    // 2. Validate stream_options dependency
411    if req.stream_options.is_some() && !req.stream {
412        let mut e = validator::ValidationError::new("stream_options_requires_stream");
413        e.message =
414            Some("The 'stream_options' parameter is only allowed when 'stream' is enabled".into());
415        return Err(e);
416    }
417
418    // 3. Validate token limits - min <= max
419    if let (Some(min), Some(max)) = (req.min_tokens, req.max_completion_tokens) {
420        if min > max {
421            let mut e = validator::ValidationError::new("min_tokens_exceeds_max");
422            e.message = Some("min_tokens cannot exceed max_tokens/max_completion_tokens".into());
423            return Err(e);
424        }
425    }
426
427    // 4. Validate structured output conflicts
428    let has_json_format = matches!(
429        req.response_format,
430        Some(ResponseFormat::JsonObject | ResponseFormat::JsonSchema { .. })
431    );
432
433    if has_json_format && req.regex.is_some() {
434        let mut e = validator::ValidationError::new("regex_conflicts_with_json");
435        e.message = Some("cannot use regex constraint with JSON response format".into());
436        return Err(e);
437    }
438
439    if has_json_format && req.ebnf.is_some() {
440        let mut e = validator::ValidationError::new("ebnf_conflicts_with_json");
441        e.message = Some("cannot use EBNF constraint with JSON response format".into());
442        return Err(e);
443    }
444
445    // 5. Validate mutually exclusive structured output constraints
446    let constraint_count = [
447        req.regex.is_some(),
448        req.ebnf.is_some(),
449        matches!(req.response_format, Some(ResponseFormat::JsonSchema { .. })),
450    ]
451    .iter()
452    .filter(|&&x| x)
453    .count();
454
455    if constraint_count > 1 {
456        let mut e = validator::ValidationError::new("multiple_constraints");
457        e.message = Some("only one structured output constraint (regex, ebnf, or json_schema) can be active at a time".into());
458        return Err(e);
459    }
460
461    // 6. Validate response format JSON schema name
462    if let Some(ResponseFormat::JsonSchema { json_schema }) = &req.response_format {
463        if json_schema.name.is_empty() {
464            let mut e = validator::ValidationError::new("json_schema_name_empty");
465            e.message = Some("JSON schema name cannot be empty".into());
466            return Err(e);
467        }
468    }
469
470    // 7. Validate tool_choice requires tools (except for "none")
471    if let Some(ref tool_choice) = req.tool_choice {
472        let has_tools = req.tools.as_ref().is_some_and(|t| !t.is_empty());
473
474        // Check if tool_choice is anything other than "none"
475        let is_some_choice = !matches!(tool_choice, ToolChoice::Value(ToolChoiceValue::None));
476
477        if is_some_choice && !has_tools {
478            let mut e = validator::ValidationError::new("tool_choice_requires_tools");
479            e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
480            return Err(e);
481        }
482
483        // Additional validation when tools are present
484        if let Some(tools) = req.tools.as_ref().filter(|t| !t.is_empty()) {
485            match tool_choice {
486                ToolChoice::Function { function, .. } => {
487                    // Validate that the specified function name exists in tools
488                    let function_exists = tools.iter().any(|tool| {
489                        tool.tool_type == "function" && tool.function.name == function.name
490                    });
491
492                    if !function_exists {
493                        let mut e =
494                            validator::ValidationError::new("tool_choice_function_not_found");
495                        e.message = Some(
496                            format!(
497                            "Invalid value for 'tool_choice': function '{}' not found in 'tools'.",
498                            function.name
499                        )
500                            .into(),
501                        );
502                        return Err(e);
503                    }
504                }
505                ToolChoice::AllowedTools {
506                    mode,
507                    tools: allowed_tools,
508                    ..
509                } => {
510                    // Validate mode is "auto" or "required"
511                    if mode != "auto" && mode != "required" {
512                        let mut e = validator::ValidationError::new("tool_choice_invalid_mode");
513                        e.message = Some(format!(
514                            "Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
515                        ).into());
516                        return Err(e);
517                    }
518
519                    // Validate that all ToolReferences are Function type (Chat API only supports function tools)
520                    for tool_ref in allowed_tools {
521                        match tool_ref {
522                            ToolReference::Function { name } => {
523                                // Validate that the function exists in tools array
524                                let tool_exists = tools.iter().any(|tool| {
525                                    tool.tool_type == "function" && tool.function.name == *name
526                                });
527
528                                if !tool_exists {
529                                    let mut e = validator::ValidationError::new(
530                                        "tool_choice_tool_not_found",
531                                    );
532                                    e.message = Some(
533                                        format!(
534                                            "Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
535                                        )
536                                        .into(),
537                                    );
538                                    return Err(e);
539                                }
540                            }
541                            _ => {
542                                // Chat Completion API only supports function tools in tool_choice
543                                let mut e = validator::ValidationError::new(
544                                    "tool_choice_invalid_tool_type",
545                                );
546                                e.message = Some(
547                                    format!(
548                                        "Invalid value for 'tool_choice.tools': Chat Completion API only supports function tools, got '{}'.",
549                                        tool_ref.identifier()
550                                    )
551                                    .into(),
552                                );
553                                return Err(e);
554                            }
555                        }
556                    }
557                }
558                ToolChoice::Value(_) => {}
559            }
560        }
561    }
562
563    Ok(())
564}
565
566// ============================================================================
567// Normalizable Implementation
568// ============================================================================
569
570impl Normalizable for ChatCompletionRequest {
571    /// Normalize the request by applying migrations and defaults:
572    /// 1. Migrate deprecated fields to their replacements
573    /// 2. Clear deprecated fields and log warnings
574    /// 3. Apply OpenAI defaults for tool_choice
575    fn normalize(&mut self) {
576        // Migrate deprecated max_tokens → max_completion_tokens
577        #[expect(deprecated)]
578        if self.max_completion_tokens.is_none() && self.max_tokens.is_some() {
579            self.max_completion_tokens = self.max_tokens;
580            self.max_tokens = None; // Clear deprecated field
581        }
582
583        // Migrate deprecated functions → tools
584        #[expect(deprecated)]
585        if self.tools.is_none() && self.functions.is_some() {
586            tracing::warn!("functions is deprecated, use tools instead");
587            self.tools = self.functions.as_ref().map(|functions| {
588                functions
589                    .iter()
590                    .map(|func| Tool {
591                        tool_type: "function".to_string(),
592                        function: func.clone(),
593                    })
594                    .collect()
595            });
596            self.functions = None; // Clear deprecated field
597        }
598
599        // Migrate deprecated function_call → tool_choice
600        #[expect(deprecated)]
601        if self.tool_choice.is_none() && self.function_call.is_some() {
602            tracing::warn!("function_call is deprecated, use tool_choice instead");
603            self.tool_choice = self.function_call.as_ref().map(|fc| match fc {
604                FunctionCall::None => ToolChoice::Value(ToolChoiceValue::None),
605                FunctionCall::Auto => ToolChoice::Value(ToolChoiceValue::Auto),
606                FunctionCall::Function { name } => ToolChoice::Function {
607                    tool_type: "function".to_string(),
608                    function: FunctionChoice { name: name.clone() },
609                },
610            });
611            self.function_call = None; // Clear deprecated field
612        }
613
614        // Apply tool_choice defaults
615        if self.tool_choice.is_none() {
616            if let Some(tools) = &self.tools {
617                let choice_value = if tools.is_empty() {
618                    ToolChoiceValue::None
619                } else {
620                    ToolChoiceValue::Auto
621                };
622                self.tool_choice = Some(ToolChoice::Value(choice_value));
623            }
624            // If tools is None, leave tool_choice as None (don't set it)
625        }
626    }
627}
628
629// ============================================================================
630// GenerationRequest Trait Implementation
631// ============================================================================
632
633impl GenerationRequest for ChatCompletionRequest {
634    fn rid(&self) -> Option<&str> {
635        self.rid.as_deref()
636    }
637
638    fn is_stream(&self) -> bool {
639        self.stream
640    }
641
642    fn get_model(&self) -> Option<&str> {
643        Some(&self.model)
644    }
645
646    fn extract_text_for_routing(&self) -> String {
647        // Extract text from messages for routing decisions
648        // Use a single buffer to avoid intermediate Vec<String> allocations
649        let mut buffer = String::new();
650        let mut has_content = false;
651
652        for msg in &self.messages {
653            match msg {
654                ChatMessage::System { content, .. }
655                | ChatMessage::User { content, .. }
656                | ChatMessage::Tool { content, .. }
657                | ChatMessage::Developer { content, .. } => {
658                    if has_content && content.has_text() {
659                        buffer.push(' ');
660                    }
661                    if content.append_text_to(&mut buffer) {
662                        has_content = true;
663                    }
664                }
665                ChatMessage::Assistant {
666                    content,
667                    reasoning_content,
668                    ..
669                } => {
670                    // Append main content
671                    if let Some(c) = content {
672                        if has_content && c.has_text() {
673                            buffer.push(' ');
674                        }
675                        if c.append_text_to(&mut buffer) {
676                            has_content = true;
677                        }
678                    }
679                    // Append reasoning content
680                    if let Some(reasoning) = reasoning_content {
681                        if !reasoning.is_empty() {
682                            if has_content {
683                                buffer.push(' ');
684                            }
685                            buffer.push_str(reasoning);
686                            has_content = true;
687                        }
688                    }
689                }
690                ChatMessage::Function { content, .. } => {
691                    if !content.is_empty() {
692                        if has_content {
693                            buffer.push(' ');
694                        }
695                        buffer.push_str(content);
696                        has_content = true;
697                    }
698                }
699            }
700        }
701
702        buffer
703    }
704}
705
706// ============================================================================
707// Response Types
708// ============================================================================
709
710#[serde_with::skip_serializing_none]
711#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
712pub struct ChatCompletionResponse {
713    pub id: String,
714    pub object: String, // "chat.completion"
715    pub created: u64,
716    pub model: String,
717    pub choices: Vec<ChatChoice>,
718    pub usage: Option<Usage>,
719    pub system_fingerprint: Option<String>,
720}
721
722impl ChatCompletionResponse {
723    /// Create a new builder for ChatCompletionResponse
724    pub fn builder(
725        id: impl Into<String>,
726        model: impl Into<String>,
727    ) -> ChatCompletionResponseBuilder {
728        ChatCompletionResponseBuilder::new(id, model)
729    }
730}
731
732/// Response message structure for ChatCompletionResponse (different from request ChatMessage)
733#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
734pub struct ChatCompletionMessage {
735    pub role: String, // Always "assistant" for responses
736    #[serde(skip_serializing_if = "Option::is_none")]
737    pub content: Option<String>,
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub tool_calls: Option<Vec<ToolCall>>,
740    pub reasoning_content: Option<String>,
741    // Note: function_call is deprecated and not included
742    // Note: refusal, annotations, audio are not added yet
743}
744
745#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
746pub struct ChatChoice {
747    pub index: u32,
748    pub message: ChatCompletionMessage,
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub logprobs: Option<ChatLogProbs>,
751    pub finish_reason: Option<String>, // "stop", "length", "tool_calls", "content_filter", "function_call"
752    /// Information about which stop condition was matched
753    #[serde(skip_serializing_if = "Option::is_none")]
754    pub matched_stop: Option<Value>, // Can be string or integer
755    /// Hidden states from the model (SGLang extension)
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub hidden_states: Option<Vec<f32>>,
758}
759
760#[serde_with::skip_serializing_none]
761#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
762pub struct ChatCompletionStreamResponse {
763    pub id: String,
764    pub object: String, // "chat.completion.chunk"
765    pub created: u64,
766    pub model: String,
767    pub system_fingerprint: Option<String>,
768    pub choices: Vec<ChatStreamChoice>,
769    pub usage: Option<Usage>,
770}
771
772impl ChatCompletionStreamResponse {
773    /// Create a new builder for ChatCompletionStreamResponse
774    pub fn builder(
775        id: impl Into<String>,
776        model: impl Into<String>,
777    ) -> ChatCompletionStreamResponseBuilder {
778        ChatCompletionStreamResponseBuilder::new(id, model)
779    }
780}
781
782/// Delta structure for streaming chat completion responses
783#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
784pub struct ChatMessageDelta {
785    #[serde(skip_serializing_if = "Option::is_none")]
786    pub role: Option<String>,
787    #[serde(skip_serializing_if = "Option::is_none")]
788    pub content: Option<String>,
789    #[serde(skip_serializing_if = "Option::is_none")]
790    pub tool_calls: Option<Vec<ToolCallDelta>>,
791    pub reasoning_content: Option<String>,
792}
793
794#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
795pub struct ChatStreamChoice {
796    pub index: u32,
797    pub delta: ChatMessageDelta,
798    pub logprobs: Option<ChatLogProbs>,
799    pub finish_reason: Option<String>,
800    #[serde(skip_serializing_if = "Option::is_none")]
801    pub matched_stop: Option<Value>,
802}
803
804#[cfg(test)]
805mod tests {
806    use serde_json::{json, Value};
807
808    use super::{thinking_from_reasoning_effort, ChatCompletionRequest};
809
810    fn request_with_output_fields(fields: &[(&str, Value)]) -> ChatCompletionRequest {
811        let mut value = json!({
812            "model": "test-model",
813            "messages": [{"role": "user", "content": "hello"}]
814        });
815        let object = value.as_object_mut().expect("request must be an object");
816        for (name, field_value) in fields {
817            object.insert((*name).to_string(), field_value.clone());
818        }
819        serde_json::from_value(value).expect("request must deserialize")
820    }
821
822    #[test]
823    fn default_sglang_flags_are_omitted_and_absent_reads_defaults() {
824        let request = request_with_output_fields(&[]);
825        let value = serde_json::to_value(&request).expect("serialize");
826        for field in [
827            "no_stop_trim",
828            "ignore_eos",
829            "continue_final_message",
830            "return_hidden_states",
831            "separate_reasoning",
832            "stream_reasoning",
833        ] {
834            assert!(value.get(field).is_none(), "{field} serialized at default");
835        }
836
837        let back: ChatCompletionRequest = serde_json::from_value(value).expect("roundtrip");
838        assert!(!back.no_stop_trim);
839        assert!(!back.ignore_eos);
840        assert!(!back.continue_final_message);
841        assert!(!back.return_hidden_states);
842        assert!(back.separate_reasoning);
843        assert!(back.stream_reasoning);
844    }
845
846    #[test]
847    fn non_default_sglang_flags_round_trip() {
848        let request = request_with_output_fields(&[
849            ("no_stop_trim", json!(true)),
850            ("ignore_eos", json!(true)),
851            ("continue_final_message", json!(true)),
852            ("return_hidden_states", json!(true)),
853            ("separate_reasoning", json!(false)),
854            ("stream_reasoning", json!(false)),
855        ]);
856        let value = serde_json::to_value(&request).expect("serialize");
857        assert_eq!(value["no_stop_trim"], true);
858        assert_eq!(value["ignore_eos"], true);
859        assert_eq!(value["continue_final_message"], true);
860        assert_eq!(value["return_hidden_states"], true);
861        assert_eq!(value["separate_reasoning"], false);
862        assert_eq!(value["stream_reasoning"], false);
863
864        let back: ChatCompletionRequest = serde_json::from_value(value).expect("roundtrip");
865        assert!(back.no_stop_trim);
866        assert!(back.ignore_eos);
867        assert!(back.continue_final_message);
868        assert!(back.return_hidden_states);
869        assert!(!back.separate_reasoning);
870        assert!(!back.stream_reasoning);
871    }
872
873    #[test]
874    fn thinking_from_reasoning_effort_maps_disable_values() {
875        // "none"/"minimal" mean do-not-reason -> thinking OFF.
876        assert_eq!(thinking_from_reasoning_effort(Some("none")), Some(false));
877        assert_eq!(thinking_from_reasoning_effort(Some("minimal")), Some(false));
878        // Level values do not toggle thinking on their own.
879        assert_eq!(thinking_from_reasoning_effort(Some("low")), None);
880        assert_eq!(thinking_from_reasoning_effort(Some("medium")), None);
881        assert_eq!(thinking_from_reasoning_effort(Some("high")), None);
882        // Unspecified / unknown -> defer.
883        assert_eq!(thinking_from_reasoning_effort(None), None);
884        assert_eq!(thinking_from_reasoning_effort(Some("bogus")), None);
885    }
886
887    #[test]
888    fn reasoning_effort_accepts_scalar_json_and_rejects_other_types() {
889        for (value, expected) in [
890            (json!("high"), Some("high")),
891            (json!(0.2), Some("0.2")),
892            (json!(0.99), Some("0.99")),
893            (Value::Null, None),
894        ] {
895            let request = request_with_output_fields(&[("reasoning_effort", value)]);
896            assert_eq!(request.reasoning_effort.as_deref(), expected);
897        }
898
899        for value in [json!(true), json!([]), json!({"level": "high"})] {
900            let mut request = json!({
901                "model": "test-model",
902                "messages": [{"role": "user", "content": "hello"}],
903            });
904            request["reasoning_effort"] = value;
905            let error = serde_json::from_value::<ChatCompletionRequest>(request).unwrap_err();
906            assert!(error
907                .to_string()
908                .contains("reasoning_effort must be a string, number, or null"));
909        }
910    }
911
912    #[test]
913    fn return_audio_preserves_explicit_values() {
914        for fields in [vec![], vec![("return_audio", Value::Null)]] {
915            let request = request_with_output_fields(&fields);
916            assert_eq!(request.return_audio, None);
917            assert!(!request.other.contains_key("return_audio"));
918            let serialized = serde_json::to_value(request).expect("request must serialize");
919            assert!(serialized.get("return_audio").is_none());
920        }
921
922        for value in [false, true] {
923            let request = request_with_output_fields(&[("return_audio", json!(value))]);
924            assert_eq!(request.return_audio, Some(value));
925            assert!(!request.other.contains_key("return_audio"));
926            let serialized = serde_json::to_value(request).expect("request must serialize");
927            assert_eq!(serialized.get("return_audio"), Some(&Value::Bool(value)));
928        }
929    }
930
931    #[test]
932    fn chat_request_accepts_function_tool_without_parameters() {
933        // https://github.com/smg-project/smg/issues/1974 — omitting
934        // `parameters` is spec-legal and must not reject the request.
935        let value = json!({
936            "model": "test-model",
937            "messages": [{"role": "user", "content": "hello"}],
938            "tools": [
939                {"type": "function", "function": {"name": "web_search", "description": ""}}
940            ],
941        });
942        let request: ChatCompletionRequest =
943            serde_json::from_value(value).expect("request must deserialize");
944        let tools = request.tools.expect("tools must be present");
945        assert_eq!(tools[0].function.parameters, json!({}));
946    }
947}