use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReasoningEffort {
Low,
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatRequest {
pub model: String,
pub messages: Vec<Message>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logit_bias: Option<HashMap<String, f32>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<ResponseFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_thinking: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_budget: Option<u32>,
#[serde(skip_serializing)]
pub api_key: Option<String>,
#[serde(skip_serializing)]
pub base_url: Option<String>,
#[serde(skip_serializing)]
pub extra_headers: Option<HashMap<String, String>>,
}
impl ChatRequest {
pub fn new(model: impl Into<String>) -> Self {
Self {
model: model.into(),
..Default::default()
}
}
pub fn new_with_messages(model: impl Into<String>, messages: Vec<Message>) -> Self {
Self {
model: model.into(),
messages,
..Default::default()
}
}
pub fn with_messages(mut self, messages: Vec<Message>) -> Self {
self.messages = messages;
self
}
pub fn add_message(mut self, message: Message) -> Self {
self.messages.push(message);
self
}
pub fn add_message_block(mut self, block: super::message_block::MessageBlock) -> Self {
if let Some(last) = self.messages.last_mut()
&& last.role == Role::User
{
last.content.push(block);
return self;
}
self.messages.push(Message::new(Role::User, vec![block]));
self
}
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn with_top_p(mut self, top_p: f32) -> Self {
self.top_p = Some(top_p);
self
}
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn with_stream(mut self, stream: bool) -> Self {
self.stream = Some(stream);
self
}
pub fn with_stop(mut self, stop: Vec<String>) -> Self {
self.stop = Some(stop);
self
}
pub fn with_presence_penalty(mut self, penalty: f32) -> Self {
self.presence_penalty = Some(penalty);
self
}
pub fn with_frequency_penalty(mut self, penalty: f32) -> Self {
self.frequency_penalty = Some(penalty);
self
}
pub fn with_user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_tools(mut self, tools: Vec<Tool>) -> Self {
self.tools = Some(tools);
self
}
pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
self.tool_choice = Some(tool_choice);
self
}
pub fn with_response_format(mut self, format: ResponseFormat) -> Self {
self.response_format = Some(format);
self
}
pub fn with_enable_thinking(mut self, enable: bool) -> Self {
self.enable_thinking = Some(enable);
self
}
pub fn with_reasoning_effort(mut self, effort: ReasoningEffort) -> Self {
self.reasoning_effort = Some(effort);
self
}
pub fn with_thinking_budget(mut self, budget: u32) -> Self {
self.thinking_budget = Some(budget);
self
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = Some(base_url.into());
self
}
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra_headers
.get_or_insert_with(HashMap::new)
.insert(key.into(), value.into());
self
}
pub fn with_extra_headers(mut self, headers: HashMap<String, String>) -> Self {
self.extra_headers = Some(headers);
self
}
pub fn has_non_text_content(&self) -> bool {
self.messages
.iter()
.flat_map(|message| message.content.iter())
.any(|block| !block.is_text())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: Vec<super::message_block::MessageBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thought: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<String>,
}
impl Default for Message {
fn default() -> Self {
Self {
role: Role::User,
content: Vec::new(),
name: None,
tool_calls: None,
tool_call_id: None,
reasoning_content: None,
reasoning: None,
thought: None,
thinking: None,
}
}
}
impl Message {
pub fn text(role: Role, text: impl Into<String>) -> Self {
Self {
role,
content: vec![super::message_block::MessageBlock::text(text)],
name: None,
tool_calls: None,
tool_call_id: None,
reasoning_content: None,
reasoning: None,
thought: None,
thinking: None,
}
}
pub fn new(role: Role, content: Vec<super::message_block::MessageBlock>) -> Self {
Self {
role,
content,
name: None,
tool_calls: None,
tool_call_id: None,
reasoning_content: None,
reasoning: None,
thought: None,
thinking: None,
}
}
pub fn system(text: impl Into<String>) -> Self {
Self::text(Role::System, text)
}
pub fn user(text: impl Into<String>) -> Self {
Self::text(Role::User, text)
}
pub fn assistant(text: impl Into<String>) -> Self {
Self::text(Role::Assistant, text)
}
pub fn reasoning_any(&self) -> Option<&str> {
self.reasoning_content
.as_deref()
.or(self.reasoning.as_deref())
.or(self.thought.as_deref())
.or(self.thinking.as_deref())
}
pub fn content_as_text(&self) -> String {
self.content
.iter()
.filter_map(|block| block.as_text())
.collect::<Vec<_>>()
.join("\n")
}
pub fn content_as_images_base64(&self) -> Vec<String> {
self.content
.iter()
.filter_map(|block| block.as_image_base64())
.collect()
}
pub fn is_text_only(&self) -> bool {
self.content.iter().all(|block| block.is_text())
}
pub fn has_images(&self) -> bool {
self.content.iter().any(|block| block.is_image())
}
pub fn populate_reasoning_from_json(&mut self, raw: &serde_json::Value) {
fn collect_synonyms(
val: &serde_json::Value,
acc: &mut std::collections::HashMap<String, String>,
) {
match val {
serde_json::Value::Array(arr) => {
for v in arr {
collect_synonyms(v, acc);
}
}
serde_json::Value::Object(map) => {
for (k, v) in map {
let key = k.to_ascii_lowercase();
if let serde_json::Value::String(s) = v {
match key.as_str() {
"reasoning_content" | "reasoning" | "thought" | "thinking" => {
acc.entry(key).or_insert_with(|| s.clone());
}
_ => {}
}
}
collect_synonyms(v, acc);
}
}
_ => {}
}
}
let mut found = std::collections::HashMap::<String, String>::new();
collect_synonyms(raw, &mut found);
if self.reasoning_content.is_none()
&& let Some(v) = found.get("reasoning_content")
{
self.reasoning_content = Some(v.clone());
}
if self.reasoning.is_none()
&& let Some(v) = found.get("reasoning")
{
self.reasoning = Some(v.clone());
}
if self.thought.is_none()
&& let Some(v) = found.get("thought")
{
self.thought = Some(v.clone());
}
if self.thinking.is_none()
&& let Some(v) = found.get("thinking")
{
self.thinking = Some(v.clone());
}
}
pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
Self {
role: Role::Tool,
content: vec![super::message_block::MessageBlock::text(content)],
tool_call_id: Some(tool_call_id.into()),
name: None,
tool_calls: None,
..Default::default()
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_tool_calls(mut self, tool_calls: Vec<ToolCall>) -> Self {
self.tool_calls = Some(tool_calls);
self
}
pub fn assistant_with_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
Self {
role: Role::Assistant,
content: Vec::new(),
tool_calls: Some(tool_calls),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tool {
#[serde(rename = "type")]
pub tool_type: String,
pub function: Function,
}
impl Tool {
pub fn function(
name: impl Into<String>,
description: Option<String>,
parameters: serde_json::Value,
) -> Self {
Self {
tool_type: "function".to_string(),
function: Function {
name: name.into(),
description,
parameters,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Function {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolChoice {
Mode(String),
Function {
#[serde(rename = "type")]
tool_type: String,
function: FunctionChoice,
},
}
impl ToolChoice {
pub fn none() -> Self {
Self::Mode("none".to_string())
}
pub fn auto() -> Self {
Self::Mode("auto".to_string())
}
pub fn required() -> Self {
Self::Mode("required".to_string())
}
pub fn function(name: impl Into<String>) -> Self {
Self::Function {
tool_type: "function".to_string(),
function: FunctionChoice { name: name.into() },
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionChoice {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolCall {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub id: String,
#[serde(rename = "type", default, skip_serializing_if = "String::is_empty")]
pub call_type: String,
#[serde(default)]
pub function: FunctionCall,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FunctionCall {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
impl ToolCall {
pub fn merge_delta(&mut self, delta: &ToolCall) {
if !delta.id.is_empty() {
self.id = delta.id.clone();
}
if !delta.call_type.is_empty() {
self.call_type = delta.call_type.clone();
}
if !delta.function.name.is_empty() {
self.function.name = delta.function.name.clone();
}
if !delta.function.arguments.is_empty() {
self.function.arguments.push_str(&delta.function.arguments);
}
if delta.index.is_some() {
self.index = delta.index;
}
if delta.thought_signature.is_some() {
self.thought_signature = delta.thought_signature.clone();
}
if delta.function.thought_signature.is_some() {
self.function.thought_signature = delta.function.thought_signature.clone();
}
}
pub fn is_complete(&self) -> bool {
!self.id.is_empty() && !self.call_type.is_empty() && !self.function.name.is_empty()
}
pub fn parse_arguments<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
serde_json::from_str(&self.function.arguments)
}
pub fn arguments_value(&self) -> Result<serde_json::Value, serde_json::Error> {
if self.function.arguments.is_empty() {
Ok(serde_json::Value::Object(serde_json::Map::new()))
} else {
serde_json::from_str(&self.function.arguments)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseFormat {
#[serde(rename = "type")]
pub format_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub json_schema: Option<JsonSchemaSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonSchemaSpec {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
impl ResponseFormat {
pub fn text() -> Self {
Self {
format_type: "text".to_string(),
json_schema: None,
}
}
pub fn json_object() -> Self {
Self {
format_type: "json_object".to_string(),
json_schema: None,
}
}
pub fn json_schema(name: impl Into<String>, schema: serde_json::Value) -> Self {
Self {
format_type: "json_schema".to_string(),
json_schema: Some(JsonSchemaSpec {
name: name.into(),
description: None,
schema,
strict: Some(true),
}),
}
}
pub fn json_schema_with_desc(
name: impl Into<String>,
description: impl Into<String>,
schema: serde_json::Value,
) -> Self {
Self {
format_type: "json_schema".to_string(),
json_schema: Some(JsonSchemaSpec {
name: name.into(),
description: Some(description.into()),
schema,
strict: Some(true),
}),
}
}
}