1use crate::kernel::*;
8
9pub struct InputBuilder {
27 input: KernelInput,
28}
29
30impl InputBuilder {
31 #[must_use]
33 pub fn new(request_sequence: u64, requested_model_id: u32) -> Self {
34 Self {
35 input: KernelInput {
36 request_sequence,
37 requested_model_id,
38 input_tokens: 0,
39 output_tokens: 0,
40 business_value_microunits: 0,
41 budget_limit_microunits: u64::MAX,
42 risk_bps: 0,
43 confidence_bps: BASIS_POINTS as u16,
44 minimum_quality_bps: 0,
45 max_p95_latency_ms: 0,
46 required_capabilities: 0,
47 allowed_provider_mask: ALL_PROVIDERS,
48 required_region_mask: 0,
49 },
50 }
51 }
52
53 #[must_use]
54 pub fn tokens(mut self, input: u32, output: u32) -> Self {
55 self.input.input_tokens = input;
56 self.input.output_tokens = output;
57 self
58 }
59
60 #[must_use]
61 pub fn business_value(mut self, microunits: i64) -> Self {
62 self.input.business_value_microunits = microunits;
63 self
64 }
65
66 #[must_use]
67 pub fn budget_limit(mut self, microunits: u64) -> Self {
68 self.input.budget_limit_microunits = microunits;
69 self
70 }
71
72 #[must_use]
74 pub fn risk(mut self, risk_bps: u16, confidence_bps: u16) -> Self {
75 self.input.risk_bps = risk_bps;
76 self.input.confidence_bps = confidence_bps;
77 self
78 }
79
80 #[must_use]
81 pub fn minimum_quality(mut self, bps: u16) -> Self {
82 self.input.minimum_quality_bps = bps;
83 self
84 }
85
86 #[must_use]
87 pub fn max_latency(mut self, ms: u32) -> Self {
88 self.input.max_p95_latency_ms = ms;
89 self
90 }
91
92 #[must_use]
93 pub fn capabilities(mut self, mask: u64) -> Self {
94 self.input.required_capabilities = mask;
95 self
96 }
97
98 #[must_use]
99 pub fn providers(mut self, mask: u64) -> Self {
100 self.input.allowed_provider_mask = mask;
101 self
102 }
103
104 #[must_use]
105 pub fn regions(mut self, mask: u64) -> Self {
106 self.input.required_region_mask = mask;
107 self
108 }
109
110 #[must_use]
117 pub fn build(self) -> KernelInput {
118 self.try_build()
119 .expect("KernelInput validation failed — use try_build() for fallible construction")
120 }
121
122 pub fn try_build(self) -> Result<KernelInput, InputError> {
124 self.input.validate()?;
125 Ok(self.input)
126 }
127}
128
129pub struct ModelBuilder {
144 model: KernelModel,
145}
146
147impl ModelBuilder {
148 #[must_use]
150 pub fn new(model_id: u32, provider_id: u16) -> Self {
151 Self {
152 model: KernelModel {
153 model_id,
154 provider_id,
155 quality_bps: 8_000,
156 risk_ceiling_bps: 9_500,
157 enabled: 1,
158 p95_latency_ms: 200,
159 capabilities: 0,
160 region_mask: ALL_REGIONS,
161 input_cost_microunits_per_million_tokens: 0,
162 output_cost_microunits_per_million_tokens: 0,
163 },
164 }
165 }
166
167 #[must_use]
168 pub fn quality(mut self, bps: u16) -> Self {
169 self.model.quality_bps = bps;
170 self
171 }
172
173 #[must_use]
174 pub fn risk_ceiling(mut self, bps: u16) -> Self {
175 self.model.risk_ceiling_bps = bps;
176 self
177 }
178
179 #[must_use]
180 pub fn enabled(mut self, yes: bool) -> Self {
181 self.model.enabled = u8::from(yes);
182 self
183 }
184
185 #[must_use]
186 pub fn latency(mut self, p95_ms: u32) -> Self {
187 self.model.p95_latency_ms = p95_ms;
188 self
189 }
190
191 #[must_use]
192 pub fn capabilities(mut self, mask: u64) -> Self {
193 self.model.capabilities = mask;
194 self
195 }
196
197 #[must_use]
198 pub fn regions(mut self, mask: u64) -> Self {
199 self.model.region_mask = mask;
200 self
201 }
202
203 #[must_use]
205 pub fn cost(mut self, input_per_m: u64, output_per_m: u64) -> Self {
206 self.model.input_cost_microunits_per_million_tokens = input_per_m;
207 self.model.output_cost_microunits_per_million_tokens = output_per_m;
208 self
209 }
210
211 #[must_use]
213 pub fn build(self) -> KernelModel {
214 self.model
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
220pub enum BuildError {
221 #[error("config error: {0}")]
222 Config(#[from] crate::config::ConfigError),
223 #[error("policy error: {0}")]
224 Policy(#[from] crate::kernel::PolicyError),
225 #[error("catalog too large: {len} models exceeds max_catalog_size {max}")]
226 CatalogTooLarge { len: usize, max: usize },
227}
228
229pub struct PolicyBuilder {
245 config: crate::config::EngineConfig,
246 policy_epoch: u64,
247 catalog_epoch: u64,
248 models: Vec<KernelModel>,
249}
250
251impl PolicyBuilder {
252 #[must_use]
254 pub fn new(config: crate::config::EngineConfig) -> Self {
255 Self {
256 config,
257 policy_epoch: 1,
258 catalog_epoch: 1,
259 models: Vec::new(),
260 }
261 }
262
263 #[must_use]
265 pub fn epochs(mut self, policy: u64, catalog: u64) -> Self {
266 self.policy_epoch = policy;
267 self.catalog_epoch = catalog;
268 self
269 }
270
271 #[must_use]
273 pub fn model(mut self, model: KernelModel) -> Self {
274 self.models.push(model);
275 self
276 }
277
278 #[must_use]
280 pub fn models(mut self, models: impl IntoIterator<Item = KernelModel>) -> Self {
281 self.models.extend(models);
282 self
283 }
284
285 pub fn build(self) -> Result<PolicySnapshot, BuildError> {
290 self.config.validate()?;
291 if self.models.len() > self.config.max_catalog_size {
292 return Err(BuildError::CatalogTooLarge {
293 len: self.models.len(),
294 max: self.config.max_catalog_size,
295 });
296 }
297 Ok(PolicySnapshot::try_new(
298 self.policy_epoch,
299 self.catalog_epoch,
300 self.config.hard_risk_limit_bps,
301 self.config.minimum_confidence_bps,
302 self.config.risk_penalty_multiplier_bps,
303 self.config.latency_penalty_microunits_per_ms,
304 self.models,
305 )?)
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn input_builder_defaults() {
315 let input = InputBuilder::new(1, 10).tokens(500, 200).build();
316 assert_eq!(input.request_sequence, 1);
317 assert_eq!(input.requested_model_id, 10);
318 assert_eq!(input.input_tokens, 500);
319 assert_eq!(input.allowed_provider_mask, ALL_PROVIDERS);
320 assert_eq!(input.required_region_mask, 0);
321 assert_eq!(input.budget_limit_microunits, u64::MAX);
322 }
323
324 #[test]
325 fn model_builder_defaults() {
326 let model = ModelBuilder::new(1, 0).cost(100, 400).build();
327 assert_eq!(model.model_id, 1);
328 assert_eq!(model.enabled, 1);
329 assert_eq!(model.quality_bps, 8_000);
330 assert_eq!(model.risk_ceiling_bps, 9_500);
331 }
332
333 #[test]
334 fn policy_builder_roundtrip() {
335 let config = crate::config::EngineConfig::new();
336 let snap = PolicyBuilder::new(config)
337 .epochs(7, 11)
338 .model(
339 ModelBuilder::new(1, 0)
340 .quality(9000)
341 .cost(250, 1000)
342 .build(),
343 )
344 .model(ModelBuilder::new(2, 1).quality(7000).cost(25, 125).build())
345 .build()
346 .unwrap();
347 assert_eq!(snap.policy_epoch, 7);
348 assert_eq!(snap.models().len(), 2);
349 }
350
351 #[test]
352 fn builder_integrates_with_prescribe() {
353 let config = crate::config::EngineConfig::new();
354 let snap = PolicyBuilder::new(config)
355 .model(ModelBuilder::new(1, 0).quality(9000).cost(100, 400).build())
356 .build()
357 .unwrap();
358 let input = InputBuilder::new(1, 1)
359 .tokens(1000, 500)
360 .business_value(100_000)
361 .risk(1000, 9000)
362 .minimum_quality(5000)
363 .build();
364 let decision = snap.prescribe(input);
365 assert!(decision.is_executable());
366 }
367
368 #[test]
369 fn disabled_model_via_builder() {
370 let model = ModelBuilder::new(1, 0).enabled(false).build();
371 assert_eq!(model.enabled, 0);
372 }
373
374 #[test]
375 fn catalog_too_large_rejected() {
376 let config = crate::config::EngineConfig::new().max_catalog_size(1);
377 let result = PolicyBuilder::new(config)
378 .model(ModelBuilder::new(1, 0).cost(100, 400).build())
379 .model(ModelBuilder::new(2, 1).cost(10, 40).build())
380 .build();
381 assert!(matches!(result, Err(BuildError::CatalogTooLarge { .. })));
382 }
383
384 #[test]
385 fn invalid_config_rejected_at_build() {
386 let config = crate::config::EngineConfig::new().hard_risk_limit(10_001);
387 let result = PolicyBuilder::new(config)
388 .model(ModelBuilder::new(1, 0).cost(100, 400).build())
389 .build();
390 assert!(matches!(result, Err(BuildError::Config(_))));
391 }
392
393 use proptest::prelude::*;
394
395 proptest! {
396 #[test]
397 fn builder_prescribe_never_panics(
398 seq in any::<u64>(),
399 model_id in 1_u32..=2,
400 input_tokens in any::<u32>(),
401 output_tokens in any::<u32>(),
402 value in any::<i64>(),
403 risk in 0_u16..=MAX_BPS,
404 confidence in 0_u16..=MAX_BPS,
405 ) {
406 let config = crate::config::EngineConfig::new();
407 let snap = PolicyBuilder::new(config)
408 .model(ModelBuilder::new(1, 0).quality(9000).cost(100, 400).build())
409 .model(ModelBuilder::new(2, 1).quality(7000).cost(10, 40).build())
410 .build()
411 .unwrap();
412 let input = InputBuilder::new(seq, model_id)
413 .tokens(input_tokens, output_tokens)
414 .business_value(value)
415 .risk(risk, confidence)
416 .try_build()
417 .expect("valid bounded bps");
418 let _ = snap.prescribe(input);
419 }
420 }
421}