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
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ You don't need to know the schema to file one. A link to the official docs is th

3. **Required top-level fields:** `provider`, `authType` (`api_key` or `subscription`), `model`, `params`.

Optional lifecycle fields are `status` (`active`, `deprecated`, or
`retired`), `replacement` (a provider-qualified model id), and `shutdownOn`
(ISO `YYYY-MM-DD`). Omit `status` when lifecycle status has not been tracked.

4. **Parameter shape:** each item in `params` has:
- `path` (required): exact provider API request parameter path; supports dot notation for nested fields (`thinking.type`, `generationConfig.topK`).
- `type` (required): one of `boolean`, `enum`, `integer`, `number`, `string`.
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ curl -s https://modelparams.dev/api/v1/validate \
}
```

### Model lifecycle

Full model records also expose lifecycle metadata:

```yaml
status: deprecated
replacement: openai/gpt-5.6-sol
shutdownOn: 2026-10-23
```

`status` is `active`, `deprecated`, or `retired`. When lifecycle status has not
been tracked, the field stays absent from YAML, the API, and package data.
`replacement` and `shutdownOn` stay absent until the provider publishes them.

## Agents

Give a coding agent the catalog, so it looks parameters up instead of recalling them.
Expand All @@ -111,7 +125,7 @@ claude mcp add --transport http modelparams https://modelparams.dev/mcp
codex mcp add modelparams --url https://modelparams.dev/mcp
```

Four tools: `validate_model_params` to check a params object before you send it, `get_model_params` for one model's full surface, `list_models` and `find_models_supporting` to search the catalog.
Four tools: `validate_model_params` to check a params object before you send it, `get_model_params` for one model's parameter surface and lifecycle metadata, `list_models` and `find_models_supporting` to search the catalog.

**Without MCP** — `npx skills add mnfst/modelparams.dev` installs the companion agent skill, and [llms.txt](https://modelparams.dev/llms.txt) points an agent at a URL.

Expand Down
21 changes: 21 additions & 0 deletions docs/model-parameters-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ parameters.
"provider": "anthropic",
"authType": "api_key",
"model": "claude-haiku-4-5",
"status": "active",
"params": [
{
"path": "top_p",
Expand Down Expand Up @@ -66,6 +67,12 @@ Conventions:
go stale the next time the host adds a region. Availability per region is
account state and is deliberately not recorded here.
- `authType` is `api_key` or `subscription`.
- `status` is `active`, `deprecated`, or `retired`. Omit it when lifecycle
status has not been tracked; generated catalog data preserves the omission.
- `replacement` is an optional provider-qualified model id, such as
`anthropic/claude-opus-4-8`.
- `shutdownOn` is an optional provider-published shutdown date in ISO
`YYYY-MM-DD` format.
- `params` is the non-empty list of parameters for that exact route.
- `path` is the exact provider API request parameter path in dot notation. Use
the provider's documented field casing, such as `top_p`,
Expand All @@ -81,6 +88,20 @@ Conventions:
- `group` is a semantic grouping for ordering and display.
- `applicability` is optional. Omitted means always available.

## Lifecycle Metadata

Lifecycle fields sit beside `params`; they are not request parameters.

```yaml
status: deprecated
replacement: anthropic/claude-opus-4-8
shutdownOn: 2026-10-01
```

Omit `replacement` or `shutdownOn` when the provider has not published one.
Use the provider-qualified catalog id for `replacement` so consumers can look
up the target directly.

## Parameter Scope

MPS entries should describe only parameters the user or consumer can configure
Expand Down
2 changes: 1 addition & 1 deletion packages/modelparams-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Issue codes: `unknown_parameter` (no such knob on this model), `invalid_value` (

### `get_model_params`

Every parameter for one model — type, range, enum values, default, and the conditional rules that gate it.
Every parameter for one model — type, range, enum values, default, and the conditional rules that gate it. The top-level response also includes `status`, `replacement`, and `shutdownOn` when that lifecycle metadata is tracked; these fields are not request parameters.

### `list_models`

Expand Down
5 changes: 3 additions & 2 deletions packages/modelparams-mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,9 @@ export function createServer(version = "0.0.0"): McpServer {
title: "Get model parameters",
description:
"List every parameter a model accepts, with its type, allowed range or enum values, " +
"default, and any conditional rules governing when it applies. Use before writing " +
"code that calls a model, or when building a model settings UI.",
"default, and any conditional rules governing when it applies. Also returns lifecycle " +
"status and migration guidance when tracked. Use before writing code that calls a " +
"model, or when building a model settings UI.",
inputSchema: {
model: z
.string()
Expand Down
3 changes: 3 additions & 0 deletions packages/modelparams-mcp/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export function getModelParams(input: { model: string; baseUrl?: string }): Tool
provider: entry.provider,
authType: entry.authType,
...("wireId" in entry ? { wireId: entry.wireId } : {}),
...(entry.status !== undefined ? { status: entry.status } : {}),
...(entry.replacement !== undefined ? { replacement: entry.replacement } : {}),
...(entry.shutdownOn !== undefined ? { shutdownOn: entry.shutdownOn } : {}),
parameterCount: params.length,
params: params.map(describeParam),
defaults: getDefaults(resolved.id),
Expand Down
35 changes: 35 additions & 0 deletions packages/modelparams-mcp/tests/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { getModel } from "modelparams";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createServer } from "../src/server.js";

Expand All @@ -25,6 +26,9 @@ interface ValidateResult {

interface ModelParamsResult {
model: string;
status?: "active" | "deprecated" | "retired";
replacement?: string;
shutdownOn?: string;
parameterCount: number;
params: ParamInfo[];
}
Expand Down Expand Up @@ -169,6 +173,37 @@ describe("get_model_params", () => {
const display = out.params.find((p) => p.path === "thinking.display");
expect(display!.appliesOnlyWhen).toBeDefined();
});

it("returns tracked lifecycle metadata at the top level", async () => {
const entry = getModel("openai/gpt-5.5") as unknown as Record<string, unknown>;
const previous = {
status: entry.status,
replacement: entry.replacement,
shutdownOn: entry.shutdownOn,
};

Object.assign(entry, {
status: "deprecated",
replacement: "openai/gpt-5.6-sol",
shutdownOn: "2026-10-23",
});

try {
const out = await call<ModelParamsResult>("get_model_params", {
model: "openai/gpt-5.5",
});
expect(out).toMatchObject({
status: "deprecated",
replacement: "openai/gpt-5.6-sol",
shutdownOn: "2026-10-23",
});
} finally {
for (const [key, value] of Object.entries(previous)) {
if (value === undefined) delete entry[key];
else entry[key] = value;
}
}
});
});

describe("list_models", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/modelparams-python/scripts/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ async function main(): Promise<void> {
providerModels.push(model);
byProvider.set(model.provider, providerModels);
}

const typeFiles = new Set<string>(["__init__.py"]);
for (const [provider, providerModels] of byProvider) {
const filename = `${moduleName(provider)}.py`;
Expand Down
2 changes: 2 additions & 0 deletions packages/modelparams-python/src/modelparams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ApplicabilityCondition,
CatalogEntry,
JsonPrimitive,
LifecycleStatus,
Parameter,
ParamGroup,
ParamRange,
Expand Down Expand Up @@ -43,6 +44,7 @@ def _package_version() -> str:
"ApplicabilityCondition",
"CatalogEntry",
"JsonPrimitive",
"LifecycleStatus",
"ModelId",
"Parameter",
"ParamGroup",
Expand Down
4 changes: 4 additions & 0 deletions packages/modelparams-python/src/modelparams/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
JsonPrimitive: TypeAlias = str | int | float | bool | None
JsonNumber: TypeAlias = int | float
AuthType: TypeAlias = Literal["api_key", "subscription"]
LifecycleStatus: TypeAlias = Literal["active", "deprecated", "retired"]
ParamType: TypeAlias = Literal["boolean", "enum", "integer", "number", "string"]
ParamGroup: TypeAlias = Literal[
"generation_length",
Expand Down Expand Up @@ -62,4 +63,7 @@ class CatalogEntry(FrozenModel):
# Exact wire string when the host's native id differs from the catalog
# slug (pathed ids: accounts/fireworks/models/kimi-k3, openai/gpt-oss-20b).
wire_id: str | None = Field(alias="wireId", default=None)
status: LifecycleStatus | None = None
replacement: str | None = None
shutdown_on: str | None = Field(alias="shutdownOn", default=None)
params: tuple[Parameter, ...]
1 change: 1 addition & 0 deletions packages/modelparams-python/tests/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def test_get_model_returns_frozen_pythonic_metadata() -> None:
assert model.provider == "anthropic"
assert model.auth_type == "api_key"
assert model.model == "claude-haiku-4-5-20251001"
assert model.status is None
assert model.params
with pytest.raises(ValidationError):
model.model = "changed"
Expand Down
15 changes: 12 additions & 3 deletions packages/modelparams/scripts/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ async function main(): Promise<void> {

const ids = models.map(modelId);
const providers = [...new Set(models.map((m) => m.provider))].sort();

// 1. model-ids.ts — ModelId union + Provider union
await fs.writeFile(
path.join(OUT_DIR, "model-ids.ts"),
Expand Down Expand Up @@ -103,8 +102,18 @@ async function main(): Promise<void> {
path.join(OUT_DIR, "data.ts"),
HEADER +
`import type { ModelId } from "./model-ids.js";\n\n` +
`export const CATALOG = ${JSON.stringify(models, null, 2)} as const;\n\n` +
`export type CatalogEntry = (typeof CATALOG)[number];\n\n` +
`const GENERATED_CATALOG = ${JSON.stringify(models, null, 2)} as const;\n\n` +
`type GeneratedCatalogEntry = (typeof GENERATED_CATALOG)[number];\n` +
`export type LifecycleStatus = "active" | "deprecated" | "retired";\n` +
`type WithLifecycle<T> = T extends unknown\n` +
` ? Omit<T, "status" | "replacement" | "shutdownOn"> & {\n` +
` readonly status?: LifecycleStatus;\n` +
` readonly replacement?: string;\n` +
` readonly shutdownOn?: string;\n` +
` }\n` +
` : never;\n` +
`export type CatalogEntry = WithLifecycle<GeneratedCatalogEntry>;\n\n` +
`export const CATALOG: readonly CatalogEntry[] = GENERATED_CATALOG;\n\n` +
`function authSuffix(authType: CatalogEntry["authType"]): "" | "-subscription" {\n` +
` return authType === "api_key" ? "" : "-subscription";\n` +
`}\n\n` +
Expand Down
15 changes: 13 additions & 2 deletions packages/modelparams/src/generated/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import type { ModelId } from "./model-ids.js";

export const CATALOG = [
const GENERATED_CATALOG = [
{
"provider": "alibaba",
"authType": "api_key",
Expand Down Expand Up @@ -29408,7 +29408,18 @@ export const CATALOG = [
}
] as const;

export type CatalogEntry = (typeof CATALOG)[number];
type GeneratedCatalogEntry = (typeof GENERATED_CATALOG)[number];
export type LifecycleStatus = "active" | "deprecated" | "retired";
type WithLifecycle<T> = T extends unknown
? Omit<T, "status" | "replacement" | "shutdownOn"> & {
readonly status?: LifecycleStatus;
readonly replacement?: string;
readonly shutdownOn?: string;
}
: never;
export type CatalogEntry = WithLifecycle<GeneratedCatalogEntry>;

export const CATALOG: readonly CatalogEntry[] = GENERATED_CATALOG;

function authSuffix(authType: CatalogEntry["authType"]): "" | "-subscription" {
return authType === "api_key" ? "" : "-subscription";
Expand Down
2 changes: 1 addition & 1 deletion packages/modelparams/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type {
export type { ApplicabilityIssue, DropCode, DroppedParam, DropResult } from "./applicability.js";
export type { ModelId, Provider } from "./generated/model-ids.js";
export type { ParamsById } from "./generated/params-by-id.js";
export type { CatalogEntry } from "./generated/data.js";
export type { CatalogEntry, LifecycleStatus } from "./generated/data.js";
export type { ParamIssue, ParseParamsResult } from "./parse.js";
export type {
StandardSchemaV1,
Expand Down
2 changes: 2 additions & 0 deletions packages/modelparams/test-d/types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expectAssignable, expectError, expectType } from "tsd";
import { getModel, parseParams, paramsSchema } from "../dist/index.js";
import type {
JsonPrimitive,
LifecycleStatus,
Param,
ParamsOf,
ParseParamsResult,
Expand Down Expand Up @@ -34,6 +35,7 @@ expectType<Haiku>(empty);

// The precise catalog params assign to the loose `Param` type with no cast.
expectAssignable<readonly Param[]>(getModel("openai/gpt-4.1").params);
expectType<LifecycleStatus | undefined>(getModel("openai/gpt-4.1").status);

// parseParams returns the discriminated result and rejects unknown model ids.
expectType<ParseParamsResult>(parseParams("openai/gpt-4.1", {}));
Expand Down
1 change: 1 addition & 0 deletions packages/modelparams/tests/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe("getModel", () => {
expect(m.provider).toBe("anthropic");
expect(m.authType).toBe("api_key");
expect(m.model).toBe("claude-haiku-4-5-20251001");
expect(m.status).toBeUndefined();
expect(m.params.length).toBeGreaterThan(0);
});
});
Expand Down
11 changes: 11 additions & 0 deletions src/build/render-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ export function modelIntro(model: Model): string {
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 function modelLifecycleSummary(model: Model): string | null {
const status = model.status;
const parts: string[] = [];
if (status === "deprecated") parts.push("This model is deprecated.");
if (status === "retired") parts.push("This model is retired.");
if (model.shutdownOn) parts.push(`Shutdown date: ${model.shutdownOn}.`);
if (model.replacement) parts.push(`Suggested replacement: ${model.replacement}.`);
return parts.length > 0 ? parts.join(" ") : null;
}

export async function renderModelPage(model: Model, allModels: Model[]): Promise<string> {
const siblings = allModels
.filter((other) => other.provider === model.provider && modelId(other) !== modelId(model))
Expand All @@ -117,6 +127,7 @@ export async function renderModelPage(model: Model, allModels: Model[]): Promise
siblings,
faqs,
intro: modelIntro(model),
lifecycleSummary: modelLifecycleSummary(model),
providerName: providerLabel(model.provider),
modelName: modelLabel(model),
fullName: modelFullLabel(model),
Expand Down
7 changes: 7 additions & 0 deletions src/client/webmcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// server-side modules (and no zod) into the client bundle.

type AuthType = "api_key" | "subscription";
type LifecycleStatus = "active" | "deprecated" | "retired";

interface CatalogParam {
path: string;
Expand All @@ -18,6 +19,9 @@ interface CatalogModel {
provider: string;
authType: AuthType;
model: string;
status?: LifecycleStatus;
replacement?: string;
shutdownOn?: string;
params: CatalogParam[];
}

Expand Down Expand Up @@ -144,6 +148,9 @@ export function searchCatalog(catalog: Catalog, params: Record<string, unknown>)
provider: model.provider,
model: model.model,
authType: model.authType,
status: model.status,
replacement: model.replacement,
shutdownOn: model.shutdownOn,
parameterCount: model.params.length,
parameters: model.params.map((p) => p.path),
})),
Expand Down
Loading
Loading