pi extension: Command Code / SCNet providers + model catalog
  • TypeScript 53.9%
  • JavaScript 46.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-22 18:36:31 +08:00
.codestable docs: note /custom-providers init as the intentional write exception 2026-09-22 18:36:31 +08:00
extensions/custom-providers feat!: providers come only from directories (no built-in ids) 2026-09-22 18:26:37 +08:00
scripts feat!: providers come only from directories (no built-in ids) 2026-09-22 18:26:37 +08:00
tests feat!: providers come only from directories (no built-in ids) 2026-09-22 18:26:37 +08:00
.gitignore feat: custom-providers extension with SCNet merge 2026-09-18 21:02:11 +08:00
LICENSE feat: custom-providers extension with SCNet merge 2026-09-18 21:02:11 +08:00
README.md docs: install tag v0.2.4 (directory-driven providers, no built-in ids) 2026-09-22 18:31:33 +08:00

custom-providers

pi extension把订阅型中转站Command Code / GOAT、SCNet注册成 pi provider模型目录随仓库维护配置词汇全部用 pi 自己的字段。

安装:pi install ssh://forgejo@git.lentech.site/C02-1010751281/pi-custom-providers.git@v0.2.4(镜像:git:github.com/P02-1010751281/pi-custom-providers;源码 extensions/custom-providers/。v0.2.4 = providers 只来自目录(无内置 id旧装用 /custom-providers init 补目录v0.2.3 = catalog 刷新 + 目录覆盖内置不再告警v0.2.2 = 改名残留清理v0.2.0 = 通用多协议引擎v0.1.0 是旧版。本包无 package.jsonpi 按约定目录 extensions/ 自动发现git 安装不依赖 npm不要再加回。

为什么独立成包

  • 这些中转站的 /models 端点几乎不带元数据SCNet 只返回 id参数只能人工整理原先放在 gitignore 的 ~/.pi/agent/models.json 里,会漂移,刷新后新模型静默拿到 pi 默认值128000 上下文 / 16384 输出)。
  • pi 要求每个注册模型带 cost;缺了会让 calculateCost() 在第一次上报用量时抛 Cannot read properties of undefined (reading 'tiers'),整轮任务中断。
  • 一个厂商常有两个协议端点服务同一批 id而 pi 的文件层表达不了(一个 provider id 里同名的 model.id 只能有一份,且 model.id 就是请求体的 model),所以协议只能按模型选——这正是本扩展要补的那一块。

安装与迁移

旧版是单文件 ~/.pi/agent/extensions/subscription-providers.ts(如果本机还留着,必须先删):

rm -f ~/.pi/agent/extensions/subscription-providers.ts

两种安装方式只能选一种,否则 pi 会同时加载两份、provider 注册两次。本地开发用方式 B

pi install ssh://forgejo@git.lentech.site/C02-1010751281/pi-custom-providers.git@v0.2.4   # 方式 AGitHub 镜像把 host 换成 git:github.com/P02-1010751281/pi-custom-providers
rm -rf ~/.pi/agent/extensions/custom-providers && cp -R extensions/custom-providers ~/.pi/agent/extensions/   # 方式 B随后 /reload

从旧版升级v0.1.0 → v4.0 引擎)

  • provider id 由 codecommand 改为 commandcode(域名拼写)。旧键 providers.codecommand 仍然被读取(aliases),但 pi 会把只在 models.json 里声明的 id 也注册成一个 provider于是选择器里会多出一个 codecommand 条目。把 models.json 里的键改成 providers.commandcode 即可消掉。
  • providers.<id>.wire / wire.models 不再被读取pi 原生 api 就是协议):一个模型要换协议,写模型条目的 api
  • siblingId / anthropicBaseUrl 这两个旧字段已删除SCNet 的 Anthropic 端点现在是 apis."anthropic-messages"

Providers

provider id 默认协议 默认端点 其它端点 密钥变量
commandcode openai-completions https://api.commandcode.ai/provider/v1 anthropic-messages: https://api.commandcode.ai/provider CMD_API_KEY
scnet openai-completions https://api.scnet.cn/api/llm/v1 anthropic-messages: https://api.scnet.cn/api/llm/anthropic SCNET_API_KEY

SCNet 两条线服务同一批 id19 个里重叠 18 个),注册成一个 scnet选择器里只有一条每个模型带自己的协议。Command Code 的 8 个 Claude id 只走 Anthropic 端点,因此它们的条目自带 api: "anthropic-messages"

为什么 Anthropic 线的 baseUrl 要短一截

