OpenCode 如何使用 Codex 订阅,以及 DSH 适配器实现说明

August 14, 2026 · View on GitHub

English | 中文

本文回答两个问题:OpenCode v1.18.18 实际怎样通过 ChatGPT/Codex 订阅调用模型;dsh-codex-subs-plugin 应怎样把这条链路适配到 DeepSeek Harness。文中的 OpenCode 链路固定到 v1.18.18,避免把滚动分支上的变化误写成稳定事实。

Warning

auth.openai.com 的 OAuth/设备码细节、ChatGPT-Account-Id、公共 client ID 和 chatgpt.com/backend-api/codex 都能在 OpenAI 官方 Codex 开源实现中找到,但 OpenAI 官方文档没有把它们定义为面向第三方的稳定 API 合约。OpenCode 和本项目依赖的是私有兼容面,可能随服务端或官方客户端升级而变化。

ChatGPT Plus、Pro、Business 或 Enterprise 订阅也不等于无限额度。模型可用性、速率、用量上限、工作区策略和风控均由服务端决定;OpenCode 将本地展示价格设为零,只表示它不按 Platform API token 价格估算这条订阅流量。

结论

OpenCode 没有把 ChatGPT 订阅“转换成 API key”。它复用了 Codex 客户端的 OAuth 登录,取得用户的 access_tokenrefresh_token 和 ChatGPT account/workspace id,再把 AI SDK 生成的 Responses API 请求改发到 ChatGPT Codex 后端:

ChatGPT OAuth(浏览器 PKCE 或设备码)
  -> access token + refresh token + account id
  -> @ai-sdk/openai 生成 Responses 请求
  -> OpenCode 自定义 fetch 删掉 dummy API key
  -> 注入 Bearer token 和 ChatGPT-Account-Id
  -> 改写到 https://chatgpt.com/backend-api/codex/responses
  -> Responses SSE
  -> AI SDK fullStream
  -> OpenCode 内部 LLM 事件
sequenceDiagram
    actor User as 用户
    participant Client as OpenCode
    participant Auth as auth.openai.com
    participant SDK as @ai-sdk/openai
    participant Codex as ChatGPT Codex backend

    User->>Client: 选择 ChatGPT Pro/Plus 登录
    Client->>Auth: OAuth authorize + PKCE,或申请设备码
    Auth-->>Client: authorization code
    Client->>Auth: POST /oauth/token
    Auth-->>Client: access / refresh / id token
    Client->>Client: 提取 account id 并持久化
    Client->>SDK: streamText + Responses model
    SDK->>Client: /v1/responses 请求
    Client->>Codex: Bearer + ChatGPT-Account-Id + 改写后的请求
    Codex-->>SDK: Responses SSE
    SDK-->>Client: fullStream 事件
    Client-->>User: 文本、推理、工具调用、用量与结束事件

证据层级与官方边界

1. OpenAI 官方文档明确保证什么

OpenAI 官方认证文档明确区分:

  • “Sign in with ChatGPT”用于订阅访问;
  • API key 用于按量计费的 Platform API 访问;
  • ChatGPT 登录会打开浏览器,并把凭据返回给 Codex;
  • ChatGPT 登录遵循所属 ChatGPT workspace 的权限、RBAC 和数据策略;
  • 登录凭据会缓存,Codex 会在令牌到期前自动刷新。

这份文档没有公开承诺第三方可以使用某个 OAuth client ID、设备码接口、ChatGPT-Account-Id 请求头或 chatgpt.com/backend-api/codex。因此,这些细节不能表述成“OpenAI 的公开订阅 API”。

2. OpenAI 官方 Codex 开源实现证明什么

OpenAI 官方 Codex 仓库展示了官方客户端自己的实现:

这些是很强的实现证据,但仍然是客户端源码,不是第三方兼容承诺。OpenCode 的做法可以描述为“跟随官方 Codex 客户端实现”,不能描述为“调用已公开、长期稳定的订阅 API”。

