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_token、refresh_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 仓库展示了官方客户端自己的实现:
- 浏览器 OAuth、PKCE、本地回调和 code exchange
- 设备码申请、轮询和约 15 分钟总超时
- ChatGPT 登录使用 Codex backend,API key 使用 Platform API
- Bearer token 与
ChatGPT-Account-ID https://auth.openai.com/oauth/token刷新- Token claims 与 account id 数据结构
这些是很强的实现证据,但仍然是客户端源码,不是第三方兼容承诺。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. 固定常量
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,验证 code 和 state,并在 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_id 和 user_code |
| 尚未确认 | HTTP 403 或 404,等待服务端 interval 加 3 秒 safety margin |
| 授权成功 | 得到 authorization_code 与 code_verifier |
| 换 token | POST /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_token、refresh_token、id_token 和可选 expires_in。OpenCode 只解码 JWT payload 来提取 account id,不在本地验证签名;它依赖 TLS 保护的 OpenAI token endpoint,并只把 claims 用作路由元数据。
提取优先级见 codex.ts#L38-L76:
chatgpt_account_id"https://api.openai.com/auth".chatgpt_account_idorganizations[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-L20和 auth/index.ts#L58-L80。刷新后会持久化旋转后的 refresh token;若新 token 没带 account id,则保留旧值。
5. Provider 与请求改写
OpenCode 的 OpenAI provider 始终调用 sdk.responses(modelID),见 provider.ts#L202-L209。OAuth loader 再返回一个自定义 fetch:
- 先提供 dummy API key,让 AI SDK 能初始化;
- 请求发出前删除 AI SDK 生成的 dummy
Authorization; - 每次请求重读认证记录;
- access token 缺失或已过期时刷新;
- 用共享
refreshPromise合并并发刷新; - 注入真实请求头;
- 把 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"
}
}
对应变换证据:
store: false:transform.ts#L1173-L1182- session 作为 prompt cache key:
transform.ts#L1262-L1274 - 默认 reasoning、summary 和 encrypted reasoning:
transform.ts#L1292-L1305 store !== true时去除 Responses item id:transform.ts#L502-L516- 为匹配 Codex CLI,最终清除
maxOutputTokens:codex.ts#L549-L563
模型 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.created、response.in_progress、response.output_text.delta、response.function_call_arguments.delta、response.output_item.done 和 response.completed。只有收到明确 terminal event,调用才应视为完成。
8. Stateless replay 为什么必需
OpenCode 请求设置 store: false,不能仅靠 previous_response_id 让服务端恢复完整上下文。后续轮次需要重放此前输出中的最小原生信息:
- encrypted reasoning item;
- reasoning summary;
- assistant output text;
function_call的call_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/codexendpoint 改写; - 没有
ChatGPT-Account-Idheader 注入。
因此,Core V2 文件本身还不是旧 CodexAuthPlugin 的完整替代。分析或移植时不能只看到 V2 已有 OAuth 就认为订阅调用链已经闭合;v1.18.18 实际可工作的完整证据仍以旧 codex.ts 为准。
DSH 适配器实现
本项目当前包版本为 0.1.0,已经形成可构建、可登录并可注册 DSH 主模型 route 的端到端插件。它采用非侵入 bundle:安装只增加 provider,不修改用户现有的默认模型。
已实现
| 模块 | 已有能力 | 相比直接照抄 OpenCode 的改进 |
|---|---|---|
src/constants.ts | client id、issuer、Codex endpoint、provider id、originator、回调端口 | 常量集中,DSH attribution 与 OpenCode 分离 |
src/auth.ts | PKCE、authorize URL、code exchange、refresh、JWT metadata、凭据读写 | 独立凭据文件;目录 0700、文件 0600;临时文件 + fsync + rename 原子替换;不偷读 Codex CLI/OpenCode 凭据 |
src/transport.ts | OAuth、refresh、device 与 Responses 共用的 proxy-aware fetch | 私有 dispatcher;大小写代理变量小写优先;禁止 redirect;错误不回显 URL 或代理凭据 |
CodexAuthManager | 到期前 60 秒刷新、并发 single-flight、refreshNow() | 避免刚发请求 token 就过期,并为一次 401 强制刷新预留明确入口 |
src/protocol/serialize.ts | instructions、tools、store:false、stream、cache key、encrypted reasoning | 不支持的内容显式报错,不静默丢字段;按 DSH purpose 关闭标题推理 |
src/protocol/sse.ts | 标准 SSE framing、JSON 校验、terminal 前断流错误 | 不依赖 AI SDK 黑盒,可把协议错误映射为稳定 LlmError |
src/protocol/replay.ts | versioned replay state、output index、reasoning/text/tool 映射 | 验证 provider、model、顺序、重复 index 和 tool call id,降低错误或跨路由 replay 风险 |
src/login.ts / src/bin.ts / src/doctor.ts | browser、headless、status、doctor、logout CLI | callback 仅绑定 127.0.0.1;5/15 分钟有界等待;doctor 只做无网络的脱敏静态诊断 |
src/protocol/translate.ts | Responses 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 会带来旋转竞争、意外登出和权限边界混淆。
端到端闭环
dsh-codex-subs login运行 browser PKCE;--headless运行有 15 分钟上限的 device flow。- OAuth code/token、refresh 和 device 请求走统一 transport;浏览器授权页本身使用外部浏览器的网络栈。
- OAuth token 原子写入独立 auth 文件;
CodexAuthManager在到期前刷新并保留旋转 token。 - Cordis 插件将
CodexSubscriptionAdapter注册为codex-subscription,但不改变默认模型。 - 用户在
settings.yaml显式选择 provider/model;headless runtime 读取有效选择后创建 agent。 - serializer 生成 stateless Responses body;transport 只向固定 Codex endpoint 注入 Bearer/account header。
- 首次 401 在读取任何 SSE 前强制刷新并重试一次。
- 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-subscription,apply() 只调用 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"]
三种检查回答的是不同问题:
dsh --profile headless --dump-config只运行 boot-free Cordis patch 组合,可确认 bundle 行存在;它不启动插件,也不加载$DSH_HOME/settings.yaml的运行时默认模型。dsh-codex-subs doctor直接读取本地 auth/settings/环境,报告凭据是否存在或到期、account id 是否存在、profile 安装检测、插件/provider、声明的默认 provider/model、代理变量是否存在、Node 环境代理开关和固定 endpoint。它不刷新 token、不发网络请求,也不会打印 token、account id、代理 URL 或变量值,所以只能证明本地静态状态。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_rs或opencode。 - 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_PROXY、HTTPS_PROXY、NO_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,再根据是否配置代理给出脱敏建议。排查顺序如下:
- 运行
status与doctor,确认本地 auth、默认 provider/model、代理 presence;这些命令不测试远端。 - 核对 OpenAI 的受支持国家和地区。该官方页面描述 API 服务,而这里调用的是私有 ChatGPT Codex 兼容面,最终资格仍由服务端决定。
- 让网络或企业管理员确认 CLI 进程的合规公网出口国家/地区;不要用浏览器出口代替 CLI 出口作判断。
- 有代理时检查小写/大写冲突和
NO_PROXY是否让auth.openai.com或chatgpt.com意外直连;无代理且组织要求统一出口时,按管理员提供的值配置HTTPS_PROXY。只配置ALL_PROXY对当前 transport 无效。 - 让浏览器与 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,
})
不要打印 accessToken、refreshToken,也不要把 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 被发往错误 host | issuer/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 或企业代理凭据进入日志/issue | doctor 只输出 presence;不打印 token、account id、代理 URL/值或完整请求头 |
维护时的核对顺序
- 先查 OpenAI 官方认证文档,确认产品支持和账号策略;
- 再查 OpenAI Codex 官方开源仓库,确认官方客户端当前实现;
- 对照 OpenCode 的固定 release/tag,而不是只看
dev分支; - 将私有兼容变化隔离在 auth/transport/protocol 模块,避免污染 DSH provider-neutral seam;
- 用录制的脱敏协议 fixture 和小规模真实账号 smoke test 验证,不把一次成功请求当成稳定合约。