PixelVault Generate
Generate an image with an AI model, then host it on PixelVault so what you embed
is a permanent URL — not a model link that expires in an hour, and not a
base64 blob you have to carry around.
OpenRouter (image model) → base64 → PixelVault → https://img.pixelvault.dev/…
When it triggers: "make a hero image for this post and host it", "generate an
OG card", "create a diagram and give me a URL", or any task that needs a new
image at a durable link.
Prerequisites
OPENROUTER_API_KEY — for image generation. Get one at
https://openrouter.ai/keys and export it in the environment.
- PixelVault CLI, configured — for hosting. If
pixelvault isn't installed,
run npm install -g pixelvault-cli; if no key is configured, run
/pixelvault-setup or set PIXELVAULT_API_KEY.
jq, base64, curl — used to build the request safely and decode the image.
These are authoring-time credentials — they belong in your shell/.env, never
in a deployed site or client bundle. What ships is only the resulting CDN URL.
Steps
Run these in order. Each step fails loudly so an agent never proceeds with a
missing key or a broken file.
Preflight — verify tools + auth before spending a generation call:
: "${OPENROUTER_API_KEY:?set OPENROUTER_API_KEY — https://openrouter.ai/keys}"
for t in curl jq base64 pixelvault; do
command -v "$t" >/dev/null || { echo "missing required tool: $t"; exit 1; }
done
pixelvault whoami --json >/dev/null 2>&1 \
|| { echo "PixelVault not authenticated — run /pixelvault-setup"; exit 1; }
Pick a model. Only image-output models work (OpenRouter routes image
generation through the chat API's modalities). The default,
google/gemini-3.1-flash-image, is fast and cheap; google/gemini-3-pro-image
("Nano Banana Pro") is higher fidelity. Slugs change — list current ones with:
curl -fsS https://openrouter.ai/api/v1/models \
| jq -r '.data[] | select(.architecture.output_modalities // [] | index("image")) | .id'
Generate. Build the request body with jq so the prompt is always
JSON-safe (quotes, apostrophes, and newlines in art prompts won't break it),
fail on HTTP errors (-fsS), and extract the image with jq -er so a missing
image is caught instead of silently decoding to garbage:
MODEL="${MODEL:-google/gemini-3.1-flash-image}"
PROMPT="a flat vector blog hero, a robot writing at a laptop, soft gradient, 16:9, no text"
body="$(jq -n --arg m "$MODEL" --arg p "$PROMPT" \
'{model:$m, modalities:["image","text"], messages:[{role:"user", content:$p}]}')"
resp="$(curl -fsS https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$body")" || { echo "OpenRouter request failed"; exit 1; }
# -e makes jq exit non-zero when the path is absent/null (i.e. an error response)
data_url="$(printf '%s' "$resp" | jq -er '.choices[0].message.images[0].image_url.url')" \
|| { printf '%s\n' "$resp" | jq -r '.error.message // "no image in response"'; exit 1; }
# Decode to a temp file (no clobbering the CWD) and confirm it's non-empty
img="$(mktemp -t pv-gen).png"
printf '%s' "$data_url" | sed 's/^data:image\/[^;]*;base64,//' | base64 -d > "$img"
test -s "$img" || { echo "decoded image is empty — aborting"; exit 1; }
base64 -d reads stdin on both GNU and macOS/BSD. If an older BSD build
rejects -d, use -D.
Host it on PixelVault (reuses /pixelvault-upload):
pixelvault upload "$img"
# → https://img.pixelvault.dev/proj_abc/img_xyz.png
Report the permanent URL. Need an OG card or a thumbnail? Add transform
params to the same URL — no re-upload: …img_xyz.png?w=1200&h=630&fmt=auto
(social card), …img_xyz.png?w=700&fmt=auto&q=auto (inline). See
/pixelvault-transform.
Error Handling
| Error |
Action |
OPENROUTER_API_KEY unset |
Get a key at openrouter.ai/keys and export it |
curl fails / non-200 from OpenRouter |
Inspect $resp for .error.message; check the key and the model slug |
jq -e exits non-zero (no image in response) |
Model returned no image — verify the slug is image-output (step 2); read the printed error |
404 No endpoints found for <model> |
Slug is stale or not image-capable — pick one from the step-2 list |
decoded image empty (test -s fails) |
Decode failed — on older BSD base64, use -D; re-check the response |
command not found: pixelvault |
npm install -g pixelvault-cli |
PixelVault 401 / not authenticated |
Run /pixelvault-setup or set PIXELVAULT_API_KEY |
PixelVault 413 Payload Too Large |
Image exceeds the plan limit (5 MB free) — downscale, or use a paid plan |
PixelVault 415 Unsupported Media Type |
Decoded file isn't a supported image — the generation step likely failed |
Notes
- Prompts are non-deterministic. The same prompt yields a different image each
run. Keep the prompt in version control (frontmatter, a manifest, a comment) so
the art is reproducible.
- Attribute correctly. Record which model produced an image if it matters —
gemini-3.1-flash-image and gemini-3-pro-image are different looks.
MODEL respects the model arg — set MODEL=<slug> before running, or it
falls back to the default.
- For OpenAI's
gpt-image-1 specifically, call OpenAI's images API directly and
host the result with pixelvault upload the same way.
1---2name: pixelvault-generate3description: Generate an image with an AI model (via OpenRouter — e.g. Gemini image / "Nano Banana") and host it on PixelVault, getting a permanent CDN URL back. Use when you need to CREATE an image and then embed it somewhere durable — a blog hero, an OG/social card, a README diagram, a docs illustration — and a temporary model URL or a base64 blob won't do. Prompt in, permanent https://img.pixelvault.dev/... URL out.4---56# PixelVault Generate78Generate an image with an AI model, then host it on PixelVault so what you embed9is a **permanent** URL — not a model link that expires in an hour, and not a10base64 blob you have to carry around.1112```13OpenRouter (image model) → base64 → PixelVault → https://img.pixelvault.dev/…14```1516**When it triggers:** "make a hero image for this post and host it", "generate an17OG card", "create a diagram and give me a URL", or any task that needs a *new*18image at a durable link.1920## Prerequisites21221. **`OPENROUTER_API_KEY`** — for image generation. Get one at23 <https://openrouter.ai/keys> and export it in the environment.242. **PixelVault CLI, configured** — for hosting. If `pixelvault` isn't installed,25 run `npm install -g pixelvault-cli`; if no key is configured, run26 `/pixelvault-setup` or set `PIXELVAULT_API_KEY`.273. **`jq`, `base64`, `curl`** — used to build the request safely and decode the image.2829These are **authoring-time** credentials — they belong in your shell/`.env`, never30in a deployed site or client bundle. What ships is only the resulting CDN URL.3132## Steps3334Run these in order. Each step fails loudly so an agent never proceeds with a35missing key or a broken file.36371. **Preflight** — verify tools + auth *before* spending a generation call:3839 ```bash40 : "${OPENROUTER_API_KEY:?set OPENROUTER_API_KEY — https://openrouter.ai/keys}"41 for t in curl jq base64 pixelvault; do42 command -v "$t" >/dev/null || { echo "missing required tool: $t"; exit 1; }43 done44 pixelvault whoami --json >/dev/null 2>&1 \45 || { echo "PixelVault not authenticated — run /pixelvault-setup"; exit 1; }46 ```47482. **Pick a model.** Only image-*output* models work (OpenRouter routes image49 generation through the chat API's `modalities`). The default,50 `google/gemini-3.1-flash-image`, is fast and cheap; `google/gemini-3-pro-image`51 ("Nano Banana Pro") is higher fidelity. Slugs change — list current ones with:5253 ```bash54 curl -fsS https://openrouter.ai/api/v1/models \55 | jq -r '.data[] | select(.architecture.output_modalities // [] | index("image")) | .id'56 ```57583. **Generate.** Build the request body with `jq` so the prompt is always59 JSON-safe (quotes, apostrophes, and newlines in art prompts won't break it),60 fail on HTTP errors (`-fsS`), and extract the image with `jq -er` so a missing61 image is caught instead of silently decoding to garbage:6263 ```bash64 MODEL="${MODEL:-google/gemini-3.1-flash-image}"65 PROMPT="a flat vector blog hero, a robot writing at a laptop, soft gradient, 16:9, no text"6667 body="$(jq -n --arg m "$MODEL" --arg p "$PROMPT" \68 '{model:$m, modalities:["image","text"], messages:[{role:"user", content:$p}]}')"6970 resp="$(curl -fsS https://openrouter.ai/api/v1/chat/completions \71 -H "Authorization: Bearer $OPENROUTER_API_KEY" \72 -H "Content-Type: application/json" \73 -d "$body")" || { echo "OpenRouter request failed"; exit 1; }7475 # -e makes jq exit non-zero when the path is absent/null (i.e. an error response)76 data_url="$(printf '%s' "$resp" | jq -er '.choices[0].message.images[0].image_url.url')" \77 || { printf '%s\n' "$resp" | jq -r '.error.message // "no image in response"'; exit 1; }7879 # Decode to a temp file (no clobbering the CWD) and confirm it's non-empty80 img="$(mktemp -t pv-gen).png"81 printf '%s' "$data_url" | sed 's/^data:image\/[^;]*;base64,//' | base64 -d > "$img"82 test -s "$img" || { echo "decoded image is empty — aborting"; exit 1; }83 ```8485 > `base64 -d` reads stdin on both GNU and macOS/BSD. If an older BSD build86 > rejects `-d`, use `-D`.87884. **Host** it on PixelVault (reuses `/pixelvault-upload`):8990 ```bash91 pixelvault upload "$img"92 # → https://img.pixelvault.dev/proj_abc/img_xyz.png93 ```94955. **Report** the permanent URL. Need an OG card or a thumbnail? Add transform96 params to the same URL — no re-upload: `…img_xyz.png?w=1200&h=630&fmt=auto`97 (social card), `…img_xyz.png?w=700&fmt=auto&q=auto` (inline). See98 `/pixelvault-transform`.99100## Error Handling101102| Error | Action |103|-------|--------|104| `OPENROUTER_API_KEY` unset | Get a key at openrouter.ai/keys and export it |105| `curl` fails / non-200 from OpenRouter | Inspect `$resp` for `.error.message`; check the key and the model slug |106| jq `-e` exits non-zero (no image in response) | Model returned no image — verify the slug is image-output (step 2); read the printed error |107| `404 No endpoints found for <model>` | Slug is stale or not image-capable — pick one from the step-2 list |108| decoded image empty (`test -s` fails) | Decode failed — on older BSD `base64`, use `-D`; re-check the response |109| `command not found: pixelvault` | `npm install -g pixelvault-cli` |110| PixelVault `401` / not authenticated | Run `/pixelvault-setup` or set `PIXELVAULT_API_KEY` |111| PixelVault `413 Payload Too Large` | Image exceeds the plan limit (5 MB free) — downscale, or use a paid plan |112| PixelVault `415 Unsupported Media Type` | Decoded file isn't a supported image — the generation step likely failed |113114## Notes115116- **Prompts are non-deterministic.** The same prompt yields a different image each117 run. Keep the prompt in version control (frontmatter, a manifest, a comment) so118 the art is reproducible.119- **Attribute correctly.** Record which model produced an image if it matters —120 `gemini-3.1-flash-image` and `gemini-3-pro-image` are different looks.121- **`MODEL` respects the `model` arg** — set `MODEL=<slug>` before running, or it122 falls back to the default.123- For OpenAI's `gpt-image-1` specifically, call OpenAI's images API directly and124 host the result with `pixelvault upload` the same way.