From cbe88b540badab7160d8135ea7df7db8a7084dd0 Mon Sep 17 00:00:00 2001 From: Bruno Perez Date: Tue, 4 Aug 2026 18:02:21 +0200 Subject: [PATCH 1/2] fix: stop ranking for "how many parameters does GPT have" Search results were pulling the site into weight-count queries ("how many parameters is haiku 4.5") instead of the API-settings audience it serves. Titles, H1s and the model FAQ all matched the count phrasing exactly. Every page title and H1 now says "API parameters". The model FAQ no longer asks "how many parameters does X accept", which sat in FAQPage JSON-LD as a claim to answer count queries. Dataset JSON-LD gained keywords and an about pointing at a DefinedTerm for the API-parameter sense. Adds /model-parameters-vs-api-parameters to absorb the count intent so the 200+ model pages stop competing for it. llms.txt states the distinction up front for AI answer engines. --- README.md | 4 +- src/build/build.ts | 8 +++ src/build/og.ts | 14 ++++- src/build/render-disambiguation.ts | 40 ++++++++++++++ src/build/render-glossary.ts | 4 +- src/build/render-model.ts | 18 ++++-- src/build/render-provider.ts | 8 ++- src/build/render.ts | 13 +++-- src/build/structured-data.ts | 89 +++++++++++++++++++++++++++--- src/data/disambiguation.ts | 56 +++++++++++++++++++ src/data/faq.ts | 12 +++- src/data/llms.ts | 19 +++++-- src/data/site.ts | 17 +++++- src/data/urls.ts | 8 +++ src/server/app.ts | 12 ++++ src/views/disambiguation.ejs | 87 +++++++++++++++++++++++++++++ src/views/glossary.ejs | 2 +- src/views/index.ejs | 12 ++-- src/views/model.ejs | 7 ++- src/views/partials/footer.ejs | 3 +- src/views/provider.ejs | 2 +- tests/disambiguation.test.ts | 50 +++++++++++++++++ tests/faq.test.ts | 15 ++++- tests/render-meta.test.ts | 24 +++++++- tests/server.test.ts | 15 ++++- 25 files changed, 486 insertions(+), 53 deletions(-) create mode 100644 src/build/render-disambiguation.ts create mode 100644 src/data/disambiguation.ts create mode 100644 src/views/disambiguation.ejs create mode 100644 tests/disambiguation.test.ts diff --git a/README.md b/README.md index c8a2ddd..a8e161b 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ # modelparams.dev -> An open, community-maintained catalog of model parameters. +> An open, community-maintained catalog of LLM API parameters. [![npm version](https://img.shields.io/npm/v/modelparams.svg)](https://www.npmjs.com/package/modelparams) [![npm downloads](https://img.shields.io/npm/dm/modelparams.svg)](https://www.npmjs.com/package/modelparams) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -Every parameter each AI model accepts, in one place. Inspired by [models.dev](https://github.com/anomalyco/models.dev); we use it at [Manifest](https://manifest.build/). +Every API parameter each AI model accepts, in one place: `temperature`, `top_p`, `max_tokens` and the rest of the request body, with defaults, ranges and gating conditions. Not weight counts — see [model parameters vs. API parameters](https://modelparams.dev/model-parameters-vs-api-parameters). Inspired by [models.dev](https://github.com/anomalyco/models.dev); we use it at [Manifest](https://manifest.build/). ## TypeScript diff --git a/src/build/build.ts b/src/build/build.ts index ffb4ad5..e89194d 100644 --- a/src/build/build.ts +++ b/src/build/build.ts @@ -21,6 +21,7 @@ import { SITE_URL } from "../data/site.js"; import { buildRobotsTxt } from "../data/robots.js"; import { API_PATH, + DISAMBIGUATION_PATH, GLOSSARY_PATH, modelPagePath, parameterPagePath, @@ -32,6 +33,7 @@ import { bundleClientScript, compileStyles, copyStaticAssets } from "./assets.js import { gitLastmodMap, modelLastmod, newestLastmod } from "./lastmod.js"; import { renderIndex } from "./render.js"; import { renderApiPage } from "./render-api.js"; +import { renderDisambiguationPage } from "./render-disambiguation.js"; import { renderGlossaryPage } from "./render-glossary.js"; import { renderModelPage } from "./render-model.js"; import { defaultSummary,renderParameterPage, rangeSummary } from "./render-parameter.js"; @@ -75,6 +77,7 @@ async function writeRobotsAndSitemap(models: Model[]): Promise { const entries: { path: string; priority: string; lastmod: string }[] = [ { path: "/", priority: "1.0", lastmod: freshest(models) }, { path: GLOSSARY_PATH, priority: "0.7", lastmod: freshest(models) }, + { path: DISAMBIGUATION_PATH, priority: "0.6", lastmod: freshest(models) }, { path: API_PATH, priority: "0.5", lastmod: freshest(models) }, ...uniqueProviders(models).map((provider) => ({ path: providerPagePath(provider), @@ -123,6 +126,11 @@ async function writeHtmlPages(models: Model[]): Promise { } await fs.writeFile(path.join(DIST_DIR, "glossary.html"), await renderGlossaryPage(models), "utf8"); + await fs.writeFile( + path.join(DIST_DIR, `${DISAMBIGUATION_PATH.replace(/^\//, "")}.html`), + await renderDisambiguationPage(models), + "utf8", + ); await fs.writeFile(path.join(DIST_DIR, "api.html"), await renderApiPage(models), "utf8"); await fs.writeFile(path.join(DIST_DIR, "404.html"), await renderNotFoundPage(models), "utf8"); } diff --git a/src/build/og.ts b/src/build/og.ts index 9940c53..6e06c5a 100644 --- a/src/build/og.ts +++ b/src/build/og.ts @@ -14,6 +14,7 @@ import { buildParameterIndex, type ParameterDetail } from "../data/parameters.js import { CLIENT_DIR, DIST_ASSETS_DIR, DIST_DIR } from "../data/paths.js"; import { API_PATH, + DISAMBIGUATION_PATH, GLOSSARY_PATH, modelPagePath, ogImagePath, @@ -68,7 +69,7 @@ export function providerCard(provider: string, models: Model[]): OgCard { const paths = paramPaths(models); return { eyebrow: "Provider", - headline: `${providerLabel(provider)} model parameters`, + headline: `${providerLabel(provider)} API parameters`, subline: `${models.length} model${models.length === 1 ? "" : "s"} · ${paths.length} parameters tracked`, chips: paths.slice(0, CHIP_LIMIT), }; @@ -87,11 +88,19 @@ export function parameterCard(detail: ParameterDetail, facts: string[]): OgCard export function glossaryCard(parameterCount: number): OgCard { return { eyebrow: "Glossary", - headline: "LLM parameter glossary", + headline: "LLM API parameter glossary", subline: `${parameterCount} parameters defined, with defaults and ranges`, }; } +export function disambiguationCard(): OgCard { + return { + eyebrow: "Explainer", + headline: "Model parameters vs. API parameters", + subline: "Weight count or request settings — which one you're looking for", + }; +} + export function apiCard(modelCount: number): OgCard { return { eyebrow: "Documentation", @@ -140,6 +149,7 @@ export async function writeOgImages( await writeCard("/", home); await writeCard(GLOSSARY_PATH, glossaryCard(details.length)); await writeCard(API_PATH, apiCard(models.length)); + await writeCard(DISAMBIGUATION_PATH, disambiguationCard()); for (const provider of providers) { const providerModels = models.filter((model) => model.provider === provider); diff --git a/src/build/render-disambiguation.ts b/src/build/render-disambiguation.ts new file mode 100644 index 0000000..4a4fa7f --- /dev/null +++ b/src/build/render-disambiguation.ts @@ -0,0 +1,40 @@ +import path from "node:path"; +import ejs from "ejs"; +import { disambiguationFaq } from "../data/disambiguation.js"; +import { VIEWS_DIR } from "../data/paths.js"; +import { SITE_NAME, SITE_URL } from "../data/site.js"; +import { DISAMBIGUATION_PATH, GLOSSARY_PATH, absolute, ogImagePath } from "../data/urls.js"; +import { type Model } from "../schema/model.js"; +import { buildDisambiguationStructuredData } from "./structured-data.js"; +import { hubLinks, renderShell, viewHelpers } from "./render.js"; + +const TITLE = `Model parameters vs. API parameters · ${SITE_NAME}`; + +// Names both senses in the snippet. Someone searching for a weight count should +// be able to tell from the SERP that this page answers them and that the rest of +// the site won't. +const DESCRIPTION = + "A model's parameter count is its trained weights. API parameters are the request settings like temperature and top_p. Which one you want, and where to find it."; + +export async function renderDisambiguationPage(allModels: Model[]): Promise { + const faqs = disambiguationFaq(allModels.length); + const body = await ejs.renderFile(path.join(VIEWS_DIR, "disambiguation.ejs"), { + helpers: viewHelpers, + faqs, + modelCount: allModels.length, + glossaryPath: GLOSSARY_PATH, + }); + + return renderShell( + { + title: TITLE, + description: DESCRIPTION, + canonicalUrl: absolute(SITE_URL, DISAMBIGUATION_PATH), + ogImage: ogImagePath(DISAMBIGUATION_PATH), + ogType: "article", + structuredData: buildDisambiguationStructuredData(faqs, DESCRIPTION, SITE_URL), + providerHubs: hubLinks(allModels), + }, + body, + ); +} diff --git a/src/build/render-glossary.ts b/src/build/render-glossary.ts index b676165..d5e82e5 100644 --- a/src/build/render-glossary.ts +++ b/src/build/render-glossary.ts @@ -8,14 +8,14 @@ import { type Model } from "../schema/model.js"; import { buildGlossaryStructuredData } from "./structured-data.js"; import { hubLinks, renderShell, viewHelpers } from "./render.js"; -const GLOSSARY_TITLE = `LLM parameter glossary · ${SITE_NAME}`; +const GLOSSARY_TITLE = `LLM API parameter glossary · ${SITE_NAME}`; const GLOSSARY_DESCRIPTION = "Every LLM API parameter defined: what temperature, top_p, max_tokens and reasoning effort do. Open any parameter for its default and range on every model."; function glossaryIntro(groups: GlossaryGroup[]): string { const total = groups.reduce((sum, groupItem) => sum + groupItem.entries.length, 0); - return `${total} parameters appear across the catalog. This page defines each one, grouped by what it controls. Open any parameter for the full breakdown — its default, range, and conditions on every model that accepts it. Definitions come from the same community-maintained data as the JSON API.`; + return `${total} API parameters appear across the catalog. This page defines each one, grouped by what it controls. Open any parameter for the full breakdown — its default, range, and conditions on every model that accepts it. Definitions come from the same community-maintained data as the JSON API.`; } export async function renderGlossaryPage(allModels: Model[]): Promise { diff --git a/src/build/render-model.ts b/src/build/render-model.ts index c69506d..f40e2d7 100644 --- a/src/build/render-model.ts +++ b/src/build/render-model.ts @@ -7,6 +7,7 @@ import { groupParams } from "../data/group.js"; import { VIEWS_DIR } from "../data/paths.js"; import { SITE_NAME, SITE_URL } from "../data/site.js"; import { + DISAMBIGUATION_PATH, absolute, modelJsonPath, modelPagePath, @@ -27,7 +28,15 @@ export function modelPageTitle(model: Model): string { // separate URLs, so a fallback without it would give them the same title. const variant = model.authType === "subscription" ? " (subscription)" : ""; const who = `${modelFullLabel(model)}${variant}`; - return fitTitle([`${who} parameters · ${SITE_NAME}`, `${who} parameters`, who]); + // "API" survives every fallback, including the shortest. Bare " + // parameters" is what people type when they want a weight count, and a title + // that matches it pulls the page into the wrong result set — so the site name + // and then the provider prefix go first, and the qualifier never does. + return fitTitle([ + `${who} API parameters · ${SITE_NAME}`, + `${who} API parameters`, + `${modelLabel(model)}${variant} API parameters`, + ]); } const PARAM_TAIL = ". Type, default, range, and gating conditions for each."; @@ -35,7 +44,7 @@ const PARAM_TAIL = ". Type, default, range, and gating conditions for each."; export function modelPageDescription(model: Model): string { const who = `${modelFullLabel(model)}${authNote(model)}`; if (model.params.length === 0) { - return `${who}: no parameters documented yet. Browse the open catalog of model parameters on ${SITE_NAME}.`; + return `${who}: no API parameters documented yet. Browse the open catalog of LLM API parameters on ${SITE_NAME}.`; } const count = `${model.params.length} API parameter${model.params.length === 1 ? "" : "s"}`; const head = `All ${count} for ${who}`; @@ -87,13 +96,13 @@ export function modelParamProse(model: Model): ParamProseGroup[] { export function modelIntro(model: Model): string { const who = modelFullLabel(model); if (model.params.length === 0) { - return `No parameters are documented yet for ${who}. The data is community-maintained, so this page fills in as entries land.`; + return `No API parameters are documented yet for ${who}. The data is community-maintained, so this page fills in as entries land.`; } const access = model.authType === "subscription" ? " when you reach it through a subscription rather than an API key" : ""; - return `These are the parameters ${SITE_NAME} tracks for ${who}${access}. Each row gives the type, default, valid range or values, and the conditions that gate it. It's the same data the JSON API serves.`; + return `These are the API parameters ${SITE_NAME} tracks for ${who}${access} — the settings you send in a request. Each row gives the type, default, valid range or values, and the conditions that gate it. It's the same data the JSON API serves.`; } export async function renderModelPage(model: Model, allModels: Model[]): Promise { @@ -116,6 +125,7 @@ export async function renderModelPage(model: Model, allModels: Model[]): Promise modelJson: JSON.stringify({ $schema: "https://modelparams.dev/api/v1/schema.json", ...model }, null, 2), isSubscription: model.authType === "subscription", proseGroups: modelParamProse(model), + disambiguationPath: DISAMBIGUATION_PATH, }); const description = modelPageDescription(model); diff --git a/src/build/render-provider.ts b/src/build/render-provider.ts index a82e997..652bdb0 100644 --- a/src/build/render-provider.ts +++ b/src/build/render-provider.ts @@ -10,18 +10,20 @@ import { DESCRIPTION_MAX, fitDescription, sampleList } from "./meta.js"; import { hubLinks, renderShell, viewHelpers } from "./render.js"; export function providerPageTitle(provider: string): string { - return `${providerLabel(provider)} model parameters · ${SITE_NAME}`; + return `${providerLabel(provider)} API parameters · ${SITE_NAME}`; } const PARAM_TAIL = ". Type, default, range, and gating conditions for each."; export function providerPageDescription(provider: string, models: Model[]): string { const count = `${models.length} ${providerLabel(provider)} model${models.length === 1 ? "" : "s"}`; - const head = `Parameters for ${count}`; + const head = `API parameters for ${count}`; const sample = sampleList( sampleParams(models), DESCRIPTION_MAX - head.length - PARAM_TAIL.length - 2, ); + // Every candidate keeps "API parameters" via `head`; none may fall back to a + // bare "Parameters for …", which reads as weight count. return fitDescription([ `${head}: ${sample}${PARAM_TAIL}`, `${head}${PARAM_TAIL}`, @@ -39,7 +41,7 @@ function sampleParams(models: Model[]): string[] { export function providerIntro(provider: string, models: Model[]): string { const count = `${models.length} ${providerLabel(provider)} model${models.length === 1 ? "" : "s"}`; - return `${SITE_NAME} tracks parameters for ${count}. Open a model to see its full set: the type, default, valid range or values, and the conditions that gate each parameter.`; + return `${SITE_NAME} tracks API parameters for ${count}. Open a model to see its full set: the type, default, valid range or values, and the conditions that gate each parameter.`; } export async function renderProviderPage( diff --git a/src/build/render.ts b/src/build/render.ts index e9586db..ef88062 100644 --- a/src/build/render.ts +++ b/src/build/render.ts @@ -115,15 +115,16 @@ export interface RenderOptions { /** * Brand-first homepage title. Interior pages read " · modelparams.dev"; * the homepage inverts that so the brand leads, which is what a branded search - * ("modelparams") and a link in a feed both want to see first. "LLM parameters" - * is the head term people actually type — "model parameters" reads as weights - * to an ML audience, so the descriptive half says LLM. The live count carries - * the scale that makes the result worth clicking. + * ("modelparams") and a link in a feed both want to see first. The descriptive + * half says "LLM API parameters" in full: "model parameters" and even "LLM + * parameters" both read as weight count to a general audience, and API is the + * one word that weight-count queries never contain. The live count carries the + * scale that makes the result worth clicking. */ export function homeTitle(modelCount: number): string { return fitTitle([ - `${SITE_NAME} — LLM Parameters for ${modelCount} Models`, - `${SITE_NAME} — LLM Parameters`, + `${SITE_NAME} — LLM API Parameters for ${modelCount} Models`, + `${SITE_NAME} — LLM API Parameters`, ]); } diff --git a/src/build/structured-data.ts b/src/build/structured-data.ts index 7741c46..0565c37 100644 --- a/src/build/structured-data.ts +++ b/src/build/structured-data.ts @@ -5,8 +5,9 @@ import { modelFullLabel, modelLabel, paramLabel, providerLabel } from "../data/d import type { ModelFaq } from "../data/faq.js"; import type { GlossaryGroup } from "../data/glossary.js"; import type { ParameterDetail } from "../data/parameters.js"; -import { SITE_DESCRIPTION, SITE_NAME } from "../data/site.js"; +import { SITE_DESCRIPTION, SITE_KEYWORDS, SITE_NAME } from "../data/site.js"; import { + DISAMBIGUATION_PATH, GLOSSARY_PATH, absolute, modelJsonPath, @@ -19,6 +20,30 @@ import { type Model } from "../schema/model.js"; const REPO_URL = "https://github.com/mnfst/modelparams.dev"; const NPM_URL = "https://www.npmjs.com/package/modelparams"; +/** Stable @id for the concept node every dataset on the site points `about` at. */ +function conceptId(siteUrl: string): string { + return `${siteUrl}/#api-parameter`; +} + +/** + * The subject of this whole site, stated once as an entity: an API request + * parameter, not a trained weight. `variableMeasured` already names temperature + * and top_p on each model page, which is good evidence of the intended sense; + * this makes it explicit rather than inferred, so a crawler resolving "model + * parameters" has something to bind to besides the ambiguous word itself. + */ +function apiParameterConceptNode(siteUrl: string) { + return { + "@type": "DefinedTerm", + "@id": conceptId(siteUrl), + name: "LLM API parameter", + description: + "A setting sent in the body of an LLM API request — temperature, top_p, max_tokens and the like. Not the same as a model parameter in the sense of a trained weight, which is what a parameter count measures.", + inDefinedTermSet: `${siteUrl}${GLOSSARY_PATH}#termset`, + url: absolute(siteUrl, DISAMBIGUATION_PATH), + }; +} + interface Crumb { name: string; path: string; @@ -75,6 +100,8 @@ function homeDatasetNode(siteUrl: string, imageUrl: string) { description: SITE_DESCRIPTION, url: `${siteUrl}/`, image: imageUrl, + keywords: SITE_KEYWORDS, + about: { "@id": conceptId(siteUrl) }, license: "https://opensource.org/licenses/MIT", isAccessibleForFree: true, creator: { "@id": `${siteUrl}/#org` }, @@ -115,15 +142,22 @@ export function buildHomeStructuredData( return graph([ organizationNode(siteUrl), homeWebsiteNode(siteUrl), + apiParameterConceptNode(siteUrl), homeDatasetNode(siteUrl, imageUrl), homeItemListNode(models, siteUrl), ]); } -function faqPageNode(faqs: ModelFaq[], model: Model, siteUrl: string) { +/** Both the model FAQ and the disambiguation FAQ reduce to this shape. */ +interface QuestionAnswer { + question: string; + answer: string; +} + +function faqPageNode(faqs: QuestionAnswer[], id: string) { return { "@type": "FAQPage", - "@id": `${siteUrl}${modelPagePath(model)}#faq`, + "@id": id, mainEntity: faqs.map((faq) => ({ "@type": "Question", name: faq.question, @@ -138,7 +172,7 @@ export function buildModelStructuredData( siteUrl: string, faqs: ModelFaq[] = [], ): string { - const name = `${modelFullLabel(model)} parameters`; + const name = `${modelFullLabel(model)} API parameters`; const dataset = { "@type": "Dataset", "@id": `${siteUrl}${modelPagePath(model)}#dataset`, @@ -146,6 +180,8 @@ export function buildModelStructuredData( description, url: absolute(siteUrl, modelPagePath(model)), isPartOf: { "@id": `${siteUrl}/#dataset` }, + keywords: SITE_KEYWORDS, + about: { "@id": conceptId(siteUrl) }, license: "https://opensource.org/licenses/MIT", isAccessibleForFree: true, creator: { "@type": "Organization", name: SITE_NAME, url: `${siteUrl}/` }, @@ -166,8 +202,10 @@ export function buildModelStructuredData( { name: providerLabel(model.provider), path: providerPagePath(model.provider) }, { name: modelLabel(model), path: modelPagePath(model) }, ]); - const nodes: unknown[] = [crumbs, dataset]; - if (faqs.length > 0) nodes.push(faqPageNode(faqs, model, siteUrl)); + const nodes: unknown[] = [crumbs, apiParameterConceptNode(siteUrl), dataset]; + if (faqs.length > 0) { + nodes.push(faqPageNode(faqs, `${siteUrl}${modelPagePath(model)}#faq`)); + } return graph(nodes); } @@ -179,7 +217,7 @@ export function buildProviderStructuredData( ): string { const itemList = { "@type": "ItemList", - name: `${providerLabel(provider)} model parameters`, + name: `${providerLabel(provider)} API parameters`, description, numberOfItems: models.length, itemListElement: models.map((model, index) => ({ @@ -230,6 +268,39 @@ export function buildParameterStructuredData( return graph([crumbs, definedTerm, itemList]); } +/** + * The disambiguation page is the only place on the site that should match a + * weight-count query, so it's the only place carrying those questions in an + * FAQPage. It states both senses and points `about` at the one this catalog + * covers, which is what lets the model pages stay out of that result set. + */ +export function buildDisambiguationStructuredData( + faqs: QuestionAnswer[], + description: string, + siteUrl: string, +): string { + const pagePath = DISAMBIGUATION_PATH; + const page = { + "@type": "WebPage", + "@id": `${siteUrl}${pagePath}#page`, + name: "Model parameters vs. API parameters", + url: absolute(siteUrl, pagePath), + description, + about: { "@id": conceptId(siteUrl) }, + isPartOf: { "@id": `${siteUrl}/#website` }, + }; + const crumbs = breadcrumb(siteUrl, [ + { name: "Home", path: "/" }, + { name: "Model parameters vs. API parameters", path: pagePath }, + ]); + return graph([ + crumbs, + apiParameterConceptNode(siteUrl), + page, + faqPageNode(faqs, `${siteUrl}${pagePath}#faq`), + ]); +} + export function buildGlossaryStructuredData(groups: GlossaryGroup[], siteUrl: string): string { const terms = groups .flatMap((groupItem) => groupItem.entries) @@ -243,7 +314,9 @@ export function buildGlossaryStructuredData(groups: GlossaryGroup[], siteUrl: st const termSet = { "@type": "DefinedTermSet", "@id": `${siteUrl}${GLOSSARY_PATH}#termset`, - name: "LLM parameter glossary", + name: "LLM API parameter glossary", + description: + "Definitions for the settings you send in an LLM API request. Not model weight counts.", url: absolute(siteUrl, GLOSSARY_PATH), hasDefinedTerm: terms, }; diff --git a/src/data/disambiguation.ts b/src/data/disambiguation.ts new file mode 100644 index 0000000..ca2fcb2 --- /dev/null +++ b/src/data/disambiguation.ts @@ -0,0 +1,56 @@ +// Content for /model-parameters-vs-api-parameters. +// +// "Parameters" has two unrelated meanings in this corner of the world: the +// weight count of a trained model, and the settings you send in an API request. +// The catalog only covers the second, but search engines match model pages +// against weight-count queries ("how many parameters does gpt 3.5 have") +// because the phrasing is identical. This page exists to take that intent so +// the model pages don't have to compete for it. +// +// The FAQ lives here rather than in the view so it can back both the visible +// Q&A and the FAQPage JSON-LD from one source. + +export interface DisambiguationFaq { + question: string; + answer: string; +} + +/** + * The weight-count questions, answered straight. These are deliberately the + * phrasings people actually type — this is the one page on the site that + * should match them. + */ +export function disambiguationFaq(modelCount: number): DisambiguationFaq[] { + return [ + { + question: "Does modelparams.dev list how many parameters a model has?", + answer: + `No. ${modelCount} models are in the catalog and none of them carry a weight count. ` + + "We track API parameters, meaning the settings you send in a request. For an " + + "open-weight model the count is usually in its Hugging Face model card. For a " + + "closed model there is often no published figure at all.", + }, + { + question: "How many parameters does GPT-3.5 have?", + answer: + "OpenAI has never published it. The 175 billion figure people repeat belongs to " + + "GPT-3, which is a different model, and every number quoted for GPT-3.5 and later " + + "is an estimate rather than a disclosure.", + }, + { + question: "Are model parameters and hyperparameters the same thing?", + answer: + "No, and that is a third meaning of the word. Hyperparameters are the settings " + + "used to train a model, like learning rate and batch size. Parameters are the " + + "weights that training produces. API parameters are what you send at inference " + + "time. Same word, three unrelated things.", + }, + { + question: "Is temperature a model parameter?", + answer: + "Temperature is an API parameter. It changes how the output is sampled and is " + + "not stored in the model. Two requests to the same model at different " + + "temperatures run against identical weights.", + }, + ]; +} diff --git a/src/data/faq.ts b/src/data/faq.ts index 181a917..da5253a 100644 --- a/src/data/faq.ts +++ b/src/data/faq.ts @@ -2,6 +2,12 @@ // back both the visible Q&A section and the FAQPage JSON-LD without duplicating // logic. The questions mirror real long-tail queries ("what is the default // temperature for ") and every answer is derived from the tracked data. +// +// No question here asks "how many parameters does have". That phrasing +// belongs to the other sense of the word — weight count — and search engines +// read an FAQPage question as a strong claim about what the page answers. The +// disambiguation lives on /model-parameters-vs-api-parameters instead, so one +// page absorbs that intent rather than all 200+ model pages competing for it. import { modelFullLabel } from "./display.js"; import type { Model, Parameter } from "../schema/model.js"; @@ -39,10 +45,12 @@ export function modelFaq(model: Model): ModelFaq[] { const paths = model.params.map((param) => param.path); const faqs: ModelFaq[] = [ { - question: `How many parameters does ${subject} accept?`, + question: `Which API parameters does ${subject} support?`, answer: `${subject} accepts ${model.params.length} API parameter${ model.params.length === 1 ? "" : "s" - }: ${paths.slice(0, 6).join(", ")}${paths.length > 6 ? ", and more" : ""}.`, + } in the request body: ${paths.slice(0, 6).join(", ")}${ + paths.length > 6 ? ", and more" : "" + }.`, }, ]; const byPath = new Map(model.params.map((param) => [param.path, param])); diff --git a/src/data/llms.ts b/src/data/llms.ts index 63cb02d..c760d09 100644 --- a/src/data/llms.ts +++ b/src/data/llms.ts @@ -3,7 +3,7 @@ import { buildProviderFacets } from "./catalog.js"; import { authLabel, modelFullLabel, paramGroupLabel, providerLabel } from "./display.js"; import { groupParams } from "./group.js"; import { buildParameterIndex } from "./parameters.js"; -import { parameterPagePath } from "./urls.js"; +import { DISAMBIGUATION_PATH, parameterPagePath } from "./urls.js"; import { modelId, type Model, type Parameter } from "../schema/model.js"; const REPO_URL = "https://github.com/mnfst/modelparams.dev"; @@ -25,9 +25,14 @@ function guideIntro(siteUrl: string): string[] { return [ "# How to use modelparams.dev", "", - `[modelparams.dev](${siteUrl}) is an open, community-maintained catalog of model`, - "parameters. Each entry shows the knobs you can turn — type, default, range, and the", - "conditions that gate it.", + `[modelparams.dev](${siteUrl}) is an open, community-maintained catalog of LLM API`, + "parameters: the settings you send in a request, like `temperature` and `max_tokens`.", + "Each entry shows the knobs you can turn — type, default, range, and the conditions", + "that gate it.", + "", + 'It does not track parameter counts. "Parameters" here never means trained weights,', + "so nothing on this site answers how many parameters a model has. See", + `${siteUrl}${DISAMBIGUATION_PATH} for the difference.`, "", "The same model accessed via an **API key** and via a **subscription** usually exposes a", "different set of parameters. We list both as separate entries so the data stays honest.", @@ -154,8 +159,9 @@ export function buildLlmsTxt(siteUrl: string, models: Model[]): string { const lines: string[] = [ "# modelparams.dev", "", - "> An open, community-maintained catalog of model parameters — every knob you can", - "> turn, for every model, with API-key and subscription variants tracked separately.", + "> An open, community-maintained catalog of LLM API parameters — every request-body knob", + "> you can turn, for every model, with API-key and subscription variants tracked", + "> separately. Not parameter counts: nothing here describes model weights.", "", "All data is machine-readable: CORS-enabled static JSON validated against a published JSON", "Schema, served from the edge under `/api/v1/`. IDs are `provider/model` for API-key", @@ -169,6 +175,7 @@ export function buildLlmsTxt(siteUrl: string, models: Model[]): string { "", "## Guides", `- [Usage guide + full parameter dump](${siteUrl}/llms-full.txt): How to call the API plus every model's parameters inline.`, + `- [Model parameters vs. API parameters](${siteUrl}${DISAMBIGUATION_PATH}): Why "parameters" here means request settings and never weight counts.`, "", "## Parameters", ]; diff --git a/src/data/site.ts b/src/data/site.ts index 1a71136..88433dc 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -7,7 +7,22 @@ export const SITE_NAME = "modelparams.dev"; export const SITE_URL = process.env.SITE_URL ?? "https://modelparams.dev"; export const SITE_DESCRIPTION = - "An open, community-maintained catalog of model parameters. Search and filter every knob you can turn — API-key and subscription variants tracked separately."; + "An open, community-maintained catalog of LLM API parameters — the request settings like temperature and top_p, not model weight counts. API-key and subscription variants tracked separately."; + +/** + * Sense-fixing keywords for the JSON-LD. "Parameters" is ambiguous: the other + * reading is weight count ("how many parameters does GPT-5.5 have"). These + * name the reading this catalog actually covers. + */ +export const SITE_KEYWORDS = [ + "LLM API parameters", + "sampling parameters", + "inference settings", + "request body", + "temperature", + "top_p", + "max_tokens", +]; /** Path to the social-share image, relative to the site root. */ export const OG_IMAGE_PATH = "/assets/og.png"; diff --git a/src/data/urls.ts b/src/data/urls.ts index f710387..e7966b8 100644 --- a/src/data/urls.ts +++ b/src/data/urls.ts @@ -22,6 +22,14 @@ export const GLOSSARY_PATH = "/glossary"; /** API documentation page. The HTML docs, not the JSON endpoints under /api/v1. */ export const API_PATH = "/api"; +/** + * The page that separates the two meanings of "model parameters": weight count + * versus the request settings this catalog documents. Every other page links + * here rather than restating the distinction, so search engines have one target + * for weight-count queries instead of matching them against every model page. + */ +export const DISAMBIGUATION_PATH = "/model-parameters-vs-api-parameters"; + /** * URL-safe slug for a parameter path: lowercased, with nested-path dots turned into * hyphens, e.g. `thinking.type` → `thinking-type`. Underscores are kept so that diff --git a/src/server/app.ts b/src/server/app.ts index 32b8ac9..b5cf2c1 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -10,8 +10,10 @@ import { renderModelPage } from "../build/render-model.js"; import { renderParameterPage } from "../build/render-parameter.js"; import { renderProviderPage } from "../build/render-provider.js"; import { renderGlossaryPage } from "../build/render-glossary.js"; +import { renderDisambiguationPage } from "../build/render-disambiguation.js"; import { renderApiPage } from "../build/render-api.js"; import { SITE_URL } from "../data/site.js"; +import { DISAMBIGUATION_PATH } from "../data/urls.js"; import { modelId, type Model } from "../schema/model.js"; /** @@ -71,6 +73,16 @@ export function makeApp(loadModels: LoadModels): express.Express { } }); + app.get(DISAMBIGUATION_PATH, async (_req, res, next) => { + try { + const models = await loadModels(); + res.setHeader("Cache-Control", "no-store"); + res.type("html").send(await renderDisambiguationPage(models)); + } catch (err) { + next(err); + } + }); + app.get("/parameters/:slug", async (req, res, next) => { try { const models = await loadModels(); diff --git a/src/views/disambiguation.ejs b/src/views/disambiguation.ejs new file mode 100644 index 0000000..b000bcf --- /dev/null +++ b/src/views/disambiguation.ejs @@ -0,0 +1,87 @@ +<%- include("partials/breadcrumbs", { items: [ + { name: "Home", href: "/" }, + { name: "Model parameters vs. API parameters" } +] }) %> + +
+

