开源编程 Agent pi 接入国产模型:目录、接入位与切换机制

2026-07-29

本文基于 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-responsesanthropic-messagesgoogle-generative-ai。本文涉及的国内厂商条目只用到其中两种——openai-completionsanthropic-messages,选哪种取决于厂商自己对外暴露的是什么协议,不是 pi 的偏好。

至于每个 provider 有哪些具体模型,来自同名的 *.models.ts。这些文件开头都写着「auto-generated」,实际内容是从数据文件展开的目录。所以模型列表是随版本刷新的数据,不是手写常量——providers.md 也提到,内置目录随 pi 发行,已配置的 provider 可能拉取更新的目录并缓存到 ~/.pi/agent/models-store.json 供离线使用,命令行侧对应 pi update --models

二、内置目录里的国内厂商条目

all.tsbuiltinProviders() 数,内置 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 键仓库文件你什么时候会碰到它
deepseekopenai-completionsDEEPSEEK_API_KEYdeepseek.ts最常见的第一站,配置形状最简单
zaiopenai-completionsZAI_API_KEYzai.ts用 Z.AI 的全球站点时
zai-coding-cnopenai-completionsZAI_CODING_CN_API_KEYzai-coding-cn.ts用国内站点时,注意与上一条是两个 id
moonshotaiopenai-completionsMOONSHOT_API_KEYmoonshotai.ts月之暗面全球端点
moonshotai-cnopenai-completionsMOONSHOT_API_KEYmoonshotai-cn.ts月之暗面国内端点,与上一条共用变量
kimi-codinganthropic-messagesKIMI_API_KEYkimi-coding.ts走 Kimi 编程订阅,支持 OAuth 登录
minimaxanthropic-messagesMINIMAX_API_KEYminimax.tsMiniMax 的 Anthropic 兼容端点
minimax-cnanthropic-messagesMINIMAX_CN_API_KEYminimax-cn.tsMiniMax 国内端点
qwen-token-planopenai-completionsQWEN_TOKEN_PLAN_API_KEYqwen-token-plan.ts通义千问 token 套餐,境外可用区
qwen-token-plan-cnopenai-completionsQWEN_TOKEN_PLAN_CN_API_KEYqwen-token-plan-cn.ts同上,北京可用区
xiaomiopenai-completionsXIAOMI_API_KEYxiaomi.ts小米 MiMo 常规接入
xiaomi-token-plan-cnopenai-completionsXIAOMI_TOKEN_PLAN_CN_API_KEYxiaomi-token-plan-cn.ts小米 token 套餐,三个地区各一条
xiaomi-token-plan-amsopenai-completionsXIAOMI_TOKEN_PLAN_AMS_API_KEYxiaomi-token-plan-ams.ts同上,阿姆斯特丹
xiaomi-token-plan-sgpopenai-completionsXIAOMI_TOKEN_PLAN_SGP_API_KEYxiaomi-token-plan-sgp.ts同上,新加坡
ant-lingopenai-completionsANT_LING_API_KEYant-ling.ts蚂蚁 Ling 系列

有几个从表里读不出、但打开文件就能看到的细节,值得单独点出来。

同一厂商的国内与海外端点是两个独立 provider,不是一个开关。 比如 minimax 的 baseUrl 是 https://api.minimax.io/anthropicminimax-cnhttps://api.minimaxi.com/anthropiczaihttps://api.z.ai/api/coding/paas/v4zai-coding-cnhttps://open.bigmodel.cn/api/coding/paas/v4。两条各有各的模型目录文件。所以你不是「给 Z.AI 配一把 key」,你是给某个具体端点配一把 key。

协议选择上,国内厂商这几年分成了两派。 一派对外提供 OpenAI Chat Completions 兼容接口,pi 直接用 openai-completions;另一派提供 Anthropic Messages 兼容接口,pi 用 anthropic-messages——kimi-codingminimaxminimax-cn 三条都是后者。这不是 pi 的分类,是各家自己的对外协议决定的,你在排查请求体格式问题时得先知道自己在哪一派里。

kimi-coding 是这批里唯一同时挂了 OAuth 的。 它的 auth 对象里除了 apiKey,还有一个 oauth,登录标签写的是「Sign in with Kimi Code」。也就是说除了填 key,还可以走订阅式登录,凭据落到 auth.json

moonshotaimoonshotai-cn 共用 MOONSHOT_API_KEY 这在 env-api-keys.tsenvMap 里是两行分别指向同一个变量名。同样的共用情况还有 opencodeopencode-go(都用 OPENCODE_API_KEY)、两个 Cloudflare 条目(都用 CLOUDFLARE_API_KEY)。

