use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone, Deserialize, Serialize)]
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 top_p: 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(default)]
pub parallel_tool_calls: Option<bool>,
#[serde(default)]
pub stop: Option<Vec<String>>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl ChatCompletionRequest {
#[must_use]
pub fn wants_stream(&self) -> bool {
self.stream.unwrap_or(false)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResponsesRequest {
pub model: String,
#[serde(default)]
pub input: Option<ResponseInput>,
#[serde(default)]
pub instructions: Option<String>,
#[serde(default)]
pub stream: Option<bool>,
#[serde(default)]
pub temperature: Option<f64>,
#[serde(default)]
pub top_p: 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: Option<Value>,
#[serde(default)]
pub max_output_tokens: Option<u32>,
#[serde(default)]
pub parallel_tool_calls: Option<bool>,
#[serde(default)]
pub store: Option<bool>,
#[serde(default)]
pub previous_response_id: Option<String>,
#[serde(default)]
pub metadata: Option<Map<String, Value>>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl ResponsesRequest {
#[must_use]
pub fn wants_stream(&self) -> bool {
self.stream.unwrap_or(false)
}
#[must_use]
pub fn should_store(&self) -> bool {
self.store.unwrap_or(true)
}
#[must_use]
pub fn parallel_tool_calls(&self) -> bool {
self.parallel_tool_calls.unwrap_or(true)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ResponseInput {
Text(String),
Items(Vec<ResponseInputItem>),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ResponseInputItem {
Message(ResponseMessageInputItem),
Compaction(ResponseCompactionItem),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResponseMessageInputItem {
#[serde(default, rename = "type")]
pub kind: Option<String>,
pub role: String,
pub content: ResponseInputContent,
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ResponseCompactionItem {
#[serde(rename = "type")]
pub kind: String,
pub encrypted_content: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ResponseInputContent {
Text(String),
Parts(Vec<ResponseInputContentPart>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ResponseInputContentPart {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub image_url: Option<String>,
#[serde(default)]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
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, Eq)]
#[serde(untagged)]
pub enum ChatContent {
Text(String),
Parts(Vec<ChatContentPart>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
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, Eq)]
pub struct ImageUrl {
pub url: String,
#[serde(default)]
pub detail: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub kind: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChatTool {
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub function: Option<FunctionTool>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FunctionTool {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub parameters: Option<Value>,
#[serde(default)]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ImageGenerationRequest {
pub model: String,
pub prompt: String,
#[serde(default)]
pub n: Option<u32>,
#[serde(default)]
pub size: Option<String>,
#[serde(default)]
pub quality: Option<String>,
#[serde(default)]
pub background: Option<String>,
#[serde(default)]
pub output_format: Option<String>,
#[serde(default)]
pub response_format: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[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"
);
}
#[test]
fn parses_compaction_input_item() {
let item: ResponseInputItem = serde_json::from_value(serde_json::json!({
"type": "compaction",
"encrypted_content": "opaque"
}))
.unwrap();
match item {
ResponseInputItem::Compaction(compaction) => {
assert_eq!(compaction.kind, "compaction");
assert_eq!(compaction.encrypted_content, "opaque");
}
ResponseInputItem::Message(_) => panic!("expected compaction item"),
}
}
}