返回 CodeWhale
auto_model.rs
根目录 / crates / config / src / auto_model.rs
1 //! Legacy DeepSeek-scoped prompt complexity classifier.
2 //!
3 //! This pure scorer is retained for API compatibility, but the consolidated
4 //! CLI dispatcher must not use it to resolve provider-neutral `model = "auto"`:
5 //! doing so fabricates DeepSeek model ids for every active provider. The TUI's
6 //! provider-aware router owns runtime auto selection. Callers may use this
7 //! helper only when their candidate pair is explicitly the DeepSeek pair:
8 //!
9 //! - **`deepseek-v4-pro`** — complex tasks (debugging, refactoring, design,
10 //! security review, multi-file changes, code generation, …).
11 //! - **`deepseek-v4-flash`** — simple tasks (lookups, formatting, small edits,
12 //! translation, Q&A, …).
13 //!
14 //! This is a pure rule-based classifier. It lives in the config crate because
15 //! the resolved model name is a config-level concern; the route resolver never
16 //! sees the `"auto"` sentinel or the prompt text.
17
18 /// The resolved model name for the pro tier.
19 pub const PRO_MODEL: &str = "deepseek-v4-pro";
20
21 /// The resolved model name for the flash tier.
22 pub const FLASH_MODEL: &str = "deepseek-v4-flash";
23
24 /// The threshold score above which a task is classified as complex (pro).
25 /// Score ≥ 2 → pro, else → flash.
26 const PRO_THRESHOLD: i32 = 2;
27
28 /// Strong indicators of a complex task. Each match adds +3.
29 const COMPLEX_STRONG: &[&str] = &[
30 // Debugging & fixing
31 "debug",
32 "bug",
33 "fix",
34 "error",
35 "crash",
36 "异常",
37 "错误",
38 "调试",
39 "故障",
40 "排查",
41 "root cause",
42 // Architecture & design
43 "refactor",
44 "重构",
45 "architecture",
46 "架构",
47 "design pattern",
48 "系统设计",
49 "高并发",
50 "分布式",
51 "microservice",
52 // Security
53 "security",
54 "安全",
55 "vulnerability",
56 "漏洞",
57 "渗透",
58 "exploit",
59 // Code generation
60 "implement",
61 "实现",
62 "generate",
63 "生成",
64 "create",
65 "创建",
66 "build",
67 "构建",
68 "开发",
69 "prototype",
70 // Complex analysis
71 "analyze",
72 "分析",
73 "review",
74 "审查",
75 "audit",
76 "审计",
77 "optimize",
78 "优化",
79 "migrate",
80 "迁移",
81 // Multi-file / large scale
82 "multi-file",
83 "multiple files",
84 "多个文件",
85 "整个项目",
86 "full project",
87 "重构整个",
88 "large scale",
89 // Testing
90 "unit test",
91 "integration test",
92 "e2e test",
93 "测试用例",
94 "test suite",
95 "coverage",
96 // Complex logic
97 "algorithm",
98 "算法",
99 "状态机",
100 "state machine",
101 "concurrent",
102 "并行",
103 "异步",
104 "async",
105 // Documentation / PRD
106 "architecture document",
107 "设计文档",
108 "技术方案",
109 "prd",
110 ];
111
112 /// Medium-strength indicators. Each match adds +1.
113 const COMPLEX_MEDIUM: &[&str] = &[
114 "change",
115 "修改",
116 "update",
117 "更新",
118 "add",
119 "添加",
120 "新增",
121 "feature",
122 "功能",
123 "improve",
124 "改进",
125 "enhance",
126 "config",
127 "配置",
128 "setup",
129 "设置",
130 "deploy",
131 "部署",
132 "ci/cd",
133 "pipeline",
134 "script",
135 "脚本",
136 "tool",
137 "工具",
138 "api",
139 "interface",
140 "接口",
141 "endpoint",
142 "database",
143 "数据库",
144 "schema",
145 "query",
146 "document",
147 "文档",
148 "readme",
149 ];
150
151 /// Simple-task indicators. Each match subtracts -1.
152 const SIMPLE: &[&str] = &[
153 "find",
154 "查找",
155 "search",
156 "搜索",
157 "look up",
158 "查询",
159 "what is",
160 "什么是",
161 "explain",
162 "解释",
163 "tell me",
164 "告诉我",
165 "how to",
166 "如何",
167 "format",
168 "格式化",
169 "pretty",
170 "list",
171 "列出",
172 "show",
173 "显示",
174 "print",
175 "rename",
176 "重命名",
177 "move",
178 "移动",
179 "copy",
180 "复制",
181 "delete",
182 "删除",
183 "remove",
184 "typo",
185 "拼写",
186 "spelling",
187 "grammar",
188 "quick",
189 "快速",
190 "simple",
191 "简单",
192 "hello world",
193 "demo",
194 "example",
195 "示例",
196 "translate",
197 "翻译",
198 "convert",
199 "转换",
200 "short",
201 "简短",
202 "brief",
203 "简要",
204 ];
205
206 /// Classify a prompt for the legacy DeepSeek candidate pair.
207 ///
208 /// Uses a simple scoring system:
209 /// - Strong complex keyword: +3
210 /// - Medium complex keyword: +1
211 /// - Simple keyword: -1
212 /// - Prompt length > 500 chars: +2, > 200 chars: +1
213 /// - Contains code fence or backtick: +1
214 /// - Contains a file path: +1
215 /// - Multi-line (> 5 newlines): +1
216 ///
217 /// Total ≥ 2 → `PRO_MODEL`, else → `FLASH_MODEL`.
218 #[must_use]
219 pub fn classify(prompt: &str) -> &'static str {
220 if score(prompt) >= PRO_THRESHOLD {
221 PRO_MODEL
222 } else {
223 FLASH_MODEL
224 }
225 }
226
227 /// Compute the raw complexity score for a prompt.
228 #[must_use]
229 pub fn score(prompt: &str) -> i32 {
230 let lower = prompt.to_ascii_lowercase();
231 let mut score = 0i32;
232
233 // Strong complex keywords: +3 (first match only to avoid overcounting)
234 if COMPLEX_STRONG.iter().any(|kw| lower.contains(kw)) {
235 score += 3;
236 }
237
238 // Medium complex keywords: +1 each
239 for kw in COMPLEX_MEDIUM {
240 if lower.contains(kw) {
241 score += 1;
242 }
243 }
244
245 // Simple keywords: -1 each
246 for kw in SIMPLE {
247 if lower.contains(kw) {
248 score -= 1;
249 }
250 }
251
252 // Length factor: long prompts tend to be more complex.
253 //
254 // Counted in characters, not bytes, as the doc comment above states. Half
255 // the keyword lists here are Chinese, so CJK input is a first-class case —
256 // and every CJK character is three UTF-8 bytes, which made `prompt.len()`
257 // award the long-prompt bonus at a third of the documented length. A
258 // 200-character Chinese prompt scored +2 (600 bytes) and classified as
259 // complex, where the same-length English prompt scored 0.
260 let len = prompt.chars().count();
261 if len > 500 {
262 score += 2;
263 } else if len > 200 {
264 score += 1;
265 }
266
267 // Code fence or backtick: actual coding task
268 if prompt.contains("```") || prompt.contains('`') {
269 score += 1;
270 }
271
272 // File path pattern: e.g. /path/to/file.rs or C:\path
273 // Simple heuristic: path-like sequences contain / or \ and .
274 if (prompt.contains('/') || prompt.contains('\\')) && prompt.contains('.') {
275 score += 1;
276 }
277
278 // Multi-line: more lines = more context
279 if prompt.chars().filter(|&c| c == '\n').count() > 5 {
280 score += 1;
281 }
282
283 score
284 }
285
286 #[cfg(test)]
287 mod tests {
288 use super::*;
289
290 #[test]
291 fn test_debug_task_uses_pro() {
292 assert_eq!(classify("帮我调试这个bug,程序崩溃了"), PRO_MODEL);
293 }
294
295 #[test]
296 fn test_refactor_task_uses_pro() {
297 assert_eq!(
298 classify("refactor the user module with a new architecture"),
299 PRO_MODEL
300 );
301 }
302
303 #[test]
304 fn test_security_review_uses_pro() {
305 assert_eq!(
306 classify("review this code for security vulnerabilities"),
307 PRO_MODEL
308 );
309 }
310
311 #[test]
312 fn test_simple_lookup_uses_flash() {
313 assert_eq!(classify("查找昨天的日志文件"), FLASH_MODEL);
314 }
315
316 #[test]
317 fn test_translation_uses_flash() {
318 assert_eq!(classify("translate this to Chinese"), FLASH_MODEL);
319 }
320
321 #[test]
322 fn test_formatting_uses_flash() {
323 assert_eq!(classify("format this code"), FLASH_MODEL);
324 }
325
326 #[test]
327 fn test_long_prompt_gets_bonus() {
328 let long = "a".repeat(300);
329 // No keywords, long prompt gives +1, total = 1 < 2 → flash
330 assert_eq!(classify(&long), FLASH_MODEL);
331 }
332
333 #[test]
334 fn test_very_long_prompt_gets_more_bonus() {
335 let long = "a".repeat(600);
336 // No keywords, very long prompt gives +2, total = 2 → pro
337 assert_eq!(classify(&long), PRO_MODEL);
338 }
339
340 #[test]
341 fn test_code_block_gets_bonus() {
342 // Code block without keywords, +1, total = 1 < 2 → flash
343 assert_eq!(classify("```\nhello\n```"), FLASH_MODEL);
344 }
345
346 #[test]
347 fn test_mixed_keywords_pro_wins() {
348 // "refactor" is strong (+3), "explain" is simple (-1), total = 2 → pro
349 assert_eq!(classify("refactor and explain the code"), PRO_MODEL);
350 }
351
352 #[test]
353 fn test_implement_task_uses_pro() {
354 assert_eq!(
355 classify("implement a new feature for the user module"),
356 PRO_MODEL
357 );
358 }
359
360 #[test]
361 fn test_quick_question_uses_flash() {
362 assert_eq!(classify("what is the capital of France?"), FLASH_MODEL);
363 }
364
365 #[test]
366 fn length_bonus_counts_characters_not_utf8_bytes() {
367 // Keyword-free prompts of identical *length* must score identically
368 // regardless of script. "啊" is three UTF-8 bytes, so a byte-counted
369 // length factor gave the Chinese prompt a bonus the English one did
370 // not earn — and at 200 characters it flipped the classification.
371 let english = "a".repeat(200);
372 let chinese = "啊".repeat(200);
373 assert_eq!(score(&chinese), score(&english));
374 assert_eq!(classify(&chinese), FLASH_MODEL);
375
376 let english_long = "a".repeat(600);
377 let chinese_long = "啊".repeat(600);
378 assert_eq!(score(&chinese_long), score(&english_long));
379 assert_eq!(classify(&chinese_long), PRO_MODEL);
380 }
381
382 #[test]
383 fn test_score_never_negative() {
384 // Even for very simple queries, score should be predictable
385 let s = score("hello world");
386 assert!(s >= -10); // sanity check
387 }
388 }
389
389 lines RUST