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>;