Files
DevFlow/crates/df-ai/src/model_probe_helpers.rs
T
lxy 7aa0da32f2 修复: 协议层与体验缺陷六件
- temperature 按模型适配,仅收 1 的模型不再 400
- 流式 error 帧不再被 choices 必填整帧丢弃
- 只读缓存写后失效
- git diff 中文截断撞 UTF-8 边界 panic
- 非视觉模型纯图片直接拒绝
- 全局快捷键补输入焦点守卫
2026-10-04 16:27:31 +08:00

133 lines
6.2 KiB
Rust

//! 模型探测器 — 纯逻辑子模块。
//!
//! 从 `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<Vec<ModelConfig>> = OnceLock::new();
PRESETS.get_or_init(|| {
// include_str! 内容由仓库控制,解析失败属编译期/仓库错误,panic 合理。
serde_json::from_str::<Vec<ModelConfig>>(PRESETS_JSON)
.expect("presets/models.json 解析失败 — 检查 JSON 格式与 ModelConfig serde 映射")
})
}
// ────────────────────────────────────────────────────────────
// 启发式推断(模型名命名模式 → 4 维度)
// ────────────────────────────────────────────────────────────
/// 模型名启发式推断。仅推断功能性维度(modalities/capabilities),
/// cost_tier / intelligence 一律返中性默认(Medium / Standard)。
///
/// 取舍:模型名关键词猜档位(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<Modality> = Vec::new();
let mut capabilities: Vec<Capability> = Vec::new();
// 中性默认:不靠模型名猜档位
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,
temperature_supported: temperature_supported_by_name(&name),
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,
temperature_supported: temperature_supported_by_name(&name),
probe_source: None,
}
}
// —— 词素判定助手(命名约定式启发式,集中维护便于增删) ——
/// Vision 词素:`vision` / `vl` 子串、`v` 词素尾(任一分隔段以 v 结尾:glm-4v / qwen-v-plus)。
/// 裸 "v" 必须是词素尾:`contains("-v")` 会把版本号误判成视觉(deepseek-v3 纯文本,
/// "-v" 后接数字属版本号而非视觉词素),按段尾判定后 v3/v2 不命中、4v/v 段仍命中。
pub(super) fn has_vision_token(name: &str) -> bool {
name.contains("vision")
|| name.contains("vl")
|| name.split(['-', '.', '_']).any(|seg| seg.ends_with('v'))
}
/// 温度参数支持判定(命名约定式):商汤 sensenova 系列仅接受 temperature=1.0,
/// 透传其它温度值(标题 0.3/压缩 0.2 等)→ 上游 HTTP 400(实测 9 次)。
/// 返回 false 时协议转换层省略 temperature 字段(交上游默认),避免无效往返;
/// 其余模型默认 true(原样透传,零回归)。
pub(super) fn temperature_supported_by_name(name: &str) -> bool {
!name.starts_with("sensenova")
}