Live model catalogue from OpenRouter + per-generation cost - #6
Open
pallaoro wants to merge 1 commit into
Open
Conversation
…generation costs
The model picker was a hardcoded array. Half its OpenRouter entries no longer
exist upstream — flux.2-max, flux.2-klein-4b, riverflow-v2-fast, seedream-4.5,
gpt-image-1 and gpt-image-2-2026-04-21 are all absent from the full 424-model
catalogue, so picking one could only fail at generate time. The README
advertised two of them as headline features.
List the catalogue live instead. `/api/v1/models` is the upstream source of
truth and already carries pricing, so the same request that fixes the rot also
pays for cost visibility — the loudest complaint about hosted canvas tools
being that you cannot tell what a run will cost until it has cost it.
- `/api/models` returns the live image-output models with their price, falling
back to a verified static list only if the fetch fails, so the picker is
never empty.
- OpenRouter generate calls now send `usage: { include: true }` and the cost is
returned, persisted on `generations.cost_usd`, and totalled per run and per
workflow.
- Models are tagged `$`–`$$$$` by price relative to the cheapest available.
Not an absolute per-image figure on purpose: upstream prices per output image
*token* and the token count varies by model and resolution, so a "$0.04 per
image" label would be invented precision. The token price is comparable
across models, which is what choosing between them needs.
- Cost renders in credits when the app is deployed on Clawnify (detected via
the injected CLAWNIFY_TOKEN) and in dollars when self-hosted, since a
self-hoster pays OpenRouter directly. Credits are never shown next to a
dollar figure.
Reading `usage.cost` alone would have shown $0.00 for most generations: on a
BYOK provider key OpenRouter bills 0 and reports the real charge under
`cost_details.upstream_inference_cost`. Verified live — a Gemini image call
returns cost 0 with an upstream cost of $0.0336.
Also fixes two pre-existing bugs found while testing this end to end:
- `@clawnify/db` was pinned at ^0.2.0 while `@clawnify/app` depends on ^0.4.1,
so two copies of the module loaded: createApp initialised one and every route
read from the other. Every database route in the template returned
"not initialized — call initDB(env) first". Aligning the pin fixes it.
- Quick Generate sent `workflow_id: 0` as a number against a `z.string()`
schema, so saving a quick generation always returned 400.
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.
Why
The model picker was a hardcoded array, and it had rotted. Of its 12 OpenRouter entries, 6 no longer exist upstream — checked against the full 424-model catalogue:
black-forest-labs/flux.2-maxblack-forest-labs/flux.2-klein-4bsourceful/riverflow-v2-fastbytedance-seed/seedream-4.5openai/gpt-image-1openai/gpt-image-2-2026-04-21No FLUX, SeedDream or
gpt-imagemodel is on OpenRouter at all any more. Picking one could only fail at generate time, and the README advertised two of them as headline features.Separately, the app showed no cost anywhere. For a canvas tool that is the sharpest gap: the recurring complaint about hosted canvas tools (Krea, Flora, Figma Weave) is that you cannot tell what a run will cost until it already has. Adjacent models here differ by 15× —
gpt-5-image-minitogemini-3-pro-image— with nothing in the UI to say so.Both problems have one root: the catalogue was hardcoded.
/api/v1/modelsis the upstream source of truth and carries pricing, so listing live fixes the rot and pays for the cost display in the same request.What changed
/api/modelsis live. Filtered to image-output models, price attached. A verified static list remains as a fallback only if the fetch fails, so the picker is never empty. Withdrawn models now disappear on their own.usage: { include: true }; the cost is returned from/api/generate, persisted ongenerations.cost_usd, and totalled per run and per workflow.$–$$$$against the cheapest available model.Two deliberate judgement calls
Relative tiers, not a per-image price. Upstream prices per output image token, and the token count varies by model and resolution — Gemini 3.1 Flash Image billed 1120 image tokens at
0.00006=$0.0672. A "$0.04 per image" label would be invented precision, so the picker compares models (the token price is directly comparable) and the actual figure is only ever shown after a real generation reports it.Credits vs dollars. On Clawnify the app receives an opaque
OPENROUTER_API_KEYand cannot tell a managed sub-key (billed in credits) from an org's own key.CLAWNIFY_TOKENis injected into every app deployed on Clawnify and absent from a self-hosted clone, so it is the discriminator. Credits are never rendered beside a dollar figure. Known edge, documented inpricing.ts: an org that brings its own OpenRouter key and deploys on Clawnify sees credits — still the correct unit for a Clawnify surface, and rare.The bug that only a live call would have caught
Reading
usage.costwould have shown $0.00 for most generations. On a BYOK provider key OpenRouter bills 0 and reports the real charge undercost_details.upstream_inference_cost:extractCostUsdfalls through to the upstream figure.Two pre-existing bugs fixed
Both surfaced while testing this end to end, and both blocked verifying it:
@clawnify/dbwas pinned at^0.2.0while@clawnify/appdepends on^0.4.1, so two copies of the module loaded —createAppinitialised one, every route read from the other. All DB routes returned@clawnify/db: not initialized — call initDB(env) first. Aligning the pin fixes it.workflow_id: 0as a number against az.string()schema, so every save returned 400.Verified
Against the real OpenRouter API with a live key, not just a green build:
/api/modelsreturns 9 live OpenRouter models with prices; the 6 dead IDs are gonecostUsd: 0.033601, persists, and reads back$0.0336 + $0.0672→$0.10, shown as "Last run $0.10 (2 images)"CLAWNIFY_TOKENset: 7 and 14 credits per image, 21 credits total, no dollars alongsidegpt-5-image-mini$→gemini-3-pro-image$$$$pnpm buildclean