Skip to main content
Glama

brushrouter

Generate images with the AI you already pay for. One CLI + MCP server that routes every request to the best image backend on the machine — your ChatGPT subscription, your Google login, your OpenRouter key — in that order, falling back automatically. Logins you already pay for first, the pay-per-image key last. No new API balance, no browser tabs, and no credential ever handed to a model.

$ brushrouter gen "a red fox in snow" -o fox.png
✓ generated with gemini (gemini-3.1-flash-image-preview)
  /home/you/fox.png (image/png, 1250 KB)

It also ships a prompt library of 27 tested recipes and 137 vendored visual styles, because routing a bad prompt to a working backend just gets you a bad image faster.

  • Zero new spend per image on routes that reuse a login you already pay for

  • One command to start — brushrouter setup detects what's installed and connects it

  • No tokens exposed — models never see your credentials, enforced by a redactor on every output

  • 164-prompt library — an agent asks for "a nice product shot" and gets a tested skeleton to fill in, not its own invention

  • MCP-native — brushrouter install wires it into any agent's image work in one command

Install

Requires Node.js 20+.

Run instantly with npx:

npx brushrouter setup

Or install via one-line script:

curl -fsSL https://raw.githubusercontent.com/avinaxhroy/brushrouter/main/install.sh | bash

Or globally via npm:

npm install -g brushrouter
brushrouter setup

Or manually from source:

git clone https://github.com/avinaxhroy/brushrouter.git
cd brushrouter
npm link
brushrouter setup

setup auto-detects image backends (ChatGPT desktop CLI, ChatGPT subscriptions, Google/Antigravity CLI, agent auth stores), offers one-command login if none are found, verifies brushrouter is on your PATH, wires every detected agent's MCP config, and offers an optional test generation. Diagnose any time:

brushrouter doctor     # diagnoses all provider backends + agent MCP integrations

Related MCP server: image-gen-mcp

Use

brushrouter gen "<prompt>" [options]

  -p, --provider <id>   Force a provider (see `brushrouter providers`)
  -f, --fallback        With --provider, fall back to others on failure
  -o, --out <path>      Output path (default: brushrouter-<timestamp>.png)
  -s, --size <WxH>      e.g. 1024x1024, auto
  -q, --quality <lvl>   low | medium | high | auto
      --background <b>  opaque | transparent (codex route sends the API field;
                        other routes ask for it in the prompt)
      --images <path>   Reference image for image-to-image (repeatable)
  -m, --model <id>      Pin a model for this call
      --dry-run         Print the route order that would be attempted, then stop
      --force           Overwrite --out instead of writing a new versioned file
      --json            Machine-readable output
  -v, --verbose         Show routing decisions

All commands (br works everywhere as a shorthand for brushrouter):

br providers        # what is connected, and how
br routes           # which routes auto-routing may use
br models           # model ids (live for OpenRouter)
br prompts          # 27 recipes + 137 looks for briefing an image
br doctor           # full connectivity diagnosis
br setup            # first-run auto-connect
br login            # sign in to ChatGPT for the codex route (browser OAuth, no API key)
br vendor gptweb2api  # install the browserless ChatGPT-web gateway binary
br batch            # generate several prompts in parallel across backends
br init             # write a starter config
br install          # add brushrouter to detected agent MCP configs
br rules            # inject compact guidance into AGENTS.md / .cursorrules
br update           # update to latest version and verify health
br serve            # run as an MCP server on stdio

Reference images

--images only works on routes that accept it; brushrouter providers marks the ones that don't. Passing it to a route that can't is an error when you forced the provider, a warning when auto-routing picked it.

route

reference images

codex-cli, codex

yes

openrouter

yes (input_references data URLs)

custom-http

yes ({image} data URL in your body template)

gemini, native-web, gptweb2api, fal

no — text-to-image only

Transparent backgrounds

