velesdb_memory/context/insights.rs
1//! Savings accounting: tokens (always) and money (only when a pricing table
2//! is injected).
3//!
4//! Money is integer micro-units of one currency (1 unit = 10⁻⁶ of the
5//! currency's major unit, e.g. `1_000_000` micros = 1 EUR) — no floats, no
6//! rounding drift. Prices are **injected and versioned**, never hardcoded:
7//! the compiler works fully without a pricing table, reporting tokens only.
8//!
9//! The token figures are *local estimates* (see
10//! [`super::estimator::HeuristicEstimator`]), not the provider's exact count,
11//! nor billed tokens, nor cache-read tokens — four different numbers a caller
12//! must not conflate. The insights report says which one it carries.
13
14use std::collections::BTreeMap;
15
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19/// Pricing of one model, in integer micro-units per **million** input tokens
20/// (e.g. a `3 USD / 1M tokens` rate is `3_000_000`).
21#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
22#[schemars(transform = crate::schema::strip_int_formats)]
23pub struct ModelPricing {
24 /// Micro-units of the table's currency per million input tokens.
25 pub input_micros_per_million_tokens: u64,
26}
27
28/// A versioned, caller-injected pricing table.
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30pub struct PricingTable {
31 /// Caller-side version tag of this table (recorded in insights so a cost
32 /// figure is always traceable to the prices that produced it).
33 pub version: String,
34 /// ISO-4217 currency code of every rate in the table (e.g. `"EUR"`).
35 pub currency: String,
36 /// Rates keyed by model name (a `BTreeMap` so iteration — and therefore
37 /// every serialized output — is deterministically ordered).
38 pub models: BTreeMap<String, ModelPricing>,
39}
40
41impl PricingTable {
42 /// The cost of `tokens` input tokens on `model`, in micro-units of
43 /// [`Self::currency`] — `None` when the model has no rate in the table.
44 ///
45 /// Saturates instead of overflowing: with realistic rates (≤ 10⁹ micros
46 /// per million tokens) saturation is unreachable, and a saturated figure
47 /// is still the safer answer than a wrapped one.
48 #[must_use]
49 pub fn cost_micros(&self, model: &str, tokens: u64) -> Option<u64> {
50 let rate = self.models.get(model)?.input_micros_per_million_tokens;
51 Some(tokens.saturating_mul(rate) / 1_000_000)
52 }
53}
54
55/// Token and cost savings of one compilation.
56#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
57#[schemars(transform = crate::schema::strip_int_formats)]
58pub struct CompilationInsights {
59 /// Estimated tokens of all input fragments combined.
60 pub tokens_in: u64,
61 /// Estimated tokens of the assembled output.
62 pub tokens_out: u64,
63 /// `tokens_in − tokens_out` (saturating) — the local *estimate* of what
64 /// this compilation avoided sending; not billed tokens.
65 pub tokens_saved: u64,
66 /// Tokens saved attributed to the rule that saved them, keyed by rule id
67 /// (`BTreeMap` for deterministic serialization order).
68 pub tokens_saved_by_rule: BTreeMap<String, u64>,
69 /// Estimated cost avoided, in micro-units of [`Self::currency`] — only
70 /// when a pricing table was injected *and* it prices the target model.
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub estimated_cost_saved_micros: Option<u64>,
73 /// Currency of the cost figure, from the pricing table.
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub currency: Option<String>,
76 /// Version tag of the pricing table that produced the cost figure.
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub pricing_version: Option<String>,
79}
80
81#[cfg(test)]
82#[path = "insights_tests.rs"]
83mod tests;