diff --git a/src/build/render.ts b/src/build/render.ts index 54c7175..2b51926 100644 --- a/src/build/render.ts +++ b/src/build/render.ts @@ -1,8 +1,16 @@ import path from "node:path"; import ejs from "ejs"; import { describeApplicability } from "../data/applicability.js"; -import { apiSurfaceLabel, apiSurfaceSdkMethod } from "../data/api-surfaces.js"; -import { buildProviderFacets, type CapabilityFacet } from "../data/catalog.js"; +import { + apiSurfaceLabel, + apiSurfaceSdkMethod, + buildApiSurfaceFacets, +} from "../data/api-surfaces.js"; +import { + buildProviderFacets, + type CapabilityFacet, + type ProviderFacet, +} from "../data/catalog.js"; import { authLabel, conditionIcon, @@ -14,11 +22,6 @@ import { providerLabel, } from "../data/display.js"; import { groupParams } from "../data/group.js"; -import { - buildApiSurfaceFacets, - buildModelGroupProviderFacets, - groupModels, -} from "../data/model-groups.js"; import { usageGuideMarkdown } from "../data/llms.js"; import { logoFor } from "../data/logos.js"; import { VIEWS_DIR } from "../data/paths.js"; @@ -161,11 +164,10 @@ export function homeDescription( } export async function renderIndex(opts: RenderOptions): Promise { - const modelGroups = groupModels(opts.catalog.models); - const providers = buildModelGroupProviderFacets(modelGroups); + const providers = buildProviderFacets(opts.catalog.models); const body = await ejs.renderFile(path.join(VIEWS_DIR, "index.ejs"), { - modelGroups, - apiSurfaces: buildApiSurfaceFacets(modelGroups), + models: opts.catalog.models, + apiSurfaces: buildApiSurfaceFacets(opts.catalog.models), capabilities: opts.capabilities, providers, helpers: viewHelpers, @@ -174,8 +176,12 @@ export async function renderIndex(opts: RenderOptions): Promise { const sampleParams = opts.capabilities.slice(0, 3).map((cap) => cap.path); return renderShell( { - title: homeTitle(modelGroups.length), - description: homeDescription(modelGroups.length, providers.length, sampleParams), + title: homeTitle(opts.catalog.models.length), + description: homeDescription( + opts.catalog.models.length, + providers.length, + sampleParams, + ), canonicalUrl: `${SITE_URL}/`, ogImage: ogImagePath("/"), structuredData: buildHomeStructuredData( diff --git a/src/client/main.ts b/src/client/main.ts index 40f5fbc..b8bc74e 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -1,5 +1,4 @@ import { setupWebMCP } from "./webmcp.js"; -import { setupModelVariantTabs, syncModelVariants } from "./model-variants.js"; type AuthFilter = "all" | "api_key" | "subscription"; type SortMode = "provider" | "name" | "params"; @@ -204,19 +203,26 @@ function setupThemeToggle(): void { } function modelMatches(el: HTMLElement): boolean { + if (state.auth !== "all" && el.dataset.modelAuth !== state.auth) return false; + + if (state.apiSurface !== "all" && el.dataset.modelApiSurface !== state.apiSurface) return false; + if (state.providers.size > 0 && !state.providers.has(el.dataset.modelProvider ?? "")) return false; + if (state.capabilities.size > 0) { + const have = new Set((el.dataset.modelCapabilities ?? "").split(/\s+/).filter(Boolean)); + for (const capability of state.capabilities) { + if (!have.has(capability)) return false; + } + } + if (state.query) { const haystack = `${el.dataset.modelName ?? ""} ${el.dataset.modelProvider ?? ""} ${el.dataset.modelId ?? ""}`; if (!haystack.includes(state.query)) return false; } - return syncModelVariants(el, { - auth: state.auth, - apiSurface: state.apiSurface, - capabilities: state.capabilities, - }); + return true; } function applyFilters(): void { @@ -641,7 +647,6 @@ document.addEventListener("DOMContentLoaded", () => { setupClearFilters(); setupSort(); reorderList(); - setupModelVariantTabs(); setupModelLinks(); setupSearchShortcut(); setupScrollTopButton(); diff --git a/src/client/model-variants.ts b/src/client/model-variants.ts deleted file mode 100644 index 43be49e..0000000 --- a/src/client/model-variants.ts +++ /dev/null @@ -1,51 +0,0 @@ -export interface VariantFilters { - auth: string; - apiSurface: string; - capabilities: Set; -} - -function variantMatches(panel: HTMLElement, filters: VariantFilters): boolean { - if (filters.auth !== "all" && panel.dataset.variantAuth !== filters.auth) return false; - if (filters.apiSurface !== "all" && panel.dataset.variantSurface !== filters.apiSurface) - return false; - const paths = new Set((panel.dataset.variantCapabilities ?? "").split(/\s+/).filter(Boolean)); - return [...filters.capabilities].every((capability) => paths.has(capability)); -} - -function selectVariant(row: HTMLElement, index: string): void { - row.querySelectorAll("[data-model-variant]").forEach((panel) => { - panel.classList.toggle("hidden", panel.dataset.modelVariant !== index); - }); - row.querySelectorAll("[data-model-variant-trigger]").forEach((trigger) => { - const active = trigger.dataset.modelVariantTrigger === index; - trigger.dataset.active = String(active); - trigger.setAttribute("aria-selected", String(active)); - }); -} - -export function syncModelVariants(row: HTMLElement, filters: VariantFilters): boolean { - const panels = Array.from(row.querySelectorAll("[data-model-variant]")); - const matching = panels.filter((panel) => variantMatches(panel, filters)); - const matchingIds = new Set(matching.map((panel) => panel.dataset.modelVariant)); - - row.querySelectorAll("[data-model-variant-trigger]").forEach((trigger) => { - trigger.classList.toggle("hidden", !matchingIds.has(trigger.dataset.modelVariantTrigger)); - }); - - const active = panels.find((panel) => !panel.classList.contains("hidden")); - const selected = active && matching.includes(active) ? active : matching[0]; - if (selected?.dataset.modelVariant !== undefined) { - selectVariant(row, selected.dataset.modelVariant); - } - return matching.length > 0; -} - -export function setupModelVariantTabs(): void { - document.querySelectorAll("[data-model-variant-trigger]").forEach((trigger) => { - trigger.addEventListener("click", () => { - const row = trigger.closest(".model"); - const index = trigger.dataset.modelVariantTrigger; - if (row && index !== undefined) selectVariant(row, index); - }); - }); -} diff --git a/src/data/api-surfaces.ts b/src/data/api-surfaces.ts index 13acb7d..732b04b 100644 --- a/src/data/api-surfaces.ts +++ b/src/data/api-surfaces.ts @@ -1,4 +1,4 @@ -import { ApiSurface, type ApiSurface as ApiSurfaceName } from "../schema/model.js"; +import { ApiSurface, type ApiSurface as ApiSurfaceName, type Model } from "../schema/model.js"; export interface ApiSurfaceMeta { value: ApiSurfaceName; @@ -11,15 +11,15 @@ const META: Record> = { label: "Chat Completions", sdkMethod: "chat.completions.create", }, - "openai-responses": { label: "Responses", sdkMethod: "responses.create" }, - "anthropic-messages": { label: "Messages", sdkMethod: "messages.create" }, + "openai-responses": { label: "Responses API", sdkMethod: "responses.create" }, + "anthropic-messages": { label: "Messages API", sdkMethod: "messages.create" }, "google-generate-content": { - label: "Gemini generateContent", + label: "Generate content", sdkMethod: "models.generateContent", }, - "amazon-bedrock-converse": { label: "Bedrock Converse", sdkMethod: "ConverseCommand" }, + "amazon-bedrock-converse": { label: "Converse API", sdkMethod: "ConverseCommand" }, "google-vertex-generate-content": { - label: "Vertex generateContent", + label: "Vertex generate content", sdkMethod: "generateContent", }, "cohere-chat": { label: "Cohere Chat", sdkMethod: "chat" }, @@ -37,3 +37,15 @@ export function apiSurfaceLabel(surface: ApiSurfaceName): string { export function apiSurfaceSdkMethod(surface: ApiSurfaceName): string { return META[surface].sdkMethod; } + +export interface ApiSurfaceFacet { + apiSurface: ApiSurfaceName; + count: number; +} + +export function buildApiSurfaceFacets(models: Model[]): ApiSurfaceFacet[] { + return API_SURFACES.map(({ value }) => ({ + apiSurface: value, + count: models.filter((model) => model.apiSurface === value).length, + })).filter((facet) => facet.count > 0); +} diff --git a/src/data/model-groups.ts b/src/data/model-groups.ts deleted file mode 100644 index 404cf12..0000000 --- a/src/data/model-groups.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { API_SURFACES } from "./api-surfaces.js"; -import type { ProviderFacet } from "./catalog.js"; -import type { ApiSurface, Model } from "../schema/model.js"; - -export interface ModelGroup { - key: string; - provider: string; - model: string; - variants: Model[]; - apiSurfaces: ApiSurface[]; - capabilities: string[]; - maxParams: number; -} - -const surfaceOrder = new Map(API_SURFACES.map((surface, index) => [surface.value, index])); - -function sortVariants(a: Model, b: Model): number { - if (a.authType !== b.authType) return a.authType === "api_key" ? -1 : 1; - return (surfaceOrder.get(a.apiSurface) ?? 0) - (surfaceOrder.get(b.apiSurface) ?? 0); -} - -function finishGroup(group: ModelGroup): ModelGroup { - group.variants.sort(sortVariants); - group.apiSurfaces = [...new Set(group.variants.map((variant) => variant.apiSurface))]; - group.capabilities = [ - ...new Set(group.variants.flatMap((variant) => variant.params.map((param) => param.path))), - ]; - group.maxParams = Math.max(...group.variants.map((variant) => variant.params.length)); - return group; -} - -export function groupModels(models: Model[]): ModelGroup[] { - const groups = new Map(); - for (const entry of models) { - const key = `${entry.provider}/${entry.model}`; - const group = groups.get(key) ?? { - key, - provider: entry.provider, - model: entry.model, - variants: [], - apiSurfaces: [], - capabilities: [], - maxParams: 0, - }; - group.variants.push(entry); - groups.set(key, group); - } - return [...groups.values()].map(finishGroup); -} - -export interface ApiSurfaceFacet { - apiSurface: ApiSurface; - count: number; -} - -export function buildApiSurfaceFacets(groups: ModelGroup[]): ApiSurfaceFacet[] { - return API_SURFACES.map(({ value }) => ({ - apiSurface: value, - count: groups.filter((group) => group.apiSurfaces.includes(value)).length, - })).filter((facet) => facet.count > 0); -} - -export function buildModelGroupProviderFacets(groups: ModelGroup[]): ProviderFacet[] { - const counts = new Map(); - for (const group of groups) { - counts.set(group.provider, (counts.get(group.provider) ?? 0) + 1); - } - return [...counts.entries()] - .map(([provider, count]) => ({ provider, count })) - .sort((a, b) => b.count - a.count || a.provider.localeCompare(b.provider)); -} diff --git a/src/views/index.ejs b/src/views/index.ejs index a42ebf2..07a8690 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -4,9 +4,9 @@ Every API parameter,
for every model.

- An open, community-maintained catalog of LLM API parameters — temperature, top_p, - max_tokens and the rest of the request body. - Browse the UI below, query the API, install the npm package, or give your coding agent the MCP server. + An open, community-maintained catalog of LLM API parameters, scoped to the API + surface your SDK calls. Choose a surface, then browse the exact request fields each + model accepts.

@@ -72,8 +72,40 @@ aria-label="Search and filter models" class="search-bar -mx-4 px-4 py-2 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8" > -
-
+
+
+
+

+ API surface +

+

+ Parameters change with the API surface your SDK calls. +

+
+
+ + <% for (const surface of apiSurfaces) { %> + + <% } %> +
+
+
-
-

- API surface -

-
- - <% for (const surface of apiSurfaces) { %> - - <% } %> -
-
-
@@ -267,16 +270,16 @@
<% const byProvider = new Map(); - for (const group of modelGroups) { - const list = byProvider.get(group.provider) || []; - list.push(group); - byProvider.set(group.provider, list); + for (const model of models) { + const list = byProvider.get(model.provider) || []; + list.push(model); + byProvider.set(model.provider, list); } %>

- <%= modelGroups.length %> of - <%= modelGroups.length %> models + <%= models.length %> of + <%= models.length %> model surfaces

<% for (const pf of providers) { %> - <% const providerGroups = byProvider.get(pf.provider) || []; %> + <% const providerModels = byProvider.get(pf.provider) || []; %>

<%= helpers.providerLabel(pf.provider) %>

- <% for (const group of providerGroups) { %><%- include("partials/model_row", { group: group, helpers: helpers }) %><% } %> + <% for (const model of providerModels) { %><%- include("partials/model_row", { model: model, helpers: helpers }) %><% } %> <% } %>
- - <%= modelName %> + + <%= modelName %> + -
- <% if (group.variants.length > 1) { %> -
- <% for (let i = 0; i < group.variants.length; i++) { const variant = group.variants[i]; %> - - <% } %> +
+ SDK method + <%= helpers.apiSurfaceSdkMethod(model.apiSurface) %> + <%= helpers.authLabel(model.authType) %>
+ <% if (model.params.length === 0) { %> +

No parameters documented yet.

+ <% } else { %> + <%- include("param_table", { params: model.params, helpers: helpers }) %> <% } %> - - <% for (let i = 0; i < group.variants.length; i++) { const variant = group.variants[i]; %> -
-
- <%= helpers.apiSurfaceLabel(variant.apiSurface) %> - <%= helpers.apiSurfaceSdkMethod(variant.apiSurface) %> - <%= helpers.authLabel(variant.authType) %> - <% if (variant.status === "deprecated") { %> - Deprecated - <% } else if (variant.status === "retired") { %> - Retired - <% } %> - <%= variant.params.length %> param<%= variant.params.length === 1 ? "" : "s" %> -
- <% if (variant.params.length === 0) { %> -

No parameters documented yet.

- <% } else { %> - <%- include("param_table", { params: variant.params, helpers: helpers }) %> - <% } %> - + - <% } %>
diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index 4b9061d..feb7ed6 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -1,8 +1,11 @@ import { describe, it, expect } from "vitest"; import { buildCapabilityFacets, buildCatalog, uniqueProviders } from "../src/data/catalog.js"; import { describeApplicability } from "../src/data/applicability.js"; -import { apiSurfaceLabel, apiSurfaceSdkMethod } from "../src/data/api-surfaces.js"; -import { buildApiSurfaceFacets, groupModels } from "../src/data/model-groups.js"; +import { + apiSurfaceLabel, + apiSurfaceSdkMethod, + buildApiSurfaceFacets, +} from "../src/data/api-surfaces.js"; import { modelFullLabel, modelLabel, paramLabel, providerLabel } from "../src/data/display.js"; import { findModelParams, @@ -70,8 +73,8 @@ describe("buildCapabilityFacets", () => { }); }); -describe("groupModels", () => { - it("keeps API surfaces as flat variants under one provider/model row", () => { +describe("buildApiSurfaceFacets", () => { + it("counts each API-surface-specific model entry", () => { const chat = makeModel({ provider: "openai", model: "gpt-5.5", @@ -83,12 +86,7 @@ describe("groupModels", () => { authType: "subscription", apiSurface: "openai-responses", }); - const groups = groupModels([responses, chat]); - - expect(groups).toHaveLength(1); - expect(groups[0]?.variants).toEqual([chat, responses]); - expect(groups[0]?.apiSurfaces).toEqual(["openai-chat-completions", "openai-responses"]); - expect(buildApiSurfaceFacets(groups)).toEqual([ + expect(buildApiSurfaceFacets([responses, chat])).toEqual([ { apiSurface: "openai-chat-completions", count: 1 }, { apiSurface: "openai-responses", count: 1 }, ]); @@ -175,7 +173,7 @@ describe("providerless model params", () => { describe("display helpers", () => { it("labels API surfaces with their representative SDK method", () => { - expect(apiSurfaceLabel("openai-responses")).toBe("Responses"); + expect(apiSurfaceLabel("openai-responses")).toBe("Responses API"); expect(apiSurfaceSdkMethod("openai-responses")).toBe("responses.create"); }); diff --git a/tests/server.test.ts b/tests/server.test.ts index 16293c2..83dca07 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -166,11 +166,13 @@ describe("GET / (home)", () => { it("carries a concrete title and a crawlable browse-by-parameter section", async () => { const body = await get("/").then((r) => r.text()); - expect(body).toContain("modelparams.dev · LLM API Parameters for 2 Models"); + expect(body).toContain("modelparams.dev · LLM API Parameters for 3 Models"); expect(body).toContain("API surface"); expect(body).toContain("Chat Completions"); - expect(body.match(/id="anthropic--claude-opus-4-7"/g)).toHaveLength(1); - expect(body).toContain('data-model-variant-trigger="1"'); + expect(body.match(/data-model-name="claude opus 4\.7"/g)).toHaveLength(2); + expect(body.match(/data-model-api-surface="anthropic-messages"/g)).toHaveLength(2); + expect(body).not.toContain("data-model-variant-trigger"); + expect(body).toContain("SDK method"); expect(body).toContain("Browse by API parameter"); expect(body).toContain('href="/parameters/temperature"'); expect(body).toContain('href="/parameters/max_tokens"');