[Feature] 自定义供应商模型导入与选择缺少能力护栏——非 chat 模型(如 gemini-3.1-flash-image)可被选为聊天 Agent 并静默无法调用 Ghost
- Dominant language
- TypeScript
- Stars
- 2.7k
- Forks
- 395
- Avg merge
- 21h 48m
- Merged PRs (30d)
- 776
Description
## 现象
使用自定义供应商下的 `gemini-3.1-flash-image` 作为聊天模型时,已安装且授权正常的 XD Atlassian Ghost 无法被调用:UI 已生成「调用插件 · GHOST / XD Atlassian」召唤卡,但模型不发起 `mcp__cindy__ghost_call`,直接回复"工具集中没有 Jira 工具"。同环境切换 Claude Opus / Sonnet 后插件调用正常。
根本原因:自定义供应商的「获取模型列表」把 OpenAI 兼容 `/v1/models` 的**全量条目**(image / embedding / TTS / realtime / video 等)原样导入为 claude-code 聊天模型。上游 LiteLLM `/model_group/info` 明确该模型 `mode=image_generation`、`supports_function_calling=false`——它本就无法执行 tool use,但在 Cindy 的模型选择器里它和正常聊天模型长得完全一样,用户无从自诊断,只会误以为插件坏了。
## 根因定位过程摘要
排查中先后否定了两个假设:
1. ~~服务端把 image 模型下发到聊天列表~~ —— `GET /api/model-access/models` 实际只返回 `gpt-5.6-sol`,本地日志多次 `xd gateway models synced: 1`,XD 网关目录正确。
2. ~~历史会话/草稿持久化了已失效模型~~ —— 本机 SQLite `sessions.model` / `messages` / Local Storage 中该模型 ID 精确串 0 命中。
唯一持久化命中点是 `custom_providers` 表:一条用户添加的自定义供应商(baseUrl 指向 LiteLLM 网关),其 `runtimes."claude-code".models` 是保存时 `/v1/models` 的全量快照。上游 `/model_group/info` 的分类本身是正确的,问题在客户端导入与消费链路零护栏。
### 完整传播链
1. 添加自定义供应商 → 「获取模型列表」→ `fetchProviderModels` GET `{base}/v1/models`
(`apps/desktop/src/main/maker-host/provider-model-fetch.ts:95-129`;端点派生 `generic-oauth.ts:538-541`)
2. `parseModelsListResponse` 解析所有 id,仅按 id 去重,无 mode / 能力过滤
(`apps/desktop/src/main/maker-host/generic-oauth.ts:589-627`)
3. 勾选弹层全量列出、默认可全选,保存时 `validateCustomProviderConfig` 只校验结构 → 全量快照写入 `custom_providers.runtimes`
(`apps/desktop/src/renderer/components/settings/CustomProviderDialog.tsx:476-575`;`apps/desktop/src/main/maker-host/custom-provider-store.ts:135-164, 316-359`)
4. `refreshCustomProvidersIntoCatalog` → `buildUserProvider.toCatalogModel` 给每个 id 无条件补 chat 旗舰默认元数据(contextWindow 200k、efforts、`defaultEnabled: true`)→ 合入 active catalog
(`apps/desktop/src/main/maker-host/createDesktopProviderService.ts:253-263`;`packages/model-providers/src/user-provider.ts:45-62, 83-112`;`apps/desktop/src/main/maker-host/active-catalog.ts:343-346`)
5. `ModelSelector` 仅按「已连接 + 用户可见性开关」渲染,`CatalogModel` 类型没有 modality / tools 字段可校验
(`apps/desktop/src/renderer/components/new-chat/ModelSelector.tsx:507-544`)
6. `SET_MODEL` / 发送链路仅类型校验 + 路由存在性校验(自定义路由无 `modelPrefixes` → 恒通过),请求字节透传到上游
(`apps/desktop/src/main/maker-ipc/register.ts:5727-5763`;`runtimeSetModel.ts:99-257`;`maker-host/provider-route.ts:213-227, 253-278`)
### 本可拦截但未拦截的点
| 环节 | 缺口 |
|---|---|
| `parseModelsListResponse` | 忽略响应中可能存在的 `mode` / `model_info` 元数据 |
| 模型拉取勾选弹层 | 无非 chat 模型提示/分组,默认全选 |
| `buildUserProvider.toCatalogModel` | 任意 id 一律按 chat 模型建档且默认可见 |
| ModelSelector / send / SET_MODEL | 全链路无能力门禁 |
## 建议改进(分层,可拆子任务)
1. **导入侧(低成本高收益)**:`fetchProviderModels` 拉取时若上游是 LiteLLM(探测 `/model_group/info` 可用),用 `mode === 'chat' && supports_function_calling` 标注/过滤;至少在勾选弹层按名称启发式(`-image`、`embedding`、`tts`、`realtime`、`asr` 等)给出"疑似非聊天模型"分组提示并默认不勾选。
2. **目录侧**:`CatalogModel` 增加可选 `modality` / `supportsTools` 字段;`toCatalogModel` 对启发式命中的 id 不再 `defaultEnabled: true`。
3. **运行时侧(体验提示,非硬阻断)**:会话内模型不支持工具而用户召唤 Ghost 时,给一条确定性 UI 提示(代码判断,非靠模型自述,符合仓规「规则 9:优先用代码保证确定性」)。
4. **兼容性**:新字段全部可选、缺省视为"未知=允许",不影响现有自定义供应商配置;不动 XD 网关目录链路。
## 验证
- 单测覆盖 `parseModelsListResponse` 的过滤/标注分支 + 勾选弹层分组渲染。
- 手动对 LiteLLM 网关重走「获取模型列表」,确认 image / embedding 模型被标注或默认不选。
## 远程连接 / 手机版(规则 26)
改动集中在 desktop 设置与模型目录层;device-link / 手机版消费的是被控端 capabilities(`availableModels`),随目录修正自动受益。实现 PR 中需按规则 26 写明结论。
## 用户侧临时规避
Settings → Providers → 编辑对应自定义供应商,把模型勾选收敛到 chat 模型(或在模型可见性开关里关掉非 chat 条目),选择器即不再出现 `gemini-3.1-flash-image`。
Contributor guide
Research direction
Start with fetchProviderModels and parseModelsListResponse in apps/desktop/src/main/maker-host, then trace CustomProviderDialog, buildUserProvider.toCatalogModel, and ModelSelector. Review the existing custom-provider tests before choosing the import, catalog, and runtime boundaries. Done means non-chat models are clearly handled, are not silently presented as chat-capable, and the relevant parsing and selector behavior is tested.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, sqlite, typescript
- Domain
- ai, desktop, testing-qa
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100