ARTICLE · 1092962
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()):
https://models.dev/api.json | tool_call === truestatus !== "deprecated" | |
https://openrouter.ai/api/v1/models | supported_parameterstools | |
https://ai-gateway.vercel.sh/v1/models | 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. packages/ai/src/providers/data/<provider-id>.json—— 每个 provider 一个 JSON,模型按 API 分组存放,是唯一的运行时数据源;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. packages/ai/src/models.generated.ts—— 聚合器,把全部<ID>_MODELS常量收进MODELS对象;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.tsbuiltinProviders() | builtinModels() 时,遍历注册表中没有该厂商,运行时内存里根本不存在这个 Provider 实例。 |
| 2. 运行时工厂与网络基址 | packages/ai/src/providers/<id>.tsdeepseek.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.tsdata/<id>.json | <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. 在 types.ts的KnownApi和ApiOptionsMap登记;2. 写对应的 .lazy.ts懒加载包装(保证 API 模块按需加载,不影响启动体积);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() 就能找到现成实现,无需任何代码。
openai-completions | 接自建服务的默认选择 | |
openai-responses | ||
openai-codex-responses | ||
azure-openai-responses | ||
anthropic-messages | ||
mistral-conversations | ||
bedrock-converse-stream | ||
google-generative-ai | ||
google-vertex | ||
pi-messages | { model, context, options } 到 <baseUrl>/messages,SSE 流式返回(api/pi-messages.ts 文件头注释原文) |
三点提示:
• 自建服务绝大多数兼容 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. 用户在 TUI 里对 radius 走 OAuth 登录( auth/oauth/load.ts的loadRadiusOAuth),登录后凭据里会带上网关配置;2. 网关的 GET /v1/config返回{ baseUrl, models: [{ id, name, reasoning, thinkingLevelMap, input, cost, contextWindow, maxTokens }] }(radius-config.ts的RadiusGatewayConfig,带运行时结构校验sanitizeRadiusGatewayConfig);3. getRadiusModelsFromConfig()把这些条目转成Model<"pi-messages">——统一走pi-messages协议(Anthropic 兼容的消息协议,见docs/release/02_anthropic_llm_api_protocol.md);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 则照情况二的路子再走一遍注册。
八、总结:怎么选路
npm run generate-models(上游目录有)或改 scripts/generate-models.ts 硬编码(上游没有) | ||||
src/api/ 模块) | ||||
~/.pi/agent/models.json(baseUrl / 新 provider / modelOverrides) | ||||
/v1/config 加条目(客户端配 oauth: "radius") | ||||
registerProvider(ProviderConfigInput) | ||||
generate-image-models + providers/images/ 注册 |
一句话记忆:目录数据(情况一)随包发布,用户配置(情况三)热重载,网关下发(情况四)自动刷新,代码级接入(情况二/五)按需动工厂和 API 层。 判断自己属于哪种情况,先问三个问题:这是官方内置目录吗?(是 → 一/二)服务端能统一管理吗?(能 → 四)协议是 Pi 已认识的 OpenAI/Anthropic 兼容吗?(是 → 三,否 → 五/二)。