use serde::{Deserialize, Serialize};
use crate::Json;
use super::request::MessageContent;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct AnnotatedLlmResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ResponseToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<FinishReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_specific: Option<ApiSpecificResponse>,
#[serde(flatten)]
pub extra: serde_json::Map<String, Json>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
pub struct Usage {
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_write_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost: Option<CostEstimate>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CostSource {
ModelPricing,
ProviderReported,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CostEstimate {
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(default = "default_cost_currency")]
pub currency: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub input: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_write: Option<f64>,
pub source: CostSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub pricing_provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pricing_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pricing_as_of: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pricing_source: Option<String>,
}
impl CostEstimate {
#[must_use]
pub fn total_or_component_sum(&self) -> Option<f64> {
self.total.or_else(|| {
let (has_component, total) =
[self.input, self.output, self.cache_read, self.cache_write]
.into_iter()
.flatten()
.fold((false, 0.0), |(_, total), value| (true, total + value));
has_component.then_some(total)
})
}
#[must_use]
pub fn total_for_currency(&self, currency: &str) -> Option<f64> {
self.currency
.eq_ignore_ascii_case(currency)
.then_some(self.total)
.flatten()
}
#[must_use]
pub fn total_or_component_sum_for_currency(&self, currency: &str) -> Option<f64> {
self.currency
.eq_ignore_ascii_case(currency)
.then(|| self.total_or_component_sum())
.flatten()
}
}
fn default_cost_currency() -> String {
"USD".into()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
Complete,
Length,
ToolUse,
ContentFilter,
Unknown(String),
}
impl FinishReason {
#[must_use]
pub fn is_complete(&self) -> bool {
matches!(self, FinishReason::Complete)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResponseToolCall {
pub id: String,
pub name: String,
pub arguments: Json,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "api")]
pub enum ApiSpecificResponse {
#[serde(rename = "openai_chat")]
OpenAIChat {
#[serde(skip_serializing_if = "Option::is_none")]
logprobs: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
system_fingerprint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<String>,
},
#[serde(rename = "openai_responses")]
OpenAIResponses {
#[serde(skip_serializing_if = "Option::is_none")]
output_items: Option<Vec<Json>>,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
incomplete_details: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
previous_response_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
store: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
truncation: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
input_tokens_details: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
output_tokens_details: Option<Json>,
},
#[serde(rename = "anthropic_messages")]
AnthropicMessages {
#[serde(skip_serializing_if = "Option::is_none")]
object_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stop_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
stop_sequence: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
container: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
content_blocks: Option<Vec<Json>>,
},
#[serde(rename = "custom")]
Custom {
api_name: String,
data: Json,
},
}
impl AnnotatedLlmResponse {
#[must_use]
pub fn response_text(&self) -> Option<&str> {
match self.message.as_ref()? {
MessageContent::Text(s) => Some(s.as_str()),
MessageContent::Parts(parts) => parts.iter().find_map(|p| match p {
crate::codec::request::ContentPart::Text { text } => Some(text.as_str()),
crate::codec::request::ContentPart::ImageUrl { .. } => None,
}),
}
}
#[must_use]
pub fn has_tool_calls(&self) -> bool {
self.tool_calls
.as_ref()
.is_some_and(|calls| !calls.is_empty())
}
}