use serde_json::Value;
use crate::value::VmValue;
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct ProviderUsageReceipt {
pub(super) input_tokens: Option<i64>,
pub(super) output_tokens: Option<i64>,
pub(super) reported_total_tokens: Option<i64>,
pub(super) cache_read_tokens: i64,
pub(super) cache_write_tokens: i64,
pub(super) cache_accounting_declared: Option<bool>,
pub(super) cache_supported: bool,
pub(super) provider_cost_usd: Option<f64>,
pub(super) served_fast: bool,
}
impl ProviderUsageReceipt {
pub(crate) fn new(
input_tokens: Option<i64>,
output_tokens: Option<i64>,
provider_cost_usd: Option<f64>,
served_fast: bool,
) -> Self {
Self {
input_tokens: input_tokens.filter(|tokens| *tokens >= 0),
output_tokens: output_tokens.filter(|tokens| *tokens >= 0),
reported_total_tokens: None,
cache_read_tokens: 0,
cache_write_tokens: 0,
cache_accounting_declared: None,
cache_supported: true,
provider_cost_usd: provider_cost_usd.filter(|cost| cost.is_finite() && *cost >= 0.0),
served_fast,
}
}
pub(crate) fn from_openai_usage_tokens(usage: &Value) -> Option<Self> {
let input_tokens =
optional_non_negative_json_int(usage, &["prompt_tokens", "input_tokens"]).ok()?;
let output_tokens =
optional_non_negative_json_int(usage, &["completion_tokens", "output_tokens"]).ok()?;
let reported_total_tokens =
optional_non_negative_json_int(usage, &["total_tokens"]).ok()?;
Some(
Self::new(input_tokens, output_tokens, None, false)
.with_reported_total(reported_total_tokens),
)
}
pub(crate) fn with_reported_total(mut self, total_tokens: Option<i64>) -> Self {
self.reported_total_tokens = total_tokens.filter(|tokens| *tokens >= 0);
self
}
pub(crate) fn has_any_reported_token_count(&self) -> bool {
self.input_tokens.is_some()
|| self.output_tokens.is_some()
|| self.reported_total_tokens.is_some()
}
pub(crate) fn input_tokens(&self) -> Option<i64> {
self.input_tokens
}
pub(crate) fn reported_total_tokens(&self) -> Option<i64> {
self.reported_total_tokens
}
pub(crate) fn with_cache(
mut self,
cache_read_tokens: i64,
cache_write_tokens: i64,
cache_accounting_declared: Option<bool>,
cache_supported: bool,
) -> Self {
self.cache_read_tokens = cache_read_tokens.max(0);
self.cache_write_tokens = cache_write_tokens.max(0);
self.cache_accounting_declared = cache_accounting_declared;
self.cache_supported = cache_supported;
self
}
pub(crate) fn to_vm_value(&self) -> VmValue {
VmValue::dict(crate::value::DictMap::from_iter([
(
crate::value::intern_key("input_tokens"),
self.input_tokens.map_or(VmValue::Nil, VmValue::Int),
),
(
crate::value::intern_key("output_tokens"),
self.output_tokens.map_or(VmValue::Nil, VmValue::Int),
),
(
crate::value::intern_key("reported_total_tokens"),
self.reported_total_tokens
.map_or(VmValue::Nil, VmValue::Int),
),
(
crate::value::intern_key("cache_read_tokens"),
VmValue::Int(self.cache_read_tokens),
),
(
crate::value::intern_key("cache_write_tokens"),
VmValue::Int(self.cache_write_tokens),
),
(
crate::value::intern_key("cache_accounting_declared"),
self.cache_accounting_declared
.map_or(VmValue::Nil, VmValue::Bool),
),
(
crate::value::intern_key("cache_supported"),
VmValue::Bool(self.cache_supported),
),
(
crate::value::intern_key("provider_cost_usd"),
self.provider_cost_usd.map_or(VmValue::Nil, VmValue::Float),
),
(
crate::value::intern_key("served_fast"),
VmValue::Bool(self.served_fast),
),
]))
}
pub(crate) fn from_error(error: &crate::value::VmError) -> Option<Self> {
let crate::value::VmError::Thrown(VmValue::Dict(error_fields)) = error else {
return None;
};
let VmValue::Dict(fields) = error_fields.get("provider_usage")? else {
return None;
};
let input_tokens = optional_non_negative_int(fields, "input_tokens").ok()?;
let output_tokens = optional_non_negative_int(fields, "output_tokens").ok()?;
let reported_total_tokens =
optional_non_negative_int_if_present(fields, "reported_total_tokens").ok()?;
let cache_read_tokens = non_negative_int(fields, "cache_read_tokens")?;
let cache_write_tokens = non_negative_int(fields, "cache_write_tokens")?;
let cache_accounting_declared = optional_bool(fields, "cache_accounting_declared").ok()?;
let VmValue::Bool(cache_supported) = fields.get("cache_supported")? else {
return None;
};
let provider_cost_usd = optional_non_negative_float(fields, "provider_cost_usd").ok()?;
let VmValue::Bool(served_fast) = fields.get("served_fast")? else {
return None;
};
Some(Self {
input_tokens,
output_tokens,
reported_total_tokens,
cache_read_tokens,
cache_write_tokens,
cache_accounting_declared,
cache_supported: *cache_supported,
provider_cost_usd,
served_fast: *served_fast,
})
}
pub(super) fn has_complete_token_counts(&self) -> bool {
self.input_tokens.is_some() && self.output_tokens.is_some()
}
pub(crate) fn output_tokens(&self) -> Option<i64> {
self.output_tokens
}
}
fn optional_non_negative_json_int(
value: &Value,
keys: &[&str],
) -> Result<Option<i64>, InvalidOptionalReceiptField> {
let object = value.as_object().ok_or(InvalidOptionalReceiptField)?;
let mut observed = None;
for key in keys {
let Some(raw) = object.get(*key) else {
continue;
};
let tokens = raw
.as_i64()
.filter(|tokens| *tokens >= 0)
.ok_or(InvalidOptionalReceiptField)?;
if observed.is_some_and(|existing| existing != tokens) {
return Err(InvalidOptionalReceiptField);
}
observed = Some(tokens);
}
Ok(observed)
}
fn non_negative_int(fields: &crate::value::DictMap, key: &str) -> Option<i64> {
match fields.get(key)? {
VmValue::Int(value) if *value >= 0 => Some(*value),
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
struct InvalidOptionalReceiptField;
fn optional_bool(
fields: &crate::value::DictMap,
key: &str,
) -> Result<Option<bool>, InvalidOptionalReceiptField> {
match fields.get(key) {
None => Err(InvalidOptionalReceiptField),
Some(value) => match value {
VmValue::Nil => Ok(None),
VmValue::Bool(value) => Ok(Some(*value)),
_ => Err(InvalidOptionalReceiptField),
},
}
}
fn optional_non_negative_int(
fields: &crate::value::DictMap,
key: &str,
) -> Result<Option<i64>, InvalidOptionalReceiptField> {
match fields.get(key) {
None => Err(InvalidOptionalReceiptField),
Some(value) => match value {
VmValue::Nil => Ok(None),
VmValue::Int(value) if *value >= 0 => Ok(Some(*value)),
_ => Err(InvalidOptionalReceiptField),
},
}
}
fn optional_non_negative_int_if_present(
fields: &crate::value::DictMap,
key: &str,
) -> Result<Option<i64>, InvalidOptionalReceiptField> {
match fields.get(key) {
None | Some(VmValue::Nil) => Ok(None),
Some(VmValue::Int(value)) if *value >= 0 => Ok(Some(*value)),
Some(_) => Err(InvalidOptionalReceiptField),
}
}
fn optional_non_negative_float(
fields: &crate::value::DictMap,
key: &str,
) -> Result<Option<f64>, InvalidOptionalReceiptField> {
match fields.get(key) {
None => Err(InvalidOptionalReceiptField),
Some(value) => match value {
VmValue::Nil => Ok(None),
VmValue::Float(value) if value.is_finite() && *value >= 0.0 => Ok(Some(*value)),
_ => Err(InvalidOptionalReceiptField),
},
}
}