How OpenCode Uses a Codex Subscription and How the DSH Adapter Works
August 14, 2026 · View on GitHub
English | 中文
This document answers two questions: how OpenCode v1.18.18 actually invokes models through a ChatGPT/Codex subscription, and how dsh-codex-subs-plugin adapts that path to DeepSeek Harness. The OpenCode path described here is pinned to v1.18.18, so changes on a rolling branch are not presented as stable facts.
Warning
The OAuth and device-code details under auth.openai.com, ChatGPT-Account-Id, the public client ID, and chatgpt.com/backend-api/codex can all be found in OpenAI's official open-source Codex implementation. However, official OpenAI documentation does not define them as a stable API contract for third parties. OpenCode and this project depend on a private compatibility surface that may change with the service or official clients.
A ChatGPT Plus, Pro, Business, or Enterprise subscription does not imply unlimited usage. Model availability, rate limits, usage caps, workspace policy, and risk controls are determined by the service. OpenCode sets locally displayed prices to zero; that only means it does not estimate this subscription traffic using Platform API token prices.
Conclusion
OpenCode does not “convert” a ChatGPT subscription into an API key. It reuses the Codex client's OAuth login, obtains the user's access_token, refresh_token, and ChatGPT account/workspace ID, then redirects the Responses API request produced by the AI SDK to the ChatGPT Codex backend:
ChatGPT OAuth (browser PKCE or device code)
-> access token + refresh token + account id
-> @ai-sdk/openai produces a Responses request
-> OpenCode custom fetch removes the dummy API key
-> injects Bearer token and ChatGPT-Account-Id
-> rewrites to https://chatgpt.com/backend-api/codex/responses
-> Responses SSE
-> AI SDK fullStream
-> OpenCode internal LLM events
sequenceDiagram
actor User
participant Client as OpenCode
participant Auth as auth.openai.com
participant SDK as @ai-sdk/openai
participant Codex as ChatGPT Codex backend
User->>Client: Choose ChatGPT Pro/Plus sign-in
Client->>Auth: OAuth authorize + PKCE, or request a device code
Auth-->>Client: authorization code
Client->>Auth: POST /oauth/token
Auth-->>Client: access / refresh / id token
Client->>Client: Extract account id and persist credentials
Client->>SDK: streamText + Responses model
SDK->>Client: /v1/responses request
Client->>Codex: Bearer + ChatGPT-Account-Id + rewritten request
Codex-->>SDK: Responses SSE
SDK-->>Client: fullStream events
Client-->>User: Text, reasoning, tool calls, usage, and finish events
Evidence Levels and the Official Boundary
1. What official OpenAI documentation guarantees
Official OpenAI authentication documentation explicitly distinguishes:
- “Sign in with ChatGPT” for subscription access;
- API keys for usage-based Platform API access;
- ChatGPT sign-in opening a browser and returning credentials to Codex;
- ChatGPT sign-in following the permissions, RBAC, and data policies of the associated ChatGPT workspace;
- cached sign-in credentials and automatic token refresh by Codex before expiry.
That documentation does not publicly promise that third parties may use a particular OAuth client ID, device-code endpoint, ChatGPT-Account-Id header, or chatgpt.com/backend-api/codex. These details therefore must not be described as an “OpenAI public subscription API.”
2. What OpenAI's official open-source Codex implementation proves
The official OpenAI Codex repository shows how the official client itself is implemented:
- Browser OAuth, PKCE, local callback, and code exchange
- Device-code request, polling, and an overall timeout of about 15 minutes
- ChatGPT sign-in using the Codex backend, while API-key sign-in uses the Platform API
- Bearer token and
ChatGPT-Account-ID - Refresh through
https://auth.openai.com/oauth/token - Token claims and the account-ID data structure
This is strong implementation evidence, but it remains client source code rather than a third-party compatibility commitment. OpenCode's approach can be described as “following the official Codex client implementation,” not as “calling a published, long-term stable subscription API.”
3. Evidence pinned to an OpenCode release
This document checks OpenCode v1.18.18 line by line. The primary file is packages/opencode/src/plugin/openai/codex.ts. It is loaded directly by the built-in plugin list, so users do not install a separate npm plugin. That release pins @ai-sdk/openai@3.0.84.
The Actual OpenCode v1.18.18 Implementation
1. Fixed constants
codex.ts#L10-L16 defines:
CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
ISSUER = "https://auth.openai.com"
CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
OAUTH_PORT = 1455
This is an OAuth public client. A client secret is not, and must not be, embedded in the client. The public client relies on PKCE so an intercepted authorization code cannot be exchanged directly.
2. Browser OAuth + PKCE
OpenCode generates 43 RFC 3986 unreserved characters as the verifier, then derives the challenge using SHA-256 and base64url; see codex.ts#L23-L35. The authorization request is built in 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
A local HTTP server receives /auth/callback, validates code and state, and times out after five minutes; see codex.ts#L154-L260. It then calls:
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>
The source for code exchange and refresh is codex.ts#L107-L139.
Security difference: the older v1.18.18 plugin calls server.listen(1455) without explicitly restricting the listening host. Core V2 in the same release already uses server.listen(1455, "localhost"). An independent implementation should bind explicitly to loopback and continue validating OAuth state.
3. Headless device-code sign-in
codex.ts#L460-L542 implements sign-in without a local browser:
| Step | Request |
|---|---|
| Request a user code | POST https://auth.openai.com/api/accounts/deviceauth/usercode with JSON { "client_id": CLIENT_ID } |
| Ask the user to confirm | Open https://auth.openai.com/codex/device and enter the user_code |
| Poll | POST https://auth.openai.com/api/accounts/deviceauth/token with device_auth_id and user_code |
| Not confirmed yet | HTTP 403 or 404; wait for the server interval plus a three-second safety margin |
| Authorization succeeds | Receive authorization_code and code_verifier |
| Exchange for tokens | POST /oauth/token with redirect URI https://auth.openai.com/deviceauth/callback |
OpenCode v1.18.18 polling has no overall timeout. Current official OpenAI Codex source has a limit of about 15 minutes. A DSH implementation should use bounded waiting and support AbortSignal.
4. Tokens, account ID, and persistence
The token response contains access_token, refresh_token, id_token, and an optional expires_in. OpenCode only decodes the JWT payload to extract the account ID; it does not verify the signature locally. It relies on the TLS-protected OpenAI token endpoint and uses the claims only as routing metadata.
The extraction precedence in codex.ts#L38-L76 is:
chatgpt_account_id"https://api.openai.com/auth".chatgpt_account_idorganizations[0].id
OpenCode stores this OAuth record:
{
type: "oauth",
access: string,
refresh: string,
expires: number,
accountId?: string
}
Credentials are written to opencode/auth.json under the XDG data directory with file mode 0600; see auth/index.ts#L8-L20 and auth/index.ts#L58-L80. After a refresh, a rotated refresh token is persisted. If the new token has no account ID, the old value is retained.
5. Provider and request rewriting
OpenCode's OpenAI provider always calls sdk.responses(modelID); see provider.ts#L202-L209. The OAuth loader then returns a custom fetch that:
- first provides a dummy API key so the AI SDK can initialize;
- removes the dummy
Authorizationgenerated by the AI SDK before sending; - rereads the authentication record for every request;
- refreshes when the access token is missing or expired;
- coalesces concurrent refreshes through a shared
refreshPromise; - injects the real request headers;
- rewrites Responses or compatible chat-completions paths to the Codex backend.
The key logic is in 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>
If the original URL pathname contains /v1/responses or /chat/completions, the target becomes:
https://chatgpt.com/backend-api/codex/responses
HTTP header names are case-insensitive. The OpenAI Codex Rust source spells the header ChatGPT-Account-ID, while OpenCode writes ChatGPT-Account-Id.
OpenCode currently refreshes proactively only according to the local expires value. Its HTTP fetch layer does not fully implement a forced refresh and retry after one 401. A robust adapter should retry only once, and only after confirming that the request can be replayed safely.
6. Responses request body
During OAuth use, the system prompt goes in Responses instructions instead of being prepended again as an ordinary system message; see request.ts#L56-L112. OpenAI function tools are forced to strict: false; see request.ts#L148-L158.
A typical wire body is:
{
"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"
}
}
Transformation evidence:
store: false:transform.ts#L1173-L1182- Session as the prompt cache key:
transform.ts#L1262-L1274 - Default reasoning, summary, and encrypted reasoning:
transform.ts#L1292-L1305 - Removing Responses item IDs when
store !== true:transform.ts#L502-L516 - Finally clearing
maxOutputTokensto match Codex CLI:codex.ts#L549-L563
Model IDs and supported reasoning efforts change over time. The local v1.18.18 allowlist must not be treated as the source of truth for server-side entitlement.
7. SSE response adaptation
codex.ts does not parse Responses SSE directly. OpenCode calls the AI SDK's streamText() and then consumes result.fullStream; see llm.ts#L271-L378. The actual two-stage adaptation is:
Responses SSE
-> @ai-sdk/openai@3.0.84
-> AI SDK fullStream
-> OpenCode LLMAISDK.toLLMEvents()
-> text / reasoning / tool / usage / finish / error
OpenCode's event mapping in ai-sdk.ts#L76-L285 covers:
- text start/delta/end;
- reasoning start/delta/end;
- tool input start/delta/end;
- tool call/result/error;
- step start/finish and usage;
- finish, abort, and error.
Common lower-level events include response.created, response.in_progress, response.output_text.delta, response.function_call_arguments.delta, response.output_item.done, and response.completed. A call should be considered complete only after an explicit terminal event.
8. Why stateless replay is required
OpenCode sends store: false, so it cannot rely only on previous_response_id to recover the full context on the server. Later turns must replay the minimum native information from previous output:
- encrypted reasoning items;
- reasoning summaries;
- assistant output text;
- the
call_id, name, and original arguments of eachfunction_call; - the corresponding
function_call_output.
reasoning.encrypted_content is the key to preserving reasoning continuity without storing plaintext hidden reasoning locally. Replay must preserve output order, tool-call IDs, and provider/model provenance. Replay state from another provider, or otherwise untrusted replay state, must not be sent directly to the Codex endpoint.
The Gap in OpenCode Core V2
The same v1.18.18 tag also contains a newer Effect/Core V2 implementation: packages/core/src/plugin/provider/openai.ts. It already has:
- browser PKCE with an explicit localhost bind;
- headless device-code sign-in;
- code exchange and refresh;
- account-ID extraction into OAuth credential metadata;
- OpenAI language-model selection through
sdk.responses().
However, two facts required for the older plugin's end-to-end subscription transport are not present under packages/core/src in that tag:
- no endpoint rewrite to
https://chatgpt.com/backend-api/codex; - no
ChatGPT-Account-Idheader injection.
Core V2 in that tag is therefore not, by itself, a complete replacement for the older CodexAuthPlugin. An analysis or port must not assume that OAuth alone closes the subscription call path. The complete working evidence for v1.18.18 remains the older codex.ts.
DSH Adapter Implementation
The current package version in this project is 0.1.0. It is now an end-to-end plugin that builds, signs in, and registers a DSH primary-model route. Its bundle is non-invasive: installation adds a provider without changing the user's existing default model.
Implemented
| Module | Existing capability | Improvement over copying OpenCode directly |
|---|---|---|
src/constants.ts | Client ID, issuer, Codex endpoint, provider ID, originator, and callback port | Centralized constants; DSH attribution is distinct from OpenCode |
src/auth.ts | PKCE, authorization URL, code exchange, refresh, JWT metadata, and credential I/O | Separate credential file; directory mode 0700 and file mode 0600; atomic temporary-file + fsync + rename replacement; never reads Codex CLI or OpenCode credentials |
src/transport.ts | Proxy-aware fetch shared by OAuth, refresh, device flow, and Responses | Private dispatcher; lowercase proxy variables take precedence; redirects rejected; errors do not echo URLs or proxy credentials |
CodexAuthManager | Refresh 60 seconds before expiry, concurrent single-flight, and refreshNow() | Avoids expiry as a request begins and provides an explicit entry point for one forced refresh after a 401 |
src/protocol/serialize.ts | instructions, tools, store:false, streaming, cache key, and encrypted reasoning | Unsupported content fails explicitly instead of silently dropping fields; DSH purpose disables reasoning for title generation |
src/protocol/sse.ts | Standard SSE framing, JSON validation, and an error if the stream ends before a terminal event | Does not depend on the AI SDK as a black box; maps protocol failures to stable LlmError values |
src/protocol/replay.ts | Versioned replay state, output indexes, and reasoning/text/tool mappings | Validates provider, model, ordering, duplicate indexes, and tool-call IDs to reduce invalid or cross-route replay |
src/login.ts / src/bin.ts / src/doctor.ts | Browser, headless, status, doctor, and logout CLI | Callback binds only to 127.0.0.1; bounded five- and 15-minute waits; doctor performs only redacted, offline static diagnostics |
src/protocol/translate.ts | Responses text, reasoning, tool, usage, finish, and error events | Strictly preserves DSH block ordering and usage -> finish ordering; verifies that final content matches streamed deltas |
src/adapter.ts | Fixed-backend transport, headers, 401 recovery, and model metadata | No endpoint setting; abort and idle timeout; only one refresh-and-retry after 401; distinct quota/context/server errors |
src/index.ts / cordis.patch.yml | Registers the codex-subscription route | Registers only the provider and does not override agent-default-model; the advisory catalog is not an entitlement allowlist |
The default credential location is:
$DSH_CODEX_SUBS_AUTH_FILE
or $DSH_HOME/codex-subs/auth.json
or ~/.dsh/codex-subs/auth.json
These credentials belong only to this plugin. Do not try to “skip sign-in” by scanning or copying ~/.codex/auth.json or an OpenCode auth file. Sharing refresh tokens creates rotation races, unexpected sign-outs, and confused permission boundaries.
End-to-end closure
dsh-codex-subs loginruns browser PKCE;--headlessruns a device flow capped at 15 minutes.- OAuth code/token, refresh, and device requests use one transport; the browser authorization page itself uses the external browser's network stack.
- OAuth tokens are written atomically to a separate auth file;
CodexAuthManagerrefreshes before expiry and preserves rotated tokens. - The Cordis plugin registers
CodexSubscriptionAdapterascodex-subscriptionwithout changing the default model. - The user explicitly selects a provider/model in
settings.yaml; the headless runtime reads the effective selection before creating an agent. - The serializer creates a stateless Responses body; the transport injects the Bearer token and account header only for the fixed Codex endpoint.
- The first 401, before any SSE is consumed, forces a refresh and one retry.
- The translator emits DSH blocks, usage, finish, and minimal replay state; on the next turn it validates provenance before restoring encrypted reasoning and tool state.
Tests cover PKCE/state, device flow, token rotation, concurrent refresh, a single retry after 401, the fixed URL, the account header, SSE ordering, tool-argument deltas, truncated streams, encrypted-reasoning replay, max-token projection, and cross-model replay downgrade. Every network interaction is mocked; tests do not read real account credentials.
Non-invasive bundle and three validation layers
After the bundle is installed, cordis.patch.yml only inserts llm-codex-subscription, and apply() only calls registerAdapter(). Existing DeepSeek or other default models are therefore not replaced silently. Using this plugin as the primary-model route requires an explicit agent-default-model selection.
flowchart LR
Dump["--dump-config<br/>Cordis composition layer"] -.->|Proves only composition| Route["codex-subscription<br/>provider registered"]
Settings["settings.yaml<br/>explicit default provider/model"] --> Select["headless currentSelection()"]
Route --> Select
Doctor["status / doctor<br/>local static diagnostics"] -.->|Observes; does not prove live routing| Select
Select --> Adapter["CodexSubscriptionAdapter"]
Adapter --> Backend["Fixed Codex Responses endpoint"]
The three checks answer different questions:
dsh --profile headless --dump-configruns only boot-free Cordis patch composition. It confirms that the bundle row exists, but it neither starts the plugin nor loads the runtime default model from$DSH_HOME/settings.yaml.dsh-codex-subs doctordirectly reads local auth, settings, and environment state. It reports whether credentials exist or are expired, whether an account ID exists, profile installation detection, plugin/provider identity, the declared default provider/model, proxy-variable presence, the Node environment-proxy switch, and the fixed endpoint. It neither refreshes tokens nor sends a network request, and it never prints tokens, the account ID, proxy URLs, or variable values. It therefore proves only local static state.dsh --profile headless "Reply with codex-subscription-ok only."is the runtime closure test. The headless runner callscurrentSelection(), creates an agent with the returned provider/model, and sends a real request. Only a successful response jointly validates routing, OAuth, networking, and remote entitlement, and it consumes subscription usage.
This repository currently has no DSH command that prints the live effective model without sending a request; do not invent one. Troubleshooting should pair the expected static selection from doctor with one fresh, minimal headless request.
DSH differences that require explicit confirmation
- The DSH serializer currently sends
max_output_tokenswhen the caller suppliesmaxTokens. OpenCode v1.18.18 explicitly removes it to match Codex CLI. Before production use, confirm support through a controlled recording rather than assuming the two behaviors are equivalent. - DSH parses Responses SSE directly instead of using
@ai-sdk/openai. This reduces dependencies, but makes event evolution, unknown events, and usage aggregation the adapter's responsibility. - DSH must retain
dsh-codex-subs-pluginas itsoriginatorand must not impersonatecodex_cli_rsoropencode. - JWT claims are account metadata only, not a basis for local authorization. The server response remains the source of truth for entitlement.
- 429, quota, workspace restriction, and model-unavailable errors must remain distinct stable errors rather than being wrapped as generic network failures.
Proxy transport and region errors
src/transport.ts creates a private Undici EnvHttpProxyAgent dispatcher for this plugin and does not modify the process-global dispatcher. OAuth code exchange, refresh, device-code request/polling, and Codex Responses all receive the same fetch implementation, so plugin requests behave consistently on Node 22 and Node 24:
HTTP_PROXY,HTTPS_PROXY,NO_PROXY, and their lowercase variants are read; lowercase takes precedence when both cases are set;- if
HTTPS_PROXYis unset, Undici falls back toHTTP_PROXYfor HTTPS requests; - hosts matched by
NO_PROXYbypass the proxy; ALL_PROXYis neither used by the current transport nor counted in doctor's proxy-presence result;- redirects are always set to
errorso a Bearer token or OAuth code cannot be forwarded to an unexpected origin; - transport errors retain only safe error types/codes and do not include request URLs, proxy URLs, or credentials.
The authorization page opened in the external browser does not pass through the Node transport; the browser or operating system controls its proxy path. “The browser can sign in” therefore does not imply that subsequent CLI token or Responses requests use the same public egress.
Node 24 separately provides the official NODE_USE_ENV_PROXY=1 / --use-env-proxy, which makes Node's global HTTP(S) clients read proxy environment variables. doctor reports this as environmental context. The plugin does not depend on that switch because it always uses its own unified transport. Example:
export HTTPS_PROXY=http://proxy.example:8080
export HTTP_PROXY=http://proxy.example:8080
export NO_PROXY=localhost,127.0.0.1
A proxy URL may contain a username or password, and NO_PROXY may reveal internal domains. doctor may report only whether these variables are present, never their values. Issues, logs, and support tickets should follow the same rule.
The adapter maps the service's unsupported_country_region_territory to the stable DSH error code UNSUPPORTED_COUNTRY_REGION_TERRITORY, then gives redacted guidance based on whether a proxy is configured. Troubleshoot in this order:
- Run
statusanddoctorto confirm local auth, the default provider/model, and proxy presence. These commands do not test the remote service. - Check OpenAI's supported countries and territories. That official page describes API service, while this project calls a private ChatGPT Codex compatibility surface; the service remains the final authority on eligibility.
- Ask a network or enterprise administrator to confirm the CLI process's compliant public egress country or territory. Do not substitute the browser's egress when judging the CLI's path.
- With a proxy, check lowercase/uppercase conflicts and whether
NO_PROXYmakesauth.openai.comorchatgpt.comconnect directly by accident. Without a proxy, if the organization requires centralized egress, configureHTTPS_PROXYusing the administrator-provided value. Setting onlyALL_PROXYhas no effect on the current transport. - Retry a minimal request after the browser and CLI use legal, consistent network paths. Sign in again only if the failing stage is OAuth/device/refresh or credentials have expired. If the problem remains, retain the failure stage, HTTP status, and request ID; remove tokens, request headers, and proxy URLs before contacting OpenAI support or a workspace administrator.
Do not try to “fix” a server rejection by forging a client ID, endpoint, or account ID, copying another user's credentials, or evading regional controls.
Current limitations
- Only HTTP + SSE is implemented; OpenCode's experimental Responses WebSocket pool has not been ported.
- The current DSH catalog explicitly declares text-only support; image input fails with
UNSUPPORTED_CONTENTbefore a request is sent. - Function tools use
strict:false, but OpenCode's cleanup for incompatible JSON Schema keywords has not been ported. - All automated tests use mock OAuth and Responses data; this repository does not run online smoke tests with a user's real subscription.
- Refresh single-flight is process-local. Two processes sharing one auth file can still race on token rotation; production hardening requires a cross-process lock.
Usage Examples
Auth API
src/auth.ts can independently parse and refresh existing credentials. After building, the public dsh-codex-subs-plugin/auth entry point can be used as follows:
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,
})
Never print accessToken or refreshToken, and never commit the auth file.
End-to-end usage
First install the plugin from its checkout into the headless profile:
pnpm install
pnpm run check
dsh plugin --profile headless add .
Use this command to confirm that llm-codex-subscription has entered the Cordis composition:
dsh --profile headless --dump-config
This step does not read settings.yaml and is not a runtime-default-model check. Next, sign in:
dsh plugin --profile headless exec dsh-codex-subs login
The sign-in command opens ChatGPT browser authorization. In an environment without a browser, use:
dsh plugin --profile headless exec dsh-codex-subs login --headless
Then explicitly select the provider in $DSH_HOME/settings.yaml. If DSH_HOME is not set, the path is ~/.dsh/settings.yaml. The example model ID reflects only this document's snapshot; use a model available to the account at that time:
agent-default-model:
provider: codex-subscription
model: gpt-5.5
reasoningEffort: medium
Check local status and static diagnostics:
dsh plugin --profile headless exec dsh-codex-subs status
dsh plugin --profile headless exec dsh-codex-subs doctor
status displays local credential metadata. doctor neither refreshes nor makes network requests and displays only redacted, allowlisted fields. Neither command proves remote entitlement or live routing. Finally, send a minimal real request:
dsh --profile headless "Reply with codex-subscription-ok only."
Only a successful response closes the settings -> currentSelection -> adapter -> Codex backend path, and it consumes subscription quota. When the plugin is no longer needed:
dsh plugin --profile headless exec dsh-codex-subs logout
logout deletes only this plugin's credential file. It does not affect Codex CLI or OpenCode sign-in and does not call a remote revoke endpoint.
Risk Register
| Risk | Impact | Mitigation |
|---|---|---|
| Private endpoint/header/client ID changes | Sign-in or requests suddenly stop working | Pin compatibility tests; identify the failing stage in errors; compare both official Codex and OpenCode when upgrading |
| Subscription quota, model, or workspace permissions change | 429, quota, model-unavailable, or forbidden errors | Treat the server as the source of truth; preserve structured status and request IDs; never claim unlimited quota |
| Refresh-token rotation race | Invalid credentials and repeated sign-in | Separate auth file, in-process single-flight, and atomic persistence; add a cross-process lock when needed |
Callback listens too broadly or state is not validated | Code disclosure or login CSRF | Bind only to loopback; random state; one-shot callback; short timeout |
| Unbounded device-code polling | Zombie processes and unbounded requests | Overall timeout, cancellation signal, and respect for the interval |
| URL rewriting is too broad | Token sent to the wrong host | Issuer/backend allowlist, HTTPS, and refusal to follow redirects to an unexpected origin |
| SSE ends midway | Truncated answer treated as success | Accept only terminal responses; throw STREAM_CLOSED for an incomplete stream |
| Replay state is corrupt or reused across routes | Tool mismatch or context contamination | Validate version, provider/model, output index, and call ID |
| Token disclosure | Account and workspace risk | 0600 permissions, log redaction, never upload or commit, and exact logout deletion |
| Upstream model allowlist becomes stale | Local catalog differs from actual entitlement | Catalog is advisory only; the server response is final |
| Browser and CLI use different proxy egress | Sign-in succeeds but token/model requests are rejected by regional policy | Use one legal egress path; check proxy-variable casing and NO_PROXY; retain only a redacted request ID |
| Diagnostics disclose secrets | Tokens or enterprise proxy credentials enter logs or issues | Doctor reports presence only; never print tokens, account IDs, proxy URLs/values, or full request headers |
Maintenance Checklist
- Start with official OpenAI authentication documentation to confirm product support and account policy.
- Then inspect the official OpenAI Codex open-source repository for the current official-client implementation.
- Compare against a pinned OpenCode release or tag, not only the
devbranch. - Isolate changes to the private compatibility surface inside auth, transport, and protocol modules so they do not contaminate DSH's provider-neutral seam.
- Validate with recorded, redacted protocol fixtures and small real-account smoke tests; never treat one successful request as a stable contract.