# @dsh-external/dsh-omniroute-models > [English](README.md) OmniRoute 模型管理器:一个 DSH host+client 插件,自动拉取 OmniRoute 全部模型及其**模态**, 并用一个可搜索/可筛选/可多选的设置页,管理 DSH 实际能用 OmniRoute 的哪些模型。 > 痛点:[OmniRoute](https://github.com/diegosouzapw/OmniRoute)(OpenAI 兼容网关)模型太多(本机 1000+);DSH 自带「模型」页添加模型 > 没有搜索/筛选,且 discovery 契约不返回模态(`DiscoveredModelView` 只有 id/name/contextWindow/maxTokens)。 > 本插件把 OmniRoute `/v1/models` 的真实 `input_modalities`/`context_length`/`max_output_tokens` > 映射进 `llm-pi-ai.providers..models`,一次勾选即生效,无需重启。 ## 能做什么 - **按供应商筛选**:模型 id 里的命名空间前缀(`/` 前的部分,如 `opencode-go/deepseek-…` → `opencode-go`)会解析出来,UI 有「供应商」下拉 + 独立「供应商」列,可按供应商过滤/查看(`opencode`、`oc`、`auto`、`github`、无命名空间…)。 - **按路由(DSH 供应商)切换**:自动枚举 `llm-pi-ai.providers` 下所有已配置路由(route、displayName、api、baseURL、模型数、是否 OpenAI 兼容),UI 顶部「路由」下拉切换,管理对应端点的模型。 - 一键拉取供应商 `/v1/models`(默认 `http://localhost:20128/v1`),得到每个模型的 `input`(text/image,自动从 `input_modalities`/`capabilities.vision` 推导)、`contextWindow`、`maxTokens`。 - 在 DSH 设置面板新增「OmniRoute 模型管理」页:搜索框 + 模态筛选 + 全选匹配/全不选 + 逐行勾选。 - 「保存所选」整体替换 `llm-pi-ai.providers..models`(即 DSH 恰好能用勾选的这些), 经由官方 settings seam 写入 `settings.yaml`,下一请求立即生效;`apiKeyEnv/api/baseURL` 等兄弟字段保留。 - 已启用的模型预勾选,并在重复拉取时用存储值覆盖(不覆盖你在 `settings.yaml` 手改的模态/容量)。 - 模态只取 DSH 认识的 `text`/`image`(`video`/`audio` 等被裁剪),避免写入 DSH 无法处理的模态导致校验失败。 - **联网搜索**:把 DSH 内置 `web_search` 接到两个来源之一(在设置页「联网搜索」卡选择)——**OmniRoute 聚合搜索**(`POST /v1/search`,Tavily/Brave/Exa/Serper/Ollama 等)或 **DeepSeek 官方原生搜索**。OmniRoute 来源未启用、或来源选了 DeepSeek 官方时,`web_search` 自动回落 DeepSeek 官方搜索,不会不可用。OmniRoute 来源的 key 是可选的(OmniRoute 用自己存的各 provider 密钥);若端点确实要求 key,按此顺序解析:字面量 `searchApiKey` → DSH credentials 里由 `searchApiKeyEnv` 命名的 ref → 进程环境变量(与官方 `dsh-web-search-deepseek` 同款)。 ## API key 要求(模型 provider vs. 联网搜索) **现在每个 OmniRoute 调用都要有效 key**——自 OmniRoute 3.8.50 起,连 `GET /v1/models`(模型目录) 也拒绝匿名请求(401,`REQUIRE_API_KEY` 与否不影响)。 - **拉取目录 / 保存白名单**:插件按此顺序解析路由的 `apiKeyEnv`:DSH 凭据库优先(与 `llm-pi-ai` 聊天用的是同一 seam)→ 进程环境变量 → `?apiKey=` 查询参数。把 OmniRoute 网关 token 存一次 DSH credential(或设好环境变量),目录拉取和聊天就都能用。 - **实际聊天**:DSH 的 `llm-pi-ai` 适配器(经 `@earendil-works/pi-ai`)**强制要非空 key**,否则抛 `No API key for provider: `。在「模型」页给 provider 填 API key(DSH credential ref)。 `pi-ai` 也接受 provider 级 `headers.authorization`,但那是非标准路径,不推荐。 **联网搜索** provider 的 key 才是真正可选(OmniRoute 匿名服务 `POST /v1/search`,用自己存的上游 密钥)——见上面的 `联网搜索` 条目。 ## 内部 - **Host**(`src/index.ts`):注册两条同源 JSON 路由(`ctx.webServer.register`): - `GET /omniroute-models/api/catalog?provider=` — 枚举 `llm-pi-ai` 供应商目录(`providers`)→ 读该路由当下模型 → 请求 `GET {baseURL}/models` → 返回 `{ configured, compatible, provider, displayName, api, baseURL, providers:[...], models:[{id,name,input,contextWindow,maxTokens,enabled}], enabledCount, totalCount }`;仅当路由协议无法按 OpenAI 形状 `/v1/models` 列举时返回 `compatible:false`;`anthropic-messages`/`openai-responses` 与 `openai-completions` 一样照常拉取(OmniRoute 网关的模型目录与所用协议无关,恒为 OpenAI 格式)。 - `POST /omniroute-models/api/apply` — `{ provider, models:[{id,name,input,contextWindow,maxTokens}] }`, 校验后经 `ctx.settings.mutate('llm-pi-ai', [{op:'set', path:['providers',provider,'models'], value}], expectedRevision)` 写入; 冲突返回 `SETTINGS_CONFLICT`、schema 拒绝返回 `settings-rejected`。 - 组件**不注册/不拥有** `llm-pi-ai` 命名空间(那是适配器 `@deepseek-ai/dsh-llm-pi-ai` 的);只读/写既有命名空间。 - **Client**(`src/client/index.tsx`):订阅 `settings.section` slot(id `omniroute-models`,order 15), 渲染 React 表格;全部数据走同源 `fetch`,不依赖 `ctx.api`/LLM 远程。 - **联网搜索 provider**(`src/index.ts`):插件自有 `omniroute-models` settings 命名空间存搜索配置,开启后向 `ctx.web` 注册 `WebSearchProvider`(id `omniroute`),DSH 的 `web_search` 即走 OmniRoute;配置/测试路由 `GET|POST /api/search-config`、`POST /api/search-test`。该区段经 `installSettingsSection` 注册(会等待 `settings` 服务就绪),插件激活顺序不会与之竞态;`available()` 只要求 `enabled` + http(s) 网关地址,key 按需惰性解析。 - 只列出**可对话**模型:网关 /v1/models 会混入 `embedding`/视频/图像生成等,host 的 `fetchCatalog` 按 `type`(`embedding|video|image|audio|music|rerank`)过滤,避免当文本 chat 模型误列/误存。 OmniRoute ≥3.8.50 还会把自家搜索引擎列为伪模型(`brave-search/web`、`tavily-search/news` 等, 无 `type`),host 按 `-search` 供应商后缀过滤。 ## i18n - 采用 DSH 官方 locale 机制(`@deepseek-ai/dsh-client-locale`),namespace `omniroute-models`。 - 字典在 `src/client/locales.ts`:`zh` 为 key 集源,`en: Record` 编译期强制补全。 - `client` 的 `inject = ['slots','locale']`;`apply` 里 `ctx.effect(() => ctx.locale.register(NS, { zh, en }))`, 再 `ctx.locale.bind(NS)` 得 `t`;`settings.section` 注册带 `inject: () => ({ t })`、`label: () => t('nav')`, 组件从 `props.t` 取文案,语言切换(DSH 设置 → 语言)即时刷新。 - **host 文案本地化**:catalog/apply 的确定性文案返回 `code`(如 `catalog.notConfigured`、 `apply.duplicateId`)+ `params`,保留中文 `message` 供 curl/开发;client 的 `hostMessage` 仅翻译**已知** `code`(`host.*` 键),未知回退原文,故 `SETTINGS_CONFLICT`/`settings-rejected` 等 异常码不会被误译。 - 新增第三种语言:在 `locales.ts` 加对应字典并把它加进 `dictionaries`;在 `apply` 的注册循环里补该 locale。 ## 构建 ```bash # 需要 node + typescript + tsdown(可用 dev_build_plugin,或手动): npm run build # host → lib/index.js npm run build:client # client → lib/client.js ``` ## 安装 / 注入 - 本环境(super-injector):`dev_inject_plugin /path/to/dsh-omniroute-models`,注入即 host+client 完整生效。 - 生产 bundle(GitHub,固定版本):`dsh plugin --profile web add github:publieople/dsh-omniroute-models#v0.2.5`,然后重启目标 profile。 ## 使用 1. 刷新 DSH 页面(运行时注入的 client 模块需页面重载后才引导)。 2. 打开「设置」→ 右侧出现「OmniRoute 模型管理」入口。 3. 搜索 / 按模态筛选 / 勾选要用的模型(视觉模型会显示 `text + image` 徽标)。 4. 点「保存所选」→ 写入 `settings.yaml` 的 `llm-pi-ai.providers.omniroute.models`。 5. 用一个刚勾选的模型发一条消息验证(视觉模型可附带图片)。 ## 验证 - host:`curl "http://127.0.0.1:3080/omniroute-models/api/catalog"` 应返回模型列表 + 模态 + enabled。 - 写入:`curl -X POST .../api/apply -d '{"provider":"omniroute","models":[{...}]}'`。 - 检查 `~/.dsh/settings.yaml` 的 `llm-pi-ai.providers.omniroute.models`(含 `input`/`contextWindow`/`maxTokens`,兄弟字段保留)。