use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use validator::Validate;
use super::{
common::{
default_true, deserialize_null_as_false, is_false, is_true, validate_stop, ChatLogProbs,
ContentPart, Function, FunctionCall, FunctionChoice, GenerationRequest, ResponseFormat,
StreamOptions, StringOrArray, Tool, ToolCall, ToolCallDelta, ToolChoice, ToolChoiceValue,
ToolReference, Usage,
},
sampling_params::{validate_top_k_value, validate_top_p_value},
};
use crate::{
builders::{ChatCompletionResponseBuilder, ChatCompletionStreamResponseBuilder},
validated::Normalizable,
};
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
#[serde(tag = "role")]
pub enum ChatMessage {
#[serde(rename = "system")]
System {
content: MessageContent,
name: Option<String>,
},
#[serde(rename = "user")]
User {
content: MessageContent,
name: Option<String>,
},
#[serde(rename = "assistant")]
Assistant {
content: Option<MessageContent>,
name: Option<String>,
tool_calls: Option<Vec<ToolCall>>,
reasoning_content: Option<String>,
},
#[serde(rename = "tool")]
Tool {
content: MessageContent,
tool_call_id: String,
},
#[serde(rename = "function")]
Function { content: String, name: String },
#[serde(rename = "developer")]
Developer {
content: MessageContent,
tools: Option<Vec<Tool>>,
name: Option<String>,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, schemars::JsonSchema)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
impl MessageContent {
pub fn to_simple_string(&self) -> String {
match self {
MessageContent::Text(text) => text.clone(),
MessageContent::Parts(parts) => {
let mut result = String::new();
let mut first = true;
for part in parts {
if let ContentPart::Text { text } = part {
if !first {
result.push(' ');
}
result.push_str(text);
first = false;
}
}
result
}
}
}
#[inline]
pub fn append_text_to(&self, buffer: &mut String) -> bool {
match self {
MessageContent::Text(text) => {
if text.is_empty() {
false
} else {
buffer.push_str(text);
true
}
}
MessageContent::Parts(parts) => {
let mut appended = false;
for part in parts {
if let ContentPart::Text { text } = part {
if !text.is_empty() {
if appended {
buffer.push(' ');
}
buffer.push_str(text);
appended = true;
}
}
}
appended
}
}
}
#[inline]
pub fn has_text(&self) -> bool {
match self {
MessageContent::Text(text) => !text.is_empty(),
MessageContent::Parts(parts) => parts
.iter()
.any(|part| matches!(part, ContentPart::Text { text } if !text.is_empty())),
}
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, Default, Validate, schemars::JsonSchema)]
#[validate(schema(function = "validate_chat_cross_parameters"))]
pub struct ChatCompletionRequest {
#[validate(custom(function = "validate_messages"))]
pub messages: Vec<ChatMessage>,
pub model: String,
#[validate(range(min = -2.0, max = 2.0))]
pub frequency_penalty: Option<f32>,
#[deprecated(note = "Use tool_choice instead")]
pub function_call: Option<FunctionCall>,
#[deprecated(note = "Use tools instead")]
pub functions: Option<Vec<Function>>,
pub logit_bias: Option<HashMap<String, f32>>,
#[serde(default, deserialize_with = "deserialize_null_as_false")]
pub logprobs: bool,
#[deprecated(note = "Use max_completion_tokens instead")]
#[validate(range(min = 1))]
pub max_tokens: Option<u32>,
#[validate(range(min = 1))]
pub max_completion_tokens: Option<u32>,
pub metadata: Option<HashMap<String, String>>,
pub modalities: Option<Vec<String>>,
pub return_audio: Option<bool>,
#[validate(range(min = 1, max = 10))]
pub n: Option<u32>,
pub parallel_tool_calls: Option<bool>,
#[validate(range(min = -2.0, max = 2.0))]
pub presence_penalty: Option<f32>,
pub prompt_cache_key: Option<String>,
#[serde(default, deserialize_with = "deserialize_reasoning_effort")]
pub reasoning_effort: Option<String>,
pub response_format: Option<ResponseFormat>,
pub safety_identifier: Option<String>,
#[deprecated(note = "This feature is in Legacy mode")]
pub seed: Option<i64>,
pub service_tier: Option<String>,
#[validate(custom(function = "validate_stop"))]
pub stop: Option<StringOrArray>,
#[serde(default, deserialize_with = "deserialize_null_as_false")]
pub stream: bool,
pub stream_options: Option<StreamOptions>,
#[validate(range(min = 0.0, max = 2.0))]
pub temperature: Option<f32>,
pub tool_choice: Option<ToolChoice>,
pub tools: Option<Vec<Tool>>,
#[validate(range(min = 0, max = 20))]
pub top_logprobs: Option<u32>,
#[validate(custom(function = "validate_top_p_value"))]
pub top_p: Option<f32>,
pub verbosity: Option<i32>,
#[validate(custom(function = "validate_top_k_value"))]
pub top_k: Option<i32>,
#[validate(range(min = 0.0, max = 1.0))]
pub min_p: Option<f32>,
#[validate(range(min = 0))]
pub min_tokens: Option<u32>,
#[validate(range(min = 0.0, max = 2.0))]
pub repetition_penalty: Option<f32>,
pub regex: Option<String>,
pub ebnf: Option<String>,
pub stop_token_ids: Option<Vec<u32>>,
#[serde(default, skip_serializing_if = "is_false")]
pub no_stop_trim: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub ignore_eos: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub continue_final_message: bool,
#[serde(default = "default_true")]
pub skip_special_tokens: bool,
pub lora_path: Option<String>,
pub session_params: Option<HashMap<String, Value>>,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub separate_reasoning: bool,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub stream_reasoning: bool,
pub chat_template_kwargs: Option<HashMap<String, Value>>,
#[serde(default, skip_serializing_if = "is_false")]
pub return_hidden_states: bool,
pub sampling_seed: Option<u64>,
pub rid: Option<String>,
#[serde(flatten)]
pub other: Map<String, Value>,
}
pub fn thinking_from_reasoning_effort(reasoning_effort: Option<&str>) -> Option<bool> {
match reasoning_effort {
Some("none") | Some("minimal") => Some(false),
_ => None,
}
}
fn deserialize_reasoning_effort<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<Value>::deserialize(deserializer)?;
match value {
None | Some(Value::Null) => Ok(None),
Some(Value::String(value)) => Ok(Some(value)),
Some(Value::Number(value)) => Ok(Some(value.to_string())),
Some(_) => Err(serde::de::Error::custom(
"reasoning_effort must be a string, number, or null",
)),
}
}
fn validate_messages(messages: &[ChatMessage]) -> Result<(), validator::ValidationError> {
if messages.is_empty() {
return Err(validator::ValidationError::new("messages cannot be empty"));
}
for msg in messages {
if let ChatMessage::User { content, .. } = msg {
match content {
MessageContent::Text(text) if text.is_empty() => {
return Err(validator::ValidationError::new(
"message content cannot be empty",
));
}
MessageContent::Parts(parts) if parts.is_empty() => {
return Err(validator::ValidationError::new(
"message content parts cannot be empty",
));
}
_ => {}
}
}
}
Ok(())
}
fn validate_chat_cross_parameters(
req: &ChatCompletionRequest,
) -> Result<(), validator::ValidationError> {
if req.top_logprobs.is_some() && !req.logprobs {
let mut e = validator::ValidationError::new("top_logprobs_requires_logprobs");
e.message = Some("top_logprobs is only allowed when logprobs is enabled".into());
return Err(e);
}
if req.stream_options.is_some() && !req.stream {
let mut e = validator::ValidationError::new("stream_options_requires_stream");
e.message =
Some("The 'stream_options' parameter is only allowed when 'stream' is enabled".into());
return Err(e);
}
if let (Some(min), Some(max)) = (req.min_tokens, req.max_completion_tokens) {
if min > max {
let mut e = validator::ValidationError::new("min_tokens_exceeds_max");
e.message = Some("min_tokens cannot exceed max_tokens/max_completion_tokens".into());
return Err(e);
}
}
let has_json_format = matches!(
req.response_format,
Some(ResponseFormat::JsonObject | ResponseFormat::JsonSchema { .. })
);
if has_json_format && req.regex.is_some() {
let mut e = validator::ValidationError::new("regex_conflicts_with_json");
e.message = Some("cannot use regex constraint with JSON response format".into());
return Err(e);
}
if has_json_format && req.ebnf.is_some() {
let mut e = validator::ValidationError::new("ebnf_conflicts_with_json");
e.message = Some("cannot use EBNF constraint with JSON response format".into());
return Err(e);
}
let constraint_count = [
req.regex.is_some(),
req.ebnf.is_some(),
matches!(req.response_format, Some(ResponseFormat::JsonSchema { .. })),
]
.iter()
.filter(|&&x| x)
.count();
if constraint_count > 1 {
let mut e = validator::ValidationError::new("multiple_constraints");
e.message = Some("only one structured output constraint (regex, ebnf, or json_schema) can be active at a time".into());
return Err(e);
}
if let Some(ResponseFormat::JsonSchema { json_schema }) = &req.response_format {
if json_schema.name.is_empty() {
let mut e = validator::ValidationError::new("json_schema_name_empty");
e.message = Some("JSON schema name cannot be empty".into());
return Err(e);
}
}
if let Some(ref tool_choice) = req.tool_choice {
let has_tools = req.tools.as_ref().is_some_and(|t| !t.is_empty());
let is_some_choice = !matches!(tool_choice, ToolChoice::Value(ToolChoiceValue::None));
if is_some_choice && !has_tools {
let mut e = validator::ValidationError::new("tool_choice_requires_tools");
e.message = Some("Invalid value for 'tool_choice': 'tool_choice' is only allowed when 'tools' are specified.".into());
return Err(e);
}
if let Some(tools) = req.tools.as_ref().filter(|t| !t.is_empty()) {
match tool_choice {
ToolChoice::Function { function, .. } => {
let function_exists = tools.iter().any(|tool| {
tool.tool_type == "function" && tool.function.name == function.name
});
if !function_exists {
let mut e =
validator::ValidationError::new("tool_choice_function_not_found");
e.message = Some(
format!(
"Invalid value for 'tool_choice': function '{}' not found in 'tools'.",
function.name
)
.into(),
);
return Err(e);
}
}
ToolChoice::AllowedTools {
mode,
tools: allowed_tools,
..
} => {
if mode != "auto" && mode != "required" {
let mut e = validator::ValidationError::new("tool_choice_invalid_mode");
e.message = Some(format!(
"Invalid value for 'tool_choice.mode': must be 'auto' or 'required', got '{mode}'."
).into());
return Err(e);
}
for tool_ref in allowed_tools {
match tool_ref {
ToolReference::Function { name } => {
let tool_exists = tools.iter().any(|tool| {
tool.tool_type == "function" && tool.function.name == *name
});
if !tool_exists {
let mut e = validator::ValidationError::new(
"tool_choice_tool_not_found",
);
e.message = Some(
format!(
"Invalid value for 'tool_choice.tools': tool '{name}' not found in 'tools'."
)
.into(),
);
return Err(e);
}
}
_ => {
let mut e = validator::ValidationError::new(
"tool_choice_invalid_tool_type",
);
e.message = Some(
format!(
"Invalid value for 'tool_choice.tools': Chat Completion API only supports function tools, got '{}'.",
tool_ref.identifier()
)
.into(),
);
return Err(e);
}
}
}
}
ToolChoice::Value(_) => {}
}
}
}
Ok(())
}
impl Normalizable for ChatCompletionRequest {
fn normalize(&mut self) {
#[expect(deprecated)]
if self.max_completion_tokens.is_none() && self.max_tokens.is_some() {
self.max_completion_tokens = self.max_tokens;
self.max_tokens = None; }
#[expect(deprecated)]
if self.tools.is_none() && self.functions.is_some() {
tracing::warn!("functions is deprecated, use tools instead");
self.tools = self.functions.as_ref().map(|functions| {
functions
.iter()
.map(|func| Tool {
tool_type: "function".to_string(),
function: func.clone(),
})
.collect()
});
self.functions = None; }
#[expect(deprecated)]
if self.tool_choice.is_none() && self.function_call.is_some() {
tracing::warn!("function_call is deprecated, use tool_choice instead");
self.tool_choice = self.function_call.as_ref().map(|fc| match fc {
FunctionCall::None => ToolChoice::Value(ToolChoiceValue::None),
FunctionCall::Auto => ToolChoice::Value(ToolChoiceValue::Auto),
FunctionCall::Function { name } => ToolChoice::Function {
tool_type: "function".to_string(),
function: FunctionChoice { name: name.clone() },
},
});
self.function_call = None; }
if self.tool_choice.is_none() {
if let Some(tools) = &self.tools {
let choice_value = if tools.is_empty() {
ToolChoiceValue::None
} else {
ToolChoiceValue::Auto
};
self.tool_choice = Some(ToolChoice::Value(choice_value));
}
}
}
}
impl GenerationRequest for ChatCompletionRequest {
fn rid(&self) -> Option<&str> {
self.rid.as_deref()
}
fn is_stream(&self) -> bool {
self.stream
}
fn get_model(&self) -> Option<&str> {
Some(&self.model)
}
fn extract_text_for_routing(&self) -> String {
let mut buffer = String::new();
let mut has_content = false;
for msg in &self.messages {
match msg {
ChatMessage::System { content, .. }
| ChatMessage::User { content, .. }
| ChatMessage::Tool { content, .. }
| ChatMessage::Developer { content, .. } => {
if has_content && content.has_text() {
buffer.push(' ');
}
if content.append_text_to(&mut buffer) {
has_content = true;
}
}
ChatMessage::Assistant {
content,
reasoning_content,
..
} => {
if let Some(c) = content {
if has_content && c.has_text() {
buffer.push(' ');
}
if c.append_text_to(&mut buffer) {
has_content = true;
}
}
if let Some(reasoning) = reasoning_content {
if !reasoning.is_empty() {
if has_content {
buffer.push(' ');
}
buffer.push_str(reasoning);
has_content = true;
}
}
}
ChatMessage::Function { content, .. } => {
if !content.is_empty() {
if has_content {
buffer.push(' ');
}
buffer.push_str(content);
has_content = true;
}
}
}
}
buffer
}
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: String, pub created: u64,
pub model: String,
pub choices: Vec<ChatChoice>,
pub usage: Option<Usage>,
pub system_fingerprint: Option<String>,
}
impl ChatCompletionResponse {
pub fn builder(
id: impl Into<String>,
model: impl Into<String>,
) -> ChatCompletionResponseBuilder {
ChatCompletionResponseBuilder::new(id, model)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ChatCompletionMessage {
pub role: String, #[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
pub reasoning_content: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ChatChoice {
pub index: u32,
pub message: ChatCompletionMessage,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<ChatLogProbs>,
pub finish_reason: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub matched_stop: Option<Value>, #[serde(skip_serializing_if = "Option::is_none")]
pub hidden_states: Option<Vec<f32>>,
}
#[serde_with::skip_serializing_none]
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ChatCompletionStreamResponse {
pub id: String,
pub object: String, pub created: u64,
pub model: String,
pub system_fingerprint: Option<String>,
pub choices: Vec<ChatStreamChoice>,
pub usage: Option<Usage>,
}
impl ChatCompletionStreamResponse {
pub fn builder(
id: impl Into<String>,
model: impl Into<String>,
) -> ChatCompletionStreamResponseBuilder {
ChatCompletionStreamResponseBuilder::new(id, model)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ChatMessageDelta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallDelta>>,
pub reasoning_content: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
pub struct ChatStreamChoice {
pub index: u32,
pub delta: ChatMessageDelta,
pub logprobs: Option<ChatLogProbs>,
pub finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub matched_stop: Option<Value>,
}
#[cfg(test)]
mod tests {
use serde_json::{json, Value};
use super::{thinking_from_reasoning_effort, ChatCompletionRequest};
fn request_with_output_fields(fields: &[(&str, Value)]) -> ChatCompletionRequest {
let mut value = json!({
"model": "test-model",
"messages": [{"role": "user", "content": "hello"}]
});
let object = value.as_object_mut().expect("request must be an object");
for (name, field_value) in fields {
object.insert((*name).to_string(), field_value.clone());
}
serde_json::from_value(value).expect("request must deserialize")
}
#[test]
fn default_sglang_flags_are_omitted_and_absent_reads_defaults() {
let request = request_with_output_fields(&[]);
let value = serde_json::to_value(&request).expect("serialize");
for field in [
"no_stop_trim",
"ignore_eos",
"continue_final_message",
"return_hidden_states",
"separate_reasoning",
"stream_reasoning",
] {
assert!(value.get(field).is_none(), "{field} serialized at default");
}
let back: ChatCompletionRequest = serde_json::from_value(value).expect("roundtrip");
assert!(!back.no_stop_trim);
assert!(!back.ignore_eos);
assert!(!back.continue_final_message);
assert!(!back.return_hidden_states);
assert!(back.separate_reasoning);
assert!(back.stream_reasoning);
}
#[test]
fn non_default_sglang_flags_round_trip() {
let request = request_with_output_fields(&[
("no_stop_trim", json!(true)),
("ignore_eos", json!(true)),
("continue_final_message", json!(true)),
("return_hidden_states", json!(true)),
("separate_reasoning", json!(false)),
("stream_reasoning", json!(false)),
]);
let value = serde_json::to_value(&request).expect("serialize");
assert_eq!(value["no_stop_trim"], true);
assert_eq!(value["ignore_eos"], true);
assert_eq!(value["continue_final_message"], true);
assert_eq!(value["return_hidden_states"], true);
assert_eq!(value["separate_reasoning"], false);
assert_eq!(value["stream_reasoning"], false);
let back: ChatCompletionRequest = serde_json::from_value(value).expect("roundtrip");
assert!(back.no_stop_trim);
assert!(back.ignore_eos);
assert!(back.continue_final_message);
assert!(back.return_hidden_states);
assert!(!back.separate_reasoning);
assert!(!back.stream_reasoning);
}
#[test]
fn thinking_from_reasoning_effort_maps_disable_values() {
assert_eq!(thinking_from_reasoning_effort(Some("none")), Some(false));
assert_eq!(thinking_from_reasoning_effort(Some("minimal")), Some(false));
assert_eq!(thinking_from_reasoning_effort(Some("low")), None);
assert_eq!(thinking_from_reasoning_effort(Some("medium")), None);
assert_eq!(thinking_from_reasoning_effort(Some("high")), None);
assert_eq!(thinking_from_reasoning_effort(None), None);
assert_eq!(thinking_from_reasoning_effort(Some("bogus")), None);
}
#[test]
fn reasoning_effort_accepts_scalar_json_and_rejects_other_types() {
for (value, expected) in [
(json!("high"), Some("high")),
(json!(0.2), Some("0.2")),
(json!(0.99), Some("0.99")),
(Value::Null, None),
] {
let request = request_with_output_fields(&[("reasoning_effort", value)]);
assert_eq!(request.reasoning_effort.as_deref(), expected);
}
for value in [json!(true), json!([]), json!({"level": "high"})] {
let mut request = json!({
"model": "test-model",
"messages": [{"role": "user", "content": "hello"}],
});
request["reasoning_effort"] = value;
let error = serde_json::from_value::<ChatCompletionRequest>(request).unwrap_err();
assert!(error
.to_string()
.contains("reasoning_effort must be a string, number, or null"));
}
}
#[test]
fn return_audio_preserves_explicit_values() {
for fields in [vec![], vec![("return_audio", Value::Null)]] {
let request = request_with_output_fields(&fields);
assert_eq!(request.return_audio, None);
assert!(!request.other.contains_key("return_audio"));
let serialized = serde_json::to_value(request).expect("request must serialize");
assert!(serialized.get("return_audio").is_none());
}
for value in [false, true] {
let request = request_with_output_fields(&[("return_audio", json!(value))]);
assert_eq!(request.return_audio, Some(value));
assert!(!request.other.contains_key("return_audio"));
let serialized = serde_json::to_value(request).expect("request must serialize");
assert_eq!(serialized.get("return_audio"), Some(&Value::Bool(value)));
}
}
#[test]
fn chat_request_accepts_function_tool_without_parameters() {
let value = json!({
"model": "test-model",
"messages": [{"role": "user", "content": "hello"}],
"tools": [
{"type": "function", "function": {"name": "web_search", "description": ""}}
],
});
let request: ChatCompletionRequest =
serde_json::from_value(value).expect("request must deserialize");
let tools = request.tools.expect("tools must be present");
assert_eq!(tools[0].function.parameters, json!({}));
}
}