vtcode_config/
router.rs

1use anyhow::{Context, Result, ensure};
2use serde::{Deserialize, Serialize};
3
4/// Budget awareness for routing decisions
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Deserialize, Serialize, Default)]
7pub struct ResourceBudget {
8    /// Max tokens per request (soft cap)
9    #[serde(default)]
10    pub max_tokens: Option<usize>,
11    /// Maximum parallel tool calls allowed
12    #[serde(default)]
13    pub max_parallel_tools: Option<usize>,
14    /// Max latency target in milliseconds (advisory)
15    #[serde(default)]
16    pub latency_ms_target: Option<u64>,
17}
18
19/// Map from a complexity label to a model identifier
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[derive(Debug, Clone, Deserialize, Serialize, Default)]
22pub struct ComplexityModelMap {
23    /// Simple, quick tasks
24    #[serde(default)]
25    pub simple: String,
26    /// Standard single-turn tasks
27    #[serde(default)]
28    pub standard: String,
29    /// Complex, multi-step reasoning
30    #[serde(default)]
31    pub complex: String,
32    /// Code-generation heavy tasks (diffs, patches)
33    #[serde(default)]
34    pub codegen_heavy: String,
35    /// Retrieval/search heavy tasks
36    #[serde(default)]
37    pub retrieval_heavy: String,
38}
39
40/// Tunable thresholds for heuristic task classification
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[derive(Debug, Clone, Deserialize, Serialize)]
43pub struct HeuristicSettings {
44    /// Maximum characters treated as a "simple" request
45    #[serde(default = "default_short_request_max_chars")]
46    pub short_request_max_chars: usize,
47    /// Minimum characters before we assume a complex request
48    #[serde(default = "default_long_request_min_chars")]
49    pub long_request_min_chars: usize,
50    /// Indicators that the request contains code or patch operations
51    #[serde(default = "default_code_patch_markers")]
52    pub code_patch_markers: Vec<String>,
53    /// Indicators that the request is retrieval or search heavy
54    #[serde(default = "default_retrieval_markers")]
55    pub retrieval_markers: Vec<String>,
56    /// Indicators that the request is complex or multi-step
57    #[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/// Router configuration for dynamic model/engine selection
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct RouterConfig {
107    /// Enable router decisions for chat/ask commands
108    #[serde(default = "default_enabled")]
109    pub enabled: bool,
110    /// Use heuristics to classify complexity (no extra LLM call)
111    #[serde(default = "default_true")]
112    pub heuristic_classification: bool,
113    /// Optional: allow an LLM-based router step
114    #[serde(default)]
115    pub llm_router_model: String,
116    /// Model mapping per complexity class
117    #[serde(default)]
118    pub models: ComplexityModelMap,
119    /// Budgets used to guide generation parameters per class
120    #[serde(default)]
121    pub budgets: std::collections::HashMap<String, ResourceBudget>,
122    /// Heuristic classification configuration
123    #[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}