use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Usage {
#[serde(default)]
pub prompt_tokens: u32,
#[serde(default)]
pub completion_tokens: u32,
#[serde(default)]
pub total_tokens: u32,
#[serde(default)]
pub cost: Option<f64>,
#[serde(default)]
pub cached_tokens: Option<u32>,
#[serde(default)]
pub reasoning_tokens: Option<u32>,
}
#[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 cached_tokens: Option<u32>,
pub reasoning_tokens: Option<u32>,
pub model: String,
pub response_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CostTrackingType {
#[default]
None,
OpenRouter,
}
pub type CostCallback = Arc<dyn Fn(CostInfo) + Send + Sync>;
#[derive(Debug, Clone, Serialize, Deserialize)]
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<serde_json::Value>>,
#[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,
raw_response: None,
}
}
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)]
pub struct StreamChunk {
pub delta: String,
pub finish_reason: Option<String>,
pub usage: Option<Usage>,
pub tool_calls: Option<Vec<serde_json::Value>>,
}
impl StreamChunk {
pub fn content(delta: impl Into<String>) -> Self {
Self {
delta: delta.into(),
finish_reason: None,
usage: None,
tool_calls: None,
}
}
pub fn finished(finish_reason: impl Into<String>) -> Self {
Self {
delta: String::new(),
finish_reason: Some(finish_reason.into()),
usage: None,
tool_calls: None,
}
}
pub fn is_final(&self) -> bool {
self.finish_reason.is_some()
}
}
pub fn parse_completion_response(
raw: serde_json::Value,
) -> crate::error::Result<CompletionResponse> {
let id = raw["id"].as_str().unwrap_or("").to_string();
let model = raw["model"].as_str().unwrap_or("").to_string();
let content = raw["choices"]
.get(0)
.and_then(|c| c["message"]["content"].as_str())
.unwrap_or("")
.to_string();
let finish_reason = raw["choices"]
.get(0)
.and_then(|c| c["finish_reason"].as_str())
.map(String::from);
let usage = raw.get("usage").map(|u| Usage {
prompt_tokens: u["prompt_tokens"].as_u64().unwrap_or(0) as u32,
completion_tokens: u["completion_tokens"].as_u64().unwrap_or(0) as u32,
total_tokens: u["total_tokens"].as_u64().unwrap_or(0) as u32,
cost: u["cost"].as_f64(),
cached_tokens: u["prompt_tokens_details"]["cached_tokens"]
.as_u64()
.map(|v| v as u32),
reasoning_tokens: u["completion_tokens_details"]["reasoning_tokens"]
.as_u64()
.map(|v| v as u32),
});
let tool_calls = raw["choices"]
.get(0)
.and_then(|c| c["message"]["tool_calls"].as_array())
.cloned();
Ok(CompletionResponse {
id,
model,
content,
finish_reason,
usage,
tool_calls,
raw_response: Some(raw),
})
}
pub fn parse_stream_chunk(data: &str) -> Option<StreamChunk> {
if data.trim() == "[DONE]" {
return Some(StreamChunk::finished("stop"));
}
let json: serde_json::Value = serde_json::from_str(data).ok()?;
let usage = json.get("usage").and_then(|u| {
if u.is_null() {
None
} else {
Some(Usage {
prompt_tokens: u["prompt_tokens"].as_u64().unwrap_or(0) as u32,
completion_tokens: u["completion_tokens"].as_u64().unwrap_or(0) as u32,
total_tokens: u["total_tokens"].as_u64().unwrap_or(0) as u32,
cost: u["cost"].as_f64(),
cached_tokens: u["prompt_tokens_details"]["cached_tokens"]
.as_u64()
.map(|v| v as u32),
reasoning_tokens: u["completion_tokens_details"]["reasoning_tokens"]
.as_u64()
.map(|v| v as u32),
})
}
});
let choice = json["choices"].get(0);
let delta = choice
.and_then(|c| c["delta"]["content"].as_str())
.unwrap_or("")
.to_string();
let finish_reason = choice
.and_then(|c| c["finish_reason"].as_str())
.filter(|s| !s.is_empty())
.map(String::from);
let tool_calls = choice.and_then(|c| c["delta"]["tool_calls"].as_array().cloned());
if delta.is_empty() && finish_reason.is_none() && usage.is_none() && tool_calls.is_none() {
return None;
}
Some(StreamChunk {
delta,
finish_reason,
usage,
tool_calls,
})
}