use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponsesInput {
Text(String),
Items(Vec<InputItem>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputItem {
Message {
role: InputRole,
content: MessageContent,
},
FunctionCallOutput { call_id: String, output: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum InputRole {
User,
Assistant,
System,
Developer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Parts(Vec<ContentPart>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
InputText { text: String },
InputImage { image_url: String },
InputFile {
#[serde(skip_serializing_if = "Option::is_none")]
file_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_id: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningConfig {
#[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 ResponsesRequest {
pub model: String,
pub input: ResponsesInput,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[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_output_tokens: Option<u32>,
#[serde(default)]
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ResponsesTool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ResponsesToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfig>,
#[serde(default)]
pub background: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponsesTool {
Function {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
parameters: Option<serde_json::Value>,
},
WebSearch {},
FileSearch {},
CodeInterpreter {},
Mcp {
server_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
headers: Option<std::collections::HashMap<String, String>>,
},
ImageGeneration {},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponsesToolChoice {
String(String),
Function { r#type: String, name: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Completed,
Failed,
InProgress,
Queued,
Incomplete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesResponse {
pub id: String,
pub object: String,
pub created_at: i64,
pub model: String,
pub status: ResponseStatus,
pub output: Vec<OutputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<ResponsesUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ResponsesError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, String>>,
}
impl ResponsesResponse {
pub fn new(model: String, content: String, usage: ResponsesUsage) -> Self {
let output_item = OutputItem::Message {
id: format!("msg_{}", uuid::Uuid::new_v4()),
role: OutputRole::Assistant,
status: ItemStatus::Completed,
content: vec![OutputContentPart::OutputText {
text: content.clone(),
}],
};
Self {
id: format!("resp_{}", uuid::Uuid::new_v4()),
object: "response".to_string(),
created_at: chrono::Utc::now().timestamp(),
model,
status: ResponseStatus::Completed,
output: vec![output_item],
output_text: Some(content),
usage: Some(usage),
error: None,
metadata: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputItem {
Message {
id: String,
role: OutputRole,
status: ItemStatus,
content: Vec<OutputContentPart>,
},
FunctionCall {
id: String,
call_id: String,
name: String,
arguments: String,
status: ItemStatus,
},
Reasoning {
id: String,
status: ItemStatus,
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<Vec<ReasoningSummary>>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OutputRole {
Assistant,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ItemStatus {
Completed,
InProgress,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputContentPart {
OutputText { text: String },
Refusal { refusal: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReasoningSummary {
#[serde(rename = "type")]
pub summary_type: String,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesUsage {
pub input_tokens: u32,
pub output_tokens: u32,
pub total_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens_details: Option<OutputTokensDetails>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputTokensDetails {
pub reasoning_tokens: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesError {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
}
impl ResponsesError {
pub fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
Self {
error_type: error_type.into(),
message: message.into(),
code: None,
}
}
pub fn rate_limit() -> Self {
Self {
error_type: "rate_limit_error".to_string(),
message: "Rate limit exceeded. Please retry after some time.".to_string(),
code: Some("rate_limit_exceeded".to_string()),
}
}
pub fn server_error() -> Self {
Self {
error_type: "server_error".to_string(),
message: "The server had an error processing your request.".to_string(),
code: Some("server_error".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponsesErrorResponse {
pub error: ResponsesError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamEvent {
#[serde(rename = "type")]
pub event_type: String,
#[serde(flatten)]
pub data: StreamEventData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum StreamEventData {
Response(ResponseEventData),
OutputItem(OutputItemEventData),
ContentPart(ContentPartEventData),
TextDelta(TextDeltaEventData),
Error(ErrorEventData),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseEventData {
pub response: ResponsesResponse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputItemEventData {
pub output_index: u32,
pub item: OutputItem,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentPartEventData {
pub output_index: u32,
pub content_index: u32,
pub part: OutputContentPart,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextDeltaEventData {
pub output_index: u32,
pub content_index: u32,
pub delta: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sequence_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorEventData {
pub error: ResponsesError,
}
pub struct ResponsesStreamEvent;
impl ResponsesStreamEvent {
pub fn response_created(response: ResponsesResponse) -> String {
let event = serde_json::json!({
"type": "response.created",
"response": response
});
format!("event: response.created\ndata: {}\n\n", event)
}
pub fn response_in_progress(response: ResponsesResponse) -> String {
let event = serde_json::json!({
"type": "response.in_progress",
"response": response
});
format!("event: response.in_progress\ndata: {}\n\n", event)
}
pub fn output_item_added(output_index: u32, item: &OutputItem) -> String {
let event = serde_json::json!({
"type": "response.output_item.added",
"output_index": output_index,
"item": item
});
format!("event: response.output_item.added\ndata: {}\n\n", event)
}
pub fn content_part_added(
output_index: u32,
content_index: u32,
part: &OutputContentPart,
) -> String {
let event = serde_json::json!({
"type": "response.content_part.added",
"output_index": output_index,
"content_index": content_index,
"part": part
});
format!("event: response.content_part.added\ndata: {}\n\n", event)
}
pub fn output_text_delta(
output_index: u32,
content_index: u32,
delta: &str,
sequence_number: u32,
) -> String {
let event = serde_json::json!({
"type": "response.output_text.delta",
"output_index": output_index,
"content_index": content_index,
"delta": delta,
"sequence_number": sequence_number
});
format!("event: response.output_text.delta\ndata: {}\n\n", event)
}
pub fn output_text_done(output_index: u32, content_index: u32, text: &str) -> String {
let event = serde_json::json!({
"type": "response.output_text.done",
"output_index": output_index,
"content_index": content_index,
"text": text
});
format!("event: response.output_text.done\ndata: {}\n\n", event)
}
pub fn content_part_done(
output_index: u32,
content_index: u32,
part: &OutputContentPart,
) -> String {
let event = serde_json::json!({
"type": "response.content_part.done",
"output_index": output_index,
"content_index": content_index,
"part": part
});
format!("event: response.content_part.done\ndata: {}\n\n", event)
}
pub fn output_item_done(output_index: u32, item: &OutputItem) -> String {
let event = serde_json::json!({
"type": "response.output_item.done",
"output_index": output_index,
"item": item
});
format!("event: response.output_item.done\ndata: {}\n\n", event)
}
pub fn response_completed(response: ResponsesResponse) -> String {
let event = serde_json::json!({
"type": "response.completed",
"response": response
});
format!("event: response.completed\ndata: {}\n\n", event)
}
pub fn error(error: ResponsesError) -> String {
let event = serde_json::json!({
"type": "error",
"error": error
});
format!("event: error\ndata: {}\n\n", event)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_responses_input_text() {
let json = r#""What is the capital of France?""#;
let input: ResponsesInput = serde_json::from_str(json).unwrap();
match input {
ResponsesInput::Text(s) => assert_eq!(s, "What is the capital of France?"),
_ => panic!("Expected Text variant"),
}
}
#[test]
fn test_responses_input_items() {
let json = r#"[
{"type": "message", "role": "user", "content": "Hello!"}
]"#;
let input: ResponsesInput = serde_json::from_str(json).unwrap();
match input {
ResponsesInput::Items(items) => {
assert_eq!(items.len(), 1);
}
_ => panic!("Expected Items variant"),
}
}
#[test]
fn test_responses_request_simple() {
let json = r#"{
"model": "gpt-5",
"input": "Tell me a story"
}"#;
let request: ResponsesRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.model, "gpt-5");
assert!(!request.stream);
}
#[test]
fn test_responses_request_with_messages() {
let json = r#"{
"model": "gpt-5",
"input": [
{"type": "message", "role": "user", "content": "Hello!"},
{"type": "message", "role": "assistant", "content": "Hi there!"}
],
"temperature": 0.7,
"stream": true
}"#;
let request: ResponsesRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.model, "gpt-5");
assert_eq!(request.temperature, Some(0.7));
assert!(request.stream);
}
#[test]
fn test_responses_response_new() {
let usage = ResponsesUsage {
input_tokens: 10,
output_tokens: 20,
total_tokens: 30,
output_tokens_details: None,
};
let response = ResponsesResponse::new("gpt-5".to_string(), "Hello!".to_string(), usage);
assert_eq!(response.object, "response");
assert_eq!(response.model, "gpt-5");
assert_eq!(response.status, ResponseStatus::Completed);
assert_eq!(response.output.len(), 1);
assert_eq!(response.output_text, Some("Hello!".to_string()));
}
#[test]
fn test_responses_response_serialization() {
let usage = ResponsesUsage {
input_tokens: 10,
output_tokens: 20,
total_tokens: 30,
output_tokens_details: Some(OutputTokensDetails {
reasoning_tokens: 0,
}),
};
let response =
ResponsesResponse::new("gpt-5".to_string(), "Test response".to_string(), usage);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"object\":\"response\""));
assert!(json.contains("\"status\":\"completed\""));
assert!(json.contains("\"output_text\":\"Test response\""));
}
#[test]
fn test_content_part_types() {
let json = r#"{"type": "input_text", "text": "Hello"}"#;
let part: ContentPart = serde_json::from_str(json).unwrap();
match part {
ContentPart::InputText { text } => assert_eq!(text, "Hello"),
_ => panic!("Expected InputText variant"),
}
}
#[test]
fn test_stream_event_creation() {
let delta = ResponsesStreamEvent::output_text_delta(0, 0, "Hello", 1);
assert!(delta.contains("event: response.output_text.delta"));
assert!(delta.contains("\"delta\":\"Hello\""));
assert!(delta.contains("\"sequence_number\":1"));
}
#[test]
fn test_error_response() {
let error = ResponsesError::rate_limit();
let error_response = ResponsesErrorResponse { error };
let json = serde_json::to_string(&error_response).unwrap();
assert!(json.contains("\"type\":\"rate_limit_error\""));
}
}