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")