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}