# F-01 模型能力系统 Phase 1 — 模型配置与智能路由设计

> 状态：📐 设计定稿（2026-06-16，含多角度佐证 + 厂商适配 + 用户流程）
> 类型：架构设计文档（不碰任何 code）
> 关联：F-260614-05 多模态 Phase 2 / F-260614-04 多 Provider 负载均衡池
> 前置：F-07 df-ai-core trait 下沉 ✅ 已完成（batch61·2069f79）
> 决策记录：见 [功能决策记录-2026-06-14.md](../滚动规范/功能决策记录-2026-06-14.md) §「模型能力系统」行

---

> ## ⚠️ 实施状态（2026-06-18 核对：已落地，路由部分改方向）
>
> **阶段 1-6 全部落地**（数据模型 / 探测器 / 厂商拉取 / 路由器 / 7+ 调用点接入 / 前端）：
> - 阶段 1 数据模型：`crates/df-ai-core/src/model.rs:106` `ModelConfig`、`:20/35/55/71` 四维度枚举（Modality/Capability/CostTier/IntelligenceTier）、`:211` `deserialize_model_configs` 向后兼容；`crates/df-storage/src/models.rs:159-160` `AiProviderRecord.model_configs` 字段（**注意：实际落地字段名 `model_configs` 而非本文档 §1.1/§2.3 描述的老 `models` JSON 字段扩展**，新字段经 V18 迁移幂等补列 `crates/df-storage/src/migrations.rs:317-325`）。
> - 阶段 2 探测器：`crates/df-ai/src/model_probe.rs`（已建）+ `crates/df-ai/presets/models.json`（已建）。注意预设表/PatternRule 未拆独立 `preset_table.rs`，合并在 model_probe 内。
> - 阶段 3 厂商拉取：`crates/df-ai/src/model_fetch.rs`（已建，fetch_models 分派）。
> - 阶段 4 路由器：`crates/df-ai/src/router.rs:40/56/76` `ModelRouter::select` / `select_model_id`。
> - 阶段 5 调用点：主对话 `src-tauri/src/commands/ai/agentic.rs:418/423`、标题 `title.rs:86/91`、知识提炼/嵌入 `knowledge_inject.rs:58-63/359-364`、压缩 `compress.rs:59-64`、项目扫描 `project.rs:533-538/627-632`、灵感评估 `crates/df-ideas/src/adversarial.rs:158-163`、AiNode `crates/df-nodes/src/ai_node.rs:221-226`。
>
> **§6.1 路由逻辑已改方向（2026-06-18 决策 B-260618-03）**：本文档 §6.1 描述的 `TaskRequirements` 含 `min_intelligence`/`max_cost` 两字段、`select` 含「智力达标」「成本可控」两过滤步、`max_by_key((weight, Reverse(cost_tier)))` 同权重选便宜——**均已删除**。
> - 实际形态：`crates/df-ai/src/router.rs:27-35` `TaskRequirements` 仅 3 字段（`modalities`/`needs_tool_use`/`estimated_context`），`min_intelligence`/`max_cost` 已删；`:56-63` `select` 过滤链仅 4 步（enabled / 模态 / 能力 / 窗口），`:62` 排序纯 `max_by_key(m.weight)`，无 cost tie-break。
> - 根因：provider `/v1/models` API 不返回 cost_tier/intelligence，两维度 100% 靠预设表写死 + 模型名启发式猜，数据无客观依据不可信，不参与硬路由。枚举（CostTier/IntelligenceTier）保留在 `model.rs` 供未来出现真实判别源再接回。
>
> **§6.2 场景路由表 / §6.3 调用点表**：表中行号（如 `agentic.rs:50`/`title.rs:63`/`project.rs:378`）已漂移，实际调用点见上方阶段 5 行号清单。

---

## 0. 摘要

将当前「一个 Provider 一个 default_model 跑全场」升级为「多模型池 + 能力感知 + 智能路由」。
用户只需填 Provider 基本信息 → 自动拉取模型列表 → 自动探测每个模型的模态/能力/价格/智力 → 路由器按场景自动选最优模型。

**核心价值**：含图消息自动选 Vision 模型、标题生成自动选便宜模型、复杂推理自动选强模型、限流自动 fallback。

---

## 1. 现状盘点

### 1.1 数据模型（AiProviderRecord）

`crates/df-storage/src/models.rs:127-140`

```rust
pub struct AiProviderRecord {
    pub provider_type: String,     // "openai_compat" | "anthropic_compat"
    pub base_url: String,
    pub default_model: String,     // 唯一实际使用的模型名
    pub models: Option<String>,    // JSON array of model names（展示用，未消费）
    pub config: Option<String>,    // JSON extra config（几乎没用）
    pub is_default: bool,
    ...
}
```

**问题**：
1. `models` 存了模型列表但**没有能力标记**，不知道哪个支持 vision / tool use
2. **没有启用/禁用开关**，配了的模型无法控制哪些参与路由
3. **没有权重/优先级**，无法表达"优先用 A，A 不可用时 fallback 到 B"
4. `default_model` 是唯一实际使用的模型名，`models` 列表形同虚设

### 1.2 模型选择机制（当前）

```
用户在 Settings 配置 Provider（含 base_url + default_model）
  ↓
AiSession.active_provider_id  ← 全局唯一活跃 Provider
  ↓
get_active_provider() → 取 Provider 配置
  ↓
CompletionRequest { model: provider.default_model }  ← 直接用固定 model
```

- 一个时刻只有一个活跃 Provider
- 所有场景（对话/标题/知识提炼/项目扫描）共用同一个 model
- 无 ModelCapability / ModelRouter / 按场景路由机制

---

## 2. 模型配置数据模型

### 2.1 ModelConfig（每个模型的完整描述）

```rust
/// 单个模型的完整配置（4 维度 + 路由控制）
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
    // ── 基础 ──
    /// 模型名（如 "glm-4v-flash"）
    pub model_id: String,
    /// 启用/禁用（false = 配了但不参与路由，相当于"备档"）
    #[serde(default = "default_true")]
    pub enabled: bool,
    /// 用户自定义别名（可选）
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,

    // ── 维度 1：模态（能接收什么输入）──
    pub modalities: Vec<Modality>,

    // ── 维度 2：能力（能做什么）──
    pub capabilities: Vec<Capability>,

    // ── 维度 3：价格（成本分级）──
    pub cost_tier: CostTier,

    // ── 维度 4：聪明程度（智力分级）──
    pub intelligence: IntelligenceTier,

    // ── 路由控制 ──
    /// 权重（0-100，同能力候选中优先选权重高的）
    #[serde(default = "default_weight")]
    pub weight: u32,
    /// 上下文窗口大小（tokens）
    #[serde(default = "default_context_window")]
    pub context_window: usize,

    // ── 探测元数据（只读，由 ModelProbe 填充）──
    #[serde(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 }
```

### 2.2 枚举定义

```rust
/// 模态（输入类型）
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Modality {
    Text,
    Vision,
    // 未来扩展：Audio, Video
}

/// 能力
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
    ToolUse,    // function calling / tool use
    Embedding,  // 向量嵌入
    CodeGen,    // 代码生成强项
}

/// 价格分级
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CostTier {
    Free,   // 免费
    Low,    // flash / mini 系列
    Medium, // 标准定价
    High,   // 旗舰定价
}

/// 聪明程度（智力分级）
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum IntelligenceTier {
    Lite,      // flash/mini/lite/nano — 快但简单任务
    Standard,  // air/standard — 日常够用
    Plus,      // plus/pro/max — 复杂推理
    Ultra,     // 最强模型 — 兜底用
}
```

### 2.3 存储方式：扩展 models JSON 字段（零迁移）

`AiProviderRecord.models` 从 `["glm-4-flash", "glm-4v"]` 升级为：

```json
[
  {
    "model_id": "glm-4-flash",
    "enabled": true,
    "modalities": ["text"],
    "capabilities": ["tool_use"],
    "cost_tier": "low",
    "intelligence": "lite",
    "weight": 90,
    "context_window": 128000
  },
  {
    "model_id": "glm-4v",
    "enabled": true,
    "modalities": ["text", "vision"],
    "capabilities": ["tool_use"],
    "cost_tier": "medium",
    "intelligence": "plus",
    "weight": 70,
    "context_window": 128000
  }
]
```

**向后兼容**：自定义反序列化器，老格式 `["model-a"]` → 自动转成默认配置列表。