3. OpenCode 固定版本证据

本文逐行核对的是 OpenCode v1.18.18。核心文件为 packages/opencode/src/plugin/openai/codex.ts,并由内置插件列表直接装载,不需要用户另装 npm 插件。该版本固定使用 @ai-sdk/openai@3.0.84

OpenCode v1.18.18 的真实实现

1. 固定常量

codex.ts#L10-L16定义:

CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
ISSUER = "https://auth.openai.com"
CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
OAUTH_PORT = 1455

这是 OAuth public client;客户端没有也不应嵌入 client secret。public client 依靠 PKCE 防止 authorization code 被截获后直接兑换。

2. 浏览器 OAuth + PKCE

OpenCode 生成 43 个 RFC 3986 unreserved 字符作为 verifier,用 SHA-256 和 base64url 得到 challenge,见 codex.ts#L23-L35。授权请求由 codex.ts#L78-L92构造:

GET https://auth.openai.com/oauth/authorize

response_type=code
client_id=app_EMoamEEZ73f0CkXaXp7hrann
redirect_uri=http://localhost:1455/auth/callback
scope=openid profile email offline_access
code_challenge=<SHA-256(verifier), base64url>
code_challenge_method=S256
id_token_add_organizations=true
codex_cli_simplified_flow=true
state=<32-byte random base64url>
originator=opencode

本地 HTTP server 接收 /auth/callback,验证 codestate,并在 5 分钟后超时,见 codex.ts#L154-L260。随后调用:

POST https://auth.openai.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
code=<authorization-code>
redirect_uri=http://localhost:1455/auth/callback
client_id=<public-client-id>
code_verifier=<pkce-verifier>

code exchange 与 refresh 的源码见 codex.ts#L107-L139

安全差异:v1.18.18 的旧插件调用 server.listen(1455),没有显式限定监听 host;同版本 Core V2 已使用 server.listen(1455, "localhost")。独立实现应明确绑定 loopback,并继续校验 OAuth state

3. Headless 设备码登录

codex.ts#L460-L542实现了无浏览器登录:

步骤请求
申请用户码POST https://auth.openai.com/api/accounts/deviceauth/usercode,JSON 为 { "client_id": CLIENT_ID }
提示用户确认打开 https://auth.openai.com/codex/device 并输入 user_code
轮询POST https://auth.openai.com/api/accounts/deviceauth/token,携带 device_auth_iduser_code
尚未确认HTTP 403404,等待服务端 interval 加 3 秒 safety margin
授权成功得到 authorization_codecode_verifier
换 tokenPOST /oauth/token,redirect URI 为 https://auth.openai.com/deviceauth/callback

OpenCode v1.18.18 的轮询没有总超时;OpenAI 官方 Codex 当前源码有约 15 分钟上限。DSH 实现应采用有界等待并支持 AbortSignal

4. Token、account id 与持久化

Token response 包含 access_tokenrefresh_tokenid_token 和可选 expires_in。OpenCode 只解码 JWT payload 来提取 account id,不在本地验证签名;它依赖 TLS 保护的 OpenAI token endpoint,并只把 claims 用作路由元数据。

提取优先级见 codex.ts#L38-L76

  1. chatgpt_account_id
  2. "https://api.openai.com/auth".chatgpt_account_id
  3. organizations[0].id

OpenCode 存储的 OAuth 记录为:

{
  type: "oauth",
  access: string,
  refresh: string,
  expires: number,
  accountId?: string
}

凭据写入 XDG data 下的 opencode/auth.json,文件 mode 为 0600,见 auth/index.ts#L8-L20auth/index.ts#L58-L80。刷新后会持久化旋转后的 refresh token;若新 token 没带 account id,则保留旧值。

5. Provider 与请求改写

