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+.

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

Or manually:

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

Or instantly via npx: npx 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: Corent MCP Server

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). Enable with brushrouter routes enable openrouter. The priority list is authoritative — anything not listed is never auto-selected, though --provider still works.

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, 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"] }
  }
}

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 AGENTS.md / CLAUDE.md / .cursorrules / .windsurfrules, demarcated so it updates in place:

brushrouter rules             # syncs guidance into detected rule files
brushrouter rules --dry-run   # preview changes
brushrouter rules --create    # create a project AGENTS.md if missing
<!-- 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 -->

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 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.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to generate on-brand visuals from ideas, URLs, documents, or PDFs in over 100 formats and 150+ languages, with consistent brand kits.
    8 npm
    1
    MIT
  • A
    license
    A
    quality
    A
    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
    69 npm
    1
    MIT