1use anyhow::{Context, Result, ensure};
2use serde::{Deserialize, Serialize};
3
4#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Deserialize, Serialize, Default)]
7pub struct ResourceBudget {
8 #[serde(default)]
10 pub max_tokens: Option<usize>,
11 #[serde(default)]
13 pub max_parallel_tools: Option<usize>,
14 #[serde(default)]
16 pub latency_ms_target: Option<u64>,
17}
18
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[derive(Debug, Clone, Deserialize, Serialize, Default)]
22pub struct ComplexityModelMap {
23 #[serde(default)]
25 pub simple: String,
26 #[serde(default)]
28 pub standard: String,
29 #[serde(default)]
31 pub complex: String,
32 #[serde(default)]
34 pub codegen_heavy: String,
35 #[serde(default)]
37 pub retrieval_heavy: String,
38}
39
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct HeuristicSettings {
44 #[serde(default = "default_short_request_max_chars")]
46 pub short_request_max_chars: usize,
47 #[serde(default = "default_long_request_min_chars")]
49 pub long_request_min_chars: usize,
50 #[serde(default = "default_code_patch_markers")]
52 pub code_patch_markers: Vec<String>,
53 #[serde(default = "default_retrieval_markers")]
55 pub retrieval_markers: Vec<String>,
56 #[serde(default = "default_complex_markers")]
58 pub complex_markers: Vec<String>,
59}
60
61impl Default for HeuristicSettings {
62 fn default() -> Self {
63 Self {
64 short_request_max_chars: default_short_request_max_chars(),
65 long_request_min_chars: default_long_request_min_chars(),
66 code_patch_markers: default_code_patch_markers(),
67 retrieval_markers: default_retrieval_markers(),
68 complex_markers: default_complex_markers(),
69 }
70 }
71}
72
73impl HeuristicSettings {
74 pub fn validate(&self) -> Result<()> {
75 ensure!(
76 self.long_request_min_chars > self.short_request_max_chars,
77 "Router heuristic long_request_min_chars must be greater than short_request_max_chars"
78 );
79
80 ensure!(
81 self.code_patch_markers
82 .iter()
83 .all(|marker| !marker.trim().is_empty()),
84 "Router heuristic code_patch_markers must not contain empty entries"
85 );
86 ensure!(
87 self.retrieval_markers
88 .iter()
89 .all(|marker| !marker.trim().is_empty()),
90 "Router heuristic retrieval_markers must not contain empty entries"
91 );
92 ensure!(
93 self.complex_markers
94 .iter()
95 .all(|marker| !marker.trim().is_empty()),
96 "Router heuristic complex_markers must not contain empty entries"
97 );
98
99 Ok(())
100 }
101}
102
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct RouterConfig {
107 #[serde(default = "default_enabled")]
109 pub enabled: bool,
110 #[serde(default = "default_true")]
112 pub heuristic_classification: bool,
113 #[serde(default)]
115 pub llm_router_model: String,
116 #[serde(default)]
118 pub models: ComplexityModelMap,
119 #[serde(default)]
121 pub budgets: std::collections::HashMap<String, ResourceBudget>,
122 #[serde(default)]
124 pub heuristics: HeuristicSettings,
125}
126
127impl Default for RouterConfig {
128 fn default() -> Self {
129 use crate::constants::models;
130 Self {
131 enabled: true,
132 heuristic_classification: true,
133 llm_router_model: String::new(),
134 models: ComplexityModelMap {
135 simple: models::google::GEMINI_2_5_FLASH_PREVIEW.to_string(),
136 standard: models::google::GEMINI_2_5_FLASH_PREVIEW.to_string(),
137 complex: models::google::GEMINI_2_5_PRO.to_string(),
138 codegen_heavy: models::google::GEMINI_2_5_PRO.to_string(),
139 retrieval_heavy: models::google::GEMINI_2_5_PRO.to_string(),
140 },
141 budgets: Default::default(),
142 heuristics: HeuristicSettings::default(),
143 }
144 }
145}
146
147impl RouterConfig {
148 pub fn validate(&self) -> Result<()> {
149 if !self.enabled {
150 return Ok(());
151 }
152
153 self.heuristics
154 .validate()
155 .context("Invalid router heuristics")?;
156
157 ensure!(
158 !self.models.simple.trim().is_empty(),
159 "Router models.simple must not be empty"
160 );
161 ensure!(
162 !self.models.standard.trim().is_empty(),
163 "Router models.standard must not be empty"
164 );
165 ensure!(
166 !self.models.complex.trim().is_empty(),
167 "Router models.complex must not be empty"
168 );
169 ensure!(
170 !self.models.codegen_heavy.trim().is_empty(),
171 "Router models.codegen_heavy must not be empty"
172 );
173 ensure!(
174 !self.models.retrieval_heavy.trim().is_empty(),
175 "Router models.retrieval_heavy must not be empty"
176 );
177
178 Ok(())
179 }
180}
181
182fn default_true() -> bool {
183 true
184}
185fn default_enabled() -> bool {
186 true
187}
188
189fn default_short_request_max_chars() -> usize {
190 120
191}
192
193fn default_long_request_min_chars() -> usize {
194 1200
195}
196
197fn default_code_patch_markers() -> Vec<String> {
198 vec![
199 "```".to_string(),
200 "diff --git".to_string(),
201 "apply_patch".to_string(),
202 "unified diff".to_string(),
203 "patch".to_string(),
204 "edit_file".to_string(),
205 "create_file".to_string(),
206 ]
207}
208
209fn default_retrieval_markers() -> Vec<String> {
210 vec![
211 "search".to_string(),
212 "web".to_string(),
213 "google".to_string(),
214 "docs".to_string(),
215 "cite".to_string(),
216 "source".to_string(),
217 "up-to-date".to_string(),
218 ]
219}
220
221fn default_complex_markers() -> Vec<String> {
222 vec![
223 "plan".to_string(),
224 "multi-step".to_string(),
225 "decompose".to_string(),
226 "orchestrate".to_string(),
227 "architecture".to_string(),
228 "benchmark".to_string(),
229 "implement end-to-end".to_string(),
230 "design api".to_string(),
231 "refactor module".to_string(),
232 "evaluate".to_string(),
233 "tests suite".to_string(),
234 ]
235}