```rust
fn deserialize_models<'de, D>(d: D) -> Result<Vec<ModelConfig>, D::Error>
where D: serde::Deserializer<'de> {
    let v = serde_json::Value::deserialize(d)?;
    match v {
        // 老格式：字符串数组 → 每个转默认 ModelConfig
        serde_json::Value::Array(arr) if arr.iter().all(|v| v.is_string()) => {
            arr.into_iter()
                .filter_map(|v| v.as_str().map(|s| ModelConfig::with_defaults(s)))
                .collect::<Vec<_>>()
                .pipe(Ok)
        }
        // 新格式：ModelConfig 数组
        serde_json::Value::Array(_) => {
            serde_json::from_value::<Vec<ModelConfig>>(v).map_err(D::Error::custom)
        }
        _ => Ok(vec![]),
    }
}
```

**否决方案**：新建 `ai_models` 独立表 — 增量复杂度高（迁移 + 新 Repo），`models` JSON 字段已够用。

---

## 3. 多角度佐证

### 3.1 行业对标

| 产品 | 模型配置方式 | 对照 |
|------|------------|------|
| OpenRouter | 每个模型标 modality/pricing/context_length | 4 维度完全覆盖，且加了 intelligence 分级 |
| Cursor | 内置模型能力感知（自动判断图片/工具） | 预设表 + 启发式探测本质相同 |
| LangChain ModelRouter | 按 max_tokens/supports_tool_use 路由 | capabilities + modalities 路由一致 |
| One-API / New-API | 纯渠道管理，无能力感知 | 能力标记是显著增量 |

**结论**：4 维度配置是行业标配的超集，非过度设计。

### 3.2 路由器实际需求倒推

| 路由决策 | 必须知道 | 对应维度 | 缺了会怎样 |
|---------|---------|---------|-----------|
| 含图消息选哪个 | 是否支持 Vision | 模态 | 发给纯文本模型 → 400 |
| Agentic loop 选哪个 | 是否支持 tool_use | 能力 | 发 tool 定义给不支持的 → 忽略/报错 |
| 标题生成选哪个 | 够便宜够快 | 价格+智力 | 用旗舰模型 → 烧钱 |
| 复杂推理选哪个 | 够聪明 | 智力 | 用 flash → 质量差 |
| 上下文超长选哪个 | 窗口够大 | context_window | 超窗口 → 截断 |

**结论**：每个维度直接对应路由器必须做的判断，缺一不可。

### 3.3 用户认知负担

| 信息 | 用户负担 | 说明 |
|------|---------|------|
| 模型名 | 零 | 从文档复制 |
| 模态 | 低 | 文档一眼可见，或名字带 v |
| 能力 | 低 | 文档明确标注 |
| 价格 | 中 | 查文档，但分级别直觉可判 |
| 智力 | 低 | flash < air < plus 是行业共识 |

**结论**：4 个维度都是用户本来就知道的，加上自动填充，门槛极低。

### 3.4 可扩展性

| 未来需求 | 支持 | 扩展方式 |
|---------|------|---------|
| 音频输入 | ✅ | Modality 加 Audio |
| 视频输入 | ✅ | Modality 加 Video |
| 精确价格控制 | ✅ | cost_tier 升级为精确数值 |
| 流式支持差异 | ✅ | Capability 加 Streaming |
| 自定义能力标签 | ✅ | Capability 加 Custom(String) |

**结论**：enum + Vec 开放式，未来加维度只需加变体。

### 3.5 数据兼容性

老 `models: ["model-a"]` → 自定义反序列化自动补默认值。`default_model` 字段保留向后兼容。零迁移脚本。

### 3.6 竞品缺陷反证

| 问题 | 竞品现状 | 我们的方案 |
|------|---------|-----------|
| 含图发给纯文本模型 | One-API 不感知能力 | 路由器自动选 Vision |
| 标题生成用旗舰模型 | 固定模型手动切 | 按 intelligence+cost 自动选 Lite |
| 新模型不知道能不能用 | 用户自己试 | 预设表 + 启发式自动推断 |
| 限流后无 fallback | 手动切换 | 按 weight 自动 fallback |

### 3.7 实施风险

| 风险 | 评估 | 缓解 |
|------|------|------|
| 预设表维护 | 低（主流 20-30 个，半年更新） | JSON 配置文件可热更新 |
| 启发式误判 | 低（命名是行业惯例） | 标注 Medium 可信度，用户可修正 |
| 路由器选错 | 低 | weight fallback + 降级兜底 |

**7 个角度一致指向：方案可行且合理。**

---

## 4. 模型探测器（ModelProbe）

### 4.1 多源探测

每个模型的信息从多个来源获取，交叉验证：

