夜雨聆风学习资料网

ARTICLE · 1092962

Pi 如何扩展模型 - Pi 源码分析 04

Pi 如何扩展模型 - Pi 源码分析 04

本文是「Pi 源码剖析」系列的第四篇。本文深入剖析 Pi 的模型管理,系统拆解 Model 与 Provider 的核心抽象、三层目录(出厂预装、用户配置、网关动态)的叠加机制,并针对维护者代码贡献、用户自配模型、企业网关下发与扩展插件等全场景,回答“往 Pi 里新增一个模型有哪几种情况、每种情况分别怎么加”。文中所有文件路径、命令、字段均来自 packages/ai 与 packages/coding-agent 的真实源码。

一、先建立心智模型:模型是什么、"目录"在哪几层

在 Pi 里,"加一个模型"从来不是改一行配置那么简单或那么重——它取决于你站在哪一层。先说清楚两个核心概念。

1. 一个 Model 对象 = 一份"模型使用说明书"

Pi 对任何厂商的模型都统一抽象成 Model 对象(packages/ai/src/types.ts)。它不关心你背后是 OpenAI、Anthropic 还是自己搭的 vLLM,只看这份说明书:

export interface Model<TApi extends Api> {    id: string;               // 模型 id,如 "deepseek-v4-pro"    name: string;             // 显示名    api: TApi;                // 走哪套协议:openai-completions / anthropic-messages / ...    provider: ProviderId;     // 属于哪个 Provider(厂商接线台)    baseUrl: string;          // API 地址    reasoning: boolean;       // 是否支持思考/推理    thinkingLevelMap?: ThinkingLevelMap;  // 把 Pi 的 off/low/medium/high 映射成厂商自己的档位值    input: ("text" | "image")[];          // 支持的输入模态    cost: ModelCost;          // $/百万 token 四档价格(input/output/cacheRead/cacheWrite,可带 tiers 阶梯)    contextWindow: number;    // 上下文窗口    maxTokens: number;        // 单次最大输出    samplingParams?: Record<string, unknown>; // 默认采样参数    headers?: Record<string, string>;         // 模型级固定请求头    compat?: ...;             // 协议兼容性开关(thinkingFormat、supportsStrictMode 等)}

compat 尤其关键:它是 Pi 处理"各家 OpenAI 兼容协议都不完全兼容"这个现实问题的开关面板。比如 DeepSeek 用 thinking: {type} 而不是 reasoning_effort,就靠 thinkingFormat: "deepseek" 来切换。

2. 一个 Provider = 一个"厂商接线台"

模型本身不负责发请求。真正干活的是 Provider(packages/ai/src/models.ts 的 Provider 接口),它是一张厂商接线台,负责四件事:

  • • 身份:id、name、baseUrl;
  • • 认证:auth(API Key 从环境变量读、OAuth 登录等);
  • • 目录:getModels() 同步返回当前已知模型列表;动态 provider 还可以有 refreshModels() 去远程刷新;
  • • 接线:stream()/streamSimple() 把统一请求转成该厂商协议的流式响应。

一个模型要能被调用,必须"挂在某个 Provider 名下"。所以"加模型"的本质是:给某个 Provider 的目录里增加一条 Model 说明书,并保证它背后的协议(api)有人能接。

3. 目录分三层:出厂地图、自定义导航点、联网路况

用一个类比建立全局画面:

