diff --git a/.changeset/fable-model-support.md b/.changeset/fable-model-support.md new file mode 100644 index 000000000..c8259bf04 --- /dev/null +++ b/.changeset/fable-model-support.md @@ -0,0 +1,14 @@ +--- +'@tanstack/ai-anthropic': patch +'@tanstack/ai-openrouter': patch +--- + +Add support for Anthropic's Claude Fable 5 model (`claude-fable-5` on the Anthropic adapter, `anthropic/claude-fable-5` on the OpenRouter adapter), including model metadata, per-model provider option types, and input modality types. + +Also correct Anthropic model metadata against the live Models API and https://platform.claude.com/docs/en/about-claude/pricing: + +- `claude-opus-4-5` pricing is $5/MTok input and $25/MTok output (was wrongly listed at $15/$75); cache-read (`cached`) prices are now populated for all models that publish them. +- Fix invalid OpenRouter-style ids `claude-opus-4.8`/`claude-opus-4.8-fast` to the real Anthropic API ids `claude-opus-4-8`/`claude-opus-4-8-fast` (the dotted ids 404 against the Anthropic API). +- Correct stale context windows and output limits per the Models API: Opus 4.6, Sonnet 4.5, and Sonnet 4 have 1M context; Sonnet 4.6 supports 128K output; Opus 4.5 supports 64K output; Opus 4.1 caps at 32K output. +- Register `claude-fable-5`, `claude-opus-4-7-fast`, `claude-opus-4-8`, and `claude-opus-4-8-fast` in `AnthropicChatModelToolCapabilitiesByName` so server tools type-check on those models (the sync script now maintains this map too). +- Update `@anthropic-ai/sdk` to ^0.104.0, whose `Model` union includes `claude-fable-5` and `claude-opus-4-8`. diff --git a/.github/workflows/sync-models.yml b/.github/workflows/sync-models.yml index 9e4e14817..9d31502c6 100644 --- a/.github/workflows/sync-models.yml +++ b/.github/workflows/sync-models.yml @@ -29,6 +29,10 @@ jobs: - name: Fetch and sync model metadata run: pnpm generate:models + env: + # Required by generate:anthropic-models:fetch — Anthropic models are + # synced from the first-party Models API, not OpenRouter. + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Check for package changes id: changes @@ -44,8 +48,8 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add packages/ scripts/openrouter.models.json scripts/.sync-models-last-run .changeset/ - git commit -m "chore: sync model metadata from OpenRouter" + git add packages/ scripts/openrouter.models.json scripts/anthropic.models.json scripts/.sync-models-last-run .changeset/ + git commit -m "chore: sync model metadata from provider APIs" git push --force origin HEAD:automated/sync-models env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -61,16 +65,19 @@ jobs: if [ -z "$EXISTING_PR" ] || [ "$EXISTING_PR" = "null" ]; then BODY=$(cat <<'PRBODY' - Automated daily sync of model metadata from the OpenRouter API. + Automated daily sync of model metadata from provider APIs. - Fetches the latest model list from OpenRouter - Converts to the internal adapter format - Syncs provider-specific model metadata for affected packages + - Fetches the Anthropic catalog from the first-party Models API and + adds any new models to `@tanstack/ai-anthropic` (new models fail + the job until a pricing row is added to `sync-anthropic-models.ts`) - Creates a patch changeset for all changed packages PRBODY ) gh pr create \ - --title "chore: sync model metadata from OpenRouter" \ + --title "chore: sync model metadata from provider APIs" \ --body "$BODY" \ --base main \ --head "$BRANCH" diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index c2fbe48b6..b4c22f74f 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -204,7 +204,7 @@ Creates an Anthropic chat adapter. **Parameters:** -- `model` - Claude model id (e.g. `"claude-sonnet-4-6"`, `"claude-opus-4.8"`) +- `model` - Claude model id (e.g. `"claude-sonnet-4-6"`, `"claude-opus-4-8"`) - `config?.baseURL` - Custom base URL (optional) ### `anthropicSummarize(model, config?)` / `createAnthropicSummarize(model, apiKey, config?)` @@ -242,7 +242,7 @@ import { anthropicText } from "@tanstack/ai-anthropic"; import { webSearchTool } from "@tanstack/ai-anthropic/tools"; const stream = chat({ - adapter: anthropicText("claude-opus-4.8"), + adapter: anthropicText("claude-opus-4-8"), messages: [{ role: "user", content: "What's new in AI this week?" }], tools: [ webSearchTool({ diff --git a/docs/advanced/multimodal-content.md b/docs/advanced/multimodal-content.md index e42067a05..b6cf6d1fa 100644 --- a/docs/advanced/multimodal-content.md +++ b/docs/advanced/multimodal-content.md @@ -154,7 +154,7 @@ const docMessage = { ``` **Supported modalities:** -- Most Claude models (e.g. `claude-haiku-3`, `claude-haiku-4-5`, `claude-sonnet-4-6`, `claude-opus-4.8`): text, image, and document (PDF) +- Most Claude models (e.g. `claude-haiku-3`, `claude-haiku-4-5`, `claude-sonnet-4-6`, `claude-opus-4-8`): text, image, and document (PDF) Check each model's `supports.input` in `@tanstack/ai-anthropic`'s `model-meta.ts` for the authoritative per-model list. diff --git a/docs/tools/provider-tools.md b/docs/tools/provider-tools.md index 2aa9d3e6f..7e55f3229 100644 --- a/docs/tools/provider-tools.md +++ b/docs/tools/provider-tools.md @@ -33,7 +33,7 @@ import { anthropicText } from '@tanstack/ai-anthropic' import { webSearchTool } from '@tanstack/ai-anthropic/tools' const stream = chat({ - adapter: anthropicText('claude-opus-4.8'), + adapter: anthropicText('claude-opus-4-8'), messages: [{ role: 'user', content: "Summarize today's AI news." }], tools: [ webSearchTool({ diff --git a/package.json b/package.json index 924d53b10..b2b22399e 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,12 @@ "dev:chat": "pnpm --filter ts-react-chat dev", "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", "generate-docs": "node scripts/generate-docs.ts && pnpm run copy:readme", - "generate:models": "pnpm generate:models:fetch && pnpm regenerate:models && tsx scripts/sync-provider-models.ts && pnpm format", + "generate:models": "pnpm generate:models:fetch && pnpm regenerate:models && tsx scripts/sync-provider-models.ts && pnpm generate:anthropic-models:fetch && pnpm regenerate:anthropic-models && pnpm format", "generate:models:fetch": "tsx scripts/fetch-openrouter-models.ts", "regenerate:models": "tsx scripts/convert-openrouter-models.ts", + "generate:anthropic-models": "pnpm generate:anthropic-models:fetch && pnpm regenerate:anthropic-models && pnpm format", + "generate:anthropic-models:fetch": "tsx scripts/fetch-anthropic-models.ts", + "regenerate:anthropic-models": "tsx scripts/sync-anthropic-models.ts", "sync-docs-config": "node scripts/sync-docs-config.ts", "copy:readme": "node scripts/copy-readme.js", "changeset": "changeset", diff --git a/packages/ai-anthropic/package.json b/packages/ai-anthropic/package.json index e1b4173f2..dc81e84b9 100644 --- a/packages/ai-anthropic/package.json +++ b/packages/ai-anthropic/package.json @@ -50,7 +50,7 @@ "test:types": "tsc" }, "dependencies": { - "@anthropic-ai/sdk": "^0.97.1", + "@anthropic-ai/sdk": "^0.104.0", "@tanstack/ai-utils": "workspace:*" }, "peerDependencies": { diff --git a/packages/ai-anthropic/src/model-meta.ts b/packages/ai-anthropic/src/model-meta.ts index 86301ed7d..1a6da2a49 100644 --- a/packages/ai-anthropic/src/model-meta.ts +++ b/packages/ai-anthropic/src/model-meta.ts @@ -60,12 +60,13 @@ interface ModelMeta< const CLAUDE_OPUS_4_6 = { name: 'claude-opus-4-6', id: 'claude-opus-4-6', - context_window: 200_000, + context_window: 1_000_000, max_output_tokens: 128_000, knowledge_cutoff: '2025-05-01', pricing: { input: { normal: 5, + cached: 0.5, }, output: { normal: 25, @@ -100,14 +101,15 @@ const CLAUDE_OPUS_4_5 = { name: 'claude-opus-4-5', id: 'claude-opus-4-5', context_window: 200_000, - max_output_tokens: 32_000, + max_output_tokens: 64_000, knowledge_cutoff: '2025-11-01', pricing: { input: { - normal: 15, + normal: 5, + cached: 0.5, }, output: { - normal: 75, + normal: 25, }, }, supports: { @@ -139,11 +141,12 @@ const CLAUDE_SONNET_4_6 = { name: 'claude-sonnet-4-6', id: 'claude-sonnet-4-6', context_window: 1_000_000, - max_output_tokens: 64_000, + max_output_tokens: 128_000, knowledge_cutoff: '2025-08-01', pricing: { input: { normal: 3, + cached: 0.3, }, output: { normal: 15, @@ -178,12 +181,13 @@ const CLAUDE_SONNET_4_6 = { const CLAUDE_SONNET_4_5 = { name: 'claude-sonnet-4-5', id: 'claude-sonnet-4-5', - context_window: 200_000, + context_window: 1_000_000, max_output_tokens: 64_000, knowledge_cutoff: '2025-09-29', pricing: { input: { normal: 3, + cached: 0.3, }, output: { normal: 15, @@ -223,6 +227,7 @@ const CLAUDE_HAIKU_4_5 = { pricing: { input: { normal: 1, + cached: 0.1, }, output: { normal: 5, @@ -257,11 +262,12 @@ const CLAUDE_OPUS_4_1 = { name: 'claude-opus-4-1', id: 'claude-opus-4-1', context_window: 200_000, - max_output_tokens: 64_000, + max_output_tokens: 32_000, knowledge_cutoff: '2025-08-05', pricing: { input: { normal: 15, + cached: 1.5, }, output: { normal: 75, @@ -295,12 +301,13 @@ const CLAUDE_OPUS_4_1 = { const CLAUDE_SONNET_4 = { name: 'claude-sonnet-4', id: 'claude-sonnet-4', - context_window: 200_000, + context_window: 1_000_000, max_output_tokens: 64_000, knowledge_cutoff: '2025-05-14', pricing: { input: { normal: 3, + cached: 0.3, }, output: { normal: 15, @@ -378,6 +385,7 @@ const CLAUDE_OPUS_4 = { pricing: { input: { normal: 15, + cached: 1.5, }, output: { normal: 75, @@ -417,6 +425,7 @@ const CLAUDE_HAIKU_3_5 = { pricing: { input: { normal: 0.8, + cached: 0.08, }, output: { normal: 4, @@ -633,8 +642,8 @@ const CLAUDE_OPUS_4_7_FAST = { > const CLAUDE_OPUS_4_8 = { - name: 'claude-opus-4.8', - id: 'claude-opus-4.8', + name: 'claude-opus-4-8', + id: 'claude-opus-4-8', context_window: 1_000_000, max_output_tokens: 128_000, supports: { @@ -672,8 +681,8 @@ const CLAUDE_OPUS_4_8 = { > const CLAUDE_OPUS_4_8_FAST = { - name: 'claude-opus-4.8-fast', - id: 'claude-opus-4.8-fast', + name: 'claude-opus-4-8-fast', + id: 'claude-opus-4-8-fast', context_window: 1_000_000, max_output_tokens: 128_000, supports: { @@ -710,6 +719,46 @@ const CLAUDE_OPUS_4_8_FAST = { AnthropicSamplingOptions > +const CLAUDE_FABLE_5 = { + name: 'claude-fable-5', + id: 'claude-fable-5', + context_window: 1_000_000, + max_output_tokens: 128_000, + supports: { + input: ['text', 'image', 'document'], + extended_thinking: false, + adaptive_thinking: true, + priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], + }, + pricing: { + input: { + normal: 10, + cached: 1, + }, + output: { + normal: 50, + }, + }, +} as const satisfies ModelMeta< + AnthropicContainerOptions & + AnthropicContextManagementOptions & + AnthropicMCPOptions & + AnthropicServiceTierOptions & + AnthropicStopSequencesOptions & + AnthropicThinkingOptions & + AnthropicToolChoiceOptions & + AnthropicSamplingOptions +> + export const ANTHROPIC_MODELS = [ CLAUDE_OPUS_4_6.id, CLAUDE_OPUS_4_5.id, @@ -731,6 +780,8 @@ export const ANTHROPIC_MODELS = [ CLAUDE_OPUS_4_8.id, CLAUDE_OPUS_4_8_FAST.id, + + CLAUDE_FABLE_5.id, ] as const /** @@ -891,6 +942,14 @@ export type AnthropicChatModelProviderOptionsByName = { AnthropicThinkingOptions & AnthropicToolChoiceOptions & AnthropicSamplingOptions + [CLAUDE_FABLE_5.id]: AnthropicContainerOptions & + AnthropicContextManagementOptions & + AnthropicMCPOptions & + AnthropicServiceTierOptions & + AnthropicStopSequencesOptions & + AnthropicThinkingOptions & + AnthropicToolChoiceOptions & + AnthropicSamplingOptions } export type AnthropicChatModelToolCapabilitiesByName = { @@ -907,6 +966,10 @@ export type AnthropicChatModelToolCapabilitiesByName = { [CLAUDE_HAIKU_3.id]: typeof CLAUDE_HAIKU_3.supports.tools [CLAUDE_OPUS_4_6_FAST.id]: typeof CLAUDE_OPUS_4_6_FAST.supports.tools [CLAUDE_OPUS_4_7.id]: typeof CLAUDE_OPUS_4_7.supports.tools + [CLAUDE_OPUS_4_7_FAST.id]: typeof CLAUDE_OPUS_4_7_FAST.supports.tools + [CLAUDE_OPUS_4_8.id]: typeof CLAUDE_OPUS_4_8.supports.tools + [CLAUDE_OPUS_4_8_FAST.id]: typeof CLAUDE_OPUS_4_8_FAST.supports.tools + [CLAUDE_FABLE_5.id]: typeof CLAUDE_FABLE_5.supports.tools } /** @@ -937,4 +1000,5 @@ export type AnthropicModelInputModalitiesByName = { [CLAUDE_OPUS_4_7_FAST.id]: typeof CLAUDE_OPUS_4_7_FAST.supports.input [CLAUDE_OPUS_4_8.id]: typeof CLAUDE_OPUS_4_8.supports.input [CLAUDE_OPUS_4_8_FAST.id]: typeof CLAUDE_OPUS_4_8_FAST.supports.input + [CLAUDE_FABLE_5.id]: typeof CLAUDE_FABLE_5.supports.input } diff --git a/packages/ai-anthropic/tests/model-meta.test.ts b/packages/ai-anthropic/tests/model-meta.test.ts index 3a8c5bc44..eb5484ab1 100644 --- a/packages/ai-anthropic/tests/model-meta.test.ts +++ b/packages/ai-anthropic/tests/model-meta.test.ts @@ -1,6 +1,8 @@ import { describe, expectTypeOf, it } from 'vitest' +import { ANTHROPIC_MODELS } from '../src/model-meta' import type { AnthropicChatModelProviderOptionsByName, + AnthropicChatModelToolCapabilitiesByName, AnthropicModelInputModalitiesByName, } from '../src/model-meta' import type { AnthropicMessageMetadataByModality } from '../src/message-types' @@ -143,6 +145,29 @@ describe('Anthropic Model Provider Options Type Assertions', () => { expectTypeOf().toExtend() }) + it('claude-fable-5 should support thinking options', () => { + type Options = AnthropicChatModelProviderOptionsByName['claude-fable-5'] + + // Should have thinking options (adaptive thinking) + expectTypeOf().toExtend() + + // Should have service tier options (priority_tier support) + expectTypeOf().toExtend() + + // Should have base options + expectTypeOf().toExtend() + + // Verify specific properties exist + expectTypeOf().toHaveProperty('thinking') + expectTypeOf().toHaveProperty('service_tier') + expectTypeOf().toHaveProperty('container') + expectTypeOf().toHaveProperty('context_management') + expectTypeOf().toHaveProperty('mcp_servers') + expectTypeOf().toHaveProperty('stop_sequences') + expectTypeOf().toHaveProperty('tool_choice') + expectTypeOf().toHaveProperty('top_k') + }) + it('claude-opus-4-5 should support thinking options', () => { type Options = AnthropicChatModelProviderOptionsByName['claude-opus-4-5'] @@ -211,6 +236,7 @@ describe('Anthropic Model Provider Options Type Assertions', () => { it('AnthropicChatModelProviderOptionsByName should have entries for all chat models', () => { type Keys = keyof AnthropicChatModelProviderOptionsByName + expectTypeOf<'claude-fable-5'>().toExtend() expectTypeOf<'claude-opus-4-5'>().toExtend() expectTypeOf<'claude-sonnet-4-6'>().toExtend() expectTypeOf<'claude-sonnet-4-5'>().toExtend() @@ -222,6 +248,20 @@ describe('Anthropic Model Provider Options Type Assertions', () => { expectTypeOf<'claude-3-5-haiku'>().toExtend() expectTypeOf<'claude-3-haiku'>().toExtend() }) + + it('every model in ANTHROPIC_MODELS should have entries in all three type maps', () => { + type ModelId = (typeof ANTHROPIC_MODELS)[number] + + expectTypeOf().toExtend< + keyof AnthropicChatModelProviderOptionsByName + >() + expectTypeOf().toExtend< + keyof AnthropicModelInputModalitiesByName + >() + expectTypeOf().toExtend< + keyof AnthropicChatModelToolCapabilitiesByName + >() + }) }) describe('Detailed property type assertions', () => { @@ -560,6 +600,28 @@ describe('Anthropic Model Input Modality Type Assertions', () => { > type MessageWithContent = { role: 'user'; content: Array } + describe('Claude Fable 5 (text + image + document)', () => { + type Modalities = AnthropicModelInputModalitiesByName['claude-fable-5'] + type Message = ConstrainedModelMessage> + + it('should allow TextPart, ImagePart, and DocumentPart', () => { + expectTypeOf>().toExtend() + expectTypeOf>().toExtend() + expectTypeOf< + MessageWithContent + >().toExtend() + }) + + it('should NOT allow AudioPart or VideoPart', () => { + expectTypeOf< + MessageWithContent + >().not.toExtend() + expectTypeOf< + MessageWithContent + >().not.toExtend() + }) + }) + describe('Claude Opus 4.5 (text + image + document)', () => { type Modalities = AnthropicModelInputModalitiesByName['claude-opus-4-5'] type Message = ConstrainedModelMessage> diff --git a/packages/ai-openrouter/src/model-meta.ts b/packages/ai-openrouter/src/model-meta.ts index 42122cd2c..0d3fe7299 100644 --- a/packages/ai-openrouter/src/model-meta.ts +++ b/packages/ai-openrouter/src/model-meta.ts @@ -628,6 +628,35 @@ const ANTHROPIC_CLAUDE_3_5_HAIKU = { image: 0, }, } as const +const ANTHROPIC_CLAUDE_FABLE_5 = { + id: 'anthropic/claude-fable-5', + name: 'Anthropic: Claude Fable 5', + supports: { + input: ['text', 'image', 'document'], + output: ['text'], + supports: [ + 'maxCompletionTokens', + 'reasoning', + 'responseFormat', + 'stop', + 'toolChoice', + ], + }, + context_window: 1000000, + max_output_tokens: 128000, + pricing: { + text: { + input: { + normal: 10, + cached: 13.5, + }, + output: { + normal: 50, + }, + }, + image: 0, + }, +} as const const ANTHROPIC_CLAUDE_HAIKU_4_5 = { id: 'anthropic/claude-haiku-4.5', name: 'Anthropic: Claude Haiku 4.5', @@ -10985,6 +11014,15 @@ export type OpenRouterModelOptionsByName = { OpenRouterBaseOptions, 'maxCompletionTokens' | 'stop' | 'temperature' | 'toolChoice' | 'topP' > + [ANTHROPIC_CLAUDE_FABLE_5.id]: OpenRouterCommonOptions & + Pick< + OpenRouterBaseOptions, + | 'maxCompletionTokens' + | 'reasoning' + | 'responseFormat' + | 'stop' + | 'toolChoice' + > [ANTHROPIC_CLAUDE_HAIKU_4_5.id]: OpenRouterCommonOptions & Pick< OpenRouterBaseOptions, @@ -14922,6 +14960,7 @@ export type OpenRouterModelInputModalitiesByName = { [ANTHRACITE_ORG_MAGNUM_V4_72B.id]: ReadonlyArray<'text'> [ANTHROPIC_CLAUDE_3_HAIKU.id]: ReadonlyArray<'text' | 'image'> [ANTHROPIC_CLAUDE_3_5_HAIKU.id]: ReadonlyArray<'text' | 'image'> + [ANTHROPIC_CLAUDE_FABLE_5.id]: ReadonlyArray<'text' | 'image' | 'document'> [ANTHROPIC_CLAUDE_HAIKU_4_5.id]: ReadonlyArray<'text' | 'image' | 'document'> [ANTHROPIC_CLAUDE_OPUS_4.id]: ReadonlyArray<'image' | 'text' | 'document'> [ANTHROPIC_CLAUDE_OPUS_4_1.id]: ReadonlyArray<'image' | 'text' | 'document'> @@ -15312,6 +15351,7 @@ export const OPENROUTER_CHAT_MODELS = [ ANTHRACITE_ORG_MAGNUM_V4_72B.id, ANTHROPIC_CLAUDE_3_HAIKU.id, ANTHROPIC_CLAUDE_3_5_HAIKU.id, + ANTHROPIC_CLAUDE_FABLE_5.id, ANTHROPIC_CLAUDE_HAIKU_4_5.id, ANTHROPIC_CLAUDE_OPUS_4.id, ANTHROPIC_CLAUDE_OPUS_4_1.id, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab84c8703..d8e4ecb1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1019,8 +1019,8 @@ importers: packages/ai-anthropic: dependencies: '@anthropic-ai/sdk': - specifier: ^0.97.1 - version: 0.97.1(zod@4.2.1) + specifier: ^0.104.0 + version: 0.104.0(zod@4.2.1) '@tanstack/ai-utils': specifier: workspace:* version: link:../ai-utils @@ -2117,8 +2117,8 @@ packages: '@ag-ui/core@0.0.52': resolution: {integrity: sha512-Xo0bUaNV56EqylzcrAuhUkQX7et7+SZIrqZZtEByGwEq/I1EHny6ZMkWHLkKR7UNi0FJZwJyhKYmKJS3B2SEgA==} - '@anthropic-ai/sdk@0.97.1': - resolution: {integrity: sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg==} + '@anthropic-ai/sdk@0.104.0': + resolution: {integrity: sha512-ppxCBQkDgzm1/Y6BEtEN+Lx7ejwFwKHDu7lh9W0iMxtjb1MdicbqrSRCXecKL9ZMsXwT/hAvdsddORtBzKE2UQ==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -13896,7 +13896,7 @@ snapshots: dependencies: zod: 3.25.76 - '@anthropic-ai/sdk@0.97.1(zod@4.2.1)': + '@anthropic-ai/sdk@0.104.0(zod@4.2.1)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.0.0 diff --git a/scripts/.sync-models-last-run b/scripts/.sync-models-last-run index be7a0996a..db1a96391 100644 --- a/scripts/.sync-models-last-run +++ b/scripts/.sync-models-last-run @@ -1 +1 @@ -1780482493 +1781041133 diff --git a/scripts/anthropic.models.json b/scripts/anthropic.models.json new file mode 100644 index 000000000..e4d8291b9 --- /dev/null +++ b/scripts/anthropic.models.json @@ -0,0 +1,763 @@ +{ + "data": [ + { + "type": "model", + "id": "claude-fable-5", + "display_name": "Claude Fable 5", + "created_at": "2026-06-07T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 128000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": true + } + }, + "effort": { + "supported": true, + "low": { + "supported": true + }, + "medium": { + "supported": true + }, + "high": { + "supported": true + }, + "xhigh": { + "supported": true + }, + "max": { + "supported": true + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": false + }, + "adaptive": { + "supported": true + } + } + } + } + }, + { + "type": "model", + "id": "claude-haiku-4-5-20251001", + "display_name": "Claude Haiku 4.5", + "created_at": "2025-10-15T00:00:00Z", + "max_input_tokens": 200000, + "max_tokens": 64000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": false + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": false + } + }, + "effort": { + "supported": false, + "low": { + "supported": false + }, + "medium": { + "supported": false + }, + "high": { + "supported": false + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": false + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": false + } + } + } + } + }, + { + "type": "model", + "id": "claude-opus-4-1-20250805", + "display_name": "Claude Opus 4.1", + "created_at": "2025-08-05T00:00:00Z", + "max_input_tokens": 200000, + "max_tokens": 32000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": false + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": false + } + }, + "effort": { + "supported": false, + "low": { + "supported": false + }, + "medium": { + "supported": false + }, + "high": { + "supported": false + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": false + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": false + } + } + } + } + }, + { + "type": "model", + "id": "claude-opus-4-20250514", + "display_name": "Claude Opus 4", + "created_at": "2025-05-22T00:00:00Z", + "max_input_tokens": 200000, + "max_tokens": 32000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": false + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": false + } + }, + "effort": { + "supported": false, + "low": { + "supported": false + }, + "medium": { + "supported": false + }, + "high": { + "supported": false + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": false + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": false + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": false + } + } + } + } + }, + { + "type": "model", + "id": "claude-opus-4-5-20251101", + "display_name": "Claude Opus 4.5", + "created_at": "2025-11-24T00:00:00Z", + "max_input_tokens": 200000, + "max_tokens": 64000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": false + } + }, + "effort": { + "supported": true, + "low": { + "supported": true + }, + "medium": { + "supported": true + }, + "high": { + "supported": true + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": false + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": false + } + } + } + } + }, + { + "type": "model", + "id": "claude-opus-4-6", + "display_name": "Claude Opus 4.6", + "created_at": "2026-02-04T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 128000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": true + } + }, + "effort": { + "supported": true, + "low": { + "supported": true + }, + "medium": { + "supported": true + }, + "high": { + "supported": true + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": true + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": true + } + } + } + } + }, + { + "type": "model", + "id": "claude-opus-4-7", + "display_name": "Claude Opus 4.7", + "created_at": "2026-04-14T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 128000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": true + } + }, + "effort": { + "supported": true, + "low": { + "supported": true + }, + "medium": { + "supported": true + }, + "high": { + "supported": true + }, + "xhigh": { + "supported": true + }, + "max": { + "supported": true + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": false + }, + "adaptive": { + "supported": true + } + } + } + } + }, + { + "type": "model", + "id": "claude-opus-4-8", + "display_name": "Claude Opus 4.8", + "created_at": "2026-05-28T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 128000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": true + } + }, + "effort": { + "supported": true, + "low": { + "supported": true + }, + "medium": { + "supported": true + }, + "high": { + "supported": true + }, + "xhigh": { + "supported": true + }, + "max": { + "supported": true + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": false + }, + "adaptive": { + "supported": true + } + } + } + } + }, + { + "type": "model", + "id": "claude-sonnet-4-20250514", + "display_name": "Claude Sonnet 4", + "created_at": "2025-05-22T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 64000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": false + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": false + } + }, + "effort": { + "supported": false, + "low": { + "supported": false + }, + "medium": { + "supported": false + }, + "high": { + "supported": false + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": false + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": false + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": false + } + } + } + } + }, + { + "type": "model", + "id": "claude-sonnet-4-5-20250929", + "display_name": "Claude Sonnet 4.5", + "created_at": "2025-09-29T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 64000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": false + } + }, + "effort": { + "supported": false, + "low": { + "supported": false + }, + "medium": { + "supported": false + }, + "high": { + "supported": false + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": false + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": false + } + } + } + } + }, + { + "type": "model", + "id": "claude-sonnet-4-6", + "display_name": "Claude Sonnet 4.6", + "created_at": "2026-02-17T00:00:00Z", + "max_input_tokens": 1000000, + "max_tokens": 128000, + "capabilities": { + "batch": { + "supported": true + }, + "citations": { + "supported": true + }, + "code_execution": { + "supported": true + }, + "context_management": { + "supported": true, + "clear_tool_uses_20250919": { + "supported": true + }, + "clear_thinking_20251015": { + "supported": true + }, + "compact_20260112": { + "supported": true + } + }, + "effort": { + "supported": true, + "low": { + "supported": true + }, + "medium": { + "supported": true + }, + "high": { + "supported": true + }, + "xhigh": { + "supported": false + }, + "max": { + "supported": true + } + }, + "image_input": { + "supported": true + }, + "pdf_input": { + "supported": true + }, + "structured_outputs": { + "supported": true + }, + "thinking": { + "supported": true, + "types": { + "enabled": { + "supported": true + }, + "adaptive": { + "supported": true + } + } + } + } + } + ] +} diff --git a/scripts/fetch-anthropic-models.ts b/scripts/fetch-anthropic-models.ts new file mode 100644 index 000000000..aaeaa50f2 --- /dev/null +++ b/scripts/fetch-anthropic-models.ts @@ -0,0 +1,83 @@ +/** + * Fetches the model catalog from Anthropic's first-party Models API + * (GET /v1/models) and writes it to scripts/anthropic.models.json. + * + * Unlike the OpenRouter catalog, this is the authoritative source for + * Anthropic model IDs, context windows, max output tokens, and per-model + * capabilities (adaptive vs extended thinking, image input, etc.). + * Pricing is NOT exposed by the Models API — see the PRICING table in + * sync-anthropic-models.ts. + * + * Usage: + * ANTHROPIC_API_KEY=sk-ant-... pnpm generate:anthropic-models:fetch + */ + +import { writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const OUTPUT_PATH = resolve(__dirname, 'anthropic.models.json') +const API_URL = 'https://api.anthropic.com/v1/models' +const PAGE_LIMIT = 100 + +interface ModelsPage { + data: Array<{ id: string }> + has_more: boolean + last_id: string | null +} + +async function fetchPage( + apiKey: string, + afterId: string | undefined, +): Promise { + const url = new URL(API_URL) + url.searchParams.set('limit', String(PAGE_LIMIT)) + if (afterId) { + url.searchParams.set('after_id', afterId) + } + const response = await fetch(url, { + headers: { + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01', + }, + }) + if (!response.ok) { + throw new Error( + `Anthropic Models API request failed: ${response.status} ${await response.text()}`, + ) + } + return (await response.json()) as ModelsPage +} + +async function main() { + const apiKey = process.env.ANTHROPIC_API_KEY + if (!apiKey) { + console.error( + 'ANTHROPIC_API_KEY must be set to fetch the Anthropic model catalog.', + ) + process.exit(1) + } + + const models: Array<{ id: string }> = [] + let afterId: string | undefined + for (;;) { + const page = await fetchPage(apiKey, afterId) + models.push(...page.data) + if (!page.has_more || !page.last_id) break + afterId = page.last_id + } + + models.sort((a, b) => a.id.localeCompare(b.id)) + await writeFile( + OUTPUT_PATH, + JSON.stringify({ data: models }, null, 2) + '\n', + 'utf-8', + ) + console.log(`Wrote ${models.length} models to ${OUTPUT_PATH}`) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/scripts/openrouter.models.json b/scripts/openrouter.models.json index bcc2eeacd..f2266076d 100644 --- a/scripts/openrouter.models.json +++ b/scripts/openrouter.models.json @@ -1069,6 +1069,60 @@ "details": "/api/v1/models/anthropic/claude-3-5-haiku/endpoints" } }, + { + "id": "anthropic/claude-fable-5", + "canonical_slug": "anthropic/claude-fable-5", + "hugging_face_id": null, + "name": "Anthropic: Claude Fable 5", + "created": 1779905091, + "description": "Claude Fable 5 is Anthropic's most powerful, most intelligent model — a new tier above Opus. It supports text, image, and file inputs with text output, with reasoning support and a 1M-token context window.", + "context_length": 1000000, + "architecture": { + "modality": "text+image+file->text", + "input_modalities": ["text", "image", "file"], + "output_modalities": ["text"], + "tokenizer": "Claude", + "instruct_type": null + }, + "pricing": { + "prompt": "0.00001", + "completion": "0.00005", + "web_search": "0.01", + "input_cache_read": "0.000001", + "input_cache_write": "0.0000125" + }, + "top_provider": { + "context_length": 1000000, + "max_completion_tokens": 128000, + "is_moderated": false + }, + "per_request_limits": null, + "supported_parameters": [ + "include_reasoning", + "max_tokens", + "reasoning", + "response_format", + "stop", + "structured_outputs", + "tool_choice", + "tools", + "verbosity" + ], + "default_parameters": { + "temperature": null, + "top_p": null, + "top_k": null, + "frequency_penalty": null, + "presence_penalty": null, + "repetition_penalty": null + }, + "supported_voices": null, + "knowledge_cutoff": null, + "expiration_date": null, + "links": { + "details": "/api/v1/models/anthropic/claude-fable-5/endpoints" + } + }, { "id": "anthropic/claude-haiku-4.5", "canonical_slug": "anthropic/claude-4.5-haiku-20251001", diff --git a/scripts/sync-anthropic-models.ts b/scripts/sync-anthropic-models.ts new file mode 100644 index 000000000..ffe81b1ea --- /dev/null +++ b/scripts/sync-anthropic-models.ts @@ -0,0 +1,397 @@ +/** + * Syncs Anthropic models from the first-party Models API snapshot + * (scripts/anthropic.models.json, produced by fetch-anthropic-models.ts) + * into packages/ai-anthropic/src/model-meta.ts. + * + * This replaces the OpenRouter-based sync for the Anthropic adapter. The + * Models API is authoritative for: + * - model IDs (OpenRouter slugs use dots, e.g. 'claude-opus-4.8', which + * are not valid Anthropic API model IDs) + * - context windows and max output tokens + * - per-model capabilities (image input, adaptive vs extended thinking) + * + * Pricing is NOT exposed by the Models API, so new models require an entry + * in the PRICING table below — the script fails loudly if one is missing + * rather than inventing numbers. + * + * Usage: + * pnpm regenerate:anthropic-models # sync from committed snapshot + * pnpm tsx scripts/sync-anthropic-models.ts --dry-run # print without writing + * pnpm tsx scripts/sync-anthropic-models.ts --input --meta # test against fixtures + */ + +import { readFile, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const ROOT = resolve(__dirname, '..') +const DEFAULT_INPUT = resolve(__dirname, 'anthropic.models.json') +const DEFAULT_META = resolve(ROOT, 'packages/ai-anthropic/src/model-meta.ts') +const CHANGESET_FILE = resolve(ROOT, '.changeset/sync-anthropic-models.md') + +// --------------------------------------------------------------------------- +// Pricing (USD per million tokens) +// +// The Models API does not expose pricing, so this table is maintained by +// hand from https://platform.claude.com/docs/en/about-claude/pricing +// ("Cache Hits & Refreshes" is the `cached` column). When a new model +// appears in the catalog, add a row here — the sync fails with a clear +// error until it has one. +// --------------------------------------------------------------------------- + +interface Pricing { + input: number + cached?: number + output: number +} + +const PRICING: Record = { + 'claude-fable-5': { input: 10, cached: 1, output: 50 }, + 'claude-opus-4-8': { input: 5, cached: 0.5, output: 25 }, + 'claude-opus-4-7': { input: 5, cached: 0.5, output: 25 }, + 'claude-opus-4-6': { input: 5, cached: 0.5, output: 25 }, + 'claude-opus-4-5': { input: 5, cached: 0.5, output: 25 }, + 'claude-opus-4-1': { input: 15, cached: 1.5, output: 75 }, + 'claude-sonnet-4-6': { input: 3, cached: 0.3, output: 15 }, + 'claude-sonnet-4-5': { input: 3, cached: 0.3, output: 15 }, + 'claude-haiku-4-5': { input: 1, cached: 0.1, output: 5 }, +} + +// --------------------------------------------------------------------------- +// Models API types (subset we consume; capability leaves are optional so the +// script degrades gracefully if the API adds/renames fields) +// --------------------------------------------------------------------------- + +interface Capability { + supported?: boolean +} + +interface AnthropicApiModel { + type?: string + id: string + display_name?: string + created_at?: string + max_input_tokens?: number + max_tokens?: number + capabilities?: { + image_input?: Capability + document_input?: Capability + pdf_input?: Capability + priority_tier?: Capability + thinking?: Capability & { + types?: { + enabled?: Capability + adaptive?: Capability + } + } + } +} + +// --------------------------------------------------------------------------- +// Utilities (same conventions as sync-provider-models.ts) +// --------------------------------------------------------------------------- + +/** Strip a trailing date suffix, e.g. 'claude-haiku-4-5-20251001' -> 'claude-haiku-4-5' */ +function toAlias(id: string): string { + return id.replace(/-20\d{6}$/, '') +} + +/** Normalize for comparison (OpenRouter-style dotted IDs vs dashed IDs) */ +function normalizeId(id: string): string { + return id.replace(/[.]/g, '-') +} + +function toConstName(alias: string): string { + return alias.replace(/[-.:/]/g, '_').toUpperCase() +} + +function formatNumber(n: number): string { + if (n < 1000) return String(n) + const str = String(n) + const parts: Array = [] + let remaining = str + while (remaining.length > 3) { + parts.unshift(remaining.slice(-3)) + remaining = remaining.slice(0, -3) + } + parts.unshift(remaining) + return parts.join('_') +} + +function extractExistingModelIds(content: string): Set { + const ids = new Set() + const nameRegex = /^\s+name:\s*'([^']+)'/gm + const idRegex = /^\s+id:\s*'([^']+)'/gm + let match + while ((match = nameRegex.exec(content)) !== null) { + ids.add(normalizeId(match[1]!)) + } + while ((match = idRegex.exec(content)) !== null) { + ids.add(normalizeId(match[1]!)) + } + return ids +} + +function insertConstants(content: string, constants: Array): string { + const block = '\n' + constants.join('\n\n') + '\n' + const exportIndex = content.indexOf('\nexport ') + if (exportIndex === -1) { + return content + block + } + return content.slice(0, exportIndex) + block + content.slice(exportIndex) +} + +function addToArray( + content: string, + arrayName: string, + entries: Array, +): string { + const pattern = new RegExp( + `(export const ${arrayName} = \\[\\s*[\\s\\S]*?)(\\] as const)`, + ) + const match = pattern.exec(content) + if (!match) { + console.warn(` Warning: Could not find array '${arrayName}' in file`) + return content + } + const newEntries = entries.map((constName) => ` ${constName}.id,`).join('\n') + return content.replace( + pattern, + () => `${match[1]}\n${newEntries}\n${match[2]}`, + ) +} + +function addToTypeMap( + content: string, + typeName: string, + entries: Array, +): string { + const pattern = new RegExp( + `(export type ${typeName} = \\{[\\s\\S]*?)(\\n\\})`, + ) + const match = pattern.exec(content) + if (!match) { + console.warn(` Warning: Could not find type map '${typeName}' in file`) + return content + } + const newEntries = entries.join('\n') + return content.replace(pattern, () => `${match[1]}\n${newEntries}${match[2]}`) +} + +// --------------------------------------------------------------------------- +// Model constant generation +// --------------------------------------------------------------------------- + +const PROVIDER_OPTIONS_TYPE = + 'AnthropicContainerOptions & AnthropicContextManagementOptions & AnthropicMCPOptions & AnthropicServiceTierOptions & AnthropicStopSequencesOptions & AnthropicThinkingOptions & AnthropicToolChoiceOptions & AnthropicSamplingOptions' + +/** + * All current Claude chat models support the full server-tool set; the + * Models API does not enumerate server tools, so this stays a template + * (same as the previous OpenRouter-based sync). + */ +const TOOLS_TEMPLATE = ` tools: ['web_search', 'web_fetch', 'code_execution', 'computer_use', 'bash', 'text_editor', 'memory'],` + +function generateModelConstant( + model: AnthropicApiModel, + alias: string, +): string { + const constName = toConstName(alias) + const pricing = PRICING[alias] + if (!pricing) { + throw new Error( + `No pricing entry for new model '${alias}'. The Anthropic Models API does not expose pricing — add a row to the PRICING table in scripts/sync-anthropic-models.ts (see https://platform.claude.com/docs/en/pricing) and re-run.`, + ) + } + + const caps = model.capabilities ?? {} + const imageInput = caps.image_input?.supported ?? true + // The Models API has no separate document/PDF flag today; every current + // vision-capable Claude model also accepts PDF documents. + const documentInput = + caps.document_input?.supported ?? caps.pdf_input?.supported ?? imageInput + const extendedThinking = + caps.thinking?.types?.enabled?.supported ?? + caps.thinking?.supported ?? + false + const adaptiveThinking = caps.thinking?.types?.adaptive?.supported ?? false + const priorityTier = caps.priority_tier?.supported ?? true + + const inputModalities = [ + 'text', + ...(imageInput ? ['image'] : []), + ...(documentInput ? ['document'] : []), + ] + const inputStr = inputModalities.map((m) => `'${m}'`).join(', ') + + const lines: Array = [] + lines.push(`const ${constName} = {`) + lines.push(` name: '${alias}',`) + lines.push(` id: '${alias}',`) + if (model.max_input_tokens) { + lines.push(` context_window: ${formatNumber(model.max_input_tokens)},`) + } + if (model.max_tokens) { + lines.push(` max_output_tokens: ${formatNumber(model.max_tokens)},`) + } + lines.push(` supports: {`) + lines.push(` input: [${inputStr}],`) + lines.push(` extended_thinking: ${extendedThinking},`) + if (adaptiveThinking) { + lines.push(` adaptive_thinking: true,`) + } + lines.push(` priority_tier: ${priorityTier},`) + lines.push(TOOLS_TEMPLATE) + lines.push(` },`) + lines.push(` pricing: {`) + lines.push(` input: {`) + lines.push(` normal: ${pricing.input},`) + if (pricing.cached !== undefined) { + lines.push(` cached: ${pricing.cached},`) + } + lines.push(` },`) + lines.push(` output: {`) + lines.push(` normal: ${pricing.output},`) + lines.push(` },`) + lines.push(` },`) + lines.push(`} as const satisfies ModelMeta<${PROVIDER_OPTIONS_TYPE}>`) + return lines.join('\n') +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function parseArgs() { + const args = process.argv.slice(2) + const get = (flag: string) => { + const i = args.indexOf(flag) + return i !== -1 ? args[i + 1] : undefined + } + return { + dryRun: args.includes('--dry-run'), + input: get('--input') ?? DEFAULT_INPUT, + meta: get('--meta') ?? DEFAULT_META, + } +} + +async function main() { + const { dryRun, input, meta } = parseArgs() + + let rawJson: string + try { + rawJson = await readFile(input, 'utf-8') + } catch { + console.error( + `Could not read ${input}. Run 'pnpm generate:anthropic-models:fetch' first (requires ANTHROPIC_API_KEY).`, + ) + process.exit(1) + } + const parsed = JSON.parse(rawJson) as + | { data: Array } + | Array + const apiModels = Array.isArray(parsed) ? parsed : parsed.data + + // Collapse dated IDs onto their alias and dedupe (preferring the entry + // whose ID already is the alias, since that carries the alias metadata). + const byAlias = new Map() + for (const model of apiModels) { + const alias = toAlias(model.id) + const existing = byAlias.get(alias) + if (!existing || model.id === alias) { + byAlias.set(alias, model) + } + } + console.log( + `Found ${apiModels.length} models in catalog (${byAlias.size} after alias dedupe)`, + ) + + let content = await readFile(meta, 'utf-8') + const existingIds = extractExistingModelIds(content) + + const newModels: Array<{ alias: string; model: AnthropicApiModel }> = [] + for (const [alias, model] of byAlias) { + if (!existingIds.has(normalizeId(alias))) { + newModels.push({ alias, model }) + } + } + + // Diagnostics: IDs in the meta file that the catalog doesn't know about. + // These are either retired models, models the snapshot predates, or + // artifacts of the old OpenRouter-slug sync (e.g. dotted IDs). + const aliasSet = new Set([...byAlias.keys()].map(normalizeId)) + const unknown = [...existingIds].filter((id) => !aliasSet.has(id)) + if (unknown.length > 0) { + console.log( + `\nNote: ${unknown.length} IDs in model-meta.ts are not in the catalog snapshot` + + ` (retired, stale snapshot, or invalid IDs — verify against the live API):`, + ) + for (const id of unknown.sort()) { + console.log(` - ${id}`) + } + } + + if (newModels.length === 0) { + console.log('\nNo new models to add.') + return + } + + console.log(`\nAdding ${newModels.length} new models:`) + for (const { alias } of newModels) { + console.log(` - ${alias} (${toConstName(alias)})`) + } + + const constants = newModels.map(({ alias, model }) => + generateModelConstant(model, alias), + ) + + if (dryRun) { + console.log('\n--- dry run: generated constants ---\n') + console.log(constants.join('\n\n')) + return + } + + const constNames = newModels.map(({ alias }) => toConstName(alias)) + content = insertConstants(content, constants) + content = addToArray(content, 'ANTHROPIC_MODELS', constNames) + content = addToTypeMap( + content, + 'AnthropicChatModelProviderOptionsByName', + constNames.map( + (constName) => ` [${constName}.id]: ${PROVIDER_OPTIONS_TYPE}`, + ), + ) + content = addToTypeMap( + content, + 'AnthropicModelInputModalitiesByName', + constNames.map( + (constName) => ` [${constName}.id]: typeof ${constName}.supports.input`, + ), + ) + content = addToTypeMap( + content, + 'AnthropicChatModelToolCapabilitiesByName', + constNames.map( + (constName) => ` [${constName}.id]: typeof ${constName}.supports.tools`, + ), + ) + await writeFile(meta, content, 'utf-8') + console.log(`\nWrote updated file: ${meta}`) + + const changeset = `--- +'@tanstack/ai-anthropic': patch +--- + +Add new Anthropic models from the Models API: ${newModels + .map(({ alias }) => `\`${alias}\``) + .join(', ')} +` + await writeFile(CHANGESET_FILE, changeset, 'utf-8') + console.log(`Changeset created: ${CHANGESET_FILE}`) +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error) + process.exit(1) +}) diff --git a/scripts/sync-provider-models.ts b/scripts/sync-provider-models.ts index fdda5c77e..db78d80fb 100644 --- a/scripts/sync-provider-models.ts +++ b/scripts/sync-provider-models.ts @@ -87,26 +87,11 @@ const PROVIDER_MAP: Record = { 'chatgpt-', // ChatGPT branded models ], }, - 'anthropic/': { - packageName: '@tanstack/ai-anthropic', - metaFile: resolve(ROOT, 'packages/ai-anthropic/src/model-meta.ts'), - arrayRef: '.id', - contextField: 'context_window', - chatArrayName: 'ANTHROPIC_MODELS', - providerOptionsTypeName: 'AnthropicChatModelProviderOptionsByName', - inputModalitiesTypeName: 'AnthropicModelInputModalitiesByName', - validInputModalities: ['text', 'image', 'audio', 'video', 'document'], - referenceSupportsBody: ` extended_thinking: true, - priority_tier: true, - tools: ['web_search', 'web_fetch', 'code_execution', 'computer_use', 'bash', 'text_editor', 'memory'],`, - referenceSatisfies: - 'ModelMeta', - referenceProviderOptionsEntry: - 'AnthropicContainerOptions & AnthropicContextManagementOptions & AnthropicMCPOptions & AnthropicServiceTierOptions & AnthropicStopSequencesOptions & AnthropicThinkingOptions & AnthropicToolChoiceOptions & AnthropicSamplingOptions', - hasBothNameAndId: true, - providerOptionsIsMappedType: false, - skipPatterns: [], - }, + // NOTE: 'anthropic/' is intentionally absent. The Anthropic adapter is + // synced from the first-party Models API instead (authoritative model IDs + // and capabilities; OpenRouter slugs like 'claude-opus-4.8' are not valid + // Anthropic API model IDs). See scripts/sync-anthropic-models.ts and the + // 'generate:anthropic-models' script. 'google/': { packageName: '@tanstack/ai-gemini', metaFile: resolve(ROOT, 'packages/ai-gemini/src/model-meta.ts'),