Building a model picker from the models endpoint
Hardcoded model ids go stale. Build the picker from the live catalog endpoints so new and retired models appear without a deploy.
A hardcoded list of model names is a deploy cycle behind the catalog. Names land, names get retired, edit variants split off their text-to-image siblings, and your picker keeps offering a string that now 400s as Model not supported. The fix is to stop treating model ids as config and start treating them as a live read: open the picker from the catalog endpoints, POST the name you just fetched, and let retirement be a 404 on the list rather than a failure at generate time.
The public models pages are not that list. A model can exist in the catalog JSON and still have no page, because short descriptions are dropped and variants fold into a parent. Drive the picker off the API.
Two endpoints, two jobs
You need both. Using only one is how pickers rot in a different way.
GET /api/v1/ai-models/images and GET /api/v1/ai-models/videos are the catalog. They return the rows a human should see: name, display_name, slug, description, content_type, requires_image, categories, is_featured, is_active, credits, duration/aspect/resolution support, rankings. Default is active rows only. Add pickable=true and the server drops the names that do not belong on a generate picker at all: upscalers, background-removal and colorize models (they have their own surfaces), storyboard-only names, inpainting, and the task-id extend models that 400 without a prior provider task id.
The same handlers accept provider, category, and is_featured. Image and video both return a filters object with primary and secondary chips already computed from the rows (text-to-image, image-to-video, reference-to-video, and so on), so you do not have to reverse-engineer a chip rail. That is the same split the category browse uses on the site.
GET /api/v1/generate/models is the dispatch table. No query: { providers, priority }, models grouped by provider and by type (image, video, lipsync, audio, story, background_removal, image_upscale, video_upscale). With ?provider=kie (or runpod, fal, replicate, and the others in priority): just that provider's lists. These are the strings the generate controllers will actually route. If a name is on a catalog card and missing here, it will not generate, however pretty the card is.
A picker that shows catalog rows the dispatch table cannot route is a support queue. A picker that shows dispatch names with no requires_image flag is a 400 the first time someone picks an edit model. Join on name.
What to render, and what to hide
Start from pickable=true. Then filter in the client only for things the user has actually chosen: modality first, then duration ceiling, then whether they have a reference image. The elimination protocol that takes a huge catalog down to eight is the product version of this; the picker is the mechanical version.
Minimum fields to keep on the option:
| Field | Use |
|---|---|
name |
The string you send in model on generate. Never the slug. |
display_name |
What the human sees, if present; else name. |
requires_image |
Disable (or hide) when the user has not uploaded a reference. |
categories |
Chip rail. text-to-image and edit-image are different options even inside one family. |
supports_aspect_ratios / supports_durations |
Snap the shared form so you do not POST 21:9 to a model that does not take it. |
credits |
Headline on the row. Do not treat it as the charge for a given duration or resolution. |
is_featured |
A "recommended" rail, not a hard filter. |
GET /api/v1/ai-models/requires-images?content_type=image is the same requires_image set as a flat list, if you would rather prefetch the gate than inspect each row.
Do not offer upscale, background-removal, or colorize in this picker. pickable=true already removed them. If you skip that flag, you will render names whose generate POST 400s because they belong on a different endpoint (/generate/image-upscale, /generate/background-removal, /generate/video-upscale).
Do not offer a storyboard model as if it were text-to-video. It wants scenes, not a prompt. The picker has no scene editor; the dispatch will fail.
Refresh when the picker opens, not only at process boot. Catalog responses are cached on the server for about an hour, which is fine. A worker that fetched once at deploy time is the hardcoded list with extra steps.
Wire it to generate without a second source of truth
The value of a live picker is that the string you display is the string you POST.
- Fetch
/ai-models/images?pickable=true(or/videos). Those catalog reads are public. - Fetch
/generate/modelswith the same key you generate with. That GET sits on the generate router: it needs auth, and a zero-credit account gets403 Insufficient creditsrather than an empty list. - Keep a row only if
nameappears in the dispatch list for that type. - If the user has no reference image, drop
requires_image: true(and anything incategoriesthat isimage-to-image/edit-image/image-to-videodepending on the surface). - On confirm, POST
/generate/imageor/generate/videowithmodelset to thatname(or an array of names, if you are running a bake-off). - Poll
GET /api/v1/status/:requestId.
That is the whole loop. The developer page is the auth and base URL; the text-to-image tool is the interactive version of the same catalog if you want to see how a name behaves before you put it in a product.
Credits: POST /api/v1/ai-models/calculate-credits with { models, contentType, duration, resolution, count } for the current selection. The estimate uses the same pricing function as the charge. Show that number. Do not hardcode a credit cost next to a name; the matrix moves with duration and resolution, which is why the billing shapes index exists.
There is no free model hiding in the list. The cheapest name in the catalog still costs credits. A picker that implies otherwise will send people into a 402.
What goes stale if you skip this
The name. Nano Banana 2 and Nano Banana 2 Edit are not the same route. A hardcoded "Nano Banana" will canonicalise, or 400, or silently take the edit path because you also sent image_urls. Fetching name + requires_image + categories is how you stop guessing.
The input shape. supports_durations is the allowed ladder for that row. Union those sets across a multi-select, then snap per model on dispatch. The studio already does this; a picker that POSTs the raw shared value will fail the models that do not have that duration.
Retirement. is_active defaults to true on the list endpoints. A name that left the catalog leaves your picker on the next fetch. A hardcoded array still offers it, and generate returns Model "<name>" is not available in any provider.
Feature models leaking in. Without pickable=true, "Video Enhancer Pro" looks like a video model and 400s on /generate/video.
Compare pages and side-by-side compare are for humans choosing a default. They are not a feed. If your product needs a default when the user picks nothing, pick from the live featured set (/ai-models/featured?content_type=image) or from the cheapest remaining pickable row, and record which name you substituted. Do not bake "GPT Image 1.5" into a constant.
FAQ
Can I use /models/<slug> as the source of truth?
No. The marketing catalog gate drops short descriptions and folds variants into a parent, so a slug in the data file does not mean a page exists, and a page existing does not mean the generate controllers will route that string. Use /api/v1/ai-models/* joined to /api/v1/generate/models.
Why are there two "models" endpoints?
/ai-models is what to show (labels, flags, rankings, credit headlines). /generate/models is what will route. They drift when a name is catalogued but not on any provider list, or when a provider list still has a name the catalog has deactivated. The join is the picker.
How often should I refetch?
On picker open is enough for an interactive UI. For a headless worker, refetch on a short interval (the catalog cache is already minutes-long) and fail closed if a previously stored name disappears from the dispatch list. Do not cache names across deploys as config.
What about lipsync, audio, upscale, background removal?
Separate lists, separate generate paths. /ai-models/lipsync, /ai-models/audio, /generate/models/background-removal, /generate/models/image-upscale, /generate/models/video-upscale, /generate/story-models. Do not cram them into the image/video picker. pickable=true exists specifically so you do not.