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 /// The authenticated key's non-secret identification prefix, when the request
27 /// carries per-key identity (multi-key ring). What per-PRINCIPAL policy means —
28 /// spend caps, quotas — is the implementation's business.
29 pub principal: Option<&'a str>,
30 pub model: &'a str,
31 pub route: &'static str,
32 pub lane: &'static str,
33 pub stream: bool,
34}
35
36/// Token counts as the worker measured them. The one usage shape that crosses the
37/// seam; anything priced is derived from these on the implementation's side.
38#[derive(Debug, Clone, Copy, Default)]
39pub struct UsageCounts {
40 pub prompt_tokens: u64,
41 pub cached_prompt_tokens: u64,
42 pub completion_tokens: u64,
43}
44
45/// Why admission said no. Mirrors the HTTP contract the handlers already speak:
46/// `Insufficient`/`Blocked` answer 402 (one recovery action for callers),
47/// `Unenrolled` answers 402 with its own code, `Unavailable` is the fail-closed 500.
48#[derive(Debug, PartialEq, Eq)]
49pub enum AdmitError {
50 Insufficient,
51 Blocked,
52 Unenrolled,
53 /// The PRINCIPAL (per-key) spend ceiling is reached while the tenant itself may
54 /// still have balance. Its own 402 code: the caller's recovery is raising or
55 /// clearing the key's cap, not adding credit.
56 PrincipalCapped,
57 Unavailable(String),
58}
59
60/// Limits-source health for the operator metrics surface. Counts only.
61#[derive(Debug, Clone, Copy)]
62pub struct LimitsHealth {
63 pub source_reload_failed: u64,
64 pub source_reload_consecutive: u32,
65 pub source_available: bool,
66}
67
68/// Per-deployment admission + usage accounting. One object, present iff the
69/// deployment configured accounting at all (`AppState.metering: Option<Arc<dyn ..>>`
70/// mirrors the old `request_ledger: Option<Ledger>` exactly).
71pub trait Metering: Send + Sync {
72 /// Whether per-tenant admission limits are configured at all. `false` = every
73 /// authenticated tenant is admitted without reservation (counting may still run).
74 fn enforces_limits(&self) -> bool;
75
76 /// Whether this tenant is subject to limits. With limits enforced, an unknown
77 /// tenant is NOT a free pass — the caller rejects it as unenrolled.
78 fn is_limited(&self, tenant: &str) -> Result<bool, AdmitError>;
79
80 /// Reserve headroom for a request's worst case, in tokens. `Ok(Some(permit))`
81 /// rides the receipt and is settled to worker-truth usage; `Ok(None)` means the
82 /// implementation needs no per-request hold. `principal` is the authenticated
83 /// key's non-secret prefix when the request carries one — the hook for per-key
84 /// policy (spend caps) on the implementation's side.
85 fn reserve(
86 &self,
87 tenant: &str,
88 principal: Option<&str>,
89 model: &str,
90 prompt_tokens: u64,
91 completion_bound: u64,
92 ) -> Result<Option<Permit>, AdmitError>;
93
94 /// Open the request's usage receipt. Every terminal outcome settles it through
95 /// one of the [`Receipt`] methods; dropping it unfinalized is the abandoned-client
96 /// path and must stay safe (the reference implementation prices the partial).
97 fn open(&self, meta: &RequestMeta<'_>, permit: Option<Permit>) -> Box<dyn Receipt>;
98
99 /// Whether this tenant's requests are captured (a retention policy the
100 /// implementation owns). The one pre-receipt check handlers make so an unmarked
101 /// tenant never pays for a prompt copy; the receipt's own [`Receipt::wants_capture`]
102 /// is the post-open gate and the implementation's settle-time re-check stays
103 /// authoritative.
104 fn captures(&self, _tenant: &str) -> bool {
105 false
106 }
107
108 /// Limits-source health for the operator metrics surface, when limits exist.
109 fn limits_health(&self) -> Option<LimitsHealth>;
110
111 /// The graceful-drain deadline expired with requests still in flight: everything
112 /// dropped from this moment on was killed by OUR shutdown, not abandoned by its
113 /// client. Fault attribution (owner ruling 2026-08-23): the implementation must
114 /// settle those drops without billing the caller. Latched — the process is exiting.
115 fn drain_kill(&self) {}
116}
117
118/// One request's accounting record, admission row to terminal row. Method names
119/// deliberately match the reference implementation's inherent methods so the
120/// handler code reads identically through the seam.
121pub trait Receipt: Send {
122 /// Whether this receipt was opened captured. Gates the caller's lazy prompt build;
123 /// `false` makes `arm_capture` a no-op.
124 fn wants_capture(&self) -> bool {
125 false
126 }
127 /// Attach the prompt payload to a captured request. Where it goes and the
128 /// settle-time consent re-check belong to the implementation; the seam never
129 /// names a storage type.
130 fn arm_capture(&mut self, prompt: serde_json::Value);
131 fn capture_completion_delta(&mut self, text: &str);
132 fn record_prompt_usage(
133 &mut self,
134 prompt_tokens: u64,
135 cached_prompt_tokens: u64,
136 ) -> Result<(), String>;
137 fn record_completion_token(&mut self) -> Result<(), String>;
138 fn complete(&mut self, usage: UsageCounts, worker_elapsed_s: f64) -> Result<(), String>;
139 /// Deadline-partial: billed like `complete` but census-distinct.
140 fn complete_deadline_partial(
141 &mut self,
142 usage: UsageCounts,
143 worker_elapsed_s: f64,
144 ) -> Result<(), String>;
145 fn reject(&mut self, status: u16, error_code: &str) -> Result<(), String>;
146 /// Terminal rows with a NAMED zero-debit outcome (`deadline_exceeded`,
147 /// `shed_deadline`, `shed_queue`) — `reject`'s twin for outcomes the census
148 /// distinguishes. Never bills.
149 fn settle_unbilled(
150 &mut self,
151 outcome: &'static str,
152 status: u16,
153 error_code: &str,
154 ) -> Result<(), String>;
155}
156
157/// What a metering factory gets to see at construction time: the server's loaded
158/// model roster. Deliberately small — an implementation brings its own prices,
159/// policies, and storage; the server only vouches for what it serves.
160pub struct MeteringInit<'a> {
161 pub models: &'a [String],
162}
163
164/// Deployment hook: build the metering implementation once models are loaded.
165/// `Ok(None)` = no accounting (the stock no-ledger shape). An `Err` is a startup
166/// FATAL — accounting configuration never fails open.
167pub type MeteringFactory = Box<
168 dyn FnOnce(&MeteringInit<'_>) -> Result<Option<std::sync::Arc<dyn Metering>>, String> + Send,
169>;