brushrouter gen "a red paper lantern sticker" --background transparent -o sticker.png

Best on the codex/codex-cli routes, where transparent is a real API field (output_format: "png" makes the backend honor it). Other routes can only ask in the prompt, so check the file. Even with the field set, the model decides — subject-shaped prompts (sticker, isolated object) reliably come back RGBA; full-scene wording may stay flattened.

Codex size behavior

Codex doesn't take pixel dimensions and reads aspect ratio from the prompt. brushrouter appends your -s 1672x941 as "width 1672 pixels, height 941 pixels (16:9)" — common aspect ratios land exactly or within 1px; other exact pixel counts aren't guaranteed.

Pinning a model

brushrouter models lists what each provider can do; for OpenRouter the list is live, so it shows exactly what your key can reach.

brushrouter models openrouter
brushrouter gen "a red fox" -p openrouter -m x-ai/grok-imagine-image-2.0

A pin (-m, or providers.<id>.model in config) is never silently swapped. With no pin, brushrouter walks a preference list and takes the first model that works. Pins are honored by codex-cli, codex, openrouter, gemini, and custom-http; gptweb2api reports that it ignored the pin rather than pretending otherwise.

Models are resolved at runtime, not hardcoded — the Codex backend reads your ~/.codex/config.toml, and a different model is only tried when the error is specifically "model not found". Quota (429 usage_limit_reached), auth (401), and regional/balance errors are reported as-is and never masked by a retry.

Turning routes off

brushrouter routes                    # show state, in auto-routing order
brushrouter routes disable gptweb2api # skip it during auto-routing
brushrouter routes enable  gptweb2api

A disabled route is still reachable with --provider <id>. Non-interactive: BRUSHROUTER_DISABLE=gemini,fal and BRUSHROUTER_ENABLE=openrouter.

Auth sources for ChatGPT routes

The codex and codex-cli routes use ChatGPT browser login — no API key anywhere. Credentials come from the first of:

  1. providers.codex.authFile in config, or BRUSHROUTER_CODEX_AUTH_FILE (explicit)

  2. ~/.config/brushrouter/auth.json — written by brushrouter login

  3. ~/.codex/auth.json — Codex CLI / ChatGPT desktop app

  4. ~/.local/share/opencode/auth.json — OpenCode login

  5. ~/.omp/agent/auth.json, ~/.pi/agent/auth.json — pi and oh-my-pi OAuth logins

brushrouter login opens a browser and signs in via the codex CLI's public OAuth client (PKCE, localhost:1455 callback) — nothing else to install, and the token refreshes itself. It prints plan go/plus/... so you know which subscription is billing the route. Sign out by deleting ~/.config/brushrouter/auth.json. Free-plan users get a plain "plan-gated" message, not a raw 403.

Providers

id

needs

notes

codex-cli

ChatGPT/Codex desktop app

Native, preferred. Drives the app's own signed CLI — already logged in, brushrouter never sees the token.

codex

ChatGPT login with a paid plan (brushrouter login, or any login on disk)

Direct backend call (gpt-image-2). Shares the Codex text quota.

gemini

Antigravity (agy) or Gemini CLI login

Delegates to the CLI; Google tokens never touch brushrouter.

openrouter

sk-or-... key

Off by default — the only route billed per request. Key auto-discovered from env or existing agent stores.

fal

fal.ai key

gptweb2api

ChatGPT login (free plans work) + vendored gateway — brushrouter vendor gptweb2api

Off by default. Browserless web-session route, separate quota bucket (~2-3 free-plan images/day; see docs/TECHNICAL.md).

custom-http

your own endpoint

Template-based; point it at any image API.

Default auto-routing order:

codex-cli → codex → gemini → openrouter → fal → gptweb2api → custom-http

Three routes are off by default: openrouter (spends money), gptweb2api (needs vendor), custom-http (needs an endpoint). The priority list is authoritative — anything not listed is never auto-selected.

