From 6561e69b8b4356efa20f8e193839266e05516c97 Mon Sep 17 00:00:00 2001 From: Stephen Zweibel Date: Tue, 13 Oct 2026 16:28:49 -0400 Subject: [PATCH] Label speech and image generation models in the registry The Gateway catalog is about to publish its first text-to-image model, and text-to-speech rows already appear. Both showed raw capability IDs without icons, an empty "Context limit: Not published" figure, and "Token prices not published" although Workers AI prices them per image tile or audio minute. Name and illustrate both capabilities, omit the absent context figure for speech and image models, and say they are not priced per token. Co-Authored-By: Claude Opus 5.5 --- src/pages/models.astro | 2 +- src/scripts/model-availability.ts | 15 +++++++++++++-- tests/models_page_browser.py | 5 +++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pages/models.astro b/src/pages/models.astro index 1c7f3ef..0a9065a 100644 --- a/src/pages/models.astro +++ b/src/pages/models.astro @@ -9,7 +9,7 @@ const base = import.meta.env.BASE_URL;

Model API

Model Registry

-

Open-weight models served with zero data retention. Compare context limits and token prices, then copy the exact API ID.

+

Open-weight models served with zero data retention. Compare capabilities, context limits, and token prices, then copy the exact API ID.

How to read the registry Model API documentation diff --git a/src/scripts/model-availability.ts b/src/scripts/model-availability.ts index 21b6c9d..d1dec33 100644 --- a/src/scripts/model-availability.ts +++ b/src/scripts/model-availability.ts @@ -42,6 +42,8 @@ type Offering = z.infer; const capabilityNames = new Map([ ['text-generation', 'Text generation'], ['automatic-speech-recognition', 'Speech recognition'], + ['text-to-speech', 'Speech generation'], + ['text-to-image', 'Image generation'], ['vision', 'Image input'], ['function-calling', 'Function calling'], ['structured-output', 'Structured output'], @@ -91,6 +93,8 @@ const iconPaths = new Map([ ['structured-output', 'M8 3H5v6l-2 3 2 3v6h3 M16 3h3v6l2 3-2 3v6h-3 M10 9h4 M10 15h4'], ['text-generation', 'M4 4h16v12H8l-4 4V4Z M8 8h8 M8 12h5'], ['automatic-speech-recognition', 'M9 5a3 3 0 0 1 6 0v7a3 3 0 0 1-6 0V5Z M5 10v2a7 7 0 0 0 14 0v-2 M12 19v3 M8 22h8'], + ['text-to-speech', 'M11 5 6 9H2v6h4l5 4V5Z M15.5 8.5a5 5 0 0 1 0 7 M19 5a10 10 0 0 1 0 14'], + ['text-to-image', 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z M11 9a2 2 0 1 1-4 0 2 2 0 0 1 4 0 M21 15l-5-5L5 21'], ['long-context', 'M6 3h9l4 4v14H6V3Z M14 3v5h5 M9 12h7 M9 16h7'], ['open-weights', 'M7 11V7a5 5 0 0 1 9-3 M5 11h14v10H5V11Z M12 15v3'], ]); @@ -206,9 +210,16 @@ function apiIdentity(offering: Offering): HTMLElement { return api; } +// Speech and image models are priced per audio minute or image tile, not per +// token, and have no text context window to report. +const nonTokenCapabilities = new Set(['automatic-speech-recognition', 'text-to-speech', 'text-to-image']); + function offeringRow(offering: Offering): HTMLElement { const row = element('div', '', 'model-facts'); - const specs = figure('Context limit', offering.context_length === null ? 'Not published' : `${integers.format(offering.context_length)} tokens`); + const nonToken = offering.capabilities.some(capability => nonTokenCapabilities.has(capability)); + const specs = offering.context_length === null && nonToken + ? element('div', '', 'spec') + : figure('Context limit', offering.context_length === null ? 'Not published' : `${integers.format(offering.context_length)} tokens`); const capabilities = element('div', '', 'capabilities'); for (const capability of offering.capabilities) capabilities.append(badge(capabilityNames.get(capability) ?? capability, capability)); if (offering.capabilities.length === 0) capabilities.append(element('span', 'Capabilities not published', 'spec-value')); @@ -216,7 +227,7 @@ function offeringRow(offering: Offering): HTMLElement { const prices = element('div', '', 'offering-prices'); if (offering.pricing === undefined) { - prices.append(figure('USD / million tokens', 'Token prices not published')); + prices.append(nonToken ? figure('Pricing', 'Not priced per token') : figure('USD / million tokens', 'Token prices not published')); } else { const { input, output, basis } = offering.pricing; const prefix = basis === 'starting_at' ? 'from ' : ''; diff --git a/tests/models_page_browser.py b/tests/models_page_browser.py index cce3c22..7f61e52 100644 --- a/tests/models_page_browser.py +++ b/tests/models_page_browser.py @@ -47,6 +47,7 @@ def pricing(input_price: float, output_price: float, basis: str = "standard") -> offering("same-name-a", name="Identical name"), offering("same-name-b", name="Identical name"), offering("image-route", name="Image generator", capabilities=["text-to-image"], pricing=pricing(0.00000001, 0.1)), + offering("flux-1-schnell", "workers-ai", name="FLUX.1 schnell", capabilities=["text-to-image"], context_length=None), ]} MODEL_COUNT = len(CATALOG["data"]) @@ -89,6 +90,10 @@ def test_canonical_models_and_copy(page: Page) -> None: expect(model_card(page, "deepseek-v3.2-speciale")).to_contain_text("Input from $0.04") expect(model_card(page, "image-route")).to_contain_text("$0.00000001") expect(model_card(page, "new-release-2099")).to_contain_text("Token prices not published") + flux = model_card(page, "flux-1-schnell") + expect(flux).to_contain_text("Image generation") + expect(flux).to_contain_text("Not priced per token") + expect(flux).not_to_contain_text("Context limit") summary = model_card(page, "gpt-oss-120b").locator(".model-summary") expect(summary).to_contain_text("116.8B") summary.get_by_text("Details and sources", exact=True).press("Enter")