认证与提供方插件
凭据如何存进 auth.json,登录命令与 /connect 如何驱动插件的 auth 钩子,GitHub Copilot 与 ChatGPT 订阅等 OAuth 流程怎样实现,以及 OpenCode 自己的 console 账号做什么用。
提供方目录讲过 Provider 状态的叠加顺序,其中两层来自凭据:auth.json 里的 API key,以及插件 auth.loader 注入的选项。本篇把这两层拆开看。
怎么登录
| 方式 | 命令 |
|---|---|
| 交互登录 | opencode auth login(即 opencode providers login),-p 指定提供方,-m 指定登录方式 |
| TUI 内 | /connect |
| 查看、登出 | opencode auth list、opencode auth logout |
| 企业部署 | opencode auth login <url>:读取该地址的 /.well-known/opencode,执行其中声明的命令取 token |
| 不登录 | 直接设置目录里声明的环境变量,如 ANTHROPIC_API_KEY |
凭据存在哪里
V1 的凭据是一个 JSON 文件:~/.local/share/opencode/auth.json,以 0600 权限写入(packages/opencode/src/auth/index.ts)。键是提供方 ID(well-known 登录时是 URL),值有三种:
type | 字段 | 来源 |
|---|---|---|
api | key,可选 metadata | 手输 API key,或插件 api 方法的结果 |
oauth | access、refresh、expires,可选 accountId、enterpriseUrl | OAuth 登录 |
wellknown | key(环境变量名)、token | opencode auth login <url> |
设了环境变量 OPENCODE_AUTH_CONTENT 时,Auth.all() 直接解析它而不读文件,便于在 CI 中注入。wellknown 条目在加载配置时生效:token 被当作名为 key 的变量,用来替换该地址所返回远程配置里的占位符。
插件的两个钩子
内置插件与第三方插件都实现 @opencode-ai/plugin 的 Hooks 接口,与提供方相关的是这两个:
export type AuthHook = {
provider: string
loader?: (auth: () => Promise<Auth>, provider: Provider) => Promise<Record<string, any>>
methods: (
| {
type: "oauth"
label: string
// ...
authorize(inputs?: Record<string, string>): Promise<AuthOAuthResult>
}
// ...
)[]
}
// ...
export type ProviderHook = {
id: string
models?: (provider: ProviderV2, ctx: ProviderHookContext) => Promise<Record<string, ModelV2>>
}(packages/plugin/src/index.ts:88,删减后)
methods描述“能怎么登录”:OAuth 或 API key,都可以带text、select提示,并用when表达“前一项选了什么,后一项才出现”。loader在构建 Provider 状态时调用:拿到当前凭据,返回要并入提供方options的值,通常是一个占位apiKey加一个自定义fetch。provider.models在构建状态的第一步调用,可以按凭据改写该提供方的模型表。
登录与注入
CLI 的 handlePluginAuth()(packages/opencode/src/cli/cmd/providers.ts:39)与服务端的 ProviderAuth 服务(packages/opencode/src/provider/auth.ts)走同一套协议:调插件的 authorize(inputs) 拿到 url、instructions 与 method;method 为 auto 时由 callback() 自己轮询或等本地回调,为 code 时让用户粘贴授权码再调 callback(code);成功后经 Auth.set() 写入 auth.json。TUI 的 /connect 用的是服务端接口:GET /provider/auth 取方法列表,POST /provider/:providerID/oauth/authorize 与 .../oauth/callback 完成授权。
凭据真正起作用是在构建 Provider 状态时(packages/opencode/src/provider/provider.ts:1609,删减后):
for (const plugin of plugins) {
if (!plugin.auth) continue
const providerID = ProviderV2.ID.make(plugin.auth.provider)
if (disabled.has(providerID)) continue
const stored = yield* auth.get(providerID).pipe(Effect.orDie)
if (!stored) continue
if (!plugin.auth.loader) continue
const options = yield* Effect.promise(() =>
plugin.auth!.loader!(
() => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any,
toPublicInfo(database[plugin.auth!.provider]),
),
)
// ...
mergeProvider(providerID, patch)
}传给 loader 的是取凭据的函数而非凭据本身:它每次调用都重新读 auth.json,所以 loader 返回的 fetch 每次请求都能拿到刷新后的 token。返回的 apiKey 往往是占位符 OAUTH_DUMMY_KEY,只为让 AI SDK 不因“缺少 apiKey”报错,真实的 Authorization 头由自定义 fetch 改写。
代表性插件
内置插件列在 packages/opencode/src/plugin/index.ts:67 的 internalPlugins() 里,设 OPENCODE_DISABLE_DEFAULT_PLUGINS 可全部关掉:
| 插件 | 登录方式 | 钩子 |
|---|---|---|
| GitHub Copilot | 设备码 OAuth,支持 GitHub Enterprise | auth、provider、chat.*、small_model |
| OpenAI(Codex) | ChatGPT Pro/Plus 浏览器 PKCE 或无头设备码,也可手输 key | auth、provider、chat.* |
| xAI | SuperGrok 设备码,或手输 key | auth |
| Azure | API key,或 Microsoft Entra ID(执行 az 取 token) | auth |
| Snowflake Cortex | 浏览器 PKCE,或粘贴 PAT | auth |
| DigitalOcean | 浏览器 OAuth,把推理路由器变成模型 | auth、provider |
| Cloudflare 两家 | 只追加账户 ID、网关 ID 提示 | auth |
| Modal | 无,按凭据拉取模型列表 | provider |
| Cerebras | 无 | chat.params |
另有 opencode-gitlab-auth、opencode-poe-auth 两个 npm 包作为内置插件直接导入。
GitHub Copilot 的 authorize() 走 RFC 8628 设备码流程:申请用户码后轮询 GitHub 的 token 接口,authorization_pending 时按间隔加 3 秒余量重试,slow_down 时按 RFC 把间隔加 5 秒。拿到的 GitHub token 同时存为 access 与 refresh,expires 为 0。loader 的 fetch 这样改写请求头:
const headers: Record<string, string> = {
"x-initiator": isAgent ? "agent" : "user",
...(init?.headers as Record<string, string>),
"User-Agent": `opencode/${InstallationVersion}`,
Authorization: `Bearer ${info.refresh}`,
"Openai-Intent": "conversation-edits",
}(packages/opencode/src/plugin/github-copilot/copilot.ts:160)
请求体最后一条消息不是用户消息(或只含工具结果)就标为 agent,chat.headers 还会把压缩后的续跑与子代理会话标为 agent;带图片的请求加 Copilot-Vision-Request。provider.models 钩子用这个 token 拉取 Copilot 的 /models,按 supported_endpoints 决定走 Messages(换用 @ai-sdk/anthropic)、Responses 还是 Chat,并按 reasoning_effort 生成 variants。
OpenAI 的浏览器方式在本机 1455 端口起回调服务,走带 PKCE 与 state 校验的授权码流程;无头方式走 OpenAI 的设备授权接口。loader 的 fetch 在 token 过期时单飞刷新并写回,加上 ChatGPT-Account-Id 头,再把路径含 /v1/responses 或 /chat/completions 的请求改道到 https://chatgpt.com/backend-api/codex/responses(packages/opencode/src/plugin/openai/codex.ts:422)。provider.models 同时只保留订阅可用的模型并把价格清零。设 OPENCODE_EXPERIMENTAL_WEBSOCKETS=true 还能让 Responses 请求改走 WebSocket。
其余几家也各有巧思:xAI 解析 JWT 的 exp,在过期前 120 秒主动刷新;Azure 的 Entra ID 方式调 az account get-access-token 取 token,没装 az 时隐藏这个选项。
OpenCode 自己的账号:console
packages/opencode/src/account/ 容易与上面混淆,其实与模型凭据无关。它是 OpenCode console 的账号体系:隐藏命令 opencode console login(默认服务器 https://opencode.ai/console)以 opencode-cli 为 client ID 走设备码登录,账号与 token 存进 SQLite。选定组织后,加载配置时会拉取该组织的 /api/config 并入全局配置,组织借此统一下发提供方设置;token 还会写进 OPENCODE_CONSOLE_TOKEN 环境变量,分享功能也用它。
V2:Integration 与 Credential
迁移中的内核把“登录”抽象成 Integration:每个集成声明 oauth、key、env 几类方法,models.dev 插件为每个带 env 的提供方自动登记 key 与 env 两种方法,OpenAI 插件再登记 ChatGPT 的两种 OAuth(packages/core/src/plugin/provider/openai.ts)。凭据存进 SQLite,每个集成一条(packages/core/src/credential.ts)。OAuth 实现可以附带 refresh,由内核在离过期不到 5 分钟时统一刷新并写回,不必再塞进 fetch 闭包。更多见 V2 会话内核。
下一篇:packages/llm:模型协议层——脱离 AI SDK、以 Schema 为先的原生协议层是怎样搭起来的。