use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::value::{VmDictExt, VmValue};
use super::api::{LlmResult, ProviderAttempts};
mod billing;
pub use billing::BillingUsage;
mod cost;
pub(crate) use cost::register_settlement_builtin;
pub use cost::{
settle_call_cost, CallCostBreakdown, CallCostInput, CallCostInputError, CallCostRates,
CallCostSettlement, CallCostUnpriced,
};
mod receipt;
#[doc(inline)]
pub use harn_llm::usage::ReportedCacheUsage;
pub(crate) use harn_llm::usage::{cache_field_roots, extract_cache_counts, CacheCounts};
pub(crate) use harn_llm::usage::{InputTokenBasis, PromptTokenCounts, ReportedTokenUsage};
pub(crate) use receipt::ProviderUsageReceipt;
struct ProviderReceiptSettlement {
usage: LlmUsage,
pricing: Option<super::cost::PricingDetail>,
billing: billing::BillingSettlement,
free_inference: bool,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum UsageAccountingStatus {
Reported,
Partial,
#[default]
Unknown,
}
impl UsageAccountingStatus {
const fn as_str(self) -> &'static str {
match self {
Self::Reported => "reported",
Self::Partial => "partial",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UnpricedFacts {
pub tokens: i64,
pub reason: UnpricedReason,
pub projection_usd: Option<f64>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum UnpricedReason {
PricingUnknown,
UsageUnreported,
NoResponse,
StreamAborted,
Mixed,
}
impl UnpricedReason {
const fn as_str(self) -> &'static str {
match self {
Self::PricingUnknown => "pricing_unknown",
Self::UsageUnreported => "usage_unreported",
Self::NoResponse => "no_response",
Self::StreamAborted => "stream_aborted",
Self::Mixed => "mixed",
}
}
fn merge(self, other: Self) -> Self {
if self == other {
self
} else {
Self::Mixed
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LlmUsage {
pub input_tokens: i64,
pub output_tokens: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reported_total_tokens: Option<i64>,
pub cost_usd: Option<f64>,
pub cache_read_tokens: i64,
pub cache_write_tokens: i64,
pub cache_supported: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_accounting_declared: Option<bool>,
#[serde(default)]
pub cache_counts_present: bool,
pub cache_hit_ratio: Option<f64>,
pub cache_savings_usd: f64,
pub cache_hit: bool,
pub served_fast: bool,
#[serde(default)]
pub accounting_status: UsageAccountingStatus,
#[serde(default)]
pub known_cost_usd: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_call_count: Option<i64>,
#[serde(default)]
pub unpriced_calls: i64,
#[serde(default)]
pub usage_unknown_calls: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unpriced: Option<Box<UnpricedFacts>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pricing: Option<Box<PricingFacts>>,
#[serde(default, flatten, deserialize_with = "billing::deserialize_optional")]
pub billing: Option<Box<BillingUsage>>,
}
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PricingFacts {
pub rate_card: String,
pub settled_at_ms: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_band_minimum: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub serving_tier: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub cache_ttl_unpriced: bool,
#[serde(default)]
pub platform_fee_estimate_usd: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform_fee_basis: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hosted_tool_unpriced: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modality_unpriced: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub monthly_allowance_unapplied: Vec<String>,
}
impl LlmUsage {
#[must_use]
pub fn unpriced_tokens(&self) -> i64 {
self.unpriced.as_ref().map_or(0, |facts| facts.tokens)
}
#[must_use]
pub fn unpriced_reason(&self) -> Option<UnpricedReason> {
self.unpriced.as_ref().map(|facts| facts.reason)
}
#[must_use]
pub fn projected_cost_usd(&self) -> Option<f64> {
match &self.unpriced {
None => Some(self.known_cost_usd),
Some(facts) => facts
.projection_usd
.map(|projection| self.known_cost_usd + projection),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct UsageCostCertainty {
pub known_cost_usd: f64,
pub provider_call_count: i64,
pub unpriced_calls: i64,
pub usage_unknown_calls: i64,
pub unpriced_tokens: i64,
pub unpriced_reason: Option<UnpricedReason>,
pub unpriced_projection_usd: f64,
pub unprojectable: bool,
}
impl UsageCostCertainty {
#[must_use]
pub fn projected_cost_usd(&self) -> Option<f64> {
(!self.unprojectable).then_some(self.known_cost_usd + self.unpriced_projection_usd)
}
#[must_use]
pub const fn has_priced_attempt(&self) -> bool {
self.unpriced_calls < self.provider_call_count || self.known_cost_usd > 0.0
}
#[must_use]
pub fn cost_usd(&self) -> Option<f64> {
(self.unpriced_calls == 0).then_some(self.known_cost_usd)
}
pub(crate) fn record(&mut self, usage: &LlmUsage) {
let summary = self;
let legacy = usage.provider_call_count.is_none();
summary.known_cost_usd += if legacy {
usage.cost_usd.unwrap_or(0.0)
} else {
usage.known_cost_usd
};
summary.provider_call_count += usage.provider_call_count.unwrap_or(1);
summary.unpriced_calls += if legacy {
i64::from(usage.cost_usd.is_none())
} else {
usage.unpriced_calls
};
summary.usage_unknown_calls += if legacy {
i64::from(usage.accounting_status == UsageAccountingStatus::Unknown)
} else {
usage.usage_unknown_calls
};
summary.unpriced_tokens += if legacy {
if usage.cost_usd.is_none() {
usage.input_tokens.saturating_add(usage.output_tokens)
} else {
0
}
} else {
usage.unpriced_tokens()
};
let reason = if legacy {
usage.cost_usd.is_none().then_some(UnpricedReason::Mixed)
} else {
usage.unpriced_reason()
};
if let Some(reason) = reason {
summary.unpriced_reason = Some(
summary
.unpriced_reason
.map_or(reason, |existing| existing.merge(reason)),
);
}
let member_projection = if legacy {
usage.cost_usd
} else {
usage.projected_cost_usd()
};
let member_known = if legacy {
usage.cost_usd.unwrap_or(0.0)
} else {
usage.known_cost_usd
};
match member_projection {
Some(projected) => {
summary.unpriced_projection_usd += (projected - member_known).max(0.0);
}
None => summary.unprojectable = true,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ReportedCacheTotals {
pub reported_calls: i64,
pub read_tokens: i64,
pub write_tokens: i64,
}
impl ReportedCacheTotals {
pub(crate) fn record(&mut self, reported: bool, read_tokens: i64, write_tokens: i64) {
if !reported {
return;
}
self.reported_calls = self.reported_calls.saturating_add(1);
self.read_tokens = self.read_tokens.saturating_add(read_tokens);
self.write_tokens = self.write_tokens.saturating_add(write_tokens);
}
pub(crate) fn record_usage(&mut self, usage: &LlmUsage) {
self.record(
usage.cache_counts_reported(),
usage.cache_read_tokens,
usage.cache_write_tokens,
);
}
#[must_use]
pub(crate) fn since(&self, before: &Self) -> Self {
Self {
reported_calls: self.reported_calls.saturating_sub(before.reported_calls),
read_tokens: self.read_tokens.saturating_sub(before.read_tokens),
write_tokens: self.write_tokens.saturating_sub(before.write_tokens),
}
}
#[must_use]
pub fn reported_read_tokens(&self) -> Option<i64> {
(self.reported_calls > 0).then_some(self.read_tokens)
}
#[must_use]
pub fn reported_write_tokens(&self) -> Option<i64> {
(self.reported_calls > 0).then_some(self.write_tokens)
}
}
pub fn summarize_usage_cost_certainty<'a>(
usages: impl IntoIterator<Item = &'a LlmUsage>,
) -> UsageCostCertainty {
let mut summary = UsageCostCertainty::default();
for usage in usages {
summary.record(usage);
}
summary
}
fn unpriced_projection(
cost_usd: Option<f64>,
table_cost: Option<f64>,
) -> (Option<UnpricedReason>, Option<f64>) {
match (cost_usd, table_cost) {
(Some(cost), _) => (None, Some(cost)),
(None, Some(bound)) => (Some(UnpricedReason::UsageUnreported), Some(bound)),
(None, None) => (Some(UnpricedReason::PricingUnknown), None),
}
}
fn unpriced_facts(
tokens: i64,
reason: Option<UnpricedReason>,
projected: Option<f64>,
) -> Option<Box<UnpricedFacts>> {
reason.map(|reason| {
Box::new(UnpricedFacts {
tokens,
reason,
projection_usd: projected,
})
})
}
fn aggregate_pricing_facts(usages: &[LlmUsage]) -> Option<Box<PricingFacts>> {
match usages {
[usage] => usage.pricing.clone(),
_ => None,
}
}
fn pricing_facts(
detail: &super::cost::PricingDetail,
settled_at_ms: i64,
cache_ttl: Option<super::api::PromptCacheTtl>,
settlement: &billing::BillingSettlement,
free_inference: bool,
) -> PricingFacts {
PricingFacts {
rate_card: detail.rate_card.label(),
settled_at_ms,
input_band_minimum: detail.input_band_minimum,
serving_tier: matches!(
detail.source,
super::cost::PricingSource::CatalogServingTier
)
.then(|| super::serving_tiers::FAST_TIER_ID.to_string()),
cache_ttl_unpriced: !free_inference && !detail.cache_write_priced(cache_ttl),
platform_fee_estimate_usd: settlement.platform_fee_estimate_usd,
hosted_tool_unpriced: settlement.hosted_tool_unpriced.clone(),
modality_unpriced: settlement.modality_unpriced.clone(),
monthly_allowance_unapplied: settlement.monthly_allowance_unapplied.clone(),
platform_fee_basis: (detail.platform_fee_percent > 0.0)
.then(|| "catalog_estimate_funding_route_unknown".into()),
}
}
const fn unpriced_token_count(cost_usd: Option<f64>, input: i64, output: i64) -> i64 {
if cost_usd.is_some() {
0
} else {
input.saturating_add(output)
}
}
impl LlmUsage {
pub(crate) fn aggregate(usages: &[Self]) -> Self {
let input_tokens = usages.iter().map(|usage| usage.input_tokens).sum();
let output_tokens = usages.iter().map(|usage| usage.output_tokens).sum();
let reported_total_tokens = (!usages.is_empty())
.then(|| {
usages.iter().try_fold(0_i64, |total, usage| {
usage
.reported_total_tokens
.map(|tokens| total.saturating_add(tokens))
})
})
.flatten();
let cache_read_tokens = usages.iter().map(|usage| usage.cache_read_tokens).sum();
let cache_write_tokens = usages.iter().map(|usage| usage.cache_write_tokens).sum();
let certainty = summarize_usage_cost_certainty(usages);
let cache_supported = usages.iter().all(|usage| usage.cache_supported);
let cache_accounting_declared = usages
.iter()
.map(|usage| usage.cache_accounting_declared)
.try_fold(true, |all_true, declared| {
declared.map(|declared| all_true && declared)
});
let cache_counts_present = usages.iter().any(|usage| usage.cache_counts_present);
Self {
input_tokens,
output_tokens,
reported_total_tokens,
cost_usd: (certainty.unpriced_calls == 0 || certainty.has_priced_attempt())
.then_some(certainty.known_cost_usd),
cache_read_tokens,
cache_write_tokens,
cache_supported,
cache_accounting_declared,
cache_counts_present,
cache_hit_ratio: (cache_counts_present
&& cache_accounting_declared == Some(true)
&& cache_supported)
.then(|| super::cost::cache_hit_ratio(input_tokens, cache_read_tokens)),
cache_savings_usd: usages.iter().map(|usage| usage.cache_savings_usd).sum(),
cache_hit: usages.iter().any(|usage| usage.cache_hit),
served_fast: usages.iter().any(|usage| usage.served_fast),
accounting_status: if certainty.usage_unknown_calls == 0
&& certainty.unpriced_calls == 0
{
UsageAccountingStatus::Reported
} else if certainty.has_priced_attempt() {
UsageAccountingStatus::Partial
} else {
UsageAccountingStatus::Unknown
},
known_cost_usd: certainty.known_cost_usd,
provider_call_count: Some(certainty.provider_call_count),
unpriced_calls: certainty.unpriced_calls,
usage_unknown_calls: certainty.usage_unknown_calls,
unpriced: unpriced_facts(
certainty.unpriced_tokens,
certainty.unpriced_reason,
(!certainty.unprojectable).then_some(certainty.unpriced_projection_usd),
),
pricing: aggregate_pricing_facts(usages),
billing: usages
.iter()
.filter_map(|usage| usage.billing.as_deref())
.fold(None, |sum, value| {
let mut sum = sum.unwrap_or_else(|| Box::new(BillingUsage::default()));
sum.add(value);
Some(sum)
}),
}
}
pub(crate) fn aggregate_with_unknown_attempts(
completed: &[Self],
unknown_attempts: usize,
) -> Self {
assert!(
unknown_attempts > 0,
"terminal usage requires an unknown attempt"
);
let mut usages = Vec::with_capacity(completed.len().saturating_add(1));
usages.extend_from_slice(completed);
usages.push(Self::unknown_attempts(unknown_attempts));
Self::aggregate(&usages)
}
pub(crate) fn known_zero_attempt() -> Self {
Self {
cost_usd: Some(0.0),
accounting_status: UsageAccountingStatus::Reported,
known_cost_usd: 0.0,
provider_call_count: Some(1),
unpriced_calls: 0,
usage_unknown_calls: 0,
unpriced: None,
..Self::unknown_attempt()
}
}
pub(crate) fn no_provider_request() -> Self {
Self {
cost_usd: Some(0.0),
accounting_status: UsageAccountingStatus::Reported,
known_cost_usd: 0.0,
provider_call_count: Some(0),
unpriced_calls: 0,
usage_unknown_calls: 0,
unpriced: None,
..Self::unknown_attempt()
}
}
pub(crate) fn unknown_attempt() -> Self {
Self::unknown_attempts(1)
}
pub(crate) fn unknown_attempts(count: usize) -> Self {
let count = i64::try_from(count.max(1)).unwrap_or(i64::MAX);
Self {
input_tokens: 0,
output_tokens: 0,
reported_total_tokens: None,
cost_usd: None,
cache_read_tokens: 0,
cache_write_tokens: 0,
cache_supported: true,
cache_accounting_declared: Some(true),
cache_counts_present: false,
cache_hit_ratio: Some(0.0),
cache_savings_usd: 0.0,
cache_hit: false,
served_fast: false,
accounting_status: UsageAccountingStatus::Unknown,
known_cost_usd: 0.0,
provider_call_count: Some(count),
unpriced_calls: count,
usage_unknown_calls: count,
unpriced: Some(Box::new(UnpricedFacts {
tokens: 0,
reason: UnpricedReason::NoResponse,
projection_usd: None,
})),
pricing: None,
billing: None,
}
}
pub(crate) fn stream_aborted_attempt() -> Self {
Self {
unpriced: Some(Box::new(UnpricedFacts {
tokens: 0,
reason: UnpricedReason::StreamAborted,
projection_usd: None,
})),
..Self::unknown_attempt()
}
}
pub(crate) fn from_result(result: &LlmResult) -> Self {
let component_usage_known = result.input_tokens > 0
|| result.output_tokens > 0
|| result.telemetry.server_prompt_tokens.is_some()
|| result.telemetry.server_output_tokens.is_some();
let usage_known = component_usage_known || result.telemetry.server_total_tokens.is_some();
let authoritative_cost = result
.telemetry
.mock_replay_cost_usd()
.or(result.telemetry.provider_cost_usd)
.or_else(|| super::managed_supply::authoritative_cost_usd(result));
let free_route = crate::llm_config::provider_is_self_hosted(&result.provider);
let settled_at_ms = result
.telemetry
.started_at_ms
.unwrap_or_else(crate::stdlib::clock::now_wall_ms_unrecorded);
let at = super::cost::instant_from_wall_ms(settled_at_ms);
let cache_ttl = result
.telemetry
.prompt_cache_ttl
.as_deref()
.and_then(super::api::PromptCacheTtl::parse);
let detail = super::cost::pricing_detail_for_tier(
&result.provider,
&result.model,
result.served_fast,
result.input_tokens,
at,
);
let token_cost = detail.as_ref().map(|detail| {
super::cost::project_call_cost(
detail,
result.input_tokens,
result.output_tokens,
result.cache_read_tokens,
result.cache_write_tokens,
cache_ttl,
)
});
let billing = result.telemetry.billing.clone().map(|mut billing| {
billing.validate_normalized_counts(
result.input_tokens,
result.output_tokens,
result.cache_read_tokens,
result.cache_write_tokens,
);
billing
});
let settlement = billing
.as_deref()
.unwrap_or(&BillingUsage::default())
.settle(detail.as_ref(), token_cost, free_route);
let table_cost = (free_route || token_cost.is_some()).then_some(settlement.known_cost_usd);
let units_unpriced = authoritative_cost.is_none()
&& (settlement.has_unpriced_units()
|| (!free_route
&& ((!component_usage_known && billing.is_some())
|| (result.cache_write_tokens > 0
&& detail
.as_ref()
.is_some_and(|detail| !detail.cache_write_priced(cache_ttl))))));
let cost_usd = authoritative_cost
.or_else(|| free_route.then_some(settlement.known_cost_usd))
.or_else(|| {
(component_usage_known || billing.is_some())
.then_some(table_cost)
.flatten()
});
let (unpriced_reason, projected_cost_usd) = if units_unpriced {
(Some(UnpricedReason::Mixed), None)
} else {
unpriced_projection(cost_usd, table_cost)
};
let cache_hit_ratio = (result.cache_counts_reported
&& result.telemetry.cache_accounting_declared == Some(true)
&& result.cache_supported)
.then(|| super::cost::cache_hit_ratio(result.input_tokens, result.cache_read_tokens));
Self {
input_tokens: result.input_tokens,
output_tokens: result.output_tokens,
reported_total_tokens: result.telemetry.server_total_tokens,
cost_usd,
cache_read_tokens: result.cache_read_tokens,
cache_write_tokens: result.cache_write_tokens,
cache_supported: result.cache_supported,
cache_accounting_declared: result.telemetry.cache_accounting_declared,
cache_counts_present: result.cache_counts_reported,
cache_hit_ratio,
cache_savings_usd: super::cost::cache_savings_usd_for_pricing(
detail.as_ref(),
free_route,
result.cache_read_tokens,
result.cache_write_tokens,
cache_ttl,
),
cache_hit: result.cache_read_tokens > 0,
served_fast: result.served_fast,
accounting_status: if units_unpriced {
UsageAccountingStatus::Partial
} else if usage_known || authoritative_cost.is_some() {
UsageAccountingStatus::Reported
} else {
UsageAccountingStatus::Unknown
},
known_cost_usd: cost_usd.unwrap_or(0.0),
provider_call_count: Some(1),
unpriced_calls: i64::from(cost_usd.is_none() || units_unpriced),
usage_unknown_calls: i64::from(!usage_known && authoritative_cost.is_none()),
unpriced: unpriced_facts(
unpriced_token_count(cost_usd, result.input_tokens, result.output_tokens),
unpriced_reason,
projected_cost_usd,
),
pricing: (cost_usd.is_some()
&& authoritative_cost.is_none()
&& (!free_route || settlement.has_priced_hosted_tools))
.then(|| {
detail.as_ref().map(|detail| {
Box::new(pricing_facts(
detail,
settled_at_ms,
cache_ttl,
&settlement,
free_route,
))
})
})
.flatten(),
billing,
}
}
pub(crate) fn from_provider_receipt(
provider: &str,
model: &str,
receipt: &ProviderUsageReceipt,
) -> Self {
Self::settle_provider_receipt(provider, model, receipt).usage
}
fn settle_provider_receipt(
provider: &str,
model: &str,
receipt: &ProviderUsageReceipt,
) -> ProviderReceiptSettlement {
let input_tokens = receipt.input_tokens.unwrap_or(0);
let output_tokens = receipt.output_tokens.unwrap_or(0);
let complete_counts = receipt.has_complete_token_counts();
let free_route = crate::llm_config::provider_is_self_hosted(provider);
let settled_at_ms = receipt
.started_at_ms
.unwrap_or_else(crate::stdlib::clock::now_wall_ms_unrecorded);
let cache_ttl = receipt.prompt_cache_ttl;
let at = super::cost::instant_from_wall_ms(settled_at_ms);
let detail = super::cost::pricing_detail_for_tier(
provider,
model,
receipt.served_fast,
input_tokens,
at,
);
let token_cost = detail.as_ref().map(|detail| {
super::cost::project_call_cost(
detail,
input_tokens,
output_tokens,
receipt.cache_read_tokens,
receipt.cache_write_tokens,
cache_ttl,
)
});
let billing = receipt.billing.clone().map(|mut billing| {
billing.validate_normalized_counts(
input_tokens,
output_tokens,
receipt.cache_read_tokens,
receipt.cache_write_tokens,
);
billing
});
let settlement = billing
.as_deref()
.unwrap_or(&BillingUsage::default())
.settle(detail.as_ref(), token_cost, free_route);
let table_cost = (free_route || token_cost.is_some()).then_some(settlement.known_cost_usd);
let units_unpriced = receipt.provider_cost_usd.is_none()
&& (settlement.has_unpriced_units()
|| (!free_route
&& ((!complete_counts && billing.is_some())
|| (receipt.cache_write_tokens > 0
&& detail
.as_ref()
.is_some_and(|detail| !detail.cache_write_priced(cache_ttl))))));
let cost_usd = receipt
.provider_cost_usd
.or_else(|| free_route.then_some(settlement.known_cost_usd))
.or_else(|| {
(complete_counts || billing.is_some())
.then_some(table_cost)
.flatten()
});
let (unpriced_reason, projected_cost_usd) = if units_unpriced {
(Some(UnpricedReason::Mixed), None)
} else {
unpriced_projection(cost_usd, table_cost)
};
let usage_unknown = i64::from(!complete_counts);
let usage = Self {
input_tokens,
output_tokens,
reported_total_tokens: receipt.reported_total_tokens,
cost_usd,
cache_read_tokens: receipt.cache_read_tokens,
cache_write_tokens: receipt.cache_write_tokens,
cache_supported: receipt.cache_supported,
cache_accounting_declared: receipt.cache_accounting_declared,
cache_counts_present: receipt.cache_counts_reported,
cache_hit_ratio: (receipt.cache_counts_reported
&& receipt.cache_accounting_declared == Some(true)
&& receipt.cache_supported)
.then(|| super::cost::cache_hit_ratio(input_tokens, receipt.cache_read_tokens)),
cache_savings_usd: super::cost::cache_savings_usd_for_pricing(
detail.as_ref(),
free_route,
receipt.cache_read_tokens,
receipt.cache_write_tokens,
cache_ttl,
),
cache_hit: receipt.cache_read_tokens > 0,
served_fast: receipt.served_fast,
accounting_status: if units_unpriced {
UsageAccountingStatus::Partial
} else if usage_unknown == 0 {
UsageAccountingStatus::Reported
} else {
UsageAccountingStatus::Unknown
},
known_cost_usd: cost_usd.unwrap_or(0.0),
provider_call_count: Some(1),
unpriced_calls: i64::from(cost_usd.is_none() || units_unpriced),
usage_unknown_calls: usage_unknown,
unpriced: unpriced_facts(
unpriced_token_count(cost_usd, input_tokens, output_tokens),
unpriced_reason,
projected_cost_usd,
),
pricing: (cost_usd.is_some()
&& receipt.provider_cost_usd.is_none()
&& (!free_route || settlement.has_priced_hosted_tools))
.then(|| {
detail.as_ref().map(|detail| {
Box::new(pricing_facts(
detail,
settled_at_ms,
cache_ttl,
&settlement,
free_route,
))
})
})
.flatten(),
billing,
};
ProviderReceiptSettlement {
usage,
pricing: detail,
billing: settlement,
free_inference: free_route,
}
}
pub(crate) fn aggregate_attempt_ledger(reported: &[Self], total_attempts: usize) -> Self {
assert!(
reported.len() <= total_attempts,
"reported attempt ledgers cannot exceed physical attempts"
);
let unknown_attempts = total_attempts.saturating_sub(reported.len());
if unknown_attempts == 0 {
return Self::aggregate(reported);
}
Self::aggregate_with_unknown_attempts(reported, unknown_attempts)
}
pub(crate) fn to_vm_dict(&self, attempts: &ProviderAttempts) -> crate::value::DictMap {
let mut usage = crate::value::DictMap::new();
if let Some(billing) = &self.billing {
if let Value::Object(fields) =
serde_json::to_value(billing).expect("billing serializes")
{
for (key, value) in fields {
usage.insert(key.into(), crate::schema::json_to_vm_value(&value));
}
}
}
usage.put_int("input_tokens", self.input_tokens);
usage.put_int("output_tokens", self.output_tokens);
usage.insert(
crate::value::intern_key("reported_total_tokens"),
self.reported_total_tokens
.map_or(VmValue::Nil, VmValue::Int),
);
usage.insert(
crate::value::intern_key("cost_usd"),
self.cost_usd.map_or(VmValue::Nil, VmValue::Float),
);
usage.put("known_cost_usd", VmValue::Float(self.known_cost_usd));
if let Some(provider_call_count) = self.provider_call_count {
usage.put_int("provider_call_count", provider_call_count);
}
usage.put_int("unpriced_calls", self.unpriced_calls);
usage.put_int("usage_unknown_calls", self.usage_unknown_calls);
usage.put_int("unpriced_tokens", self.unpriced_tokens());
usage.insert(
crate::value::intern_key("unpriced_reason"),
self.unpriced_reason()
.map_or(VmValue::Nil, |reason| VmValue::string(reason.as_str())),
);
usage.insert(
crate::value::intern_key("projected_cost_usd"),
self.projected_cost_usd()
.map_or(VmValue::Nil, VmValue::Float),
);
if let Some(pricing) = self.pricing.as_deref() {
let mut card = crate::value::DictMap::new();
card.put(
"platform_fee_estimate_usd",
VmValue::Float(pricing.platform_fee_estimate_usd),
);
card.insert(
"platform_fee_basis".into(),
pricing
.platform_fee_basis
.as_deref()
.map_or(VmValue::Nil, VmValue::string),
);
card.insert(
"hosted_tool_unpriced".into(),
crate::schema::json_to_vm_value(&serde_json::json!(pricing.hosted_tool_unpriced)),
);
card.insert(
"modality_unpriced".into(),
crate::schema::json_to_vm_value(&serde_json::json!(pricing.modality_unpriced)),
);
card.insert(
"monthly_allowance_unapplied".into(),
crate::schema::json_to_vm_value(&serde_json::json!(
pricing.monthly_allowance_unapplied
)),
);
card.put("rate_card", VmValue::string(pricing.rate_card.as_str()));
card.put_int("settled_at_ms", pricing.settled_at_ms);
card.insert(
crate::value::intern_key("input_band_minimum"),
pricing
.input_band_minimum
.map_or(VmValue::Nil, |band| VmValue::Int(band as i64)),
);
card.insert(
crate::value::intern_key("serving_tier"),
pricing
.serving_tier
.as_deref()
.map_or(VmValue::Nil, VmValue::string),
);
card.put_bool("cache_ttl_unpriced", pricing.cache_ttl_unpriced);
usage.put("pricing", VmValue::Dict(std::sync::Arc::new(card)));
}
usage.put_int("cache_read_tokens", self.cache_read_tokens);
usage.put_int("cache_write_tokens", self.cache_write_tokens);
usage.put_bool("cache_supported", self.cache_supported);
usage.insert(
crate::value::intern_key("cache_hit_ratio"),
self.cache_hit_ratio.map_or(VmValue::Nil, VmValue::Float),
);
match self.cache_visibility() {
None => {
usage.insert(crate::value::intern_key("cache_visibility"), VmValue::Nil);
}
Some(state) => usage.put_str("cache_visibility", state),
}
usage.put("cache_savings_usd", VmValue::Float(self.cache_savings_usd));
usage.put(
"provider_attempts",
VmValue::dict(provider_attempts_vm_dict(attempts)),
);
usage.put_bool("served_fast", self.served_fast);
usage.put_str("accounting_status", self.accounting_status.as_str());
usage
}
fn cache_visibility(&self) -> Option<&'static str> {
match (self.cache_accounting_declared, self.cache_supported) {
(None, _) => Some("undeclared"),
(Some(false), _) | (Some(true), false) => Some("unsupported"),
(Some(true), true) => None,
}
}
pub(crate) fn cache_counts_reported(&self) -> bool {
self.cache_counts_present && self.cache_visibility().is_none()
}
pub(crate) fn project_onto_event(&self, event: &mut serde_json::Value) {
let fields = event
.as_object_mut()
.expect("usage projection target must be a JSON object");
self.project_onto_fields(fields);
}
pub(crate) fn project_onto_fields(&self, fields: &mut serde_json::Map<String, Value>) {
if let Some(billing) = &self.billing {
if let Value::Object(billing) =
serde_json::to_value(billing).expect("billing serializes")
{
fields.extend(billing);
}
}
fields.insert("input_tokens".to_string(), self.input_tokens.into());
fields.insert("output_tokens".to_string(), self.output_tokens.into());
fields.insert(
"reported_total_tokens".to_string(),
self.reported_total_tokens
.map_or(Value::Null, serde_json::Value::from),
);
fields.insert(
"cost_usd".to_string(),
self.cost_usd.map_or(Value::Null, serde_json::Value::from),
);
fields.insert("known_cost_usd".to_string(), self.known_cost_usd.into());
if let Some(provider_call_count) = self.provider_call_count {
fields.insert(
"provider_call_count".to_string(),
provider_call_count.into(),
);
}
fields.insert("unpriced_calls".to_string(), self.unpriced_calls.into());
fields.insert(
"usage_unknown_calls".to_string(),
self.usage_unknown_calls.into(),
);
fields.insert("unpriced_tokens".to_string(), self.unpriced_tokens().into());
fields.insert(
"unpriced_reason".to_string(),
self.unpriced_reason()
.map_or(Value::Null, |reason| reason.as_str().into()),
);
fields.insert(
"projected_cost_usd".to_string(),
self.projected_cost_usd()
.map_or(Value::Null, serde_json::Value::from),
);
if let Some(pricing) = self.pricing.as_deref() {
fields.insert(
"pricing".to_string(),
serde_json::to_value(pricing).unwrap_or(Value::Null),
);
}
fields.insert(
"cache_read_tokens".to_string(),
self.cache_read_tokens.into(),
);
fields.insert(
"cache_write_tokens".to_string(),
self.cache_write_tokens.into(),
);
fields.insert("cache_supported".to_string(), self.cache_supported.into());
fields.insert(
"cache_hit_ratio".to_string(),
self.cache_hit_ratio
.map_or(Value::Null, serde_json::Value::from),
);
fields.insert(
"cache_visibility".to_string(),
self.cache_visibility()
.map_or(Value::Null, |state| Value::String(state.to_string())),
);
fields.insert(
"cache_savings_usd".to_string(),
self.cache_savings_usd.into(),
);
fields.insert("cache_hit".to_string(), self.cache_hit.into());
fields.insert("served_fast".to_string(), self.served_fast.into());
fields.insert(
"accounting_status".to_string(),
self.accounting_status.as_str().into(),
);
}
pub(crate) fn measured_vm_dict(dispatches: Option<i64>) -> crate::value::DictMap {
let usage = match dispatches {
Some(0) => Self::no_provider_request(),
Some(count) if count > 0 => {
Self::unknown_attempts(usize::try_from(count).unwrap_or(usize::MAX))
}
_ => Self::unknown_attempt(),
};
let attempts = ProviderAttempts {
total: u32::try_from(dispatches.unwrap_or(0).max(0)).unwrap_or(u32::MAX),
..ProviderAttempts::default()
};
usage.to_vm_dict(&attempts)
}
pub(crate) fn metadata_pairs(
&self,
provider: &str,
model: &str,
) -> Vec<(&'static str, serde_json::Value)> {
use crate::tracing::meta;
let mut pairs = vec![
(meta::MODEL, serde_json::json!(model)),
(meta::PROVIDER, serde_json::json!(provider)),
(meta::INPUT_TOKENS, serde_json::json!(self.input_tokens)),
(meta::OUTPUT_TOKENS, serde_json::json!(self.output_tokens)),
(
meta::CACHE_READ_TOKENS,
serde_json::json!(self.cache_read_tokens),
),
(
meta::CACHE_WRITE_TOKENS,
serde_json::json!(self.cache_write_tokens),
),
];
if let Some(cost) = self.cost_usd {
pairs.push((meta::COST_USD, serde_json::json!(cost)));
}
if let Some(total_tokens) = self.reported_total_tokens {
pairs.push((meta::REPORTED_TOTAL_TOKENS, serde_json::json!(total_tokens)));
}
if let Some(pricing) = self.pricing.as_deref() {
pairs.push((meta::RATE_CARD, serde_json::json!(pricing.rate_card)));
pairs.push((
"platform_fee_estimate_usd",
serde_json::json!(pricing.platform_fee_estimate_usd),
));
if let Some(basis) = &pricing.platform_fee_basis {
pairs.push(("platform_fee_basis", serde_json::json!(basis)));
}
pairs.push((
"hosted_tool_unpriced",
serde_json::json!(pricing.hosted_tool_unpriced),
));
pairs.push((
"modality_unpriced",
serde_json::json!(pricing.modality_unpriced),
));
pairs.push((
"monthly_allowance_unapplied",
serde_json::json!(pricing.monthly_allowance_unapplied),
));
if let Some(band) = pricing.input_band_minimum {
pairs.push((meta::PRICING_BAND, serde_json::json!(band)));
}
if let Some(tier) = pricing.serving_tier.as_deref() {
pairs.push((meta::PRICING_TIER, serde_json::json!(tier)));
}
if pricing.cache_ttl_unpriced {
pairs.push((meta::CACHE_TTL_UNPRICED, serde_json::json!(true)));
}
}
pairs
}
}
fn provider_attempts_vm_dict(attempts: &ProviderAttempts) -> crate::value::DictMap {
let mut fields = crate::value::DictMap::new();
fields.put_int("total", i64::from(attempts.total));
fields.put_int("retries", i64::from(attempts.retries()));
fields.put_int("rate_limited", i64::from(attempts.rate_limited));
fields.put_int("empty_completion", i64::from(attempts.empty_completion));
fields.put_int("other", i64::from(attempts.other));
fields
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolProbeUsage {
#[serde(skip_serializing_if = "Option::is_none")]
pub input_tokens: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub output_tokens: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reported_total_tokens: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_usd: Option<f64>,
#[serde(default)]
pub accounting_status: UsageAccountingStatus,
}
impl ToolProbeUsage {
pub(crate) fn from_llm_result(result: &LlmResult) -> Self {
Self::from_usage(LlmUsage::from_result(result))
}
fn from_reported(provider: &str, model: &str, reported: ReportedTokenUsage) -> Self {
let input_tokens = reported
.prompt_counts()
.ok()
.flatten()
.map(|counts| counts.total);
let output_tokens = reported.output_tokens.filter(|tokens| *tokens >= 0);
if input_tokens.is_some() && output_tokens.is_some() {
let receipt = ProviderUsageReceipt::new(input_tokens, output_tokens, None, false)
.with_cache(
CacheCounts {
read_tokens: reported.cache_read_tokens.unwrap_or(0),
write_tokens: reported.cache_write_tokens.unwrap_or(0),
reported: reported.cache_read_tokens.is_some()
|| reported.cache_write_tokens.is_some(),
},
None,
reported.cache_read_tokens.is_some() || reported.cache_write_tokens.is_some(),
);
return Self::from_usage(LlmUsage::from_provider_receipt(provider, model, &receipt));
}
Self {
input_tokens,
output_tokens,
reported_total_tokens: None,
cost_usd: None,
accounting_status: UsageAccountingStatus::Unknown,
}
}
fn from_usage(usage: LlmUsage) -> Self {
Self {
input_tokens: Some(usage.input_tokens),
output_tokens: Some(usage.output_tokens),
reported_total_tokens: usage.reported_total_tokens,
cost_usd: usage.cost_usd,
accounting_status: usage.accounting_status,
}
}
}
pub(crate) fn extract_probe_usage(
provider: &str,
model: &str,
response: &Value,
) -> Option<ToolProbeUsage> {
let reported = reported_usage_from_response(response)?;
Some(ToolProbeUsage::from_reported(provider, model, reported))
}
fn reported_usage_from_response(response: &Value) -> Option<ReportedTokenUsage> {
let mut reported = ReportedTokenUsage::default();
if let Some(frames) = response.get("frames").and_then(Value::as_array) {
for frame in frames {
reported.merge_reported(reported_usage_from_envelope(frame));
}
}
reported.merge_reported(reported_usage_from_envelope(response));
reported.has_any().then_some(reported)
}
fn reported_usage_from_envelope(envelope: &Value) -> ReportedTokenUsage {
let mut reported = ReportedTokenUsage::default();
for usage in [
envelope.get("usage"),
envelope.pointer("/message/usage"),
envelope.get("usageMetadata"),
envelope.pointer("/message/usageMetadata"),
]
.into_iter()
.flatten()
{
reported.merge_reported(ReportedTokenUsage::from_value(usage));
}
reported
}
#[cfg(test)]
#[path = "usage_tests.rs"]
mod tests;