use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use super::{OpenResponseError, OpenUsage, OutputItem};
use crate::llm::provider::ToolDefinition;
pub type ResponseId = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Queued,
#[default]
InProgress,
Completed,
Failed,
Incomplete,
}
impl ResponseStatus {
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Incomplete)
}
}
impl std::fmt::Display for ResponseStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Queued => write!(f, "queued"),
Self::InProgress => write!(f, "in_progress"),
Self::Completed => write!(f, "completed"),
Self::Failed => write!(f, "failed"),
Self::Incomplete => write!(f, "incomplete"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IncompleteDetails {
pub reason: IncompleteReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IncompleteReason {
MaxOutputTokens,
MaxToolCalls,
ContentFilter,
Cancelled,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Response {
pub id: ResponseId,
pub object: String,
pub created_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<u64>,
pub status: ResponseStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub incomplete_details: Option<IncompleteDetails>,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
pub output: Vec<OutputItem>,
pub error: Option<OpenResponseError>,
pub tools: Option<Vec<ToolDefinition>>,
pub usage: Option<OpenUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parallel_tool_calls: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tool_calls: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl Response {
pub fn new(id: impl Into<String>, model: impl Into<String>) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
id: id.into(),
object: "response".to_string(),
created_at: now,
completed_at: None,
status: ResponseStatus::InProgress,
incomplete_details: None,
model: model.into(),
previous_response_id: None,
instructions: None,
output: Vec::new(),
error: None,
tools: None,
usage: None,
parallel_tool_calls: None,
max_output_tokens: None,
max_tool_calls: None,
temperature: None,
top_p: None,
store: None,
background: None,
service_tier: None,
metadata: None,
}
}
pub fn add_output(&mut self, item: OutputItem) {
self.output.push(item);
}
pub fn complete(&mut self) {
self.status = ResponseStatus::Completed;
self.completed_at = Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
}
pub fn fail(&mut self, error: OpenResponseError) {
self.status = ResponseStatus::Failed;
self.error = Some(error);
self.completed_at = Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
}
pub fn incomplete(&mut self, reason: IncompleteReason) {
self.status = ResponseStatus::Incomplete;
self.incomplete_details = Some(IncompleteDetails { reason });
self.completed_at = Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
}
pub fn with_usage(mut self, usage: OpenUsage) -> Self {
self.usage = Some(usage);
self
}
pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools = Some(tools);
self
}
}
impl Default for Response {
fn default() -> Self {
Self::new(generate_response_id(), "unknown")
}
}
pub fn generate_response_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("resp_{:x}_{:04x}", timestamp, count)
}
pub fn generate_item_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("item_{count}")
}
#[allow(dead_code)]
pub fn generate_content_part_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("cp_{count}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_response_creation() {
let response = Response::new("resp_123", "gpt-5");
assert_eq!(response.id, "resp_123");
assert_eq!(response.model, "gpt-5");
assert_eq!(response.object, "response");
assert_eq!(response.status, ResponseStatus::InProgress);
}
#[test]
fn test_response_complete() {
let mut response = Response::new("resp_123", "gpt-5");
response.complete();
assert_eq!(response.status, ResponseStatus::Completed);
assert!(response.completed_at.is_some());
}
#[test]
fn test_response_fail() {
let mut response = Response::new("resp_123", "gpt-5");
response.fail(OpenResponseError::server_error("Test error"));
assert_eq!(response.status, ResponseStatus::Failed);
assert!(response.error.is_some());
}
#[test]
fn test_id_generation() {
let id1 = generate_response_id();
let id2 = generate_response_id();
assert_ne!(id1, id2);
assert!(id1.starts_with("resp_"));
}
}