Model parameters vs. API parameters

+

+ Two unrelated things go by the name "parameters", and people looking for one + routinely land on the other. The short version: a parameter count tells you + how big a model is, and this site doesn't track it. API parameters are what you put + in the request body, and that's the entire catalog. +

+
+ +
+

Parameter count: the weights

+

+ A model's parameter count is how many trained weights it has. GPT-3 shipped with 175 + billion of them. Llama 3.1 405B puts the number in its name. It's a rough proxy for + how expensive a model is to run, and not much else — a good 70B model from this year + will beat a 175B model from 2020 on almost any task you care about. +

+

+ For most of the models people ask about, the number isn't public. OpenAI has never + published a count for GPT-3.5 or anything since. Neither has Anthropic for Claude, or + Google for Gemini. Figures that circulate for those models are estimates and leaks. + Open-weight models are the exception: if you can download the weights you can count + them, which is why Llama, Qwen, DeepSeek and Mistral all state exact numbers in their + model cards. +

+
+ +
+

API parameters: the request settings

+

+ These are the fields you send when you call a model: temperature, + top_p, + max_tokens, + stop, reasoning effort, + and whatever else a given endpoint accepts. They change what comes back for a given + prompt. They don't change the model. +

+

+ Every provider names and gates them differently. OpenAI's temperature range isn't + Google's. Some parameters only apply once another one is set, and some quietly stop + working on reasoning models. That's the mess this catalog documents: <%= modelCount %> + models under one schema, with the type, default, valid range, and gating conditions + for each field. +

