Run GPT models such as GPT-6 Luna inside DeepSeek Harness (dsh) on your ChatGPT subscription instead of an OpenAI API key.
Unofficial workaround. dsh already ships the
openai-codexprovider but has no sign-in UI for it yet, so this guide writes the OAuth credential by hand. It is not affiliated with DeepSeek or OpenAI. Use it at your own risk and within the terms of your ChatGPT plan. Once the dsh Models page supports OAuth sign-in, use that instead.
Verified on 2026-09-23 with dsh 0.1.7-alpha.1 (pi-ai 0.85.1) on macOS, with GPT-6 Luna at xhigh as the default model.
- dsh bundles pi-ai's
openai-codexprovider, which authenticates with ChatGPT subscription OAuth. The login flow exists in the code, but no UI or CLI starts it: the Models page states that providers signing in with OAuth, such as Codex, are not supported yet. - Copy the OAuth tokens that Codex CLI already holds into the dsh credential file as a
grantrecord, then add anopenai-codexroute to your profile'scordis.patch.yml. - The bundled model catalog (pi-ai 0.85.1) stops at GPT-6 Astra, so declare newer models such as
gpt-6-lunayourself undermodels:. - Never set
apiKeyEnvorapion this route. Either one takes the route off the OAuth path.
- pi-ai's
openai-codexprovider is OAuth-only. It sends requests with theopenai-codex-responsesprotocol tohttps://chatgpt.com/backend-api. - dsh reads the credential from its credential store,
$DSH_HOME/.credentials.yaml(default~/.dsh/.credentials.yaml), under the record keyllm-pi-ai/openai-codex.llm-pi-aiis the plugin that owns the record andopenai-codexis the provider id. - Codex CLI and pi-ai use the same OAuth client, so pi-ai can use, and refresh, tokens that Codex CLI obtained. The ChatGPT account id is read from the access token (a JWT) on every request.
- Once
expireshas passed, pi-ai refreshes the token and writes the new one back into the same record.
- dsh installed and started at least once, so
$DSH_HOMEexists. - Codex CLI signed in with
codex login, so~/.codex/auth.jsonexists. - Python 3 for
dsh-codex-sync.py. It uses only the standard library.
The paths below use $DSH_HOME (default ~/.dsh) and <profile>, your dsh profile name. ls ~/.dsh/profiles shows it, for example web.
DSH_HOME=${DSH_HOME:-$HOME/.dsh}; PROFILE=web # your profile name
TS=$(date +%Y%m%d-%H%M%S)
cp "$DSH_HOME/.credentials.yaml" "$DSH_HOME/.credentials.yaml.bak.$TS"
cp "$DSH_HOME/profiles/$PROFILE/cordis.patch.yml" "$DSH_HOME/profiles/$PROFILE/cordis.patch.yml.bak.$TS"The record looks like this. dsh refuses to load the file unless only its owner can read it (chmod 600).
version: 1
records:
llm-pi-ai/openai-codex:
kind: grant
payload:
type: oauth
access: "<access_token>"
refresh: "<refresh_token>"
expires: <epoch_ms> # access-token expiry in epoch milliseconds
accountId: "<account_id>"| Payload field | Value in ~/.codex/auth.json |
|---|---|
access |
tokens.access_token |
refresh |
tokens.refresh_token |
expires |
the access token's JWT exp claim × 1000 |
accountId |
tokens.account_id |
Instead of copying the values by hand, run the sync script:
python3 dsh-codex-sync.pyIt replaces any existing llm-pi-ai/openai-codex record, leaves every other record and refs untouched, and keeps the file readable by its owner only. It honors DSH_HOME and CODEX_HOME. dsh reloads the file automatically, so you do not need to stop it.
In $DSH_HOME/profiles/<profile>/cordis.patch.yml, add openai-codex under providers in the llm-pi-ai entry. If the entry already exists, which it does once you have saved any provider on the Models page, add to it rather than writing a second one: an override replaces the whole entry config, so keep the providers that are already there.
- id: llm-pi-ai
name: "@deepseek-ai/dsh-llm-pi-ai"
config:
providers:
openai-codex:
displayName: ChatGPT (Codex)
reasoning: xhigh # effort used while a session has picked none
models:
- id: gpt-6-luna
name: GPT-6 Luna
contextWindow: 272000
maxTokens: 128000
input: [text, image]
reasoningEfforts: # key = level shown in the picker, value = value sent on the wire
low: low
medium: medium
high: high
xhigh: xhigh
max: max
# list gpt-6-astra, gpt-6-sol, gpt-5.6-* and gpt-5.5 the same waymodels:replaces the bundled catalog instead of extending it. Only the models you list appear in the picker.- Leave out
apiandbaseURL. Listed models inheritopenai-codex-responsesandhttps://chatgpt.com/backend-apifrom the catalog route. - A model that the bundled catalog does not know gets no Effort menu unless you declare
reasoningEfforts.
Selecting a model in the picker also saves it as the default for new sessions. To set the default in the file instead:
- id: agent-default-model
name: "@deepseek-ai/dsh-agent-default-model"
config:
provider: openai-codex
model: gpt-6-luna
reasoningEffort: xhighTo let subagents use it too, add an entry to allowedModels and keep your existing ones:
- id: subagent-model-selection-settings
name: "@deepseek-ai/dsh-tool-subagent/model-selection-settings"
config:
enabled: true
allowedModels:
- provider: openai-codex
model: gpt-6-luna
# ...your existing entriesAccording to the dsh documentation, changes to cordis.patch.yml and .credentials.yaml take effect on the next request. If "ChatGPT (Codex)" does not appear in the model picker, restart dsh. Pick GPT-6 Luna and send a message. A reply means you are done.
~/.codex/models_cache.json lists the models that the Codex backend currently serves. Codex CLI fetches it, and it is newer than the catalog bundled with dsh. The table shows its visibility: list entries as of 2026-09-22. Yours may differ by plan and date.
| id | Name | Efforts |
|---|---|---|
gpt-6-luna |
GPT-6 Luna | low / medium / high / xhigh / max |
gpt-6-astra |
GPT-6 Astra | low / medium / high / xhigh / max |
gpt-6-sol |
GPT-6 Sol | low / medium / high / xhigh / max |
gpt-5.6-luna |
GPT-5.6 Luna | low / medium / high / xhigh / max |
gpt-5.6-terra |
GPT-5.6 Terra | low / medium / high / xhigh / max |
gpt-5.6-sol |
GPT-5.6 Sol | low / medium / high / xhigh / max |
gpt-5.5 |
GPT-5.5 | low / medium / high / xhigh |
contextWindow follows the backend's context_window (272,000) and maxTokens follows the bundled catalog (128,000). All of them take text and image input.
To see what your own account serves:
python3 -c "
import json, os
home = os.environ.get('CODEX_HOME') or os.path.expanduser('~/.codex')
d = json.load(open(os.path.join(home, 'models_cache.json')))
print('fetched_at', d['fetched_at'])
for m in d['models']:
if m.get('visibility') == 'list':
print(m['slug'], m['context_window'], [e['effort'] for e in m['supported_reasoning_levels']])
"- Do not set
apiKeyEnv. dsh would resolve that reference as an API key and send it ahead of the OAuth token. A reference that resolves to nothing fails withMISSING_CREDENTIAL. - Do not set
api. The config schema rejectsopenai-codex-responses, and any other protocol, such asopenai-responses, stops dsh from reusing the catalog provider, so requests no longer go through the Codex-specific implementation. - No
ultraeffort. The backend offersultraon Sol, Astra and Terra, but pi-ai's levels stop atoff / minimal / low / medium / high / xhigh / max. - Fast mode (
service_tier: priority) has no effect. pi-ai can send aserviceTier, but the dshllm-pi-aiadapter does not pass one in itsstreamSimplecall. - Shared tokens get invalidated. When the access token expires, whichever of dsh and Codex CLI refreshes first receives a new refresh token, and the other one's copy can stop working. Copying tokens from Codex CLI puts you in exactly this situation. The next section avoids it.
Signing in with pi-ai's own CLI creates an OAuth session separate from Codex CLI's, so the two never compete for a refresh token. The CLI writes auth.json to the current directory as {"openai-codex": {type, access, refresh, expires, accountId}}, which is the same shape as the grant payload, and dsh-codex-sync.py accepts that file. The CLI starts and its list command shows openai-codex, but the sign-in itself has not been tested yet.
D=$(mktemp -d) && cd "$D"
npx @earendil-works/pi-ai@0.85.1 login openai-codex # 1 = browser, 2 = device code
python3 /path/to/dsh-codex-sync.py "$D/auth.json"
cd - && rm -rf "$D"In a dsh source checkout you can run packages/llm/llm-pi-ai/node_modules/.bin/pi-ai instead of npx. After this, dsh and Codex CLI each refresh their own tokens.
| Symptom | Cause and fix |
|---|---|
Provider is not configured: openai-codex (PI_AI_ERROR) |
dsh cannot read the grant record. Check the key llm-pi-ai/openai-codex, the indentation, and chmod 600. |
OpenAI Codex token refresh failed (…) |
The refresh token is no longer valid. Run codex login and rerun the sync script, or redo the separate sign-in. |
MISSING_CREDENTIAL |
The route has an apiKeyEnv. Remove it. |
UNKNOWN_MODEL |
The default model's id is not listed under models:. |
| A model is missing from the picker | Only models listed under models: appear. Check the YAML indentation and restart dsh. |
| No Effort menu | The model has no reasoningEfforts. |
- Delete the
llm-pi-ai/openai-codexrecord from$DSH_HOME/.credentials.yaml. dsh reloads the file automatically. - Remove the
openai-codexroute fromcordis.patch.ymland restoreagent-default-modelandallowedModels, or restore the file from its*.bak.<timestamp>backup.
@deepseek-ai/dsh-subagent-codex runs genuine Codex sessions as child processes and uses Codex CLI's own sign-in. Your conversation model stays the same, and only delegated tasks run on Codex. This path is officially supported.
Links point at the dsh dsh-v0.1.7-alpha.1 tag.
- Configure models: the Models page does not support OAuth providers yet;
reasoningEffortsandreasoning dsh-llm-pi-aiREADME: route configuration,modelsversusmodelOverridesdsh-credentials-localREADME: the.credentials.yamlformat and permissionsprovider.ts:routeAuth, whatapiKeyEnvadds to an OAuth-only routecatalog.ts:resolveRouteModels, how listed models inheritapiandbaseUrl- Credential records and authorization flows: the design note behind the credential store and the unused login flow
@earendil-works/pi-ai: the Codex OAuth implementation isdist/auth/oauth/openai-codex.js