use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseCreateParams {
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<ResponseInput>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tool_calls: Option<i32>,
#[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 stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<ResponseTextConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<Reasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<Conversation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_logprobs: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub safety_identifier: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponseInput {
Simple(String),
Structured(Vec<InputItem>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InputItem {
Message {
role: MessageRole,
content: MessageContent,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
},
#[serde(rename = "function_call_output")]
FunctionCallOutput {
call_id: String,
output: String,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
#[serde(rename = "function_call")]
FunctionToolCall {
id: String,
call_id: String,
name: String,
arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
Reasoning {
id: String,
#[serde(default)]
content: Vec<ReasoningContent>,
#[serde(default)]
summary: Vec<ReasoningContent>,
#[serde(skip_serializing_if = "Option::is_none")]
encrypted_content: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
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 {
#[serde(rename = "input_text")]
InputText {
text: String,
},
#[serde(rename = "input_image")]
InputImage {
image_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
#[serde(rename = "input_file")]
InputFile {
#[serde(skip_serializing_if = "Option::is_none")]
file_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file_url: Option<String>,
},
#[serde(rename = "output_text")]
OutputText {
text: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub id: String,
pub created_at: f64,
pub object: String,
pub model: String,
pub status: ResponseStatus,
pub output: Vec<OutputItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default)]
pub tools: Vec<Tool>,
#[serde(default)]
pub tool_choice: ToolChoice,
#[serde(default)]
pub parallel_tool_calls: bool,
#[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 usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ResponseError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub incomplete_details: Option<IncompleteDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation: Option<Conversation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Completed,
Failed,
InProgress,
Cancelled,
Queued,
Incomplete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputItem {
Message {
id: String,
role: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
content: Vec<OutputContent>,
},
#[serde(rename = "function_call")]
FunctionCall {
id: String,
call_id: String,
name: String,
arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
},
Reasoning {
id: String,
#[serde(default)]
content: Vec<ReasoningContent>,
#[serde(default)]
summary: Vec<ReasoningContent>,
#[serde(skip_serializing_if = "Option::is_none")]
encrypted_content: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OutputContent {
#[serde(rename = "output_text")]
OutputText {
text: String,
#[serde(default)]
annotations: Vec<Annotation>,
#[serde(skip_serializing_if = "Option::is_none")]
logprobs: Option<Vec<Logprob>>,
},
#[serde(rename = "output_refusal")]
OutputRefusal {
refusal: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReasoningContent {
#[serde(rename = "reasoning_text")]
ReasoningText {
text: String,
},
#[serde(rename = "summary_text")]
SummaryText {
text: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Annotation {
#[serde(rename = "file_citation")]
FileCitation {
file_id: String,
filename: String,
index: usize,
},
#[serde(rename = "url_citation")]
UrlCitation {
url: String,
title: String,
start_index: usize,
end_index: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Logprob {
pub token: String,
pub bytes: Vec<u8>,
pub logprob: f32,
#[serde(default)]
pub top_logprobs: Vec<TopLogprob>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopLogprob {
pub token: String,
pub bytes: Vec<u8>,
pub logprob: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Tool {
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>,
#[serde(skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolChoice {
String(String),
Specific(ToolChoiceSpecific),
}
impl Default for ToolChoice {
fn default() -> Self {
Self::String("auto".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolChoiceSpecific {
Function {
function: FunctionChoice,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionChoice {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_obfuscation: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TextVerbosity {
Low,
#[default]
Medium,
High,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseTextFormat {
Text,
#[serde(rename = "json_object")]
JsonObject,
#[serde(rename = "json_schema")]
JsonSchema {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
strict: Option<bool>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseTextConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<ResponseTextFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verbosity: Option<TextVerbosity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Reasoning {
#[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)]
#[serde(untagged)]
pub enum Conversation {
Id {
id: String,
},
Simple(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
pub input_tokens: i32,
#[serde(default)]
pub input_tokens_details: InputTokensDetails,
pub output_tokens: i32,
#[serde(default)]
pub output_tokens_details: OutputTokensDetails,
#[serde(default)]
pub total_tokens: i32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InputTokensDetails {
#[serde(default)]
pub cached_tokens: i32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OutputTokensDetails {
#[serde(default)]
pub reasoning_tokens: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseError {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub message: String,
}
impl ResponseError {
pub fn is_retryable(&self) -> bool {
if let Some(ref code) = self.code {
matches!(
code.as_str(),
"internal_server_error" | "server_error" | "timeout"
)
} else {
let msg = self.message.to_ascii_lowercase();
msg.contains("internal server error")
|| msg.contains("server error")
|| msg.contains("timeout")
|| msg.contains("temporarily unavailable")
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IncompleteDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_error_is_retryable_with_code() {
let error = ResponseError {
code: Some("internal_server_error".to_string()),
message: "Internal server error".to_string(),
};
assert!(error.is_retryable());
let error = ResponseError {
code: Some("server_error".to_string()),
message: "Server error".to_string(),
};
assert!(error.is_retryable());
let error = ResponseError {
code: Some("timeout".to_string()),
message: "Request timeout".to_string(),
};
assert!(error.is_retryable());
}
#[test]
fn test_response_error_is_retryable_without_code() {
let error = ResponseError {
code: None,
message: "Internal server error occurred".to_string(),
};
assert!(error.is_retryable());
let error = ResponseError {
code: None,
message: "Server error: timeout".to_string(),
};
assert!(error.is_retryable());
let error = ResponseError {
code: None,
message: "Service temporarily unavailable".to_string(),
};
assert!(error.is_retryable());
}
#[test]
fn test_response_error_not_retryable() {
let error = ResponseError {
code: Some("invalid_request".to_string()),
message: "Invalid request".to_string(),
};
assert!(!error.is_retryable());
let error = ResponseError {
code: Some("authentication_error".to_string()),
message: "Invalid API key".to_string(),
};
assert!(!error.is_retryable());
let error = ResponseError {
code: None,
message: "Bad request format".to_string(),
};
assert!(!error.is_retryable());
}
#[test]
fn test_usage_defaults_when_details_missing() {
let payload = r#"{
"input_tokens": 0,
"output_tokens": 0,
"total_tokens": 0,
"output_tokens_details": {"reasoning_tokens": 0}
}"#;
let usage: Usage = serde_json::from_str(payload).expect("usage payload should deserialize");
assert_eq!(usage.input_tokens_details.cached_tokens, 0);
assert_eq!(usage.output_tokens_details.reasoning_tokens, 0);
}
#[test]
fn test_usage_with_all_fields_populated() {
let payload = r#"{
"input_tokens": 1500,
"output_tokens": 750,
"input_tokens_details": {
"cached_tokens": 500
},
"output_tokens_details": {
"reasoning_tokens": 250
},
"total_tokens": 2250
}"#;
let usage: Usage = serde_json::from_str(payload).expect("usage payload should deserialize");
assert_eq!(usage.input_tokens, 1500);
assert_eq!(usage.output_tokens, 750);
assert_eq!(usage.input_tokens_details.cached_tokens, 500);
assert_eq!(usage.output_tokens_details.reasoning_tokens, 250);
assert_eq!(usage.total_tokens, 2250);
}
#[test]
fn test_unified_usage_conversion_from_openai() {
let usage = Usage {
input_tokens: 1000,
output_tokens: 500,
input_tokens_details: InputTokensDetails { cached_tokens: 200 },
output_tokens_details: OutputTokensDetails {
reasoning_tokens: 100,
},
total_tokens: 1500,
};
let input_tokens = usage.input_tokens.max(0) as u32;
let output_tokens = usage.output_tokens.max(0) as u32;
let cache_read_tokens = usage.input_tokens_details.cached_tokens.max(0) as u32;
let reasoning_tokens = usage.output_tokens_details.reasoning_tokens.max(0) as u32;
let unified = crate::llm::unified::UnifiedUsage {
input_tokens,
output_tokens,
cache_creation_input_tokens: None,
cache_read_input_tokens: (cache_read_tokens > 0).then_some(cache_read_tokens),
reasoning_tokens: (reasoning_tokens > 0).then_some(reasoning_tokens),
};
assert_eq!(unified.input_tokens, 1000);
assert_eq!(unified.output_tokens, 500);
assert_eq!(unified.cache_read_input_tokens, Some(200));
assert_eq!(unified.reasoning_tokens, Some(100));
assert_eq!(unified.cache_creation_input_tokens, None);
}
#[test]
fn test_usage_with_zeros() {
let payload = r#"{
"input_tokens": 0,
"output_tokens": 0,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 0
}"#;
let usage: Usage = serde_json::from_str(payload).expect("usage payload should deserialize");
assert_eq!(usage.input_tokens, 0);
assert_eq!(usage.output_tokens, 0);
assert_eq!(usage.input_tokens_details.cached_tokens, 0);
assert_eq!(usage.output_tokens_details.reasoning_tokens, 0);
let input_tokens = usage.input_tokens.max(0) as u32;
let output_tokens = usage.output_tokens.max(0) as u32;
let cache_read_tokens = usage.input_tokens_details.cached_tokens.max(0) as u32;
let reasoning_tokens = usage.output_tokens_details.reasoning_tokens.max(0) as u32;
let unified = crate::llm::unified::UnifiedUsage {
input_tokens,
output_tokens,
cache_creation_input_tokens: None,
cache_read_input_tokens: (cache_read_tokens > 0).then_some(cache_read_tokens),
reasoning_tokens: (reasoning_tokens > 0).then_some(reasoning_tokens),
};
assert_eq!(unified.input_tokens, 0);
assert_eq!(unified.output_tokens, 0);
assert_eq!(unified.cache_read_input_tokens, None);
assert_eq!(unified.reasoning_tokens, None);
}
}