pi 把 model.baseUrl 原样交给 Anthropic SDK而 SDK 自己会在后面拼 /v1/messages。所以 OpenAI 线的 baseUrl 带 /v1Anthropic 线的不能带(否则路径变成 /v1/v1/messages。实测2026-09-18POST /provider/v1/messages 返回 403 MODEL_NOT_IN_PLAN(路由存在),POST /provider/v1/v1/messages 返回 404「not a registered API route」——旧写法下 codecommand 的 8 个 claude 模型全部打不通。

配置:~/.pi/agent/custom-providers/<id>/

目录是 provider 的唯一来源:没有 provider.json 的目录不算 vendor没有目录就没有这个 provider。出厂的 sources.ts + catalog.ts 只在同名目录存在时作为它的基底(端点表、模型表、密钥变量),绝不单独注册——所以没有「内置 id」也没有「目录 replace 内置」这回事。新装机器先跑一次 /custom-providers init(无参数 = 全部出厂 vendor也可只写名字把默认端点写出来。

~/.pi/agent/custom-providers/
├── scnet/
│   ├── provider.json     # 端点表api / baseUrl / modelsPath / headers / apis无秘密、无 compat
│   ├── models.json       # 模型基底表(可省;省了就用该 id 的出厂默认表)
│   └── accounts.json     # 凭据(唯一秘密文件;可省,省了用出厂密钥变量或提示 /login
└── my-relay/
    └── provider.json     # 新 vendor 只要这一个文件
  • 发现 = 扫描子目录,含可解析 provider.json 的子目录才算一个 vendor其余忽略/custom-providers files 会列出来)。
  • 字段全是 pi 自己的词汇,只有四个是我们加的:apis(第二协议端点)、modelsPath(发现路径)、override(接管 pi 内置 provider idaccounts.json

provider.json(必需)

{
  "name": "My Relay",
  "api": "openai-completions",
  "baseUrl": "https://relay.example/v1",
  "modelsPath": "/models",
  "headers": {},
  "apis": {
    "anthropic-messages": { "baseUrl": "https://relay.example/anthropic", "modelsPath": "/v1/models" }
  }
}
字段 必填 说明
api 默认协议,取 pi 全部 10 种内置协议之一(别名:openai/chatanthropic/messagesresponses)。表外的值 → 报告并跳过该 vendor。
baseUrl 默认端点。
modelsPath 默认端点列模型的路径;缺省 = 无发现(不猜 /models)。
headers vendor 级请求头(值支持 pi 的值语法;不写秘密)。
apis 额外协议端点,键 = pi 的 api 值,值 = { baseUrl(必填), modelsPath?, headers? }。省略 modelsPath = 继承 provider.json.modelsPath
override 允许接管 pi 已知 provider id如自建 anthropic 代理)。不加则跳过并报告。

apiKey / authHeader / envVar / compat 写在这里会被报告并忽略:凭据只属 accounts.jsoncompat 只属模型条目pi 只认模型级 compat

models.json(可选)= 模型基底表

{ "models": [
  { "id": "glm-5.2", "api": "anthropic-messages", "name": "GLM-5.2", "reasoning": true, "input": ["text"],
    "contextWindow": 200000, "maxTokens": 131072,
    "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } }
] }

字段 = pi 的 ModelDefinitionSchema 全部字段(id/name/api/baseUrl/reasoning/thinkingLevelMap/input/cost/contextWindow/maxTokens/samplingParams/headers/compat),没有自有字段。也接受纯数组简写。省略 api = 默认协议;写了 apis 里的协议就自动用该端点的 baseUrl

accounts.json(可选)= 凭据

{ "default": "main",
  "main": { "apiKey": "$SCNET_API_KEY", "authHeader": true },
  "work": { "apiKey": "!pass show scnet/work" } }

default账号 id 字符串指针:被指向的账号注册为 <id>,其余注册为 <id>-<name>(上例 → scnetscnet-work)。账号只装认证apiKey / authHeader / headers)。

  • 没有 apiKey 的账号被跳过并报告;非法账号名同理。
  • accounts.json 不存在 / 为空 / 全无凭据 → <id> 照常注册(不带凭据、模型不进可用快照),/login <id>--api-key 或 stored 凭据随时能把它救回来。
  • 有账号但 default 指针缺失或指向不存在的账号 → 目录 vendor 不注册 <id>(其余账号照常);同 id 有出厂默认账号时回落该密钥变量。
  • 账号级模型覆盖写 pi 全局 models.jsonproviders.<accountId>,本扩展不另造一层。
  • 同一产品多把 key = 一个目录多个账号;不同产品/计费 = 不同目录、不同 provider idpi 的 auth.json 是一 id 一凭据)。

配置优先级4 层,逐字段补丁)

# 粒度
1 基底模型表(catalog.ts<id>/models.json 模型
2 provider.json(默认协议 + 端点 + apis + headers provider
3 pi 全局 models.jsonproviders.<id>provider 字段 + models[] 逐条补丁) provider + 模型
4 pi 全局 models.jsonmodelOverrides[M] 模型pi 自己最后应用)

每一层都是补丁:写了就赢,没写往下掉。唯一例外是 baseUrl,按 pi 原义 config.baseUrl ?? model.baseUrl —— 第 3 层的 provider 级 baseUrl 只重定向「自己没有端点的模型」。

