Skip to content

feat: generate images via Gemini or OpenRouter - #89

Open
jsnapoli1 wants to merge 2 commits into
AJFrio:mainfrom
jsnapoli1:feat/openrouter-image-generation
Open

feat: generate images via Gemini or OpenRouter#89
jsnapoli1 wants to merge 2 commits into
AJFrio:mainfrom
jsnapoli1:feat/openrouter-image-generation

Conversation

@jsnapoli1

Copy link
Copy Markdown

Stacked on #87 (Developer Settings) — provider and model are configured there. Supersedes #86, which only swapped the model id; close that if this lands.

Problem

Two things:

  1. The feature is broken. /api/admin/ai/generate-image calls gemini-2.5-flash-image-preview, which Google shut down on 2026-01-15. Every request fails.
  2. It needs a second API key even for stores that already configured OPENROUTER_API_KEY for the agent feature.

Approach

An ImageGenerationService with two providers behind one contract:

gemini (default) — Google directly, on gemini-3.1-flash-image ("Nano Banana 2"), the replacement Google names for the retired model.

openrouter — the unified POST /api/v1/images endpoint, reaching ~48 image models from ByteDance, Black Forest Labs, Qwen, Recraft, Sourceful and Google under one key.

Deliberately not /chat/completions with the openrouter:image_generation server tool: that reaches image models only via an extra LLM inference pass and hides the generation parameters.

Both return { mimeType, dataBase64 }, so the admin UI is untouched. With no explicit IMAGE_PROVIDER, whichever key exists is used — a store that has only ever set one just works.

On cost — worth being precise

OpenRouter is not cheaper for the same model. Its Gemini token rate is byte-identical to Google's ($60/1M output tokens), but credit purchases carry a ~5.5% fee:

Direct Via OpenRouter
gemini-3.1-flash-image $0.0670 $0.0707

So gemini stays the default. OpenRouter earns its place through model choice, not price — these all clear the 4-reference bar the UI needs:

Model Per image
black-forest-labs/flux.2-klein-4b $0.014
sourceful/riverflow-v2.5-fast $0.019
qwen/qwen-image-3 $0.030
google/gemini-3.1-flash-lite-image $0.034
bytedance-seed/seedream-5-0-lite $0.035

Prices from OpenRouter's live API (/api/v1/images/models/<id>/endpoints), not marketing pages. Cheaper isn't automatically better — reported text-rendering quality varies considerably, which matters for apparel mockups with printed logos. Documented as such rather than picking a cheap default.

Reference images

Translated per provider: inline_data parts for Gemini, data-URL input_references for OpenRouter. Still capped at the 4 the UI sends — several OpenRouter models (all Krea, Microsoft MAI, most Recraft variants) accept only one, so raising the cap needs a per-model capability check.

Verification

Adds tests/integration/image-generation.test.js (8 cases): no-provider error, each provider selected by key presence, explicit override, reference images as data URLs, the 4-image cap, upstream failure surfacing as 502, and invalid-provider rejection at save time.

Test Files  25 passed (25)
     Tests  295 passed (295)

npm run lint — 0 errors. Build clean. npm run harness:docs passes.

Not verified: I have no Gemini or OpenRouter key, so neither provider was exercised against a live API. Request and response shapes come from OpenRouter's live model metadata and its image-generation guide (data[0].b64_json, data[0].media_type, input_references as data URLs), and the tests mock fetch at those shapes — but a smoke test with real keys is worth doing before merge.

https://claude.ai/code/session_015XLFFfHWNeazSuz6UcsQ4C

jackn added 2 commits September 2, 2026 09:45
Setting up a store currently means running `wrangler secret put` for
every key. That is a poor handoff for a shop owner who just wants to
turn on AI images or connect Stripe.

Adds a Developer Settings page for STRIPE_SECRET_KEY, GEMINI_API_KEY,
OPENROUTER_API_KEY, OPENROUTER_MODEL, SITE_URL and the admin password.

A Worker cannot write its own secrets — `env` is injected per request and
is read-only, and changing a Cloudflare secret needs an account-scoped
API token that has no business inside a store. So values are stored in
KV, and every runtime read goes through resolveSetting(), which checks
KV before `env`. A value set in the UI therefore genuinely takes effect
rather than being shadowed by a stale binding.

Clearing a field deletes the KV entry and restores the `env` value. That
is also the recovery path if a bad value is saved:

    wrangler kv key delete developer:settings --namespace-id <id> --remote

The admin password gets extra handling, since 295ee74 deliberately moved
auth onto the ADMIN_PASSWORD binding:

- stored salted-and-hashed, never as recoverable plaintext
- changing it requires the current password, so an unattended session
  cannot be used to take the store over
- DELETE drops the override and falls back to the binding
- it cannot be set through the generic settings route

Secret values are write-only over the API: responses report only whether
a key is set and where it came from, never the value.

Adds tests/integration/developer-settings.test.js (9 cases), covering
KV-over-env precedence, fallthrough on clear, the recovery path, and
that plaintext is never persisted. Full suite: 287 passing.

Claude-Session: https://claude.ai/code/session_015XLFFfHWNeazSuz6UcsQ4C
Image generation was hardwired to gemini-2.5-flash-image-preview, which
Google shut down on 2026-01-15 — so the feature fails outright — and it
required a second API key even for stores that already have OpenRouter
configured for the agent.

Adds an ImageGenerationService with two providers behind one contract:

- gemini: Google directly. Defaults to gemini-3.1-flash-image, the
  replacement Google names for the retired model. Still the cheapest
  route to it: OpenRouter matches Google's token rate but adds ~5.5% on
  credit purchases.
- openrouter: the unified /api/v1/images endpoint, reaching ~48 models
  from ByteDance, Black Forest Labs, Qwen, Recraft, Sourceful and Google
  under one key. Useful for trying a cheaper or better-suited model
  without a billing relationship per vendor.

Not /chat/completions with a server tool — that reaches image models only
through an extra LLM pass and hides the generation parameters.

Both return { mimeType, dataBase64 }, so the admin UI is unchanged. With
no explicit IMAGE_PROVIDER, whichever key exists is used, so a store that
has only ever set one just works.

Reference images are translated per provider: inline_data parts for
Gemini, data-URL input_references for OpenRouter. Still capped at the 4
the UI sends; several OpenRouter models accept only one, so raising it
needs a per-model check.

IMAGE_PROVIDER is validated on save, since a typo would otherwise surface
much later as a confusing "not configured" error.

Adds tests/integration/image-generation.test.js (8 cases). Full suite:
295 passing.

Claude-Session: https://claude.ai/code/session_015XLFFfHWNeazSuz6UcsQ4C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant