# 认证与提供方插件

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

- 作者：David（道雾轩）
- 专栏：OpenCode 源码解读（https://daiw.org/manual/opencode.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/opencode/auth-and-plugins
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

[提供方目录](https://daiw.org/manual/opencode/provider-catalog)讲过 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` 接口，与提供方相关的是这两个：

```ts
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`，删减后）：

```ts
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` 这样改写请求头：

```ts
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 会话内核](https://daiw.org/manual/opencode/session-v2)。

下一篇：[packages/llm：模型协议层](https://daiw.org/manual/opencode/llm-package)——脱离 AI SDK、以 Schema 为先的原生协议层是怎样搭起来的。