OpenCode 的 OpenAI provider 始终调用 sdk.responses(modelID),见 provider.ts#L202-L209。OAuth loader 再返回一个自定义 fetch

  1. 先提供 dummy API key,让 AI SDK 能初始化;
  2. 请求发出前删除 AI SDK 生成的 dummy Authorization
  3. 每次请求重读认证记录;
  4. access token 缺失或已过期时刷新;
  5. 用共享 refreshPromise 合并并发刷新;
  6. 注入真实请求头;
  7. 把 Responses 或兼容 chat-completions 路径改写到 Codex backend。

关键逻辑见 codex.ts#L320-L428

Authorization: Bearer <access-token>
ChatGPT-Account-Id: <chatgpt-account-id>
originator: opencode
User-Agent: opencode/<version> (<platform>; <arch>)
session-id: <session-id>

如果原 URL pathname 包含 /v1/responses/chat/completions,目标被替换为:

https://chatgpt.com/backend-api/codex/responses

HTTP header 名大小写不敏感;OpenAI Codex Rust 源码中的拼写是 ChatGPT-Account-ID,OpenCode 写成 ChatGPT-Account-Id

OpenCode 当前只依据本地 expires 主动刷新,没有在这个 HTTP fetch 层完整实现一次 401 强制刷新重试;稳健适配器应在确认请求可安全重放后,只重试一次。

6. Responses 请求体

OAuth 登录时,系统提示词放入 Responses 的 instructions,而不是再次作为普通 system message 前置,见 request.ts#L56-L112。OpenAI function tools 被强制设为 strict: false,见 request.ts#L148-L158

典型 wire body 为:

{
  "model": "gpt-5.5",
  "input": [],
  "instructions": "...",
  "tools": [
    {
      "type": "function",
      "name": "read_file",
      "description": "...",
      "parameters": {},
      "strict": false
    }
  ],
  "store": false,
  "stream": true,
  "prompt_cache_key": "<session-id>",
  "include": ["reasoning.encrypted_content"],
  "reasoning": {
    "effort": "medium",
    "summary": "auto"
  },
  "text": {
    "verbosity": "low"
  }
}

对应变换证据:

模型 id 和支持的 reasoning effort 会变化,不能把 v1.18.18 的本地 allowlist 当作服务端 entitlement 真源。

7. SSE 响应适配

codex.ts 不直接解析 Responses SSE。OpenCode 调用 AI SDK 的 streamText(),再消费 result.fullStream,见 llm.ts#L271-L378。实际两级适配是:

Responses SSE
  -> @ai-sdk/openai@3.0.84
  -> AI SDK fullStream
  -> OpenCode LLMAISDK.toLLMEvents()
  -> text / reasoning / tool / usage / finish / error

OpenCode 的事件映射见 ai-sdk.ts#L76-L285,覆盖:

  • text start/delta/end;
  • reasoning start/delta/end;
  • tool input start/delta/end;
  • tool call/result/error;
  • step start/finish 和 usage;
  • finish、abort 与 error。

底层常见事件包括 response.createdresponse.in_progressresponse.output_text.deltaresponse.function_call_arguments.deltaresponse.output_item.doneresponse.completed。只有收到明确 terminal event,调用才应视为完成。

8. Stateless replay 为什么必需

OpenCode 请求设置 store: false,不能仅靠 previous_response_id 让服务端恢复完整上下文。后续轮次需要重放此前输出中的最小原生信息:

  • encrypted reasoning item;
  • reasoning summary;
  • assistant output text;
  • function_callcall_id、name 和原始 arguments;
  • 对应的 function_call_output

其中 reasoning.encrypted_content 是保持推理连续性而又不在本地保存明文隐藏推理的关键。重放时必须保持 output 顺序、tool call id 和 provider/model provenance;跨 provider 或不可信 replay state 不应直接送往 Codex endpoint。

OpenCode Core V2 的缺口

同一个 v1.18.18 tag 中还有新的 Effect/Core V2 实现:packages/core/src/plugin/provider/openai.ts。它已经具备:

  • 浏览器 PKCE 和显式 localhost 绑定;
  • headless 设备码登录;
  • code exchange 与 refresh;
  • account id 提取并存入 OAuth credential metadata;
  • 将 OpenAI language model 选择为 sdk.responses()

