1use serde::{Deserialize, Serialize};
11
12use crate::catalog::{ModelRecord, ModelRole, PricingPerMillion};
13use crate::health::{ModelVerification, VerificationStatus};
14use crate::{DEFAULT_MODEL, EXPERIMENTAL_MODEL, PREMIUM_CODING_MODEL, PREMIUM_REASONING_MODEL};
15
16pub const POLICY_VERSION: &str = "1";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ModelStatus {
23 Recommended,
25 #[default]
27 Available,
28 Experimental,
30 Degraded,
32 Blocked,
34 Deprecated,
36 Hidden,
38}
39
40fn default_true() -> bool {
41 true
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub struct PolicyEntry {
48 pub model_id: String,
49 pub status: ModelStatus,
50 pub rank: u32,
52 #[serde(default)]
53 pub default: bool,
54 #[serde(default = "default_true")]
55 pub primary_agent_eligible: bool,
56 pub roles: Vec<ModelRole>,
57 pub reason: String,
59 pub cost_tier: Option<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub struct ModelPolicy {
66 pub version: String,
67 pub models: Vec<PolicyEntry>,
68}
69
70impl Default for ModelPolicy {
71 fn default() -> Self {
72 Self::default_policy()
73 }
74}
75
76impl ModelPolicy {
77 pub fn default_policy() -> Self {
81 Self {
82 version: POLICY_VERSION.to_string(),
83 models: vec![
84 PolicyEntry {
85 model_id: DEFAULT_MODEL.to_string(),
86 status: ModelStatus::Recommended,
87 rank: 10,
88 default: true,
89 primary_agent_eligible: true,
90 roles: vec![ModelRole::CodingAgent, ModelRole::GeneralChat],
91 reason: "Development default: validated reliability and tool-agent suitability.".to_string(),
92 cost_tier: None,
93 },
94 PolicyEntry {
95 model_id: PREMIUM_REASONING_MODEL.to_string(),
96 status: ModelStatus::Recommended,
97 rank: 20,
98 default: false,
99 primary_agent_eligible: true,
100 roles: vec![ModelRole::Reasoning],
101 reason: "Premium reasoning model.".to_string(),
102 cost_tier: Some("high".to_string()),
103 },
104 PolicyEntry {
105 model_id: PREMIUM_CODING_MODEL.to_string(),
106 status: ModelStatus::Recommended,
107 rank: 30,
108 default: false,
109 primary_agent_eligible: true,
110 roles: vec![ModelRole::CodingAgent],
111 reason: "Premium coding model.".to_string(),
112 cost_tier: Some("high".to_string()),
113 },
114 PolicyEntry {
115 model_id: "@cf/openai/gpt-oss-120b".to_string(),
116 status: ModelStatus::Available,
117 rank: 40,
118 default: false,
119 primary_agent_eligible: true,
120 roles: vec![ModelRole::GeneralChat],
121 reason: "General fallback model.".to_string(),
122 cost_tier: None,
123 },
124 PolicyEntry {
125 model_id: "@cf/openai/gpt-oss-20b".to_string(),
126 status: ModelStatus::Available,
127 rank: 50,
128 default: false,
129 primary_agent_eligible: true,
130 roles: vec![ModelRole::GeneralChat],
131 reason: "Budget general model.".to_string(),
132 cost_tier: None,
133 },
134 PolicyEntry {
135 model_id: EXPERIMENTAL_MODEL.to_string(),
136 status: ModelStatus::Experimental,
137 rank: 900,
138 default: false,
139 primary_agent_eligible: true,
140 roles: vec![ModelRole::CodingAgent],
141 reason: "Observed delivery failures; requires passing conformance suite.".to_string(),
142 cost_tier: None,
143 },
144 PolicyEntry {
145 model_id: "@cf/zai-org/glm-5.3".to_string(),
146 status: ModelStatus::Experimental,
147 rank: 910,
148 default: false,
149 primary_agent_eligible: true,
150 roles: vec![ModelRole::CodingAgent],
151 reason: "Delivery conformance not validated".to_string(),
152 cost_tier: None,
153 },
154 PolicyEntry {
155 model_id: "@cf/meta/llama-guard-3-8b".to_string(),
156 status: ModelStatus::Hidden,
157 rank: 1000,
158 default: false,
159 primary_agent_eligible: false,
160 roles: vec![ModelRole::Safety],
161 reason: "Safety classifier.".to_string(),
162 cost_tier: None,
163 },
164 ],
165 }
166 }
167
168 pub fn status_for(&self, model_id: &str) -> ModelStatus {
170 self.models
171 .iter()
172 .find(|entry| entry.model_id == model_id)
173 .map(|entry| entry.status)
174 .unwrap_or_default()
175 }
176
177 pub fn is_primary_agent_eligible(&self, model_id: &str) -> bool {
180 self.models
181 .iter()
182 .find(|entry| entry.model_id == model_id)
183 .map(|entry| entry.primary_agent_eligible)
184 .unwrap_or(true)
185 }
186
187 pub fn default_model(&self) -> Option<&str> {
189 self.models
190 .iter()
191 .find(|entry| entry.default)
192 .map(|entry| entry.model_id.as_str())
193 }
194}
195
196pub const WEIGHT_DELIVERY: f64 = 0.40;
199pub const WEIGHT_TOOL_LOOP: f64 = 0.25;
201pub const WEIGHT_CONTEXT: f64 = 0.15;
203pub const WEIGHT_LATENCY: f64 = 0.10;
205pub const WEIGHT_PRICE: f64 = 0.10;
207
208pub const REFERENCE_CONTEXT_TOKENS: u64 = 1_310_720;
211pub const REFERENCE_LATENCY_MS: f64 = 60_000.0;
213pub const REFERENCE_PRICE_PER_MILLION: f64 = 10.0;
216
217#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227#[serde(rename_all = "snake_case")]
228pub struct RankingBreakdown {
229 pub model_id: String,
230 pub score: Option<f64>,
233 pub delivery: f64,
234 pub tool_loop: f64,
235 pub context: f64,
236 pub latency: f64,
237 pub price: f64,
238 pub weight_delivery: f64,
239 pub weight_tool_loop: f64,
240 pub weight_context: f64,
241 pub weight_latency: f64,
242 pub weight_price: f64,
243}
244
245impl RankingBreakdown {
246 pub fn breakdown_for(model: &ModelRecord, verification: Option<&ModelVerification>) -> Self {
251 let delivery = verification.map(delivery_score).unwrap_or(0.0);
252 let tool_loop = verification.and_then(|v| v.multi_turn_tool_success_rate).unwrap_or(0.0);
253 let context = context_score(model);
254 let latency = verification.map(latency_score).unwrap_or(0.0);
255 let price = price_score(&model.pricing);
256 let score = ranking_score(model, verification);
257 Self {
258 model_id: model.id.clone(),
259 score,
260 delivery,
261 tool_loop,
262 context,
263 latency,
264 price,
265 weight_delivery: WEIGHT_DELIVERY,
266 weight_tool_loop: WEIGHT_TOOL_LOOP,
267 weight_context: WEIGHT_CONTEXT,
268 weight_latency: WEIGHT_LATENCY,
269 weight_price: WEIGHT_PRICE,
270 }
271 }
272}
273
274pub fn ranking_score(model: &ModelRecord, verification: Option<&ModelVerification>) -> Option<f64> {
302 let policy = ModelPolicy::default_policy();
303 if !policy.is_primary_agent_eligible(&model.id) {
304 return None;
305 }
306 let verification = verification?;
307 if verification.status != VerificationStatus::Passing {
308 return None;
309 }
310 Some(
311 WEIGHT_DELIVERY * delivery_score(verification)
312 + WEIGHT_TOOL_LOOP * verification.multi_turn_tool_success_rate.unwrap_or(0.0)
313 + WEIGHT_CONTEXT * context_score(model)
314 + WEIGHT_LATENCY * latency_score(verification)
315 + WEIGHT_PRICE * price_score(&model.pricing),
316 )
317}
318
319fn delivery_score(verification: &ModelVerification) -> f64 {
322 verification.successful_runs as f64 / verification.total_runs.max(1) as f64
323}
324
325fn context_score(model: &ModelRecord) -> f64 {
327 model
328 .limits
329 .context_tokens
330 .map(|tokens| (tokens as f64 / REFERENCE_CONTEXT_TOKENS as f64).min(1.0))
331 .unwrap_or(0.0)
332}
333
334fn latency_score(verification: &ModelVerification) -> f64 {
337 verification
338 .median_latency_ms
339 .map(|ms| 1.0 - (ms as f64 / REFERENCE_LATENCY_MS).min(1.0))
340 .unwrap_or(0.0)
341}
342
343fn price_score(pricing: &PricingPerMillion) -> f64 {
348 match (pricing.input, pricing.output) {
349 (None, None) => 0.0,
350 (Some(input), Some(output)) => 1.0 - (((input + output) / 2.0) / REFERENCE_PRICE_PER_MILLION).min(1.0),
351 (Some(input), None) => 1.0 - (input / REFERENCE_PRICE_PER_MILLION).min(1.0),
352 (None, Some(output)) => 1.0 - (output / REFERENCE_PRICE_PER_MILLION).min(1.0),
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn defaults_match_feedback() {
362 let policy = ModelPolicy::default_policy();
363 let deepseek = policy
364 .models
365 .iter()
366 .find(|entry| entry.model_id == DEFAULT_MODEL)
367 .expect("deepseek entry present");
368 assert_eq!(deepseek.status, ModelStatus::Recommended);
369 assert_eq!(deepseek.rank, 10);
370 assert!(deepseek.default);
371
372 let glm = policy
373 .models
374 .iter()
375 .find(|entry| entry.model_id == EXPERIMENTAL_MODEL)
376 .expect("glm entry present");
377 assert_eq!(glm.status, ModelStatus::Experimental);
378 assert_eq!(glm.rank, 900);
379 assert!(!glm.default);
380 assert_eq!(glm.reason, "Observed delivery failures; requires passing conformance suite.");
381
382 let glm_53 = policy
383 .models
384 .iter()
385 .find(|entry| entry.model_id == "@cf/zai-org/glm-5.3")
386 .expect("glm-5.3 entry present");
387 assert_eq!(glm_53.status, ModelStatus::Experimental);
388 assert_eq!(glm_53.rank, 910);
389 assert!(!glm_53.default);
390 assert!(glm_53.primary_agent_eligible);
391 assert_eq!(glm_53.reason, "Delivery conformance not validated");
392
393 let guard = policy
394 .models
395 .iter()
396 .find(|entry| entry.model_id == "@cf/meta/llama-guard-3-8b")
397 .expect("guard entry present");
398 assert_eq!(guard.status, ModelStatus::Hidden);
399 assert!(!guard.primary_agent_eligible);
400 assert_eq!(guard.reason, "Safety classifier.");
401 }
402
403 #[test]
404 fn status_for_unknown_is_available() {
405 let policy = ModelPolicy::default_policy();
406 assert_eq!(policy.status_for("@cf/some-org/unknown-model"), ModelStatus::Available);
407 assert_eq!(policy.status_for(DEFAULT_MODEL), ModelStatus::Recommended);
408 assert_eq!(policy.status_for(EXPERIMENTAL_MODEL), ModelStatus::Experimental);
409 assert_eq!(policy.status_for("@cf/meta/llama-guard-3-8b"), ModelStatus::Hidden);
410 }
411
412 #[test]
413 fn primary_eligibility_follows_entries() {
414 let policy = ModelPolicy::default_policy();
415 assert!(policy.is_primary_agent_eligible(DEFAULT_MODEL));
416 assert!(policy.is_primary_agent_eligible("@cf/openai/gpt-oss-120b"));
417 assert!(policy.is_primary_agent_eligible("@cf/unknown/not-in-policy"));
418 assert!(!policy.is_primary_agent_eligible("@cf/meta/llama-guard-3-8b"));
419 }
420
421 #[test]
422 fn default_model_is_deepseek() {
423 let policy = ModelPolicy::default_policy();
424 assert_eq!(policy.default_model(), Some(DEFAULT_MODEL));
425 }
426
427 #[test]
428 fn policy_roundtrips_serde() {
429 let policy = ModelPolicy::default_policy();
430 let json = serde_json::to_string(&policy).expect("serializes");
431 let back: ModelPolicy = serde_json::from_str(&json).expect("deserializes");
432 assert_eq!(policy, back);
433 let value: serde_json::Value = serde_json::from_str(&json).expect("parses");
434 assert_eq!(value["version"], "1");
435 assert_eq!(value["models"][0]["status"], "recommended");
436 assert_eq!(value["models"][5]["status"], "experimental");
437 assert_eq!(value["models"][6]["status"], "experimental");
438 assert_eq!(value["models"][7]["status"], "hidden");
439 }
440}
441
442#[cfg(test)]
443mod ranking_tests {
444 use super::*;
445 use crate::health::{CONFORMANCE_SUITE_VERSION, VerificationConfidence};
446
447 fn record(id: &str, context: u64, input: f64, output: f64) -> ModelRecord {
450 let entry = serde_json::json!({
451 "id": id,
452 "name": id,
453 "context_length": context,
454 "created": 1_788_800_000,
455 "pricing": {
456 "prompt": format!("{:.8}", input / 1_000_000.0),
457 "completion": format!("{:.8}", output / 1_000_000.0),
458 },
459 });
460 ModelRecord::from_openrouter(&entry).expect("record normalizes")
461 }
462
463 fn verification(
466 status: VerificationStatus,
467 total: u32,
468 successful: u32,
469 multi: Option<f64>,
470 latency: Option<u64>,
471 ) -> ModelVerification {
472 ModelVerification {
473 model_id: "ranking-test".to_string(),
474 latest_run_at: None,
475 expires_at: None,
476 suite_version: CONFORMANCE_SUITE_VERSION.to_string(),
477 runner_version: "test".to_string(),
478 status,
479 agent_eligible: true,
480 confidence: VerificationConfidence::ConformanceTested,
481 total_runs: total,
482 successful_runs: successful,
483 text_completion_success_rate: None,
484 stream_completion_success_rate: None,
485 single_tool_success_rate: Some(0.96),
486 multi_turn_tool_success_rate: multi,
487 structured_output_success_rate: None,
488 median_latency_ms: latency,
489 p95_latency_ms: None,
490 total_failures: total.saturating_sub(successful),
491 timeout_failures: 0,
492 transport_failures: 0,
493 provider_5xx_failures: 0,
494 malformed_response_failures: 0,
495 malformed_tool_call_failures: 0,
496 tool_loop_failures: 0,
497 last_failure: None,
498 }
499 }
500
501 #[test]
502 fn unverified_models_never_rank() {
503 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
504 assert_eq!(ranking_score(&model, None), None);
505 let breakdown = RankingBreakdown::breakdown_for(&model, None);
506 assert_eq!(breakdown.score, None);
507 assert_eq!(breakdown.model_id, DEFAULT_MODEL);
508 }
509
510 #[test]
511 fn non_passing_verification_never_ranks() {
512 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
513 for status in [
514 VerificationStatus::Degraded,
515 VerificationStatus::Failing,
516 VerificationStatus::Untested,
517 VerificationStatus::Expired,
518 ] {
519 let verification = verification(status, 100, 99, Some(0.96), Some(2_000));
520 assert_eq!(ranking_score(&model, Some(&verification)), None, "{status:?} must not rank");
521 }
522 }
523
524 #[test]
525 fn non_primary_agent_models_never_rank() {
526 let model = record("@cf/meta/llama-guard-3-8b", 131_072, 0.05, 0.05);
529 let verification = verification(VerificationStatus::Passing, 100, 100, Some(0.99), Some(1_000));
530 assert_eq!(ranking_score(&model, Some(&verification)), None);
531 }
532
533 #[test]
534 fn passing_deepseek_like_outscores_passing_glm_like_on_tool_loop() {
535 let deepseek = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
536 let glm = record(EXPERIMENTAL_MODEL, 1_310_720, 0.15, 0.5);
537 let ds_verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
538 let glm_verification = verification(VerificationStatus::Passing, 100, 99, Some(0.70), Some(2_000));
539 let ds_score = ranking_score(&deepseek, Some(&ds_verification)).expect("deepseek ranks");
540 let glm_score = ranking_score(&glm, Some(&glm_verification)).expect("glm ranks");
541 assert!(ds_score > glm_score, "deepseek {ds_score} must beat glm {glm_score}");
542 let gap = ds_score - glm_score;
545 assert!((gap - WEIGHT_TOOL_LOOP * 0.26).abs() < 1e-9, "gap {gap}");
546 }
547
548 #[test]
549 fn breakdown_weights_sum_to_one() {
550 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
551 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
552 let breakdown = RankingBreakdown::breakdown_for(&model, Some(&verification));
553 let sum = breakdown.weight_delivery
554 + breakdown.weight_tool_loop
555 + breakdown.weight_context
556 + breakdown.weight_latency
557 + breakdown.weight_price;
558 assert!((sum - 1.0).abs() < 1e-12, "weights must sum to 1.0, got {sum}");
559 assert_eq!(breakdown.score, ranking_score(&model, Some(&verification)));
560 let dot = WEIGHT_DELIVERY * breakdown.delivery
562 + WEIGHT_TOOL_LOOP * breakdown.tool_loop
563 + WEIGHT_CONTEXT * breakdown.context
564 + WEIGHT_LATENCY * breakdown.latency
565 + WEIGHT_PRICE * breakdown.price;
566 assert!((dot - breakdown.score.expect("score present")).abs() < 1e-9);
567 }
568
569 #[test]
570 fn bigger_context_increases_score_other_components_held() {
571 let small = record(DEFAULT_MODEL, 262_144, 0.15, 0.5);
572 let big = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
573 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
574 let small_score = ranking_score(&small, Some(&verification)).expect("small ranks");
575 let big_score = ranking_score(&big, Some(&verification)).expect("big ranks");
576 assert!(big_score > small_score);
577 let capped = record(DEFAULT_MODEL, 2_000_000, 0.15, 0.5);
579 let capped_score = ranking_score(&capped, Some(&verification)).expect("capped ranks");
580 assert!((capped_score - big_score).abs() < 1e-9);
581 }
582
583 #[test]
584 fn lower_latency_increases_score_other_components_held() {
585 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
586 let fast = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
587 let slow = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(30_000));
588 let fast_score = ranking_score(&model, Some(&fast)).expect("fast ranks");
589 let slow_score = ranking_score(&model, Some(&slow)).expect("slow ranks");
590 assert!(fast_score > slow_score);
591 let missing = verification(VerificationStatus::Passing, 100, 99, Some(0.96), None);
593 let missing_score = ranking_score(&model, Some(&missing)).expect("missing-latency ranks");
594 assert!((missing_score - (slow_score - WEIGHT_LATENCY * 0.5)).abs() < 1e-9);
595 let maxed = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(61_000));
597 let maxed_score = ranking_score(&model, Some(&maxed)).expect("maxed-latency ranks");
598 assert!((maxed_score - (slow_score - WEIGHT_LATENCY * 0.5)).abs() < 1e-9);
599 }
600
601 #[test]
602 fn lower_price_increases_score_other_components_held() {
603 let cheap = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
604 let expensive = record(DEFAULT_MODEL, 1_310_720, 10.0, 12.0);
605 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
606 let cheap_score = ranking_score(&cheap, Some(&verification)).expect("cheap ranks");
607 let expensive_score = ranking_score(&expensive, Some(&verification)).expect("expensive ranks");
608 assert!(cheap_score > expensive_score);
609 let no_price = ModelRecord {
611 pricing: crate::catalog::PricingPerMillion { input: None, cached_input: None, output: None },
612 ..cheap.clone()
613 };
614 let no_price_score = ranking_score(&no_price, Some(&verification)).expect("no-price ranks");
615 assert!(no_price_score < cheap_score);
616 let cheap_breakdown = RankingBreakdown::breakdown_for(&cheap, Some(&verification));
617 let no_price_breakdown = RankingBreakdown::breakdown_for(&no_price, Some(&verification));
618 assert!(cheap_breakdown.price > no_price_breakdown.price);
619 assert_eq!(no_price_breakdown.price, 0.0);
620 }
621
622 #[test]
623 fn degraded_but_cheaper_never_ranks() {
624 let model = record(DEFAULT_MODEL, 1_310_720, 0.05, 0.10);
626 let verification = verification(VerificationStatus::Degraded, 100, 60, Some(0.50), Some(1_000));
627 assert_eq!(ranking_score(&model, Some(&verification)), None);
628 }
629
630 #[test]
631 fn breakdown_serde_is_snake_case() {
632 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
633 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
634 let breakdown = RankingBreakdown::breakdown_for(&model, Some(&verification));
635 let json = serde_json::to_string(&breakdown).expect("ser");
636 let value: serde_json::Value = serde_json::from_str(&json).expect("parse");
638 for key in [
639 "model_id",
640 "score",
641 "delivery",
642 "tool_loop",
643 "context",
644 "latency",
645 "price",
646 "weight_delivery",
647 "weight_tool_loop",
648 "weight_context",
649 "weight_latency",
650 "weight_price",
651 ] {
652 assert!(value.get(key).is_some(), "missing snake_case key {key}");
653 }
654 let back: RankingBreakdown = serde_json::from_str(&json).expect("de");
657 assert_eq!(back.model_id, breakdown.model_id);
658 assert!(close_f64(back.score, breakdown.score), "score {back:?} vs {breakdown:?}");
659 for (actual, expected) in [
660 (back.delivery, breakdown.delivery),
661 (back.tool_loop, breakdown.tool_loop),
662 (back.context, breakdown.context),
663 (back.latency, breakdown.latency),
664 (back.price, breakdown.price),
665 (back.weight_delivery, breakdown.weight_delivery),
666 (back.weight_tool_loop, breakdown.weight_tool_loop),
667 (back.weight_context, breakdown.weight_context),
668 (back.weight_latency, breakdown.weight_latency),
669 (back.weight_price, breakdown.weight_price),
670 ] {
671 assert!((actual - expected).abs() < 1e-12, "{actual} vs {expected}");
672 }
673 }
674
675 fn close_f64(actual: Option<f64>, expected: Option<f64>) -> bool {
676 match (actual, expected) {
677 (Some(actual), Some(expected)) => (actual - expected).abs() < 1e-12,
678 (None, None) => true,
679 _ => false,
680 }
681 }
682}