Skip to main content

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}