12-配置体系与自定义供应商
12 配置与自定义
读这一篇的价值:Pi 的行为几乎全部由「配置文件 + 扩展」描述,而不是写死在代码里。这一篇把四件事讲清:配置放哪里、有哪些设置项、怎么接非内置的模型端点、怎么用一个扩展把新的供应商/虚拟模型塞进去。
1. 配置放在哪里
两层配置:用户层在代理目录(agent directory,默认 ~/.pi/agent,可用 PI_CODING_AGENT_DIR 或 SDK 的 agentDir 改写),项目层在工作目录下的 .pi/。项目配置在项目信任(project trust)授予后才加载;唯一例外是 sessionDir,Pi 在解析信任之前就读它,否则找不到会话。
代理目录
| 路径 | 职责 |
|---|---|
<agent-dir>/settings.json | 用户级设置:偏好、默认值、资源路径、Pi 包声明 |
<agent-dir>/keybindings.json | TUI 与应用快捷键 |
<agent-dir>/mcp.json | 在每个项目都可用的 MCP 服务器 |
<agent-dir>/models.json | 兼容端点、模型、模型覆盖 |
<agent-dir>/auth.json | 保存的 API key 与 OAuth 凭据 |
<agent-dir>/AGENTS.override.md、AGENTS.md、AGENTS.MD、CLAUDE.md、CLAUDE.MD | 跨工作目录生效的用户指令 |
<agent-dir>/SYSTEM.md | 替换 Pi 默认系统提示词 |
<agent-dir>/APPEND_SYSTEM.md | 追加到系统提示词 |
<agent-dir>/extensions/ | 用户扩展 |
<agent-dir>/skills/ | 用户技能与支持文件 |
<agent-dir>/prompts/ | 用户提示模板(暴露为斜杠命令) |
<agent-dir>/themes/ | 用户主题文件 |
项目 .pi
| 路径 | 职责 |
|---|---|
.pi/settings.json | 项目级设置、资源路径、Pi 包声明 |
.pi/mcp.json | 项目 MCP 服务器 |
.pi/SYSTEM.md | 项目系统提示词(替换) |
.pi/APPEND_SYSTEM.md | 项目追加指令 |
.pi/extensions/、.pi/skills/、.pi/prompts/、.pi/themes/ | 项目级扩展、技能、提示模板、主题 |
SYSTEM.md/APPEND_SYSTEM.md:受信任的项目文件优先于代理目录里的同名文件,二者不合并。- 交互模式下用
/settings改常见偏好;手改任意设置、快捷键、指令或资源后要跑/reload。
上下文文件(与 .pi 配置是两回事)
上下文文件由 Pi 从代理目录、工作目录及其所有父目录收集,在某个目录或其任意子目录下运行就生效。AGENTS.override.md 只替换同目录的 AGENTS.md/CLAUDE.md,不会盖掉代理目录或其他目录的上下文文件。上下文发现不需要项目信任。
2. settings.json 设置参考
项目设置覆盖代理目录设置;资源列表是合并而非覆盖。
模型与思考
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
defaultProvider | string | 自动 | 启动供应商 |
defaultModel | string | 自动 | 启动模型 ID |
defaultThinkingLevel | off/minimal/low/medium/high/xhigh/max | medium | 启动思考级别 |
modelThinkingLevels | object | 无 | 按 provider/modelId 精确键的每模型启动级别 |
thinkingBudgets | object | 内置 | minimal/low/medium/high 的 token 预算 |
enabledModels | string[] | 全部可用 | 启动选择与模型循环用的模式,格式同 --models |
hideThinkingBlock | boolean | false | 隐藏思考块 |
showCacheMissNotices | boolean | false | 显示显著 cache miss / 缓存预热 / 压缩用量 / 供应商恢复提示 |
cacheWarming | off/streaming/idle | streaming | 活动运行期或(idle)运行间保持可缓存供应商的 prompt cache 温热。仅能全局设置 |
cacheWarming 只有在模型自己声明了缓存生命周期、且 Pi 估算至少能避免 $0.05 cache-miss 成本时才跑;预热用量计入会话统计但不进模型上下文;/session 显示下一次决策;扩展可用 cache_warming_decision 事件覆盖。
交互
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
steeringMode | all/one-at-a-time | one-at-a-time | 排队 steering 消息的投递方式 |
followUpMode | all/one-at-a-time | one-at-a-time | 排队 follow-up 的投递方式 |
externalEditor | string | $VISUAL → $EDITOR → 平台默认 | 外部编辑器命令 |
doubleEscapeAction | tree/fork/none | tree | 编辑器为空时双击 Escape 的行为 |
treeFilterMode | default/no-tools/user-only/labeled-only/all | default | /tree 初始过滤 |
defaultProjectTrust | ask/always/never | ask | 项目信任回退策略。只能写在代理目录设置里 |
工具
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
defaultTools | string[] | read, bash, edit, write | 启动启用的工具 |
codemode.mode | on/only | on | on:已声明工具的描述里附「可从脚本调用」备注,codemode 只列未声明的;only:codemode 列全部,内置与扩展工具对模型隐藏 |
codemode.inlineBudget | number | 3000 | codemode 工具描述可用于工具声明的估算 token(字符 / 4);放不下的用 searchTools() 找;0 只列命名空间 |
defaultTools 语义:纯名字替换默认选择,+name 增、-name 减;空数组禁用所有内置工具(但不禁用扩展或 SDK 工具)。可用内置工具名:read、bash、powershell、edit、write、grep、find、ls;还可写 codemode、tool_search 及其他以非活动状态注册的扩展工具。
{ "defaultTools": ["+codemode"] }
本项目设置叠在用户设置之上:只有 +/- 的项目列表修改用户选择,含纯名字的项目列表则替换它;同一列表里先由纯名字形成选择,再按顺序应用 +/-。/reload 会启用新增到 defaultTools 的工具,但不会禁用从里移除的、也不会重新启用你手动关掉的工具;--tools / --no-tools / --no-builtin-tools 覆盖该设置(含 reload),且 --tools 不接受 +name/-name。
会话与上下文
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
sessionDir | string | 代理会话目录 | 会话存储目录;相对路径从工作目录解析;PI_CODING_AGENT_SESSION_DIR 与 --session-dir 覆盖它 |
compaction.enabled | boolean | true | 自动压缩开关 |
compaction.reserveTokens | number | 16384 | 给模型响应保留的 token |
compaction.keepRecentTokens | number | 20000 | 不摘要保留的近期 token |
compaction.modelOverrides | object | 无 | 按 provider/modelId 的每模型 token 设置 |
branchSummary.reserveTokens | number | 16384 | 摘要分支历史时保留的 token |
branchSummary.skipPrompt | boolean | false | 跳过分支摘要提示,默认不生成摘要 |
压缩 token 值必须是非负安全整数。每个值的解析顺序:匹配的模型覆盖 → 普通压缩设置 → 内置默认;项目与用户对象先合并再按模型查找。
终端与显示
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
theme | string | system | 内置或自定义主题名;system 从终端主题推导颜色 |
quietStartup | boolean/header | false | true 隐藏启动头与已加载资源列表;header 保留头(版本与按键提示)但隐藏模型范围行与资源列表 |
tuiMode | regular/fullscreen | fullscreen | 交互终端 UI 模式 |
fullscreenExitOutput | transcript/resume-hint | transcript | 退出全屏时打印什么 |
fullscreenScrollbar | auto/always/hidden | auto | 全屏抄本滚动条 |
fullscreenCopyOnSelect | boolean | true | 全屏下选中即复制 |
fullscreenWheelScrollLines | auto/number(1-100) | auto | 每滚轮事件行数;macOS 本地终端 auto 为 1 行,其他及 SSH 下快速滚轮加速到最多 6 行;Alt+滚轮×5 |
editorPaddingX | number(0-3) | 0 | 编辑器水平内边距 |
outputPad | 0/1 | 1 | 抄本水平内边距 |
autocompleteMaxVisible | number(3-20) | 5 | 可见自动补全条数 |
showHardwareCursor | boolean | false | 显示硬件光标 |
terminal.showImages | boolean | true | 支持时显示内联图片 |
terminal.imageWidthCells | number | 60 | 内联图片首选宽度(终端列) |
terminal.clearOnShrink | boolean | false | 内容变短时清除空行 |
terminal.showTerminalProgress | boolean | false | 在终端标签显示 OSC 9;4 进度 |
terminal.hyperlinks | boolean/auto | auto | 覆盖 OSC 8 超链接检测 |
terminal.images | kitty/iterm2/auto/false | auto | 覆盖内联图片协议检测 |
terminal.trueColor | boolean/auto | auto | 覆盖真彩检测 |
images.autoResize | boolean | true | 发给模型前把图片缩到至多 2000×2000 |
images.blockImages | boolean | false | 阻止向模型发送图片 |
markdown.codeBlockIndent | string | 两个空格 | 代码块缩进前缀 |
markdown.mermaid | off/final/streaming | streaming | Mermaid 渲染模式 |
网络与重试
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
transport | auto/sse/websocket/websocket-cached | auto | 多传输供应商的首选传输 |
httpProxy | string | 无 | 作为 HTTP_PROXY/HTTPS_PROXY 应用于 Pi 管理的 HTTP 客户端。只能写在代理目录设置里 |
httpIdleTimeoutMs | number | 300000 | HTTP 头/体空闲超时;0 禁用 |
websocketConnectTimeoutMs | number | 15000 | WebSocket 连接超时;0 禁用 |
retry.enabled | boolean | true | agent 级瞬时失败自动重试 |
retry.maxRetries | number | 3 | agent 级最大重试次数 |
retry.baseDelayMs | number | 2000 | 指数退避初始延迟 |
retry.maxAgentDelayMs | number | 60000 | agent 级最大重试延迟 |
retry.provider.timeoutMs | number | 同 httpIdleTimeoutMs | 供应商请求超时 |
retry.provider.maxRetries | number | 0 | 供应商级重试次数 |
retry.provider.maxRetryDelayMs | number | 60000 | 服务端要求的最大延迟;0 取消限制 |
除非确实需要,retry.provider.maxRetries 保持 0:供应商级重试会推迟 Pi 自己对配额与用量上限错误的处理。
Shell
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
shellPath | string | 平台默认 | 自定义 shell 可执行路径,支持前导 ~ |
shellCommandPrefix | string | 无 | 加在每条 shell 命令前的前缀 |
npmCommand | string[] | npm | npm 包查找与安装所用命令与参数 |
资源
用户设置里的资源路径从代理目录解析,项目设置里的从项目 .pi 目录解析;支持绝对路径与 ~。
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
packages | array | [] | npm / git / 本地 Pi 包来源 |
extensions | string[] | [] | 扩展文件或目录 |
skills | string[] | [] | 技能文件或目录 |
prompts | string[] | [] | 提示模板文件或目录 |
themes | string[] | [] | 主题文件或目录 |
enableSkillCommands | boolean | true | 把技能注册为 /skill:name 命令 |
资源数组支持 !pattern 排除通配、+path 精确包含、-path 精确排除。用户级与项目级都列出的资源 Pi 都会加载(合并不去重)。
内置扩展在 extensions 里叫 builtin:mcp、builtin:llama.cpp、builtin:codemode、builtin:tool-search,默认都加载;-builtin:mcp 禁其中一个;项目设置里的 +builtin:<name> / -builtin:<name> 覆盖用户设置;pi config 在 Built-in 下列出它们;--no-extensions 也会禁掉它们,-e builtin:<name> 可显式加载。
更新、遥测与警告
| 设置 | 类型 | 默认 | 说明 |
|---|---|---|---|
collapseChangelog | boolean | false | 更新后显示精简 changelog |
enableInstallTelemetry | boolean | true | 匿名安装/更新上报与部分供应商归因头;不控制更新检查 |
enableAnalytics | boolean | false | 选择加入分析数据共享(目前仅实验性首次运行设置使用) |
warnings.anthropicExtraUsage | boolean | true | Anthropic 订阅认证可能用付费额外用量时给警告 |
3. 环境变量
三种用途:配置 Pi 进程、给子进程留下标记、给 shell 工具注入当前会话状态。
进程标记
CLI 与 RPC 入口会设两个标记,子进程继承(不从会话派生,SDK 嵌入时不自动设):
AI_AGENT=pi:通用标记,让工具知道是 Pi 启动的进程。PI_CODING_AGENT=true:Pi 专属,让子进程知道自己跑在 Pi 里面。
给 shell 工具的会话环境
bash 与 powershell 工具执行的命令会拿到:
| 变量 | 含义 |
|---|---|
PI_SESSION_ID | 当前会话 ID |
PI_SESSION_FILE | 当前会话 JSONL 的绝对路径;临时会话不设 |
PI_PROVIDER | 当前选中的模型供应商 |
PI_MODEL | 当前选中的模型 ID |
PI_REASONING_LEVEL | 当前生效推理级别:off/minimal/low/medium/high/xhigh/max |
值在每条命令启动时解析:切模型或改推理级别后,下一条 shell 命令就是新值,不用重启 Pi。PI_PROVIDER/PI_MODEL 是 Pi 选中的模型,不是路由器内部可能换成的上游模型。被问「现在跑的是什么模型」时应该查这两个变量,而不是从系统提示词猜:
printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"
这些变量只注入模型可调用的 bash/powershell 工具,不注入用户手输的 ! / !! 命令。用 createBashTool() / createPowerShellTool() 建的工具默认暴露;注入发生在 spawnHook 之前(钩子能在 ctx.env 里拿到),可用 exposeSessionEnvironment: false 单独关掉,关掉时 Pi 会移除继承来的值,避免嵌套 Pi 进程看到陈旧的父会话元数据。
Pi 自己读的变量
| 变量 | 含义 |
|---|---|
PI_CODING_AGENT_DIR | 覆盖配置目录,默认 ~/.pi/agent |
PI_CODING_AGENT_SESSION_DIR | 覆盖会话存储(--session-dir 优先级更高) |
PI_PACKAGE_DIR | 覆盖包目录,Nix/Guix store 路径有用 |
PI_OFFLINE | 禁用自动网络活动,包括模型目录刷新 |
PI_SKIP_VERSION_CHECK | 禁用对 pi.dev 的最新版本请求 |
PI_TELEMETRY | 覆盖安装/更新遥测与供应商归因头:1/true/yes 或 0/false/no |
PI_CACHE_RETENTION | 设为 long 以在支持的供应商上延长 prompt 缓存 |
PI_SHARE_VIEWER_URL | 覆盖 /share 用的基础 URL |
PI_RADIUS_GATEWAY | 覆盖 /bug 上传与 Radius 中继用的网关源 |
PI_HARDWARE_CURSOR | 设 1 显示硬件光标 |
PI_HYPERLINKS | 用 1/0/auto 覆盖 OSC 8 检测 |
PI_IMAGE_PROTOCOL | 用 kitty/iterm2/none/auto 覆盖内联图片检测 |
PI_TRUE_COLOR | 用 1/0/auto 覆盖真彩检测 |
PI_TUI_ESC_TIMEOUT | 单个 ESC 后等多久才当 Escape(毫秒);SSH 下默认 100,其他 10;Alt 键被误读为 Escape 时调大 |
VISUAL、EDITOR | externalEditor 未设时的外部编辑器回退 |
HTTP_PROXY、HTTPS_PROXY | 出站 HTTP 代理 |
供应商凭据变量(ANTHROPIC_API_KEY、OPENAI_API_KEY 等)见官方 providers 文档。
4. 快捷键
Pi 暴露命名动作(如 app.session.new),快捷键只是给动作绑定键位。跑 /hotkeys 看当前主编辑器与应用的实际快捷键。
绑定方法
建 <agent-dir>/keybindings.json,把动作标识映射到一个键或键数组:
{
"app.session.new": "ctrl+shift+n",
"app.session.tree": ["ctrl+shift+t", "alt+shift+t"]
}
配置值替换该动作的默认键;用空数组禁用其绑定:
{ "tui.altScreen.pageUp": [] }
改完要跑 /reload 在当前会话生效。
键位语法
修饰符+键;修饰符为 ctrl、shift、alt、super,可组合。合法键:
- 字母
a-z;数字0-9。 - 特殊键:
escape/esc、enter/return、tab、space、backspace、delete、insert、clear、home、end、pageUp、pageDown、up、down、left、right。 - 功能键
f1-f12。 - 符号:
`、-、=、[、]、\、;、'、,、.、/、!、@、#、$、%、^、&、*、(、)、_、+、|、~、{、}、:、<、>、?。
例:ctrl+shift+x、alt+ctrl+x、super+k、ctrl+1。super 绑定需要终端单独上报修饰键(通常是 Kitty 键盘协议),不支持时可能无效。
常用动作(默认键)
编辑器:tui.editor.cursorUp=up(到顶部时翻历史)、cursorDown=down;historyPrevious/historyNext 默认未绑。cursorLeft=left,ctrl+b;cursorRight=right,ctrl+f;cursorWordLeft=alt+left,ctrl+left,alt+b;cursorWordRight=alt+right,ctrl+right,alt+f;cursorLineStart=home,ctrl+home,ctrl+a;cursorLineEnd=end,ctrl+end,ctrl+e;jumpForward=ctrl+];jumpBackward=ctrl+alt+];pageUp/pageDown。专用历史动作不受光标位置影响,且优先于同键的应用动作。
编辑:deleteCharBackward=backspace;deleteCharForward=delete,ctrl+d;deleteWordBackward=ctrl+w,alt+backspace;deleteWordForward=alt+d,alt+delete;deleteToLineStart=ctrl+u;deleteToLineEnd=ctrl+k;yank=ctrl+y;yankPop=alt+y;undo=ctrl+-(Windows ctrl+z,WSL alt+z)。
输入与选择:tui.input.newLine=shift+enter,ctrl+j;tui.input.submit=enter;tui.input.tab=tab;tui.input.copy=ctrl+c;tui.select.up/down/pageUp/pageDown/confirm(=enter)/cancel(=escape,ctrl+c)。
全屏(优先于同键编辑器动作):altScreen.pageUp/pageDown=pageUp/pageDown;halfPageUp/halfPageDown/lineUp/lineDown 默认未绑;previousPrompt=ctrl+shift+up,ctrl+up(Windows/WSL 仅 ctrl+up);nextPrompt=ctrl+shift+down,ctrl+down;search=ctrl+shift+f(Windows/WSL ctrl+f);searchNext=enter,ctrl+g;searchPrevious=shift+enter,ctrl+shift+g;searchClose=escape;top=home;bottom=end。
应用:app.interrupt=escape(取消/中止);app.clear=ctrl+c(先清编辑器,再按退出);app.exit=ctrl+d(编辑器为空时);app.suspend=ctrl+z(Windows 默认无,手绑也只显示状态而不挂起;WSL 用正常 ctrl+z+fg);app.editor.external=ctrl+g(externalEditor/$VISUAL/$EDITOR/Windows 记事本/其他 nano);app.clipboard.pasteImage=ctrl+v(Windows/WSL alt+v)。
会话:app.session.new/tree/fork/resume 默认都未绑定(可用 /new、/tree、/fork、/resume);togglePath=ctrl+p;toggleSort=ctrl+s;toggleNamedFilter=ctrl+n;rename=ctrl+r;delete=ctrl+d;deleteNoninvasive=ctrl+backspace(查询为空时才删)。
模型与思考:app.model.select=ctrl+l;cycleForward=ctrl+p;cycleBackward=shift+ctrl+p(Windows/WSL alt+p);models.save=ctrl+s;app.thinking.cycle=shift+tab;app.thinking.save=ctrl+s;app.thinking.toggle=ctrl+t。
显示与队列:app.tools.expand=ctrl+o;app.message.copy=ctrl+x;app.message.followUp=alt+enter(Windows/WSL ctrl+q);app.message.dequeue=alt+up(Windows/WSL alt+q)。
树导航:app.tree.foldOrUp=ctrl+left,alt+left;unfoldOrDown=ctrl+right,alt+right;editLabel=shift+l;toggleLabelTimestamp=shift+t;过滤动作 filter.default=ctrl+d、filter.noTools=ctrl+t、filter.userOnly=ctrl+u、filter.labeledOnly=ctrl+l、filter.all=ctrl+a、filter.cycleForward=ctrl+o、filter.cycleBackward=shift+ctrl+o。
范围模型选择器(/scoped-models 内):app.models.enableAll=ctrl+a;clearAll=ctrl+x;toggleProvider=ctrl+p;reorderUp=alt+up;reorderDown=alt+down。
5. models.json:接一个兼容端点
当端点说的 API 已经是 Pi 支持的(大多数 Ollama、LM Studio、vLLM、SGLang、代理部署),不要写扩展,写 models.json:
{
"providers": {
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"api": "openai-completions",
"apiKey": "ollama",
"models": [{ "id": "qwen2.5-coder:7b" }]
}
}
}
- 假 key 只是让模型在 Pi 里可用,Ollama 会忽略它。
apiKey与 header 值支持$NAME/${NAME}环境插值、字面量、或前导!command(命令在请求时跑,Pi 不缓存)。- 打开
/model会重新加载该文件。models条目按 ID 新增或替换该供应商的同 ID 模型;想改已有内置/扩展模型的元数据而不替换供应商模型列表时用modelOverrides(未知 ID 被忽略)。
模型输入限制与缓存寿命
{
"id": "vision-model",
"input": ["text", "image"],
"inputLimits": {
"images": {
"resize": { "maxWidth": 1568, "maxHeight": 1568, "maxBytes": 524288, "jpegQuality": 75 }
}
}
}
inputLimits.images.resize 控制 Pi 在把新图片附件、read 结果和工具结果图片写进历史前的编码方式;maxBytes 限制 base64 编码后的负载;省略的字段用保守默认:2000×2000、编码后 4.5 MiB、JPEG 质量 80。图片只编码一次,换模型不会重写历史图片。目录还能用 inputLimits.maxRequestBytes、images.maxPerMessage、images.maxPerRequest 描述硬性请求上限,但 Pi 目前还不会据此重写或拒绝历史。
{ "id": "claude-sonnet-5", "promptCache": { "short": 300, "long": 3600 } }
promptCache 声明供应商尽力而为的缓存寿命(秒),分 short/long 两档;取公开范围里保守的一端;当前档位没有寿命的模型不参与缓存预热。
兼容性开关只应用于已验证的端点行为差异,不要因为对方宣传「兼容 OpenAI/Anthropic」就打开。
凭据优先级
同时配置多个来源时,Pi 依次使用:运行时 --api-key → auth.json 里存的凭据 → models.json 的 apiKey → 供应商环境变量或云环境凭据。auth.json 与凭据命令必须保密;项目设置在授予信任后会在 Pi 进程内执行。
分类器与图像模型
这两类模型不出现在 /model 里,模型只能通过 codemode 工具用它们:
- 分类器不做对话,只对 JSON 状态回答类型化问题(多选一、是否、给分,各带概率)。内置 TypeSafe Jev:
typesafe/jev-latest(TYPESAFE_API_KEY)、openrouter/typesafe/jev-1.13与~typesafe/jev-latest(OPENROUTER_API_KEY或/login)、cloudflare-workers-ai/typesafe/jev(需CLOUDFLARE_API_KEY+CLOUDFLARE_ACCOUNT_ID)、vercel-ai-gateway/typesafe-ai/jev(AI_GATEWAY_API_KEY)、opencode/jev-1.13与jev-1.13-free(OPENCODE_API_KEY);llama.cpp 路由器上的聊天模型也作为分类器列出。脚本里models.getAvailableOfType("classifier")+models.classify(model, { state, questions })。 - 图像模型按提示词与可选输入图生成图:Pi 在
openrouter下列出google/gemini-2.5-flash-image、black-forest-labs/flux.2-pro等,用同一把 key。脚本里models.getAvailableOfType("image")+models.generateImages(model, { input }),结果output是 base64 图像块,用image(block)挂到 codemode 结果上让模型看到;生成的图片不落盘。
脚本里分类器与图像调用的用量会累加到 codemode 工具结果,因此计入页脚与 /session 的会话成本;成本按目录价格算,没有价格的模型(如 TypeSafe 直连 jev-latest)只报 token 不算钱。扩展里不经 codemode,直接用 ctx.modelRegistry.classify() / ctx.modelRegistry.generateImages()。
本地模型
Pi 直接集成 llama.cpp 路由器:路由器发现 GGUF 文件并按需加载模型,/llama 管理路由器,/model 选它已加载的模型。Ollama、LM Studio、vLLM、SGLang 等其他兼容服务器走上面的 models.json。
6. 自定义供应商(provider 扩展)
当服务需要自定义认证、动态模型发现、自定义请求处理或自定义流式时,才写 provider 扩展。它在 Pi 进程内运行,能读凭据、提示词、工具定义、模型响应与用量——当作可信代码对待,不要打印凭据或供应商负载。
选最小的集成方式
| 需求 | 用什么 |
|---|---|
| 在已支持 API 后面加模型 | models.json |
| 改已有供应商的端点或头 | models.json 或小 provider 扩展 |
| 动态发现模型 | 带 refreshModels 的 provider |
加 /login 流程 | 带原生或 legacy OAuth 配置的 provider |
| 实现不支持的线协议 | 带 stream 或 streamSimple 的 provider |
provider 扩展就是普通扩展,加载、信任、reload、错误行为完全一致。
注册
在扩展工厂里调 pi.registerProvider()。Pi 会等异步工厂完成才继续启动,所以那里注册的 provider 对启动时的模型选择与 pi --list-models 都可见。两种形态:
- 注册
@earendil-works/pi-ai的完整Provider:原生认证、过滤、发现、刷新、流式。新集成只要拥有超过「静态端点 + 模型元数据」的东西,就优先用这个。 - 注册名字 +
ProviderConfig:老的配置式写法。
Pi 把 models.json 的覆盖叠在注册的原生 provider 之上。只给已有供应商注册 baseUrl 或 headers 会保留其内置模型;legacy 形态里给了 models 就会替换该供应商在 chat / image / classifier 三种操作上的模型。省略 type 等于 "chat";图像与分类器模型必须显式写判别式,并按 api 键在 images / classifiers 字段里提供实现:
pi.registerProvider("media-tools", {
apiKey: "$MEDIA_TOOLS_API_KEY",
models: [
{ type: "image", id: "image-v1", name: "Image V1", api: "media-images",
baseUrl: "https://media.example.com/v1", input: ["text"], output: ["image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
{ type: "classifier", id: "classifier-v1", name: "Classifier V1", api: "media-classifier",
baseUrl: "https://media.example.com/v1", input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 64000 },
],
images: { "media-images": { generateImages: async (model, context, options) => result } },
classifiers: { "media-classifier": { classify: async (model, context, options) => result } },
});
- 模型级
baseUrl优先于供应商端点;没给models列表时每种操作的内置模型都保留;不同操作里的同 ID 模型互不混淆,各自的模型级 header 也保留。 - 扩展初始加载之后的调用立即生效;
pi.unregisterProvider()移除动态 provider 并恢复它替换掉的内置行为。 - 官方完整示例见仓库
examples/extensions/custom-provider-gitlab-duo/(把流式委托给内置 API 实现)。
认证
静态 provider 的 key 可以来自字面量、环境插值或命令,语法同 models.json:$NAME / ${NAME} 读环境变量;前导 !command 用命令输出;$$ 输出字面 $;$! 输出字面前导 !。需要存储凭据、自定义解析、供应商作用域环境或多登录方式时用原生 provider 认证。OAuth provider 要提供显示名、登录流程、token 刷新与 access-token 解析;注册后它出现在 /login 里,凭据存进 ~/.pi/agent/auth.json。OAuth 回调是UI 中立的(可开授权 URL、显示设备码、报进度、要输入、要用户选登录方式),网络请求中必须尊重取消与传入的 abort signal。绝不把 access token、refresh token、authorization 头或完整供应商响应写进普通日志。
模型目录与刷新
每个模型都需要 ID、显示名、输入能力与成本元数据;chat 与 classifier 模型还需要上下文窗口,chat 模型还要输出上限与推理支持,图像模型要声明输出模态。API 实现选在供应商级,除非单个模型需要覆盖。想让 Pi 保持空闲 prompt cache 温热时,把 promptCache.short 或 .long 设成供应商尽力而为的缓存寿命(秒);不设即该档不预热。
refreshModels 用于目录来自活服务的场景,给阻塞 I/O 传 context.signal 以便调用者取消刷新。两种注册形态的刷新契约不同:
- 完整
Provider不返回任何东西,它调context.publish({ update })安装 provider 自有的模型状态,之后同步的getModels()暴露最新列表。 - legacy
ProviderConfig.refreshModels返回混合操作的模型定义,Pi 用返回的列表替换该注册的活模型,并应用其请求的持久化。
只有需要跨运行保留的目录数据才持久化:像 llama.cpp 这类活服务可以在内存里更新列表而不落盘,远程目录可以留快照以便离线启动。
复用已支持的流式 API
协议能对上就复用 Pi AI 的 API 实现,别自己拷一个。已支持的实现覆盖:Anthropic Messages、OpenAI Chat Completions 与 Responses、Google Generative AI 与 Vertex、Azure OpenAI Responses、Mistral Conversations、Bedrock Converse。provider 仍然可以自定义认证、base URL、header、模型过滤与发现,只把请求转换与流式委托出去——这比拷贝流实现安全,因为保留了消息转换、工具处理、用量统计、取消与兼容行为。
自己实现流式
只有没有任何现有 API 实现能表达该服务时才写 streamSimple,并且先研究仓库 packages/ai/src/api 下的实现。
- 流拿到的是归一化的
TranscriptContext:系统提示词与工具声明都在 transcript 的 system 消息里,要用getCurrentSystemPrompt(context.messages)与getCurrentTools(context.messages)读,不要指望context.systemPrompt/context.tools。支持会话中途 system 消息的模型可以原地收;否则用collapseSystemMessages(context)把后续 system 消息折进开头那条。 - 自定义流必须:① 先建一条 assistant 消息(provider、model、timestamp、pending stop reason、content、归零 usage);② 请求建立成功后、发内容事件之前发恰好一个
start;③ 发平衡的 text / thinking / tool-call 事件并同步更新消息;④ 定稿 usage、cost、content 与 stop reason;⑤ 发恰好一个终止done或error并关流;⑥ 把取消转成 aborted 结果。 - 请求建立阶段可以在
start之前失败,此时流可以直接以error终止;缺认证也可能在返回流之前同步抛错。 - content index 指向 assistant 消息里的块;每次发事件前要先更新对应块(事件的
partial暴露该状态);到toolcall_end时参数必须已是合法解析后的输入。 - 还必须尊重
SimpleStreamOptions的请求插桩:发请求前调options.onPayload并使用其返回的替换负载;拿到响应后、消费 body 前调options.onResponse;对每个解析出的供应商事件先await options.onProviderStreamEvent?.(providerEvent, model)再归一化;透传 abort signal 与供应商作用域环境。这些钩子支撑扩展的请求检查、响应头事件与供应商流观测,漏掉它们会让 provider 行为与内置不一致。
失败与用量
给具体的终止 stop reason;错误与中止的消息要有 errorMessage,成功的要有准确的 input/output/cache/total token 与 cost。Pi 能对被识别的上下文溢出错误做压缩加重试——如果服务用未知文案,只在带守卫的 message_end 处理器里把该供应商自己的溢出响应归一化成 context_length_exceeded。不要把限流或瞬时供应商失败改写成上下文溢出:那些走 Pi 的正常重试。
测试清单
至少覆盖:普通与空文本响应、工具调用与工具结果、支持时的图片输入与图片工具结果、用量与成本统计、中止行为、上下文溢出、畸形或部分流、Unicode 边界、跨供应商会话交接、认证刷新与取消。仓库 packages/ai/test 里的 provider 测试定义了内置 provider 被期望的行为,改造相关套件而不是只靠手测。开发时先直接跑扩展,再放进被发现的扩展位置或用 Pi 包分发;改动活动会话里的供应商扩展后跑 /reload。
7. 虚拟模型(路由模型)
虚拟模型是「可选中的、每次请求再挑一个物理模型」的模型。按任务、成本或对话状态路由时用它:用户只选一个模型,路由器把快问快答给小模型、难题给大模型。
虚拟模型由扩展注册,然后和普通模型一样出现在 /model、--model、范围模型与设置里;可以挂在任何 provider 下(包括已有物理模型的,如 openai-codex/auto)。
选择与派发的分离
虚拟模型选的是「模型 + 思考级别」,路由器把这对映射成物理的一对:
selected (virtual model, virtual level) -> dispatched (physical model, physical level)
jev/auto:low -> anthropic/claude-sonnet-4-5:high
虚拟思考级别只是路由器的输入,含义由路由器决定,不必对应推理预算。Pi 把两对分得很开:
| 选择 | 派发 | |
|---|---|---|
| 记录在 | model_change 与 thinking_level_change 条目 | 每条 assistant 消息的 provider、api、model、thinkingLevel |
| 表现为 | ctx.model、ctx.thinkingLevel、PI_MODEL、PI_REASONING_LEVEL、/model | 每次响应的 assistant 消息 |
供应商只会拿到物理模型;assistant 消息写的是物理模型,所以把一段对话在不同物理模型之间重放,与手动切模型后一样。恢复会话时从最新的 model_change 条目恢复虚拟选择;虚拟模型已不存在时,回退到最后作答的物理模型。交互模式页脚会在选择旁显示实际路由,如 auto • high → gpt-5.6-luna • medium;/session 按物理模型列成本。
上下文用量按产生最新响应的物理模型的限制计算(即使那条响应发生在切到虚拟模型之前);没有这样的响应时用虚拟模型自己声明的限制。压缩检查同样的限制,并且同样检查每个请求被路由到的模型:目标模型上下文窗口装不下对话时,Pi 在发请求前压缩,但路由仍按路由器的选择。
注册
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.registerVirtualModel({
provider: "router",
id: "auto",
name: "Auto",
thinkingLevels: ["low", "high"],
route(request, ctx) {
// 工具后续调用与重试留在处理该 turn 的模型上。
const sticky = request.failed ?? request.previous;
if (request.reason !== "user" && sticky) {
return { model: sticky.model, thinkingLevel: sticky.thinkingLevel ?? "medium" };
}
const id = request.thinkingLevel === "high" ? "claude-sonnet-4-5" : "claude-haiku-4-5";
return { model: ctx.modelRegistry.find("anthropic", id)!, thinkingLevel: "medium" };
},
});
}
provider是列表里显示的 provider,可以是任何 ID;同一 provider 可以把虚拟模型和物理模型并列;挂在物理 provider 上时该 provider 有凭据才可用,挂在没人用的 ID 下则始终可用。id不能是该 provider 某个物理模型的 ID;若之后目录刷新加进了同 ID 的物理模型,虚拟模型会把它藏起来。thinkingLevels是可选中的级别,默认["off"];contextWindow与maxTokens在首次响应前显示,未设即未知;input是可选中输入类型,默认文本与图片(不支持图片的物理模型会收到占位符)。- 注册遵循与
pi.registerProvider()相同的排队与 reload 规则;重复注册同 provider+id 会替换;pi.unregisterVirtualModel(provider, id)移除,pi.unregisterProvider()不会移除它;SDK 可以不经扩展直接modelRuntime.registerVirtualModel(definition)。
路由契约
route(request, ctx) 在用该虚拟模型的每次请求前运行,返回 { model, thinkingLevel }。model 可以是目录里任何其 provider 有凭据的物理模型(用 ctx.modelRegistry 查);虚拟模型不能路由到另一个虚拟模型;Pi 会把思考级别夹到目标模型支持的范围内。
| 字段 | 含义 |
|---|---|
model、thinkingLevel | 选中的虚拟模型与级别 |
reason | 这次请求为什么发起(见下表) |
previous | messages 里最近一次成功响应的物理模型与思考级别 |
failed | 仅 retry 时:失败请求的物理模型、思考级别与 assistant message(该消息已不在 messages 里,带 stopReason 与 errorMessage);路由本身失败时不存在 |
state | 该会话分支上上次返回的路由状态 |
messages | 本次请求的对话,含 system 消息 |
signal | 请求的 abort signal |
reason | 是什么请求 |
|---|---|
user | 用户写完一条消息后的首个请求,含 steering 与 follow-up 消息 |
continuation | agent 循环里其他请求,如工具结果或扩展消息之后 |
retry | 失败后的自动重试,包括上下文溢出后的压缩重试 |
direct | agent 循环之外的请求,如压缩摘要、扩展调 ctx.modelRegistry.streamSimple() |
continuation返回previous、retry返回failed,能保住 prompt cache 与 thinking 签名;turn 之间换模型是允许的,但会丢 prompt cache;重试也可以换模型,例如failed.message.errorMessage说供应商过载或上下文溢出时。route()抛错、返回了虚拟模型、或返回的模型没有凭据,请求就以错误响应结束。
路由状态
route() 可以在模型旁边返回 state,Pi 把它存在会话分支上,后续请求作为 request.state 传回。用于对话记录里没有的决策,比如分类器结果或路由阶段:
pi.registerVirtualModel<{ phase: "plan" | "build" }>({
provider: "router",
id: "phased",
name: "Phased",
route(request, ctx) {
const state = request.state ?? { phase: "plan" };
const id = state.phase === "plan" ? "claude-opus-4-5" : "claude-haiku-4-5";
return { model: ctx.modelRegistry.find("anthropic", id)!, thinkingLevel: "medium", state };
},
});
- 状态必须可 JSON 序列化。返回
undefined或request.state本身表示保持当前状态。 - 返回其他对象会在请求发出去之前存为新状态(即使它与当前状态相等),所以只在状态真的变了时返回新对象;请求后来失败,状态也仍然保留。
- 状态跟随会话树:fork 与
/tree导航看到的是各自分支的状态,压缩后依然存在。direct请求没有状态,其返回的状态被忽略。 - 对话记录本身就记了选择与每次派发的模型,
ctx.sessionManager.getBranch()能同时拿到两者。
路由器可以通过 ctx.modelRegistry 调用别的模型,例如用 ctx.modelRegistry.classify() 配 ctx.modelRegistry.findOfType("classifier", provider, id);这会给 turn 的首个 token 前加延迟。完整示例见仓库 examples/extensions/jev-router.ts:它先用 Jev 分类器选中的强 OpenAI Codex 模型做规划,让该模型做第一次编辑,之后只切一次到更便宜的模型,接受一次 prompt-cache miss,并把阶段作为路由状态保存。
8. 配置速查
- 改偏好、默认模型、工具集 →
settings.json(用户级 / 项目级),/reload生效。 - 改键位 →
keybindings.json,动作名映射键位,空数组禁用。 - 接兼容端点(Ollama/vLLM/代理)→
models.json的providers.<name>,顺带能声明inputLimits与promptCache。 - 端点需要自定义协议/认证/发现 → 写 provider 扩展,
pi.registerProvider()。 - 想按任务/成本路由 → 写虚拟模型,
pi.registerVirtualModel()。 - 切 prompt cache 档位 / 离线 / 超时 / 光标 → 环境变量,不用改配置文件。
溯源
- 官方
docs/settings.md、docs/configuration.md、docs/environment-variables.md、docs/keybindings.md、docs/models.md、docs/custom-provider.md、docs/virtual-models.md