1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ModelTier {
15 Speculator,
16 Verifier,
17 Actuator,
18 Architect,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum AgentPhase {
25 Explore,
26 Plan,
27 Implement,
28 Verify,
29 Repair,
30 Review,
31 Research,
32}
33
34impl AgentPhase {
35 pub fn default_tier(self) -> ModelTier {
37 match self {
38 AgentPhase::Explore | AgentPhase::Research => ModelTier::Speculator,
39 AgentPhase::Verify | AgentPhase::Review => ModelTier::Verifier,
40 AgentPhase::Implement | AgentPhase::Repair => ModelTier::Actuator,
41 AgentPhase::Plan => ModelTier::Architect,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
48pub struct ModelBudget {
49 pub max_tokens: u64,
50 pub max_calls: u32,
51 pub max_wall_clock_secs: u64,
52}
53
54impl Default for ModelBudget {
55 fn default() -> Self {
56 Self {
57 max_tokens: 100_000,
58 max_calls: 50,
59 max_wall_clock_secs: 600,
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct ModelTierConfig {
67 pub speculator_model: String,
68 pub verifier_model: String,
69 pub actuator_model: String,
70 pub architect_model: String,
71 pub explorer_model: Option<String>,
73 pub explorer_tier: Option<ModelTier>,
75}
76
77impl ModelTierConfig {
78 pub fn uniform(model: impl Into<String>) -> Self {
80 let m = model.into();
81 Self {
82 speculator_model: m.clone(),
83 verifier_model: m.clone(),
84 actuator_model: m.clone(),
85 architect_model: m,
86 explorer_model: None,
87 explorer_tier: None,
88 }
89 }
90
91 pub fn model_for_tier(&self, tier: ModelTier) -> &str {
92 match tier {
93 ModelTier::Speculator => &self.speculator_model,
94 ModelTier::Verifier => &self.verifier_model,
95 ModelTier::Actuator => &self.actuator_model,
96 ModelTier::Architect => &self.architect_model,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub struct ModelRoute {
104 pub phase: AgentPhase,
105 pub requested_tier: Option<ModelTier>,
106 pub resolved_tier: ModelTier,
107 pub model: String,
108 pub fallback_model: Option<String>,
109 pub budget: ModelBudget,
110 pub reason: String,
111}
112
113pub fn resolve_route(
121 phase: AgentPhase,
122 config: &ModelTierConfig,
123 requested_tier: Option<ModelTier>,
124 budget: ModelBudget,
125) -> ModelRoute {
126 if let Some(tier) = requested_tier {
128 return ModelRoute {
129 phase,
130 requested_tier,
131 resolved_tier: tier,
132 model: config.model_for_tier(tier).to_string(),
133 fallback_model: Some(config.speculator_model.clone()),
134 budget,
135 reason: "explicit tier override".into(),
136 };
137 }
138
139 if phase == AgentPhase::Explore {
141 if let Some(model) = &config.explorer_model {
142 let tier = config.explorer_tier.unwrap_or(ModelTier::Speculator);
143 return ModelRoute {
144 phase,
145 requested_tier: None,
146 resolved_tier: tier,
147 model: model.clone(),
148 fallback_model: Some(config.speculator_model.clone()),
149 budget,
150 reason: "explorer_model override".into(),
151 };
152 }
153 if let Some(tier) = config.explorer_tier {
154 return ModelRoute {
155 phase,
156 requested_tier: None,
157 resolved_tier: tier,
158 model: config.model_for_tier(tier).to_string(),
159 fallback_model: Some(config.speculator_model.clone()),
160 budget,
161 reason: "explorer_tier override".into(),
162 };
163 }
164 }
165
166 let tier = phase.default_tier();
167 ModelRoute {
168 phase,
169 requested_tier: None,
170 resolved_tier: tier,
171 model: config.model_for_tier(tier).to_string(),
172 fallback_model: Some(config.speculator_model.clone()),
173 budget,
174 reason: "phase default tier".into(),
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn config() -> ModelTierConfig {
183 ModelTierConfig {
184 speculator_model: "spec-1".into(),
185 verifier_model: "verif-1".into(),
186 actuator_model: "act-1".into(),
187 architect_model: "arch-1".into(),
188 explorer_model: None,
189 explorer_tier: None,
190 }
191 }
192
193 #[test]
194 fn explore_defaults_to_speculator() {
195 let route = resolve_route(AgentPhase::Explore, &config(), None, ModelBudget::default());
196 assert_eq!(route.resolved_tier, ModelTier::Speculator);
197 assert_eq!(route.model, "spec-1");
198 }
199
200 #[test]
201 fn explorer_model_wins_over_speculator() {
202 let mut config = config();
203 config.explorer_model = Some("cheap-explorer".into());
204 let route = resolve_route(AgentPhase::Explore, &config, None, ModelBudget::default());
205 assert_eq!(route.model, "cheap-explorer");
206 assert_eq!(route.reason, "explorer_model override");
207 }
208
209 #[test]
210 fn explorer_tier_reuses_existing_tier_model() {
211 let mut config = config();
212 config.explorer_tier = Some(ModelTier::Verifier);
213 let route = resolve_route(AgentPhase::Explore, &config, None, ModelBudget::default());
214 assert_eq!(route.resolved_tier, ModelTier::Verifier);
215 assert_eq!(route.model, "verif-1");
216 }
217
218 #[test]
219 fn plan_routes_to_architect() {
220 let route = resolve_route(AgentPhase::Plan, &config(), None, ModelBudget::default());
221 assert_eq!(route.resolved_tier, ModelTier::Architect);
222 assert_eq!(route.model, "arch-1");
223 }
224
225 #[test]
226 fn explicit_tier_override_beats_phase_default() {
227 let route = resolve_route(
228 AgentPhase::Implement,
229 &config(),
230 Some(ModelTier::Speculator),
231 ModelBudget::default(),
232 );
233 assert_eq!(route.resolved_tier, ModelTier::Speculator);
234 }
235
236 #[test]
237 fn uniform_sets_all_tiers() {
238 let config = ModelTierConfig::uniform("one-model");
239 for phase in [
240 AgentPhase::Explore,
241 AgentPhase::Plan,
242 AgentPhase::Implement,
243 AgentPhase::Verify,
244 ] {
245 let route = resolve_route(phase, &config, None, ModelBudget::default());
246 assert_eq!(route.model, "one-model");
247 }
248 }
249}