开源编程 Agent pi 接入国产模型:目录、接入位与切换机制
本文基于 pi v0.82.1(仓库 commit 027a584,2026-07-28)梳理,该项目仍在快速迭代,具体行为以官方文档 https://pi.dev/docs/latest 与仓库最新代码为准。
**在 pi 里换用国产模型,你改的不是代码,是一条 provider 条目加一把凭据的存放位置。**厂商的接入地址、走哪套 API、认哪个环境变量,这三件事已经硬编码在仓库的 provider 目录里了;你要做的是让 pi 在四级解析顺序中的某一级找到你的 key,然后在 /model 里把它选出来。搞清楚这条链路,比记住任何一篇「换模型教程」都管用——因为教程会过期,而 packages/ai/src/providers/ 这个目录你随时能打开对答案。
pi 是 Mario Zechner 的开源编程 Agent,MIT 许可证,主仓库在 https://github.com/earendil-works/pi ,截至 2026 年 7 月 GitHub 上约 8 万 star。下面所有说法都能在这个仓库里逐条核对。
站内已经有几篇讲通用做法的:Claude Code 接入国产模型讲的是官方客户端怎么改端点,Cursor 接入 DeepSeek 讲的是 IDE 里的自定义模型入口,国产 API 价格对比讲的是选型判断。那三篇是方法论,本篇是「一个具体项目把这套方法论落成了什么样的代码结构」——你能看到目录、文件名、字段名,而不是抽象的步骤。
一、先找到模型接入代码住在哪
pi 是 monorepo。跟模型接入相关的东西集中在两个包里:packages/ai 放的是与各家 API 打交道的实现,packages/coding-agent 放的是 CLI 与文档。
花名册只有一份:packages/ai/src/providers/all.ts。这个文件顶部是一长串 import,底部有一个 builtinProviders() 函数,把所有内置 provider 挨个构造出来返回成数组。它是唯一权威——一个厂商在不在 pi 的内置支持里,看这个数组就行,不用去猜。同一文件里还有 getBuiltinProviders()、getBuiltinModels() 这类读取生成好的模型目录的函数。
每个 provider 是一个独立小文件,命名就是 provider 的 id。以 DeepSeek 为例,packages/ai/src/providers/deepseek.ts 全文只有十几行:
export function deepseekProvider(): Provider<"openai-completions"> {
return createProvider({
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://api.deepseek.com",
auth: { apiKey: envApiKeyAuth("DeepSeek API key", ["DEEPSEEK_API_KEY"]) },
models: Object.values(DEEPSEEK_MODELS),
api: openAICompletionsApi(),
});
}
五个字段就是一个厂商的全部接入信息:id、显示名、baseUrl、认哪个环境变量、模型目录、走哪套 API。所有内置 provider 都是这个形状,你读懂一个就读懂全部。
api 这一项的取值范围是固定的四种,models.md 里列得很清楚:openai-completions(OpenAI Chat Completions,兼容面最广)、openai-responses、anthropic-messages、google-generative-ai。本文涉及的国内厂商条目只用到其中两种——openai-completions 或 anthropic-messages,选哪种取决于厂商自己对外暴露的是什么协议,不是 pi 的偏好。
至于每个 provider 有哪些具体模型,来自同名的 *.models.ts。这些文件开头都写着「auto-generated」,实际内容是从数据文件展开的目录。所以模型列表是随版本刷新的数据,不是手写常量——providers.md 也提到,内置目录随 pi 发行,已配置的 provider 可能拉取更新的目录并缓存到 ~/.pi/agent/models-store.json 供离线使用,命令行侧对应 pi update --models。
二、内置目录里的国内厂商条目
按 all.ts 的 builtinProviders() 数,内置 provider 一共 38 条,其中属于国内厂商的有 15 条。下表里的仓库路径都在 packages/ai/src/providers/ 下,环境变量取自 packages/coding-agent/docs/providers.md 的对照表与 packages/ai/src/env-api-keys.ts 里的 envMap。
| provider id | 走哪套 API | 环境变量 / auth.json 键 | 仓库文件 | 你什么时候会碰到它 |
|---|---|---|---|---|
deepseek | openai-completions | DEEPSEEK_API_KEY | deepseek.ts | 最常见的第一站,配置形状最简单 |
zai | openai-completions | ZAI_API_KEY | zai.ts | 用 Z.AI 的全球站点时 |
zai-coding-cn | openai-completions | ZAI_CODING_CN_API_KEY | zai-coding-cn.ts | 用国内站点时,注意与上一条是两个 id |
moonshotai | openai-completions | MOONSHOT_API_KEY | moonshotai.ts | 月之暗面全球端点 |
moonshotai-cn | openai-completions | MOONSHOT_API_KEY | moonshotai-cn.ts | 月之暗面国内端点,与上一条共用变量 |
kimi-coding | anthropic-messages | KIMI_API_KEY | kimi-coding.ts | 走 Kimi 编程订阅,支持 OAuth 登录 |
minimax | anthropic-messages | MINIMAX_API_KEY | minimax.ts | MiniMax 的 Anthropic 兼容端点 |
minimax-cn | anthropic-messages | MINIMAX_CN_API_KEY | minimax-cn.ts | MiniMax 国内端点 |
qwen-token-plan | openai-completions | QWEN_TOKEN_PLAN_API_KEY | qwen-token-plan.ts | 通义千问 token 套餐,境外可用区 |
qwen-token-plan-cn | openai-completions | QWEN_TOKEN_PLAN_CN_API_KEY | qwen-token-plan-cn.ts | 同上,北京可用区 |
xiaomi | openai-completions | XIAOMI_API_KEY | xiaomi.ts | 小米 MiMo 常规接入 |
xiaomi-token-plan-cn | openai-completions | XIAOMI_TOKEN_PLAN_CN_API_KEY | xiaomi-token-plan-cn.ts | 小米 token 套餐,三个地区各一条 |
xiaomi-token-plan-ams | openai-completions | XIAOMI_TOKEN_PLAN_AMS_API_KEY | xiaomi-token-plan-ams.ts | 同上,阿姆斯特丹 |
xiaomi-token-plan-sgp | openai-completions | XIAOMI_TOKEN_PLAN_SGP_API_KEY | xiaomi-token-plan-sgp.ts | 同上,新加坡 |
ant-ling | openai-completions | ANT_LING_API_KEY | ant-ling.ts | 蚂蚁 Ling 系列 |
有几个从表里读不出、但打开文件就能看到的细节,值得单独点出来。
同一厂商的国内与海外端点是两个独立 provider,不是一个开关。 比如 minimax 的 baseUrl 是 https://api.minimax.io/anthropic,minimax-cn 是 https://api.minimaxi.com/anthropic;zai 是 https://api.z.ai/api/coding/paas/v4,zai-coding-cn 是 https://open.bigmodel.cn/api/coding/paas/v4。两条各有各的模型目录文件。所以你不是「给 Z.AI 配一把 key」,你是给某个具体端点配一把 key。
协议选择上,国内厂商这几年分成了两派。 一派对外提供 OpenAI Chat Completions 兼容接口,pi 直接用 openai-completions;另一派提供 Anthropic Messages 兼容接口,pi 用 anthropic-messages——kimi-coding、minimax、minimax-cn 三条都是后者。这不是 pi 的分类,是各家自己的对外协议决定的,你在排查请求体格式问题时得先知道自己在哪一派里。
kimi-coding 是这批里唯一同时挂了 OAuth 的。 它的 auth 对象里除了 apiKey,还有一个 oauth,登录标签写的是「Sign in with Kimi Code」。也就是说除了填 key,还可以走订阅式登录,凭据落到 auth.json。
moonshotai 与 moonshotai-cn 共用 MOONSHOT_API_KEY。 这在 env-api-keys.ts 的 envMap 里是两行分别指向同一个变量名。同样的共用情况还有 opencode 与 opencode-go(都用 OPENCODE_API_KEY)、两个 Cloudflare 条目(都用 CLOUDFLARE_API_KEY)。
另外提一句:providers.md 里那张环境变量对照表并没有收录 moonshotai 和 moonshotai-cn,但代码里两条都在。文档表和代码不完全同步是快速迭代项目的常态,所以遇到分歧以 all.ts 和 env-api-keys.ts 为准。
三、切换模型时,改动到底落在哪个文件
这一节是本篇的核心。pi 把「凭据从哪来」和「用哪个模型」拆成了两件事。
凭据这一侧,providers.md 结尾给了明确的解析顺序,一共四级:命令行 --api-key 参数、auth.json 里的条目(API key 或 OAuth token)、环境变量、models.json 里自定义 provider 的 key。文档还单独强调了一句:auth 文件里的凭据优先于环境变量。这条顺序解释了绝大多数「我明明 export 了新 key 却没生效」的场面。
凭据文件默认在 ~/.pi/agent/auth.json,创建时权限是 0600。结构是一个扁平对象,键就是上表里的 provider id:
{
"anthropic": { "type": "api_key", "key": "sk-ant-..." },
"ant-ling": { "type": "api_key", "key": "..." },
"deepseek": { "type": "api_key", "key": "sk-..." },
"qwen-token-plan-cn": { "type": "api_key", "key": "sk-sp-..." },
"xiaomi-token-plan-cn": { "type": "api_key", "key": "..." }
}
key 字段不只接受字面量。providers.md 的 Key Resolution 一节列了四种写法:以 ! 开头的整串当命令执行、取标准输出(在 auth.json 里按进程生命周期缓存);$ENV_VAR 或 ${ENV_VAR} 做环境变量插值,可以嵌在更长的字符串里;$$ 转义出字面美元号、$! 转义出字面感叹号;其余按字面量处理。这里有个容易反直觉的规则:纯大写的字符串如 MY_API_KEY 被当作字面量,要引用环境变量必须写 $MY_API_KEY。
API key 条目还能带一个 env 对象,把 provider 作用域的环境值圈在里面。文档说明这些值在解析凭据、请求头和 provider 配置时优先于进程环境变量,覆盖范围包括 Cloudflare 账号 ID、Azure OpenAI 设置、Vertex 的 project/location、Bedrock 设置,以及 PI_CACHE_RETENTION 和 HTTP_PROXY/HTTPS_PROXY。当你希望 pi 用一套跟项目 shell 不同的 provider 设置时,就用它。
选模型这一侧有好几个入口,覆盖不同场景:
- 交互式里
/model或 Ctrl+L 打开选择器,Shift+Tab 循环思考等级,Ctrl+P / Shift+Ctrl+P 在作用域模型之间轮换(作用域由/scoped-models圈定)。 - 命令行
--provider <name>配--model <pattern>;--model也支持provider/id的合写形式,还能接:<thinking>后缀直接指定思考等级。--api-key覆盖环境变量,--list-models列出可用模型。 - 想固定默认值就写设置文件:
~/.pi/agent/settings.json(全局)或.pi/settings.json(项目级,项目级覆盖全局),键名是defaultProvider、defaultModel、defaultThinkingLevel。 - 想限定 Ctrl+P 的轮换范围,命令行用
--models "<patterns>",设置文件里对应enabledModels。
还有一个很实用的运行时确认手段。pi 会把当前会话状态注入给 LLM 可调用的 bash 工具,其中就有 PI_PROVIDER、PI_MODEL、PI_REASONING_LEVEL。文档明确写了:被问到当前跑的是哪个模型时,去读这些变量,不要从系统提示词里推断。environment-variables.md 给的确认命令是:
printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"
这些值在每条命令启动时解析,所以中途切模型或改思考等级,下一条 bash 命令就会反映出来,不用重启。文档还补了一句边界:这两个变量标识的是你选中的 pi 模型,不是路由器内部可能另选的上游模型。这个区分在你用网关类 provider 时很关键。
关于凭据管理本身的通用做法,可以配合API 密钥安全管理那篇一起看,本篇只负责说清 pi 把它们放在哪。
四、内置条目不够用时:models.json 与 compat
内置目录覆盖不到的情况有三类:自建或本地推理服务、公司内部代理网关、厂商上了新模型但 pi 目录还没跟上。这三类都由 ~/.pi/agent/models.json 接手,models.md 整篇讲的都是它。
最小配置只需要 baseUrl、api、apiKey 加一个模型 id 数组。有个反直觉的点文档专门说了:本地服务器即便不校验 key,也得留个占位值,因为 pi 在模型出现在 /model 之前会先检查这个 provider 是否配了认证——要么留 dummy 值,要么用 /login 存一把,要么选模型时带 --api-key。
改内置 provider 的接入地址而不动模型列表,是最常用的一招:
{
"providers": {
"anthropic": {
"baseUrl": "https://my-proxy.example.com/v1"
}
}
}
文档说明这样写之后,内置的模型全部保留,已有的 OAuth 或 API key 认证继续工作。如果还要往里塞自定义模型,加上 models 数组即可,合并语义是:内置模型保留,自定义模型按 id upsert——id 撞上内置的就替换,是新 id 就并列添加。只想微调某几个内置模型的元数据,用 modelOverrides,它支持的字段是 name、reasoning、thinkingLevelMap、input、cost(可部分覆盖)、contextWindow、maxTokens、headers、compat。
真正体现「兼容一家新厂商有多少细节」的是 compat 字段。provider 级设默认,模型级覆盖,两级都设时会合并。挑几个跟国内厂商直接相关的:
supportsDeveloperRole:某些 OpenAI 兼容服务器不认developer角色,设成false后 pi 改用system消息发系统提示词。supportsReasoningEffort:服务器不认reasoning_effort参数时关掉。thinkingFormat:这一项的取值直接暴露了各家思考开关的差异,可选reasoning_effort、openrouter、deepseek、together、zai、qwen、chat-template、qwen-chat-template。文档还点名说,qwen用顶层enable_thinking;本地 Qwen 兼容服务器如果要chat_template_kwargs.enable_thinking和preserve_thinking,用qwen-chat-template;vLLM / Hugging Face 的聊天模板走chat-template配chatTemplateKwargs。maxTokensField:选max_completion_tokens还是max_tokens。cacheControlFormat:给那些用 Anthropic 风格cache_control标记暴露提示词缓存的 OpenAI 兼容服务用,目前只支持anthropic一个取值。
走 anthropic-messages 的那一派另有一组开关,其中 allowEmptySignature 的注释很说明问题:某些 Anthropic 兼容服务会发出签名为空的思考块并且在重放时还要求带上,而真正的 Anthropic 会拒绝空的思考签名。这种「协议名字一样、行为不一样」的坑,就是 compat 存在的理由。同组还有 forceAdaptiveThinking、supportsEagerToolInputStreaming、supportsStrictTools。
思考等级本身也可以逐模型描述。thinkingLevelMap 的键是 pi 的七个等级(off、minimal、low、medium、high、xhigh、max),值是三态:省略表示走 provider 默认映射且不支持扩展的 xhigh/max;字符串表示支持并把这个值发给厂商;null 表示不支持,会在界面上隐藏或跳过。有的模型思考关不掉,就把 off 设成 null。
models.json 有个便利特性:每次打开 /model 都会重新加载,会话中途改完不用重启。
如果连 compat 都摆不平——比如厂商要自定义鉴权流程或非标准流式协议——出口是写扩展。custom-provider.md 讲的是扩展通过 pi.registerProvider() 注册完整 provider,仓库 packages/coding-agent/examples/extensions/ 下有 custom-provider-anthropic 和 custom-provider-gitlab-duo 两个完整示例可抄。文档也说明了叠加关系:models.json 里的覆盖会叠加在已注册的原生 provider 之上,两者不是二选一。
五、边界与代价:这个设计放弃了什么
把厂商接入压缩成一个几十行的 provider 文件,代价是明确的。
pi 的核心刻意做得很薄。 usage.md 的 Design Principles 一节直接列了不内置的东西:MCP、sub-agents、权限弹窗、plan mode、to-dos、后台 bash。这些要靠扩展、skills、prompt templates、packages 自己装,或者用容器、tmux 这类外部工具。换句话说,你选 pi 不是选一个功能齐全的客户端,是选一个可以按需组装的底座。如果你的团队工作流强依赖上面那几项,接国产模型这件事顺不顺利跟你能不能用起来是两回事。想先搞清楚该不该选这类框架,Agent 框架横向对比那篇的判断维度更合适。
换 provider 不等于换到等价能力。 compat 里那一长串开关本身就是证据:思考参数格式、缓存标记方式、工具定义是否支持严格模式、流式用量统计是否可用,各家都不一样。pi 提供的是把差异表达出来的位置,不是替你抹平差异的保证。
shell 命令型凭据没有内置兜底。 models.md 明确说,models.json 里的 shell 命令在请求时解析,pi 有意不加内置 TTL、陈旧值复用或恢复逻辑,理由是不同命令需要不同的缓存与失败策略,pi 无法推断哪种是对的。命令慢、贵、被限流,或者希望瞬时失败时沿用旧值,得你自己包一层脚本。同时还有一条:/model 的可用性检查只看是否配了认证,不执行 shell 命令。
区域可达性 pi 不管。 pi 支持 ChatGPT Plus/Pro、Claude Pro/Max、GitHub Copilot 等订阅式登录,也支持 Amazon Bedrock、Google Vertex AI 这些云端点,但这些服务的官方条款对中国大陆存在区域限制、不支持直连,能不能用不取决于 pi 的代码。市面上确实存在第三方中转服务,本文不背书也不给具体渠道,风险和合规责任在你自己。至于各家的计费口径与限流规则,各家规则不同且会调整,以官方最新说明为准。
它明确不管的还有: 你的账号是否有权限用某个模型、厂商目录里有没有你要的那个新模型(内置目录随版本刷新,落后是常态,这时候就该动 models.json)、以及路由类网关内部实际选了哪个上游。前面提过 PI_MODEL 标识的是 pi 侧选中的模型,不是网关内部的选择。多模型之间怎么排布与降级,属于模型路由策略的范畴,pi 只给到配置位。
六、上手与避坑清单
每条都写清楚为什么会踩,以及怎么绕开。
一、export 了新 key 却不生效。 因为 auth.json 的优先级高于环境变量,你之前用 /login 存过的旧条目还在那儿压着。动手改环境变量之前先打开 ~/.pi/agent/auth.json 看一眼,需要清掉就用 /logout。
二、把国内端点的 key 填进了海外端点的条目。 zai 与 zai-coding-cn、minimax 与 minimax-cn、qwen-token-plan 与 qwen-token-plan-cn、moonshotai 与 moonshotai-cn,都是名字相近但 baseUrl 完全不同的两条。挑 id 的时候不要凭印象,去对应的 provider 文件里看 baseUrl 的域名,跟你申请 key 的控制台域名对上再填。
三、以为 moonshotai 和 moonshotai-cn 是两把 key。 它们在 env-api-keys.ts 里映射到同一个 MOONSHOT_API_KEY。要让两个端点用不同的凭据,就别靠环境变量,在 auth.json 里按 provider id 分别写条目,或者用 API key 条目里的 env 对象把变量限定在 provider 作用域内。
四、接本地或自建服务时一片 400。 大概率是 developer 角色和 reasoning_effort 这两处。models.md 直接点名 Ollama、vLLM、SGLang 这类服务,建议在 provider 级 compat 里把 supportsDeveloperRole 和 supportsReasoningEffort 都设成 false 再逐项打开。
五、本地服务不需要 key,结果模型压根不出现在 /model 里。 pi 在展示模型前会检查该 provider 是否配了认证。给一个占位值就行——文档的 Ollama 示例里 apiKey 直接写的 "ollama"。
六、思考等级调了没反应。 各家的思考开关格式不同,thinkingFormat 选错就是静默不生效。先确认厂商文档要求的是 reasoning_effort、enable_thinking 还是 chat_template_kwargs,再对着 thinkingFormat 的取值表选。模型只支持部分等级的,用 thinkingLevelMap 把不支持的写成 null,界面上就不会再出现。
七、models.json 里写了 MY_API_KEY 当环境变量引用。 纯大写字符串按字面量处理,pi 会老老实实把这七个字符当成 key 发出去。要引用环境变量必须带 $。同理,key 本身以 ! 开头的话会被当成命令执行,需要字面感叹号就写 $!。
八、用密码管理器取 key,结果每次请求都卡一下。 auth.json 里的命令按进程生命周期缓存,models.json 里的命令是请求时解析且没有内置 TTL。把慢命令放进自己写的带缓存的脚本里,再让 pi 调这个脚本。
九、分不清当前在跑哪个模型,跑去问模型自己。 模型不知道。用 bash 工具读 PI_PROVIDER 和 PI_MODEL,这是文档明确推荐的做法。
十、为了走公司代理,新建了一个 provider 把模型全抄了一遍。 没必要。只在 models.json 里给内置 provider 写一个 baseUrl 覆盖,内置模型和已有认证都还在。要额外加模型再补 models 数组,要微调元数据用 modelOverrides。
收束:三步自检,两条阅读路线
配完之后按这三步自检:第一步 --list-models 或 /model,确认目标模型出现在列表里——出不来通常是认证没被识别,而不是模型不存在;第二步跑一次实际请求,让 bash 工具打一遍 PI_PROVIDER / PI_MODEL,确认选中的确实是你想要的那条;第三步把思考等级切一次,确认 PI_REASONING_LEVEL 跟着变、并且厂商侧真的收到了思考参数,这一步能把 thinkingFormat 配错的问题当场揪出来。
接下来该读什么,看你的目标。想把接入这件事彻底吃透,顺序是 packages/coding-agent/docs/providers.md(凭据与解析顺序)→ models.md(自定义 provider 与全部 compat 字段)→ custom-provider.md(扩展式 provider)。想验证本文任何一句话,直接看代码:packages/ai/src/providers/all.ts 拿到花名册,点进任意一个同名 .ts 看接入五要素,再翻 packages/ai/src/env-api-keys.ts 里的 envMap 核对环境变量。这几个文件加起来读完不用半小时,而且它们随版本更新,比任何二手教程都新。
本篇属于一个把开源编程 Agent 项目 pi 逐层拆开讲的系列,整体地图见开源编程 Agent pi 是什么;沿着这条线往下,还可以看 终端编程 Agent 选型五条线 和 开源编程 Agent pi 上手实录。