feat: generate images via Gemini or OpenRouter - #89
Open
jsnapoli1 wants to merge 2 commits into
Open
Conversation
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
This was referenced Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Two things:
/api/admin/ai/generate-imagecallsgemini-2.5-flash-image-preview, which Google shut down on 2026-01-15. Every request fails.OPENROUTER_API_KEYfor the agent feature.Approach
An
ImageGenerationServicewith two providers behind one contract:gemini(default) — Google directly, ongemini-3.1-flash-image("Nano Banana 2"), the replacement Google names for the retired model.openrouter— the unifiedPOST /api/v1/imagesendpoint, reaching ~48 image models from ByteDance, Black Forest Labs, Qwen, Recraft, Sourceful and Google under one key.Deliberately not
/chat/completionswith theopenrouter:image_generationserver 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 explicitIMAGE_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:
gemini-3.1-flash-imageSo
geministays the default. OpenRouter earns its place through model choice, not price — these all clear the 4-reference bar the UI needs:black-forest-labs/flux.2-klein-4bsourceful/riverflow-v2.5-fastqwen/qwen-image-3google/gemini-3.1-flash-lite-imagebytedance-seed/seedream-5-0-litePrices 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_dataparts for Gemini, data-URLinput_referencesfor 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.npm run lint— 0 errors. Build clean.npm run harness:docspasses.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_referencesas data URLs), and the tests mockfetchat those shapes — but a smoke test with real keys is worth doing before merge.https://claude.ai/code/session_015XLFFfHWNeazSuz6UcsQ4C