但在该 tag 的 packages/core/src 中尚未找到旧插件端到端订阅传输所需的两个事实:

  • 没有 https://chatgpt.com/backend-api/codex endpoint 改写;
  • 没有 ChatGPT-Account-Id header 注入。

因此,Core V2 文件本身还不是旧 CodexAuthPlugin 的完整替代。分析或移植时不能只看到 V2 已有 OAuth 就认为订阅调用链已经闭合;v1.18.18 实际可工作的完整证据仍以旧 codex.ts 为准。

DSH 适配器实现

本项目当前包版本为 0.1.0,已经形成可构建、可登录并可注册 DSH 主模型 route 的端到端插件。它采用非侵入 bundle:安装只增加 provider,不修改用户现有的默认模型。

已实现

模块已有能力相比直接照抄 OpenCode 的改进
src/constants.tsclient id、issuer、Codex endpoint、provider id、originator、回调端口常量集中,DSH attribution 与 OpenCode 分离
src/auth.tsPKCE、authorize URL、code exchange、refresh、JWT metadata、凭据读写独立凭据文件;目录 0700、文件 0600;临时文件 + fsync + rename 原子替换;不偷读 Codex CLI/OpenCode 凭据
src/transport.tsOAuth、refresh、device 与 Responses 共用的 proxy-aware fetch私有 dispatcher;大小写代理变量小写优先;禁止 redirect;错误不回显 URL 或代理凭据
CodexAuthManager到期前 60 秒刷新、并发 single-flight、refreshNow()避免刚发请求 token 就过期,并为一次 401 强制刷新预留明确入口
src/protocol/serialize.tsinstructions、tools、store:false、stream、cache key、encrypted reasoning不支持的内容显式报错,不静默丢字段;按 DSH purpose 关闭标题推理
src/protocol/sse.ts标准 SSE framing、JSON 校验、terminal 前断流错误不依赖 AI SDK 黑盒,可把协议错误映射为稳定 LlmError
src/protocol/replay.tsversioned replay state、output index、reasoning/text/tool 映射验证 provider、model、顺序、重复 index 和 tool call id,降低错误或跨路由 replay 风险
src/login.ts / src/bin.ts / src/doctor.tsbrowser、headless、status、doctor、logout CLIcallback 仅绑定 127.0.0.1;5/15 分钟有界等待;doctor 只做无网络的脱敏静态诊断
src/protocol/translate.tsResponses text、reasoning、tool、usage、finish/error 事件严格遵守 DSH block 与 usage -> finish 顺序;校验最终内容和 streamed delta 一致
src/adapter.ts固定后端 transport、headers、401 恢复、模型 metadata无 endpoint 配置;abort/idle timeout;401 只刷新重试一次;区分 quota/context/server 错误
src/index.ts / cordis.patch.yml注册 codex-subscription route只注册 provider,不覆盖 agent-default-model;advisory catalog 不充当权限白名单

默认凭据位置为:

$DSH_CODEX_SUBS_AUTH_FILE
  或 $DSH_HOME/codex-subs/auth.json
  或 ~/.dsh/codex-subs/auth.json

凭据只属于本插件。不要通过扫描或复制 ~/.codex/auth.json、OpenCode auth 文件来“免登录”;共享 refresh token 会带来旋转竞争、意外登出和权限边界混淆。

端到端闭环

  1. dsh-codex-subs login 运行 browser PKCE;--headless 运行有 15 分钟上限的 device flow。
  2. OAuth code/token、refresh 和 device 请求走统一 transport;浏览器授权页本身使用外部浏览器的网络栈。
  3. OAuth token 原子写入独立 auth 文件;CodexAuthManager 在到期前刷新并保留旋转 token。
  4. Cordis 插件将 CodexSubscriptionAdapter 注册为 codex-subscription,但不改变默认模型。
  5. 用户在 settings.yaml 显式选择 provider/model;headless runtime 读取有效选择后创建 agent。
  6. serializer 生成 stateless Responses body;transport 只向固定 Codex endpoint 注入 Bearer/account header。
  7. 首次 401 在读取任何 SSE 前强制刷新并重试一次。
  8. translator 生成 DSH block、usage、finish 与最小 replay state;下一轮校验 provenance 后恢复 encrypted reasoning/tool state。

