//! 模型探测器 — 纯逻辑子模块(F-01 阶段2) //! //! 从 `model_probe.rs` 抽离的纯函数实现(预设表加载 / 启发式推断 / 词素判定)。 //! 无 IO、无外部状态,crate 内经 `model_probe::probe` 间接复用 — //! 对齐 `model_fetch_helpers` 拆分先例(private mod,外部零直接路径需求)。 //! //! 设计来源:docs/02-架构设计/已编号方案/F-01-模型能力系统与智能路由设计-2026-06-16.md §4 use std::sync::OnceLock; use df_ai_core::model::{ Capability, CostTier, IntelligenceTier, Modality, ModelConfig, }; // ──────────────────────────────────────────────────────────── // 预设表:编译期嵌入(include_str! 相对 crate 根),零运行时文件依赖 // ──────────────────────────────────────────────────────────── /// 预设表原始 JSON(编译期从 `crates/df-ai/presets/models.json` 嵌入)。 pub(super) const PRESETS_JSON: &str = include_str!("../presets/models.json"); /// 解析后的预设表(进程内单例,首次访问惰性解析一次)。 pub(super) fn presets() -> &'static [ModelConfig] { static PRESETS: OnceLock> = OnceLock::new(); PRESETS.get_or_init(|| { // include_str! 内容由仓库控制,解析失败属编译期/仓库错误,panic 合理。 serde_json::from_str::>(PRESETS_JSON) .expect("presets/models.json 解析失败 — 检查 JSON 格式与 ModelConfig serde 映射") }) } // ──────────────────────────────────────────────────────────── // 启发式推断(模型名命名模式 → 4 维度) // ──────────────────────────────────────────────────────────── /// 模型名启发式推断。仅推断功能性维度(modalities/capabilities), /// cost_tier / intelligence 一律返中性默认(Medium / Standard)。 /// /// 取舍:B-260618-04 — 模型名关键词猜档位(flash→Lite、4o→High、pro→Plus)无依据, /// 厂商定价/智力与命名无关,瞎填会污染路由器过滤(intelligence >= min / cost_tier <= max)。 /// 改中性默认,真实档位由用户手填或更高阶探测源(如厂商 API/定价表)提供。 /// /// 规则(任务规格,设计文档 §4.4 模糊匹配规则对齐): /// - `embed`/`embedding`/`e-`(前缀) → Embedding 模型:capabilities=[Embedding],modalities=[](去 Text) /// - `v`/`vision`/`vl`(词素) → modalities 加 Vision /// - `code`/`coder` → capabilities 加 CodeGen /// - cost_tier / intelligence → 中性默认 Medium / Standard(不靠名字猜) pub(super) fn heuristic_infer(model_id: &str) -> ModelConfig { let name = model_id.to_lowercase(); let mut modalities: Vec = Vec::new(); let mut capabilities: Vec = Vec::new(); // 中性默认:不靠模型名猜档位(B-260618-04) let cost_tier = CostTier::Medium; let intelligence = IntelligenceTier::Standard; // —— Embedding 优先判定(改变模态集合,且通常独占) —— // 规则:含 embed / embedding / 以 "e-" / "embedding-" 起首 let is_embedding = name.contains("embed") || name.contains("embedding") || name.starts_with("e-") || name.starts_with("embedding-") || name.starts_with("text-embedding"); if is_embedding { capabilities.push(Capability::Embedding); // embedding 模型不走对话模态,modalities 留空(设计 §4.4 embedding-3 例:modalities=[]) return ModelConfig { model_id: model_id.to_string(), enabled: true, label: None, modalities, capabilities, cost_tier, intelligence, weight: 50, context_window: 8192, probe_source: None, }; } // —— 对话模型默认 Text 模态 —— modalities.push(Modality::Text); // —— Vision 词素:含 "v"(独立词素:glm-4v / qwen-vl)、"vision"、"vl" —— // 注:裸 "v" 子串误伤大(如 "review"),故仅在边界词素命中: // 以 "v" 结尾、含 "-v" / "v-" 分隔、含 "vl" / "vision" if has_vision_token(&name) { modalities.push(Modality::Vision); } // —— CodeGen:含 code / coder —— if name.contains("code") || name.contains("coder") { capabilities.push(Capability::CodeGen); } // —— 对话模型默认 ToolUse(主流支持 function calling) —— capabilities.push(Capability::ToolUse); ModelConfig { model_id: model_id.to_string(), enabled: true, label: None, modalities, capabilities, cost_tier, intelligence, weight: 50, context_window: 8192, probe_source: None, } } // —— 词素判定助手(命名约定式启发式,集中维护便于增删) —— /// Vision 词素:`-v`(尾缀如 glm-4v)、以 `v` 结尾、含 `vl` / `vision`。 /// 不直接 `contains('v')`(避免误伤 review/verbal 等)。 pub(super) fn has_vision_token(name: &str) -> bool { name.contains("vision") || name.contains("vl") || name.ends_with('v') || name.contains("-v") }