Experimental local adapter that exposes a small OpenAI-compatible HTTP surface and runs each chat completion request through the Codex CLI inside Docker.
The original target client is Obsidian LLM Wiki, but the adapter is intentionally
plain HTTP: it implements /v1/models and /v1/chat/completions well enough
for text-only, OpenAI-compatible clients that can work with one model alias.
This project is for local, operator-controlled use. It is not a public service, not an official OpenAI API implementation, not an API billing substitute, and not a way to bypass authentication, pool accounts, scrape tokens, or evade provider limits.
You must bring your own dedicated Codex/ChatGPT login and make sure your use of that login is allowed by the applicable OpenAI product terms and policies. This repository does not grant permission to power third-party integrations with a ChatGPT subscription.
The service deliberately refuses startup when OPENAI_API_KEY is present:
OPENAI_API_KEY
Codex authentication belongs only in the dedicated bind-mounted
data/codex-home directory. Do not copy your normal ~/.codex into this
project.
GET /healthzreadiness check.GET /v1/modelswith the local model aliascodex-cli-default.POST /v1/chat/completionsfor text-only chat messages.- Non-streaming responses and final-only SSE streaming.
- One active Codex execution at a time, with an optional short local wait queue.
- A local operator dashboard at
http://127.0.0.1:8320/dashboard/.
- Embeddings, Responses API, tools, functions, function calls, images, audio, or files.
- Token-by-token streaming. Streaming mode sends the final answer as one SSE content delta.
- Official OpenAI API billing, quotas, authentication, or service-level guarantees.
- Multi-user isolation. Treat it as a single-user local adapter.
Local OpenAI-compatible client
-> http://127.0.0.1:8320/v1
-> FastAPI wrapper in Docker
-> Codex CLI one-shot execution
-> dedicated CODEX_HOME at ./data/codex-home
-> dedicated workspace at ./data/codex-work
The Docker image installs a pinned official Codex CLI release and the Python
wrapper runtime. The Compose files publish only 127.0.0.1:8320:8320.
Pinned Codex CLI version:
codex-cli 0.141.0
Codex is invoked non-interactively with an ephemeral session. The prompt is passed on stdin, the final response is read from a private temporary output file, and Codex progress JSONL is treated as internal metadata.
Protected assets include the wrapper bearer token in
data/secrets/proxy_api_key, Codex credentials under data/codex-home, the
host filesystem, unrelated host credentials, submitted note content, and
generated responses.
The main security boundaries are:
- Every
/v1/*route requires the wrapper bearer token. - Codex/ChatGPT credentials are stored only in the project-local
data/codex-homemount. - The container root filesystem is read-only.
- The Compose files mount only
data/codex-home,data/codex-work, and the read-only wrapper bearer-token file. - Docker capabilities are dropped, with only
DAC_OVERRIDEandFOWNERadded back so container root can access the dedicated bind mounts. no-new-privilegesis enabled.- Logs avoid raw prompts, bearer tokens, and Codex credentials.
Important residual risks:
- Codex runs inside the container with
danger-full-access; Docker mount minimization is the remaining isolation boundary. - Prompt-injected Codex commands can read and modify mounted project paths,
including
data/codex-homeanddata/codex-work. - Prompt-injected Codex commands can read
/run/secrets/proxy_api_key. - Submitted content is sent to the upstream service used by the signed-in Codex CLI account.
- Prompt injection cannot be eliminated.
data/codex-home/auth.jsonis a live credential and must be protected like a password.
For the discovery notes and implementation rationale, see
docs/discovery.md.
- Linux host with Docker Engine and Docker Compose v2.
- A dedicated Codex/ChatGPT sign-in for this project.
- Optional: access to a private container registry if you choose to publish and run your own image instead of building locally.
Clone the repository:
git clone https://github.com/subdepthtech/codex-cli-provider.git
cd codex-cli-providerCreate local config and a strong wrapper bearer token:
cp .env.example .env
mkdir -p data/codex-home data/codex-work data/secrets
python3 - <<'PY'
import pathlib, secrets
path = pathlib.Path("data/secrets/proxy_api_key")
path.write_text(secrets.token_urlsafe(48) + "\n")
PY
chmod 600 .env data/secrets/proxy_api_key
chmod 700 data/codex-home data/codex-work data/secretsDo not commit .env or anything under data/.
Build and start the local container:
docker compose up --build -dComplete device login inside the running container:
docker exec -it codex-cli-provider \
codex login --device-auth \
-c forced_login_method='"chatgpt"' \
-c cli_auth_credentials_store='"file"'Confirm login without printing credentials:
docker exec -it codex-cli-provider codex login statusCheck readiness:
curl -sS http://127.0.0.1:8320/healthz/healthz returns 200 only after the container sees the pinned Codex CLI and a
dedicated ChatGPT login. It is normal for the container to report unhealthy
before device login is complete.
Load the wrapper bearer token into a shell variable:
PROXY_API_KEY="$(cat data/secrets/proxy_api_key)"List models:
curl -sS \
-H "Authorization: Bearer $PROXY_API_KEY" \
http://127.0.0.1:8320/v1/modelsRun one completion:
curl -sS \
-H "Authorization: Bearer $PROXY_API_KEY" \
-H "Content-Type: application/json" \
http://127.0.0.1:8320/v1/chat/completions \
-d '{"model":"codex-cli-default","messages":[{"role":"user","content":"Write one sentence."}]}'Exercise final-only SSE mode:
curl -N \
-H "Authorization: Bearer $PROXY_API_KEY" \
-H "Content-Type: application/json" \
http://127.0.0.1:8320/v1/chat/completions \
-d '{"model":"codex-cli-default","stream":true,"messages":[{"role":"user","content":"Write one sentence."}]}'Confirm the provider route rejects unauthenticated callers:
curl -i http://127.0.0.1:8320/v1/modelsFor Obsidian LLM Wiki, use:
- Provider:
Custom / OpenAI-compatible - Base URL:
http://127.0.0.1:8320/v1 - API Key: the value from
data/secrets/proxy_api_key - Model:
codex-cli-default - Page generation concurrency:
1 - Suggested initial batch delay:
500-800 ms
/v1/models supports Fetch Models. Embeddings are not implemented and are not
required for the inspected OpenAI-compatible provider path.
If near-simultaneous local requests collide, set QUEUE_WAIT_SECONDS=2 or 3
in .env and restart the wrapper. The wrapper still permits only one active
Codex run.
Most configuration lives in .env, copied from .env.example.
| Variable | Purpose |
|---|---|
PROXY_API_KEY_FILE |
Path to the local bearer-token file. Compose mounts it read-only at /run/secrets/proxy_api_key. |
CODEX_UPSTREAM_MODEL |
Optional Codex model override passed to the CLI. Empty uses the Codex CLI default. |
CODEX_REQUEST_TIMEOUT_SECONDS |
Per-request Codex execution timeout. |
DASHBOARD_ENABLED |
Set to false to disable /dashboard/*. |
MAX_REQUEST_BODY_BYTES |
Maximum JSON request body size. |
MAX_MESSAGES |
Maximum number of chat messages. |
MAX_TOTAL_TEXT_CHARS |
Maximum total text across messages. |
QUEUE_WAIT_SECONDS |
Short local wait queue for bursty clients. Values are clamped to 0-5. |
CORS_ALLOWED_ORIGINS |
Optional comma-separated explicit origins. Requests without Origin are allowed. |
LOG_LEVEL |
Python wrapper log level. |
Unsupported inputs return OpenAI-shaped 400 errors. Unsupported inputs include
images, audio, files, tools, functions, function calls, tool-choice controls,
multimodal content, unknown fields, and n != 1.
Accepted but unmapped compatibility fields include temperature, top_p,
max_tokens, max_completion_tokens, presence_penalty, frequency_penalty,
repetition_penalty, stop, response_format, user, seed, thinking,
reasoning_effort, and chat_template_kwargs. Codex is not guaranteed to obey
those sampling controls.
Open the local dashboard at:
http://127.0.0.1:8320/dashboard/
The dashboard shows provider health, bearer-auth gate status, runner state,
runtime limits, a fixed probe, and sanitized in-process request events. It does
not store bearer tokens in browser storage, read data/codex-home, read or
rotate proxy_api_key, run device login, mount Docker, or display raw request
payloads.
If you expose the service beyond loopback, protect /dashboard/ with the same
network controls used for the provider route. The safer default is to keep the
Compose loopback binding unchanged.
The default public path is to build locally with docker compose up --build -d.
If you publish your own image, use the image-only Compose file and a specific
versioned tag. Do not use latest.
export CODEX_CLI_PROVIDER_IMAGE=registry.example.com/your-org/codex-cli-provider:codex-cli-provider-0.1.2
docker compose -f docker-compose.image.yml pull
docker compose -f docker-compose.image.yml up -dDocker registry authentication belongs in local Docker credential storage via
docker login. Do not put Docker Hub passwords, ChatGPT/Codex credentials, or
OpenAI API keys in Compose files.
Codex authentication still lives only in the mounted data/codex-home
directory and must be completed inside the running container.
Run repository checks without live credentials:
python3 scripts/check_repo_hygiene.py
python3 scripts/check_compose_security.py
COMPOSE_FILE=docker-compose.image.yml \
CODEX_CLI_PROVIDER_IMAGE=registry.example.com/your-org/codex-cli-provider:codex-cli-provider-0.1.2 \
python3 scripts/check_compose_security.py
PYTHONPATH=. .venv/bin/pytest -qLive checks require a running container and the dedicated Codex login:
PROXY_API_KEY="$(cat data/secrets/proxy_api_key)"
curl -f http://127.0.0.1:8320/healthz
curl -f -H "Authorization: Bearer $PROXY_API_KEY" http://127.0.0.1:8320/v1/modelsDo not print or inspect data/codex-home/auth.json.
Stop the service:
docker compose downRe-authenticate:
docker compose up -d
docker exec -it codex-cli-provider \
codex login --device-auth \
-c forced_login_method='"chatgpt"' \
-c cli_auth_credentials_store='"file"'Rotate the wrapper bearer token:
python3 - <<'PY'
import pathlib, secrets
path = pathlib.Path("data/secrets/proxy_api_key")
path.write_text(secrets.token_urlsafe(48) + "\n")
PY
chmod 600 data/secrets/proxy_api_key
docker compose restart- Device login unavailable: stop and do not use API-key login.
- Missing or stale authentication: run
docker exec -it codex-cli-provider codex login status, then re-login in the dedicated container home. - Wrapper 401: your
Authorization: Bearervalue does not match the file atdata/secrets/proxy_api_key. 400withMessage text is too large: increaseMAX_TOTAL_TEXT_CHARS, up to500000, and restart.413withRequest body too large: increaseMAX_REQUEST_BODY_BYTES, up to2000000, and restart.429withcode: "wrapper_busy": wait for the active request to finish, reduce client concurrency to1, increase batch delay, or set a smallQUEUE_WAIT_SECONDS.429withcode: "upstream_rate_limit": the signed-in upstream account is rate limited; wait and retry later.502or/healthzreturning503: checkdocker exec -it codex-cli-provider codex login status, then re-run device login if needed.- Accidental API-key detection: unset
OPENAI_API_KEYbefore starting. - Sandbox failure: this configuration intentionally uses Codex
danger-full-accessinside Docker. Do not addprivileged,SYS_ADMIN, unconfined seccomp/AppArmor, host networking, or host home mounts. - Bind-mount permission failure: keep the dedicated
data/codex-home,data/codex-work, anddata/secretspaths writable by the Docker runtime. - ARM64 versus AMD64: build for the matching image architecture and verify the Dockerfile checksum step.