另外提一句:providers.md 里那张环境变量对照表并没有收录 moonshotaimoonshotai-cn,但代码里两条都在。文档表和代码不完全同步是快速迭代项目的常态,所以遇到分歧以 all.tsenv-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_RETENTIONHTTP_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(项目级,项目级覆盖全局),键名是 defaultProviderdefaultModeldefaultThinkingLevel
  • 想限定 Ctrl+P 的轮换范围,命令行用 --models "<patterns>",设置文件里对应 enabledModels

还有一个很实用的运行时确认手段。pi 会把当前会话状态注入给 LLM 可调用的 bash 工具,其中就有 PI_PROVIDERPI_MODELPI_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 整篇讲的都是它。

最小配置只需要 baseUrlapiapiKey 加一个模型 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,它支持的字段是 namereasoningthinkingLevelMapinputcost(可部分覆盖)、contextWindowmaxTokensheaderscompat

真正体现「兼容一家新厂商有多少细节」的是 compat 字段。provider 级设默认,模型级覆盖,两级都设时会合并。挑几个跟国内厂商直接相关的:

  • supportsDeveloperRole:某些 OpenAI 兼容服务器不认 developer 角色,设成 false 后 pi 改用 system 消息发系统提示词。
  • supportsReasoningEffort:服务器不认 reasoning_effort 参数时关掉。
  • thinkingFormat:这一项的取值直接暴露了各家思考开关的差异,可选 reasoning_effortopenrouterdeepseektogetherzaiqwenchat-templateqwen-chat-template。文档还点名说,qwen 用顶层 enable_thinking;本地 Qwen 兼容服务器如果要 chat_template_kwargs.enable_thinkingpreserve_thinking,用 qwen-chat-template;vLLM / Hugging Face 的聊天模板走 chat-templatechatTemplateKwargs
  • maxTokensField:选 max_completion_tokens 还是 max_tokens
  • cacheControlFormat:给那些用 Anthropic 风格 cache_control 标记暴露提示词缓存的 OpenAI 兼容服务用,目前只支持 anthropic 一个取值。

anthropic-messages 的那一派另有一组开关,其中 allowEmptySignature 的注释很说明问题:某些 Anthropic 兼容服务会发出签名为空的思考块并且在重放时还要求带上,而真正的 Anthropic 会拒绝空的思考签名。这种「协议名字一样、行为不一样」的坑,就是 compat 存在的理由。同组还有 forceAdaptiveThinkingsupportsEagerToolInputStreamingsupportsStrictTools

思考等级本身也可以逐模型描述。thinkingLevelMap 的键是 pi 的七个等级(offminimallowmediumhighxhighmax),值是三态:省略表示走 provider 默认映射且不支持扩展的 xhigh/max;字符串表示支持并把这个值发给厂商;null 表示不支持,会在界面上隐藏或跳过。有的模型思考关不掉,就把 off 设成 null

models.json 有个便利特性:每次打开 /model 都会重新加载,会话中途改完不用重启。

如果连 compat 都摆不平——比如厂商要自定义鉴权流程或非标准流式协议——出口是写扩展。custom-provider.md 讲的是扩展通过 pi.registerProvider() 注册完整 provider,仓库 packages/coding-agent/examples/extensions/ 下有 custom-provider-anthropiccustom-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 填进了海外端点的条目。 zaizai-coding-cnminimaxminimax-cnqwen-token-planqwen-token-plan-cnmoonshotaimoonshotai-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 里把 supportsDeveloperRolesupportsReasoningEffort 都设成 false 再逐项打开。

五、本地服务不需要 key,结果模型压根不出现在 /model 里。 pi 在展示模型前会检查该 provider 是否配了认证。给一个占位值就行——文档的 Ollama 示例里 apiKey 直接写的 "ollama"

六、思考等级调了没反应。 各家的思考开关格式不同,thinkingFormat 选错就是静默不生效。先确认厂商文档要求的是 reasoning_effortenable_thinking 还是 chat_template_kwargs,再对着 thinkingFormat 的取值表选。模型只支持部分等级的,用 thinkingLevelMap 把不支持的写成 null,界面上就不会再出现。

七、models.json 里写了 MY_API_KEY 当环境变量引用。 纯大写字符串按字面量处理,pi 会老老实实把这七个字符当成 key 发出去。要引用环境变量必须带 $。同理,key 本身以 ! 开头的话会被当成命令执行,需要字面感叹号就写 $!

八、用密码管理器取 key,结果每次请求都卡一下。 auth.json 里的命令按进程生命周期缓存,models.json 里的命令是请求时解析且没有内置 TTL。把慢命令放进自己写的带缓存的脚本里,再让 pi 调这个脚本。

九、分不清当前在跑哪个模型,跑去问模型自己。 模型不知道。用 bash 工具读 PI_PROVIDERPI_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 上手实录

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。