use crate::value::VmDictExt;
use crate::value::VmValue;
use super::AgentHostSession;
pub(super) struct CallAccounting {
pub(super) cost_usd: Option<f64>,
pub(super) usage_unknown: bool,
pub(super) projected_cost_usd: Option<f64>,
}
fn dict_field<'a>(value: &'a VmValue, key: &str) -> Option<&'a VmValue> {
match value {
VmValue::Dict(map) => map.get(key),
_ => None,
}
}
pub(super) fn resolve_call_accounting(
usage: &VmValue,
provider: &str,
model: &str,
input_tokens: i64,
output_tokens: i64,
cache_read_tokens: i64,
cache_write_tokens: i64,
) -> CallAccounting {
let recorded = super::dict_get(usage, "pricing");
let settled_at = recorded
.and_then(|pricing| dict_field(pricing, "settled_at_ms"))
.and_then(|value| match value {
VmValue::Int(value) => Some(*value),
_ => None,
})
.map(crate::llm::cost::instant_from_wall_ms)
.unwrap_or_else(crate::llm::cost::settlement_now);
let cache_ttl = None;
let explicit_cost = super::dict_get(usage, "cost_usd").and_then(|value| match value {
VmValue::Float(value) => Some(*value),
VmValue::Int(value) => Some(*value as f64),
_ => None,
});
let accounting_status = super::dict_get(usage, "accounting_status");
let cost_usd = explicit_cost.or_else(|| {
accounting_status.is_none().then(|| {
crate::llm::cost::pricing_aware_call_cost_with_cache(
provider,
model,
input_tokens,
output_tokens,
cache_read_tokens,
cache_write_tokens,
settled_at,
cache_ttl,
)
})?
});
let usage_unknown = matches!(
accounting_status,
Some(VmValue::String(status)) if status.as_str() == "unknown"
);
let projected_cost_usd = match super::dict_get(usage, "projected_cost_usd") {
Some(VmValue::Float(value)) => Some(*value),
Some(VmValue::Int(value)) => Some(*value as f64),
Some(VmValue::Nil) => None,
_ => cost_usd,
};
CallAccounting {
cost_usd,
usage_unknown,
projected_cost_usd,
}
}
#[derive(Clone, Copy)]
pub(super) struct SessionUsageTotals {
pub(super) tokens_used: i64,
pub(super) known_cost_usd: f64,
pub(super) input_tokens: i64,
pub(super) output_tokens: i64,
pub(super) cache_read_tokens: i64,
pub(super) cache_write_tokens: i64,
pub(super) provider_call_count: i64,
pub(super) unpriced_calls: i64,
pub(super) usage_unknown_calls: i64,
pub(super) priced_calls: i64,
pub(super) projected_cost_used: f64,
pub(super) unprojectable_calls: i64,
}
impl From<&AgentHostSession> for SessionUsageTotals {
fn from(session: &AgentHostSession) -> Self {
Self {
tokens_used: session.tokens_used,
known_cost_usd: session.cost_used,
input_tokens: session.input_tokens,
output_tokens: session.output_tokens,
cache_read_tokens: session.cache_read_tokens,
cache_write_tokens: session.cache_write_tokens,
provider_call_count: session.provider_call_count,
unpriced_calls: session.unpriced_calls,
usage_unknown_calls: session.usage_unknown_calls,
priced_calls: session.priced_calls,
projected_cost_used: session.projected_cost_used,
unprojectable_calls: session.unprojectable_calls,
}
}
}
impl SessionUsageTotals {
pub(super) fn projected_cost_usd(self) -> Option<f64> {
(self.unprojectable_calls == 0).then_some(self.projected_cost_used)
}
pub(super) fn to_vm(self, include_token_split: bool) -> VmValue {
let mut out = crate::value::DictMap::new();
out.put_int("tokens_used", self.tokens_used);
out.insert(
crate::value::intern_key("cost_usd"),
if self.priced_calls > 0 || self.unpriced_calls == 0 {
VmValue::Float(self.known_cost_usd)
} else {
VmValue::Nil
},
);
out.insert(
crate::value::intern_key("projected_cost_usd"),
self.projected_cost_usd()
.map_or(VmValue::Nil, VmValue::Float),
);
out.put_int("unprojectable_calls", self.unprojectable_calls);
out.put("known_cost_usd", VmValue::Float(self.known_cost_usd));
if include_token_split {
out.put_int("input_tokens", self.input_tokens);
out.put_int("output_tokens", self.output_tokens);
}
out.put_int("cache_read_tokens", self.cache_read_tokens);
out.put_int("cache_write_tokens", self.cache_write_tokens);
out.put_int("unpriced_calls", self.unpriced_calls);
out.put_int("usage_unknown_calls", self.usage_unknown_calls);
out.put_int("provider_call_count", self.provider_call_count);
VmValue::dict(out)
}
}