测试覆盖 PKCE/state、device flow、token rotation、并发 refresh、401 单次重试、固定 URL、account header、SSE 顺序、工具参数 delta、断流、encrypted reasoning replay、max-token projection 和跨模型 replay 降级。所有网络交互均为 mock;不会读取真实账号凭据。

非侵入 bundle 与三层验证

安装 bundle 后,cordis.patch.yml 只插入 llm-codex-subscriptionapply() 只调用 registerAdapter()。因此,已有 DeepSeek 或其他默认模型不会被悄悄替换;要把本插件用于主模型 route,必须显式配置 agent-default-model

flowchart LR
    Dump["--dump-config<br/>Cordis 组合层"] -.->|只证明已组合| Route["codex-subscription<br/>provider 已注册"]
    Settings["settings.yaml<br/>显式默认 provider/model"] --> Select["headless currentSelection()"]
    Route --> Select
    Doctor["status / doctor<br/>本地静态诊断"] -.->|观测,不证明实时选路| Select
    Select --> Adapter["CodexSubscriptionAdapter"]
    Adapter --> Backend["固定 Codex Responses endpoint"]

三种检查回答的是不同问题:

  1. dsh --profile headless --dump-config 只运行 boot-free Cordis patch 组合,可确认 bundle 行存在;它不启动插件,也不加载 $DSH_HOME/settings.yaml 的运行时默认模型。
  2. dsh-codex-subs doctor 直接读取本地 auth/settings/环境,报告凭据是否存在或到期、account id 是否存在、profile 安装检测、插件/provider、声明的默认 provider/model、代理变量是否存在、Node 环境代理开关和固定 endpoint。它不刷新 token、不发网络请求,也不会打印 token、account id、代理 URL 或变量值,所以只能证明本地静态状态。
  3. dsh --profile headless "只回复:codex-subscription-ok" 才是运行时闭环:headless runner 调用 currentSelection(),再按返回的 provider/model 创建 agent并发出真实请求。成功响应才能共同验证选路、OAuth、网络和远端权限,并会消耗订阅用量。

本仓当前没有“打印实时 effective model 而不发请求”的 DSH 命令;不要杜撰一个。排查时应把 doctor 的预期静态选择与一次新的最小 headless 请求配对。

需要有意确认的 DSH 差异

  • DSH serializer 当前在调用方提供 maxTokens 时发送 max_output_tokens;OpenCode v1.18.18 为匹配 Codex CLI 明确删除它。上线前要用受控录制确认服务端支持情况,不能假设两者等价。
  • DSH 直接解析 Responses SSE,不经 @ai-sdk/openai;这减少依赖,但意味着事件演进、未知事件和 usage 聚合都由本适配器负责。
  • DSH 的 originator 应保持 dsh-codex-subs-plugin,不应伪装成 codex_cli_rsopencode
  • JWT claims 只能作为 account metadata,不能成为本地授权判断;实际 entitlement 必须以服务端响应为准。
  • 429、quota、workspace restriction 和模型不可用要分别暴露为稳定错误,不得把订阅限额包装成一般网络失败。

代理传输与区域错误

