Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .changeset/model-meta-no-prices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/ai-anthropic': patch
'@tanstack/ai-gemini': patch
'@tanstack/ai-grok': patch
'@tanstack/ai-groq': patch
'@tanstack/ai-mistral': patch
'@tanstack/ai-openai': patch
'@tanstack/ai-openrouter': patch
---

Model metadata no longer carries prices. Nothing read them, and prices change faster than a package release. To price a call, read the live rate card from modelschemas: see the Model Prices guide.
7 changes: 7 additions & 0 deletions .changeset/sync-modelschemas-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-byteplus': patch
'@tanstack/ai-groq': patch
'@tanstack/ai-mistral': patch
---

Add new BytePlus, Groq, and Mistral models from the modelschemas catalog.
8 changes: 6 additions & 2 deletions .github/workflows/sync-models.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ jobs:
id: elevenlabs
continue-on-error: true
run: pnpm generate:models:elevenlabs
env:
MODELSCHEMAS_API_KEY: ${{ secrets.MODELSCHEMAS_API_KEY }}

- name: Sync provider models
id: provider
continue-on-error: true
run: pnpm exec tsx scripts/sync-provider-models.ts
env:
MODELSCHEMAS_API_KEY: ${{ secrets.MODELSCHEMAS_API_KEY }}

- name: Format
run: pnpm format
Expand Down Expand Up @@ -87,15 +91,15 @@ jobs:

if [ -z "$EXISTING_PR" ] || [ "$EXISTING_PR" = "null" ]; then
BODY=$(cat <<'PRBODY'
Automated daily sync of model metadata from OpenRouter, Vercel AI Gateway, Lovable AI Gateway, LLM Gateway, Neon AI Gateway, and modelschemas (ElevenLabs text-to-speech, music, sound effects, and voice design).
Automated daily sync of model metadata from OpenRouter, Vercel AI Gateway, Lovable AI Gateway, LLM Gateway, Neon AI Gateway, and modelschemas (native provider models, plus ElevenLabs text-to-speech, music, sound effects, and voice design).

- Fetches the latest model list from OpenRouter (chat + `GET /api/v1/videos/models`)
- Fetches the latest model list from Vercel AI Gateway
- Fetches the latest model list from Lovable AI Gateway
- Fetches the latest model list from LLM Gateway (`GET /v1/models`)
- Fetches the latest model list from Neon AI Gateway
- Converts to the internal adapter format
- Syncs provider-specific model metadata for affected packages
- Inserts new native-provider models from modelschemas (`@modelschemas/client`)
- Inserts new ElevenLabs text-to-speech, music, sound-effect, and voice-design models from modelschemas
- Creates a patch changeset for all changed packages
PRBODY
Expand Down
18 changes: 12 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,24 @@ For deeper architecture details (adapter system, isomorphic tools, framework int

1. Fetches OpenRouter, Vercel AI Gateway, Lovable AI Gateway, and Neon AI Gateway catalogs.
2. Regenerates `packages/ai-openrouter/src/model-meta.ts` and the Vercel Gateway and Neon model lists.
3. Inserts **new** native-provider models into `packages/ai-openai`, `ai-anthropic`, `ai-gemini`, and `ai-grok`.
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`. Transcription ids stay hand-maintained.
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`. Ids, activity, limits, modalities, and capabilities come from each provider's own catalog, with no second source. A field the catalog leaves empty stays empty, and a row with no modalities is skipped (the log counts it). Model metadata has no prices: they change faster than a release, so apps read them live (`docs/advanced/model-prices.md`).
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.

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

Reads against modelschemas work without a key (60 req/h per IP). Set `MODELSCHEMAS_API_KEY` on the workflow when you want the higher limit.

Rules the generator follows:

- Keep OpenRouter routing aliases (ids that start with `~`) in the OpenRouter catalog. Users can pass `chat({ model: '~anthropic/claude-haiku-latest' })`. The generated constant name maps `~` to `_`.
- Do **not** copy those aliases into native provider files (`ai-openai`, `ai-anthropic`, `ai-gemini`, `ai-grok`). Those adapters only accept the provider's own ids.
- For a new native-provider model, write id, modalities, and pricing. Infer features from OpenRouter `supported_parameters` when that field exists. Do **not** copy another model's tool list (`computer_use`, `google_search`, `x_search`, and similar).
- Anthropic first-party ids use dashes (`claude-fable-5-1`). OpenRouter uses dots (`claude-fable-5.1`). The generator hyphenates Anthropic ids on insert. Do not copy the dotted OpenRouter slug into `ai-anthropic`.
- Do **not** copy those aliases into native provider files (any of the eight packages above). Those adapters only accept the provider's own ids.
- For a new native-provider model, write id, limits, and modalities. Infer features from the catalog capabilities: a map of flags where only `true` counts (an absent key is unknown), or a feature object, as BytePlus sends. Do **not** copy another model's tool list (`computer_use`, `google_search`, `x_search`, and similar). BytePlus inserts never claim `structured_outputs`: that gate is a live-probed list.
- ElevenLabs music and voice-design ids are pinned to SDK unions. An id the ElevenLabs SDK does not know yet fails the type-check, so wait for the SDK. BytePlus video and image duration/size tables stay hand-curated.
- Anthropic first-party ids use dashes (`claude-fable-5-1`). OpenRouter uses dots (`claude-fable-5.1`). The generator takes the modelschemas native id (already hyphenated) and does not copy the dotted OpenRouter slug into `ai-anthropic`. Dated Anthropic snapshots (`claude-haiku-4-5-20251001`) are skipped; the undated alias is the public id.
- Write a row in the provider's `*ChatModelToolCapabilitiesByName` map for every new chat model, even when `supports.tools` is still `[]`. Missing that row makes `ResolveToolCapabilities` fall back to `readonly []`.
- For new Anthropic models, infer the provider-options mix from the catalog: no sampling parameters → `AnthropicMaxTokensOptions`; `reasoning.mandatory` → `AnthropicAdaptiveOnlyThinkingOptions` plus `AnthropicOutputConfigOptions`.
- For new Anthropic models, infer the provider-options mix from the catalog: no sampling parameters → `AnthropicMaxTokensOptions`; `reasoning.mandatory` → `AnthropicAdaptiveOnlyThinkingOptions` plus `AnthropicOutputConfigOptions`; reasoning params without sampling → adaptive-or-disabled thinking. Every new Claude model gets `AnthropicCacheControlOptions`.
- `firstSeenAt` is when modelschemas first crawled an id, not its release date. The first sync of a new provider can need a one-off older cutoff in `scripts/.sync-models-last-run`.
- Leave curated tools and flags on existing models alone. Edit those by hand after the sync PR opens.

Do not rebase or hand-edit `automated/sync-models`. The next scheduled run force-pushes that branch from `main`. Merge generator fixes to `main` first, then let the workflow rebuild the sync PR.
Expand Down
155 changes: 155 additions & 0 deletions docs/advanced/model-prices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
title: Model Prices
id: model-prices
description: "Price each model call from its token usage, with live rate cards from modelschemas."
keywords:
- tanstack ai
- model prices
- token cost
- modelschemas
- rate card
- onUsage
---

Get a model's rate card from [modelschemas](https://modelschemas.com) with `@modelschemas/client`. Check it with `rateCardSchema`, then price each call with `evaluateCard` from `@modelschemas/rate-card`. Prices change often, so read them live. Do not copy them into your code.

<!-- ::start:tabs variant="package-manager" mode="install" -->

react: @modelschemas/client @modelschemas/rate-card zod
vue: @modelschemas/client @modelschemas/rate-card zod
solid: @modelschemas/client @modelschemas/rate-card zod
svelte: @modelschemas/client @modelschemas/rate-card zod
preact: @modelschemas/client @modelschemas/rate-card zod
angular: @modelschemas/client @modelschemas/rate-card zod
octane: @modelschemas/client @modelschemas/rate-card zod
vanilla: @modelschemas/client @modelschemas/rate-card zod

<!-- ::end:tabs -->

## Price a chat call

Price each call in the [`onUsage`](./middleware-advanced#onusage) hook of a middleware, on the server:

```typescript
import { chat } from "@tanstack/ai";
import type { ChatMiddleware, TokenUsage } from "@tanstack/ai";
import { anthropicText } from "@tanstack/ai-anthropic";
import { createModelschemasClient, getModel } from "@modelschemas/client";
import { RateCardError, evaluateCard } from "@modelschemas/rate-card/evaluator";
import type { RateCard } from "@modelschemas/rate-card/evaluator";
import { rateCardSchema } from "@modelschemas/rate-card/schema";

const client = createModelschemasClient({ baseUrl: "https://modelschemas.com" });

// Keep each rate card for one hour.
const cards = new Map<string, { at: number; card: Promise<RateCard | undefined> }>();

function rateCard(provider: string, modelId: string) {
const key = `${provider}/${modelId}`;
const hit = cards.get(key);
if (hit && Date.now() - hit.at < 60 * 60 * 1000) return hit.card;
// A model with no rate card, or only a price summary, fails the check.
const card = getModel({ client, path: { provider, modelId } }).then(({ data }) => {
const parsed = rateCardSchema.safeParse(data?.pricing);
return parsed.success ? parsed.data : undefined;
});
cards.set(key, { at: Date.now(), card });
return card;
}

// A rate card counts each kind of input token once. `promptTokens` holds them all.
function cardUsage(usage: TokenUsage) {
const cached = usage.promptTokensDetails?.cachedTokens ?? 0;
const written = usage.promptTokensDetails?.cacheWriteTokens ?? 0;
const written1h = usage.promptTokensDetails?.cacheWrite1hTokens ?? 0;
const cacheCounts = {
cache_read_tokens: cached,
cache_write_tokens: written - written1h,
cache_write_1h_tokens: written1h,
};
return {
input_tokens: usage.promptTokens - cached - written,
output_tokens: usage.completionTokens,
// A card without a cache price rejects a cache count, even a 0.
...Object.fromEntries(Object.entries(cacheCounts).filter(([, count]) => count > 0)),
};
}

const costLog: ChatMiddleware = {
name: "cost-log",
onUsage: async (ctx, usage) => {
const card = await rateCard("anthropic", ctx.model);
if (!card) return console.log(`${ctx.model}: no price on modelschemas`);
try {
const { amount, currency } = evaluateCard(card, { usage: cardUsage(usage) });
const price = new Intl.NumberFormat("en", { style: "currency", currency, maximumFractionDigits: 6 });
console.log(`${ctx.model}: ${price.format(amount)}`);
} catch (error) {
if (!(error instanceof RateCardError)) throw error;
console.log(`${ctx.model}: price not known (${error.message})`);
}
},
};

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

The log shows the cost of each model call, for example `claude-sonnet-5-5: $0.013`. A card uses long-context tiers when the input is long enough.

## When the price is not known

The rate card does not guess:

- `rateCardSchema.safeParse` fails when modelschemas has no rate card for the model.
- `evaluateCard` throws `RateCardError` when the call used tokens that the card has no price for.
- `currency` is the card's own currency, for example `USD` or `CNY`. Do not add amounts in two currencies.

modelschemas allows 60 calls an hour without a key. With the one-hour cache above, that is one call for each model. For a higher limit, pass `apiKey` to `createModelschemasClient`.

## Price an image before you make it

A media card prices the request, so you can show the cost before you call the model:

```typescript
import { createModelschemasClient, getModel } from "@modelschemas/client";
import { evaluateCard } from "@modelschemas/rate-card/evaluator";
import { rateCardSchema } from "@modelschemas/rate-card/schema";

const client = createModelschemasClient({ baseUrl: "https://modelschemas.com" });

const { data } = await getModel({
client,
path: { provider: "fal", modelId: "alibaba/qwen-image-3/edit" },
});
const parsed = rateCardSchema.safeParse(data?.pricing);
if (parsed.success) {
const { amount, currency } = evaluateCard(parsed.data, {
request: { num_images: 2 },
});
console.log(`Two images cost ${amount} ${currency}`);
}
```

## Provider ids

Pass the modelschemas provider id, not the package name:

| Package | modelschemas provider |
| --- | --- |
| `@tanstack/ai-anthropic` | `anthropic` |
| `@tanstack/ai-openai` | `openai`, or `azure` for Azure OpenAI |
| `@tanstack/ai-gemini` | `gemini` |
| `@tanstack/ai-vertex` | `google-vertex` |
| `@tanstack/ai-grok` | `grok` |
| `@tanstack/ai-groq` | `groq` |
| `@tanstack/ai-mistral` | `mistral` |
| `@tanstack/ai-bedrock` | `amazon-bedrock` |
| `@tanstack/ai-openrouter` | `openrouter` |
| `@tanstack/ai-vercel-gateway` | `vercel` |
| `@tanstack/ai-cloudflare` | `cloudflare-workers-ai` |
| `@tanstack/ai-fal` | `fal` |
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, see [Model Prices](./model-prices).

## How it works

- The cache key is the first of these that you set:
Expand Down
2 changes: 1 addition & 1 deletion docs/community-adapters/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ const EFFORT_NONE_LOW_TO_XHIGH = {

const GPT5_2 = {
name: 'gpt-5.2',
// ...supports, pricing
// ...supports
reasoning: EFFORT_NONE_LOW_TO_XHIGH,
} as const

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-26"
},
{
"label": "Model Prices",
"to": "advanced/model-prices",
"addedAt": "2026-10-26"
},
{
"label": "Mid-Conversation Changes",
Expand Down
2 changes: 2 additions & 0 deletions kiira.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export default defineConfig({
'@cloudflare/workers-types': '^4.20260317.1',
'@decartai/tanstack-ai-adapter': '^0.0.2',
'@mynthio/tanstack-ai-adapter': '^0.0.14',
// Live model prices (docs/advanced/model-prices.md)
'@modelschemas/rate-card': '^0.2.1',
'@cencori/ai-sdk': '^0.4.0',
'@soniox/tanstack-ai-adapter': '^0.1.2',
'@upstash/agentkit-tanstack-ai': '^0.1.1',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.30.0",
"@faker-js/faker": "^10.1.0",
"@modelschemas/client": "^0.2.1",
"@stylistic/eslint-plugin": "^5.10.0",
"@tanstack/ai": "workspace:*",
"@tanstack/ai-grok-build": "workspace:*",
Expand Down
Loading
Loading