Files
UHD_Media_Tool/docs/design/pi-providers.md
T
rootandClaude Opus 5.5 b343dddbf6 docs: 清理过程文档,CHANGELOG 补齐 0.3.8–0.3.14
- 删除 docs/plan 下 00–18 的计划、评审、验收、发布记录与升级证据,docs/spikes 与 spikes 验证代码
  (保留 spikes/movieclaw-src 本地克隆),以及 4 份一次性审查 / 调研;全部可在 ceb66f8 查到
- MP 对照移到 docs/porting/moviepilot-v3-comparison.md;经验教训改为 docs/plan/lessons.md(补第 25 条)
- docs/plan/README.md:文档放哪、当前状态、待办
- 引用改写:代码注释指向在维护的文档,其余注明已删除与 git 位置;清掉原有 6 个失效链接
- README 开头改为当前状态与入口;CHANGELOG 补齐 0.3.8–0.3.14 与工程变更

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 16:08:09 +08:00

198 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pi 模型上游与订阅登录(pi-ai / pi-coding-agent 0.87.1)
> 2026-10-01 升级补充:当前接入目标已更新为 pi-agent-core/pi-ai 0.99.2,执行结果见 全面升级记录(已删除,见 git `ceb66f8`)。下文保留 0.87.1 的研究基线,不用历史计数验收新版。
>
> 新版实际目录为 42 个内置 provider(新增 TypeSafe 分类服务),合同和目录回归已同步;TypeSafe 没有 Agent 聊天模型,目录说明已标明。OAuth 共 9 个 provider:新增 `openai` 原生 Sign in with ChatGPT,浏览器流程与回调端口 1455;现有 `openai-codex` 仍保留 device/browser 两种模式。新版 `openai` 是动态注册的公开客户端流程,没有照搬旧 Codex 客户端身份提示。来源:[0.99.2 OpenAI provider](https://github.com/earendil-works/pi/blob/v0.99.2/packages/ai/src/providers/openai.ts)、[原生 ChatGPT OAuth](https://github.com/earendil-works/pi/blob/v0.99.2/packages/ai/src/auth/oauth/openai-chatgpt.ts)。
>
> Pi 原生 MCP 客户端可复用,但本次依赖升级没有新增外部 MCP 工具配置;本项目对外 MCP 服务端、权限和审批仍按既有设计运行。
> 只读研究结论,依据本机 `~/.cache/deno/npm/registry.npmjs.org/@earendil-works/{pi-ai,pi-coding-agent}/0.87.1` 的 dist 源码与随包 docs(`docs/providers.md`、`models.md`、`custom-provider.md`)。
> 目的:设置页支持 pi 的全部上游与订阅式登录。结论以代码为准,未核实的不写。
## 0. 一句话结论
- pi-ai 内置 **41 个 provider**(`providers/all.js` 的 `builtinProviders()`),模型表为**随包静态目录**(`providers/data/*.json`,生成时间 2026-09-22),只有 Radius 支持动态拉取。
- 订阅/OAuth 登录共 **8 种**:Anthropic(Claude Pro/Max)、OpenAI Codex(ChatGPT Plus/Pro)、GitHub Copilot、xAI(SuperGrok/X Premium)、Kimi Code、Meta(Muse)、OpenRouter(OAuth 换永久 key)、Radius(pi 自家网关)。
- **OpenCode Go / OpenCode Zen 没有 OAuth**:两者都只能用 API Key(`OPENCODE_API_KEY`,在 opencode.ai 控制台生成),订阅体现在 key 上。
- **Google Gemini CLI / Antigravity 的 OAuth 在 0.87.1 已不存在**(全包 grep `antigravity|gemini-cli|cloudcode-pa` 无结果)。Google 只剩 `google`(Gemini API Key)和 `google-vertex`(API Key / ADC / 服务账号)。
- 登录流程完全通过可注入的 `AuthInteraction` 回调(`prompt`/`notify`)驱动,**可以在无浏览器服务端由 Web 设置页复现**,不用 fork pi。
## 1. pi-ai provider 注册表
### 1.1 关键 API(实际签名,`dist/models.d.ts`)
```ts
createModels(options?: { credentials?: CredentialStore; modelsStore?: ModelsStore; authContext?: AuthContext }): MutableModels
createProvider<TApi>(input: {
id: string; name?: string; baseUrl?: string; headers?: ProviderHeaders;
auth: ProviderAuth; // { apiKey?: ApiKeyAuth; oauth?: OAuthAuth },至少一个
models: readonly Model<TApi>[]; // 静态基线
fetchModels?: (ctx: RefreshModelsContext) => Promise<readonly Model<TApi>[]>; // 动态覆盖层
filterModels?: (models, credential) => readonly Model<TApi>[];
api: ProviderStreams | Partial<Record<TApi, ProviderStreams>>; // 按 model.api 分派
}): Provider<TApi>
Models.getModels(provider?: string): readonly Model<Api>[] // 同步,取最近一次已知列表
Models.getModel(provider: string, id: string): Model<Api> | undefined
Models.getAvailable(providerId?, opts?): Promise<readonly Model<Api>[]> // 仅鉴权完整的 provider,且经 filterModels
Models.refresh({ providers?, force?, allowNetwork?, signal? }) // 只对动态 provider 生效
Models.checkAuth(providerId) / getAuth(providerId | model, overrides?)
Models.login(providerId, "api_key" | "oauth", interaction: AuthInteraction): Promise<Credential>
Models.logout(providerId)
getSupportedThinkingLevels(model) / clampThinkingLevel(model, level) // "off"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"
```
子路径导出:`@earendil-works/pi-ai/providers/<id>`(如 `providers/openai-codex` 导出 `openaiCodexProvider()`)、`providers/all`(`builtinProviders()`、`builtinModels()`、`getBuiltinModels(p)`、`getBuiltinProviders()`)、`api/<name>.lazy`。
### 1.2 通用能力(所有 provider 一致)
- 流式:全部走 `stream/streamSimple` 事件流(text/thinking/toolcall delta)。工具调用:10 种 API 适配器全部支持。
- thinking:`Model.reasoning: boolean` + 可选 `thinkingLevelMap`(null 表示该档不支持);请求时 `streamSimple(..., { reasoning: ThinkingLevel })`,用 `clampThinkingLevel` 夹到模型支持的档位。OpenAI 兼容端点的参数格式由 `compat.thinkingFormat` 决定(openai/openrouter/deepseek/together/baseten/zai/qwen/qwen-chat-template/chat-template/string-thinking/ant-ling)。
- 缓存:通用请求选项 `cacheRetention: "none"|"short"|"long"` + `sessionId`(做缓存亲和/路由),由各适配器映射;目录里只有 anthropic 的 15 个模型声明了 `promptCache` 寿命。
- baseUrl 在 **Model 级**(每个模型自带 `baseUrl`);provider 级 `baseUrl` 只是元数据。覆盖做法 = 克隆模型改 `baseUrl`(pi-coding-agent 的 `provider-composer.js` 就是 `config.baseUrl ?? model.baseUrl`)。OAuth 的 `toAuth()` 也可返回 `baseUrl`(Copilot 按 token 计算代理域名)。
### 1.3 内置 provider 一览(id / 认证 / API / 目录模型数)
API Key 型的 key 同时支持环境变量(下表)和存储凭据;“OAuth”栏为订阅登录名称。
| id | 名称 | 认证 | API | 模型数 |
|---|---|---|---|---|
| anthropic | Anthropic | Key `ANTHROPIC_API_KEY`(另认 `ANTHROPIC_OAUTH_TOKEN`、`ANTHROPIC_AUTH_TOKEN`)+ **OAuth Claude Pro/Max** | anthropic-messages | 15 |
| openai | OpenAI | Key `OPENAI_API_KEY` | openai-responses | 41 |
| openai-codex | OpenAI Codex | **仅 OAuth**(ChatGPT Plus/Pro) | openai-codex-responses(SSE/WebSocket) | 8 |
| azure-openai-responses | Azure OpenAI | Key `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` 或 `_RESOURCE_NAME` | azure-openai-responses | 41 |
| github-copilot | GitHub Copilot | Key `COPILOT_GITHUB_TOKEN` + **OAuth(device code)** | anthropic/openai-completions/openai-responses 混合 | 32 |
| google | Google Gemini | Key `GEMINI_API_KEY` | google-generative-ai | 22 |
| google-vertex | Vertex AI | Key `GOOGLE_CLOUD_API_KEY` / ADC / 服务账号文件 + project/location | google-vertex | 14 |
| amazon-bedrock | Bedrock | Bearer token / AWS profile / 凭据链 | bedrock-converse-stream | 165 |
| xai | xAI | Key `XAI_API_KEY` + **OAuth(SuperGrok/X Premium,device code)** | openai-responses | 4 |
| openrouter | OpenRouter | Key `OPENROUTER_API_KEY` + **OAuth(PKCE 换永久 key)** | anthropic/openai-completions | 386 |
| opencode | OpenCode Zen | Key `OPENCODE_API_KEY` | anthropic/google/openai 三种混合 | 73 |
| opencode-go | OpenCode Go | Key `OPENCODE_API_KEY`(同一个变量) | anthropic/openai-completions/openai-responses | 30 |
| kimi-coding | Kimi For Coding | Key `KIMI_API_KEY` + **OAuth Kimi Code(device code)** | anthropic-messages | 4 |
| meta | Meta | Key `META_API_KEY` + **OAuth Muse(device code)** | openai-responses | 5 |
| radius | Radius(pi 网关) | Key `RADIUS_API_KEY` + **OAuth**;**动态模型表** | pi-messages | 30 |
| vercel-ai-gateway | Vercel AI Gateway | Key `AI_GATEWAY_API_KEY` | anthropic-messages | 246 |
| cloudflare-ai-gateway | CF AI Gateway | Key `CLOUDFLARE_API_KEY` + `env` 中 ACCOUNT_ID/GATEWAY_ID | 三种混合 | 51 |
| cloudflare-workers-ai | CF Workers AI | Key + `CLOUDFLARE_ACCOUNT_ID` | openai-completions | 18 |
| mistral | Mistral | Key `MISTRAL_API_KEY` | mistral-conversations | 33 |
| deepseek / groq / cerebras / together / baseten / fireworks / huggingface / nvidia / ant-ling | 各自 | Key(`DEEPSEEK_API_KEY`、`GROQ_API_KEY`、`CEREBRAS_API_KEY`、`TOGETHER_API_KEY`、`BASETEN_API_KEY`、`FIREWORKS_API_KEY`、`HF_TOKEN`、`NVIDIA_API_KEY`、`ANT_LING_API_KEY`) | openai-completions(fireworks 另有 anthropic) | 2/7/2/22/21/33/76/19/3 |
| moonshotai / moonshotai-cn | Moonshot 国际/国内 | Key `MOONSHOT_API_KEY` | openai-completions | 4/4 |
| minimax / minimax-cn | MiniMax 国际/国内 | Key `MINIMAX_API_KEY` / `MINIMAX_CN_API_KEY` | anthropic-messages | 3/3 |
| zai / zai-coding-cn | Z.AI Coding Plan 国际/国内 | Key `ZAI_API_KEY` / `ZAI_CODING_CN_API_KEY` | openai-completions | 7/4 |
| qwen-token-plan / -cn / -individual | 阿里云百炼 Token Plan | Key `QWEN_TOKEN_PLAN_API_KEY` / `_CN_API_KEY`(individual 复用前者) | openai-completions | 20/20/9 |
| xiaomi / xiaomi-token-plan-cn/-ams/-sgp | 小米 MiMo | Key `XIAOMI_API_KEY` / `XIAOMI_TOKEN_PLAN_{CN,AMS,SGP}_API_KEY` | openai-completions | 6/4/4/4 |
另有 `faux`(测试假模型,已在用)。模型列表来源:除 Radius 外全部是随包 JSON(不联网);Copilot 有 `filterModels`,按 OAuth 凭据里的 `availableModelIds` 过滤;OpenCode 系列请求自动带 `x-opencode-session`(取 `sessionId`)。
### 1.4 OpenAI 兼容通用上游(自定义端点)
pi-ai 没有“generic provider”工厂,做法就是我们现在 `packages/agent/src/pi/model.ts` 的写法:`createProvider({ id, auth, models:[{ id, api:"openai-completions", baseUrl, reasoning, input, cost, contextWindow, maxTokens, compat? , headers? }], api: openAICompletionsApi() })`。pi-coding-agent `models.json` 的 provider 字段可参照:`name, baseUrl, apiKey, api, headers, authHeader, compat, modelOverrides, models[]`;常用 compat:`supportsDeveloperRole`、`supportsReasoningEffort`、`maxTokensField`、`thinkingFormat`、`supportsUsageInStreaming`、`supportsStrictMode`。api 也可选 `anthropic-messages`/`openai-responses`/`google-generative-ai`,对应“Anthropic 兼容 / Responses 兼容 / Gemini 兼容”端点。
## 2. 订阅/OAuth 登录(实现在 pi-ai `dist/auth/oauth/*`,pi-coding-agent 只做 UI 与存储)
`/login` 命令(`core/slash-commands.js` + `modes/interactive/interactive-mode.js`)只是把 `AuthPrompt`/`AuthEvent` 渲染到 TUI,流程本身全在 pi-ai 的 `OAuthAuth.login(interaction)`。
| provider | 流程 | 回调 / 手工粘贴 | token 形状与刷新 | 请求时如何用(toAuth) |
|---|---|---|---|---|
| anthropic | 浏览器 PKCE,`claude.ai/oauth/authorize`,token `platform.claude.com/v1/oauth/token` | 监听 `127.0.0.1:53692/callback`;**同时**给 `manual_code` 提示,可粘 redirect URL 或 `code#state`;`state` = PKCE verifier | access/refresh,`expires` 提前 5 分钟;refresh_token grant | `{ apiKey: access }`;适配器识别 `sk-ant-oat` 后改 Bearer,加 beta `claude-code-20250219,oauth-2025-04-20`,**强制注入 “You are Claude Code…” 系统提示并把工具名改成 Claude Code 风格** |
| openai-codex | 先 `select`:浏览器 PKCE(`auth.openai.com`,redirect `http://localhost:1455/auth/callback`)或 **device code**(`auth.openai.com/codex/device`,15 分钟) | 浏览器模式同时有回调监听与 `manual_code`(接受完整 URL / `code#state` / `code=` 串 / 裸 code) | access/refresh/expires + `accountId`(从 JWT 的 `chatgpt_account_id` 解出);refresh_token grant | `{ apiKey: access }`,适配器从 token 解 accountId 加请求头,baseUrl `chatgpt.com/backend-api` |
| github-copilot | 先 `text` 问 GHE 域名(空=github.com),然后 device code(`/login/device/code`,scope `read:user`) | 无回调,轮询 | `refresh` 存 GitHub OAuth token,`access` 是 Copilot 短期 token(`expires_at` 提前 5 分钟);另存 `enterpriseUrl`、`availableModelIds`;登录时还会自动对需开启的模型调 `/models/{id}/policy` | `{ apiKey: access, baseUrl: 按 token 计算的代理域名 }`,固定伪装 VS Code Copilot Chat 头 |
| xai | device code(`auth.x.ai/oauth2/device/code`) | 轮询 | access/refresh,缺省 1h,提前 5 分钟 | `{ apiKey: access }` |
| kimi-coding | device code(`auth.kimi.com`) | 轮询 | access/refresh;refresh 最多重试 3 次 | `{ headers: { Authorization: Bearer access } }` |
| meta | device code(`auth.meta.com/oidc/device/...`),再用身份 token 调 `api.meta.ai/muse-code/key` 铸 24h key | 轮询 | `refresh` 存身份 token,`access` 存铸出的 key;“刷新”= 重新铸 key(auth.meta.com 不支持 refresh grant) | `{ apiKey: access }` |
| openrouter | 浏览器 PKCE,`openrouter.ai/auth?callback_url=...`,换 `api/v1/auth/keys` | 监听 `127.0.0.1:<随机端口>/oauth/callback/<uuid>` + `manual_code` | 得到**永久 API key**,`expires = MAX_SAFE_INTEGER`,`refresh=""`,不刷新 | `{ apiKey: access }` |
| radius | 先 `select`:浏览器(`127.0.0.1:1456/oauth/callback`)或 device code;网关默认 `radius.pi.dev` | 同上 | access/refresh,提前 60s;凭据中带 `gatewayConfig`(模型目录) | `{ apiKey: access }` |
通用机制:
- `AuthInteraction = { signal?, prompt(p: AuthPrompt): Promise<string>, notify(e: AuthEvent): void }`。`AuthPrompt.type ∈ text | secret | select | manual_code`;`AuthEvent.type ∈ info | auth_url | device_code | progress`。浏览器型流程会**同时**等回调和 `manual_code`,谁先到用谁。
- 存储:`CredentialStore { read, list, modify(providerId, fn), delete }`,**按 provider id 一份凭据**。pi-coding-agent 的 `auth.json`(`~/.pi/agent/auth.json`,0600,proper-lockfile 加锁)形如:
`{ "anthropic": { "type":"oauth", "access":"…", "refresh":"…", "expires": 1790000000000 }, "openai-codex": { "type":"oauth", …, "accountId":"…" }, "opencode": { "type":"api_key", "key":"…" }, "cloudflare-workers-ai": { "type":"api_key", "key":"…", "env": { "CLOUDFLARE_ACCOUNT_ID":"…" } } }`;`key` 以 `!` 开头表示执行命令取值。
- 刷新:`Models.getAuth()` 内按需刷新——剩余 < 5 分钟(或 `overrides.minOAuthValidityMs`)时在 `modify` 锁内双重检查、调 `oauth.refresh()`、写回;失败抛 `ModelsError(code:"oauth")`,**旧凭据保留**,需重新登录。没有后台定时刷新。
- 与 `createProvider` 衔接:OAuth token 不当作“配置的 apiKey”,而是 `toAuth(credential)` 在每次请求前转成 `ModelAuth { apiKey?, headers?, baseUrl? }`。我们只要把自己的 `CredentialStore` 交给 `createModels({ credentials })`,内置 provider 就会自动使用并刷新 OAuth 凭据。
## 3. 无浏览器服务端完成 OAuth
**可行,且不用改 pi。**做法:服务端调用 `models.login(providerId, "oauth", interaction)`,interaction 由我们实现成“挂起式”:
1. `POST /oauth/start`:建 pending login(内存,TTL 15 分钟,带 AbortController),启动 `login()`;`prompt(select)` 用 start 请求里的 `method`(browser/device_code)作答,`prompt(text)` 用 `enterpriseDomain` 等参数作答;`notify(auth_url|device_code)` 写进 pending 记录;start 在拿到第一个事件后返回 `{ loginId, authUrl? , userCode?, verificationUri?, expiresIn? }`。
2. 用户在自己的浏览器授权。浏览器型的 redirect 指向用户本机的 localhost,页面会打不开,这是预期的:让用户**复制地址栏里的完整 URL**。
3. `POST /oauth/complete { loginId, input }`:把 input 交给挂起的 `manual_code` Promise;pi 自己解析 URL/`code#state`、校验 state、换 token;成功后经我们的 `CredentialStore.modify` 写 secret 表。device code 型不需要 complete,前端轮询 `GET /oauth/status?loginId=` 直到 `done/failed`。
各 provider 的回调情况:
- 必须粘回 URL(redirect 固定 localhost,无法改成我们的域名):**Anthropic(53692)、OpenAI Codex 浏览器模式(1455)、Radius 浏览器模式(1456)**。redirect_uri 与 client_id 在授权服务器登记,不能换成公网地址。
- 优先走 device code(不用粘 URL,体验最好):**Codex(可选)、Copilot、xAI、Kimi、Meta、Radius(可选)**。Codex 的 device code 若返回 404,说明账号未开启该方式,需回退浏览器模式。
- OpenRouter:`callback_url` 由客户端自由指定,理论上可直接回调到我们的设置页;但 pi 的实现写死本地监听,用 pi 就走“粘 URL”;要直回调需自己实现(流程很简单:PKCE + POST `/api/v1/auth/keys`)。
注意事项:
- pi 仍会在服务端 `listen` 这些 localhost 端口(对方连不上,无害)。**Anthropic 端口被占时会直接 reject 导致登录失败**(Codex/Radius 会降级为只等粘贴)→ 同一 provider 的登录要串行,容器内别占 53692/1455/1456。
- pending login 只能活在 **api 进程内存**(interaction 是闭包),多实例部署需粘性路由,或者直接限制单实例处理登录。
- OAuth 模块通过 `process.versions.node` 判断后动态 `import("node:http"/"node:crypto")`,Deno 的 npm 兼容层满足条件,但要做一次 spike 实测(尤其 Codex 的 `_randomBytes` 是异步赋值)。
## 4. 映射方案
### 4.1 contracts `settings.ts`:`pi` 段改为“上游列表 + 默认上游”
```ts
export const PiUpstream = z.object({
id: Id, // 我们的上游 id(同一 provider 可有多个,例如两个 Codex 账号)
label: z.string().min(1).max(64),
provider: z.string().min(1), // pi provider id(见 §1.3)或 "custom-openai" | "custom-anthropic" | "fake"
authKind: z.enum(["api_key", "oauth", "ambient", "none"]),
apiKeySecretRef: Id.nullable(), // api_key:secret 表引用
oauthSecretRef: Id.nullable(), // oauth:secret 表中存 pi Credential JSON(access/refresh/expires/+扩展字段)
providerEnv: z.record(z.string(), z.string()).nullable(), // CF ACCOUNT_ID、Vertex project/location、Azure resource 等非密值
baseUrl: z.url().nullable(), // 仅 api_key/custom 允许;OAuth 型置 null
api: z.enum(["openai-completions","openai-responses","anthropic-messages","google-generative-ai"]).nullable(), // 仅 custom
compat: z.record(z.string(), z.unknown()).nullable(), // 仅 custom
model: z.string().min(1).nullable(),
thinkingLevel: ThinkingLevel,
note: z.string().max(500).nullable(),
status: z.enum(["ok", "needs_login", "refresh_failed", "untested"]), // 服务端维护,只读
});
export const PiSettings = z.object({
upstreams: z.array(PiUpstream).max(32),
defaultUpstreamId: Id.nullable(),
contextBudget, compaction, subagentsEnabled, // 保持原字段
});
```
能力/任务级 `upstreamId?` 覆盖默认上游(没有就用 `defaultUpstreamId`)。迁移:旧 `{provider, baseUrl, model, apiKeySecretRef}` 生成一条 upstream 并设为默认。
### 4.2 运行时装配
- **每个上游建一个 `Models` 实例**,里面只放它对应的那个内置 provider(`providers/<id>` 工厂),`credentials` 用一个只映射到本上游 secret 的 `CredentialStore` 适配器。这样绕开“pi 按 provider id 只存一份凭据”的限制,同一 provider 的多个账号可以并存。
- baseUrl 覆盖:包一层 provider,`getModels: () => base.getModels().map(m => ({ ...m, baseUrl }))`。
- thinking:`clampThinkingLevel(model, upstream.thinkingLevel)` 后传 `reasoning`。
- `ambient` 型(Bedrock 凭据链、Vertex ADC)依赖服务端环境;设置页只做提示,不存密钥。
### 4.3 UI 流程
- 新增上游 → 从 `/pi/providers` 能力表选 provider(按“订阅登录 / API Key / 云凭据 / 自定义兼容端点”分组;OpenCode Go 和 Zen 放在 API Key 组并注明“opencode 订阅 key”)。
- API Key 型:填 key(和所需 env)→ 保存 → “测试连通”。
- OAuth 型三步:① 选方式(有 device code 的默认选它)→ ② 显示授权链接或 `userCode` + 验证地址(复制按钮、倒计时)→ ③ 浏览器型:粘贴地址栏 URL 提交;device 型:自动轮询。成功后显示账号信息(Codex accountId、Copilot 域名)和过期时间。
- 模型下拉:`/models` 返回 `getAvailable()` 的结果(包括 id、name、reasoning、支持的 thinking 档位、contextWindow、input),同时允许手填模型 id(目录滞后于上游时用)。
### 4.4 后端端点
| 端点 | 作用 |
|---|---|
| `GET /pi/providers` | 能力表:id、name、authKinds、oauth{name,isSubscription,methods:["browser"|"device_code"],needsPaste}、envFields、baseUrlOverridable、apis、模型数、目录生成时间 |
| `GET/POST/PATCH/DELETE /pi/upstreams[/{id}]` | CRUD;密钥写 secret 表,响应只回 ref 与掩码 |
| `POST /pi/upstreams/{id}/oauth/start` | `{ method, enterpriseDomain? }` → `{ loginId, authUrl?, userCode?, verificationUri?, expiresIn }` |
| `POST /pi/upstreams/{id}/oauth/complete` | `{ loginId, input }`(粘贴的 URL / code) |
| `GET /pi/upstreams/{id}/oauth/status` | `pending|done|failed` + 进度消息 |
| `POST /pi/upstreams/{id}/test` | `getAuth` + 发一个 1 token 的 `completeSimple`,回延迟与错误分类 |
| `GET /pi/upstreams/{id}/models` | `getAvailable()`,动态 provider 先 `refresh()` |
| `POST /pi/upstreams/{id}/logout` | 删除 secret,状态改为 needs_login(不会撤销提供方侧的授权) |
刷新:worker 定时任务(每 10 分钟)对 OAuth 上游调 `models.getAuth(providerId, { minOAuthValidityMs: 30*60_000 })`,让 pi 在锁内刷新并写回 secret;OpenRouter(永不过期)跳过。`ModelsError.code==="oauth"` → 状态改为 `refresh_failed`,创建待处理事项“上游 X 需要重新登录”;Agent 选上游时跳过不可用的,按配置回落到默认上游或报错。secret 表的 `modify` 要做行级锁(`SELECT … FOR UPDATE`),因为 api 和 worker 两个进程都可能刷新(refresh token 会轮换,并发刷新会让其中一方失效)。
## 5. 版本与许可
- pi-ai / pi-coding-agent 均为 **MIT**。OAuth client_id 全部**明文或 base64 写在公开源码里**:Anthropic `9d1c250a-e61b-44d9-88ed-5944d1962f5e`(Claude Code 的客户端)、Codex `app_EMoamEEZ73f0CkXaXp7hrann`(Codex CLI)、Copilot `Iv1.b507a08c87ecfe98`(VS Code Copilot)、xAI `b1a00492-…`、Kimi `17e5f671-…`、Meta `1031625952748946`、Radius `pi-gateway`。这些是公共客户端(PKCE / device code,没有 secret)。技术上可以直接复用;复用边界最好是“调用 pi-ai 的登录实现”,不要把 ID 抄进我们自己的代码,这样上游改 ID 或流程时跟着升级就行。
- 升级时需要回归:目录 JSON 随版本变化(模型会增删);OAuth 端点和伪装头(Copilot 的 `Editor-Version`、Anthropic 的 Claude Code 身份)随时可能被提供方收紧。锁定 0.87.1,每次升级重新跑本文件的核对。
- 条款风险提醒:这些订阅登录借用的是官方 CLI/IDE 的客户端身份(Anthropic 路径还会把请求伪装成 Claude Code)。把个人订阅用于自动化后台服务可能不符合各家的用户条款或使用政策,账号有被限流或封禁的风险。上线前请逐家确认条款,设置页也要对用户明示(这里不下法律结论)。