src/transport.ts通过 Undici EnvHttpProxyAgent 为本插件创建私有 dispatcher,不修改进程的全局 dispatcher。OAuth code exchange、refresh、设备码申请/轮询和 Codex Responses 都注入同一个 fetch,因此 Node 22 与 Node 24 的插件请求具有一致行为:

  • 读取 HTTP_PROXYHTTPS_PROXYNO_PROXY 及小写变体;大小写同时设置时,小写优先;
  • 未设置 HTTPS_PROXY 时,Undici 会回退使用 HTTP_PROXY 处理 HTTPS 请求;
  • NO_PROXY 命中的 host 会绕过代理;
  • ALL_PROXY 既不被当前 transport 使用,也不计入 doctor 的 proxy presence;
  • redirect 固定为 error,避免 bearer token 或 OAuth code 被转发到意外 origin;
  • transport 错误只保留安全的错误类型/代码,不拼入请求 URL、代理 URL 或凭据。

外部浏览器打开的授权页不经过 Node transport,而由浏览器/操作系统决定代理。因此“浏览器能登录”不等于后续 CLI token/Responses 请求拥有同一公网出口。

Node 24 另有官方的 NODE_USE_ENV_PROXY=1 / --use-env-proxy,用于让 Node 全局 HTTP(S) 客户端读取环境代理。doctor 把它作为环境背景报告;本插件并不依赖这个开关,而是始终使用自己的统一 transport。示例环境:

export HTTPS_PROXY=http://proxy.example:8080
export HTTP_PROXY=http://proxy.example:8080
export NO_PROXY=localhost,127.0.0.1

代理 URL 可能内含用户名/密码,NO_PROXY 也可能暴露内部域名。doctor 只能输出这些变量“是否存在”,不得输出值;issue、日志和支持工单也应遵循同一规则。

适配器把服务端的 unsupported_country_region_territory 映射为稳定 DSH 错误码 UNSUPPORTED_COUNTRY_REGION_TERRITORY,再根据是否配置代理给出脱敏建议。排查顺序如下:

  1. 运行 statusdoctor,确认本地 auth、默认 provider/model、代理 presence;这些命令不测试远端。
  2. 核对 OpenAI 的受支持国家和地区。该官方页面描述 API 服务,而这里调用的是私有 ChatGPT Codex 兼容面,最终资格仍由服务端决定。
  3. 让网络或企业管理员确认 CLI 进程的合规公网出口国家/地区;不要用浏览器出口代替 CLI 出口作判断。
  4. 有代理时检查小写/大写冲突和 NO_PROXY 是否让 auth.openai.comchatgpt.com 意外直连;无代理且组织要求统一出口时,按管理员提供的值配置 HTTPS_PROXY。只配置 ALL_PROXY 对当前 transport 无效。
  5. 让浏览器与 CLI 使用合法、一致的网络路径后重试最小请求;若失败阶段是 OAuth/device/refresh 或凭据已过期,再重新登录。仍失败时保留错误阶段、HTTP 状态和 request id,移除 token、请求头和代理 URL 后联系 OpenAI 支持或 workspace 管理员。

不得通过伪造 client id/endpoint、复制其他用户凭据或规避区域控制来“修复”这类服务端拒绝。

当前限制

  • 只实现 HTTP + SSE,没有移植 OpenCode 的实验性 Responses WebSocket pool。
  • 当前 DSH catalog 明确声明 text-only;图片输入会在发请求前报 UNSUPPORTED_CONTENT
  • function tool 使用 strict:false,但尚未移植 OpenCode 对不兼容 JSON Schema 关键字的清洗逻辑。
  • 自动测试全部使用 mock OAuth/Responses 数据;本仓库没有使用用户真实订阅做在线 smoke test。
  • refresh single-flight 只覆盖单进程;两个进程同时使用同一 auth 文件仍可能竞争旋转 token,生产化需要跨进程锁。

使用示例

Auth API

src/auth.ts能够独立解析并刷新现有凭据,构建后的 dsh-codex-subs-plugin/auth 公共入口可按下面方式使用:

import {
  CodexAuthManager,
  defaultAuthFile,
} from 'dsh-codex-subs-plugin/auth'

const manager = new CodexAuthManager({
  authFile: defaultAuthFile(),
})

