use crate::tools::{ToolCall, ToolCallDelta};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Default, Serialize)]
pub struct Usage {
pub uncached_input_tokens: u32,
pub cache_read_tokens: u32,
pub cache_write_tokens: u32,
pub completion_tokens: u32,
pub cost: Option<f64>,
pub upstream_inference_cost: Option<f64>,
pub reasoning_tokens: Option<u32>,
}
impl Usage {
pub fn prompt_tokens(&self) -> u32 {
self.uncached_input_tokens + self.cache_read_tokens + self.cache_write_tokens
}
pub fn total_tokens(&self) -> u32 {
self.prompt_tokens() + self.completion_tokens
}
pub(crate) fn merge_from(&mut self, other: &Usage) {
if other.uncached_input_tokens != 0 {
self.uncached_input_tokens = other.uncached_input_tokens;
}
if other.cache_read_tokens != 0 {
self.cache_read_tokens = other.cache_read_tokens;
}
if other.cache_write_tokens != 0 {
self.cache_write_tokens = other.cache_write_tokens;
}
if other.completion_tokens != 0 {
self.completion_tokens = other.completion_tokens;
}
self.cost = other.cost.or(self.cost);
self.upstream_inference_cost = other
.upstream_inference_cost
.or(self.upstream_inference_cost);
self.reasoning_tokens = other.reasoning_tokens.or(self.reasoning_tokens);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum CostResolution {
#[default]
Resolved,
Unpriced,
Unknown,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CostInfo {
pub cost: f64,
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
pub cache_read_tokens: u32,
pub cache_write_tokens: u32,
pub reasoning_tokens: Option<u32>,
pub model: String,
pub response_id: String,
pub resolution: CostResolution,
}
pub type CostCallback = Arc<dyn Fn(CostInfo) + Send + Sync>;
#[derive(Debug, Clone, Serialize)]
pub struct CompletionResponse {
pub id: String,
pub model: String,
pub content: String,
pub finish_reason: Option<String>,
pub usage: Option<Usage>,
pub tool_calls: Option<Vec<ToolCall>>,
#[serde(skip)]
pub media: Vec<crate::message::Media>,
#[serde(skip)]
pub raw_response: Option<serde_json::Value>,
}
impl CompletionResponse {
pub fn new(
id: impl Into<String>,
model: impl Into<String>,
content: impl Into<String>,
) -> Self {
Self {
id: id.into(),
model: model.into(),
content: content.into(),
finish_reason: None,
usage: None,
tool_calls: None,
media: Vec::new(),
raw_response: None,
}
}
pub fn to_assistant_message(&self) -> crate::message::Message {
let mut message = if self.media.is_empty() {
crate::message::Message::assistant(self.content.clone())
} else {
let mut parts = Vec::new();
if !self.content.is_empty() {
parts.push(crate::message::ContentPart::text(self.content.clone()));
}
parts.extend(
self.media
.iter()
.map(crate::message::ContentPart::from_media),
);
crate::message::Message::assistant(crate::message::MessageContent::parts(parts))
};
message.tool_calls = self.tool_calls.clone();
message
}
pub fn is_complete(&self) -> bool {
self.finish_reason.as_deref() == Some("stop")
}
pub fn is_truncated(&self) -> bool {
self.finish_reason.as_deref() == Some("length")
}
pub fn has_tool_calls(&self) -> bool {
self.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty())
}
}
#[derive(Debug, Clone, Default)]
pub struct StreamChunk {
pub id: Option<String>,
pub delta: String,
pub finish_reason: Option<String>,
pub usage: Option<Usage>,
pub tool_calls: Option<Vec<ToolCallDelta>>,
}
impl StreamChunk {
pub fn content(delta: impl Into<String>) -> Self {
Self {
delta: delta.into(),
..Default::default()
}
}
pub fn finished(finish_reason: impl Into<String>) -> Self {
Self {
finish_reason: Some(finish_reason.into()),
..Default::default()
}
}
pub fn is_final(&self) -> bool {
self.finish_reason.is_some()
}
}
pub(crate) fn preview_str(body: &str) -> String {
const MAX: usize = 200;
match body.char_indices().nth(MAX) {
Some((cut, _)) => format!("{}…", &body[..cut]),
None => body.to_string(),
}
}
pub(crate) fn error_object(raw: &serde_json::Value) -> Option<&serde_json::Value> {
raw.get("error")
.filter(|e| e.as_object().is_some_and(|o| !o.is_empty()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_only_response_becomes_a_text_message() {
let resp = CompletionResponse::new("id", "m", "plain answer");
let message = resp.to_assistant_message();
assert_eq!(message.text(), Some("plain answer"));
assert!(matches!(
message.content,
crate::message::MessageContent::Text(_)
));
}
#[test]
fn error_object_ignores_benign_falsy_error_fields() {
for benign in [
serde_json::json!({"error": null}),
serde_json::json!({"error": {}}),
serde_json::json!({"error": false}),
serde_json::json!({"error": 0}),
serde_json::json!({"error": ""}),
serde_json::json!({"error": "some string"}),
serde_json::json!({"error": ["a", "b"]}),
serde_json::json!({"id": "gen-1"}),
] {
assert!(
error_object(&benign).is_none(),
"benign error field must not be an error: {benign}"
);
}
let real = serde_json::json!({"error": {"message": "boom"}});
assert!(error_object(&real).is_some());
}
#[test]
fn usage_merge_accumulates_split_input_and_output() {
let mut acc = Usage {
uncached_input_tokens: 15,
completion_tokens: 1,
..Default::default()
};
let delta = Usage {
uncached_input_tokens: 0,
completion_tokens: 9,
..Default::default()
};
acc.merge_from(&delta);
assert_eq!(
acc.uncached_input_tokens, 15,
"input from message_start preserved"
);
assert_eq!(
acc.completion_tokens, 9,
"output from message_delta applied"
);
assert_eq!(
acc.total_tokens(),
24,
"total recomputed from merged buckets"
);
}
}