Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions scripts/gen-images.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Rasterizes the brand SVGs into the PNG assets that social platforms and iOS
// require (they don't reliably accept SVG). Run manually after editing
// `og.svg` or `favicon.svg` — `sharp` is intentionally NOT a project
// dependency, so pull it in ad hoc:
//
// npx -y -p sharp node scripts/gen-images.mjs
//
// Outputs are committed under src/client/ and copied into dist/assets at build.
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import sharp from "sharp";

const here = path.dirname(fileURLToPath(import.meta.url));
const clientDir = path.join(here, "..", "src", "client");

async function rasterize(srcSvg, outPng, width, height, density) {
const svg = await readFile(path.join(clientDir, srcSvg));
await sharp(svg, { density })
.resize(width, height, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toFile(path.join(clientDir, outPng));
console.log(`wrote src/client/${outPng} (${width}x${height})`);
}

await rasterize("og.svg", "og.png", 1200, 630, 144);
await rasterize("favicon.svg", "apple-touch-icon.png", 180, 180, 600);
7 changes: 4 additions & 3 deletions src/build/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export async function compileStyles(): Promise<void> {
}

export async function copyStaticAssets(): Promise<void> {
await fs.copyFile(
path.join(CLIENT_DIR, "favicon.svg"),
path.join(DIST_ASSETS_DIR, "favicon.svg"),
await Promise.all(
["favicon.svg", "og.png", "apple-touch-icon.png"].map((name) =>
fs.copyFile(path.join(CLIENT_DIR, name), path.join(DIST_ASSETS_DIR, name)),
),
);

const logoSrc = path.join(CLIENT_DIR, "logos");
Expand Down
58 changes: 44 additions & 14 deletions src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
buildCapabilityFacets,
buildCatalog,
buildProviderFacets,
uniqueProviders,
} from "../data/catalog.js";
import { loadAllModels } from "../data/load.js";
import { buildLlmsFullTxt, buildLlmsTxt } from "../data/llms.js";
Expand All @@ -13,12 +14,15 @@ import {
DIST_DIR,
MODELS_DIR,
} from "../data/paths.js";
import { SITE_URL } from "../data/site.js";
import { GLOSSARY_PATH, modelPagePath, providerPagePath } from "../data/urls.js";
import { modelId, type Model } from "../schema/model.js";
import { buildModelJsonSchema } from "../schema/generate.js";
import { bundleClientScript, compileStyles, copyStaticAssets } from "./assets.js";
import { renderIndex } from "./render.js";

const SITE_URL = process.env.SITE_URL ?? "https://modelparams.dev";
import { renderGlossaryPage } from "./render-glossary.js";
import { renderModelPage } from "./render-model.js";
import { renderProviderPage } from "./render-provider.js";

async function cleanDist(): Promise<void> {
await fs.rm(DIST_DIR, { recursive: true, force: true });
Expand All @@ -40,22 +44,26 @@ async function writeLlmsFiles(models: Model[]): Promise<void> {
);
}

async function writeRobotsAndSitemap(): Promise<void> {
async function writeRobotsAndSitemap(models: Model[]): Promise<void> {
const robots = `# AI agents welcome. Machine-readable overview: ${SITE_URL}/llms.txt\nUser-agent: *\nAllow: /\nSitemap: ${SITE_URL}/sitemap.xml\n`;
await fs.writeFile(path.join(DIST_DIR, "robots.txt"), robots, "utf8");

// Sitemaps list canonical, indexable HTML pages only — the JSON API and the
// .txt agent files are intentionally excluded (they're not search results).
const today = new Date().toISOString().slice(0, 10);
const urls = [
{ loc: `${SITE_URL}/`, priority: "1.0" },
{ loc: `${SITE_URL}/llms.txt`, priority: "0.8" },
{ loc: `${SITE_URL}/llms-full.txt`, priority: "0.7" },
{ loc: `${SITE_URL}/api/v1/models.json`, priority: "0.5" },
{ loc: `${SITE_URL}/api/v1/schema.json`, priority: "0.5" },
const entries: { path: string; priority: string }[] = [
{ path: "/", priority: "1.0" },
{ path: GLOSSARY_PATH, priority: "0.7" },
...uniqueProviders(models).map((provider) => ({
path: providerPagePath(provider),
priority: "0.8",
})),
...models.map((model) => ({ path: modelPagePath(model), priority: "0.6" })),
];
const body = urls
const body = entries
.map(
({ loc, priority }) =>
` <url><loc>${loc}</loc><lastmod>${today}</lastmod><priority>${priority}</priority></url>`,
({ path: loc, priority }) =>
` <url><loc>${SITE_URL}${loc}</loc><lastmod>${today}</lastmod><priority>${priority}</priority></url>`,
)
.join("\n");
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
Expand All @@ -66,6 +74,25 @@ ${body}
await fs.writeFile(path.join(DIST_DIR, "sitemap.xml"), sitemap, "utf8");
}

async function writeHtmlPages(models: Model[]): Promise<void> {
for (const model of models) {
const [provider, slug] = modelId(model).split("/");
if (!provider || !slug) continue;
const dir = path.join(DIST_DIR, "models", provider);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path.join(dir, `${slug}.html`), await renderModelPage(model, models), "utf8");
}

await fs.mkdir(path.join(DIST_DIR, "providers"), { recursive: true });
for (const provider of uniqueProviders(models)) {
const providerModels = models.filter((m) => m.provider === provider);
const html = await renderProviderPage(provider, providerModels, models);
await fs.writeFile(path.join(DIST_DIR, "providers", `${provider}.html`), html, "utf8");
}

await fs.writeFile(path.join(DIST_DIR, "glossary.html"), await renderGlossaryPage(models), "utf8");
}

async function writeApiIndex(modelCount: number): Promise<void> {
const body = {
name: "modelparams.dev API",
Expand All @@ -77,7 +104,7 @@ async function writeApiIndex(modelCount: number): Promise<void> {
modelByIdSubscription: "/api/v1/models/{provider}/{model}-subscription.json",
},
modelCount,
docs: "https://github.com/modelparameters/modelparameters.dev#api",
docs: "https://github.com/mnfst/modelparams.dev#api",
};
await writeJson(path.join(DIST_API_DIR, "index.json"), body);
}
Expand Down Expand Up @@ -121,8 +148,11 @@ export async function build(): Promise<{ models: number }> {
console.log("Bundling client + styles...");
await Promise.all([bundleClientScript(), compileStyles(), copyStaticAssets()]);

console.log("Rendering model, provider, and glossary pages...");
await writeHtmlPages(models);

await writeLlmsFiles(models);
await writeRobotsAndSitemap();
await writeRobotsAndSitemap(models);

const elapsed = ((Date.now() - startedAt) / 1000).toFixed(2);
console.log(`Built ${models.length} models in ${elapsed}s.`);
Expand Down
40 changes: 40 additions & 0 deletions src/build/render-glossary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import path from "node:path";
import ejs from "ejs";
import { buildGlossary, type GlossaryGroup } from "../data/glossary.js";
import { VIEWS_DIR } from "../data/paths.js";
import { SITE_NAME, SITE_URL } from "../data/site.js";
import { GLOSSARY_PATH, absolute } from "../data/urls.js";
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_DESCRIPTION =
"Every LLM API parameter in the catalog, defined: what temperature, top_p, max_tokens, reasoning effort and the rest do, with their types and which models support them.";

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, and notes its type and how many models expose it. Definitions come from the same community-maintained data as the JSON API.`;
}

export async function renderGlossaryPage(allModels: Model[]): Promise<string> {
const groups = buildGlossary(allModels);

const body = await ejs.renderFile(path.join(VIEWS_DIR, "glossary.ejs"), {
groups,
intro: glossaryIntro(groups),
helpers: viewHelpers,
});

return renderShell(
{
title: GLOSSARY_TITLE,
description: GLOSSARY_DESCRIPTION,
canonicalUrl: absolute(SITE_URL, GLOSSARY_PATH),
structuredData: buildGlossaryStructuredData(groups, SITE_URL),
providerHubs: hubLinks(allModels),
},
body,
);
}
72 changes: 72 additions & 0 deletions src/build/render-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import path from "node:path";
import ejs from "ejs";
import { modelLabel, providerLabel } from "../data/display.js";
import { VIEWS_DIR } from "../data/paths.js";
import { SITE_NAME, SITE_URL } from "../data/site.js";
import { absolute, modelJsonPath, modelPagePath, providerPagePath } from "../data/urls.js";
import { modelId, type Model } from "../schema/model.js";
import { buildModelStructuredData } from "./structured-data.js";
import { hubLinks, renderShell, viewHelpers } from "./render.js";

function authNote(model: Model): string {
return model.authType === "subscription" ? " via subscription" : "";
}

export function modelPageTitle(model: Model): string {
const variant = model.authType === "subscription" ? " (subscription)" : "";
return `${providerLabel(model.provider)} ${modelLabel(model)}${variant} parameters · ${SITE_NAME}`;
}

export function modelPageDescription(model: Model): string {
const who = `${providerLabel(model.provider)} ${modelLabel(model)}${authNote(model)}`;
if (model.params.length === 0) {
return `${who}: no parameters documented yet. Browse the open catalog of LLM model parameters on ${SITE_NAME}.`;
}
const paths = model.params.map((param) => param.path);
const sample = paths.slice(0, 4).join(", ");
const more = paths.length > 4 ? ", and more" : "";
const count = `${model.params.length} API parameter${model.params.length === 1 ? "" : "s"}`;
return `All ${count} for ${who}: ${sample}${more}. See each type, default, range, and the conditions that gate it.`;
}

export function modelIntro(model: Model): string {
const who = `${providerLabel(model.provider)} ${modelLabel(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.`;
}
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.`;
}

export async function renderModelPage(model: Model, allModels: Model[]): Promise<string> {
const siblings = allModels
.filter((other) => other.provider === model.provider && modelId(other) !== modelId(model))
.sort((a, b) => modelLabel(a).localeCompare(modelLabel(b)));

const body = await ejs.renderFile(path.join(VIEWS_DIR, "model.ejs"), {
model,
helpers: viewHelpers,
siblings,
intro: modelIntro(model),
providerName: providerLabel(model.provider),
modelName: modelLabel(model),
providerPath: providerPagePath(model.provider),
jsonPath: modelJsonPath(model),
isSubscription: model.authType === "subscription",
});

const description = modelPageDescription(model);
return renderShell(
{
title: modelPageTitle(model),
description,
canonicalUrl: absolute(SITE_URL, modelPagePath(model)),
structuredData: buildModelStructuredData(model, description, SITE_URL),
providerHubs: hubLinks(allModels),
},
body,
);
}
63 changes: 63 additions & 0 deletions src/build/render-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import path from "node:path";
import ejs from "ejs";
import { modelLabel, providerLabel } from "../data/display.js";
import { VIEWS_DIR } from "../data/paths.js";
import { SITE_NAME, SITE_URL } from "../data/site.js";
import { absolute, providerPagePath } from "../data/urls.js";
import { modelId, type Model } from "../schema/model.js";
import { buildProviderStructuredData } from "./structured-data.js";
import { hubLinks, renderShell, viewHelpers } from "./render.js";

export function providerPageTitle(provider: string): string {
return `${providerLabel(provider)} model parameters · ${SITE_NAME}`;
}

export function providerPageDescription(provider: string, models: Model[]): string {
const count = `${models.length} ${providerLabel(provider)} model${models.length === 1 ? "" : "s"}`;
const sample = sampleParams(models).slice(0, 4).join(", ");
return `Parameters for ${count}${sample ? `: ${sample}` : ""}. Each model lists its types, defaults, ranges, and the conditions that gate every parameter.`;
}

function sampleParams(models: Model[]): string[] {
const seen = new Set<string>();
for (const model of models) {
for (const param of model.params) seen.add(param.path);
}
return [...seen];
}

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.`;
}

export async function renderProviderPage(
provider: string,
providerModels: Model[],
allModels: Model[],
): Promise<string> {
const models = [...providerModels].sort((a, b) => modelLabel(a).localeCompare(modelLabel(b)));
const otherProviders = hubLinks(allModels).filter(
(hub) => hub.href !== providerPagePath(provider),
);

const body = await ejs.renderFile(path.join(VIEWS_DIR, "provider.ejs"), {
providerName: providerLabel(provider),
models,
helpers: viewHelpers,
otherProviders,
intro: providerIntro(provider, models),
});

const description = providerPageDescription(provider, models);
return renderShell(
{
title: providerPageTitle(provider),
description,
canonicalUrl: absolute(SITE_URL, providerPagePath(provider)),
structuredData: buildProviderStructuredData(provider, models, description, SITE_URL),
providerHubs: hubLinks(allModels),
},
body,
);
}
Loading
Loading