const auth = await manager.access()
console.log({
  expiresAt: auth.expiresAt,
  accountId: auth.accountId,
})

不要打印 accessTokenrefreshToken,也不要把 auth 文件提交到版本库。

端到端用法

先从插件 checkout 安装到 headless profile:

pnpm install
pnpm run check
dsh plugin --profile headless add .

可用下面的命令确认 llm-codex-subscription 已进入 Cordis 组合层:

dsh --profile headless --dump-config

这一步不读取 settings.yaml,不是运行时默认模型验证。接着登录:

dsh plugin --profile headless exec dsh-codex-subs login

登录命令会打开 ChatGPT 浏览器授权;无浏览器环境改用:

dsh plugin --profile headless exec dsh-codex-subs login --headless

然后显式在 $DSH_HOME/settings.yaml 选择 provider;未设置 DSH_HOME 时路径为 ~/.dsh/settings.yaml。示例模型 id 只反映本文快照,实际以账号当时可用模型为准:

agent-default-model:
  provider: codex-subscription
  model: gpt-5.5
  reasoningEffort: medium

检查本地状态与静态诊断:

dsh plugin --profile headless exec dsh-codex-subs status
dsh plugin --profile headless exec dsh-codex-subs doctor

status 显示本地凭据元数据;doctor 不刷新、不联网,只显示脱敏 allowlist 字段。两者都不会证明远端权限或实时选路。最后发送最小真实请求:

dsh --profile headless "只回复:codex-subscription-ok"

成功响应才闭合 settings -> currentSelection -> adapter -> Codex backend,并会消耗订阅额度。若不再使用本插件:

dsh plugin --profile headless exec dsh-codex-subs logout

logout 只删除本插件自己的凭据,不影响 Codex CLI 或 OpenCode 登录,也不调用远程 revoke。

风险清单

风险影响缓解措施
私有 endpoint/header/client id 变化登录或请求突然失效固定兼容测试;错误中标明阶段;升级时同时对照官方 Codex 与 OpenCode
订阅额度、模型或 workspace 权限变化429、quota、model unavailable、forbidden服务端为真源;保留结构化状态与 request id;不宣称无限额度
refresh token 旋转竞争凭据失效、频繁重登独立 auth 文件、进程内 single-flight、原子持久化;需要时增加跨进程锁
callback 监听过宽或 state 未校验code 泄露或 login CSRF只绑定 loopback;随机 state;一次性 callback;短超时
设备码无限轮询僵尸进程和无界请求总超时、取消信号、尊重 interval
URL 改写过宽token 被发往错误 hostissuer/backend allowlist;HTTPS;拒绝 redirect 到非预期 origin
SSE 半途断开截断答案被误判成功只接受 terminal response;未完成流抛 STREAM_CLOSED
replay state 损坏或跨路由复用工具错配、上下文污染version、provider/model、output index、call id 全部验证
token 泄漏账号与 workspace 风险0600、日志脱敏、永不上传/提交、logout 精确删除
上游模型 allowlist 过期本地列表与真实 entitlement 不一致catalog 仅作建议;最终以服务端响应为准
浏览器与 CLI 代理出口不同登录成功但 token/model 请求被区域策略拒绝统一合法出口;检查大小写代理变量与 NO_PROXY;记录脱敏 request id
诊断信息泄漏token 或企业代理凭据进入日志/issuedoctor 只输出 presence;不打印 token、account id、代理 URL/值或完整请求头

维护时的核对顺序

  1. 先查 OpenAI 官方认证文档,确认产品支持和账号策略;
  2. 再查 OpenAI Codex 官方开源仓库,确认官方客户端当前实现;
  3. 对照 OpenCode 的固定 release/tag,而不是只看 dev 分支;
  4. 将私有兼容变化隔离在 auth/transport/protocol 模块,避免污染 DSH provider-neutral seam;
  5. 用录制的脱敏协议 fixture 和小规模真实账号 smoke test 验证,不把一次成功请求当成稳定合约。