use std::sync::Arc;
use async_trait::async_trait;
use bitrouter_sdk::Result;
use bitrouter_sdk::language_model::{SettlementContext, SettlementRecorder, Usage};
use crate::metering::db::RequestMetric;
use crate::metering::pricing::{PricingTable, calculate_charge_micro_usd};
use crate::metering::store::MeteringStore;
pub struct MeteringRecorder {
store: MeteringStore,
pricing: Arc<PricingTable>,
}
impl MeteringRecorder {
pub fn new(store: MeteringStore, pricing: Arc<PricingTable>) -> Self {
Self { store, pricing }
}
fn estimate_charge(&self, ctx: &SettlementContext) -> (i64, bool) {
let usage = Usage {
prompt_tokens: ctx.prompt_tokens,
completion_tokens: ctx.completion_tokens,
reasoning_tokens: ctx.reasoning_tokens,
cache_read_tokens: ctx.cache_read_tokens,
cache_write_tokens: ctx.cache_write_tokens,
};
match self.pricing.resolve(&ctx.provider_id, &ctx.model_id) {
Some(pricing) if !pricing.is_unconfigured() => {
match calculate_charge_micro_usd(&usage, &pricing) {
Some(c) => (c, false),
None => (0, true),
}
}
_ => (0, true),
}
}
}
#[async_trait]
impl SettlementRecorder for MeteringRecorder {
async fn record(&self, ctx: &mut SettlementContext) -> Result<()> {
let (estimated_charge_micro_usd, missing_pricing) = self.estimate_charge(ctx);
if missing_pricing {
tracing::debug!(
provider = %ctx.provider_id,
model = %ctx.model_id,
request_id = %ctx.request_id,
"metering: no pricing for (provider, model); estimated charge = 0"
);
}
let metric = RequestMetric {
request_id: ctx.request_id.clone(),
user_id: ctx.caller.user_id().to_string(),
api_key_id: ctx.caller.api_key_id().to_string(),
model_id: ctx.model_id.clone(),
provider_id: ctx.provider_id.clone(),
prompt_tokens: ctx.prompt_tokens,
completion_tokens: ctx.completion_tokens,
reasoning_tokens: ctx.reasoning_tokens,
cache_read_tokens: ctx.cache_read_tokens,
cache_write_tokens: ctx.cache_write_tokens,
estimated_charge_micro_usd,
latency_ms: ctx.latency_ms,
generation_time_ms: ctx.generation_time_ms,
streamed: ctx.streamed,
error: ctx.error.as_ref().map(|e| e.to_string()),
};
self.store.record_request(metric).await
}
}