use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Map;
use crate::Json;
use super::optimization::LlmOptimizationSummary;
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 optimization_summary: Option<LlmOptimizationSummary>,
#[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 uncached_input_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(|| {
if self.source != CostSource::ProviderReported {
return None;
}
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 = "oci_genai")]
OCIGenAI {
#[serde(skip_serializing_if = "Option::is_none")]
api_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model_version: Option<String>,
},
#[serde(rename = "gemini_generate_content")]
GeminiGenerateContent {
#[serde(skip_serializing_if = "Option::is_none")]
thoughts_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
safety_ratings: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
grounding_metadata: Option<Json>,
#[serde(skip_serializing_if = "Option::is_none")]
citation_metadata: Option<Json>,
#[serde(
flatten,
default,
skip_serializing_if = "gemini_extra_is_empty",
serialize_with = "serialize_gemini_extra",
deserialize_with = "deserialize_gemini_extra"
)]
extra: serde_json::Map<String, Json>,
},
#[serde(rename = "custom")]
Custom {
api_name: String,
data: Json,
},
}
fn gemini_extra_is_empty(extra: &Map<String, Json>) -> bool {
extra.keys().all(|key| key == "api")
}
fn serialize_gemini_extra<S>(extra: &Map<String, Json>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let filtered = extra
.iter()
.filter(|(key, _)| key.as_str() != "api")
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<Map<String, Json>>();
filtered.serialize(serializer)
}
fn deserialize_gemini_extra<'de, D>(deserializer: D) -> Result<Map<String, Json>, D::Error>
where
D: Deserializer<'de>,
{
let mut extra = Map::<String, Json>::deserialize(deserializer)?;
extra.remove("api");
Ok(extra)
}
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::ProviderNative { value, .. } => value
.get("text")
.and_then(crate::Json::as_str)
.or_else(|| value.get("refusal").and_then(crate::Json::as_str)),
_ => None,
}),
}
}
#[must_use]
pub fn has_tool_calls(&self) -> bool {
self.tool_calls
.as_ref()
.is_some_and(|calls| !calls.is_empty())
}
}