opencode 怎么接模型:终端编码 Agent 的供应商层与认证机制
本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。
在开源终端编码 Agent opencode(这里说的是 anomalyco/opencode 这个具体项目,不是泛指”开源代码”)里换模型,本质不是”改一个模型名”,而是改那层叫 provider 的装配代码读到的输入。 这层输入有四个来源——配置文件里的 provider 段、环境变量、凭证存储、插件——它们按固定顺序合并成一份最终的供应商表。你改了却没生效,绝大多数时候是改错了来源,或者被后面一步覆盖掉了。搞清楚合并顺序,比记住任何一段示例配置都管用。
opencode 是一个跑在终端里的开源编码 Agent(MIT 许可证,LICENSE 里写的是 Copyright 2025 opencode),仓库地址 https://github.com/anomalyco/opencode 。它把”接哪家模型”这件事从会话逻辑里整个抽了出来:packages/ 下 32 个包,其中和模型接入直接相关的代码集中在 packages/opencode/src/provider/ 这一个目录。本篇只讲这一层的机制。站内已有的 pi 的 provider 层设计 讲的是另一个项目怎么抽这层,Agent 的模型分层调度 讲的是跨项目通用的分层思路,国产 AI 编程工具对比 讲的是工具选型;这三篇都不替你回答”opencode 这个仓库里改哪个文件”,本篇补的就是这块。
一、这层解决的是”每家都不一样”这个麻烦
各家模型服务商的差异不只在域名和密钥格式。有的走 /v1/chat/completions,有的走 /v1/responses;有的要在请求体里显式打开思考模式,有的默认就开;有的对工具的 JSON Schema 挑剔到会因为多一个字段而报错。如果这些差异散落在会话循环里,每加一家就要动一次核心逻辑。
opencode 的做法是:把差异收敛成三处可枚举的东西——用哪个 npm 包、用什么方式拿到凭证、请求发出去之前要不要改写。
第一处在 packages/opencode/src/provider/provider.ts 顶部,一个叫 BUNDLED_PROVIDERS 的映射表,把 npm 包名映射到对应的工厂函数,比如 @ai-sdk/anthropic 映射到 createAnthropic、@ai-sdk/openai-compatible 映射到 createOpenAICompatible。同一个文件里还有个 custom(dep) 函数,为每个有特殊需求的供应商挂一段定制逻辑:amazon-bedrock 在这里做区域前缀推导,azure 在这里解析资源名,google-vertex 在这里挂一个每次请求都现取 access token 的自定义 fetch。这些定制返回一个 autoload 标志,决定这家在没有显式配置时要不要自动出现在列表里。
第二处是认证,单独放在 packages/opencode/src/provider/auth.ts。第三处是请求改写,放在 packages/opencode/src/provider/transform.ts。三处各管一段,互不越界。
| 组成部分 | 它负责什么 | 对应仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| 供应商装配与合并 | 把配置、环境变量、凭证、插件合并成最终供应商表,决定加载哪个 SDK 包 | packages/opencode/src/provider/provider.ts | 改了配置没生效、自定义供应商不出现在选择器里 |
| 认证方式声明 | 声明每家支持哪几种登录方式、要问用户哪几个输入、跑授权与回调 | packages/opencode/src/provider/auth.ts | 执行 /connect 时看到的那串交互 |
| 凭证落盘 | 三种凭证形态的读写,写文件时带权限位 | packages/opencode/src/auth/index.ts | 排查密钥泄漏面、换机器迁移凭证 |
| 请求体改写 | 各家的思考开关、schema 清洗、缓存键等差异化处理 | packages/opencode/src/provider/transform.ts | 思考内容不显示、工具调用报 schema 错 |
| 供应商接入手册 | 每家的开通步骤与可抄的配置样例 | packages/web/src/content/docs/providers.mdx | 第一次接某一家 |
| 模型选择规则 | 默认模型、小模型、变体、加载优先级 | packages/web/src/content/docs/models.mdx | 启动后默认模型不是你想要的那个 |
顺带一提,这个仓库的英文文档在 packages/web/src/content/docs/ 下有 36 份 mdx,根目录还有 21 份 README 翻译——上面表格里的两份文档就在其中,遇事先翻它们通常比翻代码快。
二、认证方式其实只有两大类,剩下的是排列组合
auth.ts 里对认证方式的定义非常克制,一个 Method 只有三个字段:
export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
type: Schema.Literals(["oauth", "api"]),
label: Schema.String,
prompts: optional(Schema.Array(Prompt)),
}) {}
type 只有 oauth 和 api 两个取值。prompts 是一串交互提示,分 text 和 select 两种,还支持 when 条件(按前一个输入的值决定要不要问下一个)。你在终端里跑 /connect 看到的那些”选择认证方式 / 输入 API key / 输入账号标识”,就是这份声明渲染出来的。
api 这一类最直白:你贴一个密钥进去。oauth 这一类走 authorize 拿到一个授权 URL,回调时执行 callback。有意思的是回调之后的分流——代码里判断结果对象里有 key 还是有 refresh:有 key 就按 api 类型存下来,有 refresh 就按 oauth 类型存下 access、refresh、expires。也就是说,浏览器登录这条路最终可能落成一个普通密钥,也可能落成一组可刷新的令牌,取决于对面给什么。
再加上代码之外的两条路,你面对的其实是四类:
- 交互式录入:
/connect走完流程,凭证进入凭证文件。 - 浏览器 / 设备码授权:同样从
/connect进,区别是中途跳浏览器或让你在另一台设备上输一段短码。文档里 xAI 那节把这两种分成了”浏览器回调”和”无回调端口的设备码”,后者正是给 VPS、SSH、容器、CI 这类打不开浏览器的场景准备的。 - 环境变量:不少供应商在文档里给了直接
export的写法,Amazon Bedrock 那节甚至列了一条明确的优先级——bearer token 优先于整条 AWS 凭证链,包括你配好的 profile。 - 配置文件里写死:自定义供应商可以在
options.apiKey里填,并且支持{env:XXX}这种引用环境变量的写法,避免把明文塞进版本库。
凭证最终落在哪里?packages/opencode/src/auth/index.ts 里定义了三种形态:oauth、api、wellknown,写盘时带了权限位:
yield* fsys
.writeJson(file, { ...data, [norm]: info }, 0o600)
.pipe(Effect.mapError(fail("Failed to write auth data")))
文件名是 auth.json,文档里给的路径是 ~/.local/share/opencode/auth.json。同一个文件里还有个逃生口:环境变量 OPENCODE_AUTH_CONTENT 如果存在,就直接当整份凭证 JSON 解析,不读磁盘——这对容器和 CI 很方便,但也意味着你的全部模型凭证会以明文形式出现在进程环境里,谁能读到这个进程的环境,谁就拿到了全部密钥。
三、换模型到底改哪里
models.mdx 把启动时选模型的优先级写得很清楚,从高到低四级:命令行的 --model 或 -m;配置里的 model 键;上次用过的模型;再不行才按内部顺序挑第一个。格式统一是 provider_id/model_id:
{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-20250514"
}
自定义供应商的 provider_id 就是你在配置 provider 段里起的那个键,model_id 是 provider.models 下的键。除了主模型还有一个 small_model,用来跑生成会话标题这类轻活;文档里在自托管场景专门提醒过,如果你要求所有请求都不出自己的边界,这个键必须一起改掉,否则那些轻活仍会走别处。这条容易漏,因为它不影响主流程,出问题的时候你看不见。
不想在配置里硬写模型 ID,就在交互里用 /models 选择器。选择器里的内容还能裁剪:blacklist 从列表中剔掉指定的几个,whitelist 反过来只保留列出的几个,两者能叠加使用——先由 whitelist 收窄,再由 blacklist 剔除。更粗粒度的开关在配置根上:disabled_providers 关掉整家,enabled_providers 是白名单,文档明确说 disabled_providers 优先级更高。
同一个模型的不同”火力档位”,这里叫变体(variants)。文档列了几家的内置变体:Anthropic 是 high 和 max,OpenAI 大致是 none 到 xhigh 那一串,Google 是 low 和 high。你也能自己定义变体、或者把某个变体标成 disabled 让它消失。要在会话里快速切档,models.mdx 指向了一个叫 variant_cycle 的键位绑定。
四、接国产模型:机制层面的几个坑都在 transform 里
绝大多数国产模型服务提供 OpenAI 兼容接口,所以接法上没有神秘之处:npm 填 @ai-sdk/openai-compatible,options.baseURL 填对面给的地址,models 下把模型 ID 列出来。真正会绊人的是思考内容和工具调用,而这些差异在 transform.ts 里都能查到原文注释。
第一个是推理内容字段。OpenAI 兼容协议里没规定思考内容放哪个字段,各家自选。provider.ts 里有一段默认推断:当你走 @ai-sdk/openai-compatible、且模型 ID 里含 deepseek、且没有已知条目时,interleaved 会默认成 { field: "reasoning_content" }。配置里也能显式指定这个字段名。如果你接的国产模型思考内容一直是空的,先查它把推理放在哪个字段名下。
第二个是”思考默认不开”。transform.ts 里对阿里云 DashScope 那条通道的注释写得很直白:它的 OpenAI 兼容 API 需要请求体里带 enable_thinking: true 才会返回 reasoning_content,不带就一个思考 token 都不吐:
if (
input.model.providerID === "alibaba-cn" &&
input.model.capabilities.reasoning &&
input.model.api.npm === "@ai-sdk/openai-compatible" &&
!modelId.includes("kimi-k2-thinking")
) {
result["enable_thinking"] = true
}
同一段代码里还有几条同类处理:供应商 ID 里含 zai 或 zhipuai 且走 OpenAI 兼容包时,会补一个 thinking 对象;MiniMax 走 Anthropic 兼容接口时思考默认是关的,要显式打开;Moonshot 的 Anthropic 兼容接口用的是自适应力度而不是 token 预算。
第三个是 schema 挑剔。transform.ts 里有一段专门给 Kimi 家族做工具 schema 清洗,注释说明了原因:它会先展开 $ref 再做校验,因此不接受同一节点上并列的 description 之类关键字。这类问题的表现是工具调用直接报错,光看错误信息很难联想到是 schema 结构的锅。
这三条合起来给出的判断是:如果你只是把 baseURL 一改就宣布”接上了”,很可能接的是一个降级版本——能对话,但推理内容丢了、工具调用不稳。至于各家的价格、额度和限流规则,各家不同且会调整,以官方最新说明为准,本篇不展开。
五、接本地模型:三份样例其实是同一份
providers.mdx 里给了几种本地方案的配置,llama.cpp 那份是这样:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"llama.cpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llama-server (local)",
"options": {
"baseURL": "http://127.0.0.1:8080/v1"
},
"models": {
"qwen3-coder:a3b": {
"name": "Qwen3-Coder: a3b-30b (local)",
"limit": {
"context": 128000,
"output": 65536
}
}
}
}
}
}
把 llama.cpp 换成 lmstudio 或 ollama,把端口改掉,结构一模一样:供应商 ID 随你起、npm 都是 OpenAI 兼容包、baseURL 指本机、models 手写。区别只在端口和模型 ID 的写法。
本地场景有两件事和云端不同。
一是 limit 必须自己填。文档说得很明白:标准供应商的上下文与输出上限是自动拿到的,本地模型没人替你报数,不填的话工具就算不出你还剩多少上下文。provider.ts 里对配置来的模型,limit.context 和 limit.output 缺省值都是 0。
二是 模型 ID 必须和服务端对得上。文档里 Atomic Chat 那节给了最直接的办法:用 curl 打本地的 /v1/models 端点,看它实际报出来的 id 是什么,照抄。凭印象写一个”应该是这个名字”的 ID,选择器里会有这一项,一发请求就报模型不存在。
还有一条工程经验值得抄:文档在 Ollama 那节提醒,工具调用不正常时试着调大 num_ctx,起步给到 16k 到 32k。本地模型的上下文窗口常常被默认值卡得很小,而编码 Agent 的系统提示词加工具定义本身就不短——这个仓库 packages/opencode/src/tool/ 下有 25 个 .ts 和 15 个 .txt,packages/opencode/src/session/prompt/ 下有 14 份提示词,你可以自己去数——窗口太小时,模型往往还没读到你的问题就已经被截断了。
六、边界与代价:这层不管什么
把供应商抽成一层,换来的是加一家不用动核心逻辑,代价也是实打实的。
它不保证换了模型效果不变。 这层解决的是”连得上”,不解决”干得好”。文档里那句话说得很坦率:模型很多,但同时擅长写代码和工具调用的没几个。opencode 的核心工作流依赖工具调用,一个工具调用不稳的模型,接进来也只能聊天。文档在 Snowflake Cortex 那节干脆把可用模型限制在支持工具调用的那几个家族内。
它不接管服务商侧的规则。 各家的可用地区、模型访问审批、内容过滤策略都在对面手里。文档里能看到几条具体后果:Azure 建议在遇到特定拒答时去改资源的内容过滤等级;Bedrock 要先在控制台申请模型访问权限。这些 opencode 一个字都改不了。
它不替你判断某种用法是否被允许。 文档里那段关于订阅制账号的说明写得毫不含糊:有插件能让人把某家的订阅方案接进来用,而那家明确禁止这种用法,因此相关插件已不再随项目捆绑。这个边界是合规问题,不是技术问题,得你自己确认。
它不缩小外泄面。 这类工具会在你的机器上跑 shell 命令、直接改你的代码文件、并且把代码内容发给你配置的那个服务商。改一个 baseURL 是一行配置的事,但语义上等于”我的私有代码从此经过这个新端点”。中转网关、观测平台、企业代理都同理——每加一跳,代码和提示词就多经过一方。配置里那些自定义 headers 也一样,写错了就是把凭证发给了不该发的地方。凭证文件权限位是 0600,可它仍是明文,任何能以你的身份读文件的进程都能拿走全部密钥;用 OPENCODE_AUTH_CONTENT 注入更省事,也更容易被同机进程读到。
它不阻止误删误改。 供应商层只负责把请求送出去、把回复收回来,模型拿到工具后要动哪个文件、要跑什么命令,是权限与审批那一层的事。这两层千万别混着看,具体见 Agent 权限太大怎么办。
七、上手与避坑清单
1)先跑 opencode auth list 再怀疑配置。 会踩是因为凭证和配置是两个独立来源,你以为”我明明连过了”,可能连的是另一个供应商 ID。避法:文档的排查章节第一条就是它;同时留意文档的提醒——像 Bedrock 这种依赖环境变量的供应商不在这个列表里,看不到不等于没配。
2)自定义供应商的 ID 必须两处一致。 会踩是因为 /connect 里录凭证时你填了一个 ID,配置文件的 provider 段下又写了另一个键,二者对不上时凭证就挂在一个没人认领的 ID 上。避法:录完凭证马上把那个 ID 复制到配置里,别凭记忆重打。
3)npm 包选错,症状是接口路径不对。 会踩是因为 OpenAI 兼容有两条路径:走 /v1/chat/completions 用 @ai-sdk/openai-compatible,走 /v1/responses 要用 @ai-sdk/openai。避法:先看服务商文档给的是哪个端点再决定填哪个包;同一供应商下混着两种的,文档说可以按模型单独覆盖。
4)本地模型别忘了 limit。 会踩是因为不填也能跑通第一句对话,问题要到长会话才暴露:工具算不出剩余上下文,压缩时机就是错的。避法:配置时顺手把 limit.context 和 limit.output 按你实际启动服务的参数填上。
5)自托管场景要连 small_model 一起改。 会踩是因为它默认走的不是你的主模型,而是另一个用来干杂活的小模型,你盯着主模型的流量看不出异常。避法:把 model 和 small_model 都改成你自己的通道,需要时把会话分享一并关掉。
6)别在项目仓库里放明文密钥。 会踩是因为 options.apiKey 支持直接写字符串,顺手就写了,然后连着 opencode.json 一起提交。避法:用 {env:XXX} 引用环境变量;密钥轮换与最小权限的做法见 API Key 安全管理。
7)改完配置先看模型列表,再发真实任务。 会踩是因为配置错误经常表现为”选择器里根本没有这一项”,而不是报错。避法:改完先跑一次 /models 或 opencode models 确认这一项在,再让它动你的代码。
一句话自检:你能说清”当前这次请求用的是哪个供应商 ID、哪个 npm 包、哪个 baseURL、凭证来自哪一个来源”吗? 四个都答得上来,这层对你就是透明的;有一个答不上来,出问题时你只能靠试。
想继续往下读,顺序建议是:packages/web/src/content/docs/providers.mdx 找到你要接的那家先照抄一遍;packages/opencode/src/provider/provider.ts 里的 custom(dep) 看这家有没有额外的定制逻辑;packages/opencode/src/provider/transform.ts 里搜这家的供应商 ID,看请求发出去之前还被改了什么。至于要不要选这类常驻终端的 Agent 作为主力工具,那是另一个维度的判断,可以看 开源终端 Agent 怎么选。
本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 opencode 安装上手:开源终端编码 Agent 的装法与避坑 和 开源终端 Agent opencode 的两条模型接入路线怎么选。