1. DSH opencode-go 模型目录静态快照滞后:问题现象与排查场景
如果你在用 DSH(DeepSeek Harness)做编码代理,某天打开模型选择器,发现 opencode-go 路由下只有 16 个模型,而同事那边明明能看到 ox-alpha-free、glm-5.3、gpt-5.6-luna、qwen3.8-max 这些新面孔——这不是你的网络问题,也不是账号权限问题,而是 opencode-go 模型目录静态快照滞后导致的典型症状。DSH 通过@deepseek-ai/dsh-llm-pi-ai插件接入底层 provider 库@earendil-works/pi-ai,其中 opencode-go 路由的模型目录来自发包期固化的静态数据文件dist/providers/data/opencode-go.json,运行期不存在任何更新机制。换句话说,你装的那个 npm 包在打包那一刻,目录就被"冻"住了。
这个问题的核心检索词就是"DSH opencode-go 模型目录静态快照滞后",它属于 LLM 网关类工具在依赖固化场景下的典型数据一致性问题。适合谁看?三类人:一是正在用 DSH 做日常编码、发现模型列表对不上的开发者;二是维护内部 LLM 网关、需要理解"目录型 provider"数据流的运维;三是想学习"幂等数据修补"这套工程方法的同学。我试过在一台独立环境里完整复现并修复,实测下来,问题不是升级依赖就能解决的——把 pi-ai 升到 v0.84.2 也只能从 16 个缓解到 19 个,缺口依然存在。
先量化一下现象。基线测量显示,opencode-go.json(6929 字节)按协议分布收录 anthropic-messages 3 个、openai-completions 12 个、openai-responses 1 个,合计 16 个模型。而上游 live 端点https://opencode.ai/zen/go/v1/models实际返回 29 个,缺口 13 个,全部是快照生成之后上游新增的。更麻烦的是,这个路由横跨三种 wire 协议,导致既有的models/modelOverrides配置机制在结构上无法补充新模型。下面我从快照生成链路、缓存刷新时机、数据源同步差异三个角度拆解根因,再给出可复制的目录快照重建配置与数据修补脚本。
排查时你可以先做一件事:确认自己看到的模型数。打开 DSH 模型选择器数一下,或者直接跑一条命令统计内置目录规模。如果数字明显小于上游端点返回的数量,那基本可以锁定是静态快照滞后。注意,这里不要急着去改配置里的models列表——后面会讲到,显式 models 列表对内置目录有"整体替换"语义,改错了反而会让问题更隐蔽。
2. TaoToken 前置准备:API Key、Base URL 与模型目录的关系
在动手修补之前,先把"目录"和"接入凭证"这两件事分清楚。opencode-go 的模型目录是本地静态文件,决定"选择器里能看到哪些模型";而真正发起请求时用的 Base URL 和 API Key,决定"请求能不能通"。两者是独立的:目录滞后不会导致请求失败,但会让你看不到新模型;凭证配错则会在请求阶段报 401。所以修补目录之前,建议先把接入侧理顺,避免修完目录发现请求还是不通,误判成修补失败。
如果你是通过 TaoToken 这类聚合入口来统一管理模型调用,那么接入信息可以这样准备:Base URL 用https://taotoken.net/api,API Key 在控制台的 API Keys 页面生成,模型 ID 则要和目录里的 id 保持一致。这里有个容易踩的坑:目录里新增的模型 id(比如ox-alpha-free、glm-5.3)必须和你在请求里填的 model 字段完全一致,大小写、连字符都不能差,否则会报"模型不在目录中"。
具体操作路径我列一下,方便你对照:
- 生成 API Key:访问
https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite,创建一个 Key 并复制保存。 - 查看接入文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有 Base URL、鉴权头格式、常见错误码说明。 - 验证模型可用性:
https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite,可以先用对话界面确认某个模型 id 是否真的可用,再决定要不要写进目录。 - 长期编码或跑 Agent:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite,适合需要稳定额度、频繁调用的场景。
这里要强调一个原则:目录修补是"让选择器认识新模型",接入配置是"让请求能打到模型"。两者都做完,才算真正可用。很多人只改了目录文件,结果请求时 model 字段写的是目录里的新 id,但 Base URL 还是旧的、或者 Key 没配,就会在验证阶段看到 401 或 "model not found",然后误以为目录没修好。所以我的建议是:先确认接入侧能通(用模型对话页面测一个已知模型),再动目录文件。
另外,TaoToken 的 API 地址是https://taotoken.net/api,注意这个不带 UTM 参数,是给程序调用的;而上面那些带utm_source=taotoken_aicg_blog_end的链接是给人在浏览器里点的。写配置文件时用前者,别把带参数的链接填进 Base URL,否则可能出现路径拼接异常。
3. 可复制配置:目录快照重建与幂等数据修补脚本
这一节是全文的技术核心,给你可以直接复制的配置片段和脚本。修复遵循四项原则:先备份后修改保证可回滚;数据合并操作幂等,已存在 id 一律跳过,使补丁可在 pi-ai 升级后重复执行且不会回退上游新增;数据文件与用户配置同步修改,保证两条消费路径输出完整;以双路径一致性作为验收标准。
先理解数据流。opencode-go 是 pi-ai 内置的"目录型 provider",其模型目录不来自端点发现,而来自包内静态文件。这个文件是两条消费路径的唯一数据源:
dist/providers/data/opencode-go.json (静态快照,发包时生成) │ flattenModelCatalog() ├──► provider.getModels() ── 路径 P1:请求分发 └──► models.generated.js MODELS └──► getBuiltinModels('opencode-go') ── 路径 P2:dsh-llm-pi-ai resolveRouteModels() ── 用户可见的最终模型目录路径 P1 和 P2 共享同一静态文件,所以目录更新只需改一个文件;但该文件仅在发包期生成,安装后无任何运行期刷新机制,这就是根本原因。
步骤 1:基线确认。先确认 pi-ai 版本与目录规模,建立修复前基线:
# 查看 pi-ai 版本 (Get-Content "$env:USERPROFILE\.dsh\profiles\node_modules\@earendil-works\pi-ai\package.json" | ConvertFrom-Json).version # 输出 0.82.1 # 统计内置目录模型数(期望基线 16) node -e "const j=require(process.env.USERPROFILE+'/.dsh/profiles/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json');let n=0;for(const a of Object.keys(j))n+=Object.keys(j[a]).length;console.log(n)"步骤 2:原始备份。由补丁脚本在首次执行时自动将原始文件复制为同目录opencode-go.json.bak,若已存在则跳过,避免覆盖最早备份。
步骤 3:幂等数据合并。执行补丁脚本,向数据文件追加 13 个条目。算法如下:
// 输入:FILE(opencode-go.json)、ADDITIONS(协议→{模型id→条目}) // 1 j ← JSON.parse(read(FILE)) // 2 若 BAK 不存在:write(BAK, j 的紧凑 JSON) // 仅首次备份 // 3 对 ADDITIONS 中每个 (proto, models): // 4 若 j 无 proto 键:j[proto] ← {} // 5 对每个 (id, entry): // 6 若 j[proto][id] 已存在:跳过并计数 skipped // 7 否则:j[proto][id] ← entry,计数 added // 8 write(FILE, 紧凑 JSON(j));输出各协议计数与总数JavaScript 对象保持键插入顺序,所以写回结果中原有 16 条目的相对顺序与字段值完全不变,新条目追加于各协议组末尾。新增条目的字段级数据按协议分组,例如 anthropic-messages 组新增minimax-m2.5、qwen3.8-max;openai-completions 组新增glm-5、glm-5.3、kimi-k2.5、mimo-v2-pro、mimo-v2-omni、qwen3.5-plus、ox-alpha-free、hy3-preview、deepseek-v4-flash-vision-exp;openai-responses 组新增gpt-5.6-luna、muse-spark-1.2-contributor。每个条目包含 id、name、api、provider、baseUrl、reasoning、input、cost、contextWindow、maxTokens 等字段,部分还带 compat 和 thinkingLevelMap。
步骤 4:配置同步。这一步是本环境特有的关键点。向~/.dsh/settings.yaml的llm-pi-ai.providers.opencode-go.models列表追加 13 个条目,每个条目四字段(id/name/contextWindow/maxTokens),取值与目录一致。插入位置位于原grok-4.5条目之后、sen-sen提供者键之前。配置条目省略的 api/cost/input 字段会经协议回退链从已修补的目录取得,无需声明。
# settings.yaml 追加块(片段) - id: minimax-m2.5 name: MiniMax-M2.5 contextWindow: 204800 maxTokens: 65536 - id: qwen3.8-max name: Qwen3.8 Max contextWindow: 1000000 maxTokens: 131072 - id: ox-alpha-free name: Ox Alpha Free (Unlimited) contextWindow: 1000000 maxTokens: 131072 # ...其余 10 条同理步骤 5:维护脚本部署。将幂等合并脚本与验证脚本部署至$env:USERPROFILE\.dsh\patches\,供后续升级重跑。
步骤 6:验证。执行目录计数、双路径冒烟测试、配置一致性校验三项验证,通过后重启 DSH 使常驻进程重新加载目录。
这里必须提醒一个结构性陷阱:resolveRouteModels()的逻辑表明,一旦路由配置了非空models列表,内置目录即被整体替换,后续所有解析仅基于配置条目进行。如果你的 settings.yaml 里恰好存在一份与旧目录内容一致的 16 条 models 列表,那么仅修补数据文件后,路径 P2 的输出仍将是旧的 16 条配置条目,而非修补后的 29 条目录。所以配置同步是修复生效的必要步骤,不能省。
4. 验证请求与成功结果:双路径冒烟测试怎么做
修完不验证,等于没修。这一节给你一套可复制的验证动作,核心是"双路径一致性"——路径 P1(getModels())和路径 P2(getBuiltinModels())必须返回相同的模型集合,否则说明配置同步没做到位。
验证一:合并与计数。补丁执行输出应为added=13 skipped=0,协议分布为 5 / 21 / 3,合计 29。文件大小从 6929 B 变为 12823 B。再执行第二次,输出应为added=0 skipped=13,这证实了幂等性——重复跑不会重复追加,也不会回退。
验证二:双路径冒烟测试。验证脚本通过动态import()加载 pi-ai 的dist/providers/all.js,分别经两条路径获取目录,执行五项断言:
| 断言项 | 结果 |
|---|---|
| 路径 P1(getModels())计数 | 29 |
| 路径 P2(getBuiltinModels())计数 | 29 |
| 两路径 id 集合一致 | 通过 |
| 必填字段缺失(api/baseUrl/contextWindow/maxTokens/input) | 无 |
| 两路径对应条目字段差异 | 无 |
| 13 个新增 id 存在于两路径 | 通过 |
| 总体判定 | SMOKE TEST PASSED |
验证三:配置一致性。用 js-yaml 解析修改后的 settings.yaml,确认opencode-go.models含 29 个条目、id 无重复;将其 id 集合与目录 id 集合排序后逐项比对,结果应为identical: true。
验证四:实际请求。目录和配置都对了,最后用真实请求确认。你可以用模型对话页面选一个新增模型(比如ox-alpha-free)发一条测试消息,或者用 curl 直接打:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ox-alpha-free", "messages": [{"role": "user", "content": "ping"}] }'如果返回正常的 choices 结构,说明从目录到请求整条链路都通了。如果报 401,检查 Key;如果报 model not found,检查 model 字段是否和目录 id 完全一致;如果报连接错误,检查 Base URL 是否为https://taotoken.net/api。
上述验证完成后重启 DSH,模型选择器应呈现全部 29 个模型,包含此前缺失的ox-alpha-free。这一步的"成功结果"很直观:选择器里能数出 29 个,且新增的 13 个都在。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth
修补过程中会遇到几类典型报错,我按真实错误信息对照着讲,方便你快速定位。
报错一:401 Unauthorized。这几乎总是凭证问题,和目录修补无关。检查三件事:API Key 是否复制完整(有没有多余空格)、请求头是否是Authorization: Bearer <key>、Key 是否已过期或被撤销。如果你用的是 TaoToken,去 API Keys 页面重新生成一个再试。注意,目录修补不会影响鉴权,所以看到 401 先别怀疑补丁。
报错二:local proxy failed。这个错误通常出现在本地代理层,说明请求根本没打到上游。检查 Base URL 是否写成了带 UTM 参数的浏览器链接——程序调用要用https://taotoken.net/api,不要带?utm_source=...。另外检查本地是否有其他进程占用了端口,或者环境变量里有没有残留的代理设置。
报错三:reading choices 相关错误。这类错误说明请求发出去了、也收到了响应,但响应结构不符合预期。常见原因是 model 字段填了一个目录里不存在的 id,上游返回了错误结构,客户端却按正常结构去读choices。解决办法:确认 model 字段和目录 id 逐字符一致,特别是ox-alpha-free这种带连字符的,别写成ox_alpha_free。
报错四:OAuth 相关错误。如果你用的是需要 OAuth 的接入方式,报错往往和 token 刷新有关。检查 token 是否过期、刷新逻辑是否正常。这类问题和目录快照滞后是两回事,别混在一起排查。
报错五:modelOverrides 校验失败。如果你尝试用modelOverrides补充新模型,会看到类似modelOverrides names "ox-alpha-free", which the installed catalog does not describe的错误。原因是modelOverrides只允许覆盖内置目录已存在的 id,未知 id 直接触发校验错误。同理,用models列表补充新模型时,由于条目类型不含api字段,新模型的协议回退链request.api ?? base?.api ?? routeApi会断裂,报model "ox-alpha-free" needs an api; the installed catalog does not describe it。这两项机制失效的原因同源:目录既是数据的来源,又是配置合法性的裁判。所以结论是——必须直接修改包内数据文件。
报错六:升级后模型又少了。补丁作用于 node_modules 内文件,pnpm/npm 重装或 dsh 重装会还原数据文件,需要按步骤 3、4 重新执行。但 settings.yaml 的修改不受重装影响。这也是为什么脚本要设计成幂等的——升级后重跑一遍,已存在的 id 跳过,新增的补上,不会产生回退。
排查时建议按"先接入后目录"的顺序:先用模型对话页面确认接入能通,再检查目录计数,最后看配置一致性。这样能避免把接入问题和目录问题混在一起。
6. 语义一致 CTA:把目录修补接入你的日常编码流
目录修好了,接下来就是把它用起来。如果你只是偶尔查一下模型,用模型对话页面就够了;但如果你要把 DSH 当成日常编码代理,建议把接入配置固化下来,避免每次重装都重新折腾。
具体来说,长期编码或跑 Agent 的场景,可以走 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite,适合需要稳定额度、频繁调用的工作流。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有 Base URL、鉴权格式和错误码的完整说明,遇到报错可以先查这里。API Key 管理在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite,建议给不同项目分配不同的 Key,方便排查和轮换。
最后说一个我踩过的坑:目录修补脚本要放在固定位置(比如~/.dsh/patches/),并且把新增条目的数据内嵌在脚本里,而不是每次手动改 JSON。这样 pi-ai 升级后,你只需要重跑一次脚本,幂等逻辑会自动跳过已存在的条目、补上缺失的,不会把上游新增的模型覆盖掉。这套"原始备份—幂等数据合并—配置同步—双路径验证"的四阶段方法,本质上是在官方根治方案落地前的一种可持续维护手段。等官方更新了生成器、把 live 端点的 id 集合纳入并集校验,或者给PiAiModelProfile加上条目级api字段,你就可以平滑过渡过去。在那之前,这个补丁够用。