use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::request::{ContentPart, Role};
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct ErrResponse {
pub error: Err,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct Err {
pub message: String,
pub r#type: String,
pub param: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct ResponseError {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct ResponseReasoning {
#[serde(skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct TextConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<TextFormat>,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct TextFormat {
pub r#type: String,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct Response {
pub id: String,
pub object: String,
#[serde(alias = "created")]
pub created_at: u64,
pub model: String,
pub status: ResponseStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ResponseError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub incomplete_details: Option<IncompleteDetails>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub output: Vec<Output>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ResponseReasoning>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<TextConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_fingerprint: Option<String>,
}
impl Response {
pub fn builder() -> ResponseBuilder {
ResponseBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct ResponseBuilder {
inner: Response,
}
impl Default for ResponseBuilder {
fn default() -> Self {
Self::new()
}
}
impl ResponseBuilder {
pub fn new() -> Self {
Self {
inner: Response {
object: "response".to_string(),
status: ResponseStatus::InProgress,
..Default::default()
},
}
}
pub fn build(self) -> Response {
self.inner
}
pub fn id(mut self, id: impl Into<String>) -> Self {
self.inner.id = id.into();
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.inner.model = model.into();
self
}
pub fn status(mut self, status: ResponseStatus) -> Self {
self.inner.status = status;
self
}
pub fn created(mut self, created_at: u64) -> Self {
self.inner.created_at = created_at;
self
}
pub fn created_at(self, created_at: u64) -> Self {
self.created(created_at)
}
pub fn system_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
self.inner.system_fingerprint = Some(fingerprint.into());
self
}
pub fn push_text_output(mut self, text: impl Into<String>) -> Self {
let text = text.into();
let id = format!("output-{}", self.inner.output.len());
self.inner.output.push(Output {
id,
r#type: OutputType::Message,
role: Some(Role::Assistant),
content: vec![json!({
"type": "output_text",
"text": text,
})],
..Default::default()
});
self
}
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq, Serialize)]
pub struct Output {
pub id: String,
#[serde(rename = "type")]
pub r#type: OutputType,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<OutputStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<Role>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content: Vec<ContentPart>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub summary: Vec<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub call_id: Option<String>,
}
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputType {
#[default]
Message,
Reasoning,
#[serde(rename = "function_call")]
FunctionCall,
#[serde(rename = "file_search_call")]
FileSearchCall,
#[serde(rename = "web_search_call")]
WebSearchCall,
#[serde(rename = "computer_call")]
ComputerCall,
}
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OutputStatus {
#[default]
InProgress,
Completed,
Incomplete,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct Usage {
pub input_tokens: u32,
pub output_tokens: u32,
pub total_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens_details: Option<InputTokensDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens_details: Option<OutputTokensDetails>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct InputTokensDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cached_tokens: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct OutputTokensDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accepted_prediction_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rejected_prediction_tokens: Option<u32>,
}
#[derive(Debug, Default, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
#[default]
InProgress,
Completed,
Incomplete,
Failed,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct IncompleteDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<IncompleteReason>,
}
#[derive(Debug, Deserialize, Clone, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum IncompleteReason {
MaxOutputTokens,
ContentFilter,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)]
pub struct StatusDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_push_text_output() {
let response = Response::builder()
.id("resp_123")
.model("gpt-4.1-mini")
.status(ResponseStatus::Completed)
.push_text_output("Hello there")
.build();
assert_eq!(response.id, "resp_123");
assert_eq!(response.model, "gpt-4.1-mini");
assert_eq!(response.status, ResponseStatus::Completed);
assert_eq!(response.output.len(), 1);
let item = &response.output[0];
assert_eq!(item.r#type, OutputType::Message);
assert_eq!(item.role, Some(Role::Assistant));
assert_eq!(item.content.len(), 1);
assert_eq!(
item.content[0].get("text").and_then(|value| value.as_str()),
Some("Hello there")
);
}
}