use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use super::frames::ToolDef;
use super::store::McpEvent;
pub const WRITER_QUEUE: usize = 4096;
#[derive(Debug)]
pub struct MeteredExchange {
pub event: McpEvent,
pub inventory: Option<Vec<ToolDef>>,
}
static SINK: OnceLock<tokio::sync::mpsc::Sender<MeteredExchange>> = OnceLock::new();
static DROPPED: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn installed() -> bool {
SINK.get().is_some()
}
pub fn record(exchange: MeteredExchange) {
let Some(tx) = SINK.get() else { return };
if tx.try_send(exchange).is_err() {
let n = DROPPED.fetch_add(1, Ordering::Relaxed) + 1;
if n.is_power_of_two() {
tracing::warn!("mcp metering backlogged: {n} event(s) dropped so far");
}
}
}
#[must_use]
pub fn dropped_count() -> u64 {
DROPPED.load(Ordering::Relaxed)
}
#[must_use]
pub fn pending_count() -> usize {
SINK.get().map_or(0, |tx| tx.max_capacity() - tx.capacity())
}
pub fn spawn_writer(pool: deadpool_postgres::Pool) -> bool {
let (tx, mut rx) = tokio::sync::mpsc::channel::<MeteredExchange>(WRITER_QUEUE);
if SINK.set(tx).is_err() {
return false;
}
tokio::spawn(async move {
let pricing = crate::core::gain::model_pricing::ModelPricing::load();
let reference = reference_model(&crate::core::config::Config::load().proxy.baseline);
while let Some(MeteredExchange {
mut event,
inventory,
}) = rx.recv().await
{
event.reference_model = reference.clone();
event.context_cost_usd = context_cost_usd(
u64::try_from(event.result_tokens).unwrap_or(0),
reference.as_deref(),
&pricing,
);
match pool.get().await {
Ok(client) => {
if let Err(e) = super::store::insert_event(&client, &event).await {
tracing::warn!("mcp_events insert failed (fail-open): {e:#}");
}
if let Some(tools) = inventory
&& let Err(e) =
super::store::upsert_inventory(&client, &event.server_id, &tools).await
{
tracing::warn!("mcp_tool_inventory upsert failed (fail-open): {e:#}");
}
}
Err(e) => {
tracing::warn!("mcp store pool unavailable (fail-open): {e:#}");
}
}
}
});
true
}
#[must_use]
pub fn context_cost_usd(
result_tokens: u64,
reference_model: Option<&str>,
pricing: &crate::core::gain::model_pricing::ModelPricing,
) -> f64 {
let Some(model) = reference_model else {
return 0.0;
};
#[allow(clippy::cast_precision_loss)]
{
pricing.quote(Some(model)).cost.input_per_m / 1_000_000.0 * result_tokens as f64
}
}
#[must_use]
pub fn reference_model(baseline: &crate::core::config::BaselineConfig) -> Option<String> {
baseline
.reference_model
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::gain::model_pricing::ModelPricing;
#[test]
fn record_without_sink_is_a_noop() {
record(MeteredExchange {
event: McpEvent {
person: "p".into(),
team: None,
project: "default".into(),
server_id: "s".into(),
method: "tools/call".into(),
tool: Some("t".into()),
status: "ok".into(),
duration_ms: 1,
result_bytes: 1,
result_tokens: 1,
context_cost_usd: 0.0,
reference_model: None,
},
inventory: None,
});
}
#[test]
fn context_cost_uses_reference_input_rate_and_never_invents() {
let pricing = ModelPricing::load();
let cost = context_cost_usd(200_000, Some("claude-opus-4.5"), &pricing);
assert!((cost - 1.0).abs() < 1e-9, "expected $1.00, got {cost}");
assert_eq!(context_cost_usd(200_000, None, &pricing), 0.0);
assert_eq!(context_cost_usd(0, Some("claude-opus-4.5"), &pricing), 0.0);
}
#[test]
fn reference_model_trims_and_rejects_empty() {
use crate::core::config::BaselineConfig;
let some = BaselineConfig {
reference_model: Some(" claude-opus-4.5 ".into()),
local_shadow_rate_per_mtok: None,
};
assert_eq!(reference_model(&some).as_deref(), Some("claude-opus-4.5"));
let blank = BaselineConfig {
reference_model: Some(" ".into()),
local_shadow_rate_per_mtok: None,
};
assert_eq!(reference_model(&blank), None);
assert_eq!(reference_model(&BaselineConfig::default()), None);
}
}