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..69da773 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..980dda3 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..0c31d61 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");