This repository adds DeepSeek V4 Flash and Pro to the stock ChatGPT / Codex app through a loopback router.
If a user asks you to install it, follow the "Install by a coding agent" section in README.md.
The non-negotiable details:
- Never print or commit the DeepSeek API key. Persist it with
node src/cli.mjs key set(DEEPSEEK_API_KEYenv or the hidden prompt); it is stored at~/.codex/dscodex/config.jsonwith mode 0600 (DPAPI-encrypted on Windows) and survives logout/reboot. Resolution order at runtime:DEEPSEEK_API_KEYenv (one-off override), then the stored file, then the legacy macOS login-session value. Never store the key in~/.codex/config.toml;uninstalldeletes the stored key file. - Run
node src/cli.mjs install, thennode src/cli.mjs start, thennode src/cli.mjs doctor. Doctor must reportokfor config, catalog, router token, proxy, key, and the app-server bridge state. The app-server bridge is opt-in (node src/cli.mjs bridge enable) because a globalCODEX_CLI_PATHdemotes the app from its local daemon websocket (which supports reconnect) to stdio and breaks Computer Use;installtherefore never sets it and actively removes DSCodex-owned copies left by older versions, including anyCODEX_CLI_PATHthe Codex app snapshotted into[mcp_servers.*.env]. When enabled,bridge enablemust refuse a user-ownedCODEX_CLI_PATH; the variable must point at the generated shim~/.codex/dscodex/codex-cli-bridge.sh, never directly atsrc/codex-wrapper.mjs: GUI apps get a bare launchd PATH without Homebrew, so a#!/usr/bin/env nodeshebang fails there. The shim resolves node from PATH at runtime and only falls back to the absolute path baked at install time. The wrapper must resolve the stock Codex binary through the shared fallback chain (src/real-codex.mjs) instead of exiting whenDSCODEX_REAL_CODEXis missing: launchctl login variables do not survive reboots, and a staleCODEX_CLI_PATHmust degrade to stock Codex, not hard-fail every spawn. The bridge is macOS-only: Windows desktop apps spawnCODEX_CLI_PATHdirectly and cannot run a script shim (CreateProcess requires an.exe), so on Windows the bridge is unavailable and thedoctorbridge check passes trivially. - Run
npm test; all tests must pass. - The ChatGPT desktop app must be fully quit (
⌘Q) and relaunched, and the user must start a NEW task to see🐳 V4 Flashand🐳 V4 Pro. Existing tasks keep their old model state. - Verify each model with a real tool loop using
codex -m deepseek/deepseek-v4-flash -c 'model_reasoning_effort="max"' -a never exec --skip-git-repo-check 'call a shell tool exactly once …'and the same command withdeepseek/deepseek-v4-pro. - Do not edit
~/.codex/config.tomlby hand unless the user asks; the CLI owns its two marker-owned root keys. GUI-writtenmodel/model_reasoning_effortlines are user-owned and must be preserved. - Provider selection memory lives in
~/.codex/dscodex/model-selections.json. OpenAI and DeepSeek have separate reasoning-effort slots; only OpenAI owns the saved service tier. The file also persists per-thread provider memory (bounded, last 500 threads) so resumed threads switch like live ones, and astaleEffortmarker so a model-only config write never leaks the other provider's effort into a new session. On load, the bridge must reconcile its remembered active provider with the liveconfig.tomlmodel (the GUI keeps switching models while the bridge is off) and adopt that config's effort/tier into the matching slots; otherwise the next cross-provider switch is misclassified as an in-provider effort change and the carried effort overwrites the family slot instead of restoring it. The wrapper must forward all other app-server JSONL RPC unchanged to the stock Codex binary. - DeepSeek V4 Flash and Pro are text-only. The router rewrites
input_imageparts in DeepSeek-bound bodies (both message content andview_imagefunction_call_outputresults) into text descriptions generated by GPT throughchatgpt.comusing the incoming request's OAuth headers (default modelgpt-5.6-sol, override withDSCODEX_VISION_MODEL; sha256-cached per router process; concurrent per request). Without anauthorizationheader the body passes through untouched. Both DeepSeek catalog entries must keepinput_modalities = ["text", "image"]or the desktopview_imagegate rejects calls before images ever reach the router. Never route images toapi.deepseek.comunchanged expecting vision — DeepSeek accepts the parts but cannot see them. - Autostart is opt-in:
node src/cli.mjs autostart enable(launchdcom.dscodex.routeron macOS, systemd user servicedscodex.serviceon Linux, Task SchedulerDSCodexplus a hidden wscript shim on Windows). The generated plist/unit/VBS must never embed the DeepSeek API key — the router resolves it from the stored key file at runtime. KeepAlive/Restart only cover crashes: the Windows VBS must wait for a hidden supervisor that restarts nonzero router exits, with Task Scheduler restart settings as a second fallback; registration must finish before replacing a healthy manual router, and a failed handoff must restore that router.stopuses the authenticated shutdown endpoint, the router exits 0 gracefully, and it must stay down. Theserveprocess owns~/.codex/dscodex/server.pidno matter who launched it, sostopworks for autostarted instances too.uninstallmust disable autostart and delete the generated artifacts. - The router must reach chatgpt.com for GPT passthrough and GPT vision. Node's fetch ignores
proxy environment variables by default, so DSCodex resolves a proxy itself — order:
DSCODEX_HTTPS_PROXY/DSCODEX_HTTP_PROXY, then standard proxy variables (Node gives lowercase names precedence), then the storedproxy_urlwritten bynode src/cli.mjs proxy set <url>in~/.codex/dscodex/config.json— and re-execs itself with Node's--use-env-proxy(requires Node >= 24.5; uppercase and lowercase proxy variables are synchronized, andNO_PROXYalways includes loopback plusapi.deepseek.com). Proxy credentials are redacted in CLI output and DPAPI-protected on Windows; the proxy URL must never be confused with the DeepSeek key, which stays DPAPI/0600-protected and is never printed or committed. installgenerates a 256-bit router token and writes it into the managedopenai_base_url;start/servemust reconcile that marker-owned URL with the persisted token and selected port, anddoctormust verify the exact binding. The proxy must reject requests without that path token.serveowns a 0600 pid-state file with a per-instance shutdown token.stopmay only use the authenticated shutdown endpoint and must atomically preserve replacement-instance state; it must never terminate an unverified or recycled PID. Cap both compressed request bytes and decompressed request bytes before parsing JSON.- DeepSeek does not implement Codex remote compaction v2. For a DeepSeek-bound request containing
compaction_trigger, the router must remove tools and the trigger, ask the same DeepSeek model for a compact handoff summary, and return exactly one syntheticcompactionoutput item beforeresponse.completed. Encrypt the summary with AES-256-GCM using a key derived from the stable router token; on later DeepSeek requests, decrypt only DSCodex-prefixed compaction items and restore them as assistant summary context. A compaction item that cannot be decrypted (GPT-sealed after a provider switch, or a rotated token) must be dropped, never forwarded raw to DeepSeek. Never route compaction through GPT or store the summary as plaintext in the rollout file. - DeepSeek's Responses API is stricter than OpenAI's about replaying tool-call turns, and a
rejected replay wedges the session permanently because the bad shape stays in the history.
Every tool output must directly follow its call — Codex inserts PostToolUse hook context as a
developermessage that can land inside the pair — so the router re-pairs them forfunction_call,custom_tool_call, andlocal_shell_call; the repair must preserve the relative order of every other item and must leave a call whose output is missing where it is. Pair oncall_id, never onid: Codex sends both, butidis a local UUID and onlycall_idcarries thecall_…value the API matches on. A turn must never replay more than one tool call behind a singlereasoningitem (DeepSeek reports a misleading "reasoning_text must be passed back"), so the catalog entry setssupports_parallel_tool_calls = falseand the router forcesparallel_tool_calls: false; duplicating the turn's reasoning for extra calls is replay repair for already-wedged sessions only, and must never copy reasoning across a turn boundary. A turn runs from itsreasoningitem through its calls and includes the assistant preamble message Codex emits in between — real rollouts showreasoning→message(assistant)→ call, call — while a tool output ends it. - Platform differences: routing, key storage, and catalog merging work identically on all
platforms. The app-server bridge is macOS-only and opt-in — it demotes the app from its local
daemon websocket (reconnect-capable) to stdio, which breaks Computer Use.
installnever setsCODEX_CLI_PATHand actively strips DSCodex-owned copies left by older versions, including copies the Codex app snapshotted into[mcp_servers.*.env]. The bridge shim (~/.codex/dscodex/codex-cli-bridge.sh) resolves node from PATH at runtime and falls back to the absolute path baked at install time; the wrapper (src/codex-wrapper.mjs) resolves the stock Codex binary through the shared fallback chain (src/real-codex.mjs) instead of hard-failing whenDSCODEX_REAL_CODEXis missing (launchctl login variables do not survive reboots). On Windows, the bridge is unavailable (CreateProcess requires an.exe, cannot run a script shim), anddoctorpasses trivially. Windows config lives under%USERPROFILE%\\.codex;0600file permissions do not apply on NTFS (DSCodex relies on the user account ACL). Autostart uses the platform-native scheduler on all three OSes (launchd / systemd / Task Scheduler + VBS). - Non-routed client features: Voice, Pets, plugins, skills, and MCP are all client-side and
unaffected by the router. Voice is driven by GPT-Live and is never routed to DeepSeek.
The catalog declares
prefer_websockets = false— the router answers probes with 426, Codex falls back to HTTP/SSE, andcodex doctormay show a warning but requests work fine.