Skip to main content

latch_billing/
observation.rs

1//! Observation module - defines the raw usage observation types.
2//!
3//! `UsageObservation` is an immutable fact representing a single observation
4//! of token usage. It is the core input to the rating engine.
5
6use crate::identity::{CorrelationIds, UsageEventId};
7use crate::pricing::{ModelRef, ProviderRef};
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12/// Raw usage observation - an immutable fact.
13///
14/// This is the primary input to the billing system. Once created,
15/// observations should never be modified - only rated to produce derived records.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct UsageObservation {
18    /// Unique identifier for this observation (idempotency key).
19    pub event_id: UsageEventId,
20
21    /// Who is being billed for this usage.
22    pub subject: crate::identity::BillingSubject,
23
24    /// The actual meter readings (token counts, image counts, etc.).
25    pub meter_set: MeterSet,
26
27    /// Which model/pricing dimension this observation applies to.
28    pub model_ref: ModelRef,
29
30    /// Which provider handled this request (if applicable).
31    pub provider_ref: Option<ProviderRef>,
32
33    /// Where this observation came from (provider API response, stream accumulation, etc.).
34    pub source: UsageSource,
35
36    /// The final state of the request (success/error/timeout).
37    pub outcome: UsageOutcome,
38
39    /// Timing information for this observation.
40    pub timing: UsageTiming,
41
42    /// Correlation IDs for tracing across systems.
43    pub correlation: CorrelationIds,
44
45    /// Extensible attributes: is_fallback, step_type, estimated_reason, etc.
46    pub attributes: Attributes,
47}
48
49/// A collection of meter readings, keyed by `MeterKind`.
50///
51/// Uses `HashMap` to guarantee no duplicate `MeterKind` entries.
52/// Use `MeterSet::insert()` to add values - it will accumulate
53/// quantities for the same key instead of overwriting.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct MeterSet {
56    pub meters: HashMap<MeterKind, u64>,
57}
58
59impl MeterSet {
60    /// Create a new empty `MeterSet`.
61    pub fn new() -> Self {
62        Self {
63            meters: HashMap::new(),
64        }
65    }
66
67    /// Insert or accumulate a meter reading.
68    ///
69    /// If the key already exists, the quantity is added to the existing value.
70    /// Uses `checked_add` to prevent u64 overflow.
71    pub fn accumulate(&mut self, kind: MeterKind, quantity: u64) -> Result<(), MeterSetError> {
72        use std::collections::hash_map::Entry;
73        match self.meters.entry(kind) {
74            Entry::Occupied(mut e) => {
75                let new_val = e
76                    .get()
77                    .checked_add(quantity)
78                    .ok_or_else(|| MeterSetError::Overflow(e.key().clone()))?;
79                e.insert(new_val);
80            }
81            Entry::Vacant(e) => {
82                e.insert(quantity);
83            }
84        }
85        Ok(())
86    }
87
88    /// Get the quantity for a given meter kind.
89    /// Returns 0 if the key is not present.
90    pub fn get(&self, kind: &MeterKind) -> u64 {
91        self.meters.get(kind).copied().unwrap_or(0)
92    }
93}
94
95impl Default for MeterSet {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101/// Error type for `MeterSet` operations.
102#[derive(Debug, Clone)]
103pub enum MeterSetError {
104    /// Insert operation would cause u64 overflow.
105    Overflow(MeterKind),
106}
107
108/// The kind of meter being measured.
109///
110/// Uses an enum for type safety and extensibility.
111/// `Custom(String)` allows for provider-specific meter kinds.
112#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
113pub enum MeterKind {
114    InputTokens,
115    OutputTokens,
116    CachedInputTokens,
117    CachedWriteTokens,
118    ReasoningTokens,
119    AudioInputTokens,
120    AudioOutputTokens,
121    ImageCount,
122    Custom(String),
123}
124
125/// Where a usage observation originated from.
126///
127/// This is important for auditing - observations from different sources
128/// may have different trust levels.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub enum UsageSource {
131    /// Reported directly by the provider API response.
132    ProviderReported,
133    /// Accumulated from streaming response chunks.
134    StreamAccumulated,
135    /// Estimated (e.g., from a request that failed before completion).
136    Estimated,
137    /// Corrected - must carry `correction_of` attribute pointing to original UsageEventId.
138    ///
139    /// Correction rules:
140    /// - Raw observations are always append-only, never deleted or overwritten
141    /// - Correction itself is also an independent observation with its own UsageEventId
142    /// - Projection/aggregation layer only counts the latest valid version
143    /// - Latest version determined by: primary sort key `observed_at`, secondary sort key `event_id` (lexicographic)
144    Corrected { correction_of: UsageEventId },
145}
146
147/// The final state of the request.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub enum UsageOutcome {
150    Success,
151    Error { code: String },
152    Timeout,
153    Unknown,
154}
155
156/// Constrained attribute set, newtype wrapper to enforce documentation constraints.
157///
158/// Key constraints:
159/// - Keys cannot start with `sys.` (reserved prefix for library use)
160/// - Key max length: 64 characters
161/// - Value max length: 256 characters
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
163#[serde(transparent)]
164pub struct Attributes {
165    inner: HashMap<String, String>,
166}
167
168impl Attributes {
169    pub const MAX_KEY_LEN: usize = 64;
170    pub const MAX_VALUE_LEN: usize = 256;
171
172    /// Create a new empty Attributes set.
173    pub fn new() -> Self {
174        Self {
175            inner: HashMap::new(),
176        }
177    }
178
179    /// Insert an attribute. Returns error if key starts with "sys." or exceeds length limits.
180    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) -> Result<(), AttributeError> {
181        let key = key.into();
182        let value = value.into();
183
184        if key.starts_with("sys.") {
185            return Err(AttributeError::ReservedPrefix(key));
186        }
187
188        if key.len() > Self::MAX_KEY_LEN {
189            let len = key.len();
190            return Err(AttributeError::KeyTooLong { key, len });
191        }
192
193        if value.len() > Self::MAX_VALUE_LEN {
194            let len = value.len();
195            return Err(AttributeError::ValueTooLong { key, len });
196        }
197
198        self.inner.insert(key, value);
199        Ok(())
200    }
201
202    /// Get an attribute value by key.
203    pub fn get(&self, key: &str) -> Option<&str> {
204        self.inner.get(key).map(|s| s.as_str())
205    }
206
207    /// Iterate over all attributes.
208    pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
209        self.inner.iter()
210    }
211}
212
213/// Error type for Attributes operations.
214#[derive(Debug, Clone)]
215pub enum AttributeError {
216    ReservedPrefix(String),
217    KeyTooLong { key: String, len: usize },
218    ValueTooLong { key: String, len: usize },
219}
220
221/// Timing information for a usage observation.
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct UsageTiming {
224    /// When this observation was generated.
225    pub observed_at: DateTime<Utc>,
226
227    /// When the request completed (filled in when streaming ends).
228    pub completed_at: Option<DateTime<Utc>>,
229}
230
231/// Currency code, wrapped in newtype for type safety.
232#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
233pub struct CurrencyCode(pub String);
234
235impl CurrencyCode {
236    /// Helper constructors (not const - use FromStr for parsing).
237    pub fn usd() -> Self {
238        CurrencyCode("USD".to_string())
239    }
240    pub fn cny() -> Self {
241        CurrencyCode("CNY".to_string())
242    }
243    pub fn eur() -> Self {
244        CurrencyCode("EUR".to_string())
245    }
246}
247
248impl std::str::FromStr for CurrencyCode {
249    type Err = CurrencyCodeError;
250
251    /// Only accepts 3 uppercase ASCII letters (ISO 4217 format).
252    /// Does not do full ISO 4217 enum validation to avoid heavy dependencies.
253    fn from_str(s: &str) -> Result<Self, Self::Err> {
254        if s.len() == 3 && s.chars().all(|c| c.is_ascii_uppercase()) {
255            Ok(CurrencyCode(s.to_string()))
256        } else {
257            Err(CurrencyCodeError::Invalid(s.to_string()))
258        }
259    }
260}
261
262/// Error type for `CurrencyCode` parsing.
263#[derive(Debug, Clone)]
264pub enum CurrencyCodeError {
265    Invalid(String),
266}
267
268impl std::fmt::Display for CurrencyCodeError {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        match self {
271            CurrencyCodeError::Invalid(s) => write!(f, "Invalid currency code: {s}"),
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn meter_set_accumulate_accumulates() {
282        let mut ms = MeterSet::new();
283        ms.accumulate(MeterKind::InputTokens, 100).unwrap();
284        ms.accumulate(MeterKind::InputTokens, 50).unwrap();
285        assert_eq!(ms.get(&MeterKind::InputTokens), 150);
286    }
287
288    #[test]
289    fn meter_set_overflow_returns_error() {
290        let mut ms = MeterSet::new();
291        ms.accumulate(MeterKind::InputTokens, u64::MAX).unwrap();
292        let result = ms.accumulate(MeterKind::InputTokens, 1);
293        assert!(matches!(result, Err(MeterSetError::Overflow(_))));
294    }
295
296    #[test]
297    fn meter_set_get_missing_returns_zero() {
298        let ms = MeterSet::new();
299        assert_eq!(ms.get(&MeterKind::OutputTokens), 0);
300    }
301
302    #[test]
303    fn meter_kind_custom_hash() {
304        let mut map = HashMap::new();
305        map.insert(MeterKind::Custom("test".to_string()), 1);
306        assert_eq!(map.get(&MeterKind::Custom("test".to_string())), Some(&1));
307    }
308
309    #[test]
310    fn meter_kind_enum_variant_not_confused_with_custom() {
311        let mut map = HashMap::new();
312        map.insert(MeterKind::InputTokens, 1);
313        map.insert(MeterKind::Custom("InputTokens".to_string()), 2);
314        assert_eq!(map.len(), 2);
315    }
316
317    #[test]
318    fn attributes_insert_valid() {
319        let mut attrs = Attributes::new();
320        assert!(attrs.insert("key1", "value1").is_ok());
321        assert_eq!(attrs.get("key1"), Some("value1"));
322    }
323
324    #[test]
325    fn attributes_rejects_reserved_prefix() {
326        let mut attrs = Attributes::new();
327        let result = attrs.insert("sys.test", "value");
328        assert!(matches!(result, Err(AttributeError::ReservedPrefix(_))));
329    }
330
331    #[test]
332    fn attributes_rejects_too_long_key() {
333        let mut attrs = Attributes::new();
334        let long_key = "a".repeat(65);
335        let result = attrs.insert(long_key, "value");
336        assert!(matches!(result, Err(AttributeError::KeyTooLong { .. })));
337    }
338}