makecindy / makecindy/cindy

[Feature] 自定义供应商模型导入与选择缺少能力护栏——非 chat 模型(如 gemini-3.1-flash-image)可被选为聊天 Agent 并静默无法调用 Ghost

Open
#101 1 comment 0 reactions 0 assignees View on GitHub
feature
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.