Building a model picker from the live video catalogue
One read gives you models, clip-length ceilings and resolution multipliers. How to render a picker from it, validate input against it, and cache it sensibly.
If your product lets people choose a video model, the list should come from the platform rather than from your source code. GET /v1/video/capabilities on Infrai returns every model currently available with its vendor, region, clip-length ceiling and image-to-video support, plus the resolutions and the multiplier each one costs. A picker rendered from that response can only ever offer combinations the API accepts.
A hardcoded list, by contrast, drifts the first time the catalogue changes — and it changes. The same read also feeds the rest of the flow on the same credential: the duration ceiling it publishes is what your submit handler validates against before the job goes on POST /v1/queue/publish, so the picker and the enforcement share one source of truth instead of two copies that disagree.
The whole catalogue in one read
curl -sS "https://api.infrai.cc/v1/video/capabilities" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"models": [
{"vendor": "tencent_vod", "model": "Kling:3.0", "region": "china", "max_seconds": 12, "image_to_video": true},
{"vendor": "tencent_vod", "model": "Kling:3.0-Omni", "region": "china", "max_seconds": 12, "image_to_video": true},
{"vendor": "tencent_vod", "model": "Vidu:q3-pro", "region": "china", "max_seconds": 12, "image_to_video": true},
{"vendor": "tencent_vod", "model": "PixVerse:v6", "region": "china", "max_seconds": 12, "image_to_video": true},
{"vendor": "alibaba_intl", "model": "wan2.7-i2v", "region": "china", "max_seconds": 15, "image_to_video": true},
{"vendor": "alibaba_intl", "model": "happyhorse-1.1-t2v", "region": "china", "max_seconds": 15, "image_to_video": true}
],
"resolutions": ["720p", "1080p", "4k"],
"resolution_multiplier": {"720p": 1, "1080p": 2, "4k": 4}
}
}
Four fields per model, each answering a question your UI needs.
max_seconds bounds the duration slider. image_to_video decides whether the reference-image upload is shown at all. region matters for anyone with a data-residency requirement, and it is the field most likely to rule a model out for a customer before any question of quality comes up. And vendor groups the list so a user sees the Kling models together rather than scattered through an alphabetical dropdown of twelve names they have never heard of.
Render the picker from the response
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
let cache = { at: 0, data: null };
const TTL_MS = 10 * 60 * 1000;
/** Cache for ten minutes. The catalogue changes on a scale of weeks, and a read
* per page view is a request per page view for information that didn't move. */
async function capabilities() {
if (cache.data && Date.now() - cache.at < TTL_MS) return cache.data;
const res = await fetch(`${API}/v1/video/capabilities`, {
headers: { authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
// Serve a stale catalogue rather than an empty picker: an old list that mostly
// works beats a form with no options in it.
if (cache.data) return cache.data;
throw new Error(`capabilities unavailable: ${res.status}`);
}
const { data } = await res.json();
cache = { at: Date.now(), data };
return data;
}
export async function pickerOptions({ needsImageInput = false } = {}) {
const caps = await capabilities();
const models = (caps.models ?? []).filter((m) => !needsImageInput || m.image_to_video);
const byVendor = new Map();
for (const m of models) {
if (!byVendor.has(m.vendor)) byVendor.set(m.vendor, []);
byVendor.get(m.vendor).push({
model: m.model,
maxSeconds: m.max_seconds,
region: m.region,
supportsImage: m.image_to_video,
});
}
return {
groups: [...byVendor.entries()].map(([vendor, items]) => ({ vendor, items })),
resolutions: (caps.resolutions ?? []).map((r) => ({
value: r,
costMultiplier: caps.resolution_multiplier?.[r] ?? 1,
})),
};
}
Showing the cost multiplier next to each resolution is a small change with a real effect.
A user who can see that 4k costs four times as much per second picks 720p for drafts without being told to, and the support conversation about an unexpected bill never happens — which is a better outcome than any amount of documentation about resolution pricing, because nobody reads documentation while filling in a form they think they already understand.
Validate the submission against the same source
A picker is a suggestion; the submit handler is the enforcement. Validate model, duration_seconds and resolution against the live catalogue before spending anything:
import os
import time
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
_cache: dict = {"at": 0.0, "data": None}
TTL_SECONDS = 600
def capabilities() -> dict:
if _cache["data"] and time.monotonic() - _cache["at"] < TTL_SECONDS:
return _cache["data"]
resp = SESSION.get(f"{API}/v1/video/capabilities", timeout=25)
resp.raise_for_status()
_cache.update(at=time.monotonic(), data=resp.json()["data"])
return _cache["data"]
def validate(model: str, duration_seconds: int, resolution: str,
with_reference_image: bool = False) -> dict:
"""Refuse locally rather than sending a request that cannot succeed. The
clip-length ceiling is per MODEL, which is the constraint most likely to be
wrong in a form somebody built six months ago."""
caps = capabilities()
entry = next((m for m in caps.get("models", []) if m["model"] == model), None)
if entry is None:
raise ValueError(f"{model} is not in the live catalogue")
if duration_seconds > entry["max_seconds"]:
raise ValueError(f"{model} caps at {entry['max_seconds']}s, asked for {duration_seconds}s")
if resolution not in caps.get("resolutions", []):
raise ValueError(f"{resolution} not available; try {caps.get('resolutions')}")
if with_reference_image and not entry.get("image_to_video"):
raise ValueError(f"{model} does not support an image input")
return {"model": model, "vendor": entry["vendor"], "region": entry["region"],
"units": duration_seconds * (caps["resolution_multiplier"].get(resolution) or 1)}
if __name__ == "__main__":
print(validate("Kling:3.0", 8, "720p"))
Cache, but not forever
| Strategy | Effect |
|---|---|
| Read per request | correct, wasteful |
| Cache 10 minutes | correct enough, one read per interval |
| Cache at boot only | a model added this month never appears |
| Hardcode the list | wrong as soon as the catalogue moves |
| Cache with stale fallback | survives a blip without emptying the picker |
Ten minutes with a stale fallback is the shape to copy. The catalogue changes on a scale of weeks; your cache should tolerate that without pretending it never changes.
Limitations
The catalogue enumerates models for the vendors whose model names it publishes, so it isn’t a complete map of every vendor the generate route accepts — GET /v1/discovery/video.generate and its dynamic_params.vendor list is the authority on accepted vendor values, and the two lists answer different questions. A picker built only from capabilities can’t offer a vendor that has no catalogue entry.
There’s also no per-model price in this response: the rate lives in the billing block of GET /v1/discovery/video.generate and is marked approximate: true because it varies by vendor and model. So a picker can show the resolution multiplier honestly and cannot show a firm per-model price. Going direct to a provider such as Runway or Kling gets you their exact rate card and their full parameter surface, which is the better fit if one model is your whole product.
What the shared credential buys is everything after the picker: the submit on POST /v1/queue/publish, the archive with PUT /v1/storage/object/put/{bucket}/{key}, and one GET /v1/account/usage pricing the lot (verified 2026-09-21). Platform rates drift downward as vendor contracts improve, which is another reason to read them rather than print them.