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