Skip to main content

memra_server/
metering.rs

1//! The admission/accounting seam (lane engine-billing-extraction-20260829).
2//!
3//! The server's job at this boundary is to ADMIT, DENY, and REPORT COUNTS. What
4//! admission means — budgets, prices, tenancy policy — is the deployment's business,
5//! supplied behind these traits. The stock binary wires the in-repo reference
6//! implementation (`ledger::ReferenceMetering`); a deployment-owned binary can wire its own.
7//! Everything here speaks tokens and verdicts, never money: the vocabulary is the
8//! boundary, and it is what lets the policy half live outside this crate.
9//!
10//! The seam is `pub`: a deployment-owned binary wires its implementation through
11//! [`crate::ServerWiring`] and runs the same server.
12
13use std::any::Any;
14
15/// An opaque reservation handle minted by [`Metering::reserve`] and consumed by
16/// [`Metering::open`] of the SAME implementation. The server carries it between the
17/// two calls without looking inside; dropping it un-consumed must release whatever
18/// it holds (the reference implementation refunds on drop).
19pub type Permit = Box<dyn Any + Send>;
20
21/// The request identity a receipt is opened with. All borrowed: this is a view of
22/// state the handler already owns, taken for the duration of one `open` call.
23pub struct RequestMeta<'a> {
24    pub request_id: &'a str,
25    pub tenant: &'a str,
26    pub model: &'a str,
27    pub route: &'static str,
28    pub lane: &'static str,
29    pub stream: bool,
30}
31
32/// Token counts as the worker measured them. The one usage shape that crosses the
33/// seam; anything priced is derived from these on the implementation's side.
34#[derive(Debug, Clone, Copy, Default)]
35pub struct UsageCounts {
36    pub prompt_tokens: u64,
37    pub cached_prompt_tokens: u64,
38    pub completion_tokens: u64,
39}
40
41/// Why admission said no. Mirrors the HTTP contract the handlers already speak:
42/// `Insufficient`/`Blocked` answer 402 (one recovery action for callers),
43/// `Unenrolled` answers 402 with its own code, `Unavailable` is the fail-closed 500.
44#[derive(Debug, PartialEq, Eq)]
45pub enum AdmitError {
46    Insufficient,
47    Blocked,
48    Unenrolled,
49    Unavailable(String),
50}
51
52/// Limits-source health for the operator metrics surface. Counts only.
53#[derive(Debug, Clone, Copy)]
54pub struct LimitsHealth {
55    pub source_reload_failed: u64,
56    pub source_reload_consecutive: u32,
57    pub source_available: bool,
58}
59
60/// Per-deployment admission + usage accounting. One object, present iff the
61/// deployment configured accounting at all (`AppState.metering: Option<Arc<dyn ..>>`
62/// mirrors the old `request_ledger: Option<Ledger>` exactly).
63pub trait Metering: Send + Sync {
64    /// Whether per-tenant admission limits are configured at all. `false` = every
65    /// authenticated tenant is admitted without reservation (counting may still run).
66    fn enforces_limits(&self) -> bool;
67
68    /// Whether this tenant is subject to limits. With limits enforced, an unknown
69    /// tenant is NOT a free pass — the caller rejects it as unenrolled.
70    fn is_limited(&self, tenant: &str) -> Result<bool, AdmitError>;
71
72    /// Reserve headroom for a request's worst case, in tokens. `Ok(Some(permit))`
73    /// rides the receipt and is settled to worker-truth usage; `Ok(None)` means the
74    /// implementation needs no per-request hold.
75    fn reserve(
76        &self,
77        tenant: &str,
78        model: &str,
79        prompt_tokens: u64,
80        completion_bound: u64,
81    ) -> Result<Option<Permit>, AdmitError>;
82
83    /// Open the request's usage receipt. Every terminal outcome settles it through
84    /// one of the [`Receipt`] methods; dropping it unfinalized is the abandoned-client
85    /// path and must stay safe (the reference implementation prices the partial).
86    fn open(&self, meta: &RequestMeta<'_>, permit: Option<Permit>) -> Box<dyn Receipt>;
87
88    /// Whether this tenant's requests are captured (a retention policy the
89    /// implementation owns). The one pre-receipt check handlers make so an unmarked
90    /// tenant never pays for a prompt copy; the receipt's own [`Receipt::wants_capture`]
91    /// is the post-open gate and the implementation's settle-time re-check stays
92    /// authoritative.
93    fn captures(&self, _tenant: &str) -> bool {
94        false
95    }
96
97    /// Limits-source health for the operator metrics surface, when limits exist.
98    fn limits_health(&self) -> Option<LimitsHealth>;
99
100    /// The graceful-drain deadline expired with requests still in flight: everything
101    /// dropped from this moment on was killed by OUR shutdown, not abandoned by its
102    /// client. Fault attribution (owner ruling 2026-08-23): the implementation must
103    /// settle those drops without billing the caller. Latched — the process is exiting.
104    fn drain_kill(&self) {}
105}
106
107/// One request's accounting record, admission row to terminal row. Method names
108/// deliberately match the reference implementation's inherent methods so the
109/// handler code reads identically through the seam.
110pub trait Receipt: Send {
111    /// Whether this receipt was opened captured. Gates the caller's lazy prompt build;
112    /// `false` makes `arm_capture` a no-op.
113    fn wants_capture(&self) -> bool {
114        false
115    }
116    /// Attach the prompt payload to a captured request. Where it goes and the
117    /// settle-time consent re-check belong to the implementation; the seam never
118    /// names a storage type.
119    fn arm_capture(&mut self, prompt: serde_json::Value);
120    fn capture_completion_delta(&mut self, text: &str);
121    fn record_prompt_usage(
122        &mut self,
123        prompt_tokens: u64,
124        cached_prompt_tokens: u64,
125    ) -> Result<(), String>;
126    fn record_completion_token(&mut self) -> Result<(), String>;
127    fn complete(&mut self, usage: UsageCounts, worker_elapsed_s: f64) -> Result<(), String>;
128    /// Deadline-partial: billed like `complete` but census-distinct.
129    fn complete_deadline_partial(
130        &mut self,
131        usage: UsageCounts,
132        worker_elapsed_s: f64,
133    ) -> Result<(), String>;
134    fn reject(&mut self, status: u16, error_code: &str) -> Result<(), String>;
135    /// Terminal rows with a NAMED zero-debit outcome (`deadline_exceeded`,
136    /// `shed_deadline`, `shed_queue`) — `reject`'s twin for outcomes the census
137    /// distinguishes. Never bills.
138    fn settle_unbilled(
139        &mut self,
140        outcome: &'static str,
141        status: u16,
142        error_code: &str,
143    ) -> Result<(), String>;
144}
145
146/// What a metering factory gets to see at construction time: the server's loaded
147/// model roster. Deliberately small — an implementation brings its own prices,
148/// policies, and storage; the server only vouches for what it serves.
149pub struct MeteringInit<'a> {
150    pub models: &'a [String],
151}
152
153/// Deployment hook: build the metering implementation once models are loaded.
154/// `Ok(None)` = no accounting (the stock no-ledger shape). An `Err` is a startup
155/// FATAL — accounting configuration never fails open.
156pub type MeteringFactory = Box<
157    dyn FnOnce(&MeteringInit<'_>) -> Result<Option<std::sync::Arc<dyn Metering>>, String> + Send,
158>;