```rust
pub struct ModelProbe {
    preset_table: ModelPresetTable,
}

impl ModelProbe {
    pub async fn probe(&self, provider: &AiProviderRecord, model_id: &str) -> ModelProfile {
        // 1. 启发式：从模型名推断
        let heuristic = self.heuristic_infer(model_id);

        // 2. 查预设表（精确匹配 > 模糊匹配）
        let preset = self.preset_table.lookup(model_id);

        // 3. 多源合并（预设表 > 启发式）
        let merged = self.merge_sources(heuristic, preset);

        // 4. 最终结果 + 标注每个维度的置信度
        self.finalize(merged)
    }
}
```

### 4.2 探测优先级链

```
用户手动设定 (UserSet)          ← 最高优先，永不被覆盖
    ↓
内置预设表精确匹配 (PresetTable) ← 高可信
    ↓
模型名启发式 (Heuristic)         ← 中可信，标注"建议确认"
    ↓
默认值 (Default)                 ← 低可信
```

### 4.3 探测结果带置信度

```rust
pub struct ModelProfile {
    pub model_id: String,
    pub modalities: SourcedField<Vec<Modality>>,
    pub capabilities: SourcedField<Vec<Capability>>,
    pub cost_tier: SourcedField<CostTier>,
    pub intelligence: SourcedField<IntelligenceTier>,
    pub context_window: SourcedField<usize>,
}

pub struct SourcedField<T> {
    pub value: T,
    pub source: Source,
    pub confidence: Confidence,
}

pub enum Source { PresetTable, Heuristic, UserSet }

pub enum Confidence { High, Medium, Low }
```

### 4.4 内置预设表

```rust
pub struct ModelPresetTable {
    exact: HashMap<String, ModelPreset>,     // 精确匹配
    patterns: Vec<PatternRule>,              // 模糊匹配
}

// 精确匹配示例
"glm-4-flash"   => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Lite, context:128K }
"glm-4-air"     => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"glm-4-plus"    => { modalities:[Text], capabilities:[ToolUse], cost:Medium, intelligence:Plus }
"glm-4v"        => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Medium, intelligence:Plus }
"glm-4v-flash"  => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Lite }
"deepseek-chat" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"deepseek-coder"=> { modalities:[Text], capabilities:[ToolUse,CodeGen], cost:Low, intelligence:Plus }
"claude-3-5-sonnet-20241022" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra }
"claude-3-haiku-20240307"    => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"gpt-4o"        => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra }
"gpt-4o-mini"   => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard }
"embedding-3"   => { modalities:[], capabilities:[Embedding], cost:Low, intelligence:Lite }

// 模糊匹配规则
PatternRule { keyword: "flash|mini|lite|nano", → Intelligence(Lite) }
PatternRule { keyword: "plus|pro|max|ultra",  → Intelligence(Plus) }
PatternRule { keyword: "v$|vl|vision|-v\\d",   → Modality(Vision) }
PatternRule { keyword: "embed",                → Capability(Embedding) }
PatternRule { keyword: "code|coder",           → Capability(CodeGen) }
```

预设表以 JSON 配置文件形式存储（`crates/df-ai/presets/models.json`），可热更新不随版本绑。

---

## 5. 厂商模型列表适配

### 5.1 差异盘点

| 厂商/协议 | 端点 | 鉴权 | 返回格式 |
|-----------|------|------|---------|
| OpenAI 兼容（GLM/DeepSeek/Kimi/通义） | `GET /v1/models` | Bearer | `{data:[{id, owned_by}]}` |
| Anthropic | `GET /v1/models` | x-api-key + version | `{data:[{id, display_name}]}` |
| 中转站 | `GET /v1/models` | Bearer | 同 OpenAI，但可能混入噪音或返回 404 |
| Ollama | `GET /api/tags` | 无 | `{models:[{name, size}]}` — 完全不同 |

### 5.2 统一拉取接口

```rust
/// 模型拉取结果（统一格式）
pub struct FetchedModel {
    pub model_id: String,
    pub display_name: Option<String>,
}

/// 按 provider_type 分派
pub async fn fetch_models(
    provider_type: &str,
    base_url: &str,
    api_key: &str,
) -> Result<Vec<FetchedModel>, FetchError> {
    match provider_type {
        "openai_compat" => fetch_openai_compat(base_url, api_key).await,
        "anthropic_compat" => fetch_anthropic_compat(base_url, api_key).await,
        _ => Err(FetchError::Unsupported),
    }
}
```

### 5.3 URL 智能拼接

```rust
fn build_models_url(base_url: &str) -> String {
    let url = base_url.trim_end_matches('/');
    if url.ends_with("/chat/completions") {
        url.replace("/chat/completions", "/models")
    } else if url.ends_with("/v1") || url.ends_with("/v4") {
        format!("{}/models", url)
    } else if url.ends_with("/models") {
        url.to_string()
    } else {
        format!("{}/v1/models", url)
    }
}
```

### 5.4 噪音过滤

```rust
fn is_non_chat_model(id: &str) -> bool {
    let id = id.to_lowercase();
    id.contains("dall-e") || id.contains("midjourney")      // 图片生成
    || id.contains("tts") || id.contains("whisper")          // 语音
    || id.contains("moderation")                              // 审查
    // embedding 保留（知识库需要）
}
```

### 5.5 失败降级

| 错误 | 处理 |
|------|------|
| 404 / 不支持 | 静默降级到手动输入（批量文本） |
| 401 / 403 | 提示密钥问题 |
| 网络/超时 | 提示检查连接 |
| 解析失败 | 提示格式不兼容，降级手动 |

**核心原则：自动拉取是锦上添花，手动输入永远保底。**

---

## 6. 模型路由器（ModelRouter）

### 6.1 路由逻辑

```rust
pub struct TaskRequirements {
    pub modalities: Vec<Modality>,     // 任务需要什么模态
    pub needs_tool_use: bool,          // 是否需要工具调用
    pub min_intelligence: IntelligenceTier, // 最低智力要求
    pub max_cost: Option<CostTier>,    // 成本上限（可选）
    pub estimated_context: usize,      // 预估上下文大小
}

impl ModelRouter {
    pub fn select(&self, req: &TaskRequirements, pool: &[ModelConfig]) -> Option<&ModelConfig> {
        pool.iter()
            .filter(|m| m.enabled)                                              // 1. 只选启用的
            .filter(|m| req.modalities.iter().all(|r| m.modalities.contains(r))) // 2. 模态匹配
            .filter(|m| !req.needs_tool_use || m.capabilities.contains(&Capability::ToolUse)) // 3. 能力匹配
            .filter(|m| m.intelligence >= req.min_intelligence)                 // 4. 智力达标
            .filter(|m| req.max_cost.map_or(true, |max| m.cost_tier <= max))    // 5. 成本可控
            .filter(|m| m.context_window >= req.estimated_context)              // 6. 窗口够大
            .max_by_key(|m| (m.weight, -(m.cost_tier as i32)))                  // 7. 权重优先，同权重选便宜的
    }
}
```

### 6.2 场景路由表

| 场景 | 路由偏好 | 典型选择 |
|------|---------|---------|
| 标题生成 | Lite + Low cost | glm-4-flash |
| 日常对话 | Standard | glm-4-air |
| 写代码 | Standard/Plus + CodeGen | deepseek-coder |
| 含图片 | Vision + Standard | glm-4v |
| 复杂推理 | Plus/Ultra | glm-4-plus |
| 知识嵌入 | Embedding | embedding-3 |

### 6.3 7 个调用点接入

| 调用点 | 当前代码位置 | TaskRequirements |
|--------|------------|-----------------|
| 主对话 stream_llm | agentic.rs:50 | 动态（按消息内容：含图→Vision） |
| 标题生成 | title.rs:63 | Lite + Low（省钱） |
| 知识提炼 | knowledge_inject.rs:33,323 | Standard |
| 知识嵌入 | knowledge_inject.rs embedding | Embedding |
| 项目扫描描述 | project.rs:378 | Standard（含图→Vision） |
| 灵感评估 | df-ideas adversarial.rs | Standard |
| AiNode 工作流 | df-nodes ai_node.rs:118 | 按节点配置 |

---

## 7. 用户配置与使用流程

### 7.1 配置流程（做一次）

```
Step 1: 填 Provider 基本信息（名称/类型/地址/密钥）
  ↓
Step 2: 点「测试连接并拉取模型」
  ↓ 自动调 /v1/models（按协议分派）
  ↓ 自动过滤非 chat 模型
  ↓ 自动探测每个模型特征（预设表 + 启发式）
  ↓
Step 3: 显示模型列表（已标注特征 + 可信度）
  ↓ 用户确认或微调（大多数不需要进这步）
  ↓
Step 4: 完成
```

**拉取失败时降级**：手动输入模型名（批量文本，每行一个）→ 同样走探测。

### 7.2 日常使用（零配置）

```
用户发消息 → 路由器自动选模型 → 用户无感 → 出结果
```

**手动覆盖**：AiChat 顶部下拉选「自动（推荐）」或指定具体模型。

**自动 fallback**：模型限流/失败 → 按 weight 选次高 → 自动重试。

### 7.3 简洁性指标

| 传统做法 | 我们的做法 | 省了什么 |
|---------|-----------|---------|
| 手动填每个模型能力 | 预设表自动填充 | 省 90% 配置 |
| 自己记哪个支持图片 | 路由器自动选 | 省记忆 |
| 每次对话手动切模型 | 自动路由 | 省操作 |
| 限流后手动换 | 自动 fallback | 省故障处理 |

**用户最小操作路径**：填 3 个字段 → 点 1 个按钮 → 完成。

---

## 8. 实施分阶段

### 阶段 1：数据模型（df-ai-core + df-storage）

| 文件 | 改动 |
|------|------|
| `crates/df-ai-core/src/provider.rs` | 新增 ModelConfig / Modality / Capability / CostTier / IntelligenceTier |
| `crates/df-storage/src/models.rs` | AiProviderRecord.models 反序列化升级（兼容老格式） |
| `crates/df-ai/src/model_config.rs`（新建） | deserialize_models 兼容函数 + ModelConfig::with_defaults |

### 阶段 2：预设表 + 探测器（df-ai）

| 文件 | 改动 |
|------|------|
| `crates/df-ai/presets/models.json`（新建） | 主流模型预设数据 |
| `crates/df-ai/src/model_probe.rs`（新建） | ModelProbe + 启发式 + 多源合并 |
| `crates/df-ai/src/preset_table.rs`（新建） | 预设表加载 + 精确/模糊匹配 |

### 阶段 3：厂商模型列表拉取（df-ai）

| 文件 | 改动 |
|------|------|
| `crates/df-ai/src/model_fetch.rs`（新建） | fetch_models + openai_compat/anthropic_compat 分派 + URL 拼接 + 噪音过滤 |

### 阶段 4：路由器（df-ai）

| 文件 | 改动 |
|------|------|
| `crates/df-ai/src/router.rs`（新建） | ModelRouter + TaskRequirements + select 逻辑 |

### 阶段 5：7 调用点接入（src-tauri）

| 文件 | 改动 |
|------|------|
| `src-tauri/src/commands/ai/agentic.rs` | 主对话路由（按 has_image 动态选） |
| `src-tauri/src/commands/ai/title.rs` | 标题生成路由（Lite + Low） |
| `src-tauri/src/commands/ai/knowledge_inject.rs` | 知识提炼/嵌入路由 |
| `src-tauri/src/commands/project.rs` | 项目扫描路由 |
| `src-tauri/src/commands/ai/commands.rs` | 新增 IPC：ai_fetch_models / ai_probe_model |

### 阶段 6：前端（src）

| 文件 | 改动 |
|------|------|
| `src/api/types.ts` | ModelConfig 类型对齐 |
| `src/components/settings/` | Provider 配置页加模型列表 + 探测结果展示 + 启用/禁用/权重 |
| `src/components/AiChat.vue` | 顶部模型下拉（自动/指定） |

---

## 9. 与 F-05（多模态）的关系

| 子能力 | 依赖 F-01？ | 说明 |
|--------|------------|------|
| ContentPart 数据模型 | ❌ | 纯类型升级，与 ModelConfig 解耦 |
| provider content 数组化 | ❌ | 协议层改造，与路由器无关 |
| 前端粘贴/拖拽/渲染 | ❌ | 纯前端 |
| **vision 模型自动路由** | ✅ | ModelRouter 按 modalities 含 Vision 筛选 |

F-05 可独立先做 80%（数据模型 + provider + 前端），vision 路由留到 F-01 落地后接入。
过渡期靠 ModelConfig.modalities 手动标注 + 路由器筛选。

---

## 10. 不做的事（显式排除）

- 不做模型自动下载/安装（仅 API 模型，不含本地模型管理）
- 不做模型 benchmark 自动跑分（智力分级靠预设表 + 用户确认）
- 不做多 Provider 负载均衡（属 F-04，本任务只做单 Provider 内多模型路由）
- 不做模型用量统计/成本告警（属运维增强，后续独立做）
- 不新建 ai_models 独立表（扩展 models JSON 字段够用）
- 本文档不实施任何 code（纯设计）
