use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ChatCompletionRequest {
pub model: String,
#[serde(default)]
pub messages: Vec<ChatMessage>,
#[serde(default)]
pub stream: Option<bool>,
#[serde(default)]
pub temperature: Option<f64>,
#[serde(default)]
pub tools: Option<Vec<ChatTool>>,
#[serde(default)]
pub tool_choice: Option<Value>,
#[serde(default)]
pub service_tier: Option<String>,
#[serde(default)]
pub reasoning_effort: Option<String>,
#[serde(default)]
pub max_completion_tokens: Option<u32>,
#[serde(default)]
pub max_tokens: Option<u32>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl ChatCompletionRequest {
pub fn wants_stream(&self) -> bool {
self.stream.unwrap_or(false)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ChatMessage {
pub role: String,
#[serde(default)]
pub content: Option<ChatContent>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub tool_call_id: Option<String>,
#[serde(default)]
pub tool_calls: Option<Vec<ToolCall>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum ChatContent {
Text(String),
Parts(Vec<ChatContentPart>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ChatContentPart {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub image_url: Option<ImageUrl>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ImageUrl {
pub url: String,
#[serde(default)]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct ChatTool {
#[serde(rename = "type")]
pub kind: String,
pub function: FunctionTool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct FunctionTool {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub parameters: Option<Value>,
#[serde(default)]
pub strict: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_standard_string_message() {
let request: ChatCompletionRequest = serde_json::from_value(serde_json::json!({
"model": "gpt-5.4",
"messages": [{"role": "user", "content": "hello"}]
}))
.unwrap();
assert!(!request.wants_stream());
assert_eq!(
request.messages[0].content,
Some(ChatContent::Text("hello".into()))
);
}
#[test]
fn parses_multimodal_content_parts() {
let message: ChatMessage = serde_json::from_value(serde_json::json!({
"role": "user",
"content": [
{"type": "text", "text": "look"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}
]
}))
.unwrap();
let parts = match message.content.unwrap() {
ChatContent::Parts(parts) => parts,
ChatContent::Text(_) => panic!("expected parts"),
};
assert_eq!(parts.len(), 2);
assert_eq!(
parts[1].image_url.as_ref().unwrap().url,
"data:image/png;base64,abc"
);
}
}