enlil/usage.rs
1//! The **OSS ↔ cloud seam** for the proxy hot path.
2//!
3//! `routing::proxy` is the heart of the open-source `enlil` binary, but it needs
4//! two things the OSS build deliberately does not have: per-tenant billing/quota
5//! accounting, and a database-backed per-tenant upstream override.
6//!
7//! Rather than have the OSS proxy depend on `finops::cost_tracker`, `redis_store`
8//! and `db` (which are proprietary), it is generic over [`ProxyEnv`]. Both hooks
9//! have no-op defaults, so the OSS build gets correct behaviour for free, while
10//! the cloud build implements them on `AppState`.
11//!
12//! See DEVELOPMENT_PLAN.md, Step 2.
13
14use crate::state::EnlilState;
15use crate::tokens::TokenUsage;
16use std::future::Future;
17
18/// A completed upstream call, described for downstream accounting.
19///
20/// Owned rather than borrowed: this is built once per request *after* the response
21/// has been handled (off the latency-critical path), so the allocations are
22/// irrelevant and owning the data keeps the hook free of lifetime plumbing.
23pub struct UsageEvent {
24 pub tenant_id: String,
25 pub api_key_id: Option<uuid::Uuid>,
26 pub model: String,
27 pub usage: TokenUsage,
28 /// Actual (cache-aware) cost in microdollars.
29 pub cost_micro: u64,
30 pub total_tokens: u32,
31 pub prompt_tokens: u32,
32 pub completion_tokens: u32,
33 pub protocol: String,
34 pub path: String,
35}
36
37/// The environment the proxy handler runs in.
38///
39/// Requires `Deref<Target = EnlilState>` so that every existing `state.<field>`
40/// access in the proxy continues to resolve against the OSS core state.
41///
42/// Both methods default to OSS behaviour (no proprietary accounting), so the
43/// open-source build simply does not implement them.
44pub trait ProxyEnv: std::ops::Deref<Target = EnlilState> + Send + Sync + 'static {
45 /// A per-tenant upstream override, if the deployment has one configured.
46 ///
47 /// OSS default: `None` — the upstream is resolved purely from local config.
48 /// Cloud: looks up the tenant's configured upstream in Postgres.
49 fn resolve_tenant_upstream(
50 &self,
51 _tenant_id: &str,
52 ) -> impl Future<Output = Option<String>> + Send {
53 async { None }
54 }
55
56 /// Record a completed call for billing, quota and usage-log purposes.
57 ///
58 /// OSS default: no-op. Local per-request cost and token counts are already
59 /// recorded by the OSS core (metrics, agent registry, trace store) before
60 /// this is called.
61 /// Cloud: cost tracker, token quota, shared Redis counters, Postgres usage log.
62 fn record_usage(&self, _ev: UsageEvent) -> impl Future<Output = ()> + Send {
63 async {}
64 }
65
66 /// Deliver an out-of-band alert about a governance event (loop break, risk
67 /// alert, prompt injection, policy alert, budget exceeded...).
68 ///
69 /// OSS default: no-op — the event is already recorded in local metrics, the
70 /// event feed and the trace store. Cloud: POSTs to the tenant's configured
71 /// webhook URL.
72 fn send_alert(
73 &self,
74 _tenant_id: &str,
75 _event: &str,
76 _message: &str,
77 ) -> impl Future<Output = ()> + Send {
78 async {}
79 }
80}