协议选择只有两步:模型条目写了 api 就用它(baseUrl 取条目自己的,否则 apis.<api>.baseUrl);没写就用有效默认协议 = 第 3 层 providers.<id>.api ?? provider.json.api。落在默认协议上的模型不带 api/baseUrl(这样 providers.<id>.baseUrl 以后还能重定向它);不在默认协议上的两者都带,显示名加 (协议) 后缀。

命令

命令 作用 写盘
/refresh-custom-models 刷新全部 vendor 全部端点的模型列表
/custom-providers 状态总览模型数、live/基底、协议分布、新 id、上次错误
/custom-providers <id> 单 provider 详情:协议分布、账号、校验问题
/custom-providers drift 与 pi 内置目录的差异(只报不改)
/custom-providers files 扫描结果:目录、三文件、被忽略的目录、校验问题
/custom-providers init [<id>...] [--force] 为出厂默认 vendor 写 <id>/provider.json(已存在则不动,--force 覆盖);不写任何密钥 provider.json
/custom-providers sync <id> [--write] 打印「基底 ⊕ 发现 vs <id>/models.json」差异;--write 才落盘(先留 .bak --write

sync --write 写的是基底 ⊕ 发现,不含第 3/4 层用户覆盖(否则一次 sync 就把用户覆盖烤进基底);发现里消失的 id 会保留并在摘要里标为「kept」。

密钥解析

accounts.jsonapiKey 支持 pi 的值语法:$VAR / ${VAR} / !command / $$ / $!,以及裸 UPPER_SNAKE(视为环境变量名,交给 pi 前会规范化为 $VAR —— pi 只插值 $…,裸字符串会被当字面量发出去)。发现请求用的凭据顺序与 pi 的请求侧一致storedauth.json / --api-key / /login)→ 账号 apiKey → 第 3 层 providers.<id>.apiKey → 内置密钥变量。启动时读取 ~/.pi/agent/.env~/.omp/agent/.env 补齐环境变量(已存在的不覆盖)。

!command 就是给秘密后端留的插座——本插件不绑定任何后端,任何能把 key 打到 stdout 的命令都行:

方式 apiKey 写法 备注
直接写 "sk-…" 单机最省事;accounts.json 必须 gitignore + chmod 600
环境变量 "$SCNET_API_KEY" key 放自己的 ~/.pi/agent/.env(该文件须在 .gitignore 里)
系统 keyring "!secret-tool lookup service scnet account main" GNOME secret-tool / KDE kwallet-query
密码库 "!keepassxc-cli show -q -s -a password ~/secrets.kdbx pi/scnet" 也可 "!pass show scnet/work""!op read op://vault/item/credential"
加密文件 "!gpg --batch --decrypt ~/.pi/secrets/scnet.gpg" age / sops 同理,能打印 key 即可

!command 在发现刷新本扩展和请求pi时都会执行10s 超时、stderr 被吞、非零退出 = 拿不到 keypi 侧结果进程内缓存,轮换 key 后需重启 pi。无人值守取密总需要本机已有可自动解开的本钱keyring 登录态 / 无口令私钥 / agent 缓存),它防的是误提交与误备份,不是本机失陷。

凭据永远不进仓库(私有仓库、镜像仓库同理):accounts.json.env 属用户层,仓库里只该出现 provider.json / models.json。别人装本插件用的是自己的 ~/.pi/agent/custom-providers/<id>/accounts.json,与本项目互不相干。

模型能力与目录生成

extensions/custom-providers/catalog.ts 是生成物(node scripts/refresh-catalog.mjs,先 --dry-run)。其中:

  • reasoning / input 的权威是官方 / pi 内置目录(同一 id 在 pi 内置各 provider 间多数票 → 厂商能力页 → 旧值);中转线自己的 /models 根本不发能力字段。
  • thinkingLevelMap 只在 anthropic 协议线上取 pi 内置值(那是 Anthropic 自己的 adaptive-effort 档位属模型事实OpenAI 形线的 effort 词表是网关自定义的,不照搬。
  • maxTokens / contextWindow / cost 以代理为准(只报不改:中转线会截断,高报会 400

测试

node tests/run-all.mjs

11 个用例:apis-test(协议选择 / 内置协议表与 pi 注册表一致)、provider-files-test(目录扫描与校验、接管边界)、accounts-test(账号展开与凭据回落)、no-builtin-test(没有目录就没有 provider、init 写盘)、sync-test(差异、.bak、round-tripresponses-test(用 pi 自己的实现验证 POST <baseUrl>/responses)、builtin-testcatalog-testpi-native-test(真 ModelRuntimeregisterProvider → refresh → publish,全程离线)、smokeloadtestpi 真实 loader 加载无错)。测试通过 pi 自己的 jiti loader 加载 TSPI_CODING_AGENT_DIR 指向临时目录,不写 ~/.pi