+
+ +
+

Which one are you after?

+
+
+

The request settings

+

+ Defaults, ranges and gating conditions for every tracked model. +

+

+ Browse the catalog → + Read the parameter glossary → +

+
+
+

The weight count

+

+ We don't have it. Check the model card on Hugging Face for open-weight models, or + the provider's own announcement post. For closed models, expect to find nothing + official. +

+
+
+
+ +
+

Frequently asked questions

+
+ <% for (const faq of faqs) { %> +
+
<%= faq.question %>
+
<%= faq.answer %>
+
+ <% } %> +
+
diff --git a/src/views/glossary.ejs b/src/views/glossary.ejs index ea0de79..27f679c 100644 --- a/src/views/glossary.ejs +++ b/src/views/glossary.ejs @@ -4,7 +4,7 @@ ] }) %>
-

LLM parameter glossary

+

LLM API parameter glossary

<%= intro %>

diff --git a/src/views/index.ejs b/src/views/index.ejs index 0658985..8abd1a5 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -1,10 +1,11 @@

- Every parameter,
for every model. + Every API parameter,
for every model.

- An open, community-maintained catalog of model parameters. + 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, or install the npm package.

@@ -269,13 +270,14 @@
<% if (capabilities.length > 0) { %> -
+
-

Browse by parameter

- All parameters → +

Browse by API parameter

+ All API parameters →

Open any parameter to see its default, range, and every model that accepts it. + Counting weights instead? That's a different thing.

<% for (const cap of capabilities) { %> diff --git a/src/views/model.ejs b/src/views/model.ejs index 0723209..f38317e 100644 --- a/src/views/model.ejs +++ b/src/views/model.ejs @@ -21,11 +21,16 @@ <%= model.params.length %> param<%= model.params.length === 1 ? "" : "s" %>

- <%= fullName %><% if (isSubscription) { %> (subscription)<% } %> parameters + <%= fullName %><% if (isSubscription) { %> (subscription)<% } %> API parameters

<%= intro %>

+

+ After the parameter count instead — how many weights <%= modelName %> has? + That's a different number, and we don't track it. + Here's the difference. +

<% if (model.params.length >= 2) { %> diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index 3dbba1b..ad4e4e6 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -14,13 +14,14 @@ modelparams.dev -

An open catalog of model parameters.

+

An open catalog of LLM API parameters.

API Glossary + Parameters vs. weights JSON API JSON Schema llms.txt diff --git a/src/views/provider.ejs b/src/views/provider.ejs index 86930b0..9913604 100644 --- a/src/views/provider.ejs +++ b/src/views/provider.ejs @@ -7,7 +7,7 @@ ] }) %>
-

