Skip to main content

oxios_kernel/token_maxing/
config.rs

1//! Token Maxing configuration (RFC-031 §2).
2//!
3//! The schema is a small surface — the user must opt in, then list each
4//! subscription provider with its plan allocation. The eligibility rule is
5//! conservative: only providers with `billing_model = "subscription"` are
6//! ever used by [`crate::token_maxing::TokenMaxer`]. A metered key can
7//! never be silently drafted.
8//!
9//! ## Example
10//!
11//! ```toml
12//! [token-maxing]
13//! enabled = false
14//!
15//! [[token-maxing.providers]]
16//! provider = "zai"
17//! billing_model = "subscription"
18//! token_limit = 2000000
19//! reset_window_secs = 18000          # 5h
20//! min_remaining_percent = 5
21//! models = ["zai/glm-4.6", "zai/glm-4.5-air"]
22//! ```
23
24use serde::{Deserialize, Serialize};
25
26/// The only accepted value of `billing_model`. This is the single choke
27/// point that upholds the "절대 동작하면 안 된다" constraint — the
28/// `QuotaTracker` will only ever mark a provider eligible when this exact
29/// string is present in the config.
30pub const SUBSCRIPTION_BILLING_MODEL: &str = "subscription";
31
32/// Top-level `[token-maxing]` configuration.
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34pub struct TokenMaxingConfig {
35    /// Whether the mode is enabled at all. When `false`, the TokenMaxer
36    /// refuses to start a session and the API returns 503.
37    #[serde(default)]
38    pub enabled: bool,
39
40    /// Per-provider plan data. Providers missing from this list are
41    /// **ineligible by default** — a metered key can never be drafted in
42    /// even by accident.
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub providers: Vec<TokenMaxingProviderConfig>,
45
46    /// Default per-provider floor (%). The orchestrator stops draining a
47    /// provider below this remaining percentage to avoid thrash. A
48    /// per-provider override is also available.
49    #[serde(default = "default_min_remaining_percent")]
50    pub default_min_remaining_percent: u8,
51
52    /// Cadence (seconds) at which the QuotaTracker tries to recalibrate
53    /// the self-tracked counter to a real provider endpoint. Has no effect
54    /// for providers that expose no fetcher (ZAI/Minimax today). 0 = off.
55    #[serde(default = "default_recalibration_interval_secs")]
56    pub recalibration_interval_secs: u64,
57
58    /// Maximum concurrent tasks (Phase 5). Phase 1-3 is single-stream.
59    #[serde(default = "default_parallel_providers")]
60    pub parallel_providers: bool,
61}
62
63fn default_min_remaining_percent() -> u8 {
64    5
65}
66
67fn default_recalibration_interval_secs() -> u64 {
68    60
69}
70
71fn default_parallel_providers() -> bool {
72    false
73}
74
75/// Per-provider subscription plan entry. One row per subscription provider
76/// the user has opted in to.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct TokenMaxingProviderConfig {
79    /// Provider id (matches the engine's provider id, e.g. `"zai"`).
80    pub provider: String,
81
82    /// Billing model. The only accepted value is `"subscription"`. Any
83    /// other value causes this entry to be **rejected at load time** so
84    /// the metered-never rule cannot be silently violated.
85    pub billing_model: String,
86
87    /// Plan allocation per window (tokens). The self-tracked counter
88    /// treats this as the cap; `remaining_percent` is computed from it.
89    pub token_limit: u64,
90
91    /// Drain window in seconds. The counter resets when `now - window_start
92    /// >= reset_window_secs` AND no real `resets_at` is known.
93    pub reset_window_secs: u64,
94
95    /// Floor: stop draining below this remaining percentage. Optional
96    /// override of [`TokenMaxingConfig::default_min_remaining_percent`].
97    #[serde(default)]
98    pub min_remaining_percent: Option<u8>,
99
100    /// Models to round-robin within the provider. Empty = use every model
101    /// the provider exposes.
102    #[serde(default)]
103    pub models: Vec<String>,
104}
105
106impl TokenMaxingProviderConfig {
107    /// Floor for this provider (the per-provider override, falling back to
108    /// the global default).
109    pub fn min_remaining_percent(&self, default: u8) -> u8 {
110        self.min_remaining_percent.unwrap_or(default)
111    }
112}
113
114impl TokenMaxingConfig {
115    /// Returns the per-provider config for `provider`, if any.
116    pub fn get(&self, provider: &str) -> Option<&TokenMaxingProviderConfig> {
117        self.providers.iter().find(|p| p.provider == provider)
118    }
119
120    /// Whether `provider` is eligible for token-maxing under this config.
121    ///
122    /// The check is the single guard that upholds the metered-never rule:
123    /// the provider must (a) have an entry in `providers`, (b) the entry
124    /// must declare `billing_model = "subscription"`, and (c) the entry's
125    /// `token_limit` must be > 0 (a zero limit would burn nothing anyway).
126    pub fn is_eligible(&self, provider: &str) -> bool {
127        match self.get(provider) {
128            Some(p) => {
129                p.billing_model == SUBSCRIPTION_BILLING_MODEL
130                    && p.token_limit > 0
131                    && p.reset_window_secs > 0
132            }
133            None => false,
134        }
135    }
136
137    /// Validate the config — called at load time. Returns the list of
138    /// errors. Empty list = valid.
139    pub fn validate(&self) -> Vec<String> {
140        let mut errors = Vec::new();
141        if self.enabled && self.providers.is_empty() {
142            errors.push(
143                "token-maxing.enabled = true but no providers configured — refuse to start"
144                    .to_string(),
145            );
146        }
147        for (i, p) in self.providers.iter().enumerate() {
148            if p.provider.trim().is_empty() {
149                errors.push(format!("token-maxing.providers[{i}].provider is empty"));
150            }
151
152            if p.token_limit == 0 {
153                errors.push(format!(
154                    "token-maxing.providers[{i}].token_limit must be > 0"
155                ));
156            }
157            if p.reset_window_secs == 0 {
158                errors.push(format!(
159                    "token-maxing.providers[{i}].reset_window_secs must be > 0"
160                ));
161            }
162        }
163        errors
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn subscription_only_rule() {
173        let cfg = TokenMaxingConfig {
174            enabled: true,
175            providers: vec![TokenMaxingProviderConfig {
176                provider: "openai".into(),
177                billing_model: "metered".into(),
178                token_limit: 1_000_000,
179                reset_window_secs: 3600,
180                min_remaining_percent: None,
181                models: vec![],
182            }],
183            default_min_remaining_percent: 5,
184            recalibration_interval_secs: 60,
185            parallel_providers: false,
186        };
187        assert!(!cfg.is_eligible("openai"));
188        assert!(!cfg.is_eligible("zai"));
189        // validate() no longer rejects metered entries — the eligibility
190        // rule is enforced at runtime by QuotaTracker via is_eligible().
191        assert!(cfg.validate().is_empty());
192    }
193
194    #[test]
195    fn subscription_passes() {
196        let cfg = TokenMaxingConfig {
197            enabled: true,
198            providers: vec![TokenMaxingProviderConfig {
199                provider: "zai".into(),
200                billing_model: SUBSCRIPTION_BILLING_MODEL.into(),
201                token_limit: 2_000_000,
202                reset_window_secs: 18000,
203                min_remaining_percent: Some(7),
204                models: vec!["zai/glm-4.6".into()],
205            }],
206            default_min_remaining_percent: 5,
207            recalibration_interval_secs: 60,
208            parallel_providers: false,
209        };
210        assert!(cfg.is_eligible("zai"));
211        assert!(!cfg.is_eligible("anthropic"));
212        assert!(cfg.validate().is_empty());
213        let p = cfg.get("zai").unwrap();
214        assert_eq!(p.min_remaining_percent(5), 7);
215    }
216
217    #[test]
218    fn empty_when_disabled_means_no_providers() {
219        let cfg = TokenMaxingConfig::default();
220        assert!(!cfg.enabled);
221        assert!(!cfg.is_eligible("anything"));
222    }
223}