Skip to content
Closed
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
20 changes: 20 additions & 0 deletions .changeset/provider-model-catalogs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@tanstack/ai': minor
'@tanstack/ai-anthropic': minor
'@tanstack/ai-bedrock': minor
'@tanstack/ai-cloudflare': minor
'@tanstack/ai-gemini': minor
'@tanstack/ai-grok': minor
'@tanstack/ai-groq': minor
'@tanstack/ai-mistral': minor
'@tanstack/ai-openai': minor
'@tanstack/ai-openrouter': minor
'@tanstack/ai-vercel-gateway': minor
'@tanstack/ai-vertex': minor
---

Each provider package exports its model catalog at `./models`, for example `import { models } from '@tanstack/ai-anthropic/models'`. A record has the model's `id`, `name`, `input` modalities, `contextWindow`, `maxOutputTokens`, `prices`, and `reasoning` levels. `@tanstack/ai-openai` also exports `./azure-models`, and `@tanstack/ai-cloudflare` also exports `./gateway-models`.

`@tanstack/ai` adds the `ModelRecord` type and `modelCost(model, usage)`. `modelCost` prices one call from its `TokenUsage`, with long-context tiers and 1-hour cache writes. It returns `undefined` when the call used tokens that have no known price.

The data comes from modelschemas, and a daily job syncs it. A value that modelschemas does not list is `undefined`, never `0`.
14 changes: 12 additions & 2 deletions .github/workflows/sync-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ jobs:
env:
MODELSCHEMAS_API_KEY: ${{ secrets.MODELSCHEMAS_API_KEY }}

- name: Sync model catalogs
id: catalogs
continue-on-error: true
run: pnpm generate:models:catalogs
env:
MODELSCHEMAS_API_KEY: ${{ secrets.MODELSCHEMAS_API_KEY }}

- name: Format
run: pnpm format

Expand Down Expand Up @@ -101,6 +108,7 @@ jobs:
- Converts to the internal adapter format
- Inserts new native-provider models from modelschemas (`@modelschemas/client`)
- Inserts new ElevenLabs text-to-speech, music, sound-effect, and voice-design models from modelschemas
- Writes each provider package's `./models` catalog (prices, limits, reasoning levels) from modelschemas
- Creates a patch changeset for all changed packages
PRBODY
)
Expand All @@ -116,7 +124,8 @@ jobs:
steps.fetch.outcome == 'failure' ||
steps.convert.outcome == 'failure' ||
steps.elevenlabs.outcome == 'failure' ||
steps.provider.outcome == 'failure'
steps.provider.outcome == 'failure' ||
steps.catalogs.outcome == 'failure'
run: |
echo "::error::A model sync step failed. Passing steps were still committed."
exit 1
Expand All @@ -130,7 +139,8 @@ jobs:
fetch=${{ steps.fetch.outcome }},
convert=${{ steps.convert.outcome }},
elevenlabs=${{ steps.elevenlabs.outcome }},
provider=${{ steps.provider.outcome }}
provider=${{ steps.provider.outcome }},
catalogs=${{ steps.catalogs.outcome }}
shell: bash
run: |
gh label create sync-models --color B60205 --description "Daily model sync failures" --force
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ For deeper architecture details (adapter system, isomorphic tools, framework int
2. Regenerates `packages/ai-openrouter/src/model-meta.ts` and the Vercel Gateway and Neon model lists.
3. Inserts **new** native-provider models from [modelschemas](https://modelschemas.com) (`@modelschemas/client`) into `ai-openai`, `ai-anthropic`, `ai-gemini`, `ai-grok`, `ai-groq`, `ai-mistral`, and `ai-byteplus`. Native ids, activity, limits, modalities, pricing, and capabilities come from each provider catalog. OpenRouter fills a field only when the native row left it empty. OpenAI, Anthropic, Gemini, and Grok hold an id back until the OpenRouter catalog has a matching row and both prices are known. The log names each held-back id. Groq and Mistral insert even when a price is unknown: the generator leaves that price out (`pricing: {}` when both are unknown) and never writes `0`. BytePlus does not write pricing.
4. Inserts **new** ElevenLabs text-to-speech, music, sound-effect, and voice-design ids from [modelschemas](https://modelschemas.com/v1/schemas/elevenlabs) into `packages/ai-elevenlabs/src/model-meta.ts` (`scripts/sync-elevenlabs-models.ts`, also through `@modelschemas/client`). Text-to-speech ids are catalog rows with `providerMetadata.canDoTextToSpeech`. Music, sound-effect, and voice-design ids are the `model_id` enums of their request schemas. Transcription ids stay hand-maintained.
5. Writes a patch changeset for the packages that changed.
5. Rewrites each provider package's `./models` catalog (`src/models.ts`) from modelschemas, with `pnpm generate:models:catalogs`. The provider list is `CATALOGS` in `scripts/model-sync/model-catalog.ts`. A value modelschemas does not list stays `undefined`, and a value the script does not know stops the sync. Fix a wrong value upstream in modelschemas, not in the generated file.
6. Writes a patch changeset for the packages that changed.

The sync stops with an error when modelschemas is down, when a catalog is empty, when no row has a `rawId`, `firstSeenAt`, or `deprecatedAt` key, or when an array or type map it must update is missing from a `model-meta.ts`.

Expand Down
115 changes: 115 additions & 0 deletions docs/advanced/model-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Model Catalog
id: model-catalog
description: "List a provider's models with their context windows, prices, and reasoning levels, and price each run with modelCost."
keywords:
- tanstack ai
- model catalog
- model prices
- context window
- reasoning levels
- modelCost
---

Import `models` from the `./models` subpath of a provider package. Each record gives the model id, the context window, the prices, and the reasoning levels.

```typescript
import { models } from "@tanstack/ai-anthropic/models";

const model = models.find((m) => m.id === "claude-sonnet-5-5");
console.log(model?.contextWindow, model?.prices?.input);
```

## Packages with a catalog

| Provider | Import from |
| --- | --- |
| Anthropic | `@tanstack/ai-anthropic/models` |
| OpenAI | `@tanstack/ai-openai/models` |
| Azure OpenAI | `@tanstack/ai-openai/azure-models` |
| Gemini | `@tanstack/ai-gemini/models` |
| Vertex AI | `@tanstack/ai-vertex/models` |
| Amazon Bedrock | `@tanstack/ai-bedrock/models` |
| Groq | `@tanstack/ai-groq/models` |
| Mistral | `@tanstack/ai-mistral/models` |
| xAI | `@tanstack/ai-grok/models` |
| OpenRouter | `@tanstack/ai-openrouter/models` |
| Vercel AI Gateway | `@tanstack/ai-vercel-gateway/models` |
| Cloudflare Workers AI | `@tanstack/ai-cloudflare/models` |
| Cloudflare AI Gateway | `@tanstack/ai-cloudflare/gateway-models` |

The data comes from [modelschemas](https://modelschemas.com). A daily job syncs it, and a patch release ships each change.

## Fill a model picker

The catalog is plain data, so you can import it in the browser. `supportedReasoningLevels` gives the levels that a model takes:

```tsx
import { useState } from "react";
import { supportedReasoningLevels } from "@tanstack/ai";
import { models } from "@tanstack/ai-openai/models";

export function ModelPicker() {
const [id, setId] = useState("gpt-6.1-sol");
const model = models.find((m) => m.id === id);

return (
<>
<select value={id} onChange={(event) => setId(event.target.value)}>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
<select>
{supportedReasoningLevels(model?.reasoning).map((level) => (
<option key={level}>{level}</option>
))}
</select>
</>
);
}
```

To pick a level that the model does not have, use `clampReasoningLevel`. It returns the nearest level that the model supports. See [Reasoning](../chat/reasoning).

## Price a run

`modelCost` gives the cost of one model call from its token usage. Call it in the [`onUsage`](./middleware-advanced#onusage) hook of a middleware:

```typescript
import { chat, modelCost } from "@tanstack/ai";
import type { ChatMiddleware } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
import { models } from "@tanstack/ai-anthropic/models";

const costLog: ChatMiddleware = {
name: "cost-log",
onUsage: (ctx, usage) => {
const model = models.find((m) => m.id === ctx.model);
const cost = model ? modelCost(model, usage) : undefined;
console.log(cost ? `${cost.total} ${cost.currency}` : "Price not known");
},
};

const stream = chat({
adapter: anthropicText("claude-sonnet-5-5"),
messages: [{ role: "user", content: "Hello!" }],
threadId: "thread-1",
middleware: [costLog],
});
```

`modelCost` uses the long-context prices when the input is more than a tier's `inputTokensAbove`. It prices cache reads, cache writes, and 1-hour cache writes at their own rates.

## Values that are not known

The catalog does not guess. When the source does not list a value, the record does not have it:

- `prices`, `contextWindow`, `maxOutputTokens`, `input`: `undefined`.
- One rate in `prices`, for example `cacheWrite`: `undefined`. That rate is not known, and it is not free.
- `modelCost`: `undefined` when the call used tokens that have no known price.
- `reasoning.levels`: empty when the source lists no levels. A model with `budget: true` takes `budgetTokens`. See [Reasoning](../chat/reasoning).

Prices are in `prices.currency`, for example `USD` or `CNY`.
2 changes: 2 additions & 0 deletions docs/advanced/prompt-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const stream = chat({

From the second call of a conversation, most of the input comes from the cache.

To turn this usage into a price, use `modelCost`. See [Model Catalog](./model-catalog#price-a-run).

## How it works

- The cache key is the first of these that you set:
Expand Down
7 changes: 6 additions & 1 deletion docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1650,7 +1650,12 @@
"label": "Prompt Caching",
"to": "advanced/prompt-caching",
"addedAt": "2026-10-09",
"updatedAt": "2026-10-22"
"updatedAt": "2026-10-23"
},
{
"label": "Model Catalog",
"to": "advanced/model-catalog",
"addedAt": "2026-10-23"
},
{
"label": "Mid-Conversation Changes",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"format": "oxfmt .",
"generate-docs": "node scripts/generate-docs.ts && pnpm run copy:readme",
"generate:fal-image-fields": "tsx scripts/generate-fal-image-field-map.ts",
"generate:models": "pnpm generate:models:fetch && pnpm regenerate:models && tsx scripts/sync-elevenlabs-models.ts && tsx scripts/sync-provider-models.ts && pnpm format",
"generate:models": "pnpm generate:models:fetch && pnpm regenerate:models && tsx scripts/sync-elevenlabs-models.ts && tsx scripts/sync-provider-models.ts && pnpm generate:models:catalogs && pnpm format",
"generate:models:catalogs": "node scripts/sync-model-catalogs.ts",
"generate:models:elevenlabs": "tsx scripts/sync-elevenlabs-models.ts",
"generate:models:fetch": "tsx scripts/fetch-openrouter-models.ts && tsx scripts/fetch-vercel-gateway-models.ts && tsx scripts/fetch-lovable-gateway-models.ts && node scripts/fetch-llmgateway-models.ts && tsx scripts/fetch-neon-models.ts",
"regenerate:models": "tsx scripts/convert-openrouter-models.ts && tsx scripts/convert-vercel-gateway-models.ts && node scripts/convert-llmgateway-models.ts && tsx scripts/convert-neon-models.ts",
Expand All @@ -72,7 +73,7 @@
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.30.0",
"@faker-js/faker": "^10.1.0",
"@modelschemas/client": "^0.1.0",
"@modelschemas/client": "^0.2.1",
"@stylistic/eslint-plugin": "^5.10.0",
"@tanstack/ai": "workspace:*",
"@tanstack/ai-grok-build": "workspace:*",
Expand Down
4 changes: 4 additions & 0 deletions packages/ai-anthropic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"./vertex": {
"types": "./dist/esm/vertex/index.d.ts",
"import": "./dist/esm/vertex/index.js"
},
"./models": {
"types": "./dist/esm/models.d.ts",
"import": "./dist/esm/models.js"
}
},
"files": [
Expand Down
Loading
Loading