<%= providerName %> model parameters

+

<%= providerName %> API parameters

<%= intro %>

diff --git a/tests/disambiguation.test.ts b/tests/disambiguation.test.ts new file mode 100644 index 0000000..1046d3b --- /dev/null +++ b/tests/disambiguation.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "vitest"; +import { disambiguationFaq } from "../src/data/disambiguation.js"; +import { buildDisambiguationStructuredData } from "../src/build/structured-data.js"; + +const SITE = "https://modelparams.dev"; + +describe("disambiguationFaq", () => { + it("carries the live model count in the lead answer", () => { + expect(disambiguationFaq(198)[0]!.answer).toContain("198 models"); + }); + + // This is the one page that should match a weight-count query. If these + // phrasings ever move off it, the model pages start competing for that + // intent again, which is the problem the page exists to solve. + it("owns the weight-count phrasings", () => { + const questions = disambiguationFaq(198).map((faq) => faq.question); + expect(questions.some((q) => /how many parameters/i.test(q))).toBe(true); + expect(questions.some((q) => /hyperparameters/i.test(q))).toBe(true); + }); + + it("answers the count question with a refusal, not a number", () => { + const [first] = disambiguationFaq(198); + expect(first!.answer).toMatch(/^No\./); + expect(first!.answer).toContain("API parameters"); + }); +}); + +describe("buildDisambiguationStructuredData", () => { + const graph = () => + JSON.parse(buildDisambiguationStructuredData(disambiguationFaq(198), "Description.", SITE))[ + "@graph" + ] as Record[]; + + it("emits a WebPage that points about at the API-parameter concept", () => { + const page = graph().find((node) => node["@type"] === "WebPage"); + expect(page?.about).toEqual({ "@id": `${SITE}/#api-parameter` }); + expect(page?.url).toBe(`${SITE}/model-parameters-vs-api-parameters`); + }); + + it("defines the concept node the about reference resolves to", () => { + const term = graph().find((node) => node["@id"] === `${SITE}/#api-parameter`); + expect(term?.["@type"]).toBe("DefinedTerm"); + expect(String(term?.description)).toContain("trained weight"); + }); + + it("puts every weight-count question in the FAQPage", () => { + const faq = graph().find((node) => node["@type"] === "FAQPage"); + expect((faq?.mainEntity as unknown[]).length).toBe(disambiguationFaq(198).length); + }); +}); diff --git a/tests/faq.test.ts b/tests/faq.test.ts index d5cb438..36274f0 100644 --- a/tests/faq.test.ts +++ b/tests/faq.test.ts @@ -32,13 +32,22 @@ function model(over: Partial = {}): Model { } describe("modelFaq", () => { - it("leads with the parameter count and names the model", () => { + it("leads with the supported parameters and names the model", () => { const faqs = modelFaq(model()); - expect(faqs[0]!.question).toBe("How many parameters does Anthropic Claude Opus 4.7 accept?"); + expect(faqs[0]!.question).toBe("Which API parameters does Anthropic Claude Opus 4.7 support?"); expect(faqs[0]!.answer).toContain("2 API parameters"); expect(faqs[0]!.answer).toContain("temperature"); }); + // "How many parameters does X have" is the weight-count query. Matching it here + // would pull every model page into that result set; /model-parameters-vs-api-parameters + // carries it instead. + it("never phrases a question as a weight-count lookup", () => { + for (const faq of modelFaq(model())) { + expect(faq.question).not.toMatch(/how many parameters/i); + } + }); + it("answers default questions with the value and range from the data", () => { const faqs = modelFaq(model()); const temp = faqs.find((f) => f.question.includes("default temperature")); @@ -69,7 +78,7 @@ describe("modelFaq", () => { }), ); expect(faqs).toHaveLength(1); - expect(faqs[0]!.question).toContain("How many parameters"); + expect(faqs[0]!.question).toContain("Which API parameters"); }); it("returns nothing for a model with no parameters", () => { diff --git a/tests/render-meta.test.ts b/tests/render-meta.test.ts index 304841d..1a2799b 100644 --- a/tests/render-meta.test.ts +++ b/tests/render-meta.test.ts @@ -101,7 +101,9 @@ describe("modelParamProse", () => { describe("model page meta", () => { it("titles api-key and subscription variants distinctly", () => { - expect(modelPageTitle(model())).toBe("Anthropic Claude Opus 4.7 parameters · modelparams.dev"); + expect(modelPageTitle(model())).toBe( + "Anthropic Claude Opus 4.7 API parameters · modelparams.dev", + ); expect(modelPageTitle(model({ authType: "subscription" }))).toContain("(subscription)"); }); @@ -114,7 +116,7 @@ describe("model page meta", () => { describe("home page meta", () => { it("leads with the brand and carries the live model count in the title", () => { - expect(homeTitle(198)).toBe("modelparams.dev — LLM Parameters for 198 Models"); + expect(homeTitle(198)).toBe("modelparams.dev — LLM API Parameters for 198 Models"); }); it("opens the description on the brand, then real parameters and live counts", () => { @@ -139,13 +141,29 @@ describe("home page meta", () => { describe("provider page meta", () => { it("names the provider and counts its models", () => { - expect(providerPageTitle("anthropic")).toBe("Anthropic model parameters · modelparams.dev"); + expect(providerPageTitle("anthropic")).toBe("Anthropic API parameters · modelparams.dev"); expect(providerPageDescription("anthropic", [model(), model()])).toContain( "2 Anthropic models", ); }); }); +// The word "parameters" alone is what someone types when they want a weight +// count. Every page title has to carry the qualifier that separates the two +// senses, including after `fitTitle` drops candidates to fit the SERP budget. +describe("titles disambiguate parameters from weight counts", () => { + it("keeps API in the model title even at the shortest fallback", () => { + const long = model({ provider: "alibaba", model: "qwen3-8-max-thinking-preview-long" }); + expect(modelPageTitle(long)).toContain("API parameters"); + expect(modelPageTitle(model({ authType: "subscription" }))).toContain("API parameters"); + }); + + it("keeps API in the home and provider titles", () => { + expect(homeTitle(1000)).toContain("API Parameters"); + expect(providerPageTitle("openai")).toContain("API parameters"); + }); +}); + // The templates above are budget-aware, but only the real catalog has the model // names and nested parameter paths long enough to blow the budget. This walks // every page the build emits so an overflowing title can't ship unnoticed. diff --git a/tests/server.test.ts b/tests/server.test.ts index 212e1f5..4fd56e1 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -163,13 +163,24 @@ 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 Parameters for 3 Models"); - expect(body).toContain("Browse by parameter"); + expect(body).toContain("modelparams.dev — LLM API Parameters for 3 Models"); + expect(body).toContain("Browse by API parameter"); expect(body).toContain('href="/parameters/temperature"'); expect(body).toContain('href="/parameters/max_tokens"'); }); }); +describe("GET /model-parameters-vs-api-parameters", () => { + it("serves the page that absorbs weight-count queries", async () => { + const res = await get("/model-parameters-vs-api-parameters"); + expect(res.status).toBe(200); + expect(res.headers.get("content-type")).toContain("text/html"); + const body = await res.text(); + expect(body).toContain("Model parameters vs. API parameters"); + expect(body).toContain("How many parameters does GPT-3.5 have?"); + }); +}); + describe("GET /glossary", () => { it("renders the glossary page", async () => { const res = await get("/glossary"); From 2de34da1ac917a999df3865c7be66fd0ceb3b973 Mon Sep 17 00:00:00 2001 From: Bruno Perez Date: Tue, 4 Aug 2026 20:13:59 +0200 Subject: [PATCH 2/2] fix: use the dot separator in the homepage title Every interior page already separates page and brand with a middot. The homepage was the only one using an em dash. --- src/build/render.ts | 4 ++-- tests/render-meta.test.ts | 2 +- tests/server.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/build/render.ts b/src/build/render.ts index ef88062..69da773 100644 --- a/src/build/render.ts +++ b/src/build/render.ts @@ -123,8 +123,8 @@ export interface RenderOptions { */ export function homeTitle(modelCount: number): string { return fitTitle([ - `${SITE_NAME} — LLM API Parameters for ${modelCount} Models`, - `${SITE_NAME} — LLM API Parameters`, + `${SITE_NAME} · LLM API Parameters for ${modelCount} Models`, + `${SITE_NAME} · LLM API Parameters`, ]); } diff --git a/tests/render-meta.test.ts b/tests/render-meta.test.ts index 1a2799b..980dda3 100644 --- a/tests/render-meta.test.ts +++ b/tests/render-meta.test.ts @@ -116,7 +116,7 @@ describe("model page meta", () => { describe("home page meta", () => { it("leads with the brand and carries the live model count in the title", () => { - expect(homeTitle(198)).toBe("modelparams.dev — LLM API Parameters for 198 Models"); + expect(homeTitle(198)).toBe("modelparams.dev · LLM API Parameters for 198 Models"); }); it("opens the description on the brand, then real parameters and live counts", () => { diff --git a/tests/server.test.ts b/tests/server.test.ts index 4fd56e1..0c31d61 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -163,7 +163,7 @@ 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 3 Models"); + expect(body).toContain("modelparams.dev · LLM API Parameters for 3 Models"); expect(body).toContain("Browse by API parameter"); expect(body).toContain('href="/parameters/temperature"'); expect(body).toContain('href="/parameters/max_tokens"');