use serde::{Deserialize, Serialize};
use crate::json::Json;
use super::request::MessageContent;
#[derive(Debug, Clone, 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, Eq, 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>,
}
#[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(rename = "anthropic_messages")]
AnthropicMessages {
#[serde(skip_serializing_if = "Option::is_none")]
stop_sequence: Option<String>,
#[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()
.map(|p| {
let super::request::ContentPart::Text { text } = p;
text.as_str()
})
.next(),
}
}
#[must_use]
pub fn has_tool_calls(&self) -> bool {
self.tool_calls
.as_ref()
.is_some_and(|calls| !calls.is_empty())
}
}
#[cfg(test)]
#[path = "../../tests/unit/codec/response_tests.rs"]
mod tests;