新增: F-01模型能力阶段1+2(ModelConfig数据模型+model_probe探测器+df-storage兼容V18)
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
//! 保持 `df_ai::provider::*` 路径不变。df-ideas 等轻消费方直接依赖本 crate
|
||||
//! 的 trait 即可接 LLM,不引入 reqwest / eventsource-stream 等重依赖。
|
||||
|
||||
pub mod model;
|
||||
pub mod provider;
|
||||
|
||||
pub use model::*;
|
||||
pub use provider::*;
|
||||
|
||||
462
crates/df-ai-core/src/model.rs
Normal file
462
crates/df-ai-core/src/model.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
//! 模型能力数据模型 — 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 — 价格分级(成本)
|
||||
///
|
||||
/// 设计文档 §2.2:Free/Low/Medium/High。序列化为 snake_case。
|
||||
/// 路由器按此做成本上限过滤(§6.1 `cost_tier <= max_cost`)。
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CostTier {
|
||||
Free,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// 维度 4 — 聪明程度(智力分级)
|
||||
///
|
||||
/// 设计文档 §2.2:Lite/Standard/Plus/Ultra。序列化为 snake_case。
|
||||
/// 派生 Ord:Lite < Standard < Plus < Ultra,供路由器 `intelligence >= min_intelligence` 比较(§6.1)。
|
||||
#[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<String>,
|
||||
|
||||
// ── 维度 1:模态(能接收什么输入)──
|
||||
#[serde(default)]
|
||||
pub modalities: Vec<Modality>,
|
||||
|
||||
// ── 维度 2:能力(能做什么)──
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<Capability>,
|
||||
|
||||
// ── 维度 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<ProbeSource>,
|
||||
}
|
||||
|
||||
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<String>) -> 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<Vec<ModelConfig>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let opt: Option<serde_json::Value> = 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::<serde_json::Value>(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<Vec<ModelConfig>, 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::<Vec<ModelConfig>>(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::Free, "\"free\""),
|
||||
(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::<CostTier>(expected).unwrap(), variant);
|
||||
}
|
||||
// Ord:Free < Low < Medium < High(路由器 max_cost 比较)
|
||||
assert!(CostTier::Free < CostTier::Low);
|
||||
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::<IntelligenceTier>(expected).unwrap(), variant);
|
||||
}
|
||||
// Ord:Lite < Standard < Plus < Ultra(路由器 min_intelligence 比较)
|
||||
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<ModelConfig>,
|
||||
}
|
||||
|
||||
#[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<ModelConfig> → serde_json 序列化 → 存 TEXT 列
|
||||
// 读出来:TEXT 列值 → 反序列化回 Vec<ModelConfig>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user