模型 id 不该写死在 TypeScript 里
· 5 分钟阅读
在整个系统里,模型 id 是变化最快的事实。厂商会改名、会下线、会发布带日期的版本、会悄悄改变不带日期的别名所指向的模型,还会根据请求来自哪个账号限制其中一半的访问。把它编译进一个包里,就等于保证这个包几周之内就会出错——不是那种显而易见的坏法(那样至少还能算个 bug 报告),而是一种特定的错法:模型选择器给人列出一个他的 key 根本调用不了的模型。
所以模型目录是一个 JSON 文件。有意思的地方不在于它是 JSON,而在于它底下的分层如何坦白各层的确定性。
五层,后面的覆盖前面的
0. pi-ai's built-in models whatever it knows about the catalogued vendors
1. the bundled catalog.json 27 providers, 6 verified models
2. ~/.oharness/catalog.json your edits, and what discovery found
3. a catalog path in config
4. providers / models in config
5. /models refresh asks the vendor directly
把那一列数字当作可信度排序,而不是加载顺序来读。第 0 层是别人对世界的一份快照。第 1 层是我们愿意明确声明的内容。第 5 层是厂商此刻就你的账号给出的回答——它优先,因为三个月前我们发布的任何东西,都不可能比你即将调用的那个端点更了解你的权限。
内置层包含 27 家厂商和 6 个经过验证的模型,这个比例就是整个设计的核心。厂商层面的事实——端点、环境变量、传输协议——足够稳定,可以随包发布。模型 id 则不然,所以明确声明的只有那些真正能验证的,其余全部由发现机制去询问各家厂商自己的 /models 得来。
6 是一个发布出来有些难堪的数字,但它也是诚实的数字。
覆盖按字段生效
这在日常使用中之所以重要,原因更小也更平淡:把 Kimi 指向它的中国大陆端点,不应该意味着要把 Kimi 的其他配置全部重写一遍。
{ "providers": { "moonshot": { "baseUrl": "https://api.moonshot.cn/v1" } } }
改动就这么多。整条覆盖正是配置格式腐化的方式——有人为了改一个 URL 复制了内置的整块配置,两个版本之后,他的这份副本就成了挡在他和一个别人早已拿到的修复之间的唯一障碍。
种子数据止于何处
第 0 层值得单独说一句,因为它最让人意外:一个你从没连接过的厂商,照样有模型可以显示。这些模型来自传输层 pi-ai,它随包附带一份自动生成的已知模型目录。这些条目带有上下文窗口、定价和推理档位,但对你的账号能调用什么一无所知——所以明确声明的条目和发现机制返回的任何结果都优先于它们,而 OHARNESS_NO_BUILTIN_MODELS=1 可以把这份种子数据整个关掉。
有两条边界防止这份种子数据造成破坏。
只有当条目的协议与 pi-ai 编目该模型时所对应的 API 一致,模型才会被预置到这个条目上。Mistral 的模型在 pi-ai 里是按它自己的 conversations API 编目的,所以它们交给发现机制处理,而不会以一种 OpenAI 兼容端点不认识的结构发过去。发错结构得到的是一个需要你去排查的 400,而不是一个能凑合用的打折回答。
当两份目录对某家厂商的 id 意见不一致时,靠显式指明对方的名字来对齐,而不是靠猜:
{ "providers": { "moonshot": { "piProvider": "moonshotai" } } }
一个既没有种子数据、也还没做过发现的厂商——本地服务器,或者没人为其内置名称的网关——仍然会在选择器里显示为一行,点开就是连接它的面板。一个能自我解释的空状态,好过一个在某个看不见的条件满足之前干脆不出现的厂商。
唯一值得命名的是协议
目录条目的 protocol 指明其端点所使用的格式:
anthropic-messages openai-chat openai-responses
gemini bedrock vertex
openai-codex
openai-codex 就是这份列表值得显式维护、而不是从厂商名推断的理由。它是 Codex CLI 所对接的后端,与 openai-responses 是真正不同的传输格式,而不是换了个主机的同一种格式:store 会被拒绝,max_output_tokens 不会发送,账号 id 走单独的请求头。一个按「这是 OpenAI,就用 OpenAI 的结构」去猜的系统,会在三个地方分别出错。
这些协议背后的 SDK 都是懒加载的,所以一个只和 Anthropic 打交道的会话永远不会加载 AWS 或 Google 的客户端。
没有动的部分
认证。凭据由 harness 自己的 AuthManager 按请求解析,因为启动时有效的订阅 token,一小时后就未必有效。所以 pi-ai 的 API 实现是被直接调用的,传入的是已经解析好的 key 和请求头,而不是走它的厂商认证和凭据存储。
这也是为什么有些条目根本不随包发布。Bedrock 和 Vertex 作为协议是可用的,但前提是凭据已经在环境变量里——完整的 SigV4 签名和 gcloud ADC 恰恰位于这个 harness 拒绝用于认证的那层厂商逻辑里。Azure OpenAI 没有固定的 Base URL 可以编目,因为端点是按部署区分的。环境已经配好的人都可以自行声明这两者;但它们都不会被当作一个「能用」、实际上却悄悄不能用的默认配置发布出去。
贯穿这一切的模式是:稳定的事实直接声明,不稳定的去问,并且把两者的区别明明白白放在文件里,而不是埋在版本说明中。
OHarness 是一套开源的 agent harness,采用 MIT 许可证。厂商参考记录了上面提到的每一层、每一种覆盖和每一种协议,厂商汇总页列出了目前内置目录里包含的内容。