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> {
301 let policy = ModelPolicy::default_policy();
302 if !policy.is_primary_agent_eligible(&model.id) {
303 return None;
304 }
305 let verification = verification?;
306 if verification.status != VerificationStatus::Passing {
307 return None;
308 }
309 Some(
310 WEIGHT_DELIVERY * delivery_score(verification)
311 + WEIGHT_TOOL_LOOP * verification.multi_turn_tool_success_rate.unwrap_or(0.0)
312 + WEIGHT_CONTEXT * context_score(model)
313 + WEIGHT_LATENCY * latency_score(verification)
314 + WEIGHT_PRICE * price_score(&model.pricing),
315 )
316}
317
318fn delivery_score(verification: &ModelVerification) -> f64 {
321 verification.successful_runs as f64 / verification.total_runs.max(1) as f64
322}
323
324fn context_score(model: &ModelRecord) -> f64 {
326 model
327 .limits
328 .context_tokens
329 .map(|tokens| (tokens as f64 / REFERENCE_CONTEXT_TOKENS as f64).min(1.0))
330 .unwrap_or(0.0)
331}
332
333fn latency_score(verification: &ModelVerification) -> f64 {
336 verification
337 .median_latency_ms
338 .map(|ms| 1.0 - (ms as f64 / REFERENCE_LATENCY_MS).min(1.0))
339 .unwrap_or(0.0)
340}
341
342fn price_score(pricing: &PricingPerMillion) -> f64 {
347 match (pricing.input, pricing.output) {
348 (None, None) => 0.0,
349 (Some(input), Some(output)) => 1.0 - (((input + output) / 2.0) / REFERENCE_PRICE_PER_MILLION).min(1.0),
350 (Some(input), None) => 1.0 - (input / REFERENCE_PRICE_PER_MILLION).min(1.0),
351 (None, Some(output)) => 1.0 - (output / REFERENCE_PRICE_PER_MILLION).min(1.0),
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn defaults_match_policy() {
361 let policy = ModelPolicy::default_policy();
362 let deepseek = policy
363 .models
364 .iter()
365 .find(|entry| entry.model_id == DEFAULT_MODEL)
366 .expect("deepseek entry present");
367 assert_eq!(deepseek.status, ModelStatus::Recommended);
368 assert_eq!(deepseek.rank, 10);
369 assert!(deepseek.default);
370
371 let glm = policy
372 .models
373 .iter()
374 .find(|entry| entry.model_id == EXPERIMENTAL_MODEL)
375 .expect("glm entry present");
376 assert_eq!(glm.status, ModelStatus::Experimental);
377 assert_eq!(glm.rank, 900);
378 assert!(!glm.default);
379 assert_eq!(glm.reason, "Observed delivery failures; requires passing conformance suite.");
380
381 let glm_53 = policy
382 .models
383 .iter()
384 .find(|entry| entry.model_id == "@cf/zai-org/glm-5.3")
385 .expect("glm-5.3 entry present");
386 assert_eq!(glm_53.status, ModelStatus::Experimental);
387 assert_eq!(glm_53.rank, 910);
388 assert!(!glm_53.default);
389 assert!(glm_53.primary_agent_eligible);
390 assert_eq!(glm_53.reason, "Delivery conformance not validated");
391
392 let guard = policy
393 .models
394 .iter()
395 .find(|entry| entry.model_id == "@cf/meta/llama-guard-3-8b")
396 .expect("guard entry present");
397 assert_eq!(guard.status, ModelStatus::Hidden);
398 assert!(!guard.primary_agent_eligible);
399 assert_eq!(guard.reason, "Safety classifier.");
400 }
401
402 #[test]
403 fn status_for_unknown_is_available() {
404 let policy = ModelPolicy::default_policy();
405 assert_eq!(policy.status_for("@cf/some-org/unknown-model"), ModelStatus::Available);
406 assert_eq!(policy.status_for(DEFAULT_MODEL), ModelStatus::Recommended);
407 assert_eq!(policy.status_for(EXPERIMENTAL_MODEL), ModelStatus::Experimental);
408 assert_eq!(policy.status_for("@cf/meta/llama-guard-3-8b"), ModelStatus::Hidden);
409 }
410
411 #[test]
412 fn primary_eligibility_follows_entries() {
413 let policy = ModelPolicy::default_policy();
414 assert!(policy.is_primary_agent_eligible(DEFAULT_MODEL));
415 assert!(policy.is_primary_agent_eligible("@cf/openai/gpt-oss-120b"));
416 assert!(policy.is_primary_agent_eligible("@cf/unknown/not-in-policy"));
417 assert!(!policy.is_primary_agent_eligible("@cf/meta/llama-guard-3-8b"));
418 }
419
420 #[test]
421 fn default_model_is_deepseek() {
422 let policy = ModelPolicy::default_policy();
423 assert_eq!(policy.default_model(), Some(DEFAULT_MODEL));
424 }
425
426 #[test]
427 fn policy_roundtrips_serde() {
428 let policy = ModelPolicy::default_policy();
429 let json = serde_json::to_string(&policy).expect("serializes");
430 let back: ModelPolicy = serde_json::from_str(&json).expect("deserializes");
431 assert_eq!(policy, back);
432 let value: serde_json::Value = serde_json::from_str(&json).expect("parses");
433 assert_eq!(value["version"], "1");
434 assert_eq!(value["models"][0]["status"], "recommended");
435 assert_eq!(value["models"][5]["status"], "experimental");
436 assert_eq!(value["models"][6]["status"], "experimental");
437 assert_eq!(value["models"][7]["status"], "hidden");
438 }
439}
440
441#[cfg(test)]
442mod ranking_tests {
443 use super::*;
444 use crate::health::{CONFORMANCE_SUITE_VERSION, VerificationConfidence};
445
446 fn record(id: &str, context: u64, input: f64, output: f64) -> ModelRecord {
449 let entry = serde_json::json!({
450 "id": id,
451 "name": id,
452 "context_length": context,
453 "created": 1_788_800_000,
454 "pricing": {
455 "prompt": format!("{:.8}", input / 1_000_000.0),
456 "completion": format!("{:.8}", output / 1_000_000.0),
457 },
458 });
459 ModelRecord::from_openrouter(&entry).expect("record normalizes")
460 }
461
462 fn verification(
465 status: VerificationStatus,
466 total: u32,
467 successful: u32,
468 multi: Option<f64>,
469 latency: Option<u64>,
470 ) -> ModelVerification {
471 ModelVerification {
472 model_id: "ranking-test".to_string(),
473 latest_run_at: None,
474 expires_at: None,
475 suite_version: CONFORMANCE_SUITE_VERSION.to_string(),
476 runner_version: "test".to_string(),
477 status,
478 agent_eligible: true,
479 confidence: VerificationConfidence::ConformanceTested,
480 total_runs: total,
481 successful_runs: successful,
482 text_completion_success_rate: None,
483 stream_completion_success_rate: None,
484 single_tool_success_rate: Some(0.96),
485 multi_turn_tool_success_rate: multi,
486 structured_output_success_rate: None,
487 median_latency_ms: latency,
488 p95_latency_ms: None,
489 total_failures: total.saturating_sub(successful),
490 timeout_failures: 0,
491 transport_failures: 0,
492 provider_5xx_failures: 0,
493 malformed_response_failures: 0,
494 malformed_tool_call_failures: 0,
495 tool_loop_failures: 0,
496 last_failure: None,
497 }
498 }
499
500 #[test]
501 fn unverified_models_never_rank() {
502 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
503 assert_eq!(ranking_score(&model, None), None);
504 let breakdown = RankingBreakdown::breakdown_for(&model, None);
505 assert_eq!(breakdown.score, None);
506 assert_eq!(breakdown.model_id, DEFAULT_MODEL);
507 }
508
509 #[test]
510 fn non_passing_verification_never_ranks() {
511 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
512 for status in [
513 VerificationStatus::Degraded,
514 VerificationStatus::Failing,
515 VerificationStatus::Untested,
516 VerificationStatus::Expired,
517 ] {
518 let verification = verification(status, 100, 99, Some(0.96), Some(2_000));
519 assert_eq!(ranking_score(&model, Some(&verification)), None, "{status:?} must not rank");
520 }
521 }
522
523 #[test]
524 fn non_primary_agent_models_never_rank() {
525 let model = record("@cf/meta/llama-guard-3-8b", 131_072, 0.05, 0.05);
528 let verification = verification(VerificationStatus::Passing, 100, 100, Some(0.99), Some(1_000));
529 assert_eq!(ranking_score(&model, Some(&verification)), None);
530 }
531
532 #[test]
533 fn passing_deepseek_like_outscores_passing_glm_like_on_tool_loop() {
534 let deepseek = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
535 let glm = record(EXPERIMENTAL_MODEL, 1_310_720, 0.15, 0.5);
536 let ds_verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
537 let glm_verification = verification(VerificationStatus::Passing, 100, 99, Some(0.70), Some(2_000));
538 let ds_score = ranking_score(&deepseek, Some(&ds_verification)).expect("deepseek ranks");
539 let glm_score = ranking_score(&glm, Some(&glm_verification)).expect("glm ranks");
540 assert!(ds_score > glm_score, "deepseek {ds_score} must beat glm {glm_score}");
541 let gap = ds_score - glm_score;
544 assert!((gap - WEIGHT_TOOL_LOOP * 0.26).abs() < 1e-9, "gap {gap}");
545 }
546
547 #[test]
548 fn breakdown_weights_sum_to_one() {
549 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
550 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
551 let breakdown = RankingBreakdown::breakdown_for(&model, Some(&verification));
552 let sum = breakdown.weight_delivery
553 + breakdown.weight_tool_loop
554 + breakdown.weight_context
555 + breakdown.weight_latency
556 + breakdown.weight_price;
557 assert!((sum - 1.0).abs() < 1e-12, "weights must sum to 1.0, got {sum}");
558 assert_eq!(breakdown.score, ranking_score(&model, Some(&verification)));
559 let dot = WEIGHT_DELIVERY * breakdown.delivery
561 + WEIGHT_TOOL_LOOP * breakdown.tool_loop
562 + WEIGHT_CONTEXT * breakdown.context
563 + WEIGHT_LATENCY * breakdown.latency
564 + WEIGHT_PRICE * breakdown.price;
565 assert!((dot - breakdown.score.expect("score present")).abs() < 1e-9);
566 }
567
568 #[test]
569 fn bigger_context_increases_score_other_components_held() {
570 let small = record(DEFAULT_MODEL, 262_144, 0.15, 0.5);
571 let big = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
572 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
573 let small_score = ranking_score(&small, Some(&verification)).expect("small ranks");
574 let big_score = ranking_score(&big, Some(&verification)).expect("big ranks");
575 assert!(big_score > small_score);
576 let capped = record(DEFAULT_MODEL, 2_000_000, 0.15, 0.5);
578 let capped_score = ranking_score(&capped, Some(&verification)).expect("capped ranks");
579 assert!((capped_score - big_score).abs() < 1e-9);
580 }
581
582 #[test]
583 fn lower_latency_increases_score_other_components_held() {
584 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
585 let fast = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
586 let slow = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(30_000));
587 let fast_score = ranking_score(&model, Some(&fast)).expect("fast ranks");
588 let slow_score = ranking_score(&model, Some(&slow)).expect("slow ranks");
589 assert!(fast_score > slow_score);
590 let missing = verification(VerificationStatus::Passing, 100, 99, Some(0.96), None);
592 let missing_score = ranking_score(&model, Some(&missing)).expect("missing-latency ranks");
593 assert!((missing_score - (slow_score - WEIGHT_LATENCY * 0.5)).abs() < 1e-9);
594 let maxed = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(61_000));
596 let maxed_score = ranking_score(&model, Some(&maxed)).expect("maxed-latency ranks");
597 assert!((maxed_score - (slow_score - WEIGHT_LATENCY * 0.5)).abs() < 1e-9);
598 }
599
600 #[test]
601 fn lower_price_increases_score_other_components_held() {
602 let cheap = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
603 let expensive = record(DEFAULT_MODEL, 1_310_720, 10.0, 12.0);
604 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
605 let cheap_score = ranking_score(&cheap, Some(&verification)).expect("cheap ranks");
606 let expensive_score = ranking_score(&expensive, Some(&verification)).expect("expensive ranks");
607 assert!(cheap_score > expensive_score);
608 let no_price = ModelRecord {
610 pricing: crate::catalog::PricingPerMillion { input: None, cached_input: None, output: None },
611 ..cheap.clone()
612 };
613 let no_price_score = ranking_score(&no_price, Some(&verification)).expect("no-price ranks");
614 assert!(no_price_score < cheap_score);
615 let cheap_breakdown = RankingBreakdown::breakdown_for(&cheap, Some(&verification));
616 let no_price_breakdown = RankingBreakdown::breakdown_for(&no_price, Some(&verification));
617 assert!(cheap_breakdown.price > no_price_breakdown.price);
618 assert_eq!(no_price_breakdown.price, 0.0);
619 }
620
621 #[test]
622 fn degraded_but_cheaper_never_ranks() {
623 let model = record(DEFAULT_MODEL, 1_310_720, 0.05, 0.10);
625 let verification = verification(VerificationStatus::Degraded, 100, 60, Some(0.50), Some(1_000));
626 assert_eq!(ranking_score(&model, Some(&verification)), None);
627 }
628
629 #[test]
630 fn breakdown_serde_is_snake_case() {
631 let model = record(DEFAULT_MODEL, 1_310_720, 0.15, 0.5);
632 let verification = verification(VerificationStatus::Passing, 100, 99, Some(0.96), Some(2_000));
633 let breakdown = RankingBreakdown::breakdown_for(&model, Some(&verification));
634 let json = serde_json::to_string(&breakdown).expect("ser");
635 let value: serde_json::Value = serde_json::from_str(&json).expect("parse");
637 for key in [
638 "model_id",
639 "score",
640 "delivery",
641 "tool_loop",
642 "context",
643 "latency",
644 "price",
645 "weight_delivery",
646 "weight_tool_loop",
647 "weight_context",
648 "weight_latency",
649 "weight_price",
650 ] {
651 assert!(value.get(key).is_some(), "missing snake_case key {key}");
652 }
653 let back: RankingBreakdown = serde_json::from_str(&json).expect("de");
656 assert_eq!(back.model_id, breakdown.model_id);
657 assert!(close_f64(back.score, breakdown.score), "score {back:?} vs {breakdown:?}");
658 for (actual, expected) in [
659 (back.delivery, breakdown.delivery),
660 (back.tool_loop, breakdown.tool_loop),
661 (back.context, breakdown.context),
662 (back.latency, breakdown.latency),
663 (back.price, breakdown.price),
664 (back.weight_delivery, breakdown.weight_delivery),
665 (back.weight_tool_loop, breakdown.weight_tool_loop),
666 (back.weight_context, breakdown.weight_context),
667 (back.weight_latency, breakdown.weight_latency),
668 (back.weight_price, breakdown.weight_price),
669 ] {
670 assert!((actual - expected).abs() < 1e-12, "{actual} vs {expected}");
671 }
672 }
673
674 fn close_f64(actual: Option<f64>, expected: Option<f64>) -> bool {
675 match (actual, expected) {
676 (Some(actual), Some(expected)) => (actual - expected).abs() < 1e-12,
677 (None, None) => true,
678 _ => false,
679 }
680 }
681}