//! 模型能力数据模型 — F-01 阶段1 //! //! 单模型的完整描述(4 维度 + 路由控制)。纯数据结构,零 IO / 零 DB 依赖。 //! df-storage 反序列化 DB 行时直接消费本模块类型;df-ai 探测器/路由器阶段 2-4 再用。 //! //! 设计来源:docs/02-架构设计/已编号方案/F-01-模型能力系统与智能路由设计-2026-06-16.md §2 use serde::{Deserialize, Serialize}; // ============================================================ // 4 维度枚举 // ============================================================ /// 维度 1 — 模态(模型能接收什么输入) /// /// 设计文档 §2.2 仅列出 Text/Vision,Audio/Video 为未来扩展占位。 /// 序列化为 snake_case(`text` / `vision` / `audio` / `video`)。 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum Modality { Text, Vision, /// 未来扩展:音频输入(预留,Phase 2 多模态) Audio, /// 未来扩展:视频输入(预留) Video, } /// 维度 2 — 能力(模型能做什么) /// /// 设计文档 §2.2 列出 ToolUse/Embedding/CodeGen。序列化为 snake_case。 /// §3.4 提到未来可加 Streaming/Custom(String),当前 Phase 1 仅 3 个确定变体。 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum Capability { /// function calling / tool use ToolUse, /// 向量嵌入(知识库) Embedding, /// 代码生成强项 CodeGen, } /// 维度 3 — 价格分级(成本) /// /// 序列化为 snake_case。原设计文档 §2.2 列 Free/Low/Medium/High,但 Free 变体形同虚设: /// 预设表(presets/models.json)0 条 free + 启发式从不赋 Free(只 Low/Medium/High), /// B-260618-05 删除 Free 死档变体。 /// /// 路由已解耦(2026-06-18 B-260618-03):provider /v1/models API 不返回 cost_tier, /// 此维度 100% 靠预设表写死 + 模型名启发式猜,数据无客观依据不可信,不再参与硬路由 /// (原 §6.1 `cost_tier <= max_cost` 过滤已删除)。枚举保留供未来出现真实判别源时再接回。 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum CostTier { Low, Medium, High, } /// 维度 4 — 聪明程度(智力分级) /// /// 设计文档 §2.2:Lite/Standard/Plus/Ultra。序列化为 snake_case。 /// 派生 Ord:Lite < Standard < Plus < Ultra。 /// /// 路由已解耦(2026-06-18 B-260618-03):provider /v1/models API 不返回 intelligence, /// 此维度 100% 靠预设表写死 + 模型名启发式猜,数据无客观依据不可信,不再参与硬路由 /// (原 §6.1 `intelligence >= min_intelligence` 过滤已删除)。枚举保留供未来出现真实判别源时再接回。 #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum IntelligenceTier { /// flash/mini/lite/nano — 快但简单任务 Lite, /// air/standard — 日常够用 Standard, /// plus/pro/max — 复杂推理 Plus, /// 最强模型 — 兜底用 Ultra, } /// 探测元数据来源(只读,由 ModelProbe 填充,Phase 2) /// /// 设计文档 §2.1 的 ProbeSource 字段。Phase 1 仅定义占位,Phase 2 探测器填充。 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ProbeSource { /// 用户手动设定(最高优先) UserSet, /// 内置预设表精确/模糊匹配 PresetTable, /// 模型名启发式推断 Heuristic, /// 默认值兜底 Default, } // ============================================================ // 核心结构 ModelConfig // ============================================================ /// 单个模型的完整配置(4 维度 + 路由控制)。 /// /// 设计文档 §2.1。serde 默认值保证老库/部分字段缺失时向后兼容。 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ModelConfig { // ── 基础 ── /// 模型名(如 "glm-4v-flash") pub model_id: String, /// 启用/禁用(false = 配了但不参与路由,相当于"备档") #[serde(default = "default_true")] pub enabled: bool, /// 用户自定义别名(可选) #[serde(default, skip_serializing_if = "Option::is_none")] pub label: Option, // ── 维度 1:模态(能接收什么输入)── #[serde(default)] pub modalities: Vec, // ── 维度 2:能力(能做什么)── #[serde(default)] pub capabilities: Vec, // ── 维度 3:价格(成本分级)── #[serde(default)] pub cost_tier: CostTier, // ── 维度 4:聪明程度(智力分级)── #[serde(default)] pub intelligence: IntelligenceTier, // ── 路由控制 ── /// 权重(0-100,同能力候选中优先选权重高的) #[serde(default = "default_weight")] pub weight: u32, /// 上下文窗口大小(tokens) #[serde(default = "default_context_window")] pub context_window: usize, // ── 探测元数据(只读,由 ModelProbe 填充,Phase 2)── #[serde(default, skip_serializing_if = "Option::is_none")] pub probe_source: Option, } fn default_true() -> bool { true } fn default_weight() -> u32 { 50 } fn default_context_window() -> usize { 8192 } impl ModelConfig { /// 用模型名构造默认配置(老格式字符串数组 `["glm-4-flash"]` 升级时逐项调用)。 /// /// 设计文档 §2.3 向后兼容:老 `models: ["model-a"]` → 每个名字转默认 ModelConfig。 /// 默认值:enabled=true / modalities=[Text] / capabilities=[ToolUse] / /// cost_tier=Medium / intelligence=Standard / weight=50 / context_window=8192。 /// (ToolUse 默认开:主流对话模型支持 function calling;精确能力由 Phase 2 探测器修正) pub fn with_defaults(model_id: impl Into) -> Self { Self { model_id: model_id.into(), enabled: true, label: None, modalities: vec![Modality::Text], capabilities: vec![Capability::ToolUse], cost_tier: CostTier::Medium, intelligence: IntelligenceTier::Standard, weight: default_weight(), context_window: default_context_window(), probe_source: None, } } } /// 默认值(#[serde(default)] 用于 Vec/Option 字段时 serde 要求 Default impl) impl Default for Modality { fn default() -> Self { Modality::Text } } impl Default for CostTier { fn default() -> Self { CostTier::Medium } } impl Default for IntelligenceTier { fn default() -> Self { IntelligenceTier::Standard } } // ============================================================ // 向后兼容反序列化:老格式字符串数组 → ModelConfig 数组 // ============================================================ /// 反序列化 `models` JSON 字段,兼容两种格式: /// /// - 老格式:`["glm-4-flash", "glm-4v"]`(纯字符串数组)→ 每个名字转 `ModelConfig::with_defaults` /// - 新格式:`[{ "model_id": ..., "modalities": [...] }, ...]`(ModelConfig 对象数组) /// - 空/非数组:null / 缺失 / 空字符串 → 空 Vec /// /// 设计文档 §2.3。挂在 AiProviderRecord.models 字段上(#[serde(default, deserialize_with = ...)])。 pub fn deserialize_model_configs<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { use serde::de::Error; let opt: Option = Option::deserialize(deserializer)?; let Some(value) = opt else { return Ok(Vec::new()); }; match value { // null / 空字符串 / 空数组 → 空 serde_json::Value::Null => Ok(Vec::new()), serde_json::Value::String(s) if s.trim().is_empty() => Ok(Vec::new()), serde_json::Value::String(s) => { // 字符串内容可能是 JSON 文本(DB TEXT 列存 JSON 字符串) match serde_json::from_str::(s.trim()) { Ok(inner) => parse_models_value(inner).map_err(D::Error::custom), Err(_) => Ok(Vec::new()), } } serde_json::Value::Array(arr) => parse_models_value(serde_json::Value::Array(arr)).map_err(D::Error::custom), other => parse_models_value(other).map_err(D::Error::custom), } } fn parse_models_value(value: serde_json::Value) -> Result, serde_json::Error> { match value { serde_json::Value::Array(arr) => { // 老格式:全为字符串 → 每个转默认配置 if arr.iter().all(|v| v.is_string()) { return Ok(arr .into_iter() .filter_map(|v| v.as_str().map(ModelConfig::with_defaults)) .collect()); } // 新格式:ModelConfig 对象数组 serde_json::from_value::>(serde_json::Value::Array(arr)) } serde_json::Value::Null => Ok(Vec::new()), _ => Ok(Vec::new()), } } #[cfg(test)] mod tests { use super::*; // ── 枚举 serde 映射 ── #[test] fn modality_serde_snake_case() { let cases = [ (Modality::Text, "\"text\""), (Modality::Vision, "\"vision\""), (Modality::Audio, "\"audio\""), (Modality::Video, "\"video\""), ]; for (variant, expected) in cases { let s = serde_json::to_string(&variant).unwrap(); assert_eq!(s, expected, "serialize {variant:?}"); let back: Modality = serde_json::from_str(expected).unwrap(); assert_eq!(back, variant, "deserialize {variant:?}"); } } #[test] fn capability_serde_snake_case() { let cases = [ (Capability::ToolUse, "\"tool_use\""), (Capability::Embedding, "\"embedding\""), (Capability::CodeGen, "\"code_gen\""), ]; for (variant, expected) in cases { let s = serde_json::to_string(&variant).unwrap(); assert_eq!(s, expected, "serialize {variant:?}"); let back: Capability = serde_json::from_str(expected).unwrap(); assert_eq!(back, variant, "deserialize {variant:?}"); } } #[test] fn cost_tier_serde_and_ordering() { let cases = [ (CostTier::Low, "\"low\""), (CostTier::Medium, "\"medium\""), (CostTier::High, "\"high\""), ]; for (variant, expected) in cases { assert_eq!(serde_json::to_string(&variant).unwrap(), expected); assert_eq!(serde_json::from_str::(expected).unwrap(), variant); } // Ord:Low < Medium < High(枚举序,路由已解耦不再用于过滤,保留供未来判别源)。 // Free 变体已删除(B-260618-05:预设/启发式从不赋 Free,死档)。 assert!(CostTier::Low < CostTier::Medium); assert!(CostTier::Medium < CostTier::High); } #[test] fn intelligence_tier_serde_and_ordering() { let cases = [ (IntelligenceTier::Lite, "\"lite\""), (IntelligenceTier::Standard, "\"standard\""), (IntelligenceTier::Plus, "\"plus\""), (IntelligenceTier::Ultra, "\"ultra\""), ]; for (variant, expected) in cases { assert_eq!(serde_json::to_string(&variant).unwrap(), expected); assert_eq!(serde_json::from_str::(expected).unwrap(), variant); } // Ord:Lite < Standard < Plus < Ultra(枚举序,路由已解耦不再用于过滤,保留供未来判别源) assert!(IntelligenceTier::Lite < IntelligenceTier::Standard); assert!(IntelligenceTier::Standard < IntelligenceTier::Plus); assert!(IntelligenceTier::Plus < IntelligenceTier::Ultra); } // ── ModelConfig serde roundtrip ── #[test] fn model_config_full_roundtrip() { let mc = ModelConfig { model_id: "glm-4v".into(), enabled: true, label: Some("我的视觉".into()), modalities: vec![Modality::Text, Modality::Vision], capabilities: vec![Capability::ToolUse], cost_tier: CostTier::Medium, intelligence: IntelligenceTier::Plus, weight: 70, context_window: 128_000, probe_source: Some(ProbeSource::PresetTable), }; let json = serde_json::to_string(&mc).unwrap(); let back: ModelConfig = serde_json::from_str(&json).unwrap(); assert_eq!(back, mc); } #[test] fn model_config_skip_none_label_and_probe() { let mc = ModelConfig::with_defaults("glm-4-flash"); let json = serde_json::to_string(&mc).unwrap(); // label / probe_source 为 None 应被跳过 assert!(!json.contains("label"), "json={json}"); assert!(!json.contains("probe_source"), "json={json}"); // 反序列化回来相等(skip_serializing 不影响 deserialize) let back: ModelConfig = serde_json::from_str(&json).unwrap(); assert_eq!(back, mc); } #[test] fn model_config_with_defaults_values() { let mc = ModelConfig::with_defaults("test-model"); assert_eq!(mc.model_id, "test-model"); assert!(mc.enabled); assert_eq!(mc.modalities, vec![Modality::Text]); assert_eq!(mc.capabilities, vec![Capability::ToolUse]); assert_eq!(mc.cost_tier, CostTier::Medium); assert_eq!(mc.intelligence, IntelligenceTier::Standard); assert_eq!(mc.weight, 50); assert_eq!(mc.context_window, 8192); assert_eq!(mc.label, None); assert_eq!(mc.probe_source, None); } #[test] fn model_config_defaults_applied_on_missing_fields() { // 最小 JSON,仅 model_id — 其余字段靠 #[serde(default)] / default fn 填充 let json = r#"{"model_id":"minimal"}"#; let mc: ModelConfig = serde_json::from_str(json).unwrap(); assert_eq!(mc.model_id, "minimal"); assert!(mc.enabled, "enabled 默认 true"); assert_eq!(mc.weight, 50, "weight 默认 50"); assert_eq!(mc.context_window, 8192, "context_window 默认 8192"); assert!(mc.modalities.is_empty(), "Vec 字段 default 空"); assert!(mc.capabilities.is_empty()); assert_eq!(mc.cost_tier, CostTier::Medium); assert_eq!(mc.intelligence, IntelligenceTier::Standard); } // ── deserialize_model_configs 向后兼容 ── #[derive(Debug, Deserialize)] struct Wrap { #[serde(default, deserialize_with = "deserialize_model_configs")] models: Vec, } #[test] fn deserialize_old_format_string_array() { // 老格式:["glm-4-flash", "glm-4v"] let json = r#"{"models":["glm-4-flash","glm-4v"]}"#; let w: Wrap = serde_json::from_str(json).unwrap(); assert_eq!(w.models.len(), 2); assert_eq!(w.models[0].model_id, "glm-4-flash"); assert!(w.models[0].enabled); assert_eq!(w.models[0].modalities, vec![Modality::Text]); assert_eq!(w.models[1].model_id, "glm-4v"); } #[test] fn deserialize_new_format_object_array() { let json = r#"{"models":[{"model_id":"glm-4v","enabled":true,"modalities":["text","vision"],"capabilities":["tool_use"],"cost_tier":"medium","intelligence":"plus","weight":70,"context_window":128000}]}"#; let w: Wrap = serde_json::from_str(json).unwrap(); assert_eq!(w.models.len(), 1); let m = &w.models[0]; assert_eq!(m.model_id, "glm-4v"); assert_eq!(m.modalities, vec![Modality::Text, Modality::Vision]); assert_eq!(m.intelligence, IntelligenceTier::Plus); assert_eq!(m.context_window, 128_000); } #[test] fn deserialize_null_or_missing_yields_empty() { // null let w: Wrap = serde_json::from_str(r#"{"models":null}"#).unwrap(); assert!(w.models.is_empty()); // 缺失字段 let w: Wrap = serde_json::from_str(r#"{}"#).unwrap(); assert!(w.models.is_empty()); // 空字符串(DB TEXT 列可能存空串) let w: Wrap = serde_json::from_str(r#"{"models":""}"#).unwrap(); assert!(w.models.is_empty()); // 空数组 let w: Wrap = serde_json::from_str(r#"{"models":[]}"#).unwrap(); assert!(w.models.is_empty()); } #[test] fn deserialize_json_string_text_column() { // DB TEXT 列存的是 JSON 文本字符串:`"[\"model-a\"]"` let json = r#"{"models":"[\"glm-4-flash\"]"}"#; let w: Wrap = serde_json::from_str(json).unwrap(); assert_eq!(w.models.len(), 1); assert_eq!(w.models[0].model_id, "glm-4-flash"); } #[test] fn roundtrip_via_text_column() { // 模拟 DB 落库:Vec → serde_json 序列化 → 存 TEXT 列 // 读出来:TEXT 列值 → 反序列化回 Vec let original = vec![ ModelConfig::with_defaults("glm-4-flash"), ModelConfig { model_id: "glm-4v".into(), modalities: vec![Modality::Text, Modality::Vision], intelligence: IntelligenceTier::Plus, weight: 70, context_window: 128_000, ..ModelConfig::with_defaults("glm-4v") }, ]; let text = serde_json::to_string(&original).unwrap(); let wrapped = format!(r#"{{"models":"{}"}}"#, text.replace('"', "\\\"")); let w: Wrap = serde_json::from_str(&wrapped).unwrap(); assert_eq!(w.models.len(), 2); assert_eq!(w.models[1].intelligence, IntelligenceTier::Plus); } }