OpenRouter Image Generation
Generate images through OpenRouter's standard chat-completions endpoint using
image-capable models. No local GPU, no ComfyUI workflow, no Docker: the image
comes back inside the API response JSON.
Configuration Variables
Set these in your environment before running the pipeline, or document them in
a .env file at your repo root:
| Variable |
Description |
Example |
OPENROUTER_API_KEY |
(Required) OpenRouter API key for image generation |
sk-or-v1-xxxxxxxx |
OPENROUTER_ENV_FILE |
(Optional) Path to a .env file containing the key, when it is not set directly in the environment |
~/.hermes/.env |
If OPENROUTER_API_KEY is not set, the scripts fall back to reading it from
OPENROUTER_ENV_FILE (default ~/.hermes/.env), looking for a line of the
form OPENROUTER_API_KEY=....
When to Use
- User asks for an illustration, cover, thumbnail, or any generated image and
OpenRouter is the stated or available path.
- Local diffusion (ComfyUI) is not available or the job is a one-off.
- Text must render inside the image (T-shirts, signs, posters): Gemini image
models handle text well — spell the text exactly in the prompt, then verify.
Endpoint & Response Shape (the critical bit)
- POST
https://openrouter.ai/api/v1/chat/completions with the normal chat
payload: {"model": ..., "messages": [{"role":"user","content":"<prompt>"}], "n": 1}.
- The image is returned embedded in the JSON as a
data:image/...;base64,....
URL string. There is NO separate file download and NO image field you can
trust to exist: message.content can be null while the image is buried
somewhere else (e.g. nested content arrays, reasoning_details).
- Always walk the whole response JSON recursively and collect every string
starting with
data:image (fallback: http(s) URLs matching
image|png|jpe?g|webp). Decode the first hit. See scripts/generate_image.py.
Model Selection
Discover image-capable models live:
curl -s https://openrouter.ai/api/v1/models -o /tmp/or_models.json
# filter: architecture.output_modalities contains "image"
Cheap verified picks (re-check pricing via the API):
| Model |
Prompt $/token |
Notes |
google/gemini-3.1-flash-lite-image |
~$0.25/M |
Nano Banana 2 Lite — default "nice cheap" pick |
google/gemini-2.5-flash-image |
~$0.30/M |
Nano Banana; proven text quality |
google/gemini-3.1-flash-image |
~$0.50/M |
Nano Banana 2; best quality |
openai/gpt-5-image |
~$10/M |
premium, rarely needed |
Prompt guidance: state style ("editorial illustration", "slightly cartoonish"),
composition ("wide 16:9"), and explicitly "no real logos, no brand names, no
watermark".
The Recurring Series Character (recommended)
For article or blog illustrations, define ONE cartoon character and reuse it in
every illustration so the series reads as a single body of work. Save the first
good generation as the style anchor and include the full spec in every prompt.
Example spec (used by the author of this skill — customise freely):
- A cartoon person of your choosing (example used by the author: stout/bald
man, short goatee, round glasses, warm smile, plain black T-shirt, optionally
a tech joke in green text)
- Their workspace: cluttered desk with monitors (terminal text and/or data
visuals), shelves with books and electronics, warm desk lamp, string lights,
night window
- A recurring motif: sci-fi paraphernalia (model starship, poster) — describe
generically, never ask for trademarked logos
- Palette: cool blues/teals/purples with warm amber accents; wide 16:9
- Editorial cartoon style, clean linework, cel shading; "no real logos, no
brand names, no watermark"
Verification (always — the model can lie about what it drew)
The configured aux vision model may not accept images (e.g. a text-only
fallback → HTTP 400 "This model does not support image"). Workaround: send the
PNG to a cheap vision-capable model over OpenRouter as a data: URL in a
content array
[{type:"text",text:...},{type:"image_url",image_url:{url:"data:image/png;base64,..."}}].
A verified vision model: google/gemini-3.5-flash-lite (check the current
list — a gemini-3.1-flash without -image may not exist as a text model).
Ask targeted questions: is the required text legible and correctly spelled?
Are the requested elements present? See scripts/verify_image_vision.py.
Key Pitfall — stale API keys
- An invalid/rotated OpenRouter key returns HTTP 401
{"error":{"message":"User not found."}}.
GET /models may STILL return 200 with an invalid key (the models list is
not a strong auth check). Confirm the key with GET /api/v1/credits,
which 401s on a bad key.
- In Hermes, keys may live in multiple
.env files and they can DIVERGE. The
session environment may hold a stale key sourced earlier from the wrong
file. Read the key from OPENROUTER_API_KEY explicitly (or the
OPENROUTER_ENV_FILE path) rather than trusting os.environ alone.
- Never print keys to chat or commit them.
Scripts
scripts/generate_image.py <out.png> "<prompt>" [model] — reads the key from
OPENROUTER_API_KEY (or OPENROUTER_ENV_FILE), calls the API, walks the
JSON for the image, writes the PNG. Default model
google/gemini-3.1-flash-lite-image.
scripts/verify_image_vision.py <image.png> [model] — vision check of a PNG
via OpenRouter (data URL), prints the model's description. Default model
google/gemini-3.5-flash-lite.
Related
comfyui — local diffusion workflows (different path, no API cost).
baoyu-article-illustrator — style/palette guidance for article images
(pair with this skill for consistent series art).
medium-story — article pipeline; drop the PNG in the story folder and
reference it with a [*FILES*: name.png — description] placeholder.
1---2name: openrouter-image-gen3description: Use when an illustration, cover, thumbnail, or any generated image is needed and local diffusion is unavailable. Generates via OpenRouter API, cheap models, no GPU, with recurring-character guidance for consistent article series art.4license: MIT5---67# OpenRouter Image Generation89Generate images through OpenRouter's standard chat-completions endpoint using10image-capable models. No local GPU, no ComfyUI workflow, no Docker: the image11comes back inside the API response JSON.1213## Configuration Variables1415Set these in your environment before running the pipeline, or document them in16a `.env` file at your repo root:1718| Variable | Description | Example |19|----------|-------------|---------|20| `OPENROUTER_API_KEY` | (Required) OpenRouter API key for image generation | `sk-or-v1-xxxxxxxx` |21| `OPENROUTER_ENV_FILE` | (Optional) Path to a `.env` file containing the key, when it is not set directly in the environment | `~/.hermes/.env` |2223If `OPENROUTER_API_KEY` is not set, the scripts fall back to reading it from24`OPENROUTER_ENV_FILE` (default `~/.hermes/.env`), looking for a line of the25form `OPENROUTER_API_KEY=...`.2627## When to Use2829- User asks for an illustration, cover, thumbnail, or any generated image and30 OpenRouter is the stated or available path.31- Local diffusion (ComfyUI) is not available or the job is a one-off.32- Text must render inside the image (T-shirts, signs, posters): Gemini image33 models handle text well — spell the text exactly in the prompt, then verify.3435## Endpoint & Response Shape (the critical bit)3637- POST `https://openrouter.ai/api/v1/chat/completions` with the normal chat38 payload: `{"model": ..., "messages": [{"role":"user","content":"<prompt>"}], "n": 1}`.39- The image is returned **embedded in the JSON** as a `data:image/...;base64,....`40 URL string. There is NO separate file download and NO image field you can41 trust to exist: `message.content` can be `null` while the image is buried42 somewhere else (e.g. nested content arrays, `reasoning_details`).43- **Always walk the whole response JSON recursively** and collect every string44 starting with `data:image` (fallback: `http(s)` URLs matching45 image|png|jpe?g|webp). Decode the first hit. See `scripts/generate_image.py`.4647## Model Selection4849Discover image-capable models live:5051```bash52curl -s https://openrouter.ai/api/v1/models -o /tmp/or_models.json53# filter: architecture.output_modalities contains "image"54```5556Cheap verified picks (re-check pricing via the API):5758| Model | Prompt $/token | Notes |59|---|---|---|60| `google/gemini-3.1-flash-lite-image` | ~$0.25/M | Nano Banana 2 Lite — default "nice cheap" pick |61| `google/gemini-2.5-flash-image` | ~$0.30/M | Nano Banana; proven text quality |62| `google/gemini-3.1-flash-image` | ~$0.50/M | Nano Banana 2; best quality |63| `openai/gpt-5-image` | ~$10/M | premium, rarely needed |6465Prompt guidance: state style ("editorial illustration", "slightly cartoonish"),66composition ("wide 16:9"), and explicitly "no real logos, no brand names, no67watermark".6869## The Recurring Series Character (recommended)7071For article or blog illustrations, define ONE cartoon character and reuse it in72every illustration so the series reads as a single body of work. Save the first73good generation as the style anchor and include the full spec in every prompt.7475Example spec (used by the author of this skill — customise freely):7677- A cartoon person of your choosing (example used by the author: stout/bald78 man, short goatee, round glasses, warm smile, plain black T-shirt, optionally79 a tech joke in green text)80- Their workspace: cluttered desk with monitors (terminal text and/or data81 visuals), shelves with books and electronics, warm desk lamp, string lights,82 night window83- A recurring motif: sci-fi paraphernalia (model starship, poster) — describe84 generically, never ask for trademarked logos85- Palette: cool blues/teals/purples with warm amber accents; wide 16:986- Editorial cartoon style, clean linework, cel shading; "no real logos, no87 brand names, no watermark"8889## Verification (always — the model can lie about what it drew)9091The configured aux vision model may not accept images (e.g. a text-only92fallback → HTTP 400 "This model does not support image"). Workaround: send the93PNG to a cheap vision-capable model over OpenRouter as a `data:` URL in a94content array95`[{type:"text",text:...},{type:"image_url",image_url:{url:"data:image/png;base64,..."}}]`.96A verified vision model: `google/gemini-3.5-flash-lite` (check the current97list — a `gemini-3.1-flash` without `-image` may not exist as a text model).98Ask targeted questions: is the required text legible and correctly spelled?99Are the requested elements present? See `scripts/verify_image_vision.py`.100101## Key Pitfall — stale API keys102103- An invalid/rotated OpenRouter key returns HTTP 401 `{"error":{"message":"User104 not found."}}`.105- `GET /models` may STILL return 200 with an invalid key (the models list is106 not a strong auth check). **Confirm the key with `GET /api/v1/credits`**,107 which 401s on a bad key.108- In Hermes, keys may live in multiple `.env` files and they can DIVERGE. The109 session environment may hold a stale key sourced earlier from the wrong110 file. Read the key from `OPENROUTER_API_KEY` explicitly (or the111 `OPENROUTER_ENV_FILE` path) rather than trusting `os.environ` alone.112- Never print keys to chat or commit them.113114## Scripts115116- `scripts/generate_image.py <out.png> "<prompt>" [model]` — reads the key from117 `OPENROUTER_API_KEY` (or `OPENROUTER_ENV_FILE`), calls the API, walks the118 JSON for the image, writes the PNG. Default model119 `google/gemini-3.1-flash-lite-image`.120- `scripts/verify_image_vision.py <image.png> [model]` — vision check of a PNG121 via OpenRouter (data URL), prints the model's description. Default model122 `google/gemini-3.5-flash-lite`.123124## Related125126- `comfyui` — local diffusion workflows (different path, no API cost).127- `baoyu-article-illustrator` — style/palette guidance for article images128 (pair with this skill for consistent series art).129- `medium-story` — article pipeline; drop the PNG in the story folder and130 reference it with a `[*FILES*: name.png — description]` placeholder.