dsh-model-visibility
In English: a DeepSeek Harness bundle plugin that adds a Settings → Model visibility page. Every model of every provider can be switched off, so it disappears from the composer's model menu; every remaining model is labelled with what it can actually do — reasoning support and the reasoning-effort levels the adapter publishes, context window, input modalities, and whether it is a chat, image, video, embedding, rerank, audio, classifier or translation model. Those facts are merged from six sources in priority order (settings document, adapter, the relay's own
GET /models, a built-in pi-ai / models.dev catalog, model-name inference, a vendor-correction table) and the page names the source on hover, with solid borders for declared values and dashed for inferred ones. A switch, on by default, makes relay routes whose models declare text-only input accept images in memory, without editing any configuration file. The rest of this README is in Chinese.
在 DeepSeek Harness 的设置里管理「哪些模型出现在聊天输入框的模型菜单中」,按 Provider 分组一键显示/隐藏,并在每个模型下面标注它的真实能力:是否支持推理、思考强度档位、上下文窗口、输入/输出模态,以及它是对话模型还是生图 / 生视频 / 嵌入 / 重排 / 音频 / 审核 / 翻译模型。生图、生视频等类型用不同颜色标注;厂商文档能证明目录写错的行会显示 官方校正。插件还能把「显示为多模态」变成「输入框真的能发图」:对中转站路由做内存态能力对齐,不修改任何配置文件。
一个 DSH bundle 插件:Host 端负责持久化偏好、读取 Provider 配置、探测中转站模型列表,并把能力目录投影给浏览器;浏览器端过滤模型目录并渲染设置页。它不接管输入框的模型座位,因此官方的「模型 / 推理等级」两级菜单、推理等级调整,以及像 dsh-codex-effort-slider 那样改造这一行的插件都保持可用。


为什么需要它
DSH 官方的 session.modelCatalog 只把模型 id / 名称 / 推理档位投影给浏览器,没有上下文窗口、输入模态和输出类型。自定义中转站通常这样配置:
yujianwudi:
api: openai-completions
baseURL: https://example.net/v1
defaultInput: [text]
models:
- id: grok-4.7
input: [] # 不是“纯文本”,只是“没声明”
于是 registry 会把 defaultInput: [text] 当成模型能力,Grok / Claude / GPT / Gemini 全部显示成「纯文本」。本插件不再把这种默认值当成事实:它读取真实的中转站模型列表、用内置模型目录补全,并把「适配器声明」和「补全值」在 UI 上区分开。
功能
- 设置 → 模型显示:按 Provider 分组列出全部模型,每个模型一个开关;关闭的模型立即从对话菜单中消失。
- 能力标签(按可信度分层):
支持推理/不支持推理/推理未知,以及适配器公布的全部思考强度档位(默认档位在悬停提示里);上下文 1M/上下文 200K(悬停显示精确 token 数与来源);多模态/纯文本/模态未知(输入模态);生图、生视频、嵌入、重排、音频、审核、翻译:生成类 / 非对话模型单独标记,并各用一种颜色(生图蓝、生视频绿、嵌入橙、音频紫、重排红、审核玫红、翻译灰)。
- 官方校正:内置目录是快照,个别行会错(例如
glm-5.3被标成多模态,而厂商文档写明它只收文本)。这类行用厂商文档校正,页面显示官方校正标签,来源是厂商官方文档,不会被误当成适配器声明。 - 让输入框按能力标注放行图像(默认开启):中转站的
input通常是空的,适配器按defaultInput: [text]构造模型,于是“页面显示多模态、发图却被拒绝”。开启后 Host 会在内存里给这类 pi-ai 中转站模型补上图像能力,让发送前的准入检查和请求序列化都能通过;只补插件判定为可输入图像的对话模型,不改任何配置文件,关闭开关即恢复原状。 - 来源可见:实线标签来自适配器、中转站或厂商文档;虚线标签来自内置模型目录或模型名推断。悬停任一标签可看到「来源:适配器配置 / 中转站模型列表 / 内置模型目录 / 模型名推断 / 厂商官方文档」。
- 生成类模型只标类型:生图 / 生视频 / 嵌入等模型不再显示推理档位、上下文或「纯文本」,只显示
生图、分辨率 4K、可改图这类真正相关的信息。 - 筛选:顶部有
全部 / 对话 / 生图 / 生视频 / 其它快捷筛选,每个按钮带数量。 - 分组汇总:每组标题显示
已显示/总数、对话 n · 生图 n · 生视频 n · 其它 n,以及中转站探测结果(读到了多少模型、多少公布能力字段,未公布会明确写出来)。 - 分组一键开关:每组标题右侧是「全组隐藏 / 全组显示」;筛选状态下按钮只作用于当前筛选结果。
- 对话菜单只显示开启的模型:隐藏模型连同变空的分组一起消失;当前会话正在使用的模型即使被关闭也会保留,因为收起态按钮靠这份目录显示模型名。
- 偏好落在 Host 设置文档:写入 profile 的
cordis.patch.yml(- id: model-visibility → hidden: [...]),刷新、重启、切换会话都保留,不依赖浏览器本地存储。 - 设置不会因为插件重装/配置层重建而丢失:Host 端额外把同一份列表镜像到
$DSH_HOME/dsh-model-visibility/hidden.json,并用它给hidden的 schema 默认值播种。详见「持久化」。
安装
# 目录方式(开发):profile 里 link 这个目录
# 用 plugin_manager 安装:
# install_bundle target = D:\\code\\dsh-model-visibility
本地链接安装需要目录里有依赖:
npm install
安装后刷新页面(或重启 DSH)即可看到「设置 → 模型显示」。
能力标注从哪里来
答案按下面的优先级合并。每一层只填前一层不知道的字段,最终记录里带 sources,页面据此决定实线 / 虚线。
| 优先级 | 来源 | 什么时候可信 | 页面标记 |
|---|---|---|---|
| 1 | config |
设置文档里该模型显式写了 input、contextWindow、maxTokens,或 Provider 的 defaultInput 显式包含非文本模态 |
实线 |
| 2 | adapter |
适配器不是从中转站配置(内置目录),或该模型不在 Provider 的 models 列表里 |
实线 |
| 3 | provider |
中转站自己的 GET {baseURL}/models 返回了 architecture.input_modalities / input_modalities / capabilities / type / supported_endpoint_types 等字段 |
实线 |
| 4 | catalog |
内置 pi-ai / models.dev 快照里有同名模型(会规范化 openai/、~、:free、-4k 等前缀后缀) |
虚线 |
| 5 | inferred |
模型名明确带家族特征(claude / gpt-4o+ / gemini / grok-4+ / vl / vision / image / video / embed …) |
虚线 |
| 校正 | official |
厂商文档明确指出该行写错(见 lib/capability-corrections.js) |
实线 |
| — | unknown |
以上都没有:显示「未知」,不会把中转站的 defaultInput: [text] 当成纯文本 |
灰色 |
official 是校正层,不参与上面的常规优先级:合并完成后,如果厂商文档指出该行错误,且当前值来自 provider / catalog / inferred / unknown,就替换成 official;来自 config(用户显式写入的 input)或内置适配器声明的值保持不变。中转站列表经常直接抄目录,所以也允许被校正。
官方校正
lib/capability-corrections.js 是一张很小的、只收厂商文档能证明的修正表。首条是 glm-5.3:目录把它标成 text+image,但智谱文档写明 GLM-5.3 只处理文本,原生多模态的是 GLM-5.3-Flash / FlashX。被校正的行显示 官方校正 标签,悬停可以看到说明和文档链接;Provider 配置里显式写了 input 的仍然优先,中转站列表则可以被打回。
输入图像对齐
问题:llm-pi-ai 的模型对象由 entry.input ?? base.input ?? defaultInput 构造。中转站的模型通常写 input: [](表示“未声明”),于是落到 Provider 的 defaultInput: [text];会话发送前的准入检查和 pi-ai 自己的序列化都会据此拒绝图像,即使中转站和页面都认为它支持图片。
做法:Host 端只在 pi-ai 适配器实例上包装 modelOf(snapshot, provider, model)。当能力合并(与页面同一套证据)判定该模型可输入图像时,给返回的模型对象补上 image;其它适配器(DeepSeek Messages 等)不动。设置页顶部的「让输入框按能力标注放行图像」开关对应 Host 配置 applyInput(默认开启,volatile)。关闭后请求侧完全按适配器读到的 input 声明。包装是内存态、可逆的,不写设置文档、不改路由配置;插件卸载时恢复。glm-5.3 这类被官方校正为纯文本的模型不会被放行。
中转站探测
Host 端通过 settings.describe() 读取配置了 baseURL 与 apiKeyEnv 的 Provider,然后:
- 用
ctx.credentials.resolve(apiKeyEnv)在每次探测时解析密钥(不缓存、不写入、不返回给浏览器); GET {baseURL}/models(base 不带/v1时再补一次/v1/models),带Authorization: Bearer或 Anthropic 的x-api-key;- 同源尝试
GET {origin}/api/pricing(New API 的公开价格接口),读取supported_endpoint_types来判断生图 / 生视频 / 嵌入; - 只保留能力事实(模态、上下文、最大输出、推理、类型),不保留响应正文;失败只记录诊断。
安全约束:不跟随重定向(避免把 Authorization 带到别的域)、4 MB 响应上限、每请求 3.5 s 超时、Provider 并发探测、结果缓存 10 分钟。关闭探测:设置行里 probe: false,或环境变量 MODEL_VISIBILITY_PROBE=0。
内置模型目录
lib/model-catalog.js 是从当前安装的 @earendil-works/pi-ai(models.dev 数据,2026-10-07 快照)蒸馏出的紧凑表:652 个模型,包含 type(chat / image / classifier)、input / output 模态、reasoning、contextWindow、maxTokens。重新生成:
# 先从桌面安装包里提取 pi-ai 数据(只需做一次)
node .tools/asar.mjs extract "<DeepSeek Harness>/resources/app.asar" `
"dsh/node_modules/@earendil-works/pi-ai" ".tools/extract/pi-ai"
# 生成 lib/model-catalog.js
node scripts/build-catalog.mjs --data .tools/extract/pi-ai
也可以把 --data 指向任何 pi-ai 包根目录或它的 dist/providers/data。内置目录只补缺,不会覆盖适配器或中转站明确声明的事实。
路由
GET /api/dsh-model-visibility/models[?fresh=1]
Host 端遍历 ctx.llm.listProviders() → listModels() → resolveModelInfo(),再叠加设置文档里的 Provider 配置与中转站探测结果,返回:
{
"ok": true,
"value": {
"groups": [{
"id": "yujianwudi",
"name": "TianJi",
"models": [{
"id": "grok-4.7",
"name": "grok-4.7",
"kind": "chat",
"inputModalities": ["text", "image"],
"contextWindow": 500000,
"reasoning": { "efforts": [...], "defaultEffort": "medium" },
"reasoningSupported": true,
"sources": { "kind": "catalog", "modalities": "catalog", "context": "catalog", "reasoning": "config" },
"catalog": { "id": "grok-4.7", "provider": "xai", "generatedAt": "..." }
}]
}],
"failures": [],
"providers": [{ "id": "yujianwudi", "status": "ok", "models": 58, "capabilities": 14 }],
"catalog": { "generatedAt": "2026-10-07T22:01:28.515Z", "piAiVersion": "1.1.0", "models": 652 }
}
}
?fresh=1 绕过两层缓存(能力快照 15 s、中转站探测 10 min),用于页面「刷新」按钮与 llm/adapters-updated。浏览器端优先走这条路由;如果 Host 半还没加载(例如刚升级插件但页面还是旧 Host),会自动退回官方 session.modelCatalog(),页面顶部显示「模型能力信息暂不可用」。
结构
| 文件 | 作用 |
|---|---|
package.json |
bundle 清单、dsh.client 声明、./locale/*.json 与 ./icon 导出;含 npm test |
cordis.patch.yml |
插入 Host 行 model-visibility |
index.js |
Host 端:hidden 持久化 + 影子副本 + 能力路由 + 探测缓存失效 |
lib/model-info.js |
能力合并:证据优先级、describeCatalog、15 s 缓存 |
lib/provider-probe.js |
读取 Provider 配置、解析凭据、探测中转站、10 min 缓存与诊断 |
lib/capability-catalog.js |
内置目录匹配、模型类型与家族模态启发式 |
lib/capability-corrections.js |
厂商文档校正表(如 GLM-5.3 纯文本),带来源链接 |
lib/image-input-gate.js |
在 pi-ai 适配器上补图像输入能力(请求侧对齐,可逆) |
lib/model-catalog.js |
由脚本生成的 models.dev 快照(勿手改) |
scripts/build-catalog.mjs |
从 pi-ai 数据重新生成 lib/model-catalog.js |
client.js |
浏览器端:设置页、筛选、能力标签(按类型着色)、就地包装模型目录 store |
locale/*.json |
插件列表里的标题与描述 |
test/*.test.mjs |
能力合并、目录匹配、探测解析与缓存、图像输入闸门的单元测试 |
icon.svg |
插件列表图标 |
实现要点
过滤模型
- 不注册
conversation.input.model座位:官方选择器仍是唯一占位者。过滤发生在会话目录的 store 上——getSnapshot()/subscribe()被就地替换成过滤版本,因为官方座位可能在本插件加载前就捕获了同一个 store 对象,替换对象本身够不到它。 - 目录来自共享的
modelDirectories.directoryFor(sessionId);插件同时包装该方法,让之后新建的目录也走同一套 store 补丁。 - 隐藏项用
provider/model字符串作为不透明 key,Set 化后同时用于设置页与过滤。 - 设置页的读写走
ctx.configForms.get('model-visibility'),与 Host 设置文档保持同一 revision;写入失败会在页面上提示。
能力合并
describeResolvedModel只负责把resolveModelInfo()的结果变成纯对象;真正的合并发生在mergeCapabilities(纯函数,单测直接覆盖)。profile.models里为空数组的input被视为「未声明」,而不是「仅文本」。只有显式写了非空input,或defaultInput明确包含非文本模态时,才把模态记为config。- 上下文窗口同理:中转站的
defaultContextWindow: 262144是 DSH 默认值,不当事实使用;目录里有 500K / 1M 就用目录值。 - 模型类型优先取中转站返回的
type/supported_endpoint_types,其次取内置目录的type,最后用模型名关键词判断;生图 / 生视频 / 嵌入 / 重排 / 音频 / 审核 / 翻译会走generationTags,不显示推理档位与文本标签,并各自带颜色。 - 所有字段都是可选的;不知道就显示未知,不猜。
- 厂商校正只在
catalog/inferred/unknown上生效;显式config、适配器或中转站声明优先。被校正的模型带correction记录,页面渲染官方校正标签。
请求侧图像准入
createModelInfoService的imageInputAllowed(provider, model)与页面共用mergeCapabilities,所以标签和准入不会各说各话;判定结果短暂缓存(30 s),invalidate()时清空。installImageInputGate只识别同时具备modelOf/profileOf/current的 pi-ai 适配器(DeepSeek Messages 等不受影响),包装其modelOf并保留原对象;适配器重建时由llm/adapters-updated调用refresh()。- 只有在 Provider 有设置档(即中转站路由)时才参与对齐;内置适配器继续用自己的声明。
缓存与失效
- 能力快照 TTL 15 s,
?fresh=1绕过;并发的页面读共享同一个 in-flight Promise。 - 中转站探测 TTL 10 min;
?fresh=1也会强制重探。 llm/adapters-updated、credentials/record-updated、credentials/reference-updated:能力缓存与探测缓存一起失效。settings/document-updated:能力缓存失效,但探测缓存只在 Provider 路由指纹(id / baseURL / apiKeyEnv / 模型 id 集合)真的变化时才失效。因此「隐藏一个模型」不会触发网络探测。
持久化
cordis.patch.yml 是「用户覆盖层」,DSH 会整层重建它:
- profile 自愈(
sanitizeProfile)会把整份 patch 改名成.bak-<ts>后从空文件重来; - 插件重装 / 升级会重置该行的
config; - 设置写入时若新值等于继承值(例如隐藏列表为空),DSH 会直接删掉这一行。
任何一种发生,hidden 都会退回默认值 [],表现出来就是「插件重新加载后所有模型又都显示了」。
所以 Host 端做了两件事:
- 镜像:每次
hidden变化(loader/volatile-update/settings/document-updated)都原子写入$DSH_HOME/dsh-model-visibility/hidden.json;插件加载和卸载时各同步一次。 - 恢复:
hidden的 schema 默认值在模块加载时从该文件读取,所以 patch 层被清空后, 插件重新加载得到的是上次的列表而不是空列表。
存储文件缺失或损坏时按空列表处理,行为与之前一致;需要重置时删掉该文件即可。 文件是全局的(不按 profile 区分),多个 profile 共用同一份隐藏列表。
测试
npm test
覆盖:证据优先级、默认文本不被当成事实、中转站探测解析(OpenRouter / pi-ai / New API 三种 dialects)、凭据缺失与 401、/v1/models 回退、缓存 TTL / fresh / 设置变化只失效必要缓存、模型类型与家族模态、目录别名匹配、推理档位清洗、官方校正、请求侧图像准入与适配器闸门的可逆性,以及 Host 接线(能力路由 + 校正 + applyInput 开关)。
GUI 冒烟测试(改完代码后):
- 改
client.js→ 页面Ctrl+R,打开 设置 → 模型显示,确认标签、筛选、虚线来源、生图 / 生视频行的颜色与左侧色条。 - 改
index.js/lib→ 需要 Host 重载:重启 DSH,或让 profile 重新组合(桌面版 HMR 会在下一次 profile 重组时替换已改动的已链接包)。 - 重载后确认页面底部出现「中转站探测:…」和「内置目录:…」两行,且 Grok / Claude / GPT / Gemini 显示真实上下文与多模态;
glm-5.3显示纯文本+官方校正。 - 打开对话输入框的模型菜单,确认被关闭的模型没有出现、当前模型仍显示。
- 给一个页面标注为
多模态的中转站模型发一张图:应直接发送成功;关闭「让输入框按能力标注放行图像」后应恢复为「模型不支持图像输入」。
已知限制
- 内置目录是快照:
lib/model-catalog.js生成于 2026-10-07,之后新增的模型要靠中转站探测或模型名推断;用scripts/build-catalog.mjs可以随时重新生成。 - 官方校正是手工维护的小表:只收厂商文档能证明的错误行,不会自动发现所有快照错误;遇到新的错误行在
lib/capability-corrections.js增加条目即可。 - 中转站不一定公布能力:多数中转站的
/v1/models只有 id;New API 的/api/pricing经常被关闭(403)。这时只能靠内置目录和模型名家族规则。 - 模型名推断是保守的:只对明确的家族生效;不在目录里、名字也没有特征的自定义模型会显示「模态未知 / 上下文未知」,而不是猜一个。
- DSH 的模态契约只有 text / image:视频 / 音频输入无法进入官方请求准入;请求侧对齐也只处理
image。 - 请求侧对齐只覆盖 pi-ai 中转站:内置适配器(DeepSeek Messages 等)与其它协议仍按自己的声明工作;对齐不写配置,插件卸载即恢复。
- 适配器声明优先:如果 Provider 明确写了
input: [text](例如 opencode-go 的模型),页面会尊重这个声明,即使内置目录认为它支持图像。想改就以 Provider 配置为准。 - 上下文按十进制缩写(262144 →
262.1K),精确值在悬停提示里。 - 目录是实时注册表快照;如果某个动态免费模型源刚好在刷新,页面模型总数可能和输入框里较旧的目录缓存略有出入,点「刷新」即可对齐。
- 探测需要 Host 的
credentials服务;没有它时只跳过探测,页面仍显示适配器 + 内置目录。 - 中转站诊断信息会显示在分组标题旁,但不会显示 baseURL 全文、请求头或任何密钥。
隐私
- 探测只向设置文档里该 Provider 的
baseURL同源地址发请求;不跟随重定向。 - 密钥只在 Host 内存里用于构造一次请求头,不写日志、不进路由响应、不进诊断。
- 页面只拿到能力事实和来源标签。
License
MIT
No comments yet. Be the first to write one.