use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesApiRequest {
pub model: String,
pub input: ResponseInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ResponseTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[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 user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponseInput {
Text(String),
Items(Vec<ResponseInputItem>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseInputItem {
Message(ResponseInputMessage),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseInputMessage {
pub role: String,
pub content: ResponseInputContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponseInputContent {
Text(String),
Parts(Vec<ResponseInputContentPart>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseInputContentPart {
InputText { text: String },
InputImage {
#[serde(skip_serializing_if = "Option::is_none")]
image_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
OutputText { text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseTool {
WebSearch(WebSearchTool),
#[serde(rename = "web_search_preview")]
WebSearchPreview(WebSearchTool),
FileSearch(FileSearchTool),
CodeInterpreter(CodeInterpreterTool),
#[serde(rename = "computer_use_preview")]
ComputerUsePreview(ComputerUseTool),
Mcp(McpTool),
Function(ResponseFunctionTool),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct WebSearchTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_location: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search_context_size: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileSearchTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub vector_store_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_num_results: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ranking_options: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CodeInterpreterTool {
#[serde(skip_serializing_if = "Option::is_none")]
pub container: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputerUseTool {
pub display_width: u32,
pub display_height: u32,
pub environment: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpTool {
pub server_label: String,
pub server_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_tools: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<std::collections::HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub require_approval: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseFunctionTool {
pub function: ResponseFunctionDefinition,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseFunctionDefinition {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesApiResponse {
pub id: String,
pub object: String,
pub created_at: i64,
pub status: String,
pub model: String,
pub output: Vec<ResponseOutputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<ResponseUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ResponseApiError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<std::collections::HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseOutputItem {
Message(ResponseOutputMessage),
FunctionCall(ResponseFunctionCall),
WebSearchCall(ResponseToolCall),
FileSearchCall(ResponseToolCall),
CodeInterpreterCall(ResponseToolCall),
ComputerCall(ResponseToolCall),
McpCall(ResponseToolCall),
Reasoning(ResponseReasoningItem),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseOutputMessage {
pub id: String,
pub role: String,
pub status: String,
pub content: Vec<ResponseOutputContent>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseOutputContent {
OutputText {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
logprobs: Option<Vec<Value>>,
},
Refusal {
refusal: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseToolCall {
pub id: String,
pub status: String,
#[serde(flatten)]
pub data: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseReasoningItem {
pub id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<Vec<Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResponseUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub total_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens_details: Option<ResponseInputTokensDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens_details: Option<ResponseOutputTokensDetails>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResponseInputTokensDetails {
pub cached_tokens: u32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResponseOutputTokensDetails {
pub reasoning_tokens: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseApiError {
pub code: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ResponseStreamEvent {
#[serde(rename = "response.created")]
ResponseCreated { response: Box<ResponsesApiResponse> },
#[serde(rename = "response.in_progress")]
ResponseInProgress { response: Box<ResponsesApiResponse> },
#[serde(rename = "response.output_item.added")]
ResponseOutputItemAdded {
output_index: u32,
item: ResponseOutputItem,
},
#[serde(rename = "response.content_part.added")]
ResponseContentPartAdded {
output_index: u32,
content_index: u32,
part: ResponseOutputContent,
},
#[serde(rename = "response.output_text.delta")]
ResponseOutputTextDelta {
output_index: u32,
content_index: u32,
delta: String,
},
#[serde(rename = "response.output_text.done")]
ResponseOutputTextDone {
output_index: u32,
content_index: u32,
text: String,
},
#[serde(rename = "response.content_part.done")]
ResponseContentPartDone {
output_index: u32,
content_index: u32,
part: ResponseOutputContent,
},
#[serde(rename = "response.output_item.done")]
ResponseOutputItemDone {
output_index: u32,
item: ResponseOutputItem,
},
#[serde(rename = "response.completed")]
ResponseCompleted { response: Box<ResponsesApiResponse> },
#[serde(rename = "response.failed")]
ResponseFailed { response: Box<ResponsesApiResponse> },
#[serde(rename = "response.function_call_arguments.delta")]
ResponseFunctionCallArgumentsDelta {
output_index: u32,
call_id: String,
delta: String,
},
#[serde(rename = "response.function_call_arguments.done")]
ResponseFunctionCallArgumentsDone {
output_index: u32,
call_id: String,
arguments: String,
},
#[serde(other)]
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_request_text_input_deserialise() {
let json = r#"{"model":"gpt-4o","input":"Hello"}"#;
let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.model, "gpt-4o");
assert!(matches!(req.input, ResponseInput::Text(_)));
}
#[test]
fn test_request_array_input_deserialise() {
let json = r#"{
"model": "gpt-4o",
"input": [
{"type": "message", "role": "user", "content": "Hello"}
]
}"#;
let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
assert!(matches!(req.input, ResponseInput::Items(_)));
}
#[test]
fn test_request_with_instructions() {
let json = r#"{"model":"gpt-4o","input":"Hi","instructions":"Be concise"}"#;
let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.instructions.as_deref(), Some("Be concise"));
}
#[test]
fn test_request_with_previous_response_id() {
let json = r#"{"model":"gpt-4o","input":"Follow up","previous_response_id":"resp_abc"}"#;
let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.previous_response_id.as_deref(), Some("resp_abc"));
}
#[test]
fn test_request_with_store_flag() {
let json = r#"{"model":"gpt-4o","input":"Hi","store":true}"#;
let req: ResponsesApiRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.store, Some(true));
}
#[test]
fn test_web_search_tool_deserialise() {
let json = r#"{"type":"web_search"}"#;
let tool: ResponseTool = serde_json::from_str(json).unwrap();
assert!(matches!(tool, ResponseTool::WebSearch(_)));
}
#[test]
fn test_file_search_tool_deserialise() {
let json = r#"{"type":"file_search","vector_store_ids":["vs_abc"]}"#;
let tool: ResponseTool = serde_json::from_str(json).unwrap();
assert!(matches!(tool, ResponseTool::FileSearch(_)));
}
#[test]
fn test_mcp_tool_deserialise() {
let json = r#"{
"type": "mcp",
"server_label": "my-server",
"server_url": "https://example.com/mcp"
}"#;
let tool: ResponseTool = serde_json::from_str(json).unwrap();
assert!(matches!(tool, ResponseTool::Mcp(_)));
}
#[test]
fn test_function_tool_deserialise() {
let json = r#"{
"type": "function",
"function": {"name": "get_weather", "description": "Get weather"}
}"#;
let tool: ResponseTool = serde_json::from_str(json).unwrap();
assert!(matches!(tool, ResponseTool::Function(_)));
}
#[test]
fn test_response_serialise() {
let resp = ResponsesApiResponse {
id: "resp_123".to_string(),
object: "response".to_string(),
created_at: 1_700_000_000,
status: "completed".to_string(),
model: "gpt-4o".to_string(),
output: vec![],
usage: None,
error: None,
previous_response_id: None,
metadata: None,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"object\":\"response\""));
assert!(json.contains("resp_123"));
}
#[test]
fn test_response_usage_defaults() {
let usage = ResponseUsage::default();
assert_eq!(usage.input_tokens, 0);
assert_eq!(usage.output_tokens, 0);
assert_eq!(usage.total_tokens, 0);
}
#[test]
fn test_stream_event_text_delta_serialise() {
let event = ResponseStreamEvent::ResponseOutputTextDelta {
output_index: 0,
content_index: 0,
delta: "Hello".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("response.output_text.delta"));
assert!(json.contains("Hello"));
}
#[test]
fn test_reasoning_params_deserialise() {
let json = r#"{"effort":"high","summary":"auto"}"#;
let params: ReasoningParams = serde_json::from_str(json).unwrap();
assert_eq!(params.effort.as_deref(), Some("high"));
}
}