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:

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:

StepRequest
Request a user codePOST https://auth.openai.com/api/accounts/deviceauth/usercode with JSON { "client_id": CLIENT_ID }
Ask the user to confirmOpen https://auth.openai.com/codex/device and enter the user_code
PollPOST https://auth.openai.com/api/accounts/deviceauth/token with device_auth_id and user_code
Not confirmed yetHTTP 403 or 404; wait for the server interval plus a three-second safety margin
Authorization succeedsReceive authorization_code and code_verifier
Exchange for tokensPOST /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:

  1. chatgpt_account_id
  2. "https://api.openai.com/auth".chatgpt_account_id
  3. organizations[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:

  1. first provides a dummy API key so the AI SDK can initialize;
  2. removes the dummy Authorization generated by the AI SDK before sending;
  3. rereads the authentication record for every request;
  4. refreshes when the access token is missing or expired;
  5. coalesces concurrent refreshes through a shared refreshPromise;
  6. injects the real request headers;
  7. 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:

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 each function_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-Id header 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

ModuleExisting capabilityImprovement over copying OpenCode directly
src/constants.tsClient ID, issuer, Codex endpoint, provider ID, originator, and callback portCentralized constants; DSH attribution is distinct from OpenCode
src/auth.tsPKCE, authorization URL, code exchange, refresh, JWT metadata, and credential I/OSeparate credential file; directory mode 0700 and file mode 0600; atomic temporary-file + fsync + rename replacement; never reads Codex CLI or OpenCode credentials
src/transport.tsProxy-aware fetch shared by OAuth, refresh, device flow, and ResponsesPrivate dispatcher; lowercase proxy variables take precedence; redirects rejected; errors do not echo URLs or proxy credentials
CodexAuthManagerRefresh 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.tsinstructions, tools, store:false, streaming, cache key, and encrypted reasoningUnsupported content fails explicitly instead of silently dropping fields; DSH purpose disables reasoning for title generation
src/protocol/sse.tsStandard SSE framing, JSON validation, and an error if the stream ends before a terminal eventDoes not depend on the AI SDK as a black box; maps protocol failures to stable LlmError values
src/protocol/replay.tsVersioned replay state, output indexes, and reasoning/text/tool mappingsValidates provider, model, ordering, duplicate indexes, and tool-call IDs to reduce invalid or cross-route replay
src/login.ts / src/bin.ts / src/doctor.tsBrowser, headless, status, doctor, and logout CLICallback binds only to 127.0.0.1; bounded five- and 15-minute waits; doctor performs only redacted, offline static diagnostics
src/protocol/translate.tsResponses text, reasoning, tool, usage, finish, and error eventsStrictly preserves DSH block ordering and usage -> finish ordering; verifies that final content matches streamed deltas
src/adapter.tsFixed-backend transport, headers, 401 recovery, and model metadataNo endpoint setting; abort and idle timeout; only one refresh-and-retry after 401; distinct quota/context/server errors
src/index.ts / cordis.patch.ymlRegisters the codex-subscription routeRegisters 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

  1. dsh-codex-subs login runs browser PKCE; --headless runs a device flow capped at 15 minutes.
  2. OAuth code/token, refresh, and device requests use one transport; the browser authorization page itself uses the external browser's network stack.
  3. OAuth tokens are written atomically to a separate auth file; CodexAuthManager refreshes before expiry and preserves rotated tokens.
  4. The Cordis plugin registers CodexSubscriptionAdapter as codex-subscription without changing the default model.
  5. The user explicitly selects a provider/model in settings.yaml; the headless runtime reads the effective selection before creating an agent.
  6. The serializer creates a stateless Responses body; the transport injects the Bearer token and account header only for the fixed Codex endpoint.
  7. The first 401, before any SSE is consumed, forces a refresh and one retry.
  8. 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:

  1. dsh --profile headless --dump-config runs 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.
  2. dsh-codex-subs doctor directly 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.
  3. dsh --profile headless "Reply with codex-subscription-ok only." is the runtime closure test. The headless runner calls currentSelection(), 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_tokens when the caller supplies maxTokens. 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-plugin as its originator and must not impersonate codex_cli_rs or opencode.
  • 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_PROXY is unset, Undici falls back to HTTP_PROXY for HTTPS requests;
  • hosts matched by NO_PROXY bypass the proxy;
  • ALL_PROXY is neither used by the current transport nor counted in doctor's proxy-presence result;
  • redirects are always set to error so 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:

  1. Run status and doctor to confirm local auth, the default provider/model, and proxy presence. These commands do not test the remote service.
  2. 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.
  3. 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.
  4. With a proxy, check lowercase/uppercase conflicts and whether NO_PROXY makes auth.openai.com or chatgpt.com connect directly by accident. Without a proxy, if the organization requires centralized egress, configure HTTPS_PROXY using the administrator-provided value. Setting only ALL_PROXY has no effect on the current transport.
  5. 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_CONTENT before 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

RiskImpactMitigation
Private endpoint/header/client ID changesSign-in or requests suddenly stop workingPin compatibility tests; identify the failing stage in errors; compare both official Codex and OpenCode when upgrading
Subscription quota, model, or workspace permissions change429, quota, model-unavailable, or forbidden errorsTreat the server as the source of truth; preserve structured status and request IDs; never claim unlimited quota
Refresh-token rotation raceInvalid credentials and repeated sign-inSeparate auth file, in-process single-flight, and atomic persistence; add a cross-process lock when needed
Callback listens too broadly or state is not validatedCode disclosure or login CSRFBind only to loopback; random state; one-shot callback; short timeout
Unbounded device-code pollingZombie processes and unbounded requestsOverall timeout, cancellation signal, and respect for the interval
URL rewriting is too broadToken sent to the wrong hostIssuer/backend allowlist, HTTPS, and refusal to follow redirects to an unexpected origin
SSE ends midwayTruncated answer treated as successAccept only terminal responses; throw STREAM_CLOSED for an incomplete stream
Replay state is corrupt or reused across routesTool mismatch or context contaminationValidate version, provider/model, output index, and call ID
Token disclosureAccount and workspace risk0600 permissions, log redaction, never upload or commit, and exact logout deletion
Upstream model allowlist becomes staleLocal catalog differs from actual entitlementCatalog is advisory only; the server response is final
Browser and CLI use different proxy egressSign-in succeeds but token/model requests are rejected by regional policyUse one legal egress path; check proxy-variable casing and NO_PROXY; retain only a redacted request ID
Diagnostics disclose secretsTokens or enterprise proxy credentials enter logs or issuesDoctor reports presence only; never print tokens, account IDs, proxy URLs/values, or full request headers

Maintenance Checklist

  1. Start with official OpenAI authentication documentation to confirm product support and account policy.
  2. Then inspect the official OpenAI Codex open-source repository for the current official-client implementation.
  3. Compare against a pinned OpenCode release or tag, not only the dev branch.
  4. Isolate changes to the private compatibility surface inside auth, transport, and protocol modules so they do not contaminate DSH's provider-neutral seam.
  5. Validate with recorded, redacted protocol fixtures and small real-account smoke tests; never treat one successful request as a stable contract.