  • • 内置生成目录(packages/ai,构建期)像出厂预装的地图。由 scripts/generate-models.ts 从 models.dev / OpenRouter / Vercel AI Gateway 抓取并生成,随 npm 包发布,所有人共享一份。
  • • 用户 models.json(~/.pi/agent/models.json,运行时)像用户自定义导航点。不用改代码、不用重新发布,热重载生效,还能覆盖出厂地图上的信息。
  • • Radius 网关动态目录(运行时,服务端下发)像联网实时路况。模型列表由网关 /v1/config 动态下发,服务端改了,客户端自动刷新。

三层的叠加顺序(packages/coding-agent/src/core/provider-composer.ts 的 composeModelProvider):内置目录 → models.json(baseUrl/compat/models 逐层合并)→ 扩展(Extension)替换 → modelOverrides 字段级覆盖(最顶层)。

下面按"改动从轻到重"讲每一种情况。先看一张总览图,把六种情况的入口和各自的落地动作一次看清:

这张图的核心是三条泳道 = 三种改动层级:改代码(维护者,情况一/二)、改配置(用户,情况三)、改服务端(网关运维,情况四),扩展开发者的情况五挂在第三条泳道下方,图像模型的情况六是第一条泳道在 generate-image-models 上的平行版本(见第七节)。

二、情况一:给已有的内置 Provider 增加模型(维护者,改 ai 包)

内置目录里已经注册了 40 个左右 Provider(packages/ai/src/providers/all.ts 的 builtinProviders()),比如 deepseek、anthropic、openrouter。给其中一个加模型,又分两种子情况。

子情况 A:上游目录会自动带上,直接重新生成

大多数内置 Provider 的模型是构建期自动抓取的,数据源有三处(packages/ai/scripts/generate-models.ts 的 generateModels()):

数据源
覆盖的 Provider
抓取条件
https://models.dev/api.json
anthropic、openai、google、bedrock、deepseek 之外的绝大多数(groq、cerebras、mistral、moonshotai、xiaomi、zai、qwen-token-plan……)
tool_call === true
(必须支持工具调用),status !== "deprecated"
https://openrouter.ai/api/v1/models
openrouter
supported_parameters
 含 tools
https://ai-gateway.vercel.sh/v1/models
vercel-ai-gateway
tags 含 tool-use

只要上游目录(比如 models.dev)已经收录了这个新模型,Pi 侧零代码改动,跑一条命令即可:

cd packages/ainpm run generate-models   # 等价于 node scripts/generate-models.ts --strict

--strict 模式下任何抓取失败都会直接报错退出,保证不会静默生成一份缺模型的目录。合并时按 (provider, model.id) 去重,models.dev 优先于 OpenRouter(注释里写明 "models.dev has priority")。

子情况 B:目录没有或元数据不对,硬编码补充

有些模型不会被上游目录正确收录,需要在 packages/ai/scripts/generate-models.ts 里手写条目。以源码里的 deepseekV4Models 为例(generate-models.ts 中 loadModelsDevData() 没有为 deepseek 写 models.dev 抓取块,所以 DeepSeek V4 系列必须手写),看增加一个模型到底要写哪些东西:

// packages/ai/scripts/generate-models.ts(第 2588 行起)const deepseekCompat: OpenAICompletionsCompat = {    requiresReasoningContentOnAssistantMessages: true,    thinkingFormat: "deepseek",};const deepseekV4Models: Model<"openai-completions">[] = [    {        id: "deepseek-v4-flash",        name: "DeepSeek V4 Flash",        api: "openai-completions",        baseUrl: "https://api.deepseek.com",        provider: "deepseek",        reasoning: true,        input: ["text"],        cost: { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },        contextWindow: 1000000,        maxTokens: 384000,        compat: deepseekCompat,    },    // deepseek-v4-flash-vision-exp:其余相同,仅 input 换成 ["text", "image"]    // deepseek-v4-pro:其余相同,仅 cost 换成 0.435 / 0.87 / 0.003625];allModels.push(...deepseekV4Models);

一个条目要填的字段,按"说明书"的职责分组就是:

  • • 身份:id、name;
  • • 接线:api(从内建协议里选一套,这里是 openai-completions)、provider(挂到哪个厂商名下)、baseUrl(请求地址);
  • • 能力:reasoning(是否支持思考)、input(输入模态);
  • • 计费:cost 四档(input / output / cacheRead / cacheWrite,单位 $/百万 token);
  • • 限额:contextWindow、maxTokens;
  • • 协议差异:compat。这里两条都是 DeepSeek 与标准 OpenAI 协议的不同——thinkingFormat: "deepseek" 表示思考档位用 thinking: {type} 传递而不是 reasoning_effort;requiresReasoningContentOnAssistantMessages: true 表示回放 assistant 消息时必须带上 reasoning_content 字段。

三条目共用的 compat 抽成 deepseekCompat 常量,避免重复。注意有两类字段不必写进条目:thinkingLevelMap 由统一补丁函数 applyThinkingLevelMetadata() 按 model.id.includes("deepseek-v4") 的规则在生成时自动补上;compat 里可自动探测的开关由 applyOpenAICompletionsCompatMetadata() 按 baseUrl/provider 推断。手写的条目只写"和默认不一样"的部分。

写完条目后在 generateModels() 里 allModels.push(...) 追加,跑 npm run generate-models 即可。合并进 JSON 时按 (provider, id) 去重、先到先得——上游目录的条目在前,所以若 models.dev 将来收录了同 id,硬编码条目会被静默忽略;这也是 missingOpenAiModels 追加前要显式 allModels.some() 查重的原因。

另一种更轻的改法:模型已被上游收录、只是个别字段不对(如 Copilot 的 1M 上下文、OpenAI 长上下文阶梯定价),不改条目,而是在 generateModels() 的 for (const candidate of allModels) 覆盖块或 applyThinkingLevelMetadata() 等统一补丁函数里按模型 id 加规则。

生成流水线:一条命令背后的产物链

generate-models 跑完之后,会原子地更新四个产物(过程在 generateModels() 的 staging 段)。整条流水线长这样:

  1. 1. packages/ai/src/providers/data/<provider-id>.json —— 每个 provider 一个 JSON,模型按 API 分组存放,是唯一的运行时数据源;
  2. 2. packages/ai/src/providers/<provider-id>.models.ts —— 每个 provider 一个薄壳:import values from "./data/<id>.json" with { type: "json" },再用 flattenModelCatalog()(packages/ai/src/model-catalog.ts)把 JSON 摊平成类型安全目录,该文件禁止手改(文件头注释明确要求走 npm run generate-models);
  3. 3. packages/ai/src/models.generated.ts —— 聚合器,把全部 <ID>_MODELS 常量收进 MODELS 对象;
  4. 4. packages/ai/src/providers/data/.manifest.json —— 生成戳:schemaVersion、generatedAt、structureHash(模型 id 与 API 分组的哈希)、files(每个 JSON 的 sha256)。

生成过程先把新产物写进临时 staging 目录并校验通过后才整体替换旧数据;.models.ts 和聚合器则先备份,失败时 restoreGeneratedCatalog() 回滚。配套校验命令:

npm run check:model-data   # 校验聚合器 / shards / JSON 数据 / manifest 四者一致

npm run build 会自动先执行 generate-models(package.json:"build": "npm run generate-models && npm run build:offline"),所以正常流程不需要记太多命令。另外还有两个变体:

  • • npm run hydrate-model-data(--data-only):不重新抓取,只按现有 provider id 重灌 data/*.json(离线重放用);
  • • npm run generate-model-catalog(--json-only):只导出公开 JSON 目录到 .artifacts/model-catalog。

三、情况二:新增一个全新的内置 Provider(维护者,改动最深)

当一个模型背后的厂商/网关还没被 Pi 内置,就不能只加模型,而必须新增 Provider。这是改动最重的一条路。

1. 用通俗类比理解两者的缺失差异

用第一节建立的心智模型来对比情况一与情况二:

  • • 情况一(已有 Provider 加模型):就像机场的值机柜台、安检通道、登机口(Provider)全部已建好并正常运营,现在航空公司只是新增了一个航班班次(Model)。只需要在屏幕时刻表上加一条信息,乘客就能正常走原有的柜台办票起飞。
  • • 情况二(全新 Provider):来了一家全新的航空公司,机场里根本没有它的值机柜台、不知道查什么证件(Auth)、不知道对接哪个登机口协议(API)、在机场总名录(builtinProviders)里查无此人。此时如果只把航班号印在时刻表上,旅客到了现场也根本无法值机登机(Model 只是无网络执行能力的静态说明书,缺少 Provider 运行时实例就无法发起调用)。

2. 相对已有 Provider,到底具体缺少了什么?

相对情况一,新增一个全新的内置 Provider 在代码库中缺少了以下 5 项关键基础设施:

缺少的基础设施
对应源码位置
缺失导致的问题
1. 启动注册与装配入口packages/ai/src/providers/all.ts
 的 builtinProviders()
Pi 启动初始化 builtinModels() 时,遍历注册表中没有该厂商,运行时内存里根本不存在这个 Provider 实例。
2. 运行时工厂与网络基址packages/ai/src/providers/<id>.ts
(如 deepseek.ts)
没有通过 createProvider 定义默认 baseUrl、厂商显示名 name 和实例工厂函数。
3. 认证与凭据解析逻辑(Auth)packages/ai/src/auth/helpers.ts
系统不知道该去哪个环境变量(如 FOO_API_KEY)读 key,不知道未配置时的提示文案,也无法校验鉴权状态。
4. 协议分发绑定(API Binding)packages/ai/src/api/
 的流式实现
缺少该厂商默认绑定的协议分发对象(如 openAICompletionsApi()),系统不知道该用哪套协议驱动该厂商的网络请求。
5. 构建期分片产物与抓取逻辑packages/ai/scripts/generate-models.ts
 与 data/<id>.json
生成流水线没有处理该 provider 的抓取或转换分支,缺少对应的静态分片文件 <id>.models.ts 和 data/<id>.json。

根本原因在于 Pi 的架构设计:Model 只是纯静态的元数据说明书,Provider 才是真正持有凭据并执行 stream() 网络调用的运行时对象。

3. 落地六步法(按依赖顺序补齐)

把上述缺失的基础设施完整补齐,按依赖顺序分为以下六步:

第一步:在 packages/ai/src/types.ts 的 KnownProvider 联合类型里加 id。

ProviderId 本身是开放类型(KnownProvider | string),这步只是让下游获得字面量类型提示,不是硬门槛。

第二步:实现 packages/ai/src/providers/<id>.ts 工厂函数。

以 deepseek.ts 为最小模板(全文 15 行):

import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";import { envApiKeyAuth } from "../auth/helpers.ts";import { createProvider, type Provider } from "../models.ts";import { DEEPSEEK_MODELS } from "./deepseek.models.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(),    });}

要点:

  • • models 直接复用第一步生成目录会产出的 <ID>_MODELS(./<id>.models.ts 由 generate-models 自动生成,不用手写,先引用着,跑完生成就有了);
  • • api 是一个 ProviderStreams 实现。若一个 provider 混用多套协议(例如同一网关既有 Anthropic 又有 OpenAI 模型),可以传 Partial<Record<TApi, ProviderStreams>> 按 model.api 分派(createProvider 内部 apiFor() 处理);
  • • auth 至少二选一:apiKey(envApiKeyAuth() 从环境变量解析,models.ts 注释强调"即使只有 ambient 凭据的 provider 也要有 apiKey auth,其 resolve() 报告是否已配置")或 oauth。

第三步:在 scripts/generate-models.ts 里增加这个 provider 的抓取/加工逻辑。

看两个现成例子:processZaiModels() 把一个 models.dev 条目展开成国内/国际两个变体;xiaomiVariants 把同一家厂商按 billing / Token Plan cn / ams / sgp 拆成四个 provider、四个 baseUrl。新 provider 的模型要么从这里产出,要么像情况一的子情况 B 那样硬编码数组后 allModels.push(...)。

第四步:在 packages/ai/src/providers/all.ts 的 builtinProviders() 里注册工厂,并加 import。

第五步:npm run generate-models + npm run check:model-data。

第六步(仅当协议全新时需要):实现新的 API 模块。 如果新厂商的协议不是现成的 OpenAI-completions / Anthropic-messages 等 10 种内建协议,就要在 packages/ai/src/api/ 下写一个新模块,满足 ProviderStreams 契约(stream / streamSimple 两个入口,可选 fetchDeferred / cancelDeferred),再:

  1. 1. 在 types.ts 的 KnownApi 和 ApiOptionsMap 登记;
  2. 2. 写对应的 .lazy.ts 懒加载包装(保证 API 模块按需加载,不影响启动体积);
  3. 3. 在 compat.ts 的 BUILTIN_APIS 注册,让"按 api 字符串派发"的全局注册表认识它。

小结:情况二 = 新工厂 + 新抓取逻辑 + 注册,协议没变的话并不需要碰 API 层;只有出现全新协议时才动 src/api/。

四、情况三:用户侧 models.json(用户/开发者,零代码改动)

这是绝大多数日常需求走的路:接一个代理网关、接一个开源模型服务、把官方模型换到内网地址。不用改 Pi 代码。

文件位置与加载

默认路径 ~/.pi/agent/models.json(getAgentDir() 拼接,可用 PI_AGENT_DIR 环境变量重定向)。加载器 packages/coding-agent/src/core/model-config.ts 提供三个贴心特性:

  • • 支持 JSON 注释(stripJsonComments);
  • • TypeBox 严格 schema 校验,字段错了会给出"文件 + 具体路径 + 错误原因"的报错而不是静默忽略;
  • • 热重载:文件变化后 registry 异步重载(回归测试 6999-models-json-hot-reload 覆盖了这条路径)。

Schema 总览

{    "providers": {        "<provider-id>": {            "name": "...",          // 显示名            "baseUrl": "...",       // 覆盖/指定 API 地址            "apiKey": "...",        // 静态 key / "$ENV_VAR" / "${ENV_VAR}" / "!命令"            "api": "...",           // provider 级默认协议            "oauth": "radius",      // 唯一取值:走 Radius 网关 OAuth(见情况四)            "headers": {},          // provider 级请求头(值同样支持 $VAR 引用)            "compat": {},           // provider 级兼容开关            "authHeader":true,     // 把解析出的 key 放进 Authorization: Bearer 头(有的网关要求)            "models": [ { "id": "...", "baseUrl": "...", ... } ],   // 追加/替换模型            "modelOverrides": { "<model-id>": { "contextWindow": ..., ... } }  // 字段级覆盖内置模型        }    }}

apiKey 的三种写法(packages/coding-agent/src/core/resolve-config-value.ts):

  • • "sk-xxx" 字面量;
  • • "$MY_KEY" / "${MY_KEY}" 从环境变量读($$ 转义字面 $);
  • • "!gpg -d key.gpg" 执行 shell 命令取 stdout(带缓存)。

子情况 A:给已有 Provider 换 baseUrl 或追加模型

比如把官方 DeepSeek 流量导到自己的代理:

{    "providers": {        "deepseek": {            "baseUrl": "https://my-proxy.example.com",            "headers": { "X-Gateway-Token": "${GATEWAY_TOKEN}" }        }    }}

applyModelsJson() 会把 provider 级 baseUrl/compat合并到内置目录的每一条模型上(compat 是深合并,openRouterRouting 等嵌套对象逐键合并),内置模型一个都不丢。

要追加一条内置目录没有的模型(比如代理网关新接的模型),用 models 数组。注意 modelFromJson() 的取值优先级:定义级 → provider 级 → 内置同类模型默认值。api 和 baseUrl 必须有来源(provider 级或模型级),否则直接报错 "baseUrl" is required when defining custom models;cost 缺省时按全 0 计费。给已有 provider 追加时,同名 id 会替换内置条目。

{    "providers": {        "deepseek": {            "baseUrl": "https://my-proxy.example.com",            "models": [                {                    "id": "my-finetune-1",                    "name": "My Fine-tuned Model",                    "reasoning":true,                    "input": ["text"],                    "cost": { "input": 0.5, "output": 1.5, "cacheRead": 0.05, "cacheWrite": 0 },                    "contextWindow": 128000,                    "maxTokens": 16384                }            ]        }    }}

这条新模型不写 api 和 baseUrl 也能用:它继承 provider 级 baseUrl,api 则从内置 deepseek 目录里同 id(没有)→ 同 api(openai-completions)→ 第一条模型的默认值推导出来。

子情况 B:定义一个全新 Provider(接任意 OpenAI 兼容服务)

接自己搭的 vLLM / llama.cpp / 任意兼容端点,只要协议是内建 API 之一,不写一行 Pi 代码:

{    "providers": {        "my-vllm": {            "name": "My vLLM Server",            "baseUrl": "http://127.0.0.1:8000/v1",            "api": "openai-completions",            "models": [                {                    "id": "qwen3-coder-30b",                    "name": "Qwen3 Coder 30B",                    "api": "openai-completions",                    "reasoning":true,                    "thinkingLevelMap": { "off": "off", "high": "high" },                    "input": ["text"],                    "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },                    "contextWindow": 32768,                    "maxTokens": 8192,                    "compat": { "thinkingFormat": "qwen" }                }            ]        }    }}

新 provider 可以完全不写 apiKey——本地服务不需要认证;也可以配 apiKey: "$VLLM_KEY"。认证语义由 composeApiKeyAuth() 组合:内置 provider 的 env 解析逻辑会被继承,apiKey 值会作为新的一层解析来源。请求派发时,model.api 是内建 API 名(如 openai-completions)就查全局 API 注册表(compat.ts 的 getApiProvider())找到现成的协议实现——所以只要协议被 Pi 认识,新 provider 只是个配置问题。

内建 API 清单:什么样的协议才算"被 Pi 认识"

"内建 API"指 packages/ai/src/types.ts 里 KnownApi 联合类型列出的 10 种协议。每种都在全局注册表 compat.ts 的 BUILTIN_APIS 里挂了一套 ProviderStreams 流式实现(代码在 packages/ai/src/api/<name>.ts),model.api 写这 10 个名字之一,派发时 getApiProvider() 就能找到现成实现,无需任何代码。

api 值
协议
典型用途
openai-completions
OpenAI Chat Completions 协议
接自建服务的默认选择
:vLLM、llama.cpp、Ollama 等 OpenAI 兼容端点都用它;DeepSeek、Moonshot、Kimi 等国内厂商也是
openai-responses
OpenAI Responses API
OpenAI 官方新协议
openai-codex-responses
OpenAI Codex 的 Responses API
Codex 部署
azure-openai-responses
Azure OpenAI 的 Responses API
Azure 上的 OpenAI 模型
anthropic-messages
Anthropic Messages API
Claude 及 Anthropic 兼容网关
mistral-conversations
Mistral Conversations 协议
Mistral 官方 API
bedrock-converse-stream
AWS Bedrock Converse API
AWS Bedrock 上的模型
google-generative-ai
Google Gemini API
Gemini 官方 API
google-vertex
Google Vertex AI
GCP Vertex 上的 Gemini
pi-messages
Pi 自有消息协议:单个 POST { model, context, options } 到 <baseUrl>/messages,SSE 流式返回(api/pi-messages.ts 文件头注释原文)
Radius 网关;任何实现了该简单契约的自建后端

三点提示:

  • • 自建服务绝大多数兼容 openai-completions,这也是上面 vLLM 示例选它的原因。个别厂商行为有偏差时不用换协议,用 compat 开关调(如 Qwen 系写 "thinkingFormat": "qwen")。
  • • Api 类型其实是开放的(KnownApi | (string & {})),model.api 写任意字符串 TS 都不报错,但只有内建名字或扩展(情况五)注册过实现的名字才能真的发出请求——没有 stream 实现,运行时无法调用。
  • • pi-messages 虽叫"Pi 自有",但契约很简单,自建网关也可以实现它,相当于 Pi 的"原生协议"(api/pi-messages.ts 注释原文:"any backend implementing it can be used, e.g. via a models.json custom provider with "api": "pi-messages"")。

子情况 C:modelOverrides 只微调元数据

只想把某条内置模型的上下文窗口/价格改对,不想动其它任何东西:

{    "providers": {        "openrouter": {            "modelOverrides": {                "anthropic/claude-opus-5": {                    "contextWindow": 1000000,                    "maxTokens": 64000,                    "headers": { "HTTP-Referer": "https://example.com" },                    "compat": { "thinkingFormat": "openrouter" }                }            }        }    }}

modelOverrides 是最顶层用户配置(provider-composer.ts 注释原文:"models.json modelOverrides are the topmost user-config layer"),在自定义模型 upsert、扩展替换之后才应用,且是字段级合并(thinkingLevelMap、cost 的四个费率、samplingParams 逐字段合并,不整体覆盖)。

五、情况四:Radius 网关动态模型(服务端下发,客户端自动刷新)

如果你的模型托管在一个自建网关后面,且希望服务端随时上下架模型、客户端完全不用改,就走 Radius 通道。这是 Pi 内置 radius Provider(packages/ai/src/providers/radius.ts)的能力。

客户端配置(models.json):

{    "providers": {        "radius": {            "baseUrl": "https://my-gateway.example.com",            "oauth": "radius"        }    }}

工作链路:

  1. 1. 用户在 TUI 里对 radius 走 OAuth 登录(auth/oauth/load.ts 的 loadRadiusOAuth),登录后凭据里会带上网关配置;
  2. 2. 网关的 GET /v1/config 返回 { baseUrl, models: [{ id, name, reasoning, thinkingLevelMap, input, cost, contextWindow, maxTokens }] }(radius-config.ts 的 RadiusGatewayConfig,带运行时结构校验 sanitizeRadiusGatewayConfig);
  3. 3. getRadiusModelsFromConfig() 把这些条目转成 Model<"pi-messages">——统一走 pi-messages 协议(Anthropic 兼容的消息协议,见 docs/release/02_anthropic_llm_api_protocol.md);
  4. 4. 模型列表通过 refreshModels() 动态刷新,并持久化到 ModelsStore(models-store.ts,带 ETag/Last-Modified),断网时先恢复缓存(ModelsImpl.refresh() 的两阶段:先 restore stored、再允许网络时拉新)。

服务端加模型的步骤:在网关的 /v1/config 返回里增加一条模型记录——客户端下次刷新(或重启)自动拿到,Pi 与 models.json 都不需要改动。注意 RadiusGatewayModel 的必需字段:id、name、reasoning、input、cost、contextWindow、maxTokens,缺一个整条会被过滤掉。

六、情况五(补充):SDK 扩展注册 Provider(插件/扩展开发者)

给 Pi 写扩展时,可以在代码里通过 ProviderConfigInput 注册自定义 provider(provider-composer.ts),能力比 models.json 更强:

  • • apiKey / oauth 自定义登录流程;
  • • streamSimple + api:注册一个全新的协议实现(models.json 做不到,它只能挑内建 API);
  • • refreshModels:运行时动态拉模型;
  • • modifyModels(models, credentials):OAuth 场景下按凭据改写模型列表(例如订阅级别不同可见模型不同)。

扩展层在合成顺序中位于 models.json 之后、modelOverrides 之前(composeModelProvider() 的 getModels() 链)。

七、情况六(补充):图像生成模型

上面全是文本 LLM。图像模型是另一条独立目录(ImagesModel,packages/ai/src/image-models.ts):

  • • 生成器 scripts/generate-image-models.ts --strict 从 OpenRouter /models 抓取输出模态含 image 的模型,产出 packages/ai/src/image-models.generated.ts;
  • • Provider 工厂放在 providers/images/,在 providers/all.ts 的 builtinImagesProviders() 注册(当前只有 openrouter-images);
  • • 新增图像模型的日常操作就是重跑 npm run generate-image-models;新增图像 Provider 则照情况二的路子再走一遍注册。

八、总结:怎么选路

我的身份
我要做的事
走哪条路
改动面
生效方式
Pi 维护者
给已有内置厂商加模型
情况一:npm run generate-models(上游目录有)或改 scripts/generate-models.ts 硬编码(上游没有)
ai 包数据
随 npm 包发布
Pi 维护者
接入一家全新厂商/网关
情况二:工厂 + 抓取逻辑 + 注册(协议全新还要写 src/api/ 模块)
ai 包代码
随 npm 包发布
终端用户
换代理地址 / 接 OpenAI 兼容服务 / 微调模型参数
情况三:~/.pi/agent/models.json(baseUrl / 新 provider / modelOverrides)
零代码
热重载
网关运维
服务端统一上下架模型
情况四:网关 /v1/config 加条目(客户端配 oauth: "radius")
服务端配置
客户端动态刷新
扩展开发者
插件自带模型/协议
情况五:registerProvider(ProviderConfigInput)
扩展代码
扩展加载
图像模型
接图像生成服务
情况六:generate-image-models + providers/images/ 注册
ai 包数据
随 npm 包发布

一句话记忆:目录数据(情况一)随包发布,用户配置(情况三)热重载,网关下发(情况四)自动刷新,代码级接入(情况二/五)按需动工厂和 API 层。 判断自己属于哪种情况,先问三个问题:这是官方内置目录吗?(是 → 一/二)服务端能统一管理吗?(能 → 四)协议是 Pi 已认识的 OpenAI/Anthropic 兼容吗?(是 → 三,否 → 五/二)。

相关学习资料