From c5a93c69c17ee5a7e128b2492e551bbfff9c525f Mon Sep 17 00:00:00 2001
From: Bruno Perez
Date: Tue, 26 May 2026 08:19:43 -0700
Subject: [PATCH 1/2] feat: improve catalog browsing on mobile and at scale
Mobile:
- Collapse header nav into a menu so it no longer overflows at 375px
- Let the parameter table scroll horizontally instead of crushing columns
Browsing:
- Pin the search/filter bar while scrolling the full model list
- Group models by provider with a sort control (provider / name / params)
- Collapse the 36 parameter chips behind a "show all" expander
- Add a clear-all-filters control, a search clear button, and "/" to focus search
Fixes:
- Stop date stamps fusing onto versions (e.g. "Claude Opus 4 20250514")
- Render hyphenated dates cleanly ("Gpt 4 Turbo 2024-04-09")
- Distinguish "Max completion tokens" from "Max tokens" in the glossary
Also: aria-pressed on filter toggles, provider-card chevrons, direct links on
model rows, and a dist/ mkdir so a clean npm run dev works.
---
models/moonshot/kimi-k2.5.yaml | 2 +-
models/moonshot/kimi-k2.6.yaml | 2 +-
models/moonshot/moonshot-v1-128k.yaml | 2 +-
models/moonshot/moonshot-v1-32k.yaml | 2 +-
models/moonshot/moonshot-v1-8k.yaml | 2 +-
models/openai/gpt-5-chat-latest.yaml | 2 +-
models/openai/gpt-5-mini.yaml | 2 +-
models/openai/gpt-5-nano.yaml | 2 +-
models/openai/gpt-5.1.yaml | 2 +-
models/openai/gpt-5.2.yaml | 2 +-
models/openai/gpt-5.4-mini.yaml | 2 +-
models/openai/gpt-5.4.yaml | 2 +-
models/openai/gpt-5.5.yaml | 2 +-
models/openai/gpt-5.yaml | 2 +-
models/openai/o1.yaml | 2 +-
models/openai/o3-mini.yaml | 2 +-
models/openai/o3.yaml | 2 +-
models/openai/o4-mini.yaml | 2 +-
src/build/assets.ts | 1 +
src/client/main.ts | 207 +++++++++++++++++++++++++-
src/client/styles.css | 5 +
src/data/display.ts | 27 +++-
src/views/index.ejs | 183 ++++++++++++++++-------
src/views/partials/header.ejs | 83 ++++++++---
src/views/partials/model_row.ejs | 7 +-
src/views/partials/param_table.ejs | 4 +-
src/views/provider.ejs | 3 +
tests/catalog.test.ts | 14 ++
28 files changed, 466 insertions(+), 104 deletions(-)
diff --git a/models/moonshot/kimi-k2.5.yaml b/models/moonshot/kimi-k2.5.yaml
index 9bb3ada..fcd15c8 100644
--- a/models/moonshot/kimi-k2.5.yaml
+++ b/models/moonshot/kimi-k2.5.yaml
@@ -5,7 +5,7 @@ model: kimi-k2.5
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/kimi-k2.6.yaml b/models/moonshot/kimi-k2.6.yaml
index 9bb30a1..04cca02 100644
--- a/models/moonshot/kimi-k2.6.yaml
+++ b/models/moonshot/kimi-k2.6.yaml
@@ -5,7 +5,7 @@ model: kimi-k2.6
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/moonshot-v1-128k.yaml b/models/moonshot/moonshot-v1-128k.yaml
index f9290cf..9d1fea1 100644
--- a/models/moonshot/moonshot-v1-128k.yaml
+++ b/models/moonshot/moonshot-v1-128k.yaml
@@ -5,7 +5,7 @@ model: moonshot-v1-128k
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/moonshot-v1-32k.yaml b/models/moonshot/moonshot-v1-32k.yaml
index a0c3df4..cec1226 100644
--- a/models/moonshot/moonshot-v1-32k.yaml
+++ b/models/moonshot/moonshot-v1-32k.yaml
@@ -5,7 +5,7 @@ model: moonshot-v1-32k
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/moonshot-v1-8k.yaml b/models/moonshot/moonshot-v1-8k.yaml
index 3d09595..5f3ef9b 100644
--- a/models/moonshot/moonshot-v1-8k.yaml
+++ b/models/moonshot/moonshot-v1-8k.yaml
@@ -5,7 +5,7 @@ model: moonshot-v1-8k
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/openai/gpt-5-chat-latest.yaml b/models/openai/gpt-5-chat-latest.yaml
index f26cce5..0a734af 100644
--- a/models/openai/gpt-5-chat-latest.yaml
+++ b/models/openai/gpt-5-chat-latest.yaml
@@ -5,7 +5,7 @@ model: gpt-5-chat-latest
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5-mini.yaml b/models/openai/gpt-5-mini.yaml
index 8e9dc1c..1eb5c7f 100644
--- a/models/openai/gpt-5-mini.yaml
+++ b/models/openai/gpt-5-mini.yaml
@@ -5,7 +5,7 @@ model: gpt-5-mini
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5-nano.yaml b/models/openai/gpt-5-nano.yaml
index 2ee2928..e5f39bf 100644
--- a/models/openai/gpt-5-nano.yaml
+++ b/models/openai/gpt-5-nano.yaml
@@ -5,7 +5,7 @@ model: gpt-5-nano
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.1.yaml b/models/openai/gpt-5.1.yaml
index 97b0626..7e6e08d 100644
--- a/models/openai/gpt-5.1.yaml
+++ b/models/openai/gpt-5.1.yaml
@@ -5,7 +5,7 @@ model: gpt-5.1
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.2.yaml b/models/openai/gpt-5.2.yaml
index fac9a8e..c5210cd 100644
--- a/models/openai/gpt-5.2.yaml
+++ b/models/openai/gpt-5.2.yaml
@@ -5,7 +5,7 @@ model: gpt-5.2
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.4-mini.yaml b/models/openai/gpt-5.4-mini.yaml
index bc1feaf..52ac17c 100644
--- a/models/openai/gpt-5.4-mini.yaml
+++ b/models/openai/gpt-5.4-mini.yaml
@@ -5,7 +5,7 @@ model: gpt-5.4-mini
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.4.yaml b/models/openai/gpt-5.4.yaml
index faddab5..014b531 100644
--- a/models/openai/gpt-5.4.yaml
+++ b/models/openai/gpt-5.4.yaml
@@ -5,7 +5,7 @@ model: gpt-5.4
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.5.yaml b/models/openai/gpt-5.5.yaml
index 14f123d..d1194e4 100644
--- a/models/openai/gpt-5.5.yaml
+++ b/models/openai/gpt-5.5.yaml
@@ -5,7 +5,7 @@ model: gpt-5.5
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.yaml b/models/openai/gpt-5.yaml
index 5bb0bd4..fd4cad0 100644
--- a/models/openai/gpt-5.yaml
+++ b/models/openai/gpt-5.yaml
@@ -5,7 +5,7 @@ model: gpt-5
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o1.yaml b/models/openai/o1.yaml
index e6d4c04..ee73459 100644
--- a/models/openai/o1.yaml
+++ b/models/openai/o1.yaml
@@ -5,7 +5,7 @@ model: o1
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o3-mini.yaml b/models/openai/o3-mini.yaml
index 33051a1..66ed740 100644
--- a/models/openai/o3-mini.yaml
+++ b/models/openai/o3-mini.yaml
@@ -5,7 +5,7 @@ model: o3-mini
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o3.yaml b/models/openai/o3.yaml
index f13640d..c7614ed 100644
--- a/models/openai/o3.yaml
+++ b/models/openai/o3.yaml
@@ -5,7 +5,7 @@ model: o3
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o4-mini.yaml b/models/openai/o4-mini.yaml
index 069916d..2278fd4 100644
--- a/models/openai/o4-mini.yaml
+++ b/models/openai/o4-mini.yaml
@@ -5,7 +5,7 @@ model: o4-mini
params:
- path: max_completion_tokens
type: integer
- label: Max tokens
+ label: Max completion tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/src/build/assets.ts b/src/build/assets.ts
index 6be9664..d8cf2bf 100644
--- a/src/build/assets.ts
+++ b/src/build/assets.ts
@@ -29,6 +29,7 @@ export async function compileStyles(): Promise {
}
export async function copyStaticAssets(): Promise {
+ await fs.mkdir(DIST_ASSETS_DIR, { recursive: true });
await Promise.all(
["favicon.svg", "og.png", "apple-touch-icon.png"].map((name) =>
fs.copyFile(path.join(CLIENT_DIR, name), path.join(DIST_ASSETS_DIR, name)),
diff --git a/src/client/main.ts b/src/client/main.ts
index a53994f..2c9b1ae 100644
--- a/src/client/main.ts
+++ b/src/client/main.ts
@@ -1,12 +1,14 @@
import { setupWebMCP } from "./webmcp.js";
type AuthFilter = "all" | "api_key" | "subscription";
+type SortMode = "provider" | "name" | "params";
interface FilterState {
query: string;
auth: AuthFilter;
providers: Set;
capabilities: Set;
+ sort: SortMode;
}
const state: FilterState = {
@@ -14,6 +16,7 @@ const state: FilterState = {
auth: "all",
providers: new Set(),
capabilities: new Set(),
+ sort: "provider",
};
function setupHowToUseModal(): void {
@@ -144,6 +147,9 @@ function applyFilters(): void {
const empty = document.querySelector("[data-empty-state]");
if (empty) empty.classList.toggle("hidden", visible !== 0);
+
+ updateGroupHeaders();
+ syncFilterChrome();
}
function setupSearch(): void {
@@ -154,6 +160,14 @@ function setupSearch(): void {
applyFilters();
});
+ const clear = document.querySelector("[data-search-clear]");
+ clear?.addEventListener("click", () => {
+ input.value = "";
+ state.query = "";
+ applyFilters();
+ input.focus();
+ });
+
// Deep link: /?q=opus pre-fills the search (backs the schema.org SearchAction).
const initial = new URLSearchParams(window.location.search).get("q");
if (initial) {
@@ -163,6 +177,11 @@ function setupSearch(): void {
}
}
+function setActive(el: Element, active: boolean): void {
+ el.setAttribute("data-active", String(active));
+ el.setAttribute("aria-pressed", String(active));
+}
+
function setupAuthFilters(): void {
const buttons = document.querySelectorAll("[data-auth-filter]");
buttons.forEach((btn) => {
@@ -170,7 +189,7 @@ function setupAuthFilters(): void {
const auth = btn.dataset.authFilter as AuthFilter | undefined;
if (!auth) return;
state.auth = auth;
- buttons.forEach((b) => b.setAttribute("data-active", String(b === btn)));
+ buttons.forEach((b) => setActive(b, b === btn));
applyFilters();
});
});
@@ -184,23 +203,205 @@ function setupToggleChips(selector: string, datasetKey: string, bucket: Set 0 ||
+ state.capabilities.size > 0
+ );
+}
+
+function syncFilterChrome(): void {
+ const clear = document.querySelector("[data-clear-filters]");
+ if (clear) clear.classList.toggle("hidden", !hasActiveFilters());
+
+ const searchClear = document.querySelector("[data-search-clear]");
+ if (searchClear) searchClear.classList.toggle("hidden", state.query === "");
+
+ // The "/" hint and the clear button share the same slot; show one at a time.
+ const hint = document.querySelector("[data-search-hint]");
+ if (hint) hint.style.display = state.query === "" ? "" : "none";
+}
+
+function setupClearFilters(): void {
+ const button = document.querySelector("[data-clear-filters]");
+ if (!button) return;
+ button.addEventListener("click", () => {
+ state.query = "";
+ state.auth = "all";
+ state.providers.clear();
+ state.capabilities.clear();
+
+ const input = document.querySelector("[data-search]");
+ if (input) input.value = "";
+
+ document
+ .querySelectorAll("[data-auth-filter]")
+ .forEach((b) => setActive(b, b.dataset.authFilter === "all"));
+ document
+ .querySelectorAll("[data-provider], [data-capability]")
+ .forEach((c) => setActive(c, false));
+
+ applyFilters();
+ });
+}
+
+const modelList = (): HTMLElement | null => document.querySelector("[data-model-list]");
+
+// Snapshot of the server-rendered order (headers + rows), used to restore grouping.
+let originalOrder: Element[] = [];
+
+function updateGroupHeaders(): void {
+ const list = modelList();
+ if (!list) return;
+ const headers = list.querySelectorAll("[data-group-header]");
+
+ // Provider headers only make sense in the grouped (provider) ordering.
+ if (state.sort !== "provider") {
+ headers.forEach((h) => h.classList.add("hidden"));
+ return;
+ }
+
+ // Show a header only when its group has at least one visible row.
+ let current: HTMLElement | null = null;
+ let hasVisible = false;
+ const finalize = (): void => {
+ if (current) current.classList.toggle("hidden", !hasVisible);
+ };
+ for (const child of Array.from(list.children) as HTMLElement[]) {
+ if (child.matches("[data-group-header]")) {
+ finalize();
+ current = child;
+ hasVisible = false;
+ } else if (child.classList.contains("model") && !child.classList.contains("hidden")) {
+ hasVisible = true;
+ }
+ }
+ finalize();
+}
+
+function reorderList(): void {
+ const list = modelList();
+ if (!list) return;
+ if (originalOrder.length === 0) originalOrder = Array.from(list.children);
+
+ if (state.sort === "provider") {
+ originalOrder.forEach((el) => list.appendChild(el));
+ return;
+ }
+
+ const rows = Array.from(list.querySelectorAll(".model"));
+ rows.sort((a, b) => {
+ const nameA = a.dataset.modelName ?? "";
+ const nameB = b.dataset.modelName ?? "";
+ if (state.sort === "name") return nameA.localeCompare(nameB);
+ const byParams = Number(b.dataset.modelParams ?? 0) - Number(a.dataset.modelParams ?? 0);
+ return byParams !== 0 ? byParams : nameA.localeCompare(nameB);
+ });
+ rows.forEach((row) => list.appendChild(row));
+}
+
+function setupSort(): void {
+ const select = document.querySelector("[data-sort]");
+ if (!select) return;
+ originalOrder = Array.from(modelList()?.children ?? []);
+ select.addEventListener("change", () => {
+ state.sort = (select.value as SortMode) || "provider";
+ reorderList();
+ applyFilters();
+ });
+}
+
+function setupModelLinks(): void {
+ document.querySelectorAll("[data-model-link]").forEach((link) => {
+ link.addEventListener("click", (event) => {
+ // Leave modified clicks (open in new tab, etc.) to the browser.
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
+ return;
+ }
+ // A plain click navigates to the model page rather than toggling the row.
+ event.preventDefault();
+ const href = link.getAttribute("href");
+ if (href) window.location.href = href;
+ });
+ });
+}
+
+function setupSearchShortcut(): void {
+ const input = document.querySelector("[data-search]");
+ if (!input) return;
+ document.addEventListener("keydown", (event) => {
+ if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return;
+ const target = event.target as HTMLElement | null;
+ const tag = target?.tagName.toLowerCase();
+ if (tag === "input" || tag === "textarea" || target?.isContentEditable) return;
+ event.preventDefault();
+ input.focus();
+ });
+}
+
+function setupCapabilityCollapse(): void {
+ const bar = document.querySelector("[data-capability-bar]");
+ const button = document.querySelector("[data-capability-expand]");
+ if (!bar || !button) return;
+
+ const LIMIT = 12;
+ const chips = Array.from(bar.querySelectorAll("[data-capability]"));
+ if (chips.length <= LIMIT) return;
+
+ let expanded = false;
+ const render = (): void => {
+ chips.forEach((chip, i) => chip.classList.toggle("hidden", !expanded && i >= LIMIT));
+ button.textContent = expanded ? "Show fewer" : `Show all ${chips.length} parameters`;
+ button.setAttribute("aria-expanded", String(expanded));
+ };
+
+ button.classList.remove("hidden");
+ render();
+ button.addEventListener("click", () => {
+ expanded = !expanded;
+ render();
+ });
+}
+
+function setupMobileMenu(): void {
+ const menu = document.querySelector("[data-mobile-menu]");
+ if (!menu) return;
+ // Close after a selection or when clicking outside.
+ menu
+ .querySelectorAll("a, [data-open-how-to-use]")
+ .forEach((el) => el.addEventListener("click", () => menu.removeAttribute("open")));
+ document.addEventListener("click", (event) => {
+ if (menu.open && !menu.contains(event.target as Node)) menu.removeAttribute("open");
+ });
+}
+
document.addEventListener("DOMContentLoaded", () => {
setupThemeToggle();
setupHowToUseModal();
setupCopyHowToUse();
+ setupMobileMenu();
setupSearch();
setupAuthFilters();
setupToggleChips("[data-provider]", "provider", state.providers);
setupToggleChips("[data-capability]", "capability", state.capabilities);
+ setupCapabilityCollapse();
+ setupClearFilters();
+ setupSort();
+ setupModelLinks();
+ setupSearchShortcut();
+ updateGroupHeaders();
+ syncFilterChrome();
setupWebMCP();
});
diff --git a/src/client/styles.css b/src/client/styles.css
index bfa8ef5..ecf6cd6 100644
--- a/src/client/styles.css
+++ b/src/client/styles.css
@@ -9,6 +9,11 @@
details > summary::-webkit-details-marker {
display: none;
}
+ /* Hide the native clear control on type=search; we render our own button. */
+ input[type="search"]::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+ appearance: none;
+ }
}
@layer components {
diff --git a/src/data/display.ts b/src/data/display.ts
index 01c58a9..62795d8 100644
--- a/src/data/display.ts
+++ b/src/data/display.ts
@@ -92,12 +92,27 @@ function titleCase(slug: string): string {
.join(" ");
}
-function mergeAdjacentNumbers(parts: string[]): string[] {
+// Collapses numeric slug segments into readable versions and dates:
+// "4" "1" → "4.1" (short version segments join with dots)
+// "2024" "11" "20" → "2024-11-20" (a 4-digit year + month/day reads as a date)
+// "20250514" → "20250514" (8-digit date stamp is left intact)
+// This keeps a trailing date from fusing onto a version, e.g.
+// "claude-opus-4-20250514" → "Claude Opus 4 20250514", not "4.20250514".
+function formatNumericRuns(parts: string[]): string[] {
const out: string[] = [];
- for (const part of parts) {
- const prev = out[out.length - 1];
- if (prev !== undefined && /^\d+$/.test(prev) && /^\d+$/.test(part)) {
- out[out.length - 1] = `${prev}.${part}`;
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i]!;
+ if (/^\d{4}$/.test(part)) {
+ const date = [part];
+ while (date.length < 3 && /^\d{2}$/.test(parts[i + 1] ?? "")) date.push(parts[++i]!);
+ out.push(date.join("-"));
+ } else if (/^\d{1,2}$/.test(part)) {
+ const prev = out[out.length - 1];
+ if (prev !== undefined && /^\d{1,2}(\.\d{1,2})*$/.test(prev)) {
+ out[out.length - 1] = `${prev}.${part}`;
+ } else {
+ out.push(part);
+ }
} else {
out.push(part);
}
@@ -112,7 +127,7 @@ export function providerLabel(provider: string): string {
export function modelLabel(model: Pick): string {
const key = `${model.provider}/${model.model}`;
if (MODEL_LABEL_OVERRIDES[key]) return MODEL_LABEL_OVERRIDES[key];
- const parts = mergeAdjacentNumbers(model.model.split("-"));
+ const parts = formatNumericRuns(model.model.split("-"));
return parts
.map((part) => (/^\d+(\.\d+)?$/.test(part) ? part : part[0]!.toUpperCase() + part.slice(1)))
.join(" ");
diff --git a/src/views/index.ejs b/src/views/index.ejs
index 4638373..df633a0 100644
--- a/src/views/index.ejs
+++ b/src/views/index.ejs
@@ -14,59 +14,84 @@
-
+
-
-
-
-
-
+
+
/
+
+
+
+
+
+
-
+
+
<% if (providers.length > 0) { %>
-
+
Filter by provider
@@ -76,6 +101,7 @@
+
+ Show all <%= capabilities.length %> parameters
+
<% } %>
-
+ <%
+ const byProvider = new Map();
+ for (const m of models) {
+ const list = byProvider.get(m.provider) || [];
+ list.push(m);
+ byProvider.set(m.provider, list);
+ }
+ %>
+
<%= models.length %> of
<%= models.length %> models
-
No models match your filters.
+
+
+
+
+
+
+ Clear all filters
+
+
- <% for (const model of models) { %> <%- include("partials/model_row", { model: model, helpers: helpers }) %> <% } %>
+ <% for (const pf of providers) { %>
+ <% const group = byProvider.get(pf.provider) || []; %>
+ <% const groupLogo = helpers.logoFor(pf.provider); %>
+
+ <% if (groupLogo) { %><%- groupLogo %><% } %>
+ <%= helpers.providerLabel(pf.provider) %>
+ <%= group.length %>
+
+ <% for (const model of group) { %><%- include("partials/model_row", { model: model, helpers: helpers }) %><% } %>
+ <% } %>
+
+ No models match your filters.
+
diff --git a/src/views/partials/header.ejs b/src/views/partials/header.ejs
index 43c0232..19e62b5 100644
--- a/src/views/partials/header.ejs
+++ b/src/views/partials/header.ejs
@@ -13,32 +13,34 @@
modelparams.dev
-
diff --git a/src/views/partials/model_row.ejs b/src/views/partials/model_row.ejs
index dc189bc..55ac1ff 100644
--- a/src/views/partials/model_row.ejs
+++ b/src/views/partials/model_row.ejs
@@ -17,6 +17,7 @@
data-model-provider="<%= model.provider %>"
data-model-auth="<%= model.authType %>"
data-model-capabilities="<%= capabilities %>"
+ data-model-params="<%= model.params.length %>"
id="<%= id.replace('/', '--') %>"
>
<%= providerName %>
- <%= modelName %>
+ <%= modelName %>
<%= authName %>
diff --git a/src/views/partials/param_table.ejs b/src/views/partials/param_table.ejs
index 05b1002..b581f94 100644
--- a/src/views/partials/param_table.ejs
+++ b/src/views/partials/param_table.ejs
@@ -1,8 +1,8 @@
<%
const groups = helpers.groupParams(params);
%>
-
-
+
+
| Parameter |
diff --git a/src/views/provider.ejs b/src/views/provider.ejs
index cbadd92..f255e9d 100644
--- a/src/views/provider.ejs
+++ b/src/views/provider.ejs
@@ -38,6 +38,9 @@
<%= helpers.modelLabel(model) %>
<%= authName %>
<%= model.params.length %> param<%= model.params.length === 1 ? "" : "s" %>
+
+
+
<% } %>
diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts
index fcdfae3..b885168 100644
--- a/tests/catalog.test.ts
+++ b/tests/catalog.test.ts
@@ -88,6 +88,20 @@ describe("display helpers", () => {
expect(modelLabel({ provider: "anthropic", model: "claude-sonnet-4-6" })).toBe(
"Claude Sonnet 4.6",
);
+ expect(modelLabel({ provider: "anthropic", model: "claude-opus-4-1-20250805" })).toBe(
+ "Claude Opus 4.1 20250805",
+ );
+ });
+
+ it("keeps date stamps separate from version numbers", () => {
+ // An 8-digit date stamp must not fuse onto the version (the "4.20250514" bug).
+ expect(modelLabel({ provider: "anthropic", model: "claude-opus-4-20250514" })).toBe(
+ "Claude Opus 4 20250514",
+ );
+ // A hyphenated YYYY-MM-DD date reads as a date, not a dotted version.
+ expect(modelLabel({ provider: "openai", model: "gpt-4-turbo-2024-04-09" })).toBe(
+ "Gpt 4 Turbo 2024-04-09",
+ );
});
});
From c61d6ef9f668528ba024524cdc954eb84c773dff Mon Sep 17 00:00:00 2001
From: Bruno Perez
Date: Tue, 26 May 2026 08:37:20 -0700
Subject: [PATCH 2/2] refactor: dedupe parameter labels in the app, not the
data
Revert the per-model YAML label edits and instead normalize
max_completion_tokens to "Max completion tokens" at render time
(glossary, model pages, JSON-LD). The catalog data and JSON API
stay faithful to source, mirroring how model slugs are prettified
for display only.
---
models/moonshot/kimi-k2.5.yaml | 2 +-
models/moonshot/kimi-k2.6.yaml | 2 +-
models/moonshot/moonshot-v1-128k.yaml | 2 +-
models/moonshot/moonshot-v1-32k.yaml | 2 +-
models/moonshot/moonshot-v1-8k.yaml | 2 +-
models/openai/gpt-5-chat-latest.yaml | 2 +-
models/openai/gpt-5-mini.yaml | 2 +-
models/openai/gpt-5-nano.yaml | 2 +-
models/openai/gpt-5.1.yaml | 2 +-
models/openai/gpt-5.2.yaml | 2 +-
models/openai/gpt-5.4-mini.yaml | 2 +-
models/openai/gpt-5.4.yaml | 2 +-
models/openai/gpt-5.5.yaml | 2 +-
models/openai/gpt-5.yaml | 2 +-
models/openai/o1.yaml | 2 +-
models/openai/o3-mini.yaml | 2 +-
models/openai/o3.yaml | 2 +-
models/openai/o4-mini.yaml | 2 +-
src/build/render.ts | 2 ++
src/build/structured-data.ts | 4 ++--
src/data/display.ts | 11 +++++++++++
src/data/glossary.ts | 4 ++--
src/views/partials/param_table.ejs | 2 +-
tests/catalog.test.ts | 7 ++++++-
tests/glossary.test.ts | 16 ++++++++++++++++
25 files changed, 58 insertions(+), 24 deletions(-)
diff --git a/models/moonshot/kimi-k2.5.yaml b/models/moonshot/kimi-k2.5.yaml
index fcd15c8..9bb3ada 100644
--- a/models/moonshot/kimi-k2.5.yaml
+++ b/models/moonshot/kimi-k2.5.yaml
@@ -5,7 +5,7 @@ model: kimi-k2.5
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/kimi-k2.6.yaml b/models/moonshot/kimi-k2.6.yaml
index 04cca02..9bb30a1 100644
--- a/models/moonshot/kimi-k2.6.yaml
+++ b/models/moonshot/kimi-k2.6.yaml
@@ -5,7 +5,7 @@ model: kimi-k2.6
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/moonshot-v1-128k.yaml b/models/moonshot/moonshot-v1-128k.yaml
index 9d1fea1..f9290cf 100644
--- a/models/moonshot/moonshot-v1-128k.yaml
+++ b/models/moonshot/moonshot-v1-128k.yaml
@@ -5,7 +5,7 @@ model: moonshot-v1-128k
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/moonshot-v1-32k.yaml b/models/moonshot/moonshot-v1-32k.yaml
index cec1226..a0c3df4 100644
--- a/models/moonshot/moonshot-v1-32k.yaml
+++ b/models/moonshot/moonshot-v1-32k.yaml
@@ -5,7 +5,7 @@ model: moonshot-v1-32k
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/moonshot/moonshot-v1-8k.yaml b/models/moonshot/moonshot-v1-8k.yaml
index 5f3ef9b..3d09595 100644
--- a/models/moonshot/moonshot-v1-8k.yaml
+++ b/models/moonshot/moonshot-v1-8k.yaml
@@ -5,7 +5,7 @@ model: moonshot-v1-8k
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of tokens to generate in the chat completion.
range:
min: 1
diff --git a/models/openai/gpt-5-chat-latest.yaml b/models/openai/gpt-5-chat-latest.yaml
index 0a734af..f26cce5 100644
--- a/models/openai/gpt-5-chat-latest.yaml
+++ b/models/openai/gpt-5-chat-latest.yaml
@@ -5,7 +5,7 @@ model: gpt-5-chat-latest
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5-mini.yaml b/models/openai/gpt-5-mini.yaml
index 1eb5c7f..8e9dc1c 100644
--- a/models/openai/gpt-5-mini.yaml
+++ b/models/openai/gpt-5-mini.yaml
@@ -5,7 +5,7 @@ model: gpt-5-mini
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5-nano.yaml b/models/openai/gpt-5-nano.yaml
index e5f39bf..2ee2928 100644
--- a/models/openai/gpt-5-nano.yaml
+++ b/models/openai/gpt-5-nano.yaml
@@ -5,7 +5,7 @@ model: gpt-5-nano
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.1.yaml b/models/openai/gpt-5.1.yaml
index 7e6e08d..97b0626 100644
--- a/models/openai/gpt-5.1.yaml
+++ b/models/openai/gpt-5.1.yaml
@@ -5,7 +5,7 @@ model: gpt-5.1
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.2.yaml b/models/openai/gpt-5.2.yaml
index c5210cd..fac9a8e 100644
--- a/models/openai/gpt-5.2.yaml
+++ b/models/openai/gpt-5.2.yaml
@@ -5,7 +5,7 @@ model: gpt-5.2
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.4-mini.yaml b/models/openai/gpt-5.4-mini.yaml
index 52ac17c..bc1feaf 100644
--- a/models/openai/gpt-5.4-mini.yaml
+++ b/models/openai/gpt-5.4-mini.yaml
@@ -5,7 +5,7 @@ model: gpt-5.4-mini
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.4.yaml b/models/openai/gpt-5.4.yaml
index 014b531..faddab5 100644
--- a/models/openai/gpt-5.4.yaml
+++ b/models/openai/gpt-5.4.yaml
@@ -5,7 +5,7 @@ model: gpt-5.4
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.5.yaml b/models/openai/gpt-5.5.yaml
index d1194e4..14f123d 100644
--- a/models/openai/gpt-5.5.yaml
+++ b/models/openai/gpt-5.5.yaml
@@ -5,7 +5,7 @@ model: gpt-5.5
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/gpt-5.yaml b/models/openai/gpt-5.yaml
index fd4cad0..5bb0bd4 100644
--- a/models/openai/gpt-5.yaml
+++ b/models/openai/gpt-5.yaml
@@ -5,7 +5,7 @@ model: gpt-5
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o1.yaml b/models/openai/o1.yaml
index ee73459..e6d4c04 100644
--- a/models/openai/o1.yaml
+++ b/models/openai/o1.yaml
@@ -5,7 +5,7 @@ model: o1
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o3-mini.yaml b/models/openai/o3-mini.yaml
index 66ed740..33051a1 100644
--- a/models/openai/o3-mini.yaml
+++ b/models/openai/o3-mini.yaml
@@ -5,7 +5,7 @@ model: o3-mini
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o3.yaml b/models/openai/o3.yaml
index c7614ed..f13640d 100644
--- a/models/openai/o3.yaml
+++ b/models/openai/o3.yaml
@@ -5,7 +5,7 @@ model: o3
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/models/openai/o4-mini.yaml b/models/openai/o4-mini.yaml
index 2278fd4..069916d 100644
--- a/models/openai/o4-mini.yaml
+++ b/models/openai/o4-mini.yaml
@@ -5,7 +5,7 @@ model: o4-mini
params:
- path: max_completion_tokens
type: integer
- label: Max completion tokens
+ label: Max tokens
description: Maximum number of output tokens the model may generate.
default: 4096
range:
diff --git a/src/build/render.ts b/src/build/render.ts
index 64fc300..1a8d6ee 100644
--- a/src/build/render.ts
+++ b/src/build/render.ts
@@ -8,6 +8,7 @@ import {
modelLabel,
paramGroupIcon,
paramGroupLabel,
+ paramLabel,
providerLabel,
} from "../data/display.js";
import { groupParams } from "../data/group.js";
@@ -29,6 +30,7 @@ export const viewHelpers = {
authLabel,
paramGroupLabel,
paramGroupIcon,
+ paramLabel,
conditionIcon,
describeApplicability,
groupParams,
diff --git a/src/build/structured-data.ts b/src/build/structured-data.ts
index 9a3cfd9..c428566 100644
--- a/src/build/structured-data.ts
+++ b/src/build/structured-data.ts
@@ -1,7 +1,7 @@
// JSON-LD builders for every page type. Pure functions of (data, siteUrl) so
// they can be unit-tested without touching the filesystem or the renderer.
-import { modelLabel, providerLabel } from "../data/display.js";
+import { modelLabel, paramLabel, providerLabel } from "../data/display.js";
import type { GlossaryGroup } from "../data/glossary.js";
import { SITE_DESCRIPTION, SITE_NAME } from "../data/site.js";
import {
@@ -125,7 +125,7 @@ export function buildModelStructuredData(
variableMeasured: model.params.map((param) => ({
"@type": "PropertyValue",
name: param.path,
- alternateName: param.label,
+ alternateName: paramLabel(param.path, param.label),
description: param.description,
})),
};
diff --git a/src/data/display.ts b/src/data/display.ts
index 62795d8..c240940 100644
--- a/src/data/display.ts
+++ b/src/data/display.ts
@@ -57,6 +57,13 @@ const AUTH_LABELS: Record = {
subscription: "Subscription",
};
+// Canonical display labels for parameters whose per-model YAML labels drift.
+// The raw data (and JSON API) stay untouched; this only normalizes what the
+// site renders, the same way MODEL_LABEL_OVERRIDES prettifies model slugs.
+const PARAM_LABEL_OVERRIDES: Record = {
+ max_completion_tokens: "Max completion tokens",
+};
+
const PARAM_GROUP_LABELS: Record = {
generation_length: "Length",
sampling: "Sampling",
@@ -137,6 +144,10 @@ export function authLabel(authType: AuthType): string {
return AUTH_LABELS[authType];
}
+export function paramLabel(path: string, fallback: string): string {
+ return PARAM_LABEL_OVERRIDES[path] ?? fallback;
+}
+
export function paramGroupLabel(group: string): string {
return PARAM_GROUP_LABELS[group] ?? titleCase(group.replace(/_/g, "-"));
}
diff --git a/src/data/glossary.ts b/src/data/glossary.ts
index 40168e5..36abe34 100644
--- a/src/data/glossary.ts
+++ b/src/data/glossary.ts
@@ -2,7 +2,7 @@
// unique parameter path, grouped by parameter group. Powers the /glossary page
// and its DefinedTermSet structured data.
-import { paramGroupLabel } from "./display.js";
+import { paramGroupLabel, paramLabel } from "./display.js";
import { ParameterGroup, modelId, type Model } from "../schema/model.js";
export interface GlossaryEntry {
@@ -69,7 +69,7 @@ function aggregate(models: Model[]): Map {
function toEntry(path: string, bucket: Bucket): GlossaryEntry {
return {
path,
- label: mostCommon(bucket.labels),
+ label: paramLabel(path, mostCommon(bucket.labels)),
group: bucket.group,
types: [...bucket.types].sort(),
modelCount: bucket.models.size,
diff --git a/src/views/partials/param_table.ejs b/src/views/partials/param_table.ejs
index b581f94..1e33adb 100644
--- a/src/views/partials/param_table.ejs
+++ b/src/views/partials/param_table.ejs
@@ -42,7 +42,7 @@
%>
|
- <%= param.label %>
+ <%= helpers.paramLabel(param.path, param.label) %>
<%= param.path %>
|
diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts
index b885168..ad476b7 100644
--- a/tests/catalog.test.ts
+++ b/tests/catalog.test.ts
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { buildCapabilityFacets, buildCatalog, uniqueProviders } from "../src/data/catalog.js";
import { describeApplicability } from "../src/data/applicability.js";
-import { modelLabel, providerLabel } from "../src/data/display.js";
+import { modelLabel, paramLabel, providerLabel } from "../src/data/display.js";
import { loadAllModels } from "../src/data/load.js";
import { modelId } from "../src/schema/model.js";
import type { Model } from "../src/schema/model.js";
@@ -93,6 +93,11 @@ describe("display helpers", () => {
);
});
+ it("normalizes drifting parameter labels at display time", () => {
+ expect(paramLabel("max_completion_tokens", "Max tokens")).toBe("Max completion tokens");
+ expect(paramLabel("temperature", "Temperature")).toBe("Temperature");
+ });
+
it("keeps date stamps separate from version numbers", () => {
// An 8-digit date stamp must not fuse onto the version (the "4.20250514" bug).
expect(modelLabel({ provider: "anthropic", model: "claude-opus-4-20250514" })).toBe(
diff --git a/tests/glossary.test.ts b/tests/glossary.test.ts
index 9668e8b..49e84a5 100644
--- a/tests/glossary.test.ts
+++ b/tests/glossary.test.ts
@@ -44,4 +44,20 @@ describe("buildGlossary", () => {
expect(groups).toHaveLength(1);
expect(groups[0]?.group).toBe("generation_length");
});
+
+ it("normalizes max_completion_tokens so it is distinct from max_tokens", () => {
+ const maxCompletion = {
+ path: "max_completion_tokens",
+ type: "integer",
+ label: "Max tokens",
+ description: "Output cap.",
+ group: "generation_length",
+ } as Model["params"][number];
+
+ const groups = buildGlossary([model("openai", "gpt", [maxCompletion])]);
+ const entry = groups
+ .find((g) => g.group === "generation_length")
+ ?.entries.find((e) => e.path === "max_completion_tokens");
+ expect(entry?.label).toBe("Max completion tokens");
+ });
});