A route that is off is off for every path, including an explicit --provider: naming one is refused with a PROVIDER_DISABLED error that says how to turn it on. That matters most for the metered routes — enabled: false is the only thing between an agent holding a key and your credits, so it is not a preference a caller can talk its way past. Opt in once, deliberately:

brushrouter routes enable openrouter

OpenRouter runs two generation surfaces: multimodal chat models on /chat/completions, image-only models on /images. A "no endpoints found for output modalities" rejection is auto-retried on /images; auth, region, and credit errors are surfaced as they came.

The prompt library

An agent asked for "a nice product shot" will invent a worse prompt than one already tested. So the tool that routes the generation also hands the agent a skeleton to fill in.

164 entries:

kind

what it is

count

source

job

a layout to fill in — a product shot, a character sheet

27

written here

look

a fixed visual system to borrow — an editorial or poster style

137

vendored, MIT

brushrouter prompts                        # the map: what kinds of image exist
brushrouter prompts "product hero shot"    # 1 best match + 3 similar
brushrouter prompts "flyer" -n 1           # force exactly one
brushrouter prompts -c infographic --full  # a whole category, full skeletons
brushrouter prompts --input image          # only recipes that can use a photo
brushrouter prompts --kind look "mono noir editorial"
brushrouter prompts --id logo-mark --full --set text=ACME --set subject="a peak"

Design facts, briefly: every recipe works with no reference image (each has a text skeleton, plus an optional image upgrade naming what it accepts); unfilled {slots} are reported in missing rather than shipped to the image model; a query that matches nothing returns nothing — search is lexical with a hand-written alias map (hero shot → product-hero, dashboard → app-screen). Results are previews by default (id, goal, when to use it, missing slots, credit); --full for the skeleton; limit is capped at 10 because this fills the caller's context window.

Modes: --mode categories for a vague request, --mode related (default) for one best match plus alternatives, --mode one for a single entry.

Attribution

The 137 looks come from VigoZhao/AI-Visual-Prompt-Cookbook, published under MIT. They're vendored — pinned by commit, verified by hash, normalized into the same schema, shipped with the upstream licence and ATTRIBUTION.md. Every look carries its credit on the result itself, previews included; the licence requires it. Point BRUSHROUTER_STYLES_FILE at nothing and the library runs jobs-only instead of crashing.

Reproduce the data — neither source is read at runtime, neither is hand-edited:

npm run recipes:check    # validate every recipe and look against the schema
npm run styles:vendor    # re-vendor the looks from the pinned commit
npm run styles:check     # re-fetch upstream, recompute stylesSha256, compare
npm run corpus:analyze   # reproduce the structural findings behind the design
npm run corpus:check     # re-fetch upstream, recompute corpusSha256, compare

The 150-prompt corpus in data/prompts/ is a development fixture, excluded from the npm tarball; nothing at runtime reads it. Schema, retrieval design, and failure modes are in docs/TECHNICAL.md.

MCP server

brushrouter install detects and wires brushrouter into your installed agents (Claude Code, Claude Desktop, Cursor, Windsurf, OpenCode, Pi, Oh-My-Pi, DeepSeek Harness/DSH, CommandCode, Glass, shared MCP configs). Configs are backed up first (.bak-...), comments and formatting preserved, and no existing servers removed. Or add it by hand:

{
  "mcpServers": {
    "brushrouter": { "command": "brushrouter", "args": ["serve"] }
  }
}

DeepSeek Harness (DSH) has no mcpServers JSON — its servers are YAML patch entries in ~/.dsh/profiles/<profile>/cordis.patch.yml (profile defaults to desktop; set BRUSHROUTER_DSH_PROFILE to change). brushrouter install --agent dsh writes the marker-bounded, idempotent block for you, or by hand:

- id: mcp-brushrouter
  name: "@deepseek-ai/dsh-mcp-client"
  config:
    serverName: brushrouter
    transport: stdio
    command: brushrouter
    args: ["serve"]
    toolCallTimeoutMs: 300000

Exposed tools:

  • generate_image — { prompt, provider?, model?, fallback?, size?, quality?, out?, images?, async? }. Returns the saved path plus the image, all text through the redactor. async: true returns a jobId immediately instead of blocking for a minute or two.

  • image_result — { jobId }. Collects an async generation; poll again on a status while the job runs.

  • find_image_prompts — { query?, category?, kind?, input?, shape?, mode?, limit?, full?, id?, values?, seed? }. Returns previews or a full skeleton to build from before generating. Its description tells the agent every entry works from text alone, that {braces} must be filled first, and to pass input: "image" only when it actually holds a photo.

An agent's normal loop is find_image_prompts → fill the skeleton → generate_image. Two separate tools on purpose: the agent reads and changes the prompt instead of a router substituting one.

The initialize result also carries an instructions string telling the model when to use brushrouter (configurable via instructions in config or BRUSHROUTER_INSTRUCTIONS; "" disables). And for rule-file agents, brushrouter rules injects ~60 tokens of guidance into the rule files of 12+ harnesses — AGENTS.md (Codex, OpenCode, Zed, Pi, Oh-My-Pi, DeepSeek Harness), CLAUDE.md (Claude Code), GEMINI.md (Gemini CLI), .cursorrules / .cursor rules (Cursor), .windsurfrules (Windsurf), .github/copilot-instructions.md (Copilot), .junie/guidelines.md (Junie) — demarcated so it updates in place:

brushrouter rules             # syncs guidance where the harness is detected (smart
                              # default: file exists OR the harness is installed)
brushrouter rules -v          # like dry-run detail: see what was skipped and why
brushrouter rules --dry-run   # preview changes
brushrouter rules --create    # create missing *project* rule files (e.g. AGENTS.md)
brushrouter rules --global    # restrict to global rule files (creates globals
                              # alongside --create; bare --create never does)
brushrouter rules --all       # target every known harness, even undetected ones
brushrouter rules --only codex-global,project-agents  # cherry-pick by target id
<!-- BRUSHROUTER_START -->
## Image Generation (brushrouter)
Use the `brushrouter` MCP server (`generate_image`, `find_image_prompts`) or CLI `brushrouter gen "<prompt>" -o <path>` for creating or editing any images, UI mockups, icons, or diagrams. Avoid manual script creation or ad-hoc web scraping for image tasks. For long generations, pass `async: true` and check `image_result`.
<!-- BRUSHROUTER_END -->

Harnesses with a rule-file layout we don't know yet: paste that block into whatever context file the harness reads (system prompt, project rules, slash-command definition) — it's plain Markdown and updates in place on the next rules run if the markers are kept.

Aspect ratios and slow generations: MCP has no standard late-answer for a tools/call and most clients don't implement Tasks yet, so the portable contract is a job id — generate_image { async: true } returns immediately and image_result collects. notifications/progress and tasks/status may be emitted, but polling is the contract.

Updating

brushrouter update          # pulls latest (git or npm global), refreshes links, runs doctor
npm install -g brushrouter@latest

Configuration

Nothing is hardcoded. Every path, binary, model, and endpoint is discovered at runtime, and every one can be overridden.

Precedence: CLI flags → env → ./brushrouter.config.json → ~/.config/brushrouter/config.json → defaults.

One exception: a project config may not set baseUrl, url, cli, command, args, driver, authFile, or profile — those are read only from user config, env, or a CLI flag. A project file travels with checkouts you didn't write, and a redirected baseUrl would hand your API key to whoever wrote it. Ignored keys are reported on stderr; per-project endpoints go in BRUSHROUTER_CUSTOM_URL.

// ~/.config/brushrouter/config.json
{
  "priority": ["codex-cli", "gemini", "openrouter"],
  "instructions": "brushrouter is the preferred tool for any image work...",  // MCP session guidance; "" disables
  "providers": {
    "codex-cli": { "cli": null, "model": null },   // null => auto-discover
    "openrouter": {
      "model": null,                                // pin one, or null to use the list below
      "models": ["google/gemini-3.1-flash-image"],  // your preference order
      "resolution": null                            // optional, for /images models
    },
    "gptweb2api": {
      "enabled": false,
      "bin": null,          // null => <config dir>/gptweb2api/gptweb2api
      "authFile": null,     // null => the codex-route credential ladder
      "projectId": null,    // ChatGPT project id (g-p-...) — parks gen chats in that workspace
      "idleMs": 600000      // gateway process exits after this idle time
    },
    "custom-http": {
      "enabled": true,
      "url": "https://api.example.com/v1/images",
      "body": "{\"prompt\": \"{prompt}\", \"size\": \"{size}\"}",
      "response": "json:data.0.url",   // or json:images.0.b64_json, or raw
      "keyHeader": "Authorization",
      "keyPrefix": "Bearer "
    }
  }
}

Environment variables

variable

purpose

BRUSHROUTER_PRIORITY

comma-separated auto-route order

BRUSHROUTER_INSTRUCTIONS

session guidance sent to MCP agents in initialize (empty string disables)

BRUSHROUTER_DISABLE / BRUSHROUTER_ENABLE

comma-separated routes to turn off / on (unknown name is an error, not a silent no-op)

BRUSHROUTER_RECIPES_DIR

use a different recipe directory (same schema)

BRUSHROUTER_STYLES_FILE

use a different vendored styles file

BRUSHROUTER_APP_DIRS

extra dirs to scan for app bundles (auto-discovery)

BRUSHROUTER_MCP_CONFIGS

extra agent MCP config paths for install

BRUSHROUTER_CODEX_CLI

explicit path to a Codex CLI binary

BRUSHROUTER_CODEX_CLI_MODEL

model for the native codex-cli route

BRUSHROUTER_CODEX_MODEL

model for the direct codex provider

BRUSHROUTER_CODEX_AUTH_FILE

explicit auth file for the direct codex provider

BRUSHROUTER_CODEX_CLIENT_ID

override the derived OAuth client id

BRUSHROUTER_CODEX_ENDPOINT

override the Codex responses endpoint

BRUSHROUTER_OAUTH_TOKEN_ENDPOINT

override the OAuth token endpoint

BRUSHROUTER_ORIGINATOR

originator header sent to the Codex backend

BRUSHROUTER_GEMINI_CLI / BRUSHROUTER_GEMINI_MODEL

Gemini CLI path / model

BRUSHROUTER_OPENROUTER_KEY / BRUSHROUTER_OPENROUTER_MODEL

OpenRouter key / pinned model

BRUSHROUTER_OPENROUTER_MODELS

comma-separated OpenRouter preference order

BRUSHROUTER_OPENROUTER_RESOLUTION

image resolution requested from OpenRouter

BRUSHROUTER_FAL_KEY / BRUSHROUTER_FAL_MODEL

fal.ai

BRUSHROUTER_GPTWEB_COOKIE

cookie fallback for the vendored gptweb2api route

BRUSHROUTER_GPTWEB_PROJECT_ID

ChatGPT project (g-p-...) that anchors gptweb2api generations

BRUSHROUTER_CUSTOM_URL / BRUSHROUTER_CUSTOM_KEY

custom HTTP

BRUSHROUTER_OPENAI_KEY

fallback key name read for the OpenAI-compatible routes (OPENAI_API_KEY also works)

Discovery scans $PATH, /Applications, ~/Applications, the npm global root, and ~/.cache/codex-runtimes, so a Codex CLI installed any of the usual ways is found without configuration.

Security

A model never sees your credentials. Design constraint, not a setting:

  • Keys live in the environment or a 0600 ~/.config/brushrouter/keys.json inside a 0700 directory. Config files hold non-secret settings only.

  • Every string brushrouter emits — CLI output, --json, errors, doctor, MCP results — passes through a redactor that strips known secret values and well-known token shapes (JWTs, sk-..., ya29.…, Bearer ..., refresh tokens), plus tokens registered by value from disk. The test suite asserts a fake secret survives no output path.

  • codex-cli and gemini never read a token at all: they spawn a child process that owns its own authentication.

  • What it does not confine: --out writes anywhere you say and --images reads any local file that sniffs as an image. Deliberate — the caller is your own agent, which already has those abilities; the MCP tool description states the capability rather than implying a sandbox.

Limits

brushrouter doesn't pretend a broken backend works.

  • Quota (429 usage_limit_reached) is reported as quota, with reset time, and never triggers a wasteful model retry.

  • Auth (401, or not logged in) is reported as auth, with the command to fix it.

  • A retired model (404 model_not_found) is the only case that tries the next model.

  • A deprecated free tier (e.g. Google's retired Gemini CLI individual tier) is labeled as such.

The Codex text quota and the native image path share a bucket; when exhausted, the native route says so and routing moves on. The ChatGPT web Images bucket is separate, but reaching it needs the gptweb2api route — the app's own conduit is App-Attest protected and can't be forged.

Why I built this

Image generation is the one thing I refuse to pay per image for. I already pay for ChatGPT and already have a Google login — two capable image generators sitting on this machine — and I was still opening a browser tab or topping up an API balance. So the route order falls straight out of "spend what I already pay for first". The other two rules: ship the prompts too (a free image from a bad prompt is a wasted call), and never let a model see a token or pretend a broken backend works.

Development

npm test                        # 144 tests, no dependencies
npm run recipes:check           # validate every recipe and look against the schema
npm run styles:check            # re-fetch the pinned cookbook, verify stylesSha256
node bin/brushrouter.js doctor

Zero runtime dependencies, ESM, Node 20+.

Credits and license

MIT for the code, for the recipe library in data/recipes/, and for the normaliser that produces data/styles/. We wrote the recipes.

The direct Codex backend image generation integration (images/generations and images/edits for gpt-image-2), subscription auth discovery patterns, and prompt-driven aspect ratio handling were learned from and inspired by yuji-hatakeyama/opencode-gpt-imagegen.

The 137 looks in data/styles/ are vendored from VigoZhao/AI-Visual-Prompt-Cookbook at a pinned commit, MIT (Copyright (c) 2026 VigoZhao); upstream LICENSE and ATTRIBUTION.md ship with them.

The 150-prompt corpus in data/prompts/ is a development fixture, not shipped and not covered by that licence — third-party text redistributed under its upstream terms (115 rows CC0, 35 rows CC-BY, authors credited in data/prompts/ATTRIBUTION.md; full terms in data/prompts/LICENSES/). Quote a prompt along with its author and sourceUrl.

Disclaimer

This is an unofficial, third-party tool, not affiliated with or endorsed by OpenAI, Google, OpenRouter, fal.ai, OpenCode, or any other supported provider.

Use of this tool must comply with the terms of use, usage policies, and service agreements of whichever model providers or platforms you access through brushrouter.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Enables coding agents to generate and edit images using Gemini and OpenAI image models, saving files directly into the project with configurable providers, models, and security restrictions.
      3
      89 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      A
      maintenance
      Enables AI agents to generate images via multiple providers like OpenAI and Google, with automatic failover and configurable retry settings.
      171 npm
      MIT
    • A
      license
      Not graded
      quality
      A
      maintenance
      Enables AI assistants to generate and edit images from natural-language requests, automatically enhancing prompts and applying quality/provider presets across Gemini, OpenAI, and BytePlus Seedream.
      1,781 npm
      MIT