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, Serialize)]
#[serde(untagged)]
pub enum ResponseInputItem {
Message(ResponseMessageInputItem),
FunctionCall(ResponseFunctionCallInputItem),
FunctionCallOutput(ResponseFunctionCallOutputInputItem),
Compaction(ResponseCompactionItem),
Reasoning(ResponseReasoningInputItem),
Raw(Value),
}
impl<'de> Deserialize<'de> for ResponseInputItem {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
let parsed = match value.get("type").and_then(Value::as_str) {
Some("function_call") => serde_json::from_value(value.clone()).map(Self::FunctionCall),
Some("function_call_output") => {
serde_json::from_value(value.clone()).map(Self::FunctionCallOutput)
}
Some("compaction") => serde_json::from_value(value.clone()).map(Self::Compaction),
Some("reasoning") => serde_json::from_value(value.clone()).map(Self::Reasoning),
Some("message") => serde_json::from_value(value.clone()).map(Self::Message),
_ if is_untyped_response_message(&value) => {
serde_json::from_value(value.clone()).map(Self::Message)
}
_ => Ok(Self::Raw(value.clone())),
};
Ok(parsed.unwrap_or(Self::Raw(value)))
}
}
fn is_untyped_response_message(value: &Value) -> bool {
response_item_type_is_missing(value)
&& value.get("role").is_some()
&& value.get("content").is_some()
}
fn response_item_type_is_missing(value: &Value) -> bool {
match value.get("type") {
None | Some(Value::Null) => true,
Some(Value::String(kind)) => kind.is_empty(),
Some(_) => false,
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResponseMessageInputItem {
#[serde(default, rename = "type", skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
pub role: String,
pub content: ResponseInputContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResponseFunctionCallInputItem {
#[serde(rename = "type")]
pub kind: String,
pub call_id: String,
pub name: String,
#[serde(default)]
pub arguments: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResponseFunctionCallOutputInputItem {
#[serde(rename = "type")]
pub kind: String,
pub call_id: String,
#[serde(default)]
pub output: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ResponseReasoningInputItem {
#[serde(rename = "type")]
pub kind: String,
pub encrypted_content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[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, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[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");
}
_ => panic!("expected compaction item"),
}
}
#[test]
fn parses_responses_function_and_reasoning_input_items() {
let request: ResponsesRequest = serde_json::from_value(serde_json::json!({
"model": "gpt-5.5",
"input": [
{
"type": "function_call",
"call_id": "call_1",
"name": "lookup",
"arguments": "{\"q\":\"x\"}"
},
{
"type": "function_call_output",
"call_id": "call_1",
"output": "done"
},
{
"type": "reasoning",
"encrypted_content": "opaque",
"summary": []
}
]
}))
.unwrap();
let items = match request.input.unwrap() {
ResponseInput::Items(items) => items,
ResponseInput::Text(_) => panic!("expected structured input"),
};
assert_eq!(items.len(), 3);
match &items[0] {
ResponseInputItem::FunctionCall(call) => {
assert_eq!(call.call_id, "call_1");
assert_eq!(call.name, "lookup");
}
_ => panic!("expected function call item"),
}
match &items[1] {
ResponseInputItem::FunctionCallOutput(output) => {
assert_eq!(output.call_id, "call_1");
assert_eq!(output.output, serde_json::json!("done"));
}
_ => panic!("expected function call output item"),
}
match &items[2] {
ResponseInputItem::Reasoning(reasoning) => {
assert_eq!(reasoning.encrypted_content, "opaque");
}
_ => panic!("expected reasoning item"),
}
}
#[test]
fn preserves_unknown_typed_response_items_as_raw() {
let request: ResponsesRequest = serde_json::from_value(serde_json::json!({
"model": "gpt-5.5",
"input": [
{
"type": "future_item",
"role": "assistant",
"content": "opaque"
},
{
"role": "user",
"content": "hello"
}
]
}))
.unwrap();
let items = match request.input.unwrap() {
ResponseInput::Items(items) => items,
ResponseInput::Text(_) => panic!("expected structured input"),
};
match &items[0] {
ResponseInputItem::Raw(raw) => assert_eq!(raw["type"], "future_item"),
_ => panic!("expected unknown typed item to stay raw"),
}
match &items[1] {
ResponseInputItem::Message(message) => {
assert_eq!(message.role, "user");
}
_ => panic!("expected untyped role/content item to parse as message"),
}
}
}