12. 提供商配置架构
翻译说明: 本页是与 英文源规格 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。
1. 存储位置
由 Rust 主机 DB/settings 存储拥有。
表(04-data-storage §4.3–4.4、§4.11 中的规范 DDL):
providersmodels(单个目录表;source: bundled | discovered | user替换旧的provider_models/model_catalog_cache拆分)secrets_meta(无原始秘密值)- 最近模型的 MRU 位于
kv(ns='cache')中,而不是表中
2. 提供商记录 JSON 架构(逻辑)
{
"$id": "pi-desktop.provider.v1",
"type": "object",
"required": ["id", "name", "vendorKey", "type", "protocol", "enabled", "authKind"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1 },
"vendorKey": { "type": "string", "minLength": 1 },
"type": { "enum": ["native", "openai_compatible", "custom"] },
"protocol": {
"enum": ["openai", "anthropic", "google", "openai_compatible", "bedrock", "custom_http"]
},
"enabled": { "type": "boolean" },
"baseUrl": { "type": "string" },
"authKind": {
"enum": [
"api_key",
"api_key_and_base_url",
"bearer",
"azure_api_key",
"aws_sdk_default",
"custom_headers",
"oauth",
"none"
]
},
"secretRef": { "type": "string" },
"headers": {
"type": "object",
"additionalProperties": { "type": "string" },
"maxProperties": 32
},
"userAgent": { "type": "string", "maxLength": 256, "description": "legacy; migrates into headers.User-Agent" },
"apiStyle": {
"enum": [
"chat_completions",
"responses",
"anthropic_messages",
"google_generative_ai",
"openai_codex_responses",
"pi_messages",
"auto"
]
},
"compatibility": {
"type": "object",
"properties": {
"supportsTools": { "type": "boolean" },
"supportsVision": { "type": "boolean" },
"supportsStreaming": { "type": "boolean" },
"supportsReasoning": { "type": "boolean" },
"supportedThinkingLevels": {
"type": "array",
"items": {
"enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"]
},
"uniqueItems": true
}
}
},
"defaultModelId": { "type": "string" },
"models": {
"type": "array",
"items": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"alias": { "type": "string", "maxLength": 60 },
"contextWindow": { "type": "integer", "minimum": 0 },
"contextWindowSource": { "enum": ["catalog", "user"] },
"maxTokens": { "type": "integer", "minimum": 0 },
"thinkingLevels": {
"type": "array",
"items": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"] },
"uniqueItems": true
},
"defaultThinkingLevel": {
"type": ["string", "null"],
"enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max", "omit", null]
},
"supportsImages": { "type": ["boolean", "null"] },
"supportsDocuments": { "type": ["boolean", "null"] },
"availableForSubagents": { "type": "boolean", "default": false }
}
}
},
"ownerPluginId": { "type": ["string", "null"] },
"createdAt": { "type": "string" },
"updatedAt": { "type": "string" }
}
}插件通过 contributes.providers 声明的行带有 ownerPluginId(其行 id 为 plugin:<pluginId>:<declaredId>),对模型解析、发现、连接测试和会话绑定而言 它是一行普通 provider。用户路径对它只读:providers.update 与 providers.delete 会以 PROVIDER_OWNED_BY_PLUGIN 错误拒绝。该声明在每次插件 加载时从插件 manifest 重新读取,并对自己拥有的字段具有权威性,而已存储的 headers、OAuth 账户标签以及用户填入的凭据都会保留。停用插件保留该行并将其 关闭;卸载插件或移除该声明会删除该行及其两个凭据引用(ADR 0259, 07-plugins/02-plugin-manifest-schema.md §5.4)。
models[].alias 是可选展示标签(ADR 0192)。models[].id 仍是发给提供商的 身份,别名从不用于提供商或模型解析。host-core 会修剪别名、丢弃空白值, 并在超过 60 个 Unicode 字符时以 MODEL_ALIAS_TOO_LONG 拒绝。
models[].contextWindow 与 models[].maxTokens 在线为可选。缺失的键、或显式的 0, 都不是按模型的选择:host 把它读作 0 并补上通用默认值(128,000 / 8,192)——这与上面 legacy 绑定被物化时用的是同一个值,也与未声明限额的插件 manifest 已经产生的值相同。 存储数组与 manifest 由此对“没有限额的模型”取得一致(D610)。
存储的 models 数组逐条解码。不再符合 schema 的条目会被跳过,并在宿主日志里带上 提供商 id、条目下标与原因上报,而不是丢弃整个数组。读取仍可用,但会标记为降级:非法 JSON、根节点非对象、models 非数组,或任一条目不可读,都会被上报。缺失的 models 键和空数组仍是合法的 legacy 状态;所有条目都不可读的数组仍回退到 legacy 绑定并上报。 为防止设置页的部分视图覆盖并丢失存储数据,当存储值降级时,providers.update 会以 MODEL_BINDINGS_DEGRADED 拒绝显式替换模型数组;不涉及模型数组的提供商字段仍可更新。
models[].contextWindowSource 记录存储的 contextWindow 来自哪里:catalog 表示 models.dev 快照,之后的目录修正可以替换它;user 表示用户在设置中手改的值,永不被 替换。该字段可选,因此早于该标记写出的配置仍可读,旧客户端会忽略它。host-core 只 保留这两个取值、丢弃其它值,避免出现第三种无人识别的状态。解析规则见 13-model-catalog-and-selection §9.1。
compatibility.supportsReasoning 和 compatibility.supportedThinkingLevels 对于存储的记录保持可读状态 旧客户端兼容性,但 Electron main 在运行时忽略它们 模型分辨率。公共提供商形状是从精确的 pi-ai 中丰富的 代替模型记录。未知的自由形式模型暴露了 supportsReasoning=false 和 supportedThinkingLevels=["off"]。原始秘密和内部兼容性 JSON 保持隐藏状态。 手输的自定义模型 id 会在写入绑定前先与该快照匹配(providers.lookupModel,§9): 即使该 id 不在任何已发现的列表中,已发布的记录也会提供绑定的上下文窗口、最大输出 token 与思考等级;未发布的 id 仍沿用通用种子值。
authKind: "oauth" 标记厂商账户行(ADR 0095、D237):其凭据是保存在 secret:provider:<id>:oauth 下的 OAuth 授权,而不是粘贴的密钥,因此该行 不为它保存 secretRef,并以空密钥启动。两种账户专用 apiStyle 是厂商账户专用的 线路 API —— openai_codex_responses(Codex 会话封装)与 pi_messages (radius 网关)—— 自定义提供商对话框不提供它们,因为二者都无法配合手输的 base URL 与粘贴的密钥工作。新建自定义服务只提供 Chat Completions、Responses、Anthropic Messages 和 Google Generative AI;OpenCode Go 仍通过具名服务配置。 历史非 OAuth 行若保存了上述账户专用格式,编辑时会显示禁选的当前格式和 说明,并允许原样保存。仅打开编辑器不会根据匹配的端点预设修改协议、名称 或 URL;选择其他格式才是明确变更。复制此类行时保留草稿中的原格式供 确认,但在主动选择支持的格式前禁止保存和模型发现,并显示原因。 不迁移已有认证类型或凭据。厂商行的样式不由厂商固定:GitHub Copilot 同时 提供 Anthropic、Chat Completions 与 Responses 模型,因此样式跟随所选模型, 并在每次切换模型时重写。config_json.oauth.accountLabel 保存已登录账户的 非敏感展示标签。
将提供商配置复制为独立草稿
模型配置页在普通非 OAuth 提供商行提供复制操作,通常打开新的自定义 服务草稿。草稿中的 API 格式可以修改,便于为同一站点的其他协议复用地址 和模型绑定。OpenCode Go 是例外:副本保留命名服务与固定的 opencode_go 格式;先切换为自定义服务,才能选择普通 API 格式。OAuth 账户行不提供此操作。
草稿按明确的字段白名单构建:来源名称、baseUrl、apiStyle 以及 models 中已声明的绑定字段。模型对象及嵌套的 thinkingLevels 数组独立复制,编辑 草稿不能修改来源对象。建议名称可带复制标记,用户可以在保存前修改。 不复制来源 id、凭据或凭据引用、hasSecret 状态、OAuth 元数据、自定义 headers 或未知字段。允许使用的自定义请求头也可能包含 token,因此全部 省略。对话框说明:需要认证信息或自定义请求头时,应为新配置重新填写。
Base URL 格式无效、不是 HTTP(S),或包含用户名/密码、查询参数、片段时, 草稿中的地址留空,避免复制历史 URL 中的凭据。
草稿使用正常的新提供商发现路径,不得把来源 provider id 传给模型发现或 连接测试来解析来源保存的密钥。需要认证的发现请求只使用为新草稿明确 填写的凭据。复制操作不读取或复制秘密存储中的值。
取消草稿不持久化提供商或配置。保存走现有 createProvider / providers.create 流程,分配新的提供商身份;填写新密钥时创建该提供商 自己的凭据引用。来源提供商与全局默认提供商、模型选择保持不变。新 提供商自己的默认模型仍按现有创建规则取首个所选模型。复制操作不新增 IPC 方法、存储 schema 或权限边界。
3. 内置供应商预设
仅预设预填表单默认值;他们不是一个封闭的世界。
| 供应商密钥 | 默认协议 | 授权类型 | 需要基本网址 |
|---|---|---|---|
| 开放性 | 开放性 | api_key | 不 |
| 人择的 | 人择的 | api_key | 不 |
| 谷歌 | 谷歌 | api_key | 不 |
| 开放路由器 | openai_兼容 | api_key_and_base_url | 是的 |
| 深度搜索 | openai_兼容 | api_key_and_base_url | 是的 |
| 格罗克 | openai_兼容 | api_key_and_base_url | 是的 |
| 在一起 | openai_兼容 | api_key_and_base_url | 是的 |
| 烟花 | openai_兼容 | api_key_and_base_url | 是的 |
| 米斯塔拉尔 | openai_兼容或本机 | api_key | 可选的 |
| 赛 | openai_兼容 | api_key_and_base_url | 是的 |
| azure_openai | openai_兼容 | azure_api_key | 是的 |
| 基岩 | 基岩 | aws_sdk_默认 | 不 |
| 奥拉马 | openai_兼容 | 无 | 是的 |
| 工作室 | openai_兼容 | 无 | 是的 |
| 定制 | openai_兼容 | api_key_and_base_url | 是的 |
固定 API 风格预设
| apiStyle | 提供商类型 | authKind | 名称 | baseUrl |
|---|---|---|---|---|
opencode_go | openai_compatible | api_key_and_base_url | OpenCode Go | https://opencode.ai/zen/go/v1 |
OpenCode Go(以及任何 opencode.ai 主机)的 LLM 请求必须带稳定的 x-opencode-session。agent-runtime 在会话、子代理、提示增强与插件 one-shot 上发送该头,并附带 x-opencode-client: pi-desktop 与 User-Agent: pi-desktop/<APP_VERSION>。行上可选的 headers 会覆盖这些默认值;留空则保持适配器默认。
每行(AI 服务或 OAuth 账户)可在高级选项中用键值行编辑自定义请求头。空映射保持 pi-ai / claude-cli / OpenCode 默认。fetch 包装器是最后写入者,因此 Codex 与 Anthropic SDK 无法覆盖。禁止 Authorization / Host / Content-Type 等保留头。遗留的 userAgent 读取时迁入 headers["User-Agent"]。首次 OAuth 登录不收集请求头,登录后再编辑。覆盖 Anthropic OAuth 的 claude-cli/… 可能导致 Claude Pro/Max 拒绝请求。
命名端点预设
这些行由添加提供商对话框的服务下拉框创建。命名服务的常见路径是服务 + API 密钥;自定义端点在常见路径上并排显示 API 密钥与接口格式。vendorKey 使用 models.dev 提供商键。
国际:OpenAI、Anthropic、Google Gemini、OpenRouter、Groq、xAI、Mistral、 Together、Fireworks、OpenCode Go、Z.AI。
国内:DeepSeek、通义千问、月之暗面、智谱 / Coding Plan、硅基流动、火山方舟、 MiniMax(anthropic_messages,https://api.minimaxi.com/anthropic/v1)、 MiniMax (OpenAI)(chat_completions,https://api.minimaxi.com/v1,别名 minimax-openai / minimax-compatible)、Kimi 编程。
智谱 / Z.AI 的 Completions 请求仍使用 thinkingFormat: "zai" 与 zaiToolStream: true。DeepSeek 系 Completions 在 vendor key、URL、模型 ID 或 目录 family 能识别为 DeepSeek 时设置 requiresReasoningContentOnAssistantMessages: true,不改 thinkingFormat。
厂商账户预设
这些行由登录创建(设置 → 模型配置 → 厂商账户),而不是由自定义提供商 对话框创建。列表在运行时由 models.getProviders().filter(p => p.auth.oauth) 派生,因此它跟随 pi-ai 而不是本表;baseUrl、apiStyle 与 defaultModelId 在登录后由账户自己的目录填入。
| vendorKey | 订阅 | 典型 apiStyle | 登录形态 |
|---|---|---|---|
| anthropic | Claude Pro/Max | anthropic_messages | PKCE + 本地回调 |
| openai-codex | ChatGPT Plus/Pro | openai_codex_responses | PKCE + 本地回调,或手动贴码 |
| github-copilot | Copilot | 随模型而变 | 设备码 |
| openrouter | 账户余额 | chat_completions | PKCE + 本地回调 |
| kimi-coding | Kimi | chat_completions(仅 headers 认证) | 设备码 |
| xai | xAI | chat_completions | 设备码 |
| radius | Radius | pi_messages | PKCE + 本地回调 |
4. 模型目录缓存记录
type ModelCatalogCacheRecord = {
providerId?: string // empty for global bundled
modelId: string
displayName: string
vendorKey: string
capabilities: string[]
contextWindow?: number
source: "bundled" | "discovered" | "user"
updatedAt: string
raw?: unknown
}上下文窗口解析与 sidecar 保持一致:若 models.dev 已发布正数 limit.context,它会替换旧 binding 中的 128k 通用种子;用户在模型 Advanced 控件中设置的非默认值仍优先。未知模型继续使用 128k 的保守后备。
Copilot OAuth 行还会保留固定 pin 的 pi-ai 传输模型中的静态 IDE 身份标头 (Editor-Version、Editor-Plugin-Version 与 Copilot-Integration-Id), 即使运行时模型使用本地行 id 进行账户隔离。Agent 运行时会按每次调用提供 Copilot 的上下文相关请求标头;已保存的同名自定义 header 会覆盖默认值。
5. IPC / 主机方法(提供商域)
providers.listproviders.reorderproviders.getproviders.createproviders.updateproviders.deleteproviders.testConnectionproviders.listModelsproviders.lookupModelproviders.cacheModels(内部 Electron-main 到主机持久桥)providers.refreshModelsproviders.upsertUserModelproviders.deleteUserModel
6. 安全限制
- list/get 提供商 API 从未返回原始机密
- 如果可以使用密钥存储,
headers不得存储Authorization: Bearer <secret>3.导出设置默认排除机密
7. 迁移
- 通过
PRAGMA user_version的架构版本(04-数据存储§7) - 提供商记录累加进化;每个提供商的扩展字段登陆
config_json - 未知的未来协议值不应使旧的应用程序版本崩溃(ignore/disable,带有警告)
8. SQL(Rust 拥有的 SQLite)
规范的 DDL 位于 04-data-storage (D086) 中。提供商域表摘要:
-- providers: id/name/vendor_key/type/protocol/api_style/auth_kind/base_url/
-- enabled/secret_ref/default_model_id + config_json (headers,
-- compatibility, future knobs), INTEGER ms timestamps
-- models: PK(provider_id, model_id), display_name, source
-- (bundled|discovered|user), capabilities_json, context_window,
-- max_output_tokens, deprecated — refresh upserts never overwrite
-- source='user' rows
-- secrets_meta: secret_ref PK, owner_kind/owner_id, kind, backend原始秘密材料不存储在这些表中。
9. 宿主方法合约 (v1)
providers.list
- 在:
{ includeDisabled?: boolean } - 输出:
{ providers: ProviderPublic[] } ProviderPublic排除原始秘密;包括hasSecret: boolean(任一种凭据 存在即为真)、hasOauth: boolean、非敏感的oauthAccountLabel?: string与可选的headers?: Record<string, string>
providers.reorder
- in:
{ id: string, targetId: string, placement: "before" | "after" } - out:
{ ok: true } - Atomically move the source relative to the target in the current host list. A missing source/target or invalid placement returns
INVALID_PARAMSwithout writing. Moving to the current position is a successful no-op. - Persist ordered provider IDs in
kvatproviders.order.providers.listapplies that order before returning rows; absent metadata preserves creation order. New providers follow saved rows in creation order, deleted IDs are ignored, and disabled rows keep their relative position when filtered out. - This is a display preference, including for plugin-owned rows. Provider configuration, credentials, enabled state, timestamps and the default model remain unchanged. Plugin configuration write restrictions still apply.
- Uses the existing
kvextension boundary; no database migration or protocol version bump. Older applications ignore this metadata.
providers.create / providers.update
- 在:提供商字段 + 可选的
secretValue+ 可选的oauthAccountLabel(合并进config_json.oauth,传空字符串即清除)+ 可选的headers(合并进config_json.headers,传{}即清除);遗留userAgent读取时迁入headers["User-Agent"];旧客户端仍可能发送supportsReasoning/supportedThinkingLevels - 行为:保留配置;如果存在secretValue,则写入密钥存储并设置
secretRef;传统思维领域可能仍保留在config_json.compatibility但不影响运行时分辨率 - 输出:
ProviderPublic
providers.delete
- 在:
{ id, deleteSecret?: boolean }默认deleteSecret=true - 行为:同时清除两个凭据引用(
:api_key与:oauth)及其元数据记录, 因此重新创建的提供商绝不会继承他人的刷新令牌 - 输出:
{ ok: true }
providers.testConnection
- 在:
{ id, modelId?: string } - 输出:
{ ok: boolean, latencyMs?: number, error?: AppError, sampleModelId?: string } authKind: "oauth"行通过解析厂商认证(必要时刷新令牌)来自证,而不是用 它并不持有的密钥去访问网络
providers.listModels
- 渲染器 IPC 位于:
{ providerId, source?: "cache"|"refresh" };cache返回没有提供商网络访问权限的持久目录,而refresh在 Electron main 中运行发现 - 将 RPC 托管在:
{ providerId?: string }中;只读取 Rust 拥有的models表 - 对
authKind: "oauth"行,Electron 主进程读取已认证的目录 (models.getAvailable,它已应用厂商自己的filterModels,因此 Copilot 账户列出的是其订阅包含的模型),而不是调用/models;返回的每个模型都 带着其线路 API 所隐含的 apiStyle。openai-codex这类静态厂商使用已固定 的 pi-ai 目录(0.86.1 包含gpt-6-astra);models.dev 不会发明这些 ID。 - 输出:
{ models: ModelCatalogItem[] };每个模型都带有 pi-resolvedreasoning功能和supportedThinkingLevels。缓存的功能标签 旧提供程序字段无法覆盖 pi 模型记录。
providers.lookupModel
- 渲染器 IPC 入参:
{ modelId, baseUrl?, providerId?, vendorKey? } - 输出:
{ info: ModelInfo | null } - 只读取本地 models.dev 快照:先
ensureLoaded再findModel,不访问提供商网络, 也不调用主机 RPC。vendorKey与baseUrl仅用于在重复 id 之间消歧归属的发布提供 商;providerId会回显在返回记录上供设置界面使用。 - 需要它是因为
providers.listModels只描述已保存或已探测提供商的目录:手输的自定义 id 在提供商保存前没有别的通道取得其已发布限额。 - 命中时按拾取模型的口径(
bindingFromModelInfo)为这条绑定播种:已发布的上下文窗口、 最大输出 token 与思考等级,并标记contextWindowSource: "catalog";存储的 id 仍是 用户输入的那个(bindingForCustomModelInfo)。未命中(null)保持今天的行为:按 通用 128,000 / 8,192 与空思考等级播种(bindingForCustomModel)。行先落下再原地升级, 因此查询慢、失败或未发布时仍然只留一行可用记录,且不会覆盖期间发生的编辑或删除。
providers.cacheModels(内部主机 RPC)
- 在:
{ providerId, models: DiscoveredModelInput[] } - 行为:以事务方式将成功的实时发现更新到
models中source='discovered';永远不会覆盖source='user'行并且永远不会删除 失败或部分刷新时先前的缓存行 - 输出:
{ cached: number, models: ModelCatalogItem[] } - 原始机密和授权标头绝不是此调用的一部分
providers.refreshModels
- 在:
{ id } - 输出:
{ added: number, updated: number, removed: number, models: ModelCatalogItem[] }
providers.upsertUserModel / providers.deleteUserModel
- 管理自由格式/覆盖模型条目
10. 验证规则
name在提供商中是唯一的(不区分大小写)openai_compatible/ 本地网关需要绝对baseUrl,除非预设表示可选authKind=none禁止用于需要密钥的云预设- headers key 不区分大小写,唯一
- headers key 不区分大小写且唯一,最多 32 条;禁止保留头与 CR/LF
- 强制实施 SecretValue 最大长度(例如 8KB)
- modelId 必须是非空的修剪字符串;允许
/、.、:、-7.旧客户端上的未知协议 => 提供程序显示为禁用并带有警告,而不是崩溃 - 旧版
supportsReasoning(如果存在)仍必须验证为布尔值,但 没有运行时效果 - 旧版
supportedThinkingLevels(如果存在)仍必须验证为 一系列规范思维水平,但没有运行时效果
11. 秘密引用格式
secret:provider:<providerId>:api_key
secret:provider:<providerId>:oauth两个引用相互独立,因此一行可以只有密钥、只有厂商账户,或两者兼有;参见 14-secrets-storage §10。未来的多重秘密提供商可能会 继续添加后缀(:client_secret 等)。