codex_wrapper/
rollout_budget.rs1use crate::error::{Error, Result};
17
18#[derive(Debug, Clone, PartialEq)]
25pub struct RolloutBudgetConfig {
26 limit_tokens: u64,
27 reminder_at_remaining_tokens: Vec<u64>,
28 sampling_token_weight: f64,
29 prefill_token_weight: f64,
30}
31
32impl RolloutBudgetConfig {
33 #[must_use]
35 pub fn builder(limit_tokens: u64) -> RolloutBudgetConfigBuilder {
36 RolloutBudgetConfigBuilder {
37 limit_tokens,
38 reminder_at_remaining_tokens: Vec::new(),
39 sampling_token_weight: 1.0,
40 prefill_token_weight: 1.0,
41 }
42 }
43
44 #[must_use]
46 pub fn limit_tokens(&self) -> u64 {
47 self.limit_tokens
48 }
49
50 #[must_use]
52 pub fn reminder_at_remaining_tokens(&self) -> &[u64] {
53 &self.reminder_at_remaining_tokens
54 }
55
56 #[must_use]
58 pub fn sampling_token_weight(&self) -> f64 {
59 self.sampling_token_weight
60 }
61
62 #[must_use]
64 pub fn prefill_token_weight(&self) -> f64 {
65 self.prefill_token_weight
66 }
67
68 pub(crate) fn config_override(&self) -> String {
69 let reminders = self
70 .reminder_at_remaining_tokens
71 .iter()
72 .map(u64::to_string)
73 .collect::<Vec<_>>()
74 .join(",");
75 format!(
76 "features.rollout_budget={{enabled=true,limit_tokens={},reminder_at_remaining_tokens=[{}],sampling_token_weight={},prefill_token_weight={}}}",
77 self.limit_tokens, reminders, self.sampling_token_weight, self.prefill_token_weight,
78 )
79 }
80
81 pub(crate) fn is_config_override(value: &str) -> bool {
82 value.starts_with("features.rollout_budget={enabled=true,limit_tokens=")
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct RolloutBudgetConfigBuilder {
89 limit_tokens: u64,
90 reminder_at_remaining_tokens: Vec<u64>,
91 sampling_token_weight: f64,
92 prefill_token_weight: f64,
93}
94
95impl RolloutBudgetConfigBuilder {
96 #[must_use]
101 pub fn reminder_at_remaining_tokens(
102 mut self,
103 thresholds: impl IntoIterator<Item = u64>,
104 ) -> Self {
105 self.reminder_at_remaining_tokens = thresholds.into_iter().collect();
106 self
107 }
108
109 #[must_use]
111 pub fn sampling_token_weight(mut self, weight: f64) -> Self {
112 self.sampling_token_weight = weight;
113 self
114 }
115
116 #[must_use]
118 pub fn prefill_token_weight(mut self, weight: f64) -> Self {
119 self.prefill_token_weight = weight;
120 self
121 }
122
123 pub fn build(self) -> Result<RolloutBudgetConfig> {
125 if self.limit_tokens == 0 || self.limit_tokens > i64::MAX as u64 {
126 return Err(invalid("limit_tokens must be in 1..=i64::MAX"));
127 }
128 if self
129 .reminder_at_remaining_tokens
130 .iter()
131 .any(|&threshold| threshold == 0 || threshold >= self.limit_tokens)
132 {
133 return Err(invalid(
134 "reminder thresholds must be positive and below limit_tokens",
135 ));
136 }
137 for (field, weight) in [
138 ("sampling_token_weight", self.sampling_token_weight),
139 ("prefill_token_weight", self.prefill_token_weight),
140 ] {
141 if !weight.is_finite() || weight < 0.0 {
142 return Err(invalid(format!("{field} must be finite and non-negative")));
143 }
144 }
145 Ok(RolloutBudgetConfig {
146 limit_tokens: self.limit_tokens,
147 reminder_at_remaining_tokens: self.reminder_at_remaining_tokens,
148 sampling_token_weight: self.sampling_token_weight,
149 prefill_token_weight: self.prefill_token_weight,
150 })
151 }
152}
153
154fn invalid(message: impl Into<String>) -> Error {
155 Error::InvalidRolloutBudget {
156 message: message.into(),
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn valid_config_serializes_as_one_strict_cli_override() {
166 let budget = RolloutBudgetConfig::builder(100_000)
167 .reminder_at_remaining_tokens([50_000, 10_000])
168 .sampling_token_weight(1.5)
169 .prefill_token_weight(0.25)
170 .build()
171 .expect("valid budget");
172
173 assert_eq!(budget.limit_tokens(), 100_000);
174 assert_eq!(budget.reminder_at_remaining_tokens(), [50_000, 10_000]);
175 assert_eq!(
176 budget.config_override(),
177 "features.rollout_budget={enabled=true,limit_tokens=100000,reminder_at_remaining_tokens=[50000,10000],sampling_token_weight=1.5,prefill_token_weight=0.25}"
178 );
179 }
180
181 #[test]
182 fn invalid_limits_thresholds_and_weights_fail_before_launch() {
183 for result in [
184 RolloutBudgetConfig::builder(0).build(),
185 RolloutBudgetConfig::builder(i64::MAX as u64 + 1).build(),
186 RolloutBudgetConfig::builder(100)
187 .reminder_at_remaining_tokens([0])
188 .build(),
189 RolloutBudgetConfig::builder(100)
190 .reminder_at_remaining_tokens([100])
191 .build(),
192 RolloutBudgetConfig::builder(100)
193 .sampling_token_weight(f64::NAN)
194 .build(),
195 RolloutBudgetConfig::builder(100)
196 .prefill_token_weight(-1.0)
197 .build(),
198 ] {
199 assert!(matches!(result, Err(Error::InvalidRolloutBudget { .. })));
200 }
201 }
202}