switchyard_libsy/algorithms/vgr/
config.rs1use std::time::Duration;
5
6use switchyard_protocol::ModelId;
7
8use super::safety::{BreakerConfig, KillSwitch};
9use crate::{LibsyError, Result};
10
11pub const ACTIVE_APPROVAL: &str = "prospective-validation-and-canary-approved";
13
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
16pub enum ServingMode {
17 #[default]
19 Off,
20 Evaluate,
22 Shadow,
24 Active {
26 approval: String,
28 },
29}
30
31#[derive(Clone, Debug)]
33pub struct Targets {
34 pub local: ModelId,
36 pub cloud: ModelId,
38 pub judge: Option<ModelId>,
40 pub cloud_judge: Option<ModelId>,
42}
43
44#[derive(Clone, Debug)]
46pub struct VgrConfig {
47 pub targets: Targets,
49 pub mode: ServingMode,
51 pub deadline: Duration,
53 pub kill_switch: Option<KillSwitch>,
55 pub breaker: BreakerConfig,
57 pub task_typing: bool,
59 pub local_supports_images: bool,
61 pub confirmed_recovery_min_clean_tail: Option<u32>,
64 pub agentic_handoff: bool,
67 pub local_turn_budget: Option<Duration>,
70 pub latch_escalation: bool,
73}
74
75impl VgrConfig {
76 pub fn new(local: ModelId, cloud: ModelId) -> Self {
78 Self {
79 targets: Targets {
80 local,
81 cloud,
82 judge: None,
83 cloud_judge: None,
84 },
85 mode: ServingMode::Off,
86 deadline: Duration::from_secs(30),
87 kill_switch: None,
88 breaker: BreakerConfig::default(),
89 task_typing: true,
90 local_supports_images: false,
91 confirmed_recovery_min_clean_tail: None,
92 agentic_handoff: false,
93 local_turn_budget: None,
94 latch_escalation: false,
95 }
96 }
97
98 pub(super) fn validate(&self) -> Result<()> {
99 if self.targets.local == self.targets.cloud {
100 return Err(LibsyError::AlgorithmError {
101 message: "vgr requires distinct local and cloud model IDs".into(),
102 });
103 }
104 if self.deadline.is_zero() || self.breaker.threshold == 0 {
105 return Err(LibsyError::AlgorithmError {
106 message: "vgr deadline and breaker threshold must be non-zero".into(),
107 });
108 }
109 if let ServingMode::Active { approval } = &self.mode
110 && approval != ACTIVE_APPROVAL
111 {
112 return Err(LibsyError::AlgorithmError {
113 message: format!(
114 "vgr active mode requires approval attestation {ACTIVE_APPROVAL:?}"
115 ),
116 });
117 }
118 Ok(())
119 }
120
121 pub(super) fn judge(&self) -> &ModelId {
122 self.targets.judge.as_ref().unwrap_or(&self.targets.local)
123 }
124
125 pub(super) fn confirmed_min_clean_tail(&self) -> Option<i32> {
126 self.confirmed_recovery_min_clean_tail
127 .map(|tail| i32::try_from(tail.max(1)).unwrap_or(i32::MAX))
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn direct_callers_must_use_distinct_local_and_cloud_ids() {
137 assert!(super::super::Vgr::new(VgrConfig::new("same".into(), "same".into())).is_err());
138 assert!(super::super::Vgr::new(VgrConfig::new("local".into(), "cloud".into())).is_ok());
139 }
140}