Skip to content
Open
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
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,18 @@ The website code lives under `src/`:
- `src/build/` — SSG pipeline (renders pages, compiles assets, emits JSON API, generates a social card per page).
- `src/server/` — Express dev server.

Everything the catalog ships beyond the static site:

- `api/` — Vercel Functions, currently `POST /api/v1/validate`. These serve paths the
static build doesn't emit; the rest of `/api/v1/*` stays static JSON from `dist/`.

Conventions:

- TypeScript, ES modules, strict mode.
- No file over 300 lines, no function over 50 lines.
- Format with Prettier, lint with ESLint. `npm run format` and `npm run lint` will set you straight.
- Tests live under `tests/` and run with Vitest.
- `npm run typecheck` covers the site and `api/` (via `tsconfig.api.json`).

## Pull requests

Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,31 @@ curl https://modelparams.dev/api/v1/params/gpt-5.5.json

Schema at `https://modelparams.dev/api/v1/schema.json`, per the [Model Parameters convention](docs/model-parameters-schema.md).

### Validate a request

POST the parameters you're about to send. You get back what's wrong — including combinations the provider rejects — and a corrected payload.

```bash
curl -s https://modelparams.dev/api/v1/validate \
-H 'Content-Type: application/json' \
-d '{"model":"claude-3-opus-20240229","params":{"temperature":0.5,"top_p":0.9}}'
```

```json
{
"valid": false,
"issues": [
{
"path": "top_p",
"code": "not_applicable",
"message": "top_p does not apply when temperature ≠ 1",
"conflictsWith": ["temperature"]
}
],
"safeParams": { "temperature": 0.5 }
}
```

## Adding a model

Drop a YAML file in `models/<provider>/`, open a PR, and CI validates it against the schema. Details in [CONTRIBUTING.md](CONTRIBUTING.md). Can't open a PR? [File an issue](https://github.com/mnfst/modelparams.dev/issues/new/choose) with a link to the docs.
Expand Down
162 changes: 162 additions & 0 deletions api/v1/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// POST /api/v1/validate — check a params object against a model's catalog entry.
//
// Stateless: the catalog is compiled into the bundle at build time from the same
// generated data the npm package ships, so there is no filesystem read, no
// network call, and nothing to keep in sync at runtime.
import {
dropUnsupported,
getModel,
resolveModelId,
type DroppedParam,
} from "../../packages/modelparams/src/index.js";

const MAX_BODY_BYTES = 64 * 1024;

const CORS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Max-Age": "86400",
} as const;

function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body, null, 2) + "\n", {
status,
headers: {
"Content-Type": "application/json; charset=utf-8",
// A validation result is a pure function of the request body, but bodies
// vary per caller — caching it at the edge would serve one caller's
// verdict to another.
"Cache-Control": "no-store",
...CORS,
},
});
}

function fail(error: string, detail: Record<string, unknown>, status: number): Response {
return json({ error, ...detail }, status);
}

const USAGE = {
endpoint: "POST /api/v1/validate",
description:
"Validate a params object against a model. Reports unknown parameters, out-of-range values, " +
"and combinations the provider rejects, and returns a corrected payload.",
request: {
model: "provider/model id, or a bare model slug when it is unambiguous",
params: "object of provider-native parameter paths to values",
},
example: {
model: "anthropic/claude-3-opus-20240229",
params: { temperature: 0.5, top_p: 0.9, max_tokens: 1024 },
},
docs: "https://modelparams.dev/api",
} as const;

function toIssue(dropped: DroppedParam) {
return {
path: dropped.path,
code: dropped.code,
message: dropped.reason,
...(dropped.conflictsWith ? { conflictsWith: dropped.conflictsWith } : {}),
};
}

async function readBody(request: Request): Promise<Record<string, unknown>> {
const raw = await request.text();
if (raw.length > MAX_BODY_BYTES) {
throw new RangeError(`request body exceeds ${MAX_BODY_BYTES} bytes`);
}
if (raw.trim() === "") throw new SyntaxError("request body is empty");
const parsed: unknown = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new SyntaxError("request body must be a JSON object");
}
return parsed as Record<string, unknown>;
}

export default {
async fetch(request: Request): Promise<Response> {
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: CORS });
}

// A GET here is almost always someone exploring the API by hand or an agent
// probing the URL, so answer with the contract instead of a bare 405.
if (request.method === "GET") return json(USAGE);

if (request.method !== "POST") {
return fail("method_not_allowed", { allowed: ["POST", "GET", "OPTIONS"], usage: USAGE }, 405);
}

let body: Record<string, unknown>;
try {
body = await readBody(request);
} catch (err) {
const status = err instanceof RangeError ? 413 : 400;
return fail(
"invalid_request_body",
{ message: (err as Error).message, usage: USAGE },
status,
);
}

const { model, params } = body;
if (typeof model !== "string" || model.trim() === "") {
return fail(
"invalid_request_body",
{ message: "`model` must be a non-empty string", usage: USAGE },
400,
);
}

const resolved = resolveModelId(model);
if (!resolved.ok) {
return resolved.reason === "ambiguous"
? fail(
"ambiguous_model",
{
message: `"${model}" is published by more than one provider; qualify it with a provider prefix`,
matches: resolved.matches,
},
400,
)
: fail(
"unknown_model",
{
message: `"${model}" is not in the catalog`,
suggestions: resolved.suggestions,
catalog: "https://modelparams.dev/api/v1/models.json",
},
404,
);
}

if (
params !== undefined &&
(typeof params !== "object" || params === null || Array.isArray(params))
) {
return fail(
"invalid_request_body",
{ message: "`params` must be an object", usage: USAGE },
400,
);
}

const supplied = (params ?? {}) as Record<string, unknown>;
const { params: safeParams, dropped } = dropUnsupported(resolved.id, supplied);
const entry = getModel(resolved.id);

return json({
model: resolved.id,
provider: entry.provider,
authType: entry.authType,
valid: dropped.length === 0,
issues: dropped.map(toIssue),
// Always a payload that would pass validation — callers that just want to
// make the call work can spread this and move on.
safeParams,
docs: `https://modelparams.dev/models/${resolved.id}`,
});
},
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"dev": "tsx watch src/server/dev.ts",
"validate": "tsx src/data/validate.ts",
"guard:params": "tsx src/data/check-removals.ts",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit && tsc -p tsconfig.api.json",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"test": "vitest run",
Expand Down
1 change: 1 addition & 0 deletions src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ async function writeApiIndex(modelCount: number): Promise<void> {
modelByIdSubscription: "/api/v1/models/{provider}/{model}-subscription.json",
paramsByModelApiKey: "/api/v1/params/{model}.json",
paramsByModelSubscription: "/api/v1/params/{model}-subscription.json",
validate: "POST /api/v1/validate",
},
modelCount,
docs: "https://github.com/mnfst/modelparams.dev#api",
Expand Down
17 changes: 17 additions & 0 deletions src/data/llms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ function guideApi(siteUrl: string): string[] {
`curl ${siteUrl}/api/v1/models/anthropic/claude-opus-4-7-subscription.json`,
"```",
"",
"## Validate a request before you send it",
"",
"POST a model and a params object; the response reports unknown parameters, values",
"outside their range, and combinations the provider rejects — plus a corrected payload",
"under `safeParams` that is always safe to send as-is:",
"",
"```bash",
`curl -s ${siteUrl}/api/v1/validate \\`,
` -H 'Content-Type: application/json' \\`,
` -d '{"model":"claude-3-opus-20240229","params":{"temperature":0.5,"top_p":0.9}}'`,
"```",
"",
"Each issue carries a `code`: `unknown_parameter` (no such knob on this model),",
"`invalid_value` (out of range or not in the enum), or `not_applicable` (the knob",
"exists but conflicts with another value in the same request, listed in",
"`conflictsWith`).",
"",
"## JSON Schema",
"",
"Every entry validates against a JSON Schema you can use in your editor or pipeline:",
Expand Down
39 changes: 39 additions & 0 deletions src/views/api.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,45 @@
</div>
</section>

<!-- Validate -->
<section>
<h2 class="text-slate-900 dark:text-slate-100">Validate a request</h2>
<p class="mt-2 text-slate-600 dark:text-slate-300">
POST a model and the parameters you intend to send. The response reports unknown parameters, values outside
their range, and combinations the provider rejects — plus a corrected payload you can send as-is.
</p>
<div class="relative mt-4">
<div class="absolute right-3 top-3">
<span class="rounded bg-sky-100 px-2 py-0.5 text-[10px] font-semibold uppercase text-sky-800 dark:bg-sky-950 dark:text-sky-400">POST</span>
</div>
<pre class="overflow-x-auto border border-slate-200 bg-slate-900 px-5 py-4 font-mono text-sm leading-relaxed text-slate-200 dark:border-[hsla(60,2%,12%,0.17)] dark:bg-[#0d0d0d]"><code>curl -s https://modelparams.dev/api/v1/validate \
-H 'Content-Type: application/json' \
-d '{"model":"claude-3-opus-20240229","params":{"temperature":0.5,"top_p":0.9}}'</code></pre>
</div>
<div class="mt-3">
<pre class="overflow-x-auto border border-slate-200 bg-slate-900 px-5 py-4 font-mono text-sm leading-relaxed text-slate-200 dark:border-[hsla(60,2%,12%,0.17)] dark:bg-[#0d0d0d]"><code>{
"model": "anthropic/claude-3-opus-20240229",
"valid": false,
"issues": [
{
"path": "top_p",
"code": "not_applicable",
"message": "top_p does not apply when temperature ≠ 1",
"conflictsWith": ["temperature"]
}
],
"safeParams": { "temperature": 0.5 }
}</code></pre>
</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
Every issue carries a
<code class="bg-warm-tag px-1.5 py-0.5 font-mono text-xs text-slate-700 dark:bg-[#262626] dark:text-slate-300">code</code>:
<code class="font-mono text-xs">unknown_parameter</code>,
<code class="font-mono text-xs">invalid_value</code>, or
<code class="font-mono text-xs">not_applicable</code> for a conflict with another value in the same request.
</p>
</section>

<!-- JSON Schema -->
<section>
<h2 class="text-slate-900 dark:text-slate-100">JSON Schema</h2>
Expand Down
Loading
Loading