diff --git a/package-lock.json b/package-lock.json
index 60a2515..b679c0c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7210,7 +7210,7 @@
}
},
"packages/modelparams": {
- "version": "0.0.0",
+ "version": "0.0.1",
"license": "MIT",
"devDependencies": {
"tsd": "^0.31.0"
diff --git a/packages/modelparams/README.md b/packages/modelparams/README.md
index 2d44c6a..c629368 100644
--- a/packages/modelparams/README.md
+++ b/packages/modelparams/README.md
@@ -8,6 +8,8 @@ npm install modelparams
Stop guessing which knobs each model accepts. Get autocomplete on every parameter, compile-time errors on typos and unsupported settings, and the catalog's defaults at runtime — for every provider in one tiny zero-dependency package.
+**Catalog & API:** [browse the catalog](https://modelparams.dev) · [JSON API](https://modelparams.dev/api/v1/models.json) · [API docs](https://modelparams.dev/api) · [llms.txt](https://modelparams.dev/llms.txt)
+
## Why
You're calling `claude-opus-4-7` with `frequency_penalty` set. TypeScript doesn't tell you the param doesn't exist. The provider silently ignores it. Your evals drift. Multiply by every model in your router.
diff --git a/src/build/render-model.ts b/src/build/render-model.ts
index bac4479..5610dc7 100644
--- a/src/build/render-model.ts
+++ b/src/build/render-model.ts
@@ -2,6 +2,7 @@ import path from "node:path";
import ejs from "ejs";
import { describeApplicability } from "../data/applicability.js";
import { modelLabel, paramGroupLabel, providerLabel } from "../data/display.js";
+import { modelFaq } from "../data/faq.js";
import { groupParams } from "../data/group.js";
import { VIEWS_DIR } from "../data/paths.js";
import { SITE_NAME, SITE_URL } from "../data/site.js";
@@ -82,10 +83,12 @@ export async function renderModelPage(model: Model, allModels: Model[]): Promise
.filter((other) => other.provider === model.provider && modelId(other) !== modelId(model))
.sort((a, b) => modelLabel(a).localeCompare(modelLabel(b)));
+ const faqs = modelFaq(model);
const body = await ejs.renderFile(path.join(VIEWS_DIR, "model.ejs"), {
model,
helpers: viewHelpers,
siblings,
+ faqs,
intro: modelIntro(model),
providerName: providerLabel(model.provider),
modelName: modelLabel(model),
@@ -102,7 +105,7 @@ export async function renderModelPage(model: Model, allModels: Model[]): Promise
title: modelPageTitle(model),
description,
canonicalUrl: absolute(SITE_URL, modelPagePath(model)),
- structuredData: buildModelStructuredData(model, description, SITE_URL),
+ structuredData: buildModelStructuredData(model, description, SITE_URL, faqs),
providerHubs: hubLinks(allModels),
},
body,
diff --git a/src/build/render.ts b/src/build/render.ts
index 399292a..164fe9a 100644
--- a/src/build/render.ts
+++ b/src/build/render.ts
@@ -16,7 +16,7 @@ import { groupParams } from "../data/group.js";
import { usageGuideMarkdown } from "../data/llms.js";
import { logoFor } from "../data/logos.js";
import { VIEWS_DIR } from "../data/paths.js";
-import { OG_IMAGE_PATH, SITE_DESCRIPTION, SITE_NAME, SITE_URL } from "../data/site.js";
+import { OG_IMAGE_PATH, SITE_NAME, SITE_URL } from "../data/site.js";
import {
absolute,
modelPagePath,
@@ -102,6 +102,25 @@ export interface RenderOptions {
analytics?: boolean;
}
+/** Concrete, query-shaped homepage title — names the surface, carries the live model count. */
+export function homeTitle(modelCount: number): string {
+ return `Compare model parameters across ${modelCount} models · ${SITE_NAME}`;
+}
+
+/**
+ * Benefit-first homepage description that names real parameters (the ones users
+ * actually search) plus live counts, instead of the generic site blurb.
+ */
+export function homeDescription(
+ modelCount: number,
+ providerCount: number,
+ sampleParams: string[],
+): string {
+ const lead =
+ sampleParams.length > 0 ? `Compare ${sampleParams.join(", ")}, and every other ` : "Compare every ";
+ return `${lead}API parameter — defaults, ranges, and the conditions that gate each — across ${modelCount} models from ${providerCount} providers. An open, community-maintained catalog.`;
+}
+
export async function renderIndex(opts: RenderOptions): Promise {
const body = await ejs.renderFile(path.join(VIEWS_DIR, "index.ejs"), {
models: opts.catalog.models,
@@ -110,10 +129,11 @@ export async function renderIndex(opts: RenderOptions): Promise {
helpers: viewHelpers,
});
+ const sampleParams = opts.capabilities.slice(0, 3).map((cap) => cap.path);
return renderShell(
{
- title: `${SITE_NAME} — Open catalog of model parameters`,
- description: SITE_DESCRIPTION,
+ title: homeTitle(opts.catalog.models.length),
+ description: homeDescription(opts.catalog.models.length, opts.providers.length, sampleParams),
canonicalUrl: `${SITE_URL}/`,
structuredData: buildHomeStructuredData(
opts.catalog.models,
diff --git a/src/build/structured-data.ts b/src/build/structured-data.ts
index 5069614..f49ce28 100644
--- a/src/build/structured-data.ts
+++ b/src/build/structured-data.ts
@@ -2,6 +2,7 @@
// they can be unit-tested without touching the filesystem or the renderer.
import { modelLabel, paramLabel, providerLabel } from "../data/display.js";
+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";
@@ -15,6 +16,9 @@ import {
} from "../data/urls.js";
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";
+
interface Crumb {
name: string;
path: string;
@@ -36,6 +40,17 @@ function graph(nodes: unknown[]): string {
return JSON.stringify({ "@context": "https://schema.org", "@graph": nodes });
}
+function organizationNode(siteUrl: string) {
+ return {
+ "@type": "Organization",
+ "@id": `${siteUrl}/#org`,
+ name: SITE_NAME,
+ url: `${siteUrl}/`,
+ description: SITE_DESCRIPTION,
+ sameAs: [REPO_URL, NPM_URL],
+ };
+}
+
function homeWebsiteNode(siteUrl: string) {
return {
"@type": "WebSite",
@@ -43,6 +58,7 @@ function homeWebsiteNode(siteUrl: string) {
url: `${siteUrl}/`,
name: SITE_NAME,
description: SITE_DESCRIPTION,
+ publisher: { "@id": `${siteUrl}/#org` },
potentialAction: {
"@type": "SearchAction",
target: { "@type": "EntryPoint", urlTemplate: `${siteUrl}/?q={search_term_string}` },
@@ -61,7 +77,7 @@ function homeDatasetNode(siteUrl: string, imageUrl: string) {
image: imageUrl,
license: "https://opensource.org/licenses/MIT",
isAccessibleForFree: true,
- creator: { "@type": "Organization", name: SITE_NAME, url: `${siteUrl}/` },
+ creator: { "@id": `${siteUrl}/#org` },
distribution: {
"@type": "DataDownload",
encodingFormat: "application/json",
@@ -97,16 +113,30 @@ export function buildHomeStructuredData(
imageUrl: string,
): string {
return graph([
+ organizationNode(siteUrl),
homeWebsiteNode(siteUrl),
homeDatasetNode(siteUrl, imageUrl),
homeItemListNode(models, siteUrl),
]);
}
+function faqPageNode(faqs: ModelFaq[], model: Model, siteUrl: string) {
+ return {
+ "@type": "FAQPage",
+ "@id": `${siteUrl}${modelPagePath(model)}#faq`,
+ mainEntity: faqs.map((faq) => ({
+ "@type": "Question",
+ name: faq.question,
+ acceptedAnswer: { "@type": "Answer", text: faq.answer },
+ })),
+ };
+}
+
export function buildModelStructuredData(
model: Model,
description: string,
siteUrl: string,
+ faqs: ModelFaq[] = [],
): string {
const name = `${providerLabel(model.provider)} ${modelLabel(model)} parameters`;
const dataset = {
@@ -136,7 +166,9 @@ export function buildModelStructuredData(
{ name: providerLabel(model.provider), path: providerPagePath(model.provider) },
{ name: modelLabel(model), path: modelPagePath(model) },
]);
- return graph([crumbs, dataset]);
+ const nodes: unknown[] = [crumbs, dataset];
+ if (faqs.length > 0) nodes.push(faqPageNode(faqs, model, siteUrl));
+ return graph(nodes);
}
export function buildProviderStructuredData(
diff --git a/src/data/faq.ts b/src/data/faq.ts
new file mode 100644
index 0000000..ba9d088
--- /dev/null
+++ b/src/data/faq.ts
@@ -0,0 +1,60 @@
+// Data-driven FAQ for a model page. Pure function of the catalog entry so it can
+// 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.
+
+import { modelLabel, providerLabel } from "./display.js";
+import type { Model, Parameter } from "../schema/model.js";
+
+export interface ModelFaq {
+ question: string;
+ answer: string;
+}
+
+/** Widely-searched sampling knobs, in the order we surface them when present. */
+const FAQ_PARAMS = ["temperature", "top_p", "max_tokens", "top_k"];
+const MAX_FAQS = 5;
+
+function subjectOf(model: Model): string {
+ const auth = model.authType === "subscription" ? " (subscription)" : "";
+ return `${providerLabel(model.provider)} ${modelLabel(model)}${auth}`;
+}
+
+function defaultAnswer(param: Parameter, subject: string): string {
+ let answer = `The default ${param.path} for ${subject} is ${JSON.stringify(param.default)}`;
+ if ((param.type === "integer" || param.type === "number") && param.range) {
+ const { min, max } = param.range;
+ if (min !== undefined && max !== undefined)
+ answer += `, within a valid range of ${min} to ${max}`;
+ else if (min !== undefined) answer += `, with a minimum of ${min}`;
+ else if (max !== undefined) answer += `, with a maximum of ${max}`;
+ }
+ return `${answer}.`;
+}
+
+/** Up to five Q&A pairs for a model, or none when it has no documented parameters. */
+export function modelFaq(model: Model): ModelFaq[] {
+ if (model.params.length === 0) return [];
+ const subject = subjectOf(model);
+ const paths = model.params.map((param) => param.path);
+ const faqs: ModelFaq[] = [
+ {
+ question: `How many parameters does ${subject} accept?`,
+ answer: `${subject} accepts ${model.params.length} API parameter${
+ model.params.length === 1 ? "" : "s"
+ }: ${paths.slice(0, 6).join(", ")}${paths.length > 6 ? ", and more" : ""}.`,
+ },
+ ];
+ const byPath = new Map(model.params.map((param) => [param.path, param]));
+ for (const path of FAQ_PARAMS) {
+ if (faqs.length >= MAX_FAQS) break;
+ const param = byPath.get(path);
+ if (param && param.default !== undefined) {
+ faqs.push({
+ question: `What is the default ${path} for ${subject}?`,
+ answer: defaultAnswer(param, subject),
+ });
+ }
+ }
+ return faqs;
+}
diff --git a/src/views/index.ejs b/src/views/index.ejs
index 1669812..0658985 100644
--- a/src/views/index.ejs
+++ b/src/views/index.ejs
@@ -268,6 +268,29 @@
+<% if (capabilities.length > 0) { %>
+
+
+
+ Open any parameter to see its default, range, and every model that accepts it.
+
+
+
+<% } %>
+