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#[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: 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 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 #[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 #[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#[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 #[validate(custom(function = "validate_messages"))]
146 pub messages: Vec<ChatMessage>,
147
148 pub model: String,
150
151 #[validate(range(min = -2.0, max = 2.0))]
153 pub frequency_penalty: Option<f32>,
154
155 #[deprecated(note = "Use tool_choice instead")]
157 pub function_call: Option<FunctionCall>,
158
159 #[deprecated(note = "Use tools instead")]
161 pub functions: Option<Vec<Function>>,
162
163 pub logit_bias: Option<HashMap<String, f32>>,
165
166 #[serde(default, deserialize_with = "deserialize_null_as_false")]
168 pub logprobs: bool,
169
170 #[deprecated(note = "Use max_completion_tokens instead")]
172 #[validate(range(min = 1))]
173 pub max_tokens: Option<u32>,
174
175 #[validate(range(min = 1))]
177 pub max_completion_tokens: Option<u32>,
178
179 pub metadata: Option<HashMap<String, String>>,
181
182 pub modalities: Option<Vec<String>>,
184
185 pub return_audio: Option<bool>,
187
188 #[validate(range(min = 1, max = 10))]
190 pub n: Option<u32>,
191
192 pub parallel_tool_calls: Option<bool>,
194
195 #[validate(range(min = -2.0, max = 2.0))]
197 pub presence_penalty: Option<f32>,
198
199 pub prompt_cache_key: Option<String>,
201
202 #[serde(default, deserialize_with = "deserialize_reasoning_effort")]
209 pub reasoning_effort: Option<String>,
210
211 pub response_format: Option<ResponseFormat>,
213
214 pub safety_identifier: Option<String>,
216
217 #[deprecated(note = "This feature is in Legacy mode")]
219 pub seed: Option<i64>,
220
221 pub service_tier: Option<String>,
223
224 #[validate(custom(function = "validate_stop"))]
226 pub stop: Option<StringOrArray>,
227
228 #[serde(default, deserialize_with = "deserialize_null_as_false")]
230 pub stream: bool,
231
232 pub stream_options: Option<StreamOptions>,
234
235 #[validate(range(min = 0.0, max = 2.0))]
237 pub temperature: Option<f32>,
238
239 pub tool_choice: Option<ToolChoice>,
241
242 pub tools: Option<Vec<Tool>>,
244
245 #[validate(range(min = 0, max = 20))]
247 pub top_logprobs: Option<u32>,
248
249 #[validate(custom(function = "validate_top_p_value"))]
251 pub top_p: Option<f32>,
252
253 pub verbosity: Option<i32>,
255
256 #[validate(custom(function = "validate_top_k_value"))]
264 pub top_k: Option<i32>,
265
266 #[validate(range(min = 0.0, max = 1.0))]
268 pub min_p: Option<f32>,
269
270 #[validate(range(min = 0))]
272 pub min_tokens: Option<u32>,
273
274 #[validate(range(min = 0.0, max = 2.0))]
276 pub repetition_penalty: Option<f32>,
277
278 pub regex: Option<String>,
280
281 pub ebnf: Option<String>,
283
284 pub stop_token_ids: Option<Vec<u32>>,
286
287 #[serde(default, skip_serializing_if = "is_false")]
289 pub no_stop_trim: bool,
290
291 #[serde(default, skip_serializing_if = "is_false")]
293 pub ignore_eos: bool,
294
295 #[serde(default, skip_serializing_if = "is_false")]
297 pub continue_final_message: bool,
298
299 #[serde(default = "default_true")]
301 pub skip_special_tokens: bool,
302
303 pub lora_path: Option<String>,
305
306 pub session_params: Option<HashMap<String, Value>>,
308
309 #[serde(default = "default_true", skip_serializing_if = "is_true")]
311 pub separate_reasoning: bool,
312
313 #[serde(default = "default_true", skip_serializing_if = "is_true")]
315 pub stream_reasoning: bool,
316
317 pub chat_template_kwargs: Option<HashMap<String, Value>>,
319
320 #[serde(default, skip_serializing_if = "is_false")]
322 pub return_hidden_states: bool,
323
324 pub sampling_seed: Option<u64>,
326
327 pub rid: Option<String>,
329
330 #[serde(flatten)]
332 pub other: Map<String, Value>,
333}
334
335pub 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
369fn 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
399fn validate_chat_cross_parameters(
401 req: &ChatCompletionRequest,
402) -> Result<(), validator::ValidationError> {
403 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 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 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 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 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 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 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 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 if let Some(tools) = req.tools.as_ref().filter(|t| !t.is_empty()) {
485 match tool_choice {
486 ToolChoice::Function { function, .. } => {
487 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 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 for tool_ref in allowed_tools {
521 match tool_ref {
522 ToolReference::Function { name } => {
523 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 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
566impl Normalizable for ChatCompletionRequest {
571 fn normalize(&mut self) {
576 #[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; }
582
583 #[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; }
598
599 #[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; }
613
614 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 }
626 }
627}
628
629impl 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 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 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 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#[serde_with::skip_serializing_none]
711#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
712pub struct ChatCompletionResponse {
713 pub id: String,
714 pub object: String, 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 pub fn builder(
725 id: impl Into<String>,
726 model: impl Into<String>,
727 ) -> ChatCompletionResponseBuilder {
728 ChatCompletionResponseBuilder::new(id, model)
729 }
730}
731
732#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
734pub struct ChatCompletionMessage {
735 pub role: String, #[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 }
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>, #[serde(skip_serializing_if = "Option::is_none")]
754 pub matched_stop: Option<Value>, #[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, 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 pub fn builder(
775 id: impl Into<String>,
776 model: impl Into<String>,
777 ) -> ChatCompletionStreamResponseBuilder {
778 ChatCompletionStreamResponseBuilder::new(id, model)
779 }
780}
781
782#[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 assert_eq!(thinking_from_reasoning_effort(Some("none")), Some(false));
877 assert_eq!(thinking_from_reasoning_effort(Some("minimal")), Some(false));
878 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 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 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}