|
| 1 | +--- |
| 2 | +description: model_id 6 层解析回退机制的精确行为分析 — resolveModel() 从精确匹配到模糊回退的完整链路 |
| 3 | +protocol_version: based on proxy/src/models.ts (103 行) |
| 4 | +confidence: high |
| 5 | +last_verified: 2026-07-05 |
| 6 | +--- |
| 7 | + |
| 8 | +# model_id 6 层解析回退的精确行为 |
| 9 | + |
| 10 | +> **所属分类:** 新维度 #28 — model_id 6 层解析回退 |
| 11 | +> **关键发现:** 6 层回退设计巧妙但有 3 个隐蔽缺陷:大小写敏感、display_name 空值、第一兜底可能匹配错误模型 |
| 12 | +
|
| 13 | +## 1. 6 层解析回退架构 |
| 14 | + |
| 15 | +```mermaid |
| 16 | +flowchart TB |
| 17 | + subgraph Input["客户端请求"] |
| 18 | + ID["model: 'monkeycode/BaiZhiCloud/gpt-5.4'"] |
| 19 | + end |
| 20 | +
|
| 21 | + subgraph Layer1["L1: 精确匹配"] |
| 22 | + EXACT["`monkeycode/${provider}/${model}`<br/>=== openaiModelId"] |
| 23 | + end |
| 24 | +
|
| 25 | + subgraph Layer2["L2: Provider/Model 匹配"] |
| 26 | + PM["`${provider}/${model}`<br/>=== openaiModelId"] |
| 27 | + end |
| 28 | +
|
| 29 | + subgraph Layer3["L3: Model 名称匹配"] |
| 30 | + MN["model.name === openaiModelId<br/>大小写敏感"] |
| 31 | + end |
| 32 | +
|
| 33 | + subgraph Layer4["L4: Display Name 匹配"] |
| 34 | + DN["display_name === openaiModelId<br/>大小写敏感"] |
| 35 | + end |
| 36 | +
|
| 37 | + subgraph Layer5["L5: 默认模型"] |
| 38 | + DEF["is_default === true"] |
| 39 | + end |
| 40 | +
|
| 41 | + subgraph Layer6["L6: 第一个可用模型"] |
| 42 | + FIRST["models[0] || null"] |
| 43 | + end |
| 44 | +
|
| 45 | + subgraph Fail["解析失败"] |
| 46 | + NULL["return null<br/>→ 客户端收到 404"] |
| 47 | + end |
| 48 | +
|
| 49 | + ID --> Layer1 |
| 50 | + Layer1 -->|命中| DONE["✅ 返回 MonkeyCodeModel"] |
| 51 | + Layer1 -->|未命中| Layer2 |
| 52 | + Layer2 -->|命中| DONE |
| 53 | + Layer2 -->|未命中| Layer3 |
| 54 | + Layer3 -->|命中| DONE |
| 55 | + Layer3 -->|未命中| Layer4 |
| 56 | + Layer4 -->|命中| DONE |
| 57 | + Layer4 -->|未命中| Layer5 |
| 58 | + Layer5 -->|命中| DONE |
| 59 | + Layer5 -->|未命中| Layer6 |
| 60 | + Layer6 -->|找到| DONE |
| 61 | + Layer6 -->|空列表| NULL |
| 62 | +``` |
| 63 | + |
| 64 | +## 2. 每层匹配的精确条件 |
| 65 | + |
| 66 | +```typescript |
| 67 | +// proxy/src/models.ts:64-90 |
| 68 | +async resolveModel(openaiModelId: string): Promise<MonkeyCodeModel | null> { |
| 69 | + const models = await this.fetchModels() |
| 70 | + |
| 71 | + // L1: monkeycode/BaiZhiCloud/gpt-5.4 |
| 72 | + const exact = models.find((m) => this.toOpenAIModelId(m) === openaiModelId) |
| 73 | + if (exact) return exact |
| 74 | + |
| 75 | + // L2: BaiZhiCloud/gpt-5.4 |
| 76 | + const byProviderModel = models.find((m) => `${m.provider}/${m.model}` === openaiModelId) |
| 77 | + if (byProviderModel) return byProviderModel |
| 78 | + |
| 79 | + // L3: gpt-5.4 |
| 80 | + const byModelName = models.find((m) => m.model === openaiModelId) |
| 81 | + if (byModelName) return byModelName |
| 82 | + |
| 83 | + // L4: display_name 匹配 |
| 84 | + const byDisplayName = models.find((m) => m.display_name === openaiModelId) |
| 85 | + if (byDisplayName) return byDisplayName |
| 86 | + |
| 87 | + // L5: is_default |
| 88 | + const defaultModel = models.find((m) => m.is_default) |
| 89 | + if (defaultModel) return defaultModel |
| 90 | + |
| 91 | + // L6: 第一个 |
| 92 | + return models[0] || null |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +## 3. 每一层的客户端输入示例 |
| 97 | + |
| 98 | +| 层 | 客户端输入示例 | 命中条件 | 匹配复杂度 | |
| 99 | +|----|--------------|---------|-----------| |
| 100 | +| L1 | `monkeycode/BaiZhiCloud/qwen3.5-plus` | 完整前缀+provider+model | 精确匹配 | |
| 101 | +| L2 | `BaiZhiCloud/qwen3.5-plus` | `/` 分割的两个字段 | 精确匹配 | |
| 102 | +| L3 | `qwen3.5-plus` | 单字段匹配 model 名 | 精确匹配(可能误配) | |
| 103 | +| L4 | `通义千问 Qwen3.5` | display_name 精确匹配 | 较弱(可能为空) | |
| 104 | +| L5 | 任意值 | is_default 标记 | 弱(返回默认模型) | |
| 105 | +| L6 | 任意值 | 第一个有值 | 最弱(随机匹配) | |
| 106 | + |
| 107 | +## 4. 线上实测的模型 ID 示例 |
| 108 | + |
| 109 | +```javascript |
| 110 | +// 从线上实际获取的 37 个模型 |
| 111 | +// ID 格式: monkeycode/{provider}/{model} |
| 112 | +const modelIds = [ |
| 113 | + "monkeycode/BaiZhiCloud/monkeycode-pro/minimax-m2.7", |
| 114 | + "monkeycode/BaiZhiCloud/kimi-k2.6", |
| 115 | + "monkeycode/BaiZhiCloud/gpt-5.5", |
| 116 | + "monkeycode/BaiZhiCloud/monkeycode-pro", |
| 117 | + "monkeycode/BaiZhiCloud/qwen3.5-plus", |
| 118 | + // ... 共 37 个 |
| 119 | +] |
| 120 | + |
| 121 | +// L1 匹配示例: |
| 122 | +resolveModel("monkeycode/BaiZhiCloud/qwen3.5-plus") → ✅ 直接命中 |
| 123 | + |
| 124 | +// L2 匹配示例: |
| 125 | +resolveModel("BaiZhiCloud/qwen3.5-plus") → ✅ 命中 L2 |
| 126 | + |
| 127 | +// L3 匹配示例: 危险! |
| 128 | +resolveModel("monkeycode-pro") → ❌ L1 未命中 → ❌ L2 未命中 |
| 129 | + → → 匹配 L3: model.name === "monkeycode-pro" |
| 130 | + → → → **可能匹配到错误的模型!** |
| 131 | +``` |
| 132 | + |
| 133 | +## 5. 3 个隐蔽缺陷 |
| 134 | + |
| 135 | +### 缺陷 1: L3 同名模型可能误配 |
| 136 | + |
| 137 | +```javascript |
| 138 | +// 线上有多个 model 名称相同的模型(不同 provider): |
| 139 | +const model1 = { provider: "BaiZhiCloud", model: "glm-5", ... } |
| 140 | +const model2 = { provider: "BaiZhiCloud", model: "glm-5", ... } // 实际是同一个 |
| 141 | + |
| 142 | +// 但如果扩展到不同 provider: |
| 143 | +// L3 匹配时只查 model 名,可能忽略 provider 差异 |
| 144 | +``` |
| 145 | + |
| 146 | +实际上是:37 个模型都来自 `BaiZhiCloud`,所以 L3 误配概率低。但如果扩展到多提供商,L3 可能匹配到错误 provider 的同名模型。 |
| 147 | + |
| 148 | +### 缺陷 2: `display_name` 可能为空 |
| 149 | + |
| 150 | +```typescript |
| 151 | +// proxy/src/types.ts:67-68 |
| 152 | +name: string |
| 153 | +display_name: string |
| 154 | +``` |
| 155 | + |
| 156 | +从线上数据看,部分模型 `display_name` 为空字符串。当 L4 匹配 `"" === openaiModelId` 时,空字符串永远不会匹配非空用户输入,所以 L4 实际是安全的(只是永远跳过)。 |
| 157 | + |
| 158 | +### 缺陷 3: L6 兜底可能匹配错误模型 |
| 159 | + |
| 160 | +当所有 5 层都失败时,`models[0]` 返回的是**模型列表的第一个**,没有任何筛选逻辑。第一个模型可能是 `pro` 级别的付费模型,basic 用户拿到这个模型后会创建任务失败。 |
| 161 | + |
| 162 | +## 6. 缓存行为 |
| 163 | + |
| 164 | +```typescript |
| 165 | +// proxy/src/models.ts:11-14 |
| 166 | +private cacheTTL: number = 5 * 60 * 1000 // 5 分钟缓存 |
| 167 | + |
| 168 | +fetchModels(): Promise<MonkeyCodeModel[]> { |
| 169 | + if (this.models.length > 0 && Date.now() - this.lastFetch < this.cacheTTL) { |
| 170 | + return this.models // 缓存命中 |
| 171 | + } |
| 172 | + // 缓存失效 → 请求后端 |
| 173 | + const result = await fetch(...) |
| 174 | + this.models = result |
| 175 | + this.lastFetch = Date.now() |
| 176 | +} |
| 177 | +``` |
| 178 | +
|
| 179 | +```mermaid |
| 180 | +flowchart TB |
| 181 | + subgraph Cache["缓存逻辑"] |
| 182 | + CHECK{"模型列表非空<br/>&<br/>距上次请求 < 5分钟?"} |
| 183 | + HIT["✅ 返回缓存<br/>this.models"] |
| 184 | + MISS["❌ 请求后端<br/>GET /api/v1/users/models"] |
| 185 | + UPDATE["更新 this.models<br/>更新 lastFetch"] |
| 186 | + end |
| 187 | + |
| 188 | + subgraph Clear["缓存清理"] |
| 189 | + CLEAR_API["POST /admin/refresh-models<br/>clearCache()"] |
| 190 | + LOGIN_API["POST /admin/login/verify<br/>clearCache()"] |
| 191 | + CLEAR["this.models = []<br/>this.lastFetch = 0"] |
| 192 | + end |
| 193 | + |
| 194 | + CHECK -->|是| HIT |
| 195 | + CHECK -->|否| MISS |
| 196 | + MISS --> UPDATE |
| 197 | + UPDATE --> HIT |
| 198 | + CLEAR_API --> CLEAR |
| 199 | + LOGIN_API --> CLEAR |
| 200 | +``` |
| 201 | + |
| 202 | +## 7. 关键发现 |
| 203 | + |
| 204 | +| 发现 | 详情 | |
| 205 | +|------|------| |
| 206 | +| **6 层回退覆盖所有输入格式** | 从完整 ID 到模糊名称都支持 | |
| 207 | +| **L3 有误配风险** | model 名匹配忽略 provider,多提供商时可能匹配错 | |
| 208 | +| **L4 (display_name) 实际无效** | 线上数据 display_name 多为空 | |
| 209 | +| **L6 兜底可能暴露付费模型** | basic 用户拿到 pro 模型后任务会失败 | |
| 210 | +| **5 分钟缓存合理** | 与模型变更频率匹配 | |
| 211 | +| **缓存仅在 admin 端点触发清除** | 用户无法主动刷新模型列表 | |
| 212 | +| **大小写敏感** | 所有匹配都是 `===`,无法处理大小写差异 | |
| 213 | + |
| 214 | +## 8. 改进建议 |
| 215 | + |
| 216 | +1. **L3/L4 加大小写不敏感** — `.toLowerCase()` 比较 |
| 217 | +2. **L6 兜底按用户 access_level 过滤** — 只返回 basic 用户可用的模型 |
| 218 | +3. **L4 跳过空 display_name** — 避免不必要的比较 |
| 219 | +4. **增加 `resolveModel(modelId, accessLevel)` 参数** — 按用户等级过滤 |
| 220 | + |
| 221 | +--- |
| 222 | + |
| 223 | +**更新状态:** ✅ 新维度已分析完成 |
| 224 | +**更新索引:** docs/08-analysis-rounds/unknown-gaps-index.md |
0 commit comments