认证与提供方插件

凭据如何存进 auth.json,登录命令与 /connect 如何驱动插件的 auth 钩子,GitHub Copilot 与 ChatGPT 订阅等 OAuth 流程怎样实现,以及 OpenCode 自己的 console 账号做什么用。

作者 David更新于 第 21 篇(共 38 篇)

提供方目录讲过 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字段来源
apikey,可选 metadata手输 API key,或插件 api 方法的结果
oauthaccess、refresh、expires,可选 accountId、enterpriseUrlOAuth 登录
wellknownkey(环境变量名)、tokenopencode 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 Enterpriseauth、provider、chat.*、small_model
OpenAI(Codex)ChatGPT Pro/Plus 浏览器 PKCE 或无头设备码,也可手输 keyauth、provider、chat.*
xAISuperGrok 设备码,或手输 keyauth
AzureAPI key,或 Microsoft Entra ID(执行 az 取 token)auth
Snowflake Cortex浏览器 PKCE,或粘贴 PATauth
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 为先的原生协议层是怎样搭起来的。

本页目录