use serde::{Deserialize, Serialize};
use super::{ChatMessage, FormatSetting, KeepAliveSetting, ModelOptions, ThinkSetting};
#[cfg(feature = "tools")]
use crate::tools::ToolDefinition;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<ChatMessage>,
#[cfg(feature = "tools")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDefinition>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<FormatSetting>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<ModelOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub think: Option<ThinkSetting>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keep_alive: Option<KeepAliveSetting>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logprobs: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<i32>,
}
impl ChatRequest {
pub fn new<M, I>(model: M, messages: I) -> Self
where
M: Into<String>,
I: IntoIterator<Item = ChatMessage>,
{
Self {
model: model.into(),
messages: messages.into_iter().collect(),
#[cfg(feature = "tools")]
tools: None,
format: None,
options: None,
stream: Some(false), think: None,
keep_alive: None,
logprobs: None,
top_logprobs: None,
}
}
pub fn with_message(mut self, message: ChatMessage) -> Self {
self.messages.push(message);
self
}
pub fn with_stream(mut self, stream: bool) -> Self {
self.stream = Some(stream);
self
}
#[cfg(feature = "tools")]
pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools = Some(tools);
self
}
#[cfg(feature = "tools")]
pub fn with_tool(mut self, tool: ToolDefinition) -> Self {
self.tools.get_or_insert_with(Vec::new).push(tool);
self
}
pub fn with_format(mut self, format: impl Into<FormatSetting>) -> Self {
self.format = Some(format.into());
self
}
pub fn with_options(mut self, options: ModelOptions) -> Self {
self.options = Some(options);
self
}
pub fn with_think(mut self, think: impl Into<ThinkSetting>) -> Self {
self.think = Some(think.into());
self
}
pub fn with_keep_alive(mut self, keep_alive: impl Into<KeepAliveSetting>) -> Self {
self.keep_alive = Some(keep_alive.into());
self
}
pub fn with_logprobs(mut self, logprobs: bool) -> Self {
self.logprobs = Some(logprobs);
self
}
pub fn with_top_logprobs(mut self, n: i32) -> Self {
self.top_logprobs = Some(n);
self
}
pub fn model(&self) -> &str {
&self.model
}
pub fn messages(&self) -> &[ChatMessage] {
&self.messages
}
pub fn message_count(&self) -> usize {
self.messages.len()
}
#[cfg(feature = "tools")]
pub fn has_tools(&self) -> bool {
self.tools.as_ref().map(|t| !t.is_empty()).unwrap_or(false)
}
#[cfg(feature = "tools")]
pub fn tools(&self) -> Option<&[ToolDefinition]> {
self.tools.as_deref()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused)]
use serde_json::json;
#[test]
fn test_chat_request_new_with_vec() {
let request = ChatRequest::new("qwen3:0.6b", vec![ChatMessage::user("Hello")]);
assert_eq!(request.model, "qwen3:0.6b");
assert_eq!(request.messages.len(), 1);
assert_eq!(request.stream, Some(false));
}
#[test]
fn test_chat_request_new_with_array() {
let request = ChatRequest::new(
"qwen3:0.6b",
[
ChatMessage::system("Be helpful"),
ChatMessage::user("Hello"),
],
);
assert_eq!(request.messages.len(), 2);
assert!(request.messages[0].is_system());
assert!(request.messages[1].is_user());
}
#[test]
fn test_chat_request_new_with_iterator() {
let msgs = ["Hi", "Bye"];
let request = ChatRequest::new("model", msgs.iter().map(|s| ChatMessage::user(*s)));
assert_eq!(request.messages.len(), 2);
}
#[test]
fn test_chat_request_with_message() {
let request = ChatRequest::new("model", [ChatMessage::user("1")])
.with_message(ChatMessage::assistant("2"))
.with_message(ChatMessage::user("3"));
assert_eq!(request.messages.len(), 3);
}
#[cfg(feature = "tools")]
#[test]
fn test_chat_request_with_tools() {
let tool = ToolDefinition::function("test", json!({}));
let request = ChatRequest::new("model", [ChatMessage::user("Hi")]).with_tools(vec![tool]);
assert!(request.has_tools());
assert_eq!(request.tools().unwrap().len(), 1);
}
#[cfg(feature = "tools")]
#[test]
fn test_chat_request_with_tool() {
let request = ChatRequest::new("model", [ChatMessage::user("Hi")])
.with_tool(ToolDefinition::function("a", json!({})))
.with_tool(ToolDefinition::function("b", json!({})));
assert!(request.has_tools());
assert_eq!(request.tools().unwrap().len(), 2);
}
#[test]
fn test_chat_request_with_format() {
let request =
ChatRequest::new("model", [ChatMessage::user("Hi")]).with_format(FormatSetting::json());
assert!(request.format.is_some());
}
#[test]
fn test_chat_request_with_options() {
let options = ModelOptions::default().with_temperature(0.7);
let request = ChatRequest::new("model", [ChatMessage::user("Hi")]).with_options(options);
assert!(request.options.is_some());
assert_eq!(request.options.unwrap().temperature, Some(0.7));
}
#[test]
fn test_chat_request_with_think() {
let request = ChatRequest::new("model", [ChatMessage::user("Hi")])
.with_think(ThinkSetting::enabled());
assert!(request.think.is_some());
}
#[test]
fn test_chat_request_with_keep_alive() {
let request = ChatRequest::new("model", [ChatMessage::user("Hi")])
.with_keep_alive(KeepAliveSetting::duration("5m"));
assert!(request.keep_alive.is_some());
}
#[test]
fn test_chat_request_with_logprobs() {
let request = ChatRequest::new("model", [ChatMessage::user("Hi")])
.with_logprobs(true)
.with_top_logprobs(5);
assert_eq!(request.logprobs, Some(true));
assert_eq!(request.top_logprobs, Some(5));
}
#[test]
fn test_chat_request_model() {
let request = ChatRequest::new("my-model", [ChatMessage::user("Hi")]);
assert_eq!(request.model(), "my-model");
}
#[test]
fn test_chat_request_messages() {
let request = ChatRequest::new(
"model",
[ChatMessage::user("1"), ChatMessage::assistant("2")],
);
assert_eq!(request.messages().len(), 2);
assert_eq!(request.message_count(), 2);
}
#[cfg(feature = "tools")]
#[test]
fn test_chat_request_has_tools() {
let without = ChatRequest::new("model", [ChatMessage::user("Hi")]);
assert!(!without.has_tools());
let with = without.with_tool(ToolDefinition::function("f", json!({})));
assert!(with.has_tools());
}
#[test]
fn test_chat_request_serialize() {
let request = ChatRequest::new("qwen3:0.6b", [ChatMessage::user("Hello")]);
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["model"], "qwen3:0.6b");
assert_eq!(json["messages"][0]["role"], "user");
assert_eq!(json["messages"][0]["content"], "Hello");
assert_eq!(json["stream"], false);
#[cfg(feature = "tools")]
assert!(json.get("tools").is_none());
assert!(json.get("format").is_none());
}
#[cfg(feature = "tools")]
#[test]
fn test_chat_request_serialize_with_tools() {
let request = ChatRequest::new("model", [ChatMessage::user("Hi")]).with_tools(vec![
ToolDefinition::function(
"get_weather",
json!({
"type": "object",
"properties": {"location": {"type": "string"}}
}),
)
.with_description("Get weather"),
]);
let json = serde_json::to_value(&request).unwrap();
assert!(json.get("tools").is_some());
assert_eq!(json["tools"][0]["type"], "function");
assert_eq!(json["tools"][0]["function"]["name"], "get_weather");
}
#[test]
fn test_chat_request_serialize_full() {
let request = ChatRequest::new(
"qwen3:0.6b",
[
ChatMessage::system("Be helpful."),
ChatMessage::user("What's 2+2?"),
],
)
.with_format(FormatSetting::json())
.with_options(ModelOptions::default().with_temperature(0.7))
.with_think(ThinkSetting::enabled())
.with_keep_alive(KeepAliveSetting::duration("5m"))
.with_logprobs(true)
.with_top_logprobs(3);
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["model"], "qwen3:0.6b");
assert_eq!(json["messages"].as_array().unwrap().len(), 2);
assert_eq!(json["stream"], false);
assert_eq!(json["format"], "json");
let temp = json["options"]["temperature"].as_f64().unwrap();
assert!((temp - 0.7).abs() < 0.001, "Expected ~0.7, got {}", temp);
assert_eq!(json["think"], true);
assert_eq!(json["keep_alive"], "5m");
assert_eq!(json["logprobs"], true);
assert_eq!(json["top_logprobs"], 3);
}
#[test]
fn test_chat_request_deserialize() {
let json = r#"{
"model": "qwen3:0.6b",
"messages": [
{"role": "user", "content": "Hello"}
],
"stream": false
}"#;
let request: ChatRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.model, "qwen3:0.6b");
assert_eq!(request.messages.len(), 1);
assert_eq!(request.stream, Some(false));
}
#[test]
fn test_chat_request_clone() {
#[cfg(feature = "tools")]
let request = ChatRequest::new("model", [ChatMessage::user("Hi")])
.with_tool(ToolDefinition::function("f", json!({})));
#[cfg(not(feature = "tools"))]
let request = ChatRequest::new("model", [ChatMessage::user("Hi")]);
let cloned = request.clone();
assert_eq!(request, cloned);
}
#[test]
fn test_chat_request_equality() {
let req1 = ChatRequest::new("model", [ChatMessage::user("Hi")]);
let req2 = ChatRequest::new("model", [ChatMessage::user("Hi")]);
let req3 = ChatRequest::new("model", [ChatMessage::user("Bye")]);
assert_eq!(req1, req2);
assert_ne!(req1, req3);
}
#[cfg(feature = "tools")]
#[test]
fn test_chat_request_matches_api_format() {
let request = ChatRequest::new(
"qwen3:0.6b",
[
ChatMessage::system("You are a helpful assistant."),
ChatMessage::user("What's the weather in Paris?"),
],
)
.with_tools(vec![
ToolDefinition::function(
"get_weather",
json!({
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}),
)
.with_description("Get the current weather for a location"),
]);
let json_value = serde_json::to_value(&request).unwrap();
let json_string = serde_json::to_string_pretty(&json_value).unwrap();
assert!(json_string.contains("\"model\": \"qwen3:0.6b\""));
assert!(json_string.contains("\"stream\": false"));
assert!(json_string.contains("\"messages\""));
assert!(json_string.contains("\"tools\""));
assert!(json_string.contains("\"type\": \"function\""));
}
}