FastAPI service · personal/dev use only · MIT

one ChatGPT subscription,
a self-hosted image API

A small FastAPI wrapper around Codex CLI's $imagegen. Issue bearer keys to your internal scripts, CI jobs, and side projects; they all share one ChatGPT subscription's image-gen quota over a clean HTTP endpoint.

1Why this exists

You already pay for ChatGPT. Codex CLI installed on your homelab box is already logged in to it. But every internal script, CI workflow, or Discord/Telegram bot that wants to generate an image has to either (a) ssh into your box and shell out to codex exec, or (b) burn separate OpenAI Images API credits.

This service does (c): expose $imagegen over a tiny HTTP API behind Authorization: Bearer cimg_<random-token>, with an admin UI to issue / revoke keys per caller. One ChatGPT account, many consumers, no extra spend.

2What it produces

Under the hood it's gpt-image-2 (the model behind Codex CLI's built-in image_gen tool). The output is whatever $imagegen produces — this service does not reprocess or filter; it just delivers the PNG to the caller and tracks history.

3Install

Prerequisites: Codex CLI installed and codex login done on the host. Docker + Docker Compose for the containerized path.

A. Local testing — no nginx

git clone https://github.com/yazelin/codex-image-service
cd codex-image-service
cp .env.example .env
# edit .env: set ADMIN_PASSWORD and ADMIN_SESSION_SECRET

docker compose -f docker-compose.local.yml up -d --build
open http://localhost:8000/admin

Maps port 8000 directly to your host, no reverse proxy required. Image URLs in API responses come back as http://localhost:8000/generated/....

B. Production — behind your existing nginx

# .env: set ADMIN_PASSWORD, ADMIN_SESSION_SECRET,
# PUBLIC_BASE_URL=https://YOUR-DOMAIN/codex-image,
# ADMIN_URL_PREFIX=/codex-image

docker compose up -d --build
# then paste deploy/nginx.codex-image-service.location.conf.example
# into your existing nginx and reload

The default docker-compose.yml attaches the container to a pre-existing Docker network called nginx_bridge_network. First-run smoke test:

curl -sf https://YOUR-DOMAIN/codex-image/health
# {"status":"ok"}

C. No Docker — fastest dev loop

python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000

Uses your host's own ~/.codex/auth.json directly. Easiest for iterating on the code.

4Use

Log in to /codex-image/admin, click Create API Key, copy the cimg_<random-token> value. Refresh or leave the page and the raw value is gone forever — only the sha256 hash stays on the server. Then hit the API:

# shell / GitHub Actions
curl -sS --fail --max-time 650 \
  -X POST https://YOUR-DOMAIN/codex-image/v1/images/generate \
  -H "Authorization: Bearer $CODEX_IMAGE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"...","size":"1024x1024","quality":"medium","count":1}'
# python (httpx)
import httpx, os
r = httpx.post(
    "https://YOUR-DOMAIN/codex-image/v1/images/generate",
    headers={"Authorization": f"Bearer {os.environ['CODEX_IMAGE_KEY']}"},
    json={"prompt": "...", "size": "1024x1024", "quality": "medium", "count": 1},
    timeout=httpx.Timeout(connect=10, read=600, write=30, pool=10),
)
images = r.json()["images"]   # [{"url": "...", "expires_at": "..."}]

Response shape:

{
  "id": "img_3f81...",
  "status": "succeeded",
  "images": [{"url": "https://YOUR-DOMAIN/codex-image/generated/img_3f81....png",
              "expires_at": "..."}],
  "created_at": "..."
}

5Multi-account round-robin

A single ChatGPT subscription's per-account image-gen quota is the real cap on throughput. Configure two or more ChatGPT accounts and the service rotates CODEX_HOME between them per request, with automatic cross-account retry if one account errors.

# one host dir per account under ~/codex-homes/<label>/
mkdir -p ~/codex-homes/{personal,team}
CODEX_HOME=~/codex-homes/personal codex login   # log in with ChatGPT A
CODEX_HOME=~/codex-homes/team     codex login   # log in with ChatGPT B

# .env (paths are *inside the container*)
CODEX_HOMES=/host_codex_homes/personal:/host_codex_homes/team

# restart — the parent ~/codex-homes is already mounted into /host_codex_homes
docker compose up -d --build

Label the folders anything you want (personal / team-acme / dev / …). Two logins on the same ChatGPT user account share one quota pool, so only distinct user accounts actually expand capacity.

Admin Overview gets one card per account with the 30-day request count, success/failure split, auth-token freshness chip (green ≤6d, amber 7–9d, red ≥10d since last_refresh), and the first 8 chars of the ChatGPT account_id. Access tokens last ~10 days and the auth.json files are mounted read-only, so keep them fresh on the host side:

# weekly cron — touch each home so codex refreshes its tokens
for h in ~/codex-homes/*/; do CODEX_HOME="$h" codex --version >/dev/null; done

6What's in the admin

Issue / disable / delete keys

Names go into the api_keys table; tokens stored as sha256 hashes and shown raw exactly once at creation.

Test image generation

Pick a key + prompt + size + quality, queue a job from the admin UI to verify the key works without touching curl.

Auto-expire images

Each image carries an expires_at (default IMAGE_RETENTION_DAYS=7); a background sweep deletes PNGs + workdirs.

🗑Manual delete

Per-row Delete on the Image Requests table for immediate purge — file, workdir, DB row — without waiting for expiry.

Short live walk-through (19 s, no audio):

!Disclaimer — personal / experimental use only

This project was built for our own development and testing inside a private homelab. It is not affiliated with, endorsed by, or supported by OpenAI. It wraps the official @openai/codex CLI and re-exposes its $imagegen skill as a small HTTP API; every request consumes quota from the single ChatGPT account whose ~/.codex/auth.json is mounted into the container.

Things to be aware of before reusing this code: