liter_llm/tenant/resolver.rs
1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4
5use super::context::TenantId;
6
7/// A resolved virtual-key record returned by [`KeyResolver::resolve`].
8#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
9pub struct ResolvedKey {
10 /// Tenant this key belongs to.
11 pub tenant_id: TenantId,
12 /// Model names this key may access. Empty means unrestricted.
13 pub allowed_models: Vec<String>,
14 /// Optional per-period spending cap.
15 pub monthly_budget: Option<rust_decimal::Decimal>,
16 /// ISO-4217 currency code for `monthly_budget`, e.g. `"EUR"`.
17 pub currency: Option<String>,
18 /// Arbitrary key-value metadata (e.g. `"tier"`, `"label"`).
19 pub metadata: HashMap<String, String>,
20 /// Whether the key is currently active.
21 pub active: bool,
22}
23
24/// Errors returned by [`KeyResolver::resolve`].
25#[derive(Debug, thiserror::Error)]
26pub enum KeyResolverError {
27 /// No key matching the supplied token exists.
28 #[error("api key not found")]
29 NotFound,
30 /// The key exists but has been deactivated.
31 #[error("api key is inactive")]
32 Inactive,
33 /// A backend-specific error prevented resolution.
34 #[error("key resolver backend error: {0}")]
35 Backend(String),
36}
37
38/// Resolves a raw API token to a [`ResolvedKey`].
39///
40/// Implement this trait to plug in a database-backed, remote, or in-process
41/// key store. The built-in implementation is [`super::InMemoryKeyResolver`].
42///
43/// All methods return `Pin<Box<dyn Future… + 'static>>` so the returned future
44/// can be stored or spawned onto a Tokio executor without borrowing `self`.
45/// The trait itself requires `'static` ownership; implement via `Arc<Self>` if
46/// you need shared ownership across call sites.
47pub trait KeyResolver: Send + Sync + 'static {
48 /// Resolve `api_key` to its associated [`ResolvedKey`].
49 ///
50 /// Takes `api_key` by value so the returned future is `'static` and can be
51 /// spawned onto a Tokio task or stored without borrowing the resolver.
52 ///
53 /// Returns [`KeyResolverError::NotFound`] when no record matches,
54 /// [`KeyResolverError::Inactive`] when the record exists but
55 /// `active == false`.
56 fn resolve(
57 &self,
58 api_key: String,
59 ) -> Pin<Box<dyn Future<Output = Result<ResolvedKey, KeyResolverError>> + Send + 'static>>;
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::tenant::InMemoryKeyResolver;
66
67 /// Compile-time assertion: the future returned by `InMemoryKeyResolver::resolve`
68 /// is `Send + 'static` and can be stored or spawned without borrowing the resolver.
69 #[allow(dead_code)]
70 fn _assert_future_is_static_send<T: Future + Send + 'static>(_: T) {}
71
72 #[allow(dead_code)]
73 fn _check_resolve_future_bounds(resolver: &InMemoryKeyResolver) {
74 _assert_future_is_static_send(resolver.resolve("sk-test".to_owned()));
75 }
76}