Image Generation Skill
Generates or edits images for the current project (website assets, game assets, UI mockups, product mockups, logos, photorealistic images, infographics).
When to use
- Generate a new image (concept art, product shot, cover, website hero)
- Edit an existing image (inpainting, masked edits, background replacement, object removal)
- Batch runs (many prompts or many variants)
Decision tree (generate vs edit vs batch)
- If the user provides an input image OR says "edit/retouch/inpaint/mask" → edit
- Else if the user needs many different prompts/assets → generate-batch
- Else → generate
Environment — Priority Order
Step 0: Auto-patch config.toml (run first, always)
Before anything else, ensure ~/.codex/config.toml has the required image generation settings.
If missing entries, auto-add them without asking the user.
from pathlib import Path
import sys
config_path = Path.home() / ".codex" / "config.toml"
if not config_path.exists():
raise SystemExit("[imagegen] ~/.codex/config.toml not found. Please set up Codex first.")
raw = config_path.read_text()
lines_to_add = []
# Check [features] image_generation
if "image_generation" not in raw:
lines_to_add.append("\n[features]\nimage_generation = true")
print("[imagegen] Auto-adding: [features] image_generation = true")
# Check [image_generation] model block
if "[image_generation]" not in raw:
# Detect provider from existing config
provider = ""
for line in raw.splitlines():
if line.strip().startswith("model_provider"):
provider = line.split("=")[-1].strip().strip('"').lower()
break
model = "cx/gpt-5.4-image" if "9router" in provider else "gpt-image-2"
lines_to_add.append(f"\n[image_generation]\nmodel = \"{model}\"")
print(f"[imagegen] Auto-adding: [image_generation] model = {model}")
if lines_to_add:
with open(config_path, "a") as f:
f.write("\n" + "\n".join(lines_to_add) + "\n")
print("[imagegen] config.toml patched.")
else:
print("[imagegen] config.toml already configured.")
1st choice: Built-in image generation (PREFERRED)
Use built-in image_generation_call if:
[features] image_generation = trueis set (auto-patched above)- built-in tool is available in this session
If both → use it directly. Skip steps below.
2nd choice: Provider fallback via config.toml + auth.json
Only if built-in tool is NOT available in this session.
Setup env vars:
python3 - <<'PYSETUP' > /tmp/imagegen_env.sh
import json, tomllib
from pathlib import Path
config = tomllib.loads((Path.home() / ".codex" / "config.toml").read_text())
auth = json.loads((Path.home() / ".codex" / "auth.json").read_text())
provider_name = config.get("model_provider", "").strip()
base_url = config["model_providers"][provider_name]["base_url"].rstrip("/")
endpoint = f"{base_url}/images/generations"
img_cfg = config.get("image_generation", {})
model = img_cfg.get("model") or ("cx/gpt-5.4-image" if "9router" in provider_name.lower() else "gpt-image-2")
# KEY RESOLUTION (confirmed working order for 9Router):
# 1. OPENAI_API_KEY in auth.json — this is the 9Router sk-... key
# 2. tokens.access_token — this is an OpenAI JWT, NOT a 9Router key; do NOT use for 9Router
# If OPENAI_API_KEY is present, always prefer it regardless of provider
tokens = auth.get("tokens", {})
api_key = (
auth.get("OPENAI_API_KEY", "")
or tokens.get("access_token", "")
).strip()
if not api_key:
raise SystemExit("[imagegen] No API key found in ~/.codex/auth.json")
print(f'export IMAGEGEN_ENDPOINT={json.dumps(endpoint)}')
print(f'export IMAGEGEN_MODEL={json.dumps(model)}')
print(f'export IMAGEGEN_KEY={json.dumps(api_key)}')
PYSETUP
source /tmp/imagegen_env.sh
Build request JSON:
python3 - <<'PY' > tmp/imagegen/request.json
import json, os
prompt = """<PROMPT>""".strip()
print(json.dumps({
"model": os.environ["IMAGEGEN_MODEL"],
"prompt": prompt,
"n": 1,
"size": "auto",
"quality": "auto",
"background": "auto",
"image_detail": "high",
"output_format": "png"
}))
PY
Call endpoint:
mkdir -p tmp/imagegen output/imagegen
curl -sS -X POST "$IMAGEGEN_ENDPOINT" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $IMAGEGEN_KEY" \
-H "Accept: text/event-stream" \
--data-binary @tmp/imagegen/request.json \
-o tmp/imagegen/response.raw
Parse response (handles both SSE and plain JSON):
import json, base64
from pathlib import Path
raw = Path("tmp/imagegen/response.raw").read_text(errors="replace")
final = None
# Try SSE event stream first
for block in raw.strip().split("\n\n"):
lines = block.splitlines()
ev = next((l[6:].strip() for l in lines if l.startswith("event:")), "")
data = "".join(l[5:].strip() for l in lines if l.startswith("data:"))
if ev == "done" and data:
final = json.loads(data)
break
# Fallback: plain JSON response (9Router returns plain JSON)
if not final:
try:
final = json.loads(raw)
except Exception as exc:
print(raw[:1000])
raise SystemExit(f"[imagegen] Could not parse response: {exc}")
if "error" in final:
raise SystemExit(f"[imagegen] API error: {final['error']}")
item = final.get("data", [{}])[0]
b64 = item.get("b64_json", "")
url = item.get("url", "")
out = Path("output/imagegen/result.png")
if b64:
out.write_bytes(base64.b64decode(b64))
print(f"[imagegen] Saved: {out} ({out.stat().st_size:,} bytes)")
elif url:
import urllib.request
urllib.request.urlretrieve(url, out)
print(f"[imagegen] Downloaded: {out} ({out.stat().st_size:,} bytes)")
else:
print(raw[:1000])
raise SystemExit("[imagegen] No b64_json or url in response.")
STOP conditions (strict)
- If
~/.codex/config.tomlnot found → STOP, tell user to set up Codex - If
~/.codex/auth.jsonnot found → STOP, tell user to authenticate Codex (codex auth login) - If no API key found in auth.json → STOP, report missing key
- If provider
base_urlmissing → STOP, report which provider config is incomplete - Do NOT hardcode any API key or endpoint URL
- Do NOT use
tokens.access_tokenas image API key for 9Router — it is an OpenAI JWT, not a 9Router bearer key - Do NOT use
~/.config/imagegen/auth.json(old path — removed) - Do NOT use sharp, canvas, jimp, PIL as image substitute
- Do NOT fake image generation with placeholder or solid colour
Key resolution (confirmed working)
| Priority | Field in auth.json |
When to use |
|---|---|---|
| ✅ 1st | OPENAI_API_KEY |
Always — this is the 9Router sk-... key |
| ⚠️ 2nd | tokens.access_token |
Fallback only — OpenAI JWT, fails on 9Router |
| ❌ Never | tokens.id_token |
OpenAI identity token, never use for API calls |
| ❌ Never | tokens.refresh_token |
Token refresh only, not an API key |
Why:
OPENAI_API_KEYin Codex auth.json holds the actual 9Routersk-...bearer key.tokens.access_tokenis an OpenAI-issued JWT used only for OpenAI services, not 9Router.
Model resolution (priority order)
| Source | Field | Example |
|---|---|---|
| config.toml | [image_generation].model |
cx/gpt-5.4-image |
| Auto-detect (9Router) | provider name contains 9router |
cx/gpt-5.4-image |
| Default fallback | — | gpt-image-2 |
Recommended config.toml (9Router)
[features]
image_generation = true
model = "cx/gpt-5.5"
model_provider = "9router"
[model_providers.9router]
name = "9Router"
base_url = "http://localhost:20128/v1"
wire_api = "responses"
[agents.subagent]
model = "cx/gpt-5.5"
[image_generation]
model = "cx/gpt-5.4-image"
Workflow
Environment setup (always run first):
- a. Auto-patch
~/.codex/config.toml→ append[features]and[image_generation]blocks if missing - b. Try built-in
image_generation_call→ if available, use it - c. If built-in unavailable → read config.toml + auth.json, derive endpoint, call curl
- d. NEVER ask user to manually edit config — patch it automatically
- e. NEVER try
tokens.access_tokenbeforeOPENAI_API_KEY
- a. Auto-patch
Decide intent: generate vs edit vs batch
Collect inputs: prompt(s), constraints, input image(s) if edit
If batch: write temp JSONL under
tmp/imagegen/, delete afterAugment prompt into structured spec
Run via priority order above
Validate output: subject, style, composition, text accuracy
Iterate with single targeted change if needed
Save to
output/imagegen/and note final prompt used
Defaults & rules
- Default image model:
cx/gpt-5.4-imagefor 9Router,gpt-image-2otherwise - Endpoint: always derived from
config.tomlproviderbase_url→<base_url>/images/generations - Auth:
~/.codex/auth.jsonkey fieldOPENAI_API_KEY - Config:
~/.codex/config.toml(auto-patched if missing entries) - Use
tmp/imagegen/for intermediates,output/imagegen/for finals
Prompt augmentation template
Use case: <taxonomy slug>
Asset type: <where used>
Primary request: <user prompt>
Scene/background: <environment>
Subject: <main subject>
Style/medium: <photo/illustration/3D>
Composition/framing: <layout>
Lighting/mood: <lighting>
Color palette: <palette>
Quality: <low/medium/high/auto>
Text (verbatim): "<exact text>"
Constraints: <must keep/avoid>
Use-case taxonomy
Generate: photorealistic-natural, product-mockup, ui-mockup, infographic-diagram, logo-brand, illustration-story, stylized-concept, historical-scene
Edit: text-localization, identity-preserve, precise-object-edit, lighting-weather, background-extraction, style-transfer, compositing, sketch-to-render