From f5e915af914dcf870dd167ce7b50bf9813412912 Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:47:08 +1100 Subject: [PATCH 1/2] fix(models): remove model ids that providers have retired Each removed id 404s on the provider's model API (or is listed as shut down on its deprecations page) and is absent from modelschemas and OpenRouter. Code paths, types, docs, skills, examples, and tests that only served those models go with them. --- .changeset/remove-retired-models.md | 23 + docs/adapters/anthropic-advanced.md | 4 +- docs/adapters/anthropic.md | 9 +- docs/adapters/gemini-advanced.md | 79 +--- docs/adapters/gemini.md | 11 +- docs/adapters/groq.md | 13 +- docs/adapters/mistral.md | 14 +- docs/adapters/openai-advanced.md | 29 +- docs/adapters/openai-compatible-advanced.md | 2 +- docs/adapters/openai.md | 9 +- docs/advanced/multimodal-content.md | 2 +- docs/community-adapters/guide.md | 2 - docs/media/image-generation-advanced.md | 92 +--- docs/media/image-generation.md | 20 +- docs/media/text-to-speech.md | 8 +- .../migration-from-vercel-ai-advanced.md | 2 +- docs/reference/functions/defineAgent.md | 2 +- docs/reference/functions/generateImage.md | 16 +- docs/reference/interfaces/ImageAdapter.md | 2 +- docs/tools/provider-tools.md | 2 +- .../src/lib/mcp-provider-adapters.ts | 2 +- .../ts-react-chat/src/lib/mcp-providers.ts | 2 +- .../ts-react-chat/src/lib/model-selection.ts | 16 +- .../src/routes/api.structured-output.ts | 5 +- .../src/routes/api.subagent-brief.ts | 4 +- .../routes/generations.structured-output.tsx | 14 +- examples/ts-react-media/src/lib/models.ts | 24 -- .../src/lib/server-functions.ts | 27 -- .../app/actions/chat/controller.ts | 2 +- .../ts-svelte-chat/src/lib/model-selection.ts | 2 +- .../ts-vue-chat/src/lib/model-selection.ts | 2 +- knip.json | 1 - packages/ai-anthropic/src/adapters/text.ts | 7 +- packages/ai-anthropic/src/model-meta.ts | 119 +----- .../src/tools/computer-use-tool.ts | 2 +- .../tests/anthropic-adapter.test.ts | 136 +++--- .../tests/computer-use-generic.test.ts | 22 +- .../ai-anthropic/tests/files-source.test.ts | 10 +- .../ai-anthropic/tests/model-meta.test.ts | 21 +- .../ai-anthropic/tests/prompt-cache.test.ts | 2 +- .../tests/thinking-replay.test.ts | 2 +- .../tests/tool-result-multimodal.test.ts | 2 +- .../tests/tools-per-model-type-safety.test.ts | 2 +- .../tests/usage-extraction.test.ts | 28 +- .../ai-anthropic/tests/vertex-factory.test.ts | 2 +- .../ai-code-mode/models-eval/eval-config.ts | 4 +- packages/ai-code-mode/models-eval/run-eval.ts | 6 +- packages/ai-gemini/src/adapters/image.ts | 188 +------- .../src/image/image-provider-options.ts | 321 +------------- packages/ai-gemini/src/index.ts | 10 - packages/ai-gemini/src/model-meta.ts | 72 ---- .../ai-gemini/tests/audio-adapter.test.ts | 2 +- .../ai-gemini/tests/image-adapter.test.ts | 401 +----------------- .../tests/image-per-model-type-safety.test.ts | 82 +--- packages/ai-groq/README.md | 14 +- packages/ai-groq/src/adapters/summarize.ts | 8 +- packages/ai-groq/src/adapters/text.ts | 4 +- packages/ai-groq/src/model-meta.ts | 198 +-------- packages/ai-groq/tests/groq-adapter.test.ts | 125 +++--- .../ai-groq/tests/input-modalities.test.ts | 4 +- .../ai-groq/tests/reasoning-request.test.ts | 6 +- .../ai-groq/tests/reasoning-types.test-d.ts | 2 +- .../ai-groq/tests/tool-choice-wire.test.ts | 2 +- .../ai-groq/tests/wrap-fetch-wire.test.ts | 2 +- packages/ai-mistral/README.md | 5 +- .../ai-mistral/src/input-modalities.test.ts | 2 +- packages/ai-mistral/src/model-meta.ts | 62 +-- packages/ai-openai/src/adapters/azure-text.ts | 4 +- packages/ai-openai/src/adapters/image.ts | 46 +- packages/ai-openai/src/adapters/tts.ts | 2 +- .../src/image/image-provider-options.ts | 130 +----- packages/ai-openai/src/model-meta.ts | 292 +------------ .../ai-openai/src/tools/computer-use-tool.ts | 2 +- .../tests/chat-per-model-type-safety.test.ts | 14 +- .../tests/computer-tool-kind.test.ts | 4 - .../tests/copilot-chatgpt-wire.test.ts | 27 +- .../ai-openai/tests/image-adapter.test.ts | 104 +---- packages/ai-openai/tests/model-meta.test.ts | 129 ------ .../tests/model-sampling-support.test.ts | 3 - .../ai-openai/tests/openai-adapter.test.ts | 8 +- packages/ai-openai/tests/prompt-cache.test.ts | 7 +- .../tests/tools-per-model-type-safety.test.ts | 46 +- .../ai-core/adapter-configuration/SKILL.md | 2 +- .../references/anthropic-adapter.md | 8 +- .../references/gemini-adapter.md | 12 +- .../references/groq-adapter.md | 30 +- .../references/openai-adapter.md | 24 +- .../ai/skills/ai-core/ag-ui-protocol/SKILL.md | 10 +- .../skills/ai-core/chat-experience/SKILL.md | 36 +- packages/ai/skills/ai-core/locks/SKILL.md | 6 +- .../skills/ai-core/media-generation/SKILL.md | 91 ++-- .../activities/chat/agents/define-agent.ts | 2 +- .../src/activities/generateImage/adapter.ts | 2 +- .../ai/src/activities/generateImage/index.ts | 16 +- .../ai/tests/summarize-max-length.test.ts | 4 +- .../src/adapters/responses-text.ts | 16 +- .../src/adapters/responses-user-tools.ts | 20 +- .../src/tools/computer-use-tool.ts | 51 +-- .../tests/computer-use-generic.test.ts | 33 +- .../tests/computer-use-loop.test.ts | 16 +- .../tests/provider-tool-dispatch.test.ts | 17 - scripts/sync-provider-models.ts | 4 +- testing/e2e/global-setup.ts | 73 +--- testing/e2e/src/lib/feature-support.ts | 3 +- testing/e2e/src/lib/providers.ts | 4 +- testing/e2e/src/routeTree.gen.ts | 22 - .../routes/api.anthropic-structured-usage.ts | 97 ----- .../api.chat-completions-incomplete-stream.ts | 4 +- testing/e2e/src/routes/api.copilot-wire.ts | 12 +- .../src/routes/api.gemini-image-ga-models.ts | 5 +- .../routes/api.gemini-native-image-wire.ts | 2 +- .../routes/api.openai-computer-use-wire.ts | 66 +-- .../api.structured-output-length-wire.ts | 35 +- testing/e2e/src/routes/api.summarize.ts | 2 +- .../tests/anthropic-structured-usage.spec.ts | 59 --- .../e2e/tests/gemini-image-ga-models.spec.ts | 2 +- .../tests/openai-computer-use-wire.spec.ts | 55 +-- .../structured-output-length-wire.spec.ts | 1 - testing/panel/src/routes/tts.tsx | 4 +- .../files/src/lib/model-selection.ts__tmpl__ | 16 +- 120 files changed, 607 insertions(+), 3390 deletions(-) create mode 100644 .changeset/remove-retired-models.md delete mode 100644 testing/e2e/src/routes/api.anthropic-structured-usage.ts delete mode 100644 testing/e2e/tests/anthropic-structured-usage.spec.ts diff --git a/.changeset/remove-retired-models.md b/.changeset/remove-retired-models.md new file mode 100644 index 0000000000..10994686ef --- /dev/null +++ b/.changeset/remove-retired-models.md @@ -0,0 +1,23 @@ +--- +'@tanstack/ai': patch +'@tanstack/ai-anthropic': patch +'@tanstack/ai-gemini': patch +'@tanstack/ai-groq': patch +'@tanstack/ai-mistral': patch +'@tanstack/ai-openai': patch +'@tanstack/openai-base': patch +--- + +Remove model ids that providers have retired. Each one now fails on the provider's API, so the types no longer accept it. + +- Groq: `llama-3.3-70b-versatile`, `llama-3.1-8b-instant`, `meta-llama/llama-4-maverick-17b-128e-instruct`, `meta-llama/llama-4-scout-17b-16e-instruct`, `meta-llama/llama-guard-4-12b`, `moonshotai/kimi-k2-instruct-0905`, `qwen/qwen3-32b`. Adds `qwen/qwen3.8-27b`. +- Mistral: `pixtral-large-latest`, `pixtral-12b-2409`, `open-mistral-nemo`. +- OpenAI: `chatgpt-4o-latest`, `codex-mini-latest`, `gpt-4o-audio`, `gpt-4o-mini-audio`, `gpt-5.6`, `dall-e-2`, `dall-e-3`, `computer-use-preview`. The speech model `gpt-4o-audio-preview` is replaced by `gpt-4o-mini-tts`. +- Anthropic: `claude-opus-4-1`, `claude-opus-5-fast`. +- Gemini: `imagen-4.0-generate-001`, `imagen-4.0-ultra-generate-001`, `imagen-4.0-fast-generate-001`. + +Types and helpers that only served those models are removed: + +- `@tanstack/ai-gemini`: `GeminiImageProviderOptions`, `GeminiAnyImageProviderOptions`, `GeminiAspectRatio`, `PersonGeneration`, `SafetyFilterLevel`, `ImagePromptLanguage`, `GEMINI_NATIVE_IMAGE_MODELS`, `isGeminiNativeImageModel`. Every Gemini image model uses `GeminiNativeImageProviderOptions`. +- `@tanstack/openai-base`: `OpenAIComputerToolName`. `resolveComputerUseTool(tool)` no longer takes a model. +- `@tanstack/ai-openai`: the dall-e option and size types. diff --git a/docs/adapters/anthropic-advanced.md b/docs/adapters/anthropic-advanced.md index 598b4652ab..f47fd085e6 100644 --- a/docs/adapters/anthropic-advanced.md +++ b/docs/adapters/anthropic-advanced.md @@ -132,7 +132,7 @@ const stream = chat({ }); ``` -- `anthropicVertexText` accepts only the Claude models in the Vertex catalog (`ANTHROPIC_VERTEX_CHAT_MODELS`). It does not accept ids that only Anthropic has, such as `claude-opus-5-fast`. +- `anthropicVertexText` accepts only the Claude models in the Vertex catalog (`ANTHROPIC_VERTEX_CHAT_MODELS`). It does not accept ids that only Anthropic has, such as `claude-fable-5-1`. - `project` and `location` use the same names as `@tanstack/ai-vertex`, so one auth object works for Gemini and Claude. - If you omit `project`, Application Default Credentials can still fill it. - `location` is required. Pass it on the factory, or set `GOOGLE_CLOUD_LOCATION`, `GOOGLE_VERTEX_LOCATION`, or `CLOUD_ML_REGION`. @@ -432,7 +432,7 @@ Add an `execute` that runs the action and returns a result: Claude gives coordinates in the pixels of your screenshots. -A version that your model does not take is a type error. `claude-opus-5-fast` takes no provider tools. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). +A version that your model does not take is a type error. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). ### `bashTool` diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index 2c944b079d..cf5b6d4ccb 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -237,10 +237,9 @@ The levels of each model: | --- | --- | --- | | `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-fable-5-1`, `claude-fable-5`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7` | `low` to `max` | no | | `claude-haiku-5-5`, `claude-sonnet-5` | `off`, `low` to `max` | no | -| `claude-opus-5-fast` | `off`, `low` to `max` | yes | | `claude-opus-4-6`, `claude-sonnet-4-6` | `low`, `medium`, `high`, `max` | yes | | `claude-opus-4-5` | `low`, `medium`, `high` | yes | -| `claude-haiku-4-5`, `claude-sonnet-4-5`, `claude-opus-4-1` | `off` to `high` | yes | +| `claude-haiku-4-5`, `claude-sonnet-4-5` | `off` to `high` | yes | A budget model with `budgetTokens`: @@ -256,7 +255,7 @@ const stream = chat({ }); ``` -Structured output forces a tool, and Claude cannot think while a tool is forced. So the structured output call sends no thinking. See [Reasoning](../chat/reasoning) for the levels and the nearest-level rule. +With an `outputSchema`, every model in `ANTHROPIC_MODELS` uses native structured output, so thinking stays on. A direct `structuredOutput()` call, or a model id that is not in that list, forces a tool. Claude cannot think while a tool is forced, so that call sends no thinking. See [Reasoning](../chat/reasoning) for the levels and the nearest-level rule. ### Models @@ -265,8 +264,8 @@ Newest first. The package exports the full list as `ANTHROPIC_MODELS`. | Family | Models | | --- | --- | | Claude 5.5 | `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-haiku-5-5` | -| Claude 5 | `claude-fable-5-1`, `claude-fable-5`, `claude-opus-5`, `claude-opus-5-fast`, `claude-sonnet-5` | -| Claude 4 | `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5`, `claude-opus-4-1` | +| Claude 5 | `claude-fable-5-1`, `claude-fable-5`, `claude-opus-5`, `claude-sonnet-5` | +| Claude 4 | `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-sonnet-4-5`, `claude-haiku-4-5` | `adapter.inputModalities` lists the input kinds of the selected model. See [Check what a model accepts](../advanced/extend-adapter#check-what-a-model-accepts). diff --git a/docs/adapters/gemini-advanced.md b/docs/adapters/gemini-advanced.md index 7edc6d5f3f..03e79e7ad3 100644 --- a/docs/adapters/gemini-advanced.md +++ b/docs/adapters/gemini-advanced.md @@ -8,7 +8,6 @@ keywords: - gemini - interactions api - nano banana - - imagen - provider tools - adapter --- @@ -288,12 +287,7 @@ Tools and output: ## Images -`geminiImage` picks the API from the model: - -- A model in `GEMINI_NATIVE_IMAGE_MODELS` uses the Interactions API. `result.id` is the interaction id. -- An Imagen model, or an unknown id, uses `generateImages`. - -Import the list or `isGeminiNativeImageModel` from `@tanstack/ai-gemini`. +`geminiImage` uses the Interactions API. `result.id` is the interaction id. ### Edit a Gemini native image @@ -331,33 +325,14 @@ Keep the interaction, so that the next edit can run: Also: -- If the model is Imagen, do not pass `previous_interaction_id`. The call throws. - If this edit needs a new reference image, add that image part on this call. Do not send the image from `first`. - The Interactions API is in Beta. The same steps are in [Image Generation](../media/image-generation#edit-a-gemini-native-image). -### Imagen - -```typescript -import { generateImage } from "@tanstack/ai"; -import { geminiImage } from "@tanstack/ai-gemini"; - -const result = await generateImage({ - adapter: geminiImage("imagen-4.0-generate-001"), - prompt: "A futuristic cityscape at sunset", - threadId: "thread-1", - numberOfImages: 1, -}); - -console.log(result.images); -``` - ### Image sizes -#### Gemini native models (Nano Banana) - -The size of a native model joins an aspect ratio and a resolution tier: +The size of a Gemini image model joins an aspect ratio and a resolution tier: ```typescript ignore // Format: "aspectRatio_resolution" @@ -382,57 +357,9 @@ Each model has its own set. TypeScript narrows `size` to the set of the model: > [!NOTE] > For `gemini-3.1-flash-lite-image`, the four banner ratios (`1:4`, `4:1`, `1:8`, `8:1`) are partly inferred. The Gemini API guide has no ratio table for this model. The set of 14 comes from the Cloud model page and from the statement in the guide of "a discrete set of 14 aspect ratios". If the API rejects those four, use the 10 standard ratios on this model. -#### Imagen models - -Imagen models take WIDTHxHEIGHT. The adapter maps the size to an aspect ratio: - -| Size | Aspect ratio | -|------|-------------| -| `1024x1024` | 1:1 | -| `1920x1080` | 16:9 | -| `1080x1920` | 9:16 | - -You can also set the aspect ratio in `modelOptions`: - -```typescript -import { generateImage } from "@tanstack/ai"; -import { geminiImage } from "@tanstack/ai-gemini"; - -const result = await generateImage({ - adapter: geminiImage("imagen-4.0-generate-001"), - prompt: "A landscape photo", - threadId: "thread-1", - modelOptions: { - aspectRatio: "16:9", - }, -}); -``` - ### Image model options -The two model groups call different APIs, so `modelOptions` has a different type for each. - -Imagen models (`generateImages`) take `GenerateImagesConfig` fields: - -```typescript ignore -import { generateImage } from "@tanstack/ai"; -import { geminiImage } from "@tanstack/ai-gemini"; - -const result = await generateImage({ - adapter: geminiImage("imagen-4.0-generate-001"), - prompt: "...", - threadId: "thread-1", - modelOptions: { - aspectRatio: "16:9", // "1:1" | "3:4" | "4:3" | "9:16" | "16:9" - personGeneration: "DONT_ALLOW", // Control person generation - safetyFilterLevel: "BLOCK_SOME", // Safety filtering - }, -}); -``` - -#### Gemini native models - -Set native image options on `modelOptions`: +Set image options on `modelOptions`: ```typescript import { generateImage } from "@tanstack/ai"; diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index 5d427750b6..99ad110d52 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -2,12 +2,11 @@ title: Google Gemini id: gemini-adapter order: 3 -description: "Use Google Gemini in TanStack AI with @tanstack/ai-gemini: multimodal chat with thinking, native and Imagen images, speech, embeddings, video understanding, and Google tools." +description: "Use Google Gemini in TanStack AI with @tanstack/ai-gemini: multimodal chat with thinking, native image generation, speech, embeddings, video understanding, and Google tools." keywords: - tanstack ai - gemini - google gemini - - imagen - nano banana - image generation - adapter @@ -119,10 +118,7 @@ You now have a streaming Gemini chat. [Tools](../tools/tools) and [Structured Ou ## Images -`geminiImage` takes two model groups: - -- **Gemini native image models** (Nano Banana), such as `gemini-nano-banana-2.1`. You set an aspect ratio and a resolution tier in `size`, for example `"16:9_4K"`. You can edit the last image with a short prompt. -- **Imagen models**, such as `imagen-4.0-generate-001`. You set `size` as WIDTHxHEIGHT. +`geminiImage` takes the Gemini native image models (Nano Banana), such as `gemini-nano-banana-2.1`. Set an aspect ratio and a resolution tier in `size`, for example `"16:9_4K"`. You can edit the last image with a short prompt. ```typescript import { generateImage } from "@tanstack/ai"; @@ -354,8 +350,7 @@ Newest first. The package exports the full lists, for example `GeminiTextModels` | Activity | Factory | Models | | --- | --- | --- | | Chat | `geminiText` | `gemini-3.8-flash`, `gemini-3.7-flash`, `gemini-3.6-flash`, `gemini-3.5-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview`, `gemini-3-flash-preview`, `gemini-3.1-flash-lite`, `gemini-3.1-flash-lite-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite` | -| Images (native) | `geminiImage` | `gemini-nano-banana-2.1`, `gemini-3.1-flash-image`, `gemini-3.1-flash-lite-image`, `gemini-3-pro-image` | -| Images (Imagen) | `geminiImage` | `imagen-4.0-ultra-generate-001`, `imagen-4.0-generate-001`, `imagen-4.0-fast-generate-001` | +| Images | `geminiImage` | `gemini-nano-banana-2.1`, `gemini-3.1-flash-image`, `gemini-3.1-flash-lite-image`, `gemini-3-pro-image` | | Speech | `geminiSpeech` | `gemini-3.1-flash-tts-preview`, `gemini-2.5-flash-preview-tts`, `gemini-2.5-pro-preview-tts` | | Music | `geminiAudio` | `lyria-3-pro-preview`, `lyria-3-clip-preview` | | Video | `geminiVideo` | `veo-3.1-generate-preview`, `veo-3.1-fast-generate-preview`, `veo-3.1-lite-generate-preview`, `gemini-omni-1.1-flash` | diff --git a/docs/adapters/groq.md b/docs/adapters/groq.md index ecb1586dfb..37e776ff52 100644 --- a/docs/adapters/groq.md +++ b/docs/adapters/groq.md @@ -174,7 +174,7 @@ import { summarize } from "@tanstack/ai"; import { groqSummarize } from "@tanstack/ai-groq"; const result = await summarize({ - adapter: groqSummarize("llama-3.3-70b-versatile"), + adapter: groqSummarize("openai/gpt-oss-120b"), text: "Your long text to summarize...", threadId: "thread-1", maxLength: 100, @@ -199,7 +199,7 @@ import { chat } from "@tanstack/ai"; import { groqText } from "@tanstack/ai-groq"; const stream = chat({ - adapter: groqText("llama-3.3-70b-versatile"), + adapter: groqText("openai/gpt-oss-120b"), messages: [{ role: "user", content: "Hello!" }], threadId: "thread-1", modelOptions: { @@ -231,7 +231,7 @@ const stream = chat({ It goes out as `reasoning_effort`: - `openai/gpt-oss-120b`, `openai/gpt-oss-20b`, `openai/gpt-oss-safeguard-20b`: `low`, `medium`, `high`. -- `qwen/qwen3-32b`: `off` (sent as `none`) and `high` (sent as `default`). +- `qwen/qwen3.8-27b`: `off` (sent as `none`), `low`, `medium`, `high`. To pick how the thinking text comes back, set `reasoning_format` in `modelOptions`. @@ -242,9 +242,8 @@ The package exports the full lists as `GROQ_CHAT_MODELS`, `GROQ_TRANSCRIPTION_MO | Group | Models | | --- | --- | | OpenAI gpt-oss | `openai/gpt-oss-120b` (reasoning), `openai/gpt-oss-20b`, `openai/gpt-oss-safeguard-20b` (safety-tuned) | -| Meta Llama | `meta-llama/llama-4-maverick-17b-128e-instruct` (vision), `meta-llama/llama-4-scout-17b-16e-instruct`, `llama-3.3-70b-versatile` (128K context), `llama-3.1-8b-instant` | -| Other | `qwen/qwen3-32b` (reasoning), `moonshotai/kimi-k2-instruct-0905` (256K context) | -| Safety | `meta-llama/llama-guard-4-12b` (content moderation), `meta-llama/llama-prompt-guard-2-86m` and `meta-llama/llama-prompt-guard-2-22m` (prompt injection detection) | +| Qwen | `qwen/qwen3.8-27b` (reasoning, vision) | +| Safety | `meta-llama/llama-prompt-guard-2-86m` and `meta-llama/llama-prompt-guard-2-22m` (prompt injection detection) | `adapter.inputModalities` lists the input kinds of the selected model. See [Check what a model accepts](../advanced/extend-adapter#check-what-a-model-accepts). @@ -270,7 +269,7 @@ Groq can reject a tool call from the model with `tool_use_failed`. If the error ### Structured output -Some Groq models, for example `llama-3.3-70b-versatile`, reject strict JSON schema. For these models, `chat({ outputSchema })` sends `json_object` and adds your schema to the system prompt. Read [When strict JSON schema is rejected](../structured-outputs/overview.md#when-strict-json-schema-is-rejected). +Some Groq models, for example `meta-llama/llama-prompt-guard-2-86m`, support JSON mode but not strict JSON schema. For these models, `chat({ outputSchema })` sends `json_object` and adds your schema to the system prompt. Read [When strict JSON schema is rejected](../structured-outputs/overview.md#when-strict-json-schema-is-rejected). ### API reference diff --git a/docs/adapters/mistral.md b/docs/adapters/mistral.md index 088ee8c7f0..c61a057bec 100644 --- a/docs/adapters/mistral.md +++ b/docs/adapters/mistral.md @@ -2,19 +2,18 @@ title: Mistral id: mistral-adapter order: 7 -description: "Use Mistral models in TanStack AI with @tanstack/ai-mistral: Mistral Large and Medium, Pixtral vision, Magistral reasoning, Codestral, and Mistral embeddings." +description: "Use Mistral models in TanStack AI with @tanstack/ai-mistral: Mistral Large and Medium with vision, Magistral reasoning, Codestral, and Mistral embeddings." keywords: - tanstack ai - mistral - mistral large - - pixtral - magistral - codestral - adapter - llm --- -The Mistral adapter calls Mistral Large, Pixtral, Magistral, and Codestral. It also creates embeddings. +The Mistral adapter calls Mistral Large, Mistral Medium, Magistral, and Codestral. It also creates embeddings. ## 1. Install @@ -113,14 +112,14 @@ You now have a streaming Mistral chat. [Tools](../tools/tools) work the same as ## Images in a message -Send an image with the text to a vision model: `pixtral-large-latest`, `pixtral-12b-2409`, `mistral-medium-latest`, or `mistral-small-latest`. +Send an image with the text to a vision model: `mistral-medium-latest` or `mistral-small-latest`. ```typescript import { chat } from "@tanstack/ai"; import { mistralText } from "@tanstack/ai-mistral"; const stream = chat({ - adapter: mistralText("pixtral-large-latest"), + adapter: mistralText("mistral-medium-latest"), messages: [ { role: "user", @@ -149,7 +148,7 @@ import { mistralText } from "@tanstack/ai-mistral"; const base64String = "..."; // your base64-encoded image bytes const stream = chat({ - adapter: mistralText("pixtral-large-latest"), + adapter: mistralText("mistral-medium-latest"), messages: [ { role: "user", @@ -293,9 +292,8 @@ The other models take no `reasoning`. The Magistral models stream their thinking | Group | Models | | --- | --- | -| Chat | `mistral-large-latest` (flagship, 128k context), `mistral-medium-latest` (with vision), `mistral-small-latest` (fast, with vision), `ministral-8b-latest`, `ministral-3b-latest`, `open-mistral-nemo` | +| Chat | `mistral-large-latest` (flagship, 128k context), `mistral-medium-latest` (with vision), `mistral-small-latest` (fast, with vision), `ministral-8b-latest`, `ministral-3b-latest` | | Code | `codestral-latest` (256k context) | -| Vision | `pixtral-large-latest`, `pixtral-12b-2409` | | Reasoning | `magistral-medium-latest`, `magistral-small-latest` | | Embeddings | `mistral-embed`, `codestral-embed` | | Chat on Vertex | `mistral-medium-3`, `mistral-small-2503`, `codestral-2` | diff --git a/docs/adapters/openai-advanced.md b/docs/adapters/openai-advanced.md index 600f8397df..bc523ef842 100644 --- a/docs/adapters/openai-advanced.md +++ b/docs/adapters/openai-advanced.md @@ -73,7 +73,7 @@ export async function POST(request: Request) { const { messages, threadId } = await request.json(); const stream = chat({ - adapter: createAzureOpenaiText("gpt-5.6", process.env.AZURE_OPENAI_API_KEY!, { + adapter: createAzureOpenaiText("gpt-6.1-sol", process.env.AZURE_OPENAI_API_KEY!, { resourceName: "my-resource", deploymentName: "production-chat", }), @@ -85,9 +85,9 @@ export async function POST(request: Request) { } ``` -- Azure gets `production-chat` as the model. Your code still uses `gpt-5.6`. +- Azure gets `production-chat` as the model. Your code still uses `gpt-6.1-sol`. - The client does not change. It reads this route like any other chat route. -- `reasoning` takes the levels of the OpenAI model name, here `gpt-5.6`. A model name that is not an OpenAI model, such as a bare deployment name, takes no `reasoning`. See [Reasoning](./openai#reasoning). +- `reasoning` takes the levels of the OpenAI model name, here `gpt-6.1-sol`. A model name that is not an OpenAI model, such as a bare deployment name, takes no `reasoning`. See [Reasoning](./openai#reasoning). - `createAzureOpenaiText` reads nothing from the environment. You pass the key, and you pass the endpoint as `resourceName` or `baseURL`. To keep all of it in the environment, use `azureOpenaiText`. Like `openaiText`, it reads the key from the environment: @@ -95,13 +95,13 @@ To keep all of it in the environment, use `azureOpenaiText`. Like `openaiText`, ```sh AZURE_OPENAI_API_KEY=your-key AZURE_OPENAI_RESOURCE_NAME=my-resource -AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-5.6=production-chat +AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-6.1-sol=production-chat ``` ```typescript import { azureOpenaiText } from "@tanstack/ai-openai"; -const adapter = azureOpenaiText("gpt-5.6"); +const adapter = azureOpenaiText("gpt-6.1-sol"); ``` `azureOpenaiText` always reads the key from `AZURE_OPENAI_API_KEY`. If that variable is not set, it throws. For the other values, the config wins over the environment: @@ -510,7 +510,7 @@ const stream = chat({ ### `imageGenerationTool` -The model makes images during a conversation with DALL-E or GPT-Image. Set quality, size, and style in the config object. +The model makes images during a conversation with GPT-Image. Set quality, size, and style in the config object. ```typescript import { chat } from "@tanstack/ai"; @@ -626,20 +626,7 @@ Add an `execute` that runs the actions and returns a screenshot as an image part The adapter sends the first image of the result back as the screenshot. When the call does not fail, the adapter also tells OpenAI that the safety checks are accepted. When the call fails, they stay unaccepted. -The `computer-use-preview` model takes the older tool type. Give it the display size and the environment. Each call then has one `action` field in place of `actions`: - -```typescript -import { computerUseTool } from "@tanstack/ai-openai/tools"; - -const previewTool = computerUseTool({ - type: "computer_use_preview", - display_width: 1024, - display_height: 768, - environment: "browser", -}); -``` - -**Supported models:** `type: "computer"` works with the models that list computer use, for example `gpt-6.1-sol`. `type: "computer_use_preview"` works with `computer-use-preview`. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). +**Supported models:** `type: "computer"` works with the models that list computer use, for example `gpt-6.1-sol`. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). ### `localShellTool` @@ -846,7 +833,7 @@ Each factory pair has the same shape. The short factory (`openaiText`, `openaiIm | `openaiChatCompletions(model, config?)` / `createOpenaiChatCompletions(model, apiKey, config?)` | Text adapter on `/v1/chat/completions`. See [Chat Completions API](#chat-completions-api) | | `azureOpenaiText(model, config?)` / `createAzureOpenaiText(model, apiKey, config)` | Text adapter on Azure. See [Azure OpenAI](#azure-openai) | | `openaiSummarize(model, config?)` / `createOpenaiSummarize(model, apiKey, config?)` | Summarization adapter | -| `openaiImage(model, config?)` / `createOpenaiImage(model, apiKey, config?)` | Image adapter (DALL-E, gpt-image) | +| `openaiImage(model, config?)` / `createOpenaiImage(model, apiKey, config?)` | Image adapter (gpt-image) | | `openaiSpeech(model, config?)` / `createOpenaiSpeech(model, apiKey, config?)` | Text-to-speech adapter | | `openaiTranscription(model, config?)` / `createOpenaiTranscription(model, apiKey, config?)` | Transcription adapter (Whisper, GPT-4o transcription, and GPT-4o diarized transcription) | | `openaiEmbedding(model, config?)` / `createOpenaiEmbedding(model, apiKey, config?)` | Embedding adapter | diff --git a/docs/adapters/openai-compatible-advanced.md b/docs/adapters/openai-compatible-advanced.md index a79f1988e6..8d5acff58a 100644 --- a/docs/adapters/openai-compatible-advanced.md +++ b/docs/adapters/openai-compatible-advanced.md @@ -63,7 +63,7 @@ Use `createAzureOpenaiText` from `@tanstack/ai-openai`. It sets the `api-key` he ```typescript import { createAzureOpenaiText } from "@tanstack/ai-openai"; -const adapter = createAzureOpenaiText("gpt-5.6", process.env.AZURE_OPENAI_API_KEY!, { +const adapter = createAzureOpenaiText("gpt-6.1-sol", process.env.AZURE_OPENAI_API_KEY!, { resourceName: "my-resource", deploymentName: "production-chat", }); diff --git a/docs/adapters/openai.md b/docs/adapters/openai.md index a94b0c29c8..c47d225060 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -8,7 +8,7 @@ keywords: - openai - gpt-4o - gpt-5 - - dall-e + - gpt-image - whisper - openai tts - adapter @@ -346,7 +346,6 @@ const stream = chat({ - OpenAI rejects `temperature` and `top_p` on the reasoning models. `openaiText` drops the two options for these model ids: - The `o` models (`o1`, `o3`, `o4-mini`, and so on). - Every id that starts with `gpt-5`, except ids that end in `-chat-latest`. - - `codex-mini-latest`. - With `openaiChatCompletions`, the token limit is `max_tokens`. - Sampling options do not go at the root of `chat()`. See [Moving Sampling Options into modelOptions](../migration/sampling-options-to-model-options). @@ -394,9 +393,9 @@ Newest first. The package exports the full lists, for example `OPENAI_CHAT_MODEL | Activity | Factory | Models | | --- | --- | --- | -| Chat | `openaiText` | `gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6`, `gpt-5.5`, `gpt-4.1`, `gpt-4o`, the `o` models, and more | -| Images | `openaiImage` | `gpt-image-2.5-flare`, `gpt-image-2.5-sunburst`, `gpt-image-2`, `gpt-image-1`, `gpt-image-1-mini`, `dall-e-3`, `dall-e-2` | -| Speech | `openaiSpeech` | `tts-1`, `tts-1-hd`, `gpt-4o-audio-preview` | +| Chat | `openaiText` | `gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, the `gpt-5.6` models, `gpt-5.5`, `gpt-4.1`, `gpt-4o`, the `o` models, and more | +| Images | `openaiImage` | `gpt-image-2.5-flare`, `gpt-image-2.5-sunburst`, `gpt-image-2`, `gpt-image-1`, `gpt-image-1-mini` | +| Speech | `openaiSpeech` | `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts` | | Transcription | `openaiTranscription` | `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `gpt-4o-transcribe-diarize` | | Embeddings | `openaiEmbedding` | `text-embedding-3-small`, `text-embedding-3-large` | | Video | `openaiVideo` | `sora-2`, `sora-2-pro` | diff --git a/docs/advanced/multimodal-content.md b/docs/advanced/multimodal-content.md index 4a95b60afd..f8dae1a6fb 100644 --- a/docs/advanced/multimodal-content.md +++ b/docs/advanced/multimodal-content.md @@ -330,7 +330,7 @@ Some more rules: | Provider | Adapter | Input besides text | | --- | --- | --- | -| OpenAI | `openaiText` | Image on `gpt-6.1-sol` and most new models. Image and PDF document on `gpt-5.5`. Audio on `gpt-4o-audio`. | +| OpenAI | `openaiText` | Image on `gpt-6.1-sol` and most new models. Image and PDF document on `gpt-5.5`. Audio on `gpt-audio`. | | Anthropic | `anthropicText` | Image and PDF document on all Claude models, for example `claude-sonnet-5-5`. | | Gemini | `geminiText` | Image, audio, video, and document, for example on `gemini-3.8-flash`. | | Ollama | `ollamaText` | Image on vision models, such as `llama3.2-vision`. Support changes by model. | diff --git a/docs/community-adapters/guide.md b/docs/community-adapters/guide.md index a45ef53620..4eb4ccca9d 100644 --- a/docs/community-adapters/guide.md +++ b/docs/community-adapters/guide.md @@ -254,8 +254,6 @@ export const OPENAI_CHAT_MODELS = [ export const OPENAI_IMAGE_MODELS = [ GPT_IMAGE_1.name, GPT_IMAGE_1_MINI.name, - DALL_E_3.name, - DALL_E_2.name, ] as const export const OPENAI_VIDEO_MODELS = [SORA2.name, SORA2_PRO.name] as const diff --git a/docs/media/image-generation-advanced.md b/docs/media/image-generation-advanced.md index d213a404a2..51c4a12ceb 100644 --- a/docs/media/image-generation-advanced.md +++ b/docs/media/image-generation-advanced.md @@ -30,8 +30,6 @@ The image page uses a streaming route. To call a TanStack Start server function | `gpt-image-2` | `1024x1024`, `1536x1024`, `1024x1536`, `auto` | | `gpt-image-1` | `1024x1024`, `1536x1024`, `1024x1536`, `auto` | | `gpt-image-1-mini` | `1024x1024`, `1536x1024`, `1024x1536`, `auto` | -| `dall-e-3` | `1024x1024`, `1792x1024`, `1024x1792` | -| `dall-e-2` | `256x256`, `512x512`, `1024x1024` | ### Gemini native models (Nano Banana) @@ -59,32 +57,6 @@ Rules for the size string: > [!NOTE] > For `gemini-3.1-flash-lite-image`, Google lists the four banner ratios (`1:4`, `4:1`, `1:8`, `8:1`) on the Cloud model page, but not in the Gemini API guide. If the API rejects them, use the 10 standard ratios on this model. -### Gemini Imagen models - -Imagen takes `WIDTHxHEIGHT`. The adapter maps it to an aspect ratio: - -| Size | Aspect ratio | -| --- | --- | -| `1024x1024` | 1:1 | -| `1920x1080` | 16:9 | -| `1080x1920` | 9:16 | - -You can also set the aspect ratio directly: - -```typescript -import { generateImage } from '@tanstack/ai' -import { geminiImage } from '@tanstack/ai-gemini' - -const result = await generateImage({ - adapter: geminiImage('imagen-4.0-generate-001'), - prompt: 'A landscape photo', - threadId: 'thread-1', - modelOptions: { - aspectRatio: '16:9', - }, -}) -``` - ## Model options `modelOptions` is typed for each model. @@ -110,44 +82,6 @@ const result = await generateImage({ `gpt-image-2.5-flare` and `gpt-image-2.5-sunburst` also take `quality: 'xhigh'` and `quality: 'max'`. -### OpenAI DALL-E 3 - -```typescript -import { generateImage } from '@tanstack/ai' -import { openaiImage } from '@tanstack/ai-openai' - -const result = await generateImage({ - adapter: openaiImage('dall-e-3'), - prompt: 'A futuristic car', - threadId: 'thread-1', - modelOptions: { - quality: 'hd', // 'hd' | 'standard' - style: 'vivid', // 'vivid' | 'natural' - }, -}) -``` - -### Gemini Imagen - -```typescript ignore -import { generateImage } from '@tanstack/ai' -import { geminiImage } from '@tanstack/ai-gemini' - -const result = await generateImage({ - adapter: geminiImage('imagen-4.0-generate-001'), - prompt: 'A beautiful garden', - threadId: 'thread-1', - modelOptions: { - aspectRatio: '16:9', - // PersonGeneration enum values: 'DONT_ALLOW' | 'ALLOW_ADULT' | 'ALLOW_ALL' - personGeneration: 'ALLOW_ADULT', - negativePrompt: 'blurry, low quality', - addWatermark: true, - outputMimeType: 'image/png', // 'image/png' | 'image/jpeg' | 'image/webp' - }, -}) -``` - ### Gemini native models (Nano Banana) ```typescript @@ -261,7 +195,7 @@ When the inputs have different jobs (a mask, a reference, a start frame), set `m | --- | --- | | `'reference'` | fal `reference_image_urls`. Gemini multimodal part. Position when the provider has no reference field. | | `'character'` | Same as `'reference'`. Veo `referenceImages` slot (planned, no Veo adapter yet). | -| `'mask'` | OpenAI `mask` (gpt-image models and dall-e-2). fal `mask_url`. | +| `'mask'` | OpenAI `mask` (gpt-image models). fal `mask_url`. | | `'control'` | fal `control_image_url` (ControlNet, depth, or pose conditioning) | | `'start_frame'` | fal `start_image_url`. Veo `image` (planned). Used by `generateVideo`. | | `'end_frame'` | fal `end_image_url`. Veo `lastFrame` (planned). Used by `generateVideo`. | @@ -338,8 +272,8 @@ await generateImage({ | Provider | Behavior | | --- | --- | -| **OpenAI** | gpt-image models use `images.edit()`, with up to 16 source images and an optional mask. `dall-e-2` uses `images.edit()` with 1 source image only. `dall-e-3` throws (no edit support). | -| **Gemini** | Native models send each prompt part as an Interactions content block, in the same order. Up to 14 input images. The SDK does not enforce that limit. To edit the last image, see [Edit a Gemini native image](./image-generation#edit-a-gemini-native-image). Imagen throws on an image part, and does not chain edits. | +| **OpenAI** | gpt-image models use `images.edit()`, with up to 16 source images and an optional mask. | +| **Gemini** | Native models send each prompt part as an Interactions content block, in the same order. Up to 14 input images. The SDK does not enforce that limit. To edit the last image, see [Edit a Gemini native image](./image-generation#edit-a-gemini-native-image). | | **fal.ai** | Field names come from a map built from the endpoint types of the fal SDK (for example, nano-banana edit gets `image_urls`, and Fooocus masks get `mask_image_url`). For unknown endpoints: 1 input goes to `image_url`, more go to `image_urls`, `'mask'` goes to `mask_url`, `'control'` goes to `control_image_url`, and `'reference'` or `'character'` go to `reference_image_urls`. Use `modelOptions` for fields that only one endpoint has. | | **Grok** | grok-imagine models use the xAI `/v1/images/edits` endpoint, with up to 3 source images in request order. The prompt is sent as written. `'mask'` and `'control'` throw (the Imagine API has no match). | | **OpenRouter** | Prompt parts map 1:1 to multimodal `image_url` and `text` parts, in the same order, and go to the image model. `modelOptions.strength` (0.0 to 1.0) sets the image-to-image influence on models that document it (for example Recraft). One image per request: `numberOfImages` more than 1 throws. | @@ -405,8 +339,6 @@ if (result.usage?.billed) { | `gpt-image-2` | 1-10 | | `gpt-image-1` | 1-10 | | `gpt-image-1-mini` | 1-10 | -| `dall-e-3` | 1 | -| `dall-e-2` | 1-10 | ### Gemini native models (Nano Banana) @@ -417,14 +349,6 @@ if (result.usage?.billed) { | `gemini-3.1-flash-lite-image` | Nano Banana 2 Lite. Very low latency and low cost. | | `gemini-3-pro-image` | Nano Banana Pro. Higher quality. | -### Gemini Imagen - -| Model | Images per request | -| --- | --- | -| `imagen-4.0-ultra-generate-001` | 1-4 | -| `imagen-4.0-generate-001` | 1-4 | -| `imagen-4.0-fast-generate-001` | 1-4 | - ## Errors The adapters check the input before they call the API. An invalid size throws before the request: @@ -435,16 +359,16 @@ import { openaiImage } from '@tanstack/ai-openai' try { const result = await generateImage({ - adapter: openaiImage('dall-e-3'), + adapter: openaiImage('gpt-image-2.5-flare'), prompt: 'A cat', threadId: 'thread-1', - size: '512x512', // not a dall-e-3 size, throws at runtime + size: '512x512', // not a gpt-image-2.5-flare size, throws at runtime }) } catch (error) { if (error instanceof Error) { console.error(error.message) - // "Size "512x512" is not supported by model "dall-e-3". - // Supported sizes: 1024x1024, 1792x1024, 1024x1792" + // "Size "512x512" is not supported by model "gpt-image-2.5-flare". + // Supported sizes: 1024x1024, 1536x1024, 1024x1536, auto" } } ``` @@ -454,7 +378,7 @@ try { The image adapters read the same environment variables as the text adapters: - **OpenAI**: `OPENAI_API_KEY` -- **Gemini** (Nano Banana and Imagen): `GOOGLE_API_KEY` or `GEMINI_API_KEY` +- **Gemini** (Nano Banana): `GOOGLE_API_KEY` or `GEMINI_API_KEY` - **BytePlus** (Seedream): `ARK_API_KEY` or `BYTEPLUS_API_KEY` To pass a key in code, use the `create*` factory: diff --git a/docs/media/image-generation.md b/docs/media/image-generation.md index b7580e9a4d..dbbe3c4c78 100644 --- a/docs/media/image-generation.md +++ b/docs/media/image-generation.md @@ -7,8 +7,6 @@ keywords: - tanstack ai - image generation - generateImage - - dall-e - - imagen - nano banana - flux - fal.ai @@ -154,29 +152,20 @@ Anthropic has no image generation API. ### Gemini -Gemini has two groups of image models: - -- **Native models (Nano Banana)**, such as `gemini-nano-banana-2.1`. They use the Interactions API. `size` is `"_"`. -- **Imagen models**, such as `imagen-4.0-generate-001`. They use the `generateImages` API. `size` is `WIDTHxHEIGHT`. +Gemini image models (Nano Banana), such as `gemini-nano-banana-2.1`, use the Interactions API. `size` is `"_"`. ```typescript import { generateImage } from '@tanstack/ai' import { geminiImage } from '@tanstack/ai-gemini' -const native = await generateImage({ +const result = await generateImage({ adapter: geminiImage('gemini-nano-banana-2.1'), prompt: 'A futuristic cityscape at night', threadId: 'thread-1', size: '16:9_4K', }) -const imagen = await generateImage({ - adapter: geminiImage('imagen-4.0-generate-001'), - prompt: 'A futuristic cityscape at night', - threadId: 'thread-1', -}) - -console.log(native.images[0]?.b64Json) +console.log(result.images[0]?.b64Json) ``` ### BytePlus @@ -241,7 +230,6 @@ Rules for the edit: - Leave `store` unset. The API default is `true`. If you set `store` to `false`, a later call cannot use that id. - A free tier keeps a stored interaction for 1 day. A paid tier keeps it for 55 days. - If the edit needs a new reference image, add that image part on this call. Do not send the image from `first` again. -- If the model is Imagen, do not pass `previous_interaction_id`. The call throws. The Interactions API is in Beta. @@ -269,7 +257,7 @@ await generateImage({ What to know: - **Part order matters.** Gemini native models and OpenRouter get the parts in the same order, so text can refer to the image next to it. OpenAI, fal, and xAI take the image parts out and join the text parts into one prompt. -- **Types check the model.** An image part on a text-only model (for example `dall-e-3` or Imagen) is a type error. +- **Types check the model.** An image part on a model with no image input is a type error. - **Unsupported adapters throw.** An adapter that cannot take image inputs throws a runtime error. It does not drop the images. Masks, roles, URL rules, and per-provider limits are in [Image Generation: Advanced](./image-generation-advanced#role-hints-with-metadatarole). diff --git a/docs/media/text-to-speech.md b/docs/media/text-to-speech.md index da246dad3e..eec45cb579 100644 --- a/docs/media/text-to-speech.md +++ b/docs/media/text-to-speech.md @@ -107,7 +107,7 @@ Swap the adapter to change the provider. The rest of the route stays the same. | Provider | Adapter | Models | API key | | --- | --- | --- | --- | -| OpenAI | `openaiSpeech` from `@tanstack/ai-openai` | `tts-1-hd`, `tts-1`, `gpt-4o-audio-preview` | `OPENAI_API_KEY` | +| OpenAI | `openaiSpeech` from `@tanstack/ai-openai` | `tts-1-hd`, `tts-1`, `gpt-4o-mini-tts` | `OPENAI_API_KEY` | | Gemini (experimental) | `geminiSpeech` from `@tanstack/ai-gemini` | `gemini-3.1-flash-tts-preview`, `gemini-2.5-flash-preview-tts`, `gemini-2.5-pro-preview-tts` | `GOOGLE_API_KEY` or `GEMINI_API_KEY` | | ElevenLabs | `elevenlabsSpeech` from `@tanstack/ai-elevenlabs` | `eleven_v4`, `eleven_v4_turbo`, `eleven_v3`, and more | `ELEVENLABS_API_KEY` | | BytePlus | `byteplusSpeech` from `@tanstack/ai-byteplus` | `seed-audio-1.0` | `BYTEPLUS_VOICE_API_KEY` | @@ -341,18 +341,18 @@ OpenAI models: | --- | --- | --- | --- | | `tts-1` | Standard | Fast | Realtime apps | | `tts-1-hd` | High | Slower | Production audio | -| `gpt-4o-audio-preview` | Highest | Changes | Voice style control | +| `gpt-4o-mini-tts` | Highest | Changes | Voice style control | `voice`, `format`, and `speed` are top-level options of `generateSpeech()`. They are not `modelOptions` keys. `speed` goes from 0.25 to 4.0. -`modelOptions.instructions` sets the speaking style. Only `gpt-4o-audio-preview` accepts it. On `tts-1` and `tts-1-hd`, the call throws. +`modelOptions.instructions` sets the speaking style. Only `gpt-4o-mini-tts` accepts it. On `tts-1` and `tts-1-hd`, the call throws. ```typescript import { generateSpeech } from '@tanstack/ai' import { openaiSpeech } from '@tanstack/ai-openai' const result = await generateSpeech({ - adapter: openaiSpeech('gpt-4o-audio-preview'), + adapter: openaiSpeech('gpt-4o-mini-tts'), text: 'High quality speech synthesis', threadId: 'thread-1', voice: 'nova', diff --git a/docs/migration/migration-from-vercel-ai-advanced.md b/docs/migration/migration-from-vercel-ai-advanced.md index 293b0a9ee0..0b477b4892 100644 --- a/docs/migration/migration-from-vercel-ai-advanced.md +++ b/docs/migration/migration-from-vercel-ai-advanced.md @@ -734,7 +734,7 @@ Each provider package exports one adapter function for each activity. | --- | --- | | `openai('gpt-4o')` in `streamText` | `openaiText('gpt-4o')` in `chat` | | `openai.embedding('text-embedding-3-small')` in `embed` | `openaiEmbedding('text-embedding-3-small')` in `embed` | -| `openai.image('dall-e-3')` in `generateImage` | `openaiImage('dall-e-3')` in `generateImage` | +| `openai.image('gpt-image-2')` in `generateImage` | `openaiImage('gpt-image-2')` in `generateImage` | | `anthropic('')` | `anthropicText('')` | | `google('')` | `geminiText('')` | diff --git a/docs/reference/functions/defineAgent.md b/docs/reference/functions/defineAgent.md index 18d6c0ce86..eda7ec595c 100644 --- a/docs/reference/functions/defineAgent.md +++ b/docs/reference/functions/defineAgent.md @@ -61,7 +61,7 @@ const researcher = defineAgent({ description: 'Looks up facts', run: (ctx) => chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages: ctx.messages, threadId: ctx.threadId, runId: ctx.runId, diff --git a/docs/reference/functions/generateImage.md b/docs/reference/functions/generateImage.md index 0d26b7c0ae..3f4a566e87 100644 --- a/docs/reference/functions/generateImage.md +++ b/docs/reference/functions/generateImage.md @@ -42,27 +42,27 @@ import { generateImage } from '@tanstack/ai' import { openaiImage } from '@tanstack/ai-openai' const result = await generateImage({ - adapter: openaiImage('dall-e-3'), + adapter: openaiImage('gpt-image-2.5-flare'), prompt: 'A serene mountain landscape at sunset', threadId, }) -console.log(result.images[0].url) +console.log(result.images[0].b64Json) ``` **Generate multiple images** ```ts const result = await generateImage({ - adapter: openaiImage('dall-e-2'), + adapter: openaiImage('gpt-image-1-mini'), prompt: 'A cute robot mascot', threadId, numberOfImages: 4, - size: '512x512' + size: '1024x1024' }) result.images.forEach((image, i) => { - console.log(`Image ${i + 1}: ${image.url}`) + console.log(`Image ${i + 1}: ${image.b64Json}`) }) ``` @@ -70,13 +70,13 @@ result.images.forEach((image, i) => { ```ts const result = await generateImage({ - adapter: openaiImage('dall-e-3'), + adapter: openaiImage('gpt-image-2.5-flare'), prompt: 'A professional headshot photo', threadId, size: '1024x1024', modelOptions: { - quality: 'hd', - style: 'natural' + quality: 'high', + background: 'transparent' } }) ``` diff --git a/docs/reference/interfaces/ImageAdapter.md b/docs/reference/interfaces/ImageAdapter.md index a3e4bcfa19..7470e7ef5b 100644 --- a/docs/reference/interfaces/ImageAdapter.md +++ b/docs/reference/interfaces/ImageAdapter.md @@ -11,7 +11,7 @@ An adapter is created by a provider function: `provider('model')` → `adapter` All type resolution happens at the provider call site, not in this interface. Generic parameters: -- TModel: The specific model name (e.g., 'dall-e-3') +- TModel: The specific model name (e.g., 'gpt-image-2.5-flare') - TProviderOptions: Base provider-specific options (already resolved) - TModelProviderOptionsByName: Map from model name to its specific provider options - TModelSizeByName: Map from model name to its supported sizes diff --git a/docs/tools/provider-tools.md b/docs/tools/provider-tools.md index 62a76683b8..9d71aa1e51 100644 --- a/docs/tools/provider-tools.md +++ b/docs/tools/provider-tools.md @@ -84,7 +84,7 @@ Anthropic and OpenAI can also load hosted skills into their code sandbox. See [P The `supports.tools` array of each model in `model-meta.ts` is the source of truth: -- **Anthropic**: every registered model supports all tools, except `claude-opus-5-fast`, which supports none. +- **Anthropic**: every registered model supports all tools. - For `computerUseTool`, each model takes only some versions. The Claude 5.5 models take only `computer_toolset_20260801`. See the version table in [`computerUseTool`](../adapters/anthropic-advanced.md#computerusetool). - **OpenAI**: - GPT-6.1 Sol, the GPT-5 family, and the reasoning models (O-series) support all tools. diff --git a/examples/ts-react-chat/src/lib/mcp-provider-adapters.ts b/examples/ts-react-chat/src/lib/mcp-provider-adapters.ts index 4174ee6b46..b252ec7582 100644 --- a/examples/ts-react-chat/src/lib/mcp-provider-adapters.ts +++ b/examples/ts-react-chat/src/lib/mcp-provider-adapters.ts @@ -25,7 +25,7 @@ export function resolveTextAdapter(provider: unknown) { case 'gemini': return geminiText('gemini-3.5-flash') case 'groq': - return groqText('llama-3.3-70b-versatile') + return groqText('openai/gpt-oss-120b') case 'openai': default: return openaiText('gpt-5.5') diff --git a/examples/ts-react-chat/src/lib/mcp-providers.ts b/examples/ts-react-chat/src/lib/mcp-providers.ts index 34aa9391c4..7cecce7f9f 100644 --- a/examples/ts-react-chat/src/lib/mcp-providers.ts +++ b/examples/ts-react-chat/src/lib/mcp-providers.ts @@ -40,7 +40,7 @@ export const MCP_PROVIDERS = [ { value: 'groq', label: 'Groq', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', envKey: 'GROQ_API_KEY', }, ] as const diff --git a/examples/ts-react-chat/src/lib/model-selection.ts b/examples/ts-react-chat/src/lib/model-selection.ts index 6d29640959..bf175dce10 100644 --- a/examples/ts-react-chat/src/lib/model-selection.ts +++ b/examples/ts-react-chat/src/lib/model-selection.ts @@ -20,7 +20,7 @@ export interface ModelOption { export const MODEL_OPTIONS: Array = [ // OpenAI. GPT-6 Astra is first because ChatGPT plan sign-in supports it. { provider: 'openai', model: 'gpt-6-astra', label: 'OpenAI - GPT-6 Astra' }, - { provider: 'openai', model: 'gpt-5.6', label: 'OpenAI - GPT-5.6' }, + { provider: 'openai', model: 'gpt-6.1-sol', label: 'OpenAI - GPT-6.1 Sol' }, { provider: 'openai', model: 'gpt-5.6-sol', label: 'OpenAI - GPT-5.6 Sol' }, { provider: 'openai', @@ -249,18 +249,8 @@ export const MODEL_OPTIONS: Array = [ }, { provider: 'groq', - model: 'moonshotai/kimi-k2-instruct-0905', - label: 'Groq - Kimi K2 Instruct', - }, - { - provider: 'groq', - model: 'meta-llama/llama-4-maverick-17b-128e-instruct', - label: 'Groq - Llama 4 Maverick', - }, - { - provider: 'groq', - model: 'qwen/qwen3-32b', - label: 'Groq - Qwen3 32B', + model: 'qwen/qwen3.8-27b', + label: 'Groq - Qwen3.8 27B', }, // Grok diff --git a/examples/ts-react-chat/src/routes/api.structured-output.ts b/examples/ts-react-chat/src/routes/api.structured-output.ts index 15761bb17b..457727e32e 100644 --- a/examples/ts-react-chat/src/routes/api.structured-output.ts +++ b/examples/ts-react-chat/src/routes/api.structured-output.ts @@ -170,10 +170,7 @@ function adapterFor(provider: Provider, model?: string): AnyTextAdapter { case 'grok': return grokText((model || 'grok-build-0.1') as 'grok-build-0.1') case 'groq': - return groqText( - (model || - 'meta-llama/llama-4-maverick-17b-128e-instruct') as 'meta-llama/llama-4-maverick-17b-128e-instruct', - ) + return groqText((model || 'qwen/qwen3.8-27b') as 'qwen/qwen3.8-27b') case 'openrouter': return openRouterText( (model || 'anthropic/claude-opus-4.6') as 'anthropic/claude-opus-4.6', diff --git a/examples/ts-react-chat/src/routes/api.subagent-brief.ts b/examples/ts-react-chat/src/routes/api.subagent-brief.ts index 3175a98cf6..60082817ea 100644 --- a/examples/ts-react-chat/src/routes/api.subagent-brief.ts +++ b/examples/ts-react-chat/src/routes/api.subagent-brief.ts @@ -22,7 +22,7 @@ const researcher = defineAgent({ ctx.messages.length, ) return chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages: [{ role: 'user', content: ctx.input.task }], threadId: ctx.threadId, runId: ctx.runId, @@ -40,7 +40,7 @@ export const Route = createFileRoute('/api/subagent-brief')({ const abortController = new AbortController() const params = await chatParamsFromRequestBody(await request.json()) const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages: params.messages, threadId: params.threadId, runId: params.runId, diff --git a/examples/ts-react-chat/src/routes/generations.structured-output.tsx b/examples/ts-react-chat/src/routes/generations.structured-output.tsx index cd6ea59dfe..4b09cb501f 100644 --- a/examples/ts-react-chat/src/routes/generations.structured-output.tsx +++ b/examples/ts-react-chat/src/routes/generations.structured-output.tsx @@ -100,19 +100,7 @@ const PROVIDER_MODELS: Record< { value: 'grok-4.3', label: 'Grok 4.3' }, ], groq: [ - { - value: 'meta-llama/llama-4-maverick-17b-128e-instruct', - label: 'Llama 4 Maverick 17B', - }, - { - value: 'meta-llama/llama-4-scout-17b-16e-instruct', - label: 'Llama 4 Scout 17B', - }, - { - value: 'moonshotai/kimi-k2-instruct-0905', - label: 'Kimi K2 Instruct', - }, - { value: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70B Versatile' }, + { value: 'qwen/qwen3.8-27b', label: 'Qwen3.8 27B' }, { value: 'openai/gpt-oss-120b', label: 'GPT-OSS 120B' }, ], openrouter: [ diff --git a/examples/ts-react-media/src/lib/models.ts b/examples/ts-react-media/src/lib/models.ts index 9f3d70b6db..5daced21ec 100644 --- a/examples/ts-react-media/src/lib/models.ts +++ b/examples/ts-react-media/src/lib/models.ts @@ -85,30 +85,6 @@ export const IMAGE_MODELS = [ sizeType: 'native' as const, provider: 'gemini' as const, }, - { - id: 'imagen-4.0-ultra-generate-001', - name: 'Imagen 4.0 Ultra', - description: 'Best quality Imagen image generation', - defaultSize: '1024x1024' as const, - sizeType: 'standard' as const, - provider: 'gemini' as const, - }, - { - id: 'imagen-4.0-generate-001', - name: 'Imagen 4.0', - description: 'High quality Imagen image generation', - defaultSize: '1024x1024' as const, - sizeType: 'standard' as const, - provider: 'gemini' as const, - }, - { - id: 'imagen-4.0-fast-generate-001', - name: 'Imagen 4.0 Fast', - description: 'Fast Imagen image generation', - defaultSize: '1024x1024' as const, - sizeType: 'standard' as const, - provider: 'gemini' as const, - }, { id: 'dola-seedream-5-0-pro-260628', name: 'Seedream 5.0 Pro', diff --git a/examples/ts-react-media/src/lib/server-functions.ts b/examples/ts-react-media/src/lib/server-functions.ts index a51ad2a30b..3558b39c7c 100644 --- a/examples/ts-react-media/src/lib/server-functions.ts +++ b/examples/ts-react-media/src/lib/server-functions.ts @@ -382,33 +382,6 @@ export const generateImageFn = createServerFn({ method: 'POST' }) size: '16:9_4K', }) } - case 'imagen-4.0-ultra-generate-001': { - return generateImage({ - threadId: 'media-image', - adapter: geminiI('imagen-4.0-ultra-generate-001'), - prompt: asTextPrompt(data.prompt), - numberOfImages: 1, - size: '1024x1024', - }) - } - case 'imagen-4.0-generate-001': { - return generateImage({ - threadId: 'media-image', - adapter: geminiI('imagen-4.0-generate-001'), - prompt: asTextPrompt(data.prompt), - numberOfImages: 1, - size: '1024x1024', - }) - } - case 'imagen-4.0-fast-generate-001': { - return generateImage({ - threadId: 'media-image', - adapter: geminiI('imagen-4.0-fast-generate-001'), - prompt: asTextPrompt(data.prompt), - numberOfImages: 1, - size: '1024x1024', - }) - } case 'dola-seedream-5-0-pro-260628': { // BytePlus Seedream via ModelArk (ARK_API_KEY). `size` is either a // 1K/2K/4K token or an explicit WIDTHxHEIGHT string — never both. diff --git a/examples/ts-remix-chat/app/actions/chat/controller.ts b/examples/ts-remix-chat/app/actions/chat/controller.ts index 7c8aa2786a..85ba900373 100644 --- a/examples/ts-remix-chat/app/actions/chat/controller.ts +++ b/examples/ts-remix-chat/app/actions/chat/controller.ts @@ -45,7 +45,7 @@ export default createController(routes.chat, { try { const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), tools: mergedTools, systemPrompts: [SYSTEM_PROMPT], agentLoopStrategy: maxIterations(20), diff --git a/examples/ts-svelte-chat/src/lib/model-selection.ts b/examples/ts-svelte-chat/src/lib/model-selection.ts index 1887dd44ce..b8c58ffd7f 100644 --- a/examples/ts-svelte-chat/src/lib/model-selection.ts +++ b/examples/ts-svelte-chat/src/lib/model-selection.ts @@ -8,7 +8,7 @@ export interface ModelOption { export const MODEL_OPTIONS: Array = [ // OpenAI - { provider: 'openai', model: 'gpt-5.6', label: 'OpenAI - GPT-5.6' }, + { provider: 'openai', model: 'gpt-6.1-sol', label: 'OpenAI - GPT-6.1 Sol' }, { provider: 'openai', model: 'gpt-5.6-sol', label: 'OpenAI - GPT-5.6 Sol' }, { provider: 'openai', diff --git a/examples/ts-vue-chat/src/lib/model-selection.ts b/examples/ts-vue-chat/src/lib/model-selection.ts index 1887dd44ce..b8c58ffd7f 100644 --- a/examples/ts-vue-chat/src/lib/model-selection.ts +++ b/examples/ts-vue-chat/src/lib/model-selection.ts @@ -8,7 +8,7 @@ export interface ModelOption { export const MODEL_OPTIONS: Array = [ // OpenAI - { provider: 'openai', model: 'gpt-5.6', label: 'OpenAI - GPT-5.6' }, + { provider: 'openai', model: 'gpt-6.1-sol', label: 'OpenAI - GPT-6.1 Sol' }, { provider: 'openai', model: 'gpt-5.6-sol', label: 'OpenAI - GPT-5.6 Sol' }, { provider: 'openai', diff --git a/knip.json b/knip.json index bcaf1c98b0..245927bcb8 100644 --- a/knip.json +++ b/knip.json @@ -22,7 +22,6 @@ "packages/ai-openai/live-tests/**", "packages/ai-openai/src/**/*.test.ts", "packages/ai-openai/src/audio/audio-provider-options.ts", - "packages/ai-openai/src/image/image-provider-options.ts", "packages/ai-devtools/src/production.ts", "codemods/**/__testfixtures__/**" ], diff --git a/packages/ai-anthropic/src/adapters/text.ts b/packages/ai-anthropic/src/adapters/text.ts index f84db4ad42..234dbff8ff 100644 --- a/packages/ai-anthropic/src/adapters/text.ts +++ b/packages/ai-anthropic/src/adapters/text.ts @@ -1117,9 +1117,10 @@ export class AnthropicTextAdapter< /** * Anthropic supports `output_config.format` + `tools` in a single streaming - * Messages request only for Claude 4.5+ (GA 2026-01-29). For 4.4 and - * earlier we keep the forced-tool-use workaround in - * {@link structuredOutput} via the engine's finalization path. + * Messages request only for Claude 4.5+ (GA 2026-01-29). Every registered + * model is 4.5+; an id outside the set (e.g. an untyped one) keeps the + * forced-tool-use workaround in {@link structuredOutput} via the engine's + * finalization path. */ supportsCombinedToolsAndSchema(): boolean { return ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(this.model) diff --git a/packages/ai-anthropic/src/model-meta.ts b/packages/ai-anthropic/src/model-meta.ts index 748d798ab4..0795c24224 100644 --- a/packages/ai-anthropic/src/model-meta.ts +++ b/packages/ai-anthropic/src/model-meta.ts @@ -26,17 +26,6 @@ const BUDGET_LOW_TO_HIGH_MAX = { levels: { low: 'low', medium: 'medium', high: 'high', max: 'max' }, budget: true, } as const -const BUDGET_NONE_LOW_TO_MAX = { - levels: { - off: 'none', - low: 'low', - medium: 'medium', - high: 'high', - xhigh: 'xhigh', - max: 'max', - }, - budget: true, -} as const const BUDGET_OFF_TO_HIGH = { levels: { off: 'off', @@ -351,47 +340,6 @@ const CLAUDE_HAIKU_4_5 = { AnthropicSamplingOptions > -const CLAUDE_OPUS_4_1 = { - name: 'claude-opus-4-1', - id: 'claude-opus-4-1', - context_window: 200_000, - max_output_tokens: 64_000, - knowledge_cutoff: '2025-08-05', - pricing: { - input: { - normal: 15, - }, - output: { - normal: 75, - }, - }, - supports: { - input: ['text', 'image', 'document'], - extended_thinking: true, - priority_tier: true, - tools: [ - 'web_search', - 'web_fetch', - 'code_execution', - 'computer', - 'computer_use', - 'bash', - 'text_editor', - 'memory', - ], - }, - reasoning: BUDGET_OFF_TO_HIGH, -} as const satisfies ModelMeta< - AnthropicCacheControlOptions & - AnthropicContainerOptions & - AnthropicContextManagementOptions & - AnthropicMCPOptions & - AnthropicServiceTierOptions & - AnthropicStopSequencesOptions & - AnthropicToolChoiceOptions & - AnthropicSamplingOptions -> - // Claude Opus 4.7 removed budget-based extended thinking and the sampling // parameters (`temperature`, `top_p`, `top_k`) — sending either returns a // 400. Use adaptive thinking with `output_config.effort` instead. @@ -584,9 +532,9 @@ const CLAUDE_SONNET_5 = { * * Every id here resolves against the first-party Anthropic API * (`GET /v1/models/{id}`). Retired models (Claude 3.x, Sonnet 3.7, - * Opus 4 / Sonnet 4) and the `-fast` variant ids (fast mode is requested - * via the `speed` parameter, not a model id) were removed after Anthropic - * turned them off. + * Opus 4 / Sonnet 4, Opus 4.1) and the `-fast` variant ids (fast mode is + * requested via the `speed` parameter, not a model id) were removed after + * Anthropic turned them off. */ const CLAUDE_OPUS_5 = { name: 'claude-opus-5', @@ -628,35 +576,6 @@ const CLAUDE_OPUS_5 = { AnthropicSamplingOptions > -const CLAUDE_OPUS_5_FAST = { - name: 'claude-opus-5-fast', - id: 'claude-opus-5-fast', - context_window: 1_000_000, - max_output_tokens: 128_000, - supports: { - input: ['text', 'image', 'document'], - tools: [], - }, - pricing: { - input: { - normal: 10, - cached: 1, - }, - output: { - normal: 50, - }, - }, - reasoning: BUDGET_NONE_LOW_TO_MAX, -} as const satisfies ModelMeta< - AnthropicContainerOptions & - AnthropicContextManagementOptions & - AnthropicMCPOptions & - AnthropicServiceTierOptions & - AnthropicStopSequencesOptions & - AnthropicToolChoiceOptions & - AnthropicSamplingOptions -> - const CLAUDE_FABLE_5_1 = { name: 'claude-fable-5-1', id: 'claude-fable-5-1', @@ -839,13 +758,11 @@ export const ANTHROPIC_MODELS = [ CLAUDE_OPUS_5_5.id, CLAUDE_FABLE_5_1.id, CLAUDE_OPUS_5.id, - CLAUDE_OPUS_5_FAST.id, CLAUDE_OPUS_4_6.id, CLAUDE_OPUS_4_5.id, CLAUDE_SONNET_4_6.id, CLAUDE_SONNET_4_5.id, CLAUDE_HAIKU_4_5.id, - CLAUDE_OPUS_4_1.id, CLAUDE_OPUS_4_7.id, CLAUDE_OPUS_4_8.id, @@ -869,7 +786,6 @@ export const ANTHROPIC_VERTEX_CHAT_MODELS = [ CLAUDE_SONNET_4_6.id, CLAUDE_OPUS_4_5.id, CLAUDE_SONNET_4_5.id, - CLAUDE_OPUS_4_1.id, CLAUDE_HAIKU_4_5.id, ] as const @@ -906,13 +822,11 @@ const ANTHROPIC_MODEL_MAX_OUTPUT_TOKENS: Record = { [CLAUDE_SONNET_4_6.id]: CLAUDE_SONNET_4_6.max_output_tokens, [CLAUDE_SONNET_4_5.id]: CLAUDE_SONNET_4_5.max_output_tokens, [CLAUDE_HAIKU_4_5.id]: CLAUDE_HAIKU_4_5.max_output_tokens, - [CLAUDE_OPUS_4_1.id]: CLAUDE_OPUS_4_1.max_output_tokens, [CLAUDE_OPUS_4_7.id]: CLAUDE_OPUS_4_7.max_output_tokens, [CLAUDE_OPUS_4_8.id]: CLAUDE_OPUS_4_8.max_output_tokens, [CLAUDE_FABLE_5.id]: CLAUDE_FABLE_5.max_output_tokens, [CLAUDE_SONNET_5.id]: CLAUDE_SONNET_5.max_output_tokens, [CLAUDE_OPUS_5.id]: CLAUDE_OPUS_5.max_output_tokens, - [CLAUDE_OPUS_5_FAST.id]: CLAUDE_OPUS_5_FAST.max_output_tokens, [CLAUDE_FABLE_5_1.id]: CLAUDE_FABLE_5_1.max_output_tokens, [CLAUDE_OPUS_5_5.id]: CLAUDE_OPUS_5_5.max_output_tokens, [CLAUDE_SONNET_5_5.id]: CLAUDE_SONNET_5_5.max_output_tokens, @@ -1050,14 +964,6 @@ export type AnthropicChatModelProviderOptionsByName = { AnthropicStopSequencesOptions & AnthropicToolChoiceOptions & AnthropicSamplingOptions - [CLAUDE_OPUS_4_1.id]: AnthropicCacheControlOptions & - AnthropicContainerOptions & - AnthropicContextManagementOptions & - AnthropicMCPOptions & - AnthropicServiceTierOptions & - AnthropicStopSequencesOptions & - AnthropicToolChoiceOptions & - AnthropicSamplingOptions // Opus 4.7/4.8: adaptive thinking (or explicit disable), no // budget_tokens, no sampling parameters — see the constants above. @@ -1106,13 +1012,6 @@ export type AnthropicChatModelProviderOptionsByName = { AnthropicStopSequencesOptions & AnthropicToolChoiceOptions & AnthropicSamplingOptions - [CLAUDE_OPUS_5_FAST.id]: AnthropicContainerOptions & - AnthropicContextManagementOptions & - AnthropicMCPOptions & - AnthropicServiceTierOptions & - AnthropicStopSequencesOptions & - AnthropicToolChoiceOptions & - AnthropicSamplingOptions [CLAUDE_FABLE_5_1.id]: AnthropicCacheControlOptions & AnthropicContainerOptions & AnthropicContextManagementOptions & @@ -1157,13 +1056,11 @@ export type AnthropicChatModelToolCapabilitiesByName = { [CLAUDE_SONNET_4_6.id]: typeof CLAUDE_SONNET_4_6.supports.tools [CLAUDE_SONNET_4_5.id]: typeof CLAUDE_SONNET_4_5.supports.tools [CLAUDE_HAIKU_4_5.id]: typeof CLAUDE_HAIKU_4_5.supports.tools - [CLAUDE_OPUS_4_1.id]: typeof CLAUDE_OPUS_4_1.supports.tools [CLAUDE_OPUS_4_7.id]: typeof CLAUDE_OPUS_4_7.supports.tools [CLAUDE_OPUS_4_8.id]: typeof CLAUDE_OPUS_4_8.supports.tools [CLAUDE_FABLE_5.id]: typeof CLAUDE_FABLE_5.supports.tools [CLAUDE_SONNET_5.id]: typeof CLAUDE_SONNET_5.supports.tools [CLAUDE_OPUS_5.id]: typeof CLAUDE_OPUS_5.supports.tools - [CLAUDE_OPUS_5_FAST.id]: typeof CLAUDE_OPUS_5_FAST.supports.tools [CLAUDE_FABLE_5_1.id]: typeof CLAUDE_FABLE_5_1.supports.tools [CLAUDE_OPUS_5_5.id]: typeof CLAUDE_OPUS_5_5.supports.tools [CLAUDE_SONNET_5_5.id]: typeof CLAUDE_SONNET_5_5.supports.tools @@ -1183,13 +1080,11 @@ export const ANTHROPIC_MODEL_TOOLS: Readonly< [CLAUDE_SONNET_4_6.id]: CLAUDE_SONNET_4_6.supports.tools, [CLAUDE_SONNET_4_5.id]: CLAUDE_SONNET_4_5.supports.tools, [CLAUDE_HAIKU_4_5.id]: CLAUDE_HAIKU_4_5.supports.tools, - [CLAUDE_OPUS_4_1.id]: CLAUDE_OPUS_4_1.supports.tools, [CLAUDE_OPUS_4_7.id]: CLAUDE_OPUS_4_7.supports.tools, [CLAUDE_OPUS_4_8.id]: CLAUDE_OPUS_4_8.supports.tools, [CLAUDE_FABLE_5.id]: CLAUDE_FABLE_5.supports.tools, [CLAUDE_SONNET_5.id]: CLAUDE_SONNET_5.supports.tools, [CLAUDE_OPUS_5.id]: CLAUDE_OPUS_5.supports.tools, - [CLAUDE_OPUS_5_FAST.id]: CLAUDE_OPUS_5_FAST.supports.tools, [CLAUDE_FABLE_5_1.id]: CLAUDE_FABLE_5_1.supports.tools, [CLAUDE_OPUS_5_5.id]: CLAUDE_OPUS_5_5.supports.tools, [CLAUDE_SONNET_5_5.id]: CLAUDE_SONNET_5_5.supports.tools, @@ -1213,13 +1108,11 @@ export type AnthropicModelInputModalitiesByName = { [CLAUDE_SONNET_4_6.id]: typeof CLAUDE_SONNET_4_6.supports.input [CLAUDE_SONNET_4_5.id]: typeof CLAUDE_SONNET_4_5.supports.input [CLAUDE_HAIKU_4_5.id]: typeof CLAUDE_HAIKU_4_5.supports.input - [CLAUDE_OPUS_4_1.id]: typeof CLAUDE_OPUS_4_1.supports.input [CLAUDE_OPUS_4_7.id]: typeof CLAUDE_OPUS_4_7.supports.input [CLAUDE_OPUS_4_8.id]: typeof CLAUDE_OPUS_4_8.supports.input [CLAUDE_FABLE_5.id]: typeof CLAUDE_FABLE_5.supports.input [CLAUDE_SONNET_5.id]: typeof CLAUDE_SONNET_5.supports.input [CLAUDE_OPUS_5.id]: typeof CLAUDE_OPUS_5.supports.input - [CLAUDE_OPUS_5_FAST.id]: typeof CLAUDE_OPUS_5_FAST.supports.input [CLAUDE_FABLE_5_1.id]: typeof CLAUDE_FABLE_5_1.supports.input [CLAUDE_OPUS_5_5.id]: typeof CLAUDE_OPUS_5_5.supports.input [CLAUDE_SONNET_5_5.id]: typeof CLAUDE_SONNET_5_5.supports.input @@ -1239,13 +1132,11 @@ export const ANTHROPIC_MODEL_INPUT_MODALITIES: Readonly< [CLAUDE_SONNET_4_6.id]: CLAUDE_SONNET_4_6.supports.input, [CLAUDE_SONNET_4_5.id]: CLAUDE_SONNET_4_5.supports.input, [CLAUDE_HAIKU_4_5.id]: CLAUDE_HAIKU_4_5.supports.input, - [CLAUDE_OPUS_4_1.id]: CLAUDE_OPUS_4_1.supports.input, [CLAUDE_OPUS_4_7.id]: CLAUDE_OPUS_4_7.supports.input, [CLAUDE_OPUS_4_8.id]: CLAUDE_OPUS_4_8.supports.input, [CLAUDE_FABLE_5.id]: CLAUDE_FABLE_5.supports.input, [CLAUDE_SONNET_5.id]: CLAUDE_SONNET_5.supports.input, [CLAUDE_OPUS_5.id]: CLAUDE_OPUS_5.supports.input, - [CLAUDE_OPUS_5_FAST.id]: CLAUDE_OPUS_5_FAST.supports.input, [CLAUDE_FABLE_5_1.id]: CLAUDE_FABLE_5_1.supports.input, [CLAUDE_OPUS_5_5.id]: CLAUDE_OPUS_5_5.supports.input, [CLAUDE_SONNET_5_5.id]: CLAUDE_SONNET_5_5.supports.input, @@ -1264,13 +1155,11 @@ export const ANTHROPIC_NO_FORCED_TOOL_MODELS: ReadonlySet = new Set( CLAUDE_SONNET_4_6, CLAUDE_SONNET_4_5, CLAUDE_HAIKU_4_5, - CLAUDE_OPUS_4_1, CLAUDE_OPUS_4_7, CLAUDE_OPUS_4_8, CLAUDE_FABLE_5, CLAUDE_SONNET_5, CLAUDE_OPUS_5, - CLAUDE_OPUS_5_FAST, CLAUDE_FABLE_5_1, CLAUDE_OPUS_5_5, CLAUDE_SONNET_5_5, @@ -1294,13 +1183,11 @@ const ANTHROPIC_REASONING = { [CLAUDE_SONNET_4_6.id]: CLAUDE_SONNET_4_6.reasoning, [CLAUDE_SONNET_4_5.id]: CLAUDE_SONNET_4_5.reasoning, [CLAUDE_HAIKU_4_5.id]: CLAUDE_HAIKU_4_5.reasoning, - [CLAUDE_OPUS_4_1.id]: CLAUDE_OPUS_4_1.reasoning, [CLAUDE_OPUS_4_7.id]: CLAUDE_OPUS_4_7.reasoning, [CLAUDE_OPUS_4_8.id]: CLAUDE_OPUS_4_8.reasoning, [CLAUDE_FABLE_5.id]: CLAUDE_FABLE_5.reasoning, [CLAUDE_SONNET_5.id]: CLAUDE_SONNET_5.reasoning, [CLAUDE_OPUS_5.id]: CLAUDE_OPUS_5.reasoning, - [CLAUDE_OPUS_5_FAST.id]: CLAUDE_OPUS_5_FAST.reasoning, [CLAUDE_FABLE_5_1.id]: CLAUDE_FABLE_5_1.reasoning, [CLAUDE_OPUS_5_5.id]: CLAUDE_OPUS_5_5.reasoning, [CLAUDE_SONNET_5_5.id]: CLAUDE_SONNET_5_5.reasoning, diff --git a/packages/ai-anthropic/src/tools/computer-use-tool.ts b/packages/ai-anthropic/src/tools/computer-use-tool.ts index 1d08b7b394..fe3bd484f3 100644 --- a/packages/ai-anthropic/src/tools/computer-use-tool.ts +++ b/packages/ai-anthropic/src/tools/computer-use-tool.ts @@ -85,7 +85,7 @@ export function splitComputerToolsetCall(input: object) { * the other models that support the toolset. No beta header. * - `computer_20251124`: the earlier tool of Claude Opus 4.5 to Claude Opus 5, * Sonnet 4.6, and Sonnet 5. - * - `computer_20250124`: Claude Sonnet 4.5, Haiku 4.5, and Opus 4.1. + * - `computer_20250124`: Claude Sonnet 4.5 and Haiku 4.5. */ export function computerUseTool( config: ComputerToolsetConfig, diff --git a/packages/ai-anthropic/tests/anthropic-adapter.test.ts b/packages/ai-anthropic/tests/anthropic-adapter.test.ts index a4f48530f6..bff6651d07 100644 --- a/packages/ai-anthropic/tests/anthropic-adapter.test.ts +++ b/packages/ai-anthropic/tests/anthropic-adapter.test.ts @@ -46,7 +46,7 @@ vi.mock('@anthropic-ai/sdk', () => { return { default: MockAnthropic } }) -const createAdapter = (model: TModel) => +const createAdapter = (model: TModel) => new AnthropicTextAdapter({ apiKey: 'test-key' }, model) const toolArguments = JSON.stringify({ location: 'Berlin' }) @@ -182,7 +182,7 @@ describe('Anthropic client injection', () => { }, } - const adapter = createAnthropicChatWithClient('claude-opus-4-1', client) + const adapter = createAnthropicChatWithClient('claude-sonnet-4-5', client) for await (const _ of chat({ threadId: 'thread-1', @@ -205,7 +205,7 @@ describe('Anthropic adapter option mapping', () => { it('serializes only supported document metadata', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const metadata = { cache_control: { type: 'ephemeral' }, citations: { enabled: true }, @@ -256,7 +256,7 @@ describe('Anthropic adapter option mapping', () => { it('prefers document title over filename and serializes only cache_control for images', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -321,7 +321,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -365,7 +365,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -424,7 +424,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const logger = { debug: vi.fn(), @@ -518,7 +518,7 @@ describe('Anthropic adapter option mapping', () => { temperature: 0.4, } satisfies AnthropicTextProviderOptions - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') // Consume the stream to trigger the API call const chunks: AdapterYieldChunk[] = [] @@ -552,7 +552,7 @@ describe('Anthropic adapter option mapping', () => { const [payload] = mocks.betaMessagesCreate.mock.calls[0]! expect(payload).toMatchObject({ - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', max_tokens: 3000, temperature: 0.4, container: providerOptions.container, @@ -602,7 +602,7 @@ describe('Anthropic adapter option mapping', () => { it('sources temperature and max_tokens from modelOptions', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -624,7 +624,7 @@ describe('Anthropic adapter option mapping', () => { it('forwards context_management and attaches the required beta (issue #1074)', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const contextManagement = { edits: [{ type: 'clear_tool_uses_20250919' as const }], } @@ -648,7 +648,7 @@ describe('Anthropic adapter option mapping', () => { it('forwards mcp_servers and attaches the mcp-client beta (issue #1424)', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const mcpServers = [ { type: 'url' as const, @@ -677,7 +677,7 @@ describe('Anthropic adapter option mapping', () => { it('does not attach the mcp-client beta for an empty mcp_servers array', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -697,7 +697,7 @@ describe('Anthropic adapter option mapping', () => { it('forwards top-level cache_control from modelOptions instead of dropping it', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const logger = { debug: vi.fn(), @@ -730,7 +730,7 @@ describe('Anthropic adapter option mapping', () => { it('does not warn about dropped keys when max_tokens is passed via modelOptions', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const logger = { debug: vi.fn(), @@ -766,7 +766,7 @@ describe('Anthropic adapter option mapping', () => { // top_p is mutually exclusive with temperature, so exercise it alone. mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -786,7 +786,7 @@ describe('Anthropic adapter option mapping', () => { it("defaults max_tokens to the model's max_output_tokens when not provided via modelOptions (#849)", async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('ok')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -797,7 +797,7 @@ describe('Anthropic adapter option mapping', () => { } const [payload] = mocks.betaMessagesCreate.mock.calls[0]! - // claude-opus-4-1's model-meta max_output_tokens is 64_000 — not the old + // claude-sonnet-4-5's model-meta max_output_tokens is 64_000 — not the old // hard-coded 1024 floor that silently truncated long responses. expect(payload.max_tokens).toBe(64_000) }) @@ -825,7 +825,7 @@ describe('Anthropic adapter option mapping', () => { })() mocks.betaMessagesCreate.mockResolvedValueOnce(truncatedStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const logger = { debug: vi.fn(), @@ -860,7 +860,7 @@ describe('Anthropic adapter option mapping', () => { })() mocks.betaMessagesCreate.mockResolvedValueOnce(truncatedStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const logger = { debug: vi.fn(), @@ -895,7 +895,7 @@ describe('Anthropic adapter option mapping', () => { id: 'msg_structured', type: 'message', role: 'assistant', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', content: [ { type: 'tool_use', @@ -908,24 +908,26 @@ describe('Anthropic adapter option mapping', () => { usage: { input_tokens: 10, output_tokens: 20 }, }) - const adapter = createAdapter('claude-opus-4-1') - - for await (const _ of chat({ - threadId: 'thread-1', - adapter, - messages: [{ role: 'user', content: 'recommend a guitar as json' }], - outputSchema: z.object({ - recommendation: z.string(), - price: z.number(), - }), - stream: true, - })) { - // consume stream - } + await createAdapter('claude-sonnet-4-5').structuredOutput({ + chatOptions: { + threadId: 'thread-1', + model: 'claude-sonnet-4-5', + messages: [{ role: 'user', content: 'recommend a guitar as json' }], + logger: resolveDebugOption(false), + }, + outputSchema: { + type: 'object', + properties: { + recommendation: { type: 'string' }, + price: { type: 'number' }, + }, + required: ['recommendation', 'price'], + }, + }) const [payload] = mocks.betaMessagesCreate.mock.calls[0]! expect(payload.stream).toBe(false) - // Clamped to the non-streaming limit — NOT claude-opus-4-1's full 64K + // Clamped to the non-streaming limit — NOT claude-sonnet-4-5's full 64K // streaming ceiling, which would make the SDK throw before the request. expect(payload.max_tokens).toBe(ANTHROPIC_MAX_NONSTREAMING_TOKENS) expect(payload.max_tokens).toBeLessThanOrEqual(21_333) @@ -937,7 +939,7 @@ describe('Anthropic adapter option mapping', () => { id: 'msg_structured', type: 'message', role: 'assistant', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', content: [ { type: 'tool_use', @@ -951,10 +953,10 @@ describe('Anthropic adapter option mapping', () => { }) await expect( - createAdapter('claude-opus-4-1').structuredOutput({ + createAdapter('claude-sonnet-4-5').structuredOutput({ chatOptions: { threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'recommend a guitar as json' }], logger: resolveDebugOption(false), }, @@ -972,7 +974,7 @@ describe('Anthropic adapter option mapping', () => { id: 'msg_structured', type: 'message', role: 'assistant', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', content: [ { type: 'tool_use', @@ -985,10 +987,10 @@ describe('Anthropic adapter option mapping', () => { usage: { input_tokens: 10, output_tokens: 10 }, }) - await createAdapter('claude-opus-4-1').structuredOutput({ + await createAdapter('claude-sonnet-4-5').structuredOutput({ chatOptions: { threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'recommend a guitar as json' }], reasoning: { level: 'high', summary: true }, logger: resolveDebugOption(false), @@ -1074,14 +1076,6 @@ describe('Anthropic adapter option mapping', () => { expect(mocks.messagesCreate).not.toHaveBeenCalled() }) - it('native combined mode (#605): pre-4.5 models keep the forced-tool finalization path', async () => { - const adapter = new AnthropicTextAdapter( - { apiKey: 'test-key' }, - 'claude-opus-4-1', - ) - expect(adapter.supportsCombinedToolsAndSchema()).toBe(false) - }) - it('native combined mode (#605): claude-sonnet-5, claude-fable-5, and claude-opus-4-8 use the single-request path', () => { const sonnet5 = new AnthropicTextAdapter( { apiKey: 'test-key' }, @@ -1210,7 +1204,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') // Multi-turn: user -> assistant(tool_calls) -> tool_result -> follow-up user const chunks: AdapterYieldChunk[] = [] @@ -1285,7 +1279,7 @@ describe('Anthropic adapter option mapping', () => { createTextStream('Follow-up answer'), ) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -1343,7 +1337,7 @@ describe('Anthropic adapter option mapping', () => { createTextStream('Next answer'), ) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -1385,7 +1379,7 @@ describe('Anthropic adapter option mapping', () => { createTextStream('Follow-up answer'), ) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const providerToolMetadata = { providerExecuted: true, anthropic: { @@ -1504,7 +1498,7 @@ describe('Anthropic adapter option mapping', () => { it('sends an object tool_use input when replayed tool call arguments are truncated (issue #1582)', async () => { mocks.betaMessagesCreate.mockResolvedValueOnce(createTextStream('Done')) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') for await (const _ of chat({ threadId: 'thread-1', @@ -1583,7 +1577,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -1701,7 +1695,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -1814,7 +1808,7 @@ describe('Anthropic adapter option mapping', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -1880,7 +1874,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -1962,7 +1956,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -2082,7 +2076,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -2197,7 +2191,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(turn1) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const firstMessages = [ { role: 'user' as const, content: 'Search the drone market.' }, @@ -2319,7 +2313,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const logger = { debug: vi.fn(), @@ -2392,7 +2386,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -2458,7 +2452,7 @@ describe('Anthropic stream processing', () => { mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ @@ -2507,7 +2501,7 @@ describe('Anthropic stream processing', () => { const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ threadId: 'thread-1', - adapter: createAdapter('claude-opus-4-1'), + adapter: createAdapter('claude-sonnet-4-5'), messages: [{ role: 'user', content: 'Weather in Berlin and Paris?' }], tools: [weatherTool], })) { @@ -2545,7 +2539,7 @@ describe('Anthropic stream processing', () => { const chunks: AdapterYieldChunk[] = [] for await (const chunk of chat({ threadId: 'thread-1', - adapter: createAdapter('claude-opus-4-1'), + adapter: createAdapter('claude-sonnet-4-5'), messages: [{ role: 'user', content: 'Think twice.' }], })) { chunks.push(chunk) @@ -2598,9 +2592,9 @@ describe('Anthropic stream processing', () => { ) const chunks: Array = [] - for await (const chunk of createAdapter('claude-opus-4-1').chatStream({ + for await (const chunk of createAdapter('claude-sonnet-4-5').chatStream({ threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'Go' }], logger: createSilentLogger(), })) { @@ -2671,11 +2665,11 @@ describe('Anthropic adapter error handling', () => { }), ) - const adapter = createAdapter('claude-opus-4-1') + const adapter = createAdapter('claude-sonnet-4-5') const chunks: AdapterYieldChunk[] = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'hi' }], logger: { request: () => {}, errors: () => {} } as any, })) { diff --git a/packages/ai-anthropic/tests/computer-use-generic.test.ts b/packages/ai-anthropic/tests/computer-use-generic.test.ts index 6acb1c92d2..9bb5b92528 100644 --- a/packages/ai-anthropic/tests/computer-use-generic.test.ts +++ b/packages/ai-anthropic/tests/computer-use-generic.test.ts @@ -215,26 +215,20 @@ describe('anthropic generic computer tool', () => { ]) }) - it.each(['claude-opus-5-fast', 'claude-opus-4-5@20251101'])( - 'throws for %s, which has no computer version in the model meta', - (model) => { - expect(() => resolveComputerUseTool(computerTool(), model)).toThrow( - `Model "${model}" does not support computer use.`, - ) - }, - ) + it('throws for a model with no computer version in the model meta', () => { + const model = 'claude-opus-4-5@20251101' + expect(() => resolveComputerUseTool(computerTool(), model)).toThrow( + `Model "${model}" does not support computer use.`, + ) + }) it('returns a tool with no computer metadata unchanged', () => { const tool = { name: 'echo', description: 'Echo the input.' } - expect(resolveComputerUseTool(tool, 'claude-opus-5-fast')).toBe(tool) + expect(resolveComputerUseTool(tool, 'claude-opus-4-5@20251101')).toBe(tool) }) - it('types: chat() takes the generic tool only on a model with a computer version', () => { + it('types: chat() takes the generic tool on a model with a computer version', () => { typedTools(createAnthropicChat('claude-opus-5-5', 'k'), [computerTool()]) - typedTools(createAnthropicChat('claude-opus-5-fast', 'k'), [ - // @ts-expect-error - claude-opus-5-fast has no computer version - computerTool(), - ]) }) }) diff --git a/packages/ai-anthropic/tests/files-source.test.ts b/packages/ai-anthropic/tests/files-source.test.ts index fb8f7559ec..83f6088fe3 100644 --- a/packages/ai-anthropic/tests/files-source.test.ts +++ b/packages/ai-anthropic/tests/files-source.test.ts @@ -40,7 +40,10 @@ async function drain(iterable: AsyncIterable) { describe('anthropic file content source', () => { it('maps an anthropic file handle to a file_id source and sends the Files beta', async () => { mockEmptyStream() - const adapter = new AnthropicTextAdapter({ apiKey: 'k' }, 'claude-opus-4-1') + const adapter = new AnthropicTextAdapter( + { apiKey: 'k' }, + 'claude-sonnet-4-5', + ) await drain( chat({ @@ -77,7 +80,10 @@ describe('anthropic file content source', () => { it('errors when a foreign provider file handle reaches the anthropic adapter', async () => { mockEmptyStream() - const adapter = new AnthropicTextAdapter({ apiKey: 'k' }, 'claude-opus-4-1') + const adapter = new AnthropicTextAdapter( + { apiKey: 'k' }, + 'claude-sonnet-4-5', + ) const chunks: Array = [] for await (const chunk of chat({ diff --git a/packages/ai-anthropic/tests/model-meta.test.ts b/packages/ai-anthropic/tests/model-meta.test.ts index 945fa67e87..5e242a2827 100644 --- a/packages/ai-anthropic/tests/model-meta.test.ts +++ b/packages/ai-anthropic/tests/model-meta.test.ts @@ -65,7 +65,6 @@ type BudgetThinkingModel = | 'claude-opus-4-5' | 'claude-sonnet-4-5' | 'claude-haiku-4-5' - | 'claude-opus-4-1' type AdaptiveEraModel = | 'claude-opus-4-7' @@ -157,7 +156,6 @@ describe('Anthropic Model Provider Options Type Assertions', () => { expectTypeOf<'claude-sonnet-4-6'>().toExtend() expectTypeOf<'claude-sonnet-4-5'>().toExtend() expectTypeOf<'claude-haiku-4-5'>().toExtend() - expectTypeOf<'claude-opus-4-1'>().toExtend() expectTypeOf<'claude-opus-4-7'>().toExtend() expectTypeOf<'claude-opus-4-8'>().toExtend() expectTypeOf<'claude-fable-5'>().toExtend() @@ -381,26 +379,11 @@ describe('ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS', () => { // tools in one call. It is not the same axis as the `output_config` provider // option, which arrived with Claude 4.7 — Opus 4.5, Opus 4.6, Sonnet 4.5, // Sonnet 4.6 and Haiku 4.5 are in the set and do not accept that option. - const MODELS_WITHOUT_COMBINED_OUTPUT = [ - 'claude-opus-4-1', - 'claude-opus-5-fast', - ] as const - - it('covers every registered model that accepts output_config.format', () => { + it('covers every registered model (all are Claude 4.5+)', () => { const missing = ANTHROPIC_MODELS.filter( - (model) => - !ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(model) && - !MODELS_WITHOUT_COMBINED_OUTPUT.includes( - model as (typeof MODELS_WITHOUT_COMBINED_OUTPUT)[number], - ), + (model) => !ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(model), ) expect(missing).toEqual([]) }) - - it('leaves out the models that fall back to forced tool use', () => { - for (const model of MODELS_WITHOUT_COMBINED_OUTPUT) { - expect(ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(model)).toBe(false) - } - }) }) diff --git a/packages/ai-anthropic/tests/prompt-cache.test.ts b/packages/ai-anthropic/tests/prompt-cache.test.ts index cd6e7d2c58..77e996853a 100644 --- a/packages/ai-anthropic/tests/prompt-cache.test.ts +++ b/packages/ai-anthropic/tests/prompt-cache.test.ts @@ -7,7 +7,7 @@ import type { JSONSchema, ModelMessage, TextOptions, Tool } from '@tanstack/ai' import type { MessageCreateParams } from '@anthropic-ai/sdk/resources/beta/messages' import type { AnthropicTextProviderOptions } from '../src/adapters/text' -const MODEL = 'claude-opus-4-1' +const MODEL = 'claude-sonnet-4-5' const logger = resolveDebugOption(false) const SHORT = { type: 'ephemeral' } diff --git a/packages/ai-anthropic/tests/thinking-replay.test.ts b/packages/ai-anthropic/tests/thinking-replay.test.ts index 9a659135d5..4160e26c44 100644 --- a/packages/ai-anthropic/tests/thinking-replay.test.ts +++ b/packages/ai-anthropic/tests/thinking-replay.test.ts @@ -25,7 +25,7 @@ vi.mock('@anthropic-ai/sdk', () => { type Block = { type: string } & Record const adapter = () => - new AnthropicTextAdapter({ apiKey: 'test-key' }, 'claude-opus-4-1') + new AnthropicTextAdapter({ apiKey: 'test-key' }, 'claude-sonnet-4-5') const weather: Tool = { name: 'lookup_weather', diff --git a/packages/ai-anthropic/tests/tool-result-multimodal.test.ts b/packages/ai-anthropic/tests/tool-result-multimodal.test.ts index 4007972969..ffee0cb89b 100644 --- a/packages/ai-anthropic/tests/tool-result-multimodal.test.ts +++ b/packages/ai-anthropic/tests/tool-result-multimodal.test.ts @@ -59,7 +59,7 @@ describe('anthropic multimodal tool result', () => { const adapter = new AnthropicTextAdapter( { apiKey: 'test-key' }, - 'claude-opus-4-1', + 'claude-sonnet-4-5', ) for await (const _ of chat({ diff --git a/packages/ai-anthropic/tests/tools-per-model-type-safety.test.ts b/packages/ai-anthropic/tests/tools-per-model-type-safety.test.ts index abfca22c0f..a251c23f47 100644 --- a/packages/ai-anthropic/tests/tools-per-model-type-safety.test.ts +++ b/packages/ai-anthropic/tests/tools-per-model-type-safety.test.ts @@ -357,6 +357,6 @@ describe('Anthropic per-model tool gating', () => { : K }[keyof AnthropicChatModelToolCapabilitiesByName] - expectTypeOf().toEqualTypeOf<'claude-opus-5-fast'>() + expectTypeOf().toEqualTypeOf() }) }) diff --git a/packages/ai-anthropic/tests/usage-extraction.test.ts b/packages/ai-anthropic/tests/usage-extraction.test.ts index ae8cad221a..7848ee1c84 100644 --- a/packages/ai-anthropic/tests/usage-extraction.test.ts +++ b/packages/ai-anthropic/tests/usage-extraction.test.ts @@ -49,7 +49,7 @@ vi.mock('@anthropic-ai/sdk', () => { }) const createAdapter = () => - new AnthropicTextAdapter({ apiKey: 'test-key' }, 'claude-opus-4-1') + new AnthropicTextAdapter({ apiKey: 'test-key' }, 'claude-sonnet-4-5') function createMockStream( chunks: Array>, @@ -78,7 +78,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 100, output_tokens: 0, @@ -137,7 +137,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 100, output_tokens: 0, @@ -204,7 +204,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 12, output_tokens: 0 }, }, }, @@ -249,7 +249,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 100, output_tokens: 0, @@ -313,7 +313,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 100, output_tokens: 0, @@ -372,7 +372,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 100, output_tokens: 0, @@ -429,7 +429,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 13, output_tokens: 1, @@ -507,7 +507,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 13, output_tokens: 1, @@ -529,7 +529,7 @@ describe('Anthropic usage extraction', () => { const chunks: Array = [] for await (const chunk of createAdapter().chatStream({ threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'Hello' }], logger: createSilentLogger(), })) { @@ -557,7 +557,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 13, output_tokens: 1, @@ -587,7 +587,7 @@ describe('Anthropic usage extraction', () => { const chunks: Array = [] for await (const chunk of createAdapter().chatStream({ threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'Hello' }], logger: createSilentLogger(), })) { @@ -614,7 +614,7 @@ describe('Anthropic usage extraction', () => { type: 'message', role: 'assistant', content: [], - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', usage: { input_tokens: 13, output_tokens: 1 }, }, }, @@ -626,7 +626,7 @@ describe('Anthropic usage extraction', () => { const chunks: Array = [] for await (const chunk of createAdapter().chatStream({ threadId: 'thread-1', - model: 'claude-opus-4-1', + model: 'claude-sonnet-4-5', messages: [{ role: 'user', content: 'Hello' }], logger: createSilentLogger(), })) { diff --git a/packages/ai-anthropic/tests/vertex-factory.test.ts b/packages/ai-anthropic/tests/vertex-factory.test.ts index 1ab7e77f88..22dab63104 100644 --- a/packages/ai-anthropic/tests/vertex-factory.test.ts +++ b/packages/ai-anthropic/tests/vertex-factory.test.ts @@ -33,7 +33,7 @@ describe('ANTHROPIC_VERTEX_CHAT_MODELS', () => { it('does not include Anthropic-only model ids', () => { expect(ANTHROPIC_VERTEX_CHAT_MODELS).toContain('claude-sonnet-5') expect(ANTHROPIC_VERTEX_CHAT_MODELS).toContain('claude-opus-5') - expect(ANTHROPIC_VERTEX_CHAT_MODELS).not.toContain('claude-opus-5-fast') + expect(ANTHROPIC_VERTEX_CHAT_MODELS).not.toContain('claude-fable-5-1') }) }) diff --git a/packages/ai-code-mode/models-eval/eval-config.ts b/packages/ai-code-mode/models-eval/eval-config.ts index 29382a2a15..13cde52637 100644 --- a/packages/ai-code-mode/models-eval/eval-config.ts +++ b/packages/ai-code-mode/models-eval/eval-config.ts @@ -71,6 +71,6 @@ export const EVAL_MODELS: Array = [ { name: 'Grok Build 0.1', model: 'grok:grok-build-0.1' }, // --- Groq --- - { name: 'Llama 3.3 70B (Groq)', model: 'groq:llama-3.3-70b-versatile' }, - { name: 'Qwen3 32B (Groq)', model: 'groq:qwen/qwen3-32b' }, + { name: 'GPT-OSS 120B (Groq)', model: 'groq:openai/gpt-oss-120b' }, + { name: 'Qwen3.8 27B (Groq)', model: 'groq:qwen/qwen3.8-27b' }, ] diff --git a/packages/ai-code-mode/models-eval/run-eval.ts b/packages/ai-code-mode/models-eval/run-eval.ts index 0a9a84dfb3..ab0ca4a330 100644 --- a/packages/ai-code-mode/models-eval/run-eval.ts +++ b/packages/ai-code-mode/models-eval/run-eval.ts @@ -5,7 +5,7 @@ * Usage (from repo root): * pnpm --filter @tanstack/ai-code-mode-models-eval eval * pnpm --filter @tanstack/ai-code-mode-models-eval eval -- --ollama-only - * pnpm --filter @tanstack/ai-code-mode-models-eval eval -- --models openai:gpt-4o,groq:qwen/qwen3-32b + * pnpm --filter @tanstack/ai-code-mode-models-eval eval -- --models openai:gpt-4o,groq:qwen/qwen3.8-27b * cd packages/ai-code-mode/models-eval && pnpm eval -- --no-judge * * Env: @@ -207,7 +207,7 @@ function getTextAdapter( case 'grok': return grokText(modelId as 'grok-build-0.1') case 'groq': - return groqText(modelId as 'llama-3.3-70b-versatile') + return groqText(modelId as 'openai/gpt-oss-120b') } } @@ -308,7 +308,7 @@ function parseArgs(argv: Array): { @tanstack/ai-code-mode-models-eval (pnpm eval from this folder, or pnpm --filter @tanstack/ai-code-mode-models-eval eval) --ollama-only Only run Ollama models from eval-config - --models Comma-separated provider:model ids (e.g. openai:gpt-4o,groq:qwen/qwen3-32b) + --models Comma-separated provider:model ids (e.g. openai:gpt-4o,groq:qwen/qwen3.8-27b) --capture-only Run models and write logs without LLM judging --judge-latest Judge latest captured session from logs (no model runs) --rejudge Re-judge logs even if judge fields already exist diff --git a/packages/ai-gemini/src/adapters/image.ts b/packages/ai-gemini/src/adapters/image.ts index 3715b4b53e..d79174fe18 100644 --- a/packages/ai-gemini/src/adapters/image.ts +++ b/packages/ai-gemini/src/adapters/image.ts @@ -4,23 +4,14 @@ import { resolveMediaPrompt, } from '@tanstack/ai' import { BaseImageAdapter } from '@tanstack/ai/adapters' -import { - createGeminiClient, - generateId, - getGeminiApiKeyFromEnv, -} from '../utils' +import { createGeminiClient, getGeminiApiKeyFromEnv } from '../utils' import { flattenModalityTokenCounts, hasModalityTokens } from '../usage' import { - isGeminiNativeImageModel, parseNativeImageSize, - sizeToAspectRatio, - validateImageSize, - validateNumberOfImages, validatePrompt, } from '../image/image-provider-options' import type { GeminiImageModels } from '../model-meta' import type { - GeminiAnyImageProviderOptions, GeminiImageModelInputModalitiesByName, GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName, @@ -39,8 +30,6 @@ import { ThinkingLevel } from '@google/genai' import type { Content, ContentUnion, - GenerateImagesConfig, - GenerateImagesResponse, GoogleGenAI, Interactions, Part, @@ -61,21 +50,19 @@ export type GeminiImageModel = GeminiImageModels * Gemini Image Generation Adapter * * Tree-shakeable adapter for Gemini image generation functionality. - * Supports Imagen 3/4 models (via generateImages API) and Gemini native - * image models like Nano Banana 2 (via the Interactions API). + * Supports Gemini native image models like Nano Banana 2 via the + * Interactions API. * * Features: * - Aspect ratio-based image sizing - * - Person generation controls * - Safety filtering - * - Watermark options * - Extended resolution tiers (Nano Banana 2) */ export class GeminiImageAdapter< TModel extends GeminiImageModel, > extends BaseImageAdapter< TModel, - GeminiAnyImageProviderOptions, + GeminiNativeImageProviderOptions, GeminiImageModelProviderOptionsByName, GeminiImageModelSizeByName, GeminiImageModelInputModalitiesByName @@ -87,7 +74,7 @@ export class GeminiImageAdapter< // Type-only property - never assigned at runtime declare '~types': { - providerOptions: GeminiAnyImageProviderOptions + providerOptions: GeminiNativeImageProviderOptions modelProviderOptionsByName: GeminiImageModelProviderOptionsByName modelSizeByName: GeminiImageModelSizeByName modelInputModalitiesByName: GeminiImageModelInputModalitiesByName @@ -101,7 +88,7 @@ export class GeminiImageAdapter< } async generateImages( - options: ImageGenerationOptions, + options: ImageGenerationOptions, ): Promise { const { model, logger } = options @@ -133,37 +120,7 @@ export class GeminiImageAdapter< ) } - if (isGeminiNativeImageModel(model)) { - return await this.generateWithGeminiApi(options, resolved) - } - - // Imagen does not accept image inputs — it's strictly text-to-image. - if (resolved.images.length > 0) { - throw new Error( - `${this.name}: model "${model}" (Imagen) does not support image prompt parts. ` + - `Use a Gemini-native image model (e.g. gemini-3.1-flash-image) for image-conditioned generation.`, - ) - } - - if (options.modelOptions?.previous_interaction_id) { - throw new Error( - `${this.name}: previous_interaction_id is only supported on Gemini-native image models.`, - ) - } - - // Imagen models path (generateImages API) - validateImageSize(model, options.size) - validateNumberOfImages(model, options.numberOfImages) - - const config = this.buildImagenConfig(options) - - const response = await this.client.models.generateImages({ - model, - prompt: resolved.text, - config, - }) - - return this.transformImagenResponse(model, response) + return await this.generateWithGeminiApi(options, resolved) } catch (error) { logger.errors('gemini.generateImage fatal', { error, @@ -351,142 +308,19 @@ export class GeminiImageAdapter< ...(usage ? { usage } : {}), } } - - private buildImagenConfig( - options: ImageGenerationOptions, - ): GenerateImagesConfig { - const { size, numberOfImages, modelOptions } = options - - // Build with conditional spreads — under exactOptionalPropertyTypes the - // vendor `GenerateImagesConfig` fields are `field?: T` (no `| undefined`), - // so we can only assign the property when we actually have a value. - const sizeAspectRatio = size ? sizeToAspectRatio(size) : undefined - - // Named picks, never a wholesale spread — the mirror image of the native - // path below. A native-only field (safetySettings, thinkingConfig, - // imageConfig, systemInstruction) belongs to GenerateContentConfig and is - // rejected by generateImages with 400 INVALID_ARGUMENT, so it must not be - // able to reach here even when the caller's `modelOptions` was typed - // against both shapes at once (e.g. an adapter inferred from a union of - // model names). - return { - numberOfImages: numberOfImages ?? 1, - // Map size to aspect ratio if provided; modelOptions.aspectRatio, - // picked after it, overrides. - ...(sizeAspectRatio !== undefined && { aspectRatio: sizeAspectRatio }), - ...(modelOptions?.aspectRatio !== undefined && { - aspectRatio: modelOptions.aspectRatio, - }), - ...(modelOptions?.personGeneration !== undefined && { - personGeneration: modelOptions.personGeneration, - }), - ...(modelOptions?.safetyFilterLevel !== undefined && { - safetyFilterLevel: modelOptions.safetyFilterLevel, - }), - ...(modelOptions?.seed !== undefined && { seed: modelOptions.seed }), - ...(modelOptions?.addWatermark !== undefined && { - addWatermark: modelOptions.addWatermark, - }), - ...(modelOptions?.language !== undefined && { - language: modelOptions.language, - }), - ...(modelOptions?.negativePrompt !== undefined && { - negativePrompt: modelOptions.negativePrompt, - }), - ...(modelOptions?.outputMimeType !== undefined && { - outputMimeType: modelOptions.outputMimeType, - }), - ...(modelOptions?.outputCompressionQuality !== undefined && { - outputCompressionQuality: modelOptions.outputCompressionQuality, - }), - ...(modelOptions?.guidanceScale !== undefined && { - guidanceScale: modelOptions.guidanceScale, - }), - ...(modelOptions?.enhancePrompt !== undefined && { - enhancePrompt: modelOptions.enhancePrompt, - }), - ...(modelOptions?.includeSafetyAttributes !== undefined && { - includeSafetyAttributes: modelOptions.includeSafetyAttributes, - }), - ...(modelOptions?.includeRaiReason !== undefined && { - includeRaiReason: modelOptions.includeRaiReason, - }), - ...(modelOptions?.outputGcsUri !== undefined && { - outputGcsUri: modelOptions.outputGcsUri, - }), - ...(modelOptions?.labels !== undefined && { - labels: modelOptions.labels, - }), - } - } - - private transformImagenResponse( - model: string, - response: GenerateImagesResponse, - ): ImageGenerationResult { - const entries = response.generatedImages ?? [] - const images: Array = [] - const filterReasons: Array = [] - - for (const item of entries) { - const b64Json = item.image?.imageBytes - if (b64Json) { - images.push({ - b64Json, - ...(item.enhancedPrompt !== undefined && { - revisedPrompt: item.enhancedPrompt, - }), - }) - continue - } - // Imagen can drop individual entries with a raiFilteredReason when - // Responsible-AI filters fire. Preserve the reason so callers can - // surface it instead of silently getting back fewer images. - const reason = (item as { raiFilteredReason?: string }).raiFilteredReason - if (reason) { - filterReasons.push(reason) - } - } - - // Every entry was filtered — no usable images to return. Throw rather - // than resolve to an empty array so the caller is forced to handle the - // failure mode explicitly. - if (entries.length > 0 && images.length === 0) { - const joined = filterReasons.length > 0 ? filterReasons.join('; ') : '' - throw new Error( - `Imagen ${model} returned no images: all ${entries.length} generated image(s) were filtered by Responsible-AI${joined ? ` (${joined})` : ''}.`, - ) - } - - // Partial filter: surface via console.warn since ImageGenerationResult - // has no warnings field. Callers that care can still inspect the count - // mismatch between requested and returned images. - if (filterReasons.length > 0 && typeof console !== 'undefined') { - console.warn( - `[gemini-image] ${filterReasons.length} of ${entries.length} images from ${model} were filtered by Responsible-AI: ${filterReasons.join('; ')}`, - ) - } - - return { - id: generateId(this.name), - model, - images, - } - } } - /** * Creates a Gemini image adapter with explicit API key. * Type resolution happens here at the call site. * - * @param model - The model name (e.g., 'imagen-4.0-generate-001') + * @param model - The model name (e.g., 'gemini-3.1-flash-image') * @param apiKey - Your Google API key * @param config - Optional additional configuration * @returns Configured Gemini image adapter instance with resolved types * * @example * ```typescript - * const adapter = createGeminiImage('imagen-4.0-generate-001', "your-api-key"); + * const adapter = createGeminiImage('gemini-3.1-flash-image', "your-api-key"); * * const result = await generateImage({ * adapter, @@ -510,7 +344,7 @@ export function createGeminiImage( * - `process.env` (Node.js) * - `window.env` (Browser with injected env) * - * @param model - The model name (e.g., 'imagen-4.0-generate-001') + * @param model - The model name (e.g., 'gemini-3.1-flash-image') * @param config - Optional configuration (excluding apiKey which is auto-detected) * @returns Configured Gemini image adapter instance with resolved types * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment @@ -518,7 +352,7 @@ export function createGeminiImage( * @example * ```typescript * // Automatically uses GOOGLE_API_KEY from environment - * const adapter = geminiImage('imagen-4.0-generate-001'); + * const adapter = geminiImage('gemini-3.1-flash-image'); * * const result = await generateImage({ * adapter, diff --git a/packages/ai-gemini/src/image/image-provider-options.ts b/packages/ai-gemini/src/image/image-provider-options.ts index 2435fe2d79..1c41b8b077 100644 --- a/packages/ai-gemini/src/image/image-provider-options.ts +++ b/packages/ai-gemini/src/image/image-provider-options.ts @@ -2,140 +2,17 @@ import type { GeminiImageModels } from '../model-meta' import type { ContentUnion, ImageConfig, - ImagePromptLanguage, - PersonGeneration, - SafetyFilterLevel, SafetySetting, ThinkingConfig, } from '@google/genai' // Re-export SDK types so users can use them directly -export type { - ContentUnion, - ImageConfig, - ImagePromptLanguage, - PersonGeneration, - SafetyFilterLevel, - SafetySetting, - ThinkingConfig, -} - -/** - * Gemini Imagen aspect ratio options - * Controls the aspect ratio of generated images - */ -export type GeminiAspectRatio = - | '1:1' - | '3:4' - | '4:3' - | '9:16' - | '16:9' - | '9:21' - | '21:9' - -/** - * Provider options for Gemini image generation - * These options match the @google/genai GenerateImagesConfig interface - * and can be spread directly into the API request. - */ -export interface GeminiImageProviderOptions { - /** - * The aspect ratio of generated images - * @default '1:1' - */ - aspectRatio?: GeminiAspectRatio - - /** - * Controls whether people can appear in generated images - * Use PersonGeneration enum values: DONT_ALLOW, ALLOW_ADULT, ALLOW_ALL - * @default 'ALLOW_ADULT' - */ - personGeneration?: PersonGeneration - - /** - * Safety filter level for content filtering - * Use SafetyFilterLevel enum values - */ - safetyFilterLevel?: SafetyFilterLevel - - /** - * Optional seed for reproducible image generation - * When the same seed is used with the same prompt and settings, - * you should get similar (though not identical) results - */ - seed?: number - - /** - * Whether to add a SynthID watermark to generated images - * SynthID helps identify AI-generated content - * @default true - */ - addWatermark?: boolean - - /** - * Language of the prompt - * Use ImagePromptLanguage enum values - */ - language?: ImagePromptLanguage - - /** - * Negative prompt - what to avoid in the generated image - * Not all models support negative prompts - */ - negativePrompt?: string - - /** - * Output MIME type for the generated image - * @default 'image/png' - */ - outputMimeType?: 'image/png' | 'image/jpeg' | 'image/webp' - - /** - * Compression quality for JPEG outputs (0-100) - * Higher values mean better quality but larger file sizes - * @default 75 - */ - outputCompressionQuality?: number - - /** - * Controls how much the model adheres to the text prompt - * Large values increase output and prompt alignment, - * but may compromise image quality - */ - guidanceScale?: number - - /** - * Whether to use the prompt rewriting logic - */ - enhancePrompt?: boolean - - /** - * Whether to report the safety scores of each generated image - * and the positive prompt in the response - */ - includeSafetyAttributes?: boolean - - /** - * Whether to include the Responsible AI filter reason - * if the image is filtered out of the response - */ - includeRaiReason?: boolean - - /** - * Cloud Storage URI used to store the generated images - */ - outputGcsUri?: string - - /** - * User specified labels to track billing usage - */ - labels?: Record -} +export type { ContentUnion, ImageConfig, SafetySetting, ThinkingConfig } /** * Provider options for Gemini native image models (Nano Banana and friends). * - * These models are served by the Interactions API, not `generateImages`. + * These models are served by the Interactions API. * Sampling knobs (`temperature`, `topK`, …) and chat-only plumbing * (`tools`, `responseSchema`, …) are deliberately left out. * @@ -195,47 +72,13 @@ export interface GeminiNativeImageProviderOptions { } /** - * Every provider-option field this adapter understands, across both API - * paths. Used as the adapter's base (model-agnostic) option type; the - * per-model map below is what narrows a given model to the half that - * actually applies to it. - */ -export type GeminiAnyImageProviderOptions = GeminiImageProviderOptions & - GeminiNativeImageProviderOptions - -/** - * Model-specific provider options mapping. - * Gemini native image models go through the Interactions API. Imagen models - * go through `generateImages` and take `GenerateImagesConfig` fields. - * Mirrors the native/Imagen split in - * {@link GeminiImageModelSizeByName} and - * {@link GeminiImageModelInputModalitiesByName}. + * Model-specific provider options mapping. Every Gemini image model goes + * through the Interactions API and takes the same option set. */ export type GeminiImageModelProviderOptionsByName = { - [K in GeminiNativeImageModels]: GeminiNativeImageProviderOptions -} & { - [K in Exclude< - GeminiImageModels, - GeminiNativeImageModels - >]: GeminiImageProviderOptions + [K in GeminiImageModels]: GeminiNativeImageProviderOptions } -/** - * Supported size strings for Gemini Imagen models - * These map to aspect ratios internally - */ -export type GeminiImageSize = - | '1024x1024' - | '512x512' - | '1024x768' - | '1536x1024' - | '1792x1024' - | '1920x1080' - | '768x1024' - | '1024x1536' - | '1024x1792' - | '1080x1920' - /** * The ten aspect ratios every Gemini native image model accepts. * @@ -329,166 +172,22 @@ export type GeminiNativeImageSize = | Gemini3ProImageSize /** - * Gemini native image models that use the Interactions API path. - * These models take an aspect-ratio-based size rather than Imagen's - * WIDTHxHEIGHT pixel strings. - * - * This array is the single source of truth for the native/Imagen split: the - * `GeminiNativeImageModels` union and the per-model option/size/modality maps - * all derive from it. The `satisfies` clause makes a typo (or a name that - * is not a known image model) a build error rather than a phantom key on every - * per-model map. - * - * It is also the single source of truth for the adapter's runtime routing - * — see {@link isGeminiNativeImageModel}. Adding a new `gemini-*` image model - * means adding it here as well as to `GEMINI_IMAGE_MODELS` in model-meta. - * Until it is listed here it routes to the Imagen API instead and fails - * loudly on the first call, rather than silently taking the wrong option - * shape. - */ -export const GEMINI_NATIVE_IMAGE_MODELS = [ - 'gemini-nano-banana-2.1', - 'gemini-3.1-flash-image', - 'gemini-3.1-flash-lite-image', - 'gemini-3-pro-image', -] as const satisfies ReadonlyArray - -export type GeminiNativeImageModels = - (typeof GEMINI_NATIVE_IMAGE_MODELS)[number] - -const NATIVE_IMAGE_MODEL_NAMES: ReadonlySet = new Set( - GEMINI_NATIVE_IMAGE_MODELS, -) - -/** - * Runtime counterpart to {@link GeminiNativeImageModels} — decides which of - * the two Gemini image APIs a model goes to. - * - * Membership in {@link GEMINI_NATIVE_IMAGE_MODELS}, not a `gemini-` prefix - * test, so the runtime route and the type-level split cannot drift apart. An - * id this package does not know about reaches the Imagen endpoint and fails - * there, which is the intended signal to add the model here rather than to - * have it silently take the native path with Imagen-shaped option types. - */ -export function isGeminiNativeImageModel(model: string): boolean { - return NATIVE_IMAGE_MODEL_NAMES.has(model) -} - -/** - * Model-specific size options mapping. Each native model gets its own ratio × - * resolution set (they genuinely differ); Imagen models use pixel sizes. + * Model-specific size options mapping. Each model gets its own ratio × + * resolution set (they genuinely differ). */ export type GeminiImageModelSizeByName = { 'gemini-nano-banana-2.1': GeminiNanoBanana21ImageSize 'gemini-3.1-flash-image': Gemini31FlashImageSize 'gemini-3.1-flash-lite-image': Gemini31FlashLiteImageSize 'gemini-3-pro-image': Gemini3ProImageSize -} & { - [K in Exclude]: GeminiImageSize } /** - * Per-model prompt input modalities. Gemini-native image models accept image - * parts in the multimodal prompt (image-conditioned generation via - * generateContent); Imagen models are strictly text-to-image, so their - * `prompt` is constrained to text at compile time. + * Per-model prompt input modalities. Every Gemini image model accepts image + * parts in the multimodal prompt (image-conditioned generation). */ export type GeminiImageModelInputModalitiesByName = { - [K in GeminiNativeImageModels]: readonly ['image'] -} & { - [K in Exclude]: readonly [] -} - -/** - * Valid sizes for Gemini Imagen models - * Gemini uses aspect ratios, but we map common WIDTHxHEIGHT formats to aspect ratios - * These are approximate mappings based on common image dimensions - */ -export const GEMINI_SIZE_TO_ASPECT_RATIO: Record = { - // Square - '1024x1024': '1:1', - '512x512': '1:1', - // Landscape - '1024x768': '4:3', - '1536x1024': '4:3', - '1792x1024': '16:9', - '1920x1080': '16:9', - // Portrait - '768x1024': '3:4', - '1024x1536': '3:4', // Inverted - '1024x1792': '9:16', - '1080x1920': '9:16', -} - -/** - * Maps a WIDTHxHEIGHT size string to a Gemini aspect ratio - * Returns undefined if the size cannot be mapped - */ -export function sizeToAspectRatio( - size: string | undefined, -): GeminiAspectRatio | undefined { - if (!size) return undefined - return GEMINI_SIZE_TO_ASPECT_RATIO[size] -} - -/** - * Validates that the provided size can be mapped to an aspect ratio - * Throws an error if the size is invalid - */ -export function validateImageSize( - model: string, - size: string | undefined, -): void { - if (!size) return - - const aspectRatio = sizeToAspectRatio(size) - if (!aspectRatio) { - const validSizes = Object.keys(GEMINI_SIZE_TO_ASPECT_RATIO) - throw new Error( - `Invalid size "${size}" for model "${model}". ` + - `Gemini Imagen uses aspect ratios. Valid sizes that map to aspect ratios: ${validSizes.join(', ')}. ` + - `Alternatively, use providerOptions.aspectRatio directly with values: 1:1, 3:4, 4:3, 9:16, 16:9, 9:21, 21:9`, - ) - } -} - -/** - * Per-model caps on images per request. - * The Imagen 4 family all support up to 4 images per request via the Gemini - * API (the rumored 8-image tier is Vertex-only and isn't reachable through - * @google/genai today). Unknown models fall through to the shared cap - * defined below. - * - * @see https://ai.google.dev/gemini-api/docs/imagen - */ -const IMAGEN_MAX_IMAGES_BY_MODEL: Record = { - 'imagen-4.0-generate-001': 4, - 'imagen-4.0-ultra-generate-001': 4, - 'imagen-4.0-fast-generate-001': 4, -} - -const DEFAULT_IMAGEN_MAX_IMAGES = 4 - -/** - * Validates the number of images requested against the model's known cap. - * Uses a per-model table where available and falls back to the shared - * default otherwise — no more "some support up to 8" comments that don't - * match the error message. - */ -export function validateNumberOfImages( - model: string, - numberOfImages: number | undefined, -): void { - if (numberOfImages === undefined) return - - const maxImages = - IMAGEN_MAX_IMAGES_BY_MODEL[model] ?? DEFAULT_IMAGEN_MAX_IMAGES - if (numberOfImages < 1 || numberOfImages > maxImages) { - throw new Error( - `Invalid numberOfImages "${numberOfImages}" for model "${model}". ` + - `Must be between 1 and ${maxImages}.`, - ) - } + [K in GeminiImageModels]: readonly ['image'] } /** diff --git a/packages/ai-gemini/src/index.ts b/packages/ai-gemini/src/index.ts index 6a5916a538..90cc0643ba 100644 --- a/packages/ai-gemini/src/index.ts +++ b/packages/ai-gemini/src/index.ts @@ -35,12 +35,9 @@ export { type GeminiImageConfig, } from './adapters/image' export type { - GeminiImageProviderOptions, GeminiNativeImageConfig, GeminiNativeImageProviderOptions, - GeminiAnyImageProviderOptions, GeminiImageModelProviderOptionsByName, - GeminiAspectRatio, // Per-model size narrowing. `GeminiImageModelSizeByName` is the map // `generateImage()` applies at the call site; the per-model aliases let you // name a single model's set directly. `GeminiNativeImageSize` is the widest @@ -54,9 +51,6 @@ export type { Gemini3ProImageSize, GeminiNativeImageSize, // Re-export SDK types for convenience - PersonGeneration, - SafetyFilterLevel, - ImagePromptLanguage, SafetySetting, ThinkingConfig, ImageConfig, @@ -146,10 +140,6 @@ export { } from './model-meta' export { GEMINI_MODELS as GeminiTextModels } from './model-meta' export { GEMINI_IMAGE_MODELS as GeminiImageModels } from './model-meta' -export { - GEMINI_NATIVE_IMAGE_MODELS, - isGeminiNativeImageModel, -} from './image/image-provider-options' export { GEMINI_TTS_MODELS as GeminiTTSModels } from './model-meta' export { GEMINI_TTS_VOICES as GeminiTTSVoices } from './model-meta' export { GEMINI_AUDIO_MODELS as GeminiAudioModels } from './model-meta' diff --git a/packages/ai-gemini/src/model-meta.ts b/packages/ai-gemini/src/model-meta.ts index daa697490c..9c0d216ecb 100644 --- a/packages/ai-gemini/src/model-meta.ts +++ b/packages/ai-gemini/src/model-meta.ts @@ -609,75 +609,6 @@ const GEMINI_2_5_FLASH_LITE = { GeminiStructuredOutputOptions > -const IMAGEN_4_GENERATE = { - name: 'imagen-4.0-generate-001', - max_input_tokens: 480, - max_output_tokens: 4, - supports: { - input: ['text'], - output: ['image'], - }, - pricing: { - input: { - normal: 0, - }, - output: { - normal: 0.4, - }, - }, -} as const satisfies ModelMeta< - GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiCommonConfigOptions & - GeminiCachedContentOptions -> - -const IMAGEN_4_GENERATE_ULTRA = { - name: 'imagen-4.0-ultra-generate-001', - max_input_tokens: 480, - max_output_tokens: 4, - supports: { - input: ['text'], - output: ['image'], - }, - pricing: { - input: { - normal: 0, - }, - output: { - normal: 0.6, - }, - }, -} as const satisfies ModelMeta< - GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiCommonConfigOptions & - GeminiCachedContentOptions -> - -const IMAGEN_4_GENERATE_FAST = { - name: 'imagen-4.0-fast-generate-001', - max_input_tokens: 480, - max_output_tokens: 4, - supports: { - input: ['text'], - output: ['image'], - }, - pricing: { - input: { - normal: 0, - }, - output: { - normal: 0.2, - }, - }, -} as const satisfies ModelMeta< - GeminiToolConfigOptions & - GeminiSafetyOptions & - GeminiCommonConfigOptions & - GeminiCachedContentOptions -> - /** * Veo video generation models. Pricing is per second of generated video * (audio+video rate where the model supports audio). @@ -1046,9 +977,6 @@ export const GEMINI_IMAGE_MODELS = [ GEMINI_3_1_FLASH_IMAGE.name, GEMINI_3_1_FLASH_LITE_IMAGE.name, GEMINI_3_PRO_IMAGE.name, - IMAGEN_4_GENERATE.name, - IMAGEN_4_GENERATE_FAST.name, - IMAGEN_4_GENERATE_ULTRA.name, ] as const /** diff --git a/packages/ai-gemini/tests/audio-adapter.test.ts b/packages/ai-gemini/tests/audio-adapter.test.ts index 6d7f1ab574..18352ab9b5 100644 --- a/packages/ai-gemini/tests/audio-adapter.test.ts +++ b/packages/ai-gemini/tests/audio-adapter.test.ts @@ -181,7 +181,7 @@ describe('Gemini Audio (Lyria) Adapter', () => { // include `negativePrompt` — that field only exists on // GenerateImagesConfig / GenerateVideosConfig. Until the correct // endpoint (likely `ai.live.music.connect` with `musicGenerationConfig`) - // is confirmed, the adapter must not leak Imagen/Veo-shaped fields onto + // is confirmed, the adapter must not leak Veo-shaped fields onto // the GenerateContent call. This test pins the emitted config shape so // a future SDK author can catch regressions. mockGenerateContent.mockResolvedValueOnce({ diff --git a/packages/ai-gemini/tests/image-adapter.test.ts b/packages/ai-gemini/tests/image-adapter.test.ts index b16ad5a5e1..34f2e73298 100644 --- a/packages/ai-gemini/tests/image-adapter.test.ts +++ b/packages/ai-gemini/tests/image-adapter.test.ts @@ -1,21 +1,10 @@ import { describe, it, expect, vi } from 'vitest' -import { - HarmBlockThreshold, - HarmCategory, - ImagePromptLanguage, - PersonGeneration, - SafetyFilterLevel, - ThinkingLevel, -} from '@google/genai' +import { HarmBlockThreshold, HarmCategory, ThinkingLevel } from '@google/genai' import { generateImage } from '@tanstack/ai' import { resolveDebugOption } from '@tanstack/ai/adapter-internals' import { GeminiImageAdapter, createGeminiImage } from '../src/adapters/image' -import { GEMINI_NATIVE_IMAGE_MODELS, isGeminiNativeImageModel } from '../src' import { parseNativeImageSize, - sizeToAspectRatio, - validateImageSize, - validateNumberOfImages, validatePrompt, } from '../src/image/image-provider-options' @@ -54,50 +43,21 @@ function mockedNativeAdapter() { return { adapter, mockCreate } } -function stubCreate( - adapter: object, - mockCreate: ReturnType, - mockGenerateImages?: ReturnType, -) { +function stubCreate(adapter: object, mockCreate: ReturnType) { ;( adapter as unknown as { - client: { - interactions: { create: unknown } - models?: { generateImages: unknown } - } + client: { interactions: { create: unknown } } } ).client = { interactions: { create: mockCreate }, - ...(mockGenerateImages - ? { models: { generateImages: mockGenerateImages } } - : {}), - } -} - -/** - * An Imagen-path adapter whose `client.models.generateImages` is stubbed, so - * tests can assert the exact GenerateImagesConfig handed to the SDK. - */ -function mockedImagenAdapter() { - const mockGenerateImages = vi.fn().mockResolvedValueOnce({ - generatedImages: [{ image: { imageBytes: 'imagen-b64' } }], - }) - const adapter = createGeminiImage('imagen-4.0-generate-001', 'test-api-key') - ;( - adapter as unknown as { - client: { models: { generateImages: unknown } } - } - ).client = { - models: { generateImages: mockGenerateImages }, } - return { adapter, mockGenerateImages } } describe('Gemini Image Adapter', () => { describe('createGeminiImage', () => { it('creates an adapter with the provided API key', () => { const adapter = createGeminiImage( - 'imagen-4.0-generate-001', + 'gemini-3.1-flash-image', 'test-api-key', ) expect(adapter).toBeInstanceOf(GeminiImageAdapter) @@ -107,114 +67,26 @@ describe('Gemini Image Adapter', () => { it('has the correct model', () => { const adapter = createGeminiImage( - 'imagen-4.0-generate-001', + 'gemini-3.1-flash-image', 'test-api-key', ) - expect(adapter.model).toBe('imagen-4.0-generate-001') - }) - }) - - describe('sizeToAspectRatio', () => { - it('maps common sizes to aspect ratios', () => { - expect(sizeToAspectRatio('1024x1024')).toBe('1:1') - expect(sizeToAspectRatio('512x512')).toBe('1:1') - expect(sizeToAspectRatio('1920x1080')).toBe('16:9') - expect(sizeToAspectRatio('1080x1920')).toBe('9:16') - }) - - it('returns undefined for unknown sizes', () => { - expect(sizeToAspectRatio('999x999')).toBeUndefined() - expect(sizeToAspectRatio('invalid')).toBeUndefined() - }) - - it('returns undefined for undefined input', () => { - expect(sizeToAspectRatio(undefined)).toBeUndefined() - }) - }) - - describe('validateImageSize', () => { - it('accepts valid sizes that map to aspect ratios', () => { - expect(() => - validateImageSize('imagen-4.0-generate-001', '1024x1024'), - ).not.toThrow() - expect(() => - validateImageSize('imagen-4.0-generate-001', '1920x1080'), - ).not.toThrow() - }) - - it('rejects invalid sizes', () => { - expect(() => - validateImageSize('imagen-4.0-generate-001', '999x999'), - ).toThrow() - }) - - it('accepts undefined size', () => { - expect(() => - validateImageSize('imagen-4.0-generate-001', undefined), - ).not.toThrow() - }) - }) - - describe('validateNumberOfImages', () => { - it('accepts 1-4 images', () => { - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', 1), - ).not.toThrow() - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', 4), - ).not.toThrow() - }) - - it('rejects more than 4 images', () => { - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', 5), - ).toThrow() - }) - - it('uses the per-model cap for Imagen 4 models', () => { - // Per-model cap lookup: each Imagen family model has its own max - // rather than relying on a misleading comment about "up to 8". - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', 4), - ).not.toThrow() - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', 5), - ).toThrow(/Must be between 1 and 4/) - }) - - it('falls back to the default cap for unknown models', () => { - expect(() => validateNumberOfImages('unknown-model', 4)).not.toThrow() - expect(() => validateNumberOfImages('unknown-model', 5)).toThrow( - /Must be between 1 and 4/, - ) - }) - - it('rejects 0 images', () => { - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', 0), - ).toThrow() - }) - - it('accepts undefined', () => { - expect(() => - validateNumberOfImages('imagen-4.0-generate-001', undefined), - ).not.toThrow() + expect(adapter.model).toBe('gemini-3.1-flash-image') }) }) describe('validatePrompt', () => { it('rejects empty prompts', () => { expect(() => - validatePrompt({ prompt: '', model: 'imagen-4.0-generate-001' }), + validatePrompt({ prompt: '', model: 'gemini-3.1-flash-image' }), ).toThrow() expect(() => - validatePrompt({ prompt: ' ', model: 'imagen-4.0-generate-001' }), + validatePrompt({ prompt: ' ', model: 'gemini-3.1-flash-image' }), ).toThrow() }) it('accepts non-empty prompts', () => { expect(() => - validatePrompt({ prompt: 'A cat', model: 'imagen-4.0-generate-001' }), + validatePrompt({ prompt: 'A cat', model: 'gemini-3.1-flash-image' }), ).not.toThrow() }) }) @@ -266,94 +138,6 @@ describe('Gemini Image Adapter', () => { }) describe('generateImages', () => { - it('calls the Gemini models.generateImages API for Imagen models', async () => { - const mockResponse = { - generatedImages: [ - { - image: { - imageBytes: 'base64encodedimage', - }, - }, - ], - } - - const mockGenerateImages = vi.fn().mockResolvedValueOnce(mockResponse) - - const adapter = createGeminiImage( - 'imagen-4.0-generate-001', - 'test-api-key', - ) - // Replace the internal Gemini SDK client with our mock - ;( - adapter as unknown as { - client: { models: { generateImages: unknown } } - } - ).client = { - models: { - generateImages: mockGenerateImages, - }, - } - - const result = await generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'A cat wearing a hat', - numberOfImages: 1, - size: '1024x1024', - }) - - expect(mockGenerateImages).toHaveBeenCalledWith({ - model: 'imagen-4.0-generate-001', - prompt: 'A cat wearing a hat', - config: { - numberOfImages: 1, - aspectRatio: '1:1', - }, - }) - - expect(result.model).toBe('imagen-4.0-generate-001') - expect(result.images).toHaveLength(1) - expect(result.images[0]!.b64Json).toBe('base64encodedimage') - }) - - it('generates a unique ID for each response', async () => { - const mockResponse = { - generatedImages: [{ image: { imageBytes: 'base64' } }], - } - - const mockGenerateImages = vi.fn().mockResolvedValue(mockResponse) - - const adapter = createGeminiImage( - 'imagen-4.0-generate-001', - 'test-api-key', - ) - ;( - adapter as unknown as { - client: { models: { generateImages: unknown } } - } - ).client = { - models: { - generateImages: mockGenerateImages, - }, - } - - const result1 = await generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'Test prompt', - }) - - const result2 = await generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'Test prompt', - }) - - expect(result1.id).not.toBe(result2.id) - expect(result1.id).toMatch(/^gemini-/) - expect(result2.id).toMatch(/^gemini-/) - }) - it('calls the Interactions API for Gemini image models', async () => { const mockCreate = vi .fn() @@ -458,12 +242,11 @@ describe('Gemini Image Adapter', () => { const mockCreate = vi .fn() .mockResolvedValueOnce(interactionWithImages('ga-flash-image')) - const mockGenerateImages = vi.fn() const adapter = createGeminiImage( 'gemini-3.1-flash-image', 'test-api-key', ) - stubCreate(adapter, mockCreate, mockGenerateImages) + stubCreate(adapter, mockCreate) const result = await generateImage({ threadId: 'thread-1', @@ -482,7 +265,6 @@ describe('Gemini Image Adapter', () => { image_size: '512', }, }) - expect(mockGenerateImages).not.toHaveBeenCalled() expect(result.images[0]!.b64Json).toBe('ga-flash-image') }) @@ -490,9 +272,8 @@ describe('Gemini Image Adapter', () => { const mockCreate = vi .fn() .mockResolvedValueOnce(interactionWithImages('ga-pro-image')) - const mockGenerateImages = vi.fn() const adapter = createGeminiImage('gemini-3-pro-image', 'test-api-key') - stubCreate(adapter, mockCreate, mockGenerateImages) + stubCreate(adapter, mockCreate) const result = await generateImage({ threadId: 'thread-1', @@ -511,7 +292,6 @@ describe('Gemini Image Adapter', () => { image_size: '2K', }, }) - expect(mockGenerateImages).not.toHaveBeenCalled() expect(result.images[0]!.b64Json).toBe('ga-pro-image') }) @@ -713,10 +493,6 @@ describe('Gemini Image Adapter', () => { }) describe('native modelOptions (Interactions API)', () => { - // Regression: GeminiImageModelProviderOptionsByName used to map every - // image model — native ones included — to the Imagen-shaped - // GeminiImageProviderOptions, so these fields were a compile error and the - // adapter whitelisted only `seed`. const safetySettings = [ { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, @@ -873,129 +649,6 @@ describe('Gemini Image Adapter', () => { { signal: controller.signal }, ) }) - - it('throws when Imagen is given previous_interaction_id', async () => { - const { adapter } = mockedImagenAdapter() - - await expect( - generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'A quiet harbour', - modelOptions: { previous_interaction_id: 'int_prev' } as never, - }), - ).rejects.toThrow(/previous_interaction_id/) - }) - - it('keeps Imagen models on GenerateImagesConfig with no native fields', async () => { - const { adapter, mockGenerateImages } = mockedImagenAdapter() - - await generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'A quiet harbour', - size: '1920x1080', - modelOptions: { - personGeneration: PersonGeneration.ALLOW_ADULT, - negativePrompt: 'blurry', - // Native-only fields. The per-model type rejects them (hence the - // cast, as in the responseModalities regression test above), but an - // adapter inferred from a union of model names widens modelOptions - // to both shapes, so they can still arrive here at runtime — and - // generateImages answers a GenerateContentConfig field with - // 400 INVALID_ARGUMENT. The named picks must drop them. - safetySettings: [ - { - category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, - threshold: HarmBlockThreshold.BLOCK_ONLY_HIGH, - }, - ], - thinkingConfig: { thinkingBudget: 512 }, - imageConfig: { imageSize: '2K' }, - systemInstruction: 'Always render in watercolor.', - } as unknown as never, - }) - - // Exact match: the Imagen path gets its own GenerateImagesConfig fields - // and nothing else. - expect(mockGenerateImages).toHaveBeenCalledWith({ - model: 'imagen-4.0-generate-001', - prompt: 'A quiet harbour', - config: { - numberOfImages: 1, - aspectRatio: '16:9', - personGeneration: PersonGeneration.ALLOW_ADULT, - negativePrompt: 'blurry', - }, - }) - }) - - it('forwards the whole Imagen option set to generateImages', async () => { - // The Imagen path picks fields by name, so this guards against a field - // being forgotten in that list. - const { adapter, mockGenerateImages } = mockedImagenAdapter() - - await generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'A quiet harbour', - modelOptions: { - aspectRatio: '21:9', - personGeneration: PersonGeneration.ALLOW_ADULT, - safetyFilterLevel: SafetyFilterLevel.BLOCK_ONLY_HIGH, - seed: 42, - addWatermark: false, - language: ImagePromptLanguage.en, - negativePrompt: 'blurry', - outputMimeType: 'image/jpeg', - outputCompressionQuality: 80, - guidanceScale: 12, - enhancePrompt: true, - includeSafetyAttributes: true, - includeRaiReason: true, - outputGcsUri: 'gs://bucket/out', - labels: { team: 'design' }, - }, - }) - - expect(mockGenerateImages).toHaveBeenCalledWith({ - model: 'imagen-4.0-generate-001', - prompt: 'A quiet harbour', - config: { - numberOfImages: 1, - aspectRatio: '21:9', - personGeneration: PersonGeneration.ALLOW_ADULT, - safetyFilterLevel: SafetyFilterLevel.BLOCK_ONLY_HIGH, - seed: 42, - addWatermark: false, - language: ImagePromptLanguage.en, - negativePrompt: 'blurry', - outputMimeType: 'image/jpeg', - outputCompressionQuality: 80, - guidanceScale: 12, - enhancePrompt: true, - includeSafetyAttributes: true, - includeRaiReason: true, - outputGcsUri: 'gs://bucket/out', - labels: { team: 'design' }, - }, - }) - }) - - it('lets modelOptions.aspectRatio override the size-derived one', async () => { - const { adapter, mockGenerateImages } = mockedImagenAdapter() - - await generateImage({ - threadId: 'thread-1', - adapter, - prompt: 'A quiet harbour', - size: '1920x1080', - modelOptions: { aspectRatio: '9:16' }, - }) - - const args = mockGenerateImages.mock.calls[0]![0] - expect(args.config.aspectRatio).toBe('9:16') - }) }) describe('multimodal prompt (image-conditioned generation)', () => { @@ -1181,27 +834,6 @@ describe('Gemini Image Adapter', () => { ).rejects.toThrow(/failed/) }) - it('rejects image prompt parts for Imagen models', async () => { - const adapter = createGeminiImage( - 'imagen-4.0-generate-001', - 'test-api-key', - ) - - await expect( - adapter.generateImages({ - model: 'imagen-4.0-generate-001', - prompt: [ - { type: 'text', content: 'Edit this' }, - { - type: 'image', - source: { type: 'data', value: 'aGk=', mimeType: 'image/png' }, - }, - ], - logger: testLogger, - }), - ).rejects.toThrow(/does not support image prompt parts/) - }) - it('rejects video and audio prompt parts', async () => { const adapter = createGeminiImage( 'gemini-3.1-flash-image', @@ -1238,14 +870,3 @@ describe('Gemini Image Adapter', () => { }) }) }) - -describe('GEMINI_NATIVE_IMAGE_MODELS public routing list', () => { - it('exports the same membership the adapter uses', () => { - expect(isGeminiNativeImageModel('gemini-3.1-flash-image')).toBe(true) - expect(isGeminiNativeImageModel('gemini-nano-banana-2.1')).toBe(true) - expect(isGeminiNativeImageModel('gemini-3-pro-image')).toBe(true) - expect(isGeminiNativeImageModel('imagen-4.0-generate-001')).toBe(false) - expect(isGeminiNativeImageModel('gemini-9-pro-image')).toBe(false) - expect(GEMINI_NATIVE_IMAGE_MODELS).toContain('gemini-3.1-flash-image') - }) -}) diff --git a/packages/ai-gemini/tests/image-per-model-type-safety.test.ts b/packages/ai-gemini/tests/image-per-model-type-safety.test.ts index 107dc7ea9f..5620b8cee0 100644 --- a/packages/ai-gemini/tests/image-per-model-type-safety.test.ts +++ b/packages/ai-gemini/tests/image-per-model-type-safety.test.ts @@ -168,23 +168,6 @@ describe('Gemini per-model image size gating', () => { }) }) - describe('Imagen models keep pixel sizes', () => { - it('accepts WIDTHxHEIGHT and rejects aspect-ratio sizes', () => { - createImageOptions({ - threadId: 'thread-1', - adapter: createGeminiImage('imagen-4.0-generate-001', apiKey), - prompt: 'a cat', - size: '1024x1024', - }) - createImageOptions({ - adapter: createGeminiImage('imagen-4.0-generate-001', apiKey), - prompt: 'a cat', - // @ts-expect-error - Imagen takes pixel sizes, not aspect-ratio sizes - size: '16:9_2K', - }) - }) - }) - describe('Model name type safety', () => { it('rejects unknown image model names at the factory', () => { // @ts-expect-error - 'gemini-9-pro-image' is not a Gemini image model @@ -247,19 +230,13 @@ describe('Gemini per-model image modelOptions gating', () => { }) }) - it('rejects Imagen-only options', () => { - // The probes are plain values that are structurally valid on - // GeminiImageProviderOptions (`negativePrompt: string`, - // `aspectRatio: GeminiAspectRatio`), so the errors below come from the - // native/Imagen split and nothing else — an enum-typed field such as - // `personGeneration` would reject a string literal on either shape and - // would still "pass" with the split reverted. + it('rejects GenerateImagesConfig options', () => { createImageOptions({ threadId: 'thread-1', adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { - // @ts-expect-error - negativePrompt is a GenerateImagesConfig (Imagen) field + // @ts-expect-error - negativePrompt is a GenerateImagesConfig field negativePrompt: 'blurry', }, }) @@ -269,7 +246,7 @@ describe('Gemini per-model image modelOptions gating', () => { adapter: geminiImage('gemini-3.1-flash-image'), prompt: 'a quiet harbour', modelOptions: { - // @ts-expect-error - aspectRatio is Imagen-only; native models use size / imageConfig + // @ts-expect-error - native models set the ratio with size / imageConfig aspectRatio: '16:9', }, }) @@ -287,44 +264,6 @@ describe('Gemini per-model image modelOptions gating', () => { }) }) }) - - describe('imagen-4.0-generate-001 — Imagen (GenerateImagesConfig)', () => { - it('accepts the Imagen option set', () => { - createImageOptions({ - threadId: 'thread-1', - adapter: geminiImage('imagen-4.0-generate-001'), - prompt: 'a quiet harbour', - modelOptions: { - aspectRatio: '16:9', - negativePrompt: 'blurry', - addWatermark: true, - outputMimeType: 'image/png', - }, - }) - }) - - it('rejects native-only options', () => { - createImageOptions({ - threadId: 'thread-1', - adapter: geminiImage('imagen-4.0-generate-001'), - prompt: 'a quiet harbour', - modelOptions: { - // @ts-expect-error - safetySettings is a native Interactions field - safetySettings: [], - }, - }) - - createImageOptions({ - threadId: 'thread-1', - adapter: geminiImage('imagen-4.0-generate-001'), - prompt: 'a quiet harbour', - modelOptions: { - // @ts-expect-error - previous_interaction_id is native Interactions only - previous_interaction_id: 'int_prev', - }, - }) - }) - }) }) describe('Gemini image provider options shape assertions', () => { @@ -355,21 +294,6 @@ describe('Gemini image provider options shape assertions', () => { }) }) - describe('Imagen models keep GenerateImagesConfig fields', () => { - type Options = - GeminiImageModelProviderOptionsByName['imagen-4.0-generate-001'] - - it('has aspectRatio', () => { - expectTypeOf().toHaveProperty('aspectRatio') - }) - it('has personGeneration', () => { - expectTypeOf().toHaveProperty('personGeneration') - }) - it('has negativePrompt', () => { - expectTypeOf().toHaveProperty('negativePrompt') - }) - }) - describe('every native model id resolves to the native shape', () => { it('gemini-3.1-flash-image', () => { expectTypeOf< diff --git a/packages/ai-groq/README.md b/packages/ai-groq/README.md index 8edd4401d3..264806a630 100644 --- a/packages/ai-groq/README.md +++ b/packages/ai-groq/README.md @@ -48,11 +48,11 @@ export GROQ_API_KEY="gsk_..." import { groqText } from '@tanstack/ai-groq' import { generate } from '@tanstack/ai' -const adapter = groqText('llama-3.3-70b-versatile') +const adapter = groqText('openai/gpt-oss-120b') const result = await generate({ adapter, - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [ { role: 'user', content: 'Explain quantum computing in simple terms' }, ], @@ -66,25 +66,19 @@ console.log(result.text) ```typescript import { createGroqText } from '@tanstack/ai-groq' -const adapter = createGroqText('llama-3.3-70b-versatile', 'gsk_api_key') +const adapter = createGroqText('openai/gpt-oss-120b', 'gsk_api_key') ``` ## Supported Models ### Chat Models -- `llama-3.3-70b-versatile` - Meta Llama 3.3 70B (131k context) -- `llama-3.1-8b-instant` - Meta Llama 3.1 8B (131k context) -- `meta-llama/llama-4-maverick-17b-128e-instruct` - Meta Llama 4 Maverick (vision) -- `meta-llama/llama-4-scout-17b-16e-instruct` - Meta Llama 4 Scout -- `meta-llama/llama-guard-4-12b` - Meta Llama Guard 4 (content moderation, vision) - `meta-llama/llama-prompt-guard-2-86m` - Meta Llama Prompt Guard (content moderation) - `meta-llama/llama-prompt-guard-2-22m` - Meta Llama Prompt Guard (content moderation) - `openai/gpt-oss-120b` - GPT OSS 120B (reasoning, tools, search) - `openai/gpt-oss-20b` - GPT OSS 20B (reasoning, search) - `openai/gpt-oss-safeguard-20b` - GPT OSS Safeguard 20B (content moderation, reasoning) -- `moonshotai/kimi-k2-instruct-0905` - Kimi K2 Instruct (262k context) -- `qwen/qwen3-32b` - Qwen3 32B (reasoning, tools) +- `qwen/qwen3.8-27b` - Qwen3.8 27B (reasoning, tools, vision) ## Features diff --git a/packages/ai-groq/src/adapters/summarize.ts b/packages/ai-groq/src/adapters/summarize.ts index e7796f9589..8863da63f2 100644 --- a/packages/ai-groq/src/adapters/summarize.ts +++ b/packages/ai-groq/src/adapters/summarize.ts @@ -17,14 +17,14 @@ export type GroqSummarizeModel = (typeof GROQ_CHAT_MODELS)[number] * Creates a Groq summarize adapter with explicit API key. * Type resolution happens here at the call site. * - * @param model - The model name (e.g., 'llama-3.3-70b-versatile') + * @param model - The model name (e.g., 'openai/gpt-oss-120b') * @param apiKey - Your Groq API key * @param config - Optional additional configuration * @returns Configured Groq summarize adapter instance with resolved types * * @example * ```typescript - * const adapter = createGroqSummarize('llama-3.3-70b-versatile', "gsk_..."); + * const adapter = createGroqSummarize('openai/gpt-oss-120b', "gsk_..."); * ``` */ export function createGroqSummarize( @@ -50,7 +50,7 @@ export function createGroqSummarize( * - `process.env` (Node.js) * - `window.env` (Browser with injected env) * - * @param model - The model name (e.g., 'llama-3.3-70b-versatile') + * @param model - The model name (e.g., 'openai/gpt-oss-120b') * @param config - Optional configuration (excluding apiKey which is auto-detected) * @returns Configured Groq summarize adapter instance with resolved types * @throws Error if GROQ_API_KEY is not found in environment @@ -58,7 +58,7 @@ export function createGroqSummarize( * @example * ```typescript * // Automatically uses GROQ_API_KEY from environment - * const adapter = groqSummarize('llama-3.3-70b-versatile'); + * const adapter = groqSummarize('openai/gpt-oss-120b'); * * await summarize({ * adapter, diff --git a/packages/ai-groq/src/adapters/text.ts b/packages/ai-groq/src/adapters/text.ts index 5427ff752f..42effc8e3e 100644 --- a/packages/ai-groq/src/adapters/text.ts +++ b/packages/ai-groq/src/adapters/text.ts @@ -232,7 +232,7 @@ async function* promoteGroqUsage( * * @example * ```typescript - * const adapter = createGroqText('llama-3.3-70b-versatile', "gsk_..."); + * const adapter = createGroqText('openai/gpt-oss-120b', "gsk_..."); * ``` */ export function createGroqText< @@ -250,7 +250,7 @@ export function createGroqText< * * @example * ```typescript - * const adapter = groqText('llama-3.3-70b-versatile'); + * const adapter = groqText('openai/gpt-oss-120b'); * ``` */ export function groqText( diff --git a/packages/ai-groq/src/model-meta.ts b/packages/ai-groq/src/model-meta.ts index 88f9d5da95..e34863d571 100644 --- a/packages/ai-groq/src/model-meta.ts +++ b/packages/ai-groq/src/model-meta.ts @@ -9,8 +9,8 @@ const EFFORT_LOW_TO_HIGH = { levels: { low: 'low', medium: 'medium', high: 'high' }, budget: false, } as const -const EFFORT_NONE_DEFAULT = { - levels: { off: 'none', high: 'default' }, +const EFFORT_NONE_LOW_TO_HIGH = { + levels: { off: 'none', low: 'low', medium: 'medium', high: 'high' }, budget: false, } as const @@ -51,90 +51,6 @@ interface ModelMeta { reasoning?: ModelReasoning } -const LLAMA_3_3_70B_VERSATILE = { - name: 'llama-3.3-70b-versatile', - context_window: 131_072, - max_completion_tokens: 32_768, - pricing: { - input: { - normal: 0.59, - }, - output: { - normal: 0.79, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -const LLAMA_4_MAVERICK_17B_128E_INSTRUCT = { - name: 'meta-llama/llama-4-maverick-17b-128e-instruct', - context_window: 131_072, - max_completion_tokens: 8_192, - pricing: { - input: { - normal: 0.2, - }, - output: { - normal: 0.6, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object', 'json_schema', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -const LLAMA_4_SCOUT_17B_16E_INSTRUCT = { - name: 'meta-llama/llama-4-scout-17b-16e-instruct', - context_window: 131_072, - max_completion_tokens: 8_192, - pricing: { - input: { - normal: 0.05, - }, - output: { - normal: 0.08, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -const LLAMA_GUARD_4_12B = { - name: 'meta-llama/llama-guard-4-12b', - context_window: 131_072, - max_completion_tokens: 1024, - pricing: { - input: { - normal: 0.2, - }, - output: { - normal: 0.2, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'json_object', 'content_moderation', 'vision'], - tools: [] as const, - }, -} as const satisfies ModelMeta - const LLAMA_PROMPT_GUARD_2_86M = { name: 'meta-llama/llama-prompt-guard-2-86m', context_window: 512, @@ -156,27 +72,6 @@ const LLAMA_PROMPT_GUARD_2_86M = { }, } as const satisfies ModelMeta -const LLAMA_3_1_8B_INSTANT = { - name: 'llama-3.1-8b-instant', - context_window: 131_072, - max_completion_tokens: 131_072, - pricing: { - input: { - normal: 0.05, - }, - output: { - normal: 0.08, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'json_object', 'tools'], - tools: [] as const, - }, -} as const satisfies ModelMeta - const LLAMA_PROMPT_GUARD_2_22M = { name: 'meta-llama/llama-prompt-guard-2-22m', context_window: 512, @@ -292,63 +187,42 @@ const GPT_OSS_20B = { reasoning: EFFORT_LOW_TO_HIGH, } as const satisfies ModelMeta -const KIMI_K2_INSTRUCT_0905 = { - name: 'moonshotai/kimi-k2-instruct-0905', - context_window: 262_144, - max_completion_tokens: 16_384, - pricing: { - input: { - normal: 1, - cached: 0.5, - }, - output: { - normal: 3, - }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object', 'json_schema'], - tools: [] as const, - }, -} as const satisfies ModelMeta - -const QWEN3_32B = { - name: 'qwen/qwen3-32b', +const QWEN_QWEN3_8_27B = { + name: 'qwen/qwen3.8-27b', context_window: 131_072, - max_completion_tokens: 40_960, + max_completion_tokens: 16_384, pricing: { input: { - normal: 0.29, + normal: 0.8, }, output: { - normal: 0.59, + normal: 4, }, }, supports: { - input: ['text'], + input: ['text', 'image'], output: ['text'], endpoints: ['chat'], - features: ['streaming', 'json_object', 'tools', 'reasoning'], + features: [ + 'streaming', + 'tools', + 'json_object', + 'json_schema', + 'reasoning', + 'vision', + ], tools: [] as const, }, - reasoning: EFFORT_NONE_DEFAULT, + reasoning: EFFORT_NONE_LOW_TO_HIGH, } as const satisfies ModelMeta const GROQ_CHAT_MODEL_META = [ - LLAMA_3_1_8B_INSTANT, - LLAMA_3_3_70B_VERSATILE, - LLAMA_4_MAVERICK_17B_128E_INSTRUCT, - LLAMA_4_SCOUT_17B_16E_INSTRUCT, - LLAMA_GUARD_4_12B, LLAMA_PROMPT_GUARD_2_86M, LLAMA_PROMPT_GUARD_2_22M, GPT_OSS_20B, GPT_OSS_120B, GPT_OSS_SAFEGUARD_20B, - KIMI_K2_INSTRUCT_0905, - QWEN3_32B, + QWEN_QWEN3_8_27B, ] as const /** @@ -366,18 +240,12 @@ export function groqModelPrefersJsonObject(model: string): boolean { * All supported Groq chat model identifiers. */ export const GROQ_CHAT_MODELS = [ - LLAMA_3_1_8B_INSTANT.name, - LLAMA_3_3_70B_VERSATILE.name, - LLAMA_4_MAVERICK_17B_128E_INSTRUCT.name, - LLAMA_4_SCOUT_17B_16E_INSTRUCT.name, - LLAMA_GUARD_4_12B.name, LLAMA_PROMPT_GUARD_2_86M.name, LLAMA_PROMPT_GUARD_2_22M.name, GPT_OSS_20B.name, GPT_OSS_120B.name, GPT_OSS_SAFEGUARD_20B.name, - KIMI_K2_INSTRUCT_0905.name, - QWEN3_32B.name, + QWEN_QWEN3_8_27B.name, ] as const /** @@ -389,18 +257,12 @@ export type GroqChatModels = (typeof GROQ_CHAT_MODELS)[number] * Type-only map from Groq chat model name to its supported input modalities. */ export type GroqModelInputModalitiesByName = { - [LLAMA_3_1_8B_INSTANT.name]: typeof LLAMA_3_1_8B_INSTANT.supports.input - [LLAMA_3_3_70B_VERSATILE.name]: typeof LLAMA_3_3_70B_VERSATILE.supports.input - [LLAMA_4_MAVERICK_17B_128E_INSTRUCT.name]: typeof LLAMA_4_MAVERICK_17B_128E_INSTRUCT.supports.input - [LLAMA_4_SCOUT_17B_16E_INSTRUCT.name]: typeof LLAMA_4_SCOUT_17B_16E_INSTRUCT.supports.input - [LLAMA_GUARD_4_12B.name]: typeof LLAMA_GUARD_4_12B.supports.input [LLAMA_PROMPT_GUARD_2_86M.name]: typeof LLAMA_PROMPT_GUARD_2_86M.supports.input [LLAMA_PROMPT_GUARD_2_22M.name]: typeof LLAMA_PROMPT_GUARD_2_22M.supports.input [GPT_OSS_20B.name]: typeof GPT_OSS_20B.supports.input [GPT_OSS_120B.name]: typeof GPT_OSS_120B.supports.input [GPT_OSS_SAFEGUARD_20B.name]: typeof GPT_OSS_SAFEGUARD_20B.supports.input - [KIMI_K2_INSTRUCT_0905.name]: typeof KIMI_K2_INSTRUCT_0905.supports.input - [QWEN3_32B.name]: typeof QWEN3_32B.supports.input + [QWEN_QWEN3_8_27B.name]: typeof QWEN_QWEN3_8_27B.supports.input } /** @@ -411,20 +273,12 @@ export type GroqModelInputModalitiesByName = { export const GROQ_MODEL_INPUT_MODALITIES: Readonly< Record> > = { - [LLAMA_3_1_8B_INSTANT.name]: LLAMA_3_1_8B_INSTANT.supports.input, - [LLAMA_3_3_70B_VERSATILE.name]: LLAMA_3_3_70B_VERSATILE.supports.input, - [LLAMA_4_MAVERICK_17B_128E_INSTRUCT.name]: - LLAMA_4_MAVERICK_17B_128E_INSTRUCT.supports.input, - [LLAMA_4_SCOUT_17B_16E_INSTRUCT.name]: - LLAMA_4_SCOUT_17B_16E_INSTRUCT.supports.input, - [LLAMA_GUARD_4_12B.name]: LLAMA_GUARD_4_12B.supports.input, [LLAMA_PROMPT_GUARD_2_86M.name]: LLAMA_PROMPT_GUARD_2_86M.supports.input, [LLAMA_PROMPT_GUARD_2_22M.name]: LLAMA_PROMPT_GUARD_2_22M.supports.input, [GPT_OSS_20B.name]: GPT_OSS_20B.supports.input, [GPT_OSS_120B.name]: GPT_OSS_120B.supports.input, [GPT_OSS_SAFEGUARD_20B.name]: GPT_OSS_SAFEGUARD_20B.supports.input, - [KIMI_K2_INSTRUCT_0905.name]: KIMI_K2_INSTRUCT_0905.supports.input, - [QWEN3_32B.name]: QWEN3_32B.supports.input, + [QWEN_QWEN3_8_27B.name]: QWEN_QWEN3_8_27B.supports.input, } satisfies GroqModelInputModalitiesByName /** @@ -441,18 +295,12 @@ export type GroqChatModelProviderOptionsByName = { * a Groq adapter produces a compile-time type error. */ export type GroqChatModelToolCapabilitiesByName = { - [LLAMA_3_1_8B_INSTANT.name]: typeof LLAMA_3_1_8B_INSTANT.supports.tools - [LLAMA_3_3_70B_VERSATILE.name]: typeof LLAMA_3_3_70B_VERSATILE.supports.tools - [LLAMA_4_MAVERICK_17B_128E_INSTRUCT.name]: typeof LLAMA_4_MAVERICK_17B_128E_INSTRUCT.supports.tools - [LLAMA_4_SCOUT_17B_16E_INSTRUCT.name]: typeof LLAMA_4_SCOUT_17B_16E_INSTRUCT.supports.tools - [LLAMA_GUARD_4_12B.name]: typeof LLAMA_GUARD_4_12B.supports.tools [LLAMA_PROMPT_GUARD_2_86M.name]: typeof LLAMA_PROMPT_GUARD_2_86M.supports.tools [LLAMA_PROMPT_GUARD_2_22M.name]: typeof LLAMA_PROMPT_GUARD_2_22M.supports.tools [GPT_OSS_20B.name]: typeof GPT_OSS_20B.supports.tools [GPT_OSS_120B.name]: typeof GPT_OSS_120B.supports.tools [GPT_OSS_SAFEGUARD_20B.name]: typeof GPT_OSS_SAFEGUARD_20B.supports.tools - [KIMI_K2_INSTRUCT_0905.name]: typeof KIMI_K2_INSTRUCT_0905.supports.tools - [QWEN3_32B.name]: typeof QWEN3_32B.supports.tools + [QWEN_QWEN3_8_27B.name]: typeof QWEN_QWEN3_8_27B.supports.tools } /** @@ -554,7 +402,7 @@ const GROQ_REASONING = { [GPT_OSS_20B.name]: GPT_OSS_20B.reasoning, [GPT_OSS_120B.name]: GPT_OSS_120B.reasoning, [GPT_OSS_SAFEGUARD_20B.name]: GPT_OSS_SAFEGUARD_20B.reasoning, - [QWEN3_32B.name]: QWEN3_32B.reasoning, + [QWEN_QWEN3_8_27B.name]: QWEN_QWEN3_8_27B.reasoning, } /** diff --git a/packages/ai-groq/tests/groq-adapter.test.ts b/packages/ai-groq/tests/groq-adapter.test.ts index 18495b3ca3..da42f4b052 100644 --- a/packages/ai-groq/tests/groq-adapter.test.ts +++ b/packages/ai-groq/tests/groq-adapter.test.ts @@ -107,38 +107,34 @@ describe('Groq adapters', () => { describe('Text adapter', () => { it('creates a text adapter with explicit API key', () => { - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') expect(adapter).toBeDefined() expect(adapter.kind).toBe('text') expect(adapter.name).toBe('groq') - expect(adapter.model).toBe('llama-3.3-70b-versatile') + expect(adapter.model).toBe('openai/gpt-oss-120b') }) it('creates a text adapter from environment variable', () => { vi.stubEnv('GROQ_API_KEY', 'env-api-key') - const adapter = groqText('llama-3.1-8b-instant') + const adapter = groqText('openai/gpt-oss-20b') expect(adapter).toBeDefined() expect(adapter.kind).toBe('text') - expect(adapter.model).toBe('llama-3.1-8b-instant') + expect(adapter.model).toBe('openai/gpt-oss-20b') }) it('throws if GROQ_API_KEY is not set when using groqText', () => { vi.stubEnv('GROQ_API_KEY', '') - expect(() => groqText('llama-3.3-70b-versatile')).toThrow('GROQ_API_KEY') + expect(() => groqText('openai/gpt-oss-120b')).toThrow('GROQ_API_KEY') }) it('allows custom baseURL override', () => { - const adapter = createGroqText( - 'llama-3.3-70b-versatile', - 'test-api-key', - { - baseURL: 'https://custom.api.example.com/v1', - }, - ) + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key', { + baseURL: 'https://custom.api.example.com/v1', + }) expect(adapter).toBeDefined() }) @@ -147,12 +143,15 @@ describe('Groq adapters', () => { const mockCreate = setupMockSdkClient([], { choices: [{ message: { content: '{"name":"Ada"}' } }], }) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText( + 'meta-llama/llama-prompt-guard-2-86m', + 'test-api-key', + ) await adapter.structuredOutput({ chatOptions: { threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'meta-llama/llama-prompt-guard-2-86m', messages: [{ role: 'user', content: 'Return a person as json' }], logger: testLogger, }, @@ -179,15 +178,12 @@ describe('Groq adapters', () => { const mockCreate = setupMockSdkClient([], { choices: [{ message: { content: '{"name":"Ada"}' } }], }) - const adapter = createGroqText( - 'moonshotai/kimi-k2-instruct-0905', - 'test-api-key', - ) + const adapter = createGroqText('qwen/qwen3.8-27b', 'test-api-key') await adapter.structuredOutput({ chatOptions: { threadId: 'thread-1', - model: 'moonshotai/kimi-k2-instruct-0905', + model: 'qwen/qwen3.8-27b', messages: [{ role: 'user', content: 'Return a person as json' }], logger: testLogger, }, @@ -204,7 +200,7 @@ describe('Groq adapters', () => { }) it('opts out of native combined tools+schema mode (#605) — Groq API rejects the combination', () => { - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') // The OpenAI Chat Completions base defaults to `true`; Groq must // override to `false` because the upstream API returns 400 on // `response_format` + `tools` + `stream`. @@ -215,7 +211,7 @@ describe('Groq adapters', () => { const streamChunks = [ { id: 'chatcmpl-sampling', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [{ delta: {}, finish_reason: 'stop' }], x_groq: { usage: { prompt_tokens: 1, completion_tokens: 0, total_tokens: 1 }, @@ -224,7 +220,7 @@ describe('Groq adapters', () => { ] const mockCreate = setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const modelOptions: GroqTextProviderOptions = { temperature: 0.5, @@ -234,7 +230,7 @@ describe('Groq adapters', () => { for await (const _ of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Hello' }], modelOptions, logger: testLogger, @@ -253,21 +249,18 @@ describe('Groq adapters', () => { describe('Summarize adapter', () => { it('creates a summarize adapter with explicit API key', () => { - const adapter = createGroqSummarize( - 'llama-3.3-70b-versatile', - 'test-api-key', - ) + const adapter = createGroqSummarize('openai/gpt-oss-120b', 'test-api-key') expect(adapter).toBeDefined() expect(adapter.kind).toBe('summarize') expect(adapter.name).toBe('groq') - expect(adapter.model).toBe('llama-3.3-70b-versatile') + expect(adapter.model).toBe('openai/gpt-oss-120b') }) it('creates a summarize adapter from environment variable', () => { vi.stubEnv('GROQ_API_KEY', 'env-api-key') - const adapter = groqSummarize('llama-3.3-70b-versatile') + const adapter = groqSummarize('openai/gpt-oss-120b') expect(adapter).toBeDefined() expect(adapter.kind).toBe('summarize') @@ -276,7 +269,7 @@ describe('Groq adapters', () => { it('throws if GROQ_API_KEY is not set when using groqSummarize', () => { vi.stubEnv('GROQ_API_KEY', '') - expect(() => groqSummarize('llama-3.3-70b-versatile')).toThrow( + expect(() => groqSummarize('openai/gpt-oss-120b')).toThrow( 'GROQ_API_KEY is required', ) }) @@ -296,7 +289,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'Hello' }, @@ -306,7 +299,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: {}, @@ -324,12 +317,12 @@ describe('Groq AG-UI event emission', () => { ] setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Hello' }], logger: testLogger, })) { @@ -339,7 +332,7 @@ describe('Groq AG-UI event emission', () => { expect(chunks[0]?.type).toBe('RUN_STARTED') if (chunks[0]?.type === 'RUN_STARTED') { expect(chunks[0].runId).toBeDefined() - expect(chunks[0].model).toBe('llama-3.3-70b-versatile') + expect(chunks[0].model).toBe('openai/gpt-oss-120b') } }) @@ -347,7 +340,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'Hello' }, @@ -357,7 +350,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: {}, @@ -375,12 +368,12 @@ describe('Groq AG-UI event emission', () => { ] setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Hello' }], logger: testLogger, })) { @@ -409,7 +402,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'Hello' }, @@ -419,7 +412,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: {}, @@ -437,12 +430,12 @@ describe('Groq AG-UI event emission', () => { ] setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Hello' }], logger: testLogger, })) { @@ -472,7 +465,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-456', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { @@ -494,7 +487,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-456', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { @@ -513,7 +506,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-456', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: {}, @@ -531,12 +524,12 @@ describe('Groq AG-UI event emission', () => { ] setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Weather in Berlin?' }], tools: [weatherTool], logger: testLogger, @@ -587,11 +580,11 @@ describe('Groq AG-UI event emission', () => { }), ) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Weather in Berlin?' }], tools: [weatherTool], logger: testLogger, @@ -648,11 +641,11 @@ describe('Groq AG-UI event emission', () => { } pendingMockCreate = vi.fn().mockResolvedValue(errorIterable) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Weather in Berlin?' }], tools: [weatherTool], logger: testLogger, @@ -699,11 +692,11 @@ describe('Groq AG-UI event emission', () => { }), ) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Weather in Berlin?' }], tools: [weatherTool], logger: testLogger, @@ -721,7 +714,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'Hello' }, @@ -748,12 +741,12 @@ describe('Groq AG-UI event emission', () => { pendingMockCreate = vi.fn().mockResolvedValue(errorIterable) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Hello' }], logger: testLogger, })) { @@ -772,7 +765,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'Hello world' }, @@ -782,7 +775,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-123', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: {}, @@ -800,12 +793,12 @@ describe('Groq AG-UI event emission', () => { ] setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Hello' }], logger: testLogger, })) { @@ -842,7 +835,7 @@ describe('Groq AG-UI event emission', () => { const streamChunks = [ { id: 'chatcmpl-stream', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'Hello ' }, @@ -852,7 +845,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-stream', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: { content: 'world' }, @@ -862,7 +855,7 @@ describe('Groq AG-UI event emission', () => { }, { id: 'chatcmpl-stream', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', choices: [ { delta: {}, @@ -880,12 +873,12 @@ describe('Groq AG-UI event emission', () => { ] setupMockSdkClient(streamChunks) - const adapter = createGroqText('llama-3.3-70b-versatile', 'test-api-key') + const adapter = createGroqText('openai/gpt-oss-120b', 'test-api-key') const chunks: Array = [] for await (const chunk of adapter.chatStream({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', messages: [{ role: 'user', content: 'Say hello' }], logger: testLogger, })) { diff --git a/packages/ai-groq/tests/input-modalities.test.ts b/packages/ai-groq/tests/input-modalities.test.ts index 3229ee42e8..91009875f0 100644 --- a/packages/ai-groq/tests/input-modalities.test.ts +++ b/packages/ai-groq/tests/input-modalities.test.ts @@ -3,8 +3,8 @@ import { createGroqText } from '../src/adapters/text' describe('GroqTextAdapter inputModalities', () => { it.each([ - ['meta-llama/llama-4-scout-17b-16e-instruct', ['text', 'image']], - ['llama-3.3-70b-versatile', ['text']], + ['qwen/qwen3.8-27b', ['text', 'image']], + ['openai/gpt-oss-120b', ['text']], ] as const)('reads the list of %s from model-meta', (model, expected) => { expect(createGroqText(model, 'test-key').inputModalities).toEqual(expected) }) diff --git a/packages/ai-groq/tests/reasoning-request.test.ts b/packages/ai-groq/tests/reasoning-request.test.ts index 2266c54e0f..864bb2d7cf 100644 --- a/packages/ai-groq/tests/reasoning-request.test.ts +++ b/packages/ai-groq/tests/reasoning-request.test.ts @@ -28,10 +28,10 @@ describe('Groq chat({ reasoning }) request shape', () => { }) }) - it('Qwen 3: default to think, none for off', () => { - const adapter = new Probe({ apiKey: 'k' }, 'qwen/qwen3-32b') + it('Qwen 3.8: effort from the level, none for off', () => { + const adapter = new Probe({ apiKey: 'k' }, 'qwen/qwen3.8-27b') expect(adapter.request({ level: 'high', summary: true })).toMatchObject({ - reasoning_effort: 'default', + reasoning_effort: 'high', }) expect(adapter.request({ level: 'off', summary: true })).toMatchObject({ reasoning_effort: 'none', diff --git a/packages/ai-groq/tests/reasoning-types.test-d.ts b/packages/ai-groq/tests/reasoning-types.test-d.ts index 76c4f43ee2..ca4782603b 100644 --- a/packages/ai-groq/tests/reasoning-types.test-d.ts +++ b/packages/ai-groq/tests/reasoning-types.test-d.ts @@ -5,7 +5,7 @@ const messages = [{ role: 'user' as const, content: 'hi' }] chat({ threadId: 'thread-1', - adapter: createGroqText('qwen/qwen3-32b', 'k'), + adapter: createGroqText('qwen/qwen3.8-27b', 'k'), messages, reasoning: 'off', }) diff --git a/packages/ai-groq/tests/tool-choice-wire.test.ts b/packages/ai-groq/tests/tool-choice-wire.test.ts index a52f80fac1..efbebf0675 100644 --- a/packages/ai-groq/tests/tool-choice-wire.test.ts +++ b/packages/ai-groq/tests/tool-choice-wire.test.ts @@ -4,7 +4,7 @@ import { createGroqText } from '../src/adapters/text' import type { ModelMessage, Tool, ToolChoice } from '@tanstack/ai' const logger = resolveDebugOption(false) -const model = 'llama-3.3-70b-versatile' +const model = 'openai/gpt-oss-120b' const messages: Array = [ { role: 'user', content: 'Weather in Paris?' }, ] diff --git a/packages/ai-groq/tests/wrap-fetch-wire.test.ts b/packages/ai-groq/tests/wrap-fetch-wire.test.ts index 920d54b66c..9ef978493b 100644 --- a/packages/ai-groq/tests/wrap-fetch-wire.test.ts +++ b/packages/ai-groq/tests/wrap-fetch-wire.test.ts @@ -4,7 +4,7 @@ import { createGroqText } from '../src/adapters/text' import type { FetchWrapper } from '@tanstack/ai' const logger = resolveDebugOption(false) -const model = 'llama-3.3-70b-versatile' +const model = 'openai/gpt-oss-120b' const messages = [{ role: 'user' as const, content: 'Hi' }] /** A wrapper that adds one header, then calls the next fetch. */ diff --git a/packages/ai-mistral/README.md b/packages/ai-mistral/README.md index 9cef5aa535..d4dbf08034 100644 --- a/packages/ai-mistral/README.md +++ b/packages/ai-mistral/README.md @@ -82,11 +82,8 @@ const adapter = createMistralText( - `ministral-8b-latest` - 8B edge model - `ministral-3b-latest` - 3B edge model - `codestral-latest` - Code-specialized model (256k context) -- `pixtral-large-latest` - Large vision model -- `pixtral-12b-2409` - 12B vision model - `magistral-medium-latest` - Reasoning model - `magistral-small-latest` - Small reasoning model -- `open-mistral-nemo` - Open 12B model See [Mistral model comparison](https://docs.mistral.ai/getting-started/models/compare) for full details. @@ -96,7 +93,7 @@ See [Mistral model comparison](https://docs.mistral.ai/getting-started/models/co - ✅ Structured output (JSON Schema) - ✅ Function/tool calling - ✅ Reasoning (magistral-\_ models — streamed as `REASONING\__` events) -- ✅ Multimodal input (text + images) — requires a vision-capable model (`pixtral-large-latest`, `pixtral-12b-2409`, `mistral-medium-latest`, or `mistral-small-latest`) +- ✅ Multimodal input (text + images) — requires a vision-capable model (`mistral-medium-latest` or `mistral-small-latest`) - ❌ Embeddings (use [@mistralai/mistralai](https://github.com/mistralai/client-ts) directly) - ❌ Image generation diff --git a/packages/ai-mistral/src/input-modalities.test.ts b/packages/ai-mistral/src/input-modalities.test.ts index ec5cfd6e34..085ee3e2d7 100644 --- a/packages/ai-mistral/src/input-modalities.test.ts +++ b/packages/ai-mistral/src/input-modalities.test.ts @@ -10,7 +10,7 @@ describe('Mistral text adapter inputModalities', () => { new MistralTextAdapter(config, 'mistral-large-latest').inputModalities, ).toEqual(['text']) expect( - new MistralTextAdapter(config, 'pixtral-large-latest').inputModalities, + new MistralTextAdapter(config, 'mistral-medium-latest').inputModalities, ).toEqual(['text', 'image', 'document']) }) diff --git a/packages/ai-mistral/src/model-meta.ts b/packages/ai-mistral/src/model-meta.ts index deeccbb1ea..67660d1e30 100644 --- a/packages/ai-mistral/src/model-meta.ts +++ b/packages/ai-mistral/src/model-meta.ts @@ -17,7 +17,7 @@ const EFFORT_OFF_HIGH = { budget: false, } as const -/** Provider options for vision-capable Mistral models (pixtral-*). */ +/** Provider options for vision-capable Mistral models. */ export type MistralVisionProviderOptions = MistralTextProviderOptions /** Provider options for reasoning-capable Mistral models (magistral-*). */ @@ -153,38 +153,6 @@ const CODESTRAL_LATEST = { }, } as const satisfies ModelMeta -const PIXTRAL_LARGE_LATEST = { - name: 'pixtral-large-latest', - context_window: 131_072, - max_completion_tokens: 8_192, - pricing: { - input: { normal: 2 }, - output: { normal: 6 }, - }, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object', 'json_schema', 'vision'], - }, -} as const satisfies ModelMeta - -const PIXTRAL_12B_2409 = { - name: 'pixtral-12b-2409', - context_window: 131_072, - max_completion_tokens: 8_192, - pricing: { - input: { normal: 0.15 }, - output: { normal: 0.15 }, - }, - supports: { - input: ['text', 'image', 'document'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object', 'vision'], - }, -} as const satisfies ModelMeta - const MAGISTRAL_MEDIUM_LATEST = { name: 'magistral-medium-latest', context_window: 40_000, @@ -220,22 +188,6 @@ const MAGISTRAL_SMALL_LATEST = { reasoning: EFFORT_OFF_HIGH, } as const satisfies ModelMeta -const OPEN_MISTRAL_NEMO = { - name: 'open-mistral-nemo', - context_window: 131_072, - max_completion_tokens: 8_192, - pricing: { - input: { normal: 0.15 }, - output: { normal: 0.15 }, - }, - supports: { - input: ['text'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'tools', 'json_object'], - }, -} as const satisfies ModelMeta - // Vertex Model Garden IDs. These are not the Mistral API catalog. // Source: https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/mistral const MISTRAL_MEDIUM_3 = { @@ -296,11 +248,8 @@ export const MISTRAL_CHAT_MODELS = [ MINISTRAL_8B_LATEST.name, MINISTRAL_3B_LATEST.name, CODESTRAL_LATEST.name, - PIXTRAL_LARGE_LATEST.name, - PIXTRAL_12B_2409.name, MAGISTRAL_MEDIUM_LATEST.name, MAGISTRAL_SMALL_LATEST.name, - OPEN_MISTRAL_NEMO.name, ] as const /** @@ -332,11 +281,8 @@ export type MistralModelInputModalitiesByName = { [MINISTRAL_8B_LATEST.name]: typeof MINISTRAL_8B_LATEST.supports.input [MINISTRAL_3B_LATEST.name]: typeof MINISTRAL_3B_LATEST.supports.input [CODESTRAL_LATEST.name]: typeof CODESTRAL_LATEST.supports.input - [PIXTRAL_LARGE_LATEST.name]: typeof PIXTRAL_LARGE_LATEST.supports.input - [PIXTRAL_12B_2409.name]: typeof PIXTRAL_12B_2409.supports.input [MAGISTRAL_MEDIUM_LATEST.name]: typeof MAGISTRAL_MEDIUM_LATEST.supports.input [MAGISTRAL_SMALL_LATEST.name]: typeof MAGISTRAL_SMALL_LATEST.supports.input - [OPEN_MISTRAL_NEMO.name]: typeof OPEN_MISTRAL_NEMO.supports.input [MISTRAL_MEDIUM_3.name]: typeof MISTRAL_MEDIUM_3.supports.input [MISTRAL_SMALL_2503.name]: typeof MISTRAL_SMALL_2503.supports.input [CODESTRAL_2.name]: typeof CODESTRAL_2.supports.input @@ -356,11 +302,8 @@ export const MISTRAL_MODEL_INPUT_MODALITIES: Readonly< [MINISTRAL_8B_LATEST.name]: MINISTRAL_8B_LATEST.supports.input, [MINISTRAL_3B_LATEST.name]: MINISTRAL_3B_LATEST.supports.input, [CODESTRAL_LATEST.name]: CODESTRAL_LATEST.supports.input, - [PIXTRAL_LARGE_LATEST.name]: PIXTRAL_LARGE_LATEST.supports.input, - [PIXTRAL_12B_2409.name]: PIXTRAL_12B_2409.supports.input, [MAGISTRAL_MEDIUM_LATEST.name]: MAGISTRAL_MEDIUM_LATEST.supports.input, [MAGISTRAL_SMALL_LATEST.name]: MAGISTRAL_SMALL_LATEST.supports.input, - [OPEN_MISTRAL_NEMO.name]: OPEN_MISTRAL_NEMO.supports.input, [MISTRAL_MEDIUM_3.name]: MISTRAL_MEDIUM_3.supports.input, [MISTRAL_SMALL_2503.name]: MISTRAL_SMALL_2503.supports.input, [CODESTRAL_2.name]: CODESTRAL_2.supports.input, @@ -376,11 +319,8 @@ export type MistralChatModelProviderOptionsByName = { [MINISTRAL_8B_LATEST.name]: MistralTextProviderOptions [MINISTRAL_3B_LATEST.name]: MistralTextProviderOptions [CODESTRAL_LATEST.name]: MistralTextProviderOptions - [PIXTRAL_LARGE_LATEST.name]: MistralVisionProviderOptions - [PIXTRAL_12B_2409.name]: MistralVisionProviderOptions [MAGISTRAL_MEDIUM_LATEST.name]: MistralReasoningProviderOptions [MAGISTRAL_SMALL_LATEST.name]: MistralReasoningProviderOptions - [OPEN_MISTRAL_NEMO.name]: MistralTextProviderOptions [MISTRAL_MEDIUM_3.name]: MistralVisionProviderOptions [MISTRAL_SMALL_2503.name]: MistralVisionProviderOptions [CODESTRAL_2.name]: MistralTextProviderOptions diff --git a/packages/ai-openai/src/adapters/azure-text.ts b/packages/ai-openai/src/adapters/azure-text.ts index ad86b55301..eb8c9732fe 100644 --- a/packages/ai-openai/src/adapters/azure-text.ts +++ b/packages/ai-openai/src/adapters/azure-text.ts @@ -164,7 +164,7 @@ export class AzureOpenAITextAdapter< * * @example * ```typescript - * const adapter = createAzureOpenaiText('gpt-5.6', 'your-azure-key', { + * const adapter = createAzureOpenaiText('gpt-6.1-sol', 'your-azure-key', { * resourceName: 'my-resource', * deploymentName: 'prod-chat', * }); @@ -194,7 +194,7 @@ export function createAzureOpenaiText( * @example * ```typescript * // Automatically uses AZURE_OPENAI_API_KEY and AZURE_OPENAI_RESOURCE_NAME - * const adapter = azureOpenaiText('gpt-5.6'); + * const adapter = azureOpenaiText('gpt-6.1-sol'); * ``` */ export function azureOpenaiText( diff --git a/packages/ai-openai/src/adapters/image.ts b/packages/ai-openai/src/adapters/image.ts index 0974a30375..fecaf83d4e 100644 --- a/packages/ai-openai/src/adapters/image.ts +++ b/packages/ai-openai/src/adapters/image.ts @@ -28,17 +28,8 @@ import type { } from '../image/image-provider-options' import type { OpenAIClientConfig } from '../utils/client' -// Per OpenAI docs: dall-e-2 accepts 1 image to `images.edit()`; the -// gpt-image models accept up to 16; dall-e-3 does not support edit at all. -const EDIT_MAX_IMAGES: Record = { - 'gpt-image-2.5-flare': 16, - 'gpt-image-2.5-sunburst': 16, - 'dall-e-2': 1, - 'gpt-image-1': 16, - 'gpt-image-1-mini': 16, - 'gpt-image-2': 16, - 'dall-e-3': 0, -} +// Per OpenAI docs: the gpt-image models accept up to 16 images to `images.edit()`. +const EDIT_MAX_IMAGES = 16 /** * Configuration for OpenAI image adapter @@ -60,7 +51,7 @@ export interface OpenAIImageConfig extends OpenAIClientConfig { * * Tree-shakeable adapter for OpenAI image generation functionality. * Supports gpt-image-2.5-flare, gpt-image-2.5-sunburst, gpt-image-2, - * gpt-image-1, gpt-image-1-mini, dall-e-3, and dall-e-2 models. + * gpt-image-1, and gpt-image-1-mini models. * * Features: * - Model-specific type-safe provider options @@ -99,7 +90,7 @@ export class OpenAIImageAdapter< validatePrompt({ prompt, model }) validateImageSize(model, size) - validateNumberOfImages(model, numberOfImages) + validateNumberOfImages(numberOfImages) if (resolved.videos.length > 0) { throw new Error( @@ -198,8 +189,7 @@ export class OpenAIImageAdapter< /** * Image-conditioned generation via OpenAI's `images.edit()` endpoint. - * dall-e-2 accepts 1 input image; the gpt-image models accept up to 16; - * dall-e-3 rejects entirely. A part with `metadata.role === 'mask'` is + * The gpt-image models accept up to 16 input images. A part with `metadata.role === 'mask'` is * routed to the SDK's `mask` field (PNG with alpha channel). */ private async editImages(args: { @@ -212,14 +202,6 @@ export class OpenAIImageAdapter< logger: ImageGenerationOptions['logger'] }): Promise { const { model, prompt, numberOfImages, size, modelOptions, logger } = args - const maxImages = EDIT_MAX_IMAGES[model] - if (maxImages === 0) { - throw new Error( - `${this.name}: model "${model}" does not support image prompt parts. ` + - `Use a gpt-image model or dall-e-2 for image-conditioned generation.`, - ) - } - const maskParts = args.imageInputs.filter( (part) => part.metadata?.role === 'mask', ) @@ -237,9 +219,9 @@ export class OpenAIImageAdapter< `${this.name}: the prompt contained only mask image parts; at least one source image is required.`, ) } - if (sourceParts.length > maxImages) { + if (sourceParts.length > EDIT_MAX_IMAGES) { throw new Error( - `${this.name}: model "${model}" accepts at most ${maxImages} source image(s); received ${sourceParts.length}.`, + `${this.name}: model "${model}" accepts at most ${EDIT_MAX_IMAGES} source image(s); received ${sourceParts.length}.`, ) } @@ -253,10 +235,8 @@ export class OpenAIImageAdapter< ? await imagePartToFile(maskParts[0], 'mask', this.allowUrlFetch) : undefined - // `modelOptions` is typed across all four image models (including dall-e-3's - // `quality: 'hd' | 'standard'` which isn't valid for edit). dall-e-3 has - // already been rejected above, so any remaining quality value is valid for - // the edit endpoint — cast the spread to clear the union mismatch. + // The SDK's edit params do not list every gpt-image option (e.g. the + // gpt-image-2.5 `xhigh` / `max` quality levels), so cast the spread. const request: OpenAI_SDK.Images.ImageEditParamsNonStreaming = { model, prompt, @@ -328,14 +308,14 @@ export class OpenAIImageAdapter< * Creates an OpenAI image adapter with explicit API key. * Type resolution happens here at the call site. * - * @param model - The model name (e.g., 'dall-e-3', 'gpt-image-1') + * @param model - The model name (e.g., 'gpt-image-2.5-flare', 'gpt-image-1') * @param apiKey - Your OpenAI API key * @param config - Optional additional configuration * @returns Configured OpenAI image adapter instance with resolved types * * @example * ```typescript - * const adapter = createOpenaiImage('dall-e-3', "sk-..."); + * const adapter = createOpenaiImage('gpt-image-2.5-flare', "sk-..."); * * const result = await generateImage({ * adapter, @@ -359,7 +339,7 @@ export function createOpenaiImage( * - `process.env` (Node.js) * - `window.env` (Browser with injected env) * - * @param model - The model name (e.g., 'dall-e-3', 'gpt-image-1') + * @param model - The model name (e.g., 'gpt-image-2.5-flare', 'gpt-image-1') * @param config - Optional configuration (excluding apiKey which is auto-detected) * @returns Configured OpenAI image adapter instance with resolved types * @throws Error if OPENAI_API_KEY is not found in environment @@ -367,7 +347,7 @@ export function createOpenaiImage( * @example * ```typescript * // Automatically uses OPENAI_API_KEY from environment - * const adapter = openaiImage('dall-e-3'); + * const adapter = openaiImage('gpt-image-2.5-flare'); * * const result = await generateImage({ * adapter, diff --git a/packages/ai-openai/src/adapters/tts.ts b/packages/ai-openai/src/adapters/tts.ts index fb29088a57..cc366abf39 100644 --- a/packages/ai-openai/src/adapters/tts.ts +++ b/packages/ai-openai/src/adapters/tts.ts @@ -23,7 +23,7 @@ export interface OpenAITTSConfig extends OpenAIClientConfig {} * OpenAI Text-to-Speech Adapter * * Tree-shakeable adapter for OpenAI TTS functionality. - * Supports tts-1, tts-1-hd, and gpt-4o-audio-preview models. + * Supports tts-1, tts-1-hd, and gpt-4o-mini-tts models. * * Features: * - Multiple voice options: alloy, ash, ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse diff --git a/packages/ai-openai/src/image/image-provider-options.ts b/packages/ai-openai/src/image/image-provider-options.ts index cdc2fa9715..7b71608bb9 100644 --- a/packages/ai-openai/src/image/image-provider-options.ts +++ b/packages/ai-openai/src/image/image-provider-options.ts @@ -16,31 +16,11 @@ export type GptImageQuality = 'high' | 'medium' | 'low' | 'auto' */ export type GptImage25Quality = GptImageQuality | 'xhigh' | 'max' -/** - * Quality options for dall-e-3 model - */ -export type DallE3Quality = 'hd' | 'standard' - -/** - * Quality options for dall-e-2 model (only standard is supported) - */ -export type DallE2Quality = 'standard' - -/** - * Style options for dall-e-3 model - */ -export type DallE3Style = 'vivid' | 'natural' - /** * Output format options for gpt-image-1 models */ export type GptImageOutputFormat = 'png' | 'jpeg' | 'webp' -/** - * Response format options for dall-e models - */ -export type DallEResponseFormat = 'url' | 'b64_json' - /** * Background options for gpt-image-1 models */ @@ -56,16 +36,6 @@ export type GptImageModeration = 'low' | 'auto' */ export type GptImageSize = '1024x1024' | '1536x1024' | '1024x1536' | 'auto' -/** - * Supported sizes for dall-e-3 model - */ -export type DallE3Size = '1024x1024' | '1792x1024' | '1024x1792' - -/** - * Supported sizes for dall-e-2 model - */ -export type DallE2Size = '256x256' | '512x512' | '1024x1024' - /** * Base provider options shared across all OpenAI image models */ @@ -142,51 +112,6 @@ export interface GptImage25ProviderOptions extends Omit< quality?: GptImage25Quality } -/** - * Provider options for dall-e-3 model - * Field names match the OpenAI API for direct spreading - */ -export interface DallE3ProviderOptions extends OpenAIImageBaseProviderOptions { - /** - * The quality of the image. - * @default 'standard' - */ - quality?: DallE3Quality - - /** - * The style of the generated images. - * 'vivid' causes the model to lean towards generating hyper-real and dramatic images. - * 'natural' causes the model to produce more natural, less hyper-real looking images. - * @default 'vivid' - */ - style?: DallE3Style - - /** - * The format in which generated images are returned. - * URLs are only valid for 60 minutes after generation. - * @default 'url' - */ - response_format?: DallEResponseFormat -} - -/** - * Provider options for dall-e-2 model - * Field names match the OpenAI API for direct spreading - */ -export interface DallE2ProviderOptions extends OpenAIImageBaseProviderOptions { - /** - * The quality of the image (only 'standard' is supported). - */ - quality?: DallE2Quality - - /** - * The format in which generated images are returned. - * URLs are only valid for 60 minutes after generation. - * @default 'url' - */ - response_format?: DallEResponseFormat -} - /** * Union of all OpenAI image provider options */ @@ -194,8 +119,6 @@ export type OpenAIImageProviderOptions = | GptImage25ProviderOptions | GptImage1ProviderOptions | GptImage1MiniProviderOptions - | DallE3ProviderOptions - | DallE2ProviderOptions /** * Type-only map from model name to its specific provider options. @@ -207,8 +130,6 @@ export type OpenAIImageModelProviderOptionsByName = { 'gpt-image-2': GptImage1ProviderOptions 'gpt-image-1': GptImage1ProviderOptions 'gpt-image-1-mini': GptImage1MiniProviderOptions - 'dall-e-3': DallE3ProviderOptions - 'dall-e-2': DallE2ProviderOptions } /** @@ -220,14 +141,11 @@ export type OpenAIImageModelSizeByName = { 'gpt-image-2': GptImageSize 'gpt-image-1': GptImageSize 'gpt-image-1-mini': GptImageSize - 'dall-e-3': DallE3Size - 'dall-e-2': DallE2Size } /** - * Per-model prompt input modalities. Models with `images.edit()` support - * (gpt-image family, dall-e-2) accept image parts in the prompt; - * dall-e-3 has no edit endpoint, so its prompt is text-only at compile time. + * Per-model prompt input modalities. Every gpt-image model supports + * `images.edit()`, so each accepts image parts in the prompt. */ export type OpenAIImageModelInputModalitiesByName = { 'gpt-image-2.5-flare': readonly ['image'] @@ -235,8 +153,6 @@ export type OpenAIImageModelInputModalitiesByName = { 'gpt-image-2': readonly ['image'] 'gpt-image-1': readonly ['image'] 'gpt-image-1-mini': readonly ['image'] - 'dall-e-3': readonly [] - 'dall-e-2': readonly ['image'] } /** @@ -245,7 +161,6 @@ export type OpenAIImageModelInputModalitiesByName = { interface ImageValidationOptions { prompt: string model: string - background?: 'transparent' | 'opaque' | 'auto' | null } /** @@ -264,8 +179,6 @@ export function validateImageSize( 'gpt-image-2': ['1024x1024', '1536x1024', '1024x1536', 'auto'], 'gpt-image-1': ['1024x1024', '1536x1024', '1024x1536', 'auto'], 'gpt-image-1-mini': ['1024x1024', '1536x1024', '1024x1536', 'auto'], - 'dall-e-3': ['1024x1024', '1792x1024', '1024x1792'], - 'dall-e-2': ['256x256', '512x512', '1024x1024'], } const modelSizes = validSizes[model] @@ -282,23 +195,13 @@ export function validateImageSize( } /** - * Validates that the number of images is within bounds for the model. + * Validates that the number of images is within bounds (1-10). */ export function validateNumberOfImages( - model: string, numberOfImages: number | undefined, ): void { if (numberOfImages === undefined) return - // dall-e-3 only supports n=1 - if (model === 'dall-e-3' && numberOfImages !== 1) { - throw new Error( - `Model "dall-e-3" only supports generating 1 image at a time. ` + - `Requested: ${numberOfImages}`, - ) - } - - // Other models support 1-10 if (numberOfImages < 1 || numberOfImages > 10) { throw new Error( `Number of images must be between 1 and 10. Requested: ${numberOfImages}`, @@ -306,23 +209,6 @@ export function validateNumberOfImages( } } -export const validateBackground = (options: ImageValidationOptions) => { - if (options.background) { - const supportedModels = [ - 'gpt-image-2.5-flare', - 'gpt-image-2.5-sunburst', - 'gpt-image-2', - 'gpt-image-1', - 'gpt-image-1-mini', - ] - if (!supportedModels.includes(options.model)) { - throw new Error( - `The model ${options.model} does not support background option.`, - ) - } - } -} - export const validatePrompt = (options: ImageValidationOptions) => { if (options.prompt.length === 0) { throw new Error('Prompt cannot be empty.') @@ -337,14 +223,4 @@ export const validatePrompt = (options: ImageValidationOptions) => { 'For gpt-image-2/gpt-image-1/gpt-image-1-mini, prompt length must be less than or equal to 32000 characters.', ) } - if (options.model === 'dall-e-2' && options.prompt.length > 1000) { - throw new Error( - 'For dall-e-2, prompt length must be less than or equal to 1000 characters.', - ) - } - if (options.model === 'dall-e-3' && options.prompt.length > 4000) { - throw new Error( - 'For dall-e-3, prompt length must be less than or equal to 4000 characters.', - ) - } } diff --git a/packages/ai-openai/src/model-meta.ts b/packages/ai-openai/src/model-meta.ts index c05abdd6c2..995033f681 100644 --- a/packages/ai-openai/src/model-meta.ts +++ b/packages/ai-openai/src/model-meta.ts @@ -1212,33 +1212,6 @@ const O1_PRO = { OpenAIMetadataOptions > -const COMPUTER_USE_PREVIEW = { - name: 'computer-use-preview', - context_window: 8_192, - max_output_tokens: 1_024, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 3, - }, - output: { - normal: 12, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'batch'], - features: ['function_calling'], - tools: ['computer_use', 'computer'], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - const GPT_4O_MINI_SEARCH_PREVIEW = { name: 'gpt-4o-mini-search-preview', context_window: 128_000, @@ -1328,34 +1301,6 @@ const O3_MINI = { OpenAIMetadataOptions > -const GPT_4O_MINI_AUDIO = { - name: 'gpt-4o-mini-audio', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo audio tokens - input: { - normal: 0.15, - }, - output: { - normal: 0.6, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['chat-completions'], - features: ['function_calling', 'streaming'], - tools: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - /* const GPT_4O_MINI_REALTIME = { name: 'gpt-4o-mini-realtime', context_window: 16_000, @@ -1482,34 +1427,6 @@ const GPT_4O = { OpenAIMetadataOptions > -const GPT_4O_AUDIO = { - name: 'gpt-4o-audio', - context_window: 128_000, - max_output_tokens: 16_384, - knowledge_cutoff: '2023-10-01', - pricing: { - // todo audio tokens - input: { - normal: 2.5, - }, - output: { - normal: 10, - }, - }, - supports: { - input: ['text', 'audio'], - output: ['text', 'audio'], - endpoints: ['chat-completions'], - features: ['streaming', 'function_calling'], - tools: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - const GPT_4O_MINI = { name: 'gpt-4o-mini', context_window: 128_000, @@ -1612,37 +1529,6 @@ const GPT_4_TURBO = { OpenAIMetadataOptions > -const CHATGPT_40 = { - name: 'chatgpt-4o-latest', - context_window: 128_000, - max_output_tokens: 4_096, - knowledge_cutoff: '2023-10-01', - pricing: { - input: { - normal: 5, - }, - output: { - normal: 15, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: ['predicted_outcomes', 'streaming'], - tools: [ - 'web_search', - 'web_search_preview', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - const GPT_5_1_CODEX_MINI = { name: 'gpt-5.1-codex-mini', context_window: 400_000, @@ -1680,84 +1566,6 @@ const GPT_5_1_CODEX_MINI = { OpenAIMetadataOptions > -const CODEX_MINI_LATEST = { - name: 'codex-mini-latest', - context_window: 200_000, - max_output_tokens: 100_000, - knowledge_cutoff: '2024-06-01', - pricing: { - input: { - normal: 1.5, - cached: 0.375, - }, - output: { - normal: 6, - }, - }, - supports: { - input: ['text', 'image'], - output: ['text'], - endpoints: ['chat'], - features: ['streaming', 'function_calling', 'structured_outputs'], - tools: [ - 'file_search', - 'code_interpreter', - 'mcp', - 'local_shell', - 'shell', - 'apply_patch', - ], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - -const DALL_E_2 = { - name: 'dall-e-2', - pricing: { - // todo image tokens - input: { - normal: 0.016, - }, - output: { - normal: 0.02, - }, - }, - supports: { - input: ['text'], - output: ['image'], - endpoints: ['image-generation', 'image-edit'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - -const DALL_E_3 = { - name: 'dall-e-3', - pricing: { - // todo image tokens - input: { - normal: 0.04, - }, - output: { - normal: 0.08, - }, - }, - supports: { - input: ['text'], - output: ['image'], - endpoints: ['image-generation', 'image-edit'], - features: [], - }, -} as const satisfies ModelMeta< - OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions -> - const GPT_3_5_TURBO = { name: 'gpt-3.5-turbo', context_window: 16_385, @@ -2159,53 +1967,6 @@ const GPT_5_4_IMAGE_2 = { OpenAIMetadataOptions > -const GPT_5_6 = { - name: 'gpt-5.6', - context_window: 1_050_000, - max_output_tokens: 128_000, - knowledge_cutoff: '2026-02-16', - supports: { - input: ['image', 'text'], - output: ['text'], - endpoints: ['chat', 'chat-completions'], - features: [ - 'streaming', - 'function_calling', - 'structured_outputs', - 'distillation', - ], - tools: [ - 'web_search', - 'web_search_preview', - 'file_search', - 'image_generation', - 'code_interpreter', - 'mcp', - 'computer_use', - 'computer', - 'local_shell', - 'shell', - 'apply_patch', - ], - }, - pricing: { - input: { - normal: 5, - cached: 0.5, - }, - output: { - normal: 30, - }, - }, - reasoning: EFFORT_NONE_LOW_TO_MAX, -} as const satisfies ModelMeta< - OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions -> - const GPT_5_6_SOL = { name: 'gpt-5.6-sol', context_window: 1_050_000, @@ -2907,25 +2668,19 @@ export const OPENAI_CHAT_MODELS = [ // Audio-enabled chat models GPT_AUDIO.name, GPT_AUDIO_MINI.name, - GPT_4O_AUDIO.name, - GPT_4O_MINI_AUDIO.name, // ChatGPT models GPT_5_1_CHAT.name, GPT_5_CHAT.name, - CHATGPT_40.name, // Specialized GPT_5_1_CODEX_MINI.name, - CODEX_MINI_LATEST.name, // Preview models GPT_4O_SEARCH_PREVIEW.name, GPT_4O_MINI_SEARCH_PREVIEW.name, - COMPUTER_USE_PREVIEW.name, // Legacy reasoning O1.name, O1_PRO.name, // GPT-5.6 / 5.5 / 5.4 extended family - GPT_5_6.name, GPT_5_6_SOL.name, GPT_5_6_TERRA.name, GPT_5_6_LUNA.name, @@ -2952,7 +2707,6 @@ export type OpenAIChatModel = (typeof OPENAI_CHAT_MODELS)[number] export function openAIModelRejectsSamplingParams(model: string): boolean { if (/^o\d/.test(model)) return true if (model.startsWith('gpt-5') && !model.endsWith('-chat-latest')) return true - if (model === 'codex-mini-latest') return true return false } @@ -2978,8 +2732,6 @@ export const OPENAI_IMAGE_MODELS = [ GPT_IMAGE_2.name, GPT_IMAGE_1.name, GPT_IMAGE_1_MINI.name, - DALL_E_3.name, - DALL_E_2.name, ] as const export type OpenAIImageModel = (typeof OPENAI_IMAGE_MODELS)[number] @@ -3022,7 +2774,7 @@ export type OpenAIVideoModel = (typeof OPENAI_VIDEO_MODELS)[number] export const OPENAI_TTS_MODELS = [ 'tts-1', 'tts-1-hd', - 'gpt-4o-audio-preview', + 'gpt-4o-mini-tts', ] as const export type OpenAITTSModel = (typeof OPENAI_TTS_MODELS)[number] @@ -3163,9 +2915,6 @@ export type OpenAIChatModelProviderOptionsByName = { OpenAIToolsOptions & OpenAIStreamingOptions & OpenAIMetadataOptions - [CHATGPT_40.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions [O3.name]: OpenAIBaseOptions & OpenAIMetadataOptions [O3_PRO.name]: OpenAIBaseOptions & OpenAIMetadataOptions [O3_MINI.name]: OpenAIBaseOptions & OpenAIMetadataOptions @@ -3182,12 +2931,6 @@ export type OpenAIChatModelProviderOptionsByName = { [GPT_AUDIO_MINI.name]: OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions - [GPT_4O_AUDIO.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions - [GPT_4O_MINI_AUDIO.name]: OpenAIBaseOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions // Chat-only models [GPT_5_1_CHAT.name]: OpenAIBaseOptions & @@ -3203,11 +2946,6 @@ export type OpenAIChatModelProviderOptionsByName = { OpenAIToolsOptions & OpenAIStreamingOptions & OpenAIMetadataOptions - [CODEX_MINI_LATEST.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions // Search models [GPT_4O_SEARCH_PREVIEW.name]: OpenAIBaseOptions & @@ -3222,10 +2960,6 @@ export type OpenAIChatModelProviderOptionsByName = { OpenAIMetadataOptions // Special models - [COMPUTER_USE_PREVIEW.name]: OpenAIBaseOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions [GPT_5_4_MINI.name]: OpenAIBaseOptions & OpenAIStructuredOutputOptions & OpenAIToolsOptions & @@ -3241,11 +2975,6 @@ export type OpenAIChatModelProviderOptionsByName = { OpenAIToolsOptions & OpenAIStreamingOptions & OpenAIMetadataOptions - [GPT_5_6.name]: OpenAIBaseOptions & - OpenAIStructuredOutputOptions & - OpenAIToolsOptions & - OpenAIStreamingOptions & - OpenAIMetadataOptions [GPT_5_6_SOL.name]: OpenAIBaseOptions & OpenAIStructuredOutputOptions & OpenAIToolsOptions & @@ -3365,22 +3094,16 @@ export type OpenAIChatModelToolCapabilitiesByName = { [GPT_3_5_TURBO.name]: typeof GPT_3_5_TURBO.supports.tools [GPT_AUDIO.name]: typeof GPT_AUDIO.supports.tools [GPT_AUDIO_MINI.name]: typeof GPT_AUDIO_MINI.supports.tools - [GPT_4O_AUDIO.name]: typeof GPT_4O_AUDIO.supports.tools - [GPT_4O_MINI_AUDIO.name]: typeof GPT_4O_MINI_AUDIO.supports.tools [GPT_5_1_CHAT.name]: typeof GPT_5_1_CHAT.supports.tools [GPT_5_CHAT.name]: typeof GPT_5_CHAT.supports.tools - [CHATGPT_40.name]: typeof CHATGPT_40.supports.tools [GPT_5_1_CODEX_MINI.name]: typeof GPT_5_1_CODEX_MINI.supports.tools - [CODEX_MINI_LATEST.name]: typeof CODEX_MINI_LATEST.supports.tools [GPT_4O_SEARCH_PREVIEW.name]: typeof GPT_4O_SEARCH_PREVIEW.supports.tools [GPT_4O_MINI_SEARCH_PREVIEW.name]: typeof GPT_4O_MINI_SEARCH_PREVIEW.supports.tools - [COMPUTER_USE_PREVIEW.name]: typeof COMPUTER_USE_PREVIEW.supports.tools [O1.name]: typeof O1.supports.tools [O1_PRO.name]: typeof O1_PRO.supports.tools [GPT_5_4_MINI.name]: typeof GPT_5_4_MINI.supports.tools [GPT_5_4_NANO.name]: typeof GPT_5_4_NANO.supports.tools [GPT_5_4_IMAGE_2.name]: typeof GPT_5_4_IMAGE_2.supports.tools - [GPT_5_6.name]: typeof GPT_5_6.supports.tools [GPT_5_6_SOL.name]: typeof GPT_5_6_SOL.supports.tools [GPT_5_6_TERRA.name]: typeof GPT_5_6_TERRA.supports.tools [GPT_5_6_LUNA.name]: typeof GPT_5_6_LUNA.supports.tools @@ -3424,12 +3147,9 @@ export type OpenAIModelInputModalitiesByName = { [GPT_4O.name]: typeof GPT_4O.supports.input [GPT_4O_MINI.name]: typeof GPT_4O_MINI.supports.input [GPT_4_TURBO.name]: typeof GPT_4_TURBO.supports.input - [CHATGPT_40.name]: typeof CHATGPT_40.supports.input [GPT_5_1_CHAT.name]: typeof GPT_5_1_CHAT.supports.input [GPT_5_CHAT.name]: typeof GPT_5_CHAT.supports.input [GPT_5_1_CODEX_MINI.name]: typeof GPT_5_1_CODEX_MINI.supports.input - [CODEX_MINI_LATEST.name]: typeof CODEX_MINI_LATEST.supports.input - [COMPUTER_USE_PREVIEW.name]: typeof COMPUTER_USE_PREVIEW.supports.input [O3.name]: typeof O3.supports.input [O3_PRO.name]: typeof O3_PRO.supports.input [O3_DEEP_RESEARCH.name]: typeof O3_DEEP_RESEARCH.supports.input @@ -3441,8 +3161,6 @@ export type OpenAIModelInputModalitiesByName = { // Models with text + audio input [GPT_AUDIO.name]: typeof GPT_AUDIO.supports.input [GPT_AUDIO_MINI.name]: typeof GPT_AUDIO_MINI.supports.input - [GPT_4O_AUDIO.name]: typeof GPT_4O_AUDIO.supports.input - [GPT_4O_MINI_AUDIO.name]: typeof GPT_4O_MINI_AUDIO.supports.input // Text-only models [GPT_4.name]: typeof GPT_4.supports.input @@ -3453,7 +3171,6 @@ export type OpenAIModelInputModalitiesByName = { [GPT_5_4_MINI.name]: typeof GPT_5_4_MINI.supports.input [GPT_5_4_NANO.name]: typeof GPT_5_4_NANO.supports.input [GPT_5_4_IMAGE_2.name]: typeof GPT_5_4_IMAGE_2.supports.input - [GPT_5_6.name]: typeof GPT_5_6.supports.input [GPT_5_6_SOL.name]: typeof GPT_5_6_SOL.supports.input [GPT_5_6_TERRA.name]: typeof GPT_5_6_TERRA.supports.input [GPT_5_6_LUNA.name]: typeof GPT_5_6_LUNA.supports.input @@ -3497,12 +3214,9 @@ export const OPENAI_MODEL_INPUT_MODALITIES: Readonly< [GPT_4O.name]: GPT_4O.supports.input, [GPT_4O_MINI.name]: GPT_4O_MINI.supports.input, [GPT_4_TURBO.name]: GPT_4_TURBO.supports.input, - [CHATGPT_40.name]: CHATGPT_40.supports.input, [GPT_5_1_CHAT.name]: GPT_5_1_CHAT.supports.input, [GPT_5_CHAT.name]: GPT_5_CHAT.supports.input, [GPT_5_1_CODEX_MINI.name]: GPT_5_1_CODEX_MINI.supports.input, - [CODEX_MINI_LATEST.name]: CODEX_MINI_LATEST.supports.input, - [COMPUTER_USE_PREVIEW.name]: COMPUTER_USE_PREVIEW.supports.input, [O3.name]: O3.supports.input, [O3_PRO.name]: O3_PRO.supports.input, [O3_DEEP_RESEARCH.name]: O3_DEEP_RESEARCH.supports.input, @@ -3512,8 +3226,6 @@ export const OPENAI_MODEL_INPUT_MODALITIES: Readonly< [O1_PRO.name]: O1_PRO.supports.input, [GPT_AUDIO.name]: GPT_AUDIO.supports.input, [GPT_AUDIO_MINI.name]: GPT_AUDIO_MINI.supports.input, - [GPT_4O_AUDIO.name]: GPT_4O_AUDIO.supports.input, - [GPT_4O_MINI_AUDIO.name]: GPT_4O_MINI_AUDIO.supports.input, [GPT_4.name]: GPT_4.supports.input, [GPT_3_5_TURBO.name]: GPT_3_5_TURBO.supports.input, [O3_MINI.name]: O3_MINI.supports.input, @@ -3522,7 +3234,6 @@ export const OPENAI_MODEL_INPUT_MODALITIES: Readonly< [GPT_5_4_MINI.name]: GPT_5_4_MINI.supports.input, [GPT_5_4_NANO.name]: GPT_5_4_NANO.supports.input, [GPT_5_4_IMAGE_2.name]: GPT_5_4_IMAGE_2.supports.input, - [GPT_5_6.name]: GPT_5_6.supports.input, [GPT_5_6_SOL.name]: GPT_5_6_SOL.supports.input, [GPT_5_6_TERRA.name]: GPT_5_6_TERRA.supports.input, [GPT_5_6_LUNA.name]: GPT_5_6_LUNA.supports.input, @@ -3571,7 +3282,6 @@ const OPENAI_REASONING = { [GPT_5_4_MINI.name]: GPT_5_4_MINI.reasoning, [GPT_5_4_NANO.name]: GPT_5_4_NANO.reasoning, [GPT_5_4_IMAGE_2.name]: GPT_5_4_IMAGE_2.reasoning, - [GPT_5_6.name]: GPT_5_6.reasoning, [GPT_5_6_SOL.name]: GPT_5_6_SOL.reasoning, [GPT_5_6_TERRA.name]: GPT_5_6_TERRA.reasoning, [GPT_5_6_LUNA.name]: GPT_5_6_LUNA.reasoning, diff --git a/packages/ai-openai/src/tools/computer-use-tool.ts b/packages/ai-openai/src/tools/computer-use-tool.ts index 536ea289e2..1ca8220903 100644 --- a/packages/ai-openai/src/tools/computer-use-tool.ts +++ b/packages/ai-openai/src/tools/computer-use-tool.ts @@ -8,7 +8,7 @@ export { } from '@tanstack/openai-base' // The brand discriminator (`computer_use`) intentionally differs from the -// runtime tool name (`computer` or `computer_use_preview`). The brand matches +// runtime tool name (`computer`). The brand matches // the model-meta tool-capability union (`tools: ['computer_use', ...]`) used to // gate which models can construct this tool at compile time, while the runtime // name matches the tool type in the OpenAI SDK. The base factory records the diff --git a/packages/ai-openai/tests/chat-per-model-type-safety.test.ts b/packages/ai-openai/tests/chat-per-model-type-safety.test.ts index 2712e08b1d..ee84b1a91a 100644 --- a/packages/ai-openai/tests/chat-per-model-type-safety.test.ts +++ b/packages/ai-openai/tests/chat-per-model-type-safety.test.ts @@ -219,14 +219,14 @@ describe('OpenAI per-model chat modelOptions gating', () => { }) }) - describe('chatgpt-4o-latest — base + streaming + metadata only', () => { + describe('gpt-audio — base + streaming + metadata only', () => { it('rejects tools options', () => { chat({ threadId: 'thread-1', - adapter: openaiText('chatgpt-4o-latest'), + adapter: openaiText('gpt-audio'), messages: [{ role: 'user', content: 'hi' }], modelOptions: { - // @ts-expect-error - 'tool_choice' is not available on chatgpt-4o-latest + // @ts-expect-error - 'tool_choice' is not available on gpt-audio tool_choice: 'auto', }, }) @@ -235,10 +235,10 @@ describe('OpenAI per-model chat modelOptions gating', () => { it('rejects reasoning option', () => { chat({ threadId: 'thread-1', - adapter: openaiText('chatgpt-4o-latest'), + adapter: openaiText('gpt-audio'), messages: [{ role: 'user', content: 'hi' }], modelOptions: { - // @ts-expect-error - 'reasoning' is not available on chatgpt-4o-latest + // @ts-expect-error - 'reasoning' is not available on gpt-audio reasoning: { effort: 'low' }, }, }) @@ -311,8 +311,8 @@ describe('OpenAI provider options shape assertions', () => { }) }) - describe('chatgpt-4o-latest — base + streaming + metadata only', () => { - type Options = OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] + describe('gpt-audio — base + streaming + metadata only', () => { + type Options = OpenAIChatModelProviderOptionsByName['gpt-audio'] it('has stream_options', () => { expectTypeOf().toHaveProperty('stream_options') diff --git a/packages/ai-openai/tests/computer-tool-kind.test.ts b/packages/ai-openai/tests/computer-tool-kind.test.ts index 3d67c60635..960bc8aea7 100644 --- a/packages/ai-openai/tests/computer-tool-kind.test.ts +++ b/packages/ai-openai/tests/computer-tool-kind.test.ts @@ -33,10 +33,6 @@ describe('OpenAI computer tool kind', () => { typedTools(openaiText('gpt-6.1-sol'), [computerTool]) }) - it('computer-use-preview accepts the computer tool', () => { - typedTools(openaiText('computer-use-preview'), [computerTool]) - }) - it('gpt-4o rejects the computer tool', () => { typedTools(openaiText('gpt-4o'), [ // @ts-expect-error - gpt-4o does not list computer_use diff --git a/packages/ai-openai/tests/copilot-chatgpt-wire.test.ts b/packages/ai-openai/tests/copilot-chatgpt-wire.test.ts index 7a8ce3412c..12ee96a38c 100644 --- a/packages/ai-openai/tests/copilot-chatgpt-wire.test.ts +++ b/packages/ai-openai/tests/copilot-chatgpt-wire.test.ts @@ -75,7 +75,12 @@ const completionsDone = [ const responsesDone = [ { type: 'response.completed', - response: { id: 'r-1', model: 'gpt-5.6', status: 'completed', output: [] }, + response: { + id: 'r-1', + model: 'gpt-5.6-sol', + status: 'completed', + output: [], + }, }, ] @@ -86,7 +91,7 @@ describe('GitHub Copilot wire', () => { baseURL: 'https://api.githubcopilot.com', apiKey: 'copilot-token', defaultHeaders: copilotHeaders, - models: ['gpt-5.6'], + models: ['gpt-5.6-sol'], fetch, ...(api ? { api } : {}), }) @@ -94,10 +99,10 @@ describe('GitHub Copilot wire', () => { it('sends Chat Completions to the Copilot URL with fixed and per-request headers', async () => { const { sent, fetch } = recorder(completionsDone) await drain( - copilot(fetch)('gpt-5.6').chatStream({ + copilot(fetch)('gpt-5.6-sol').chatStream({ threadId: 'thread-1', logger, - model: 'gpt-5.6', + model: 'gpt-5.6-sol', messages: hi, wrapFetch: initiator, }), @@ -108,7 +113,7 @@ describe('GitHub Copilot wire', () => { expect(sent[0]?.headers.get('openai-intent')).toBe('conversation-edits') expect(sent[0]?.headers.get('x-github-api-version')).toBe('2026-08-01') expect(sent[0]?.headers.get('x-initiator')).toBe('user') - expect(sent[0]?.body.model).toBe('gpt-5.6') + expect(sent[0]?.body.model).toBe('gpt-5.6-sol') expect(sent[0]?.body.stream).toBe(true) expect(sent[0]?.body.messages).toStrictEqual([ { role: 'user', content: 'Hi' }, @@ -121,10 +126,10 @@ describe('GitHub Copilot wire', () => { copilot( fetch, 'responses', - )('gpt-5.6').chatStream({ + )('gpt-5.6-sol').chatStream({ threadId: 'thread-1', logger, - model: 'gpt-5.6', + model: 'gpt-5.6-sol', messages: hi, wrapFetch: initiator, modelOptions: { @@ -150,10 +155,10 @@ describe('GitHub Copilot wire', () => { ...completionsDone, ]) const chunks = await drain( - copilot(fetch)('gpt-5.6').chatStream({ + copilot(fetch)('gpt-5.6-sol').chatStream({ threadId: 'thread-1', logger, - model: 'gpt-5.6', + model: 'gpt-5.6-sol', messages: hi, }), ) @@ -168,12 +173,12 @@ describe('ChatGPT plan wire', () => { it('sends the sign-in token to the Responses API with store off', async () => { const { sent, fetch } = recorder(responsesDone) await drain( - createOpenaiChat('gpt-5.6', 'chatgpt-access-token', { + createOpenaiChat('gpt-5.6-sol', 'chatgpt-access-token', { fetch, }).chatStream({ threadId: 'thread-1', logger, - model: 'gpt-5.6', + model: 'gpt-5.6-sol', messages: hi, modelOptions: { store: false }, }), diff --git a/packages/ai-openai/tests/image-adapter.test.ts b/packages/ai-openai/tests/image-adapter.test.ts index 4f725cf257..efb6df67dd 100644 --- a/packages/ai-openai/tests/image-adapter.test.ts +++ b/packages/ai-openai/tests/image-adapter.test.ts @@ -69,65 +69,14 @@ describe('OpenAI Image Adapter', () => { expect(() => validateImageSize('gpt-image-1', undefined)).not.toThrow() }) }) - - describe('dall-e-3', () => { - it('accepts valid sizes', () => { - expect(() => validateImageSize('dall-e-3', '1024x1024')).not.toThrow() - expect(() => validateImageSize('dall-e-3', '1792x1024')).not.toThrow() - expect(() => validateImageSize('dall-e-3', '1024x1792')).not.toThrow() - }) - - it('rejects invalid sizes', () => { - expect(() => validateImageSize('dall-e-3', '512x512')).toThrow() - expect(() => validateImageSize('dall-e-3', '256x256')).toThrow() - }) - - it('accepts auto size (passes through)', () => { - // auto is treated as a pass-through and not validated - expect(() => validateImageSize('dall-e-3', 'auto')).not.toThrow() - }) - }) - - describe('dall-e-2', () => { - it('accepts valid sizes', () => { - expect(() => validateImageSize('dall-e-2', '256x256')).not.toThrow() - expect(() => validateImageSize('dall-e-2', '512x512')).not.toThrow() - expect(() => validateImageSize('dall-e-2', '1024x1024')).not.toThrow() - }) - - it('rejects invalid sizes', () => { - expect(() => validateImageSize('dall-e-2', '1792x1024')).toThrow() - expect(() => validateImageSize('dall-e-2', '1024x1792')).toThrow() - }) - }) }) describe('validateNumberOfImages', () => { - describe('dall-e-3', () => { - it('only accepts 1 image', () => { - expect(() => validateNumberOfImages('dall-e-3', 1)).not.toThrow() - expect(() => validateNumberOfImages('dall-e-3', 2)).toThrow() - expect(() => - validateNumberOfImages('dall-e-3', undefined), - ).not.toThrow() - }) - }) - - describe('dall-e-2', () => { - it('accepts 1-10 images', () => { - expect(() => validateNumberOfImages('dall-e-2', 1)).not.toThrow() - expect(() => validateNumberOfImages('dall-e-2', 5)).not.toThrow() - expect(() => validateNumberOfImages('dall-e-2', 10)).not.toThrow() - expect(() => validateNumberOfImages('dall-e-2', 11)).toThrow() - expect(() => validateNumberOfImages('dall-e-2', 0)).toThrow() - }) - }) - describe('gpt-image-1', () => { it('accepts 1-10 images', () => { - expect(() => validateNumberOfImages('gpt-image-1', 1)).not.toThrow() - expect(() => validateNumberOfImages('gpt-image-1', 10)).not.toThrow() - expect(() => validateNumberOfImages('gpt-image-1', 11)).toThrow() + expect(() => validateNumberOfImages(1)).not.toThrow() + expect(() => validateNumberOfImages(10)).not.toThrow() + expect(() => validateNumberOfImages(11)).toThrow() }) }) }) @@ -270,13 +219,13 @@ describe('OpenAI Image Adapter', () => { adapter.spyOnImagesGenerate().mockResolvedValue(mockResponse) const result1 = await adapter.generateImages({ - model: 'dall-e-3', + model: 'gpt-image-1', prompt: 'Test prompt', logger: testLogger, }) const result2 = await adapter.generateImages({ - model: 'dall-e-3', + model: 'gpt-image-1', prompt: 'Test prompt', logger: testLogger, }) @@ -416,54 +365,29 @@ describe('OpenAI Image Adapter', () => { expect(result.images[0]!.b64Json).toBe('edited-base64') }) - it('rejects dall-e-3 with a clear error when the prompt has image parts', async () => { + it('rejects more than 16 source images', async () => { const adapter = new TestOpenAIImageAdapter( { apiKey: 'test-api-key' }, - 'dall-e-3', - ) - - await expect( - adapter.generateImages({ - model: 'dall-e-3', - prompt: [ - { type: 'text', content: 'edit' }, - { - type: 'image', - source: { type: 'data', value: 'aGk=', mimeType: 'image/png' }, - }, - ], - logger: testLogger, - }), - ).rejects.toThrow(/does not support image prompt parts/) - }) - - it('rejects dall-e-2 when more than one source image is provided', async () => { - const adapter = new TestOpenAIImageAdapter( - { apiKey: 'test-api-key' }, - 'dall-e-2', + 'gpt-image-1', ) await expect( adapter.generateImages({ - model: 'dall-e-2', + model: 'gpt-image-1', prompt: [ { type: 'text', content: 'edit' }, - { - type: 'image', - source: { type: 'data', value: 'aGk=', mimeType: 'image/png' }, - }, - { - type: 'image', + ...Array.from({ length: 17 }, () => ({ + type: 'image' as const, source: { - type: 'data', - value: 'YnllCg==', + type: 'data' as const, + value: 'aGk=', mimeType: 'image/png', }, - }, + })), ], logger: testLogger, }), - ).rejects.toThrow(/at most 1 source image/) + ).rejects.toThrow(/at most 16 source image/) }) it('routes metadata.role==="mask" to the mask param', async () => { diff --git a/packages/ai-openai/tests/model-meta.test.ts b/packages/ai-openai/tests/model-meta.test.ts index fb4a499e81..a2dd307cb5 100644 --- a/packages/ai-openai/tests/model-meta.test.ts +++ b/packages/ai-openai/tests/model-meta.test.ts @@ -120,16 +120,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf().toExtend() }) - it('gpt-5.6 should support all features', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-5.6'] - - expectTypeOf().not.toHaveProperty('reasoning') - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - it('gpt-5.6-sol should support all features', () => { type Options = OpenAIChatModelProviderOptionsByName['gpt-5.6-sol'] @@ -379,16 +369,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { }) describe('Models WITH minimal features (Basic Models)', () => { - it('chatgpt-4o-latest should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] - - expectTypeOf().not.toHaveProperty('reasoning') - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - it('gpt-audio should only have streaming and base options', () => { type Options = OpenAIChatModelProviderOptionsByName['gpt-audio'] @@ -408,26 +388,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf().toExtend() expectTypeOf().toExtend() }) - - it('gpt-4o-audio should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4o-audio'] - - expectTypeOf().not.toHaveProperty('reasoning') - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - - it('gpt-4o-mini-audio should only have streaming and base options', () => { - type Options = OpenAIChatModelProviderOptionsByName['gpt-4o-mini-audio'] - - expectTypeOf().not.toHaveProperty('reasoning') - expectTypeOf().not.toExtend() - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) }) describe('Chat-only models WITH reasoning AND structured output but WITHOUT tools', () => { @@ -462,15 +422,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf().toExtend() }) - it('codex-mini-latest should have structured output and tools', () => { - type Options = OpenAIChatModelProviderOptionsByName['codex-mini-latest'] - - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) - it('gpt-4o-search-preview should have structured output and tools', () => { type Options = OpenAIChatModelProviderOptionsByName['gpt-4o-search-preview'] @@ -490,18 +441,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf().toExtend() expectTypeOf().toExtend() }) - - it('computer-use-preview should have tools and reasoning with concise but NOT structured output', () => { - type Options = - OpenAIChatModelProviderOptionsByName['computer-use-preview'] - - // Should have reasoning options with 'concise' summary support - expectTypeOf().not.toHaveProperty('reasoning') - expectTypeOf().not.toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - expectTypeOf().toExtend() - }) }) describe('Provider options type completeness', () => { @@ -513,7 +452,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf<'gpt-5.1-codex'>().toExtend() expectTypeOf<'gpt-5'>().toExtend() expectTypeOf<'gpt-5-pro'>().toExtend() - expectTypeOf<'gpt-5.6'>().toExtend() expectTypeOf<'gpt-5.6-sol'>().toExtend() expectTypeOf<'gpt-5.6-terra'>().toExtend() expectTypeOf<'gpt-5.6-luna'>().toExtend() @@ -544,11 +482,8 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf<'gpt-3.5-turbo'>().toExtend() // Basic models - expectTypeOf<'chatgpt-4o-latest'>().toExtend() expectTypeOf<'gpt-audio'>().toExtend() expectTypeOf<'gpt-audio-mini'>().toExtend() - expectTypeOf<'gpt-4o-audio'>().toExtend() - expectTypeOf<'gpt-4o-mini-audio'>().toExtend() // Chat-only models expectTypeOf<'gpt-5.1-chat-latest'>().toExtend() @@ -556,10 +491,8 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { // Codex/Preview models expectTypeOf<'gpt-5.1-codex-mini'>().toExtend() - expectTypeOf<'codex-mini-latest'>().toExtend() expectTypeOf<'gpt-4o-search-preview'>().toExtend() expectTypeOf<'gpt-4o-mini-search-preview'>().toExtend() - expectTypeOf<'computer-use-preview'>().toExtend() }) }) @@ -592,9 +525,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] >().toHaveProperty('metadata') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] - >().toHaveProperty('metadata') }) it('all models should have store option', () => { @@ -625,9 +555,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] >().toHaveProperty('store') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] - >().toHaveProperty('store') }) it('all models should have service_tier option', () => { @@ -658,9 +585,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] >().toHaveProperty('service_tier') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] - >().toHaveProperty('service_tier') }) it('models with tools support should have tool_choice and parallel_tool_calls', () => { @@ -733,9 +657,6 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-4'] >().toHaveProperty('stream_options') - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] - >().toHaveProperty('stream_options') }) }) @@ -847,18 +768,12 @@ describe('OpenAI Chat Model Provider Options Type Assertions', () => { expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-3.5-turbo'] >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['chatgpt-4o-latest'] - >().toExtend() expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-5.1-chat-latest'] >().toExtend() expectTypeOf< OpenAIChatModelProviderOptionsByName['gpt-5-chat-latest'] >().toExtend() - expectTypeOf< - OpenAIChatModelProviderOptionsByName['computer-use-preview'] - >().toExtend() }) }) }) @@ -1082,50 +997,6 @@ describe('OpenAI Model Input Modality Type Assertions', () => { }) }) - describe('codex-mini-latest (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['codex-mini-latest'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf< - MessageWithContent - >().not.toExtend() - expectTypeOf< - MessageWithContent - >().not.toExtend() - expectTypeOf< - MessageWithContent - >().not.toExtend() - }) - }) - - describe('computer-use-preview (text + image)', () => { - type Modalities = OpenAIModelInputModalitiesByName['computer-use-preview'] - type Message = ConstrainedModelMessage> - - it('should allow TextPart and ImagePart', () => { - expectTypeOf>().toExtend() - expectTypeOf>().toExtend() - }) - - it('should NOT allow AudioPart, VideoPart, or DocumentPart', () => { - expectTypeOf< - MessageWithContent - >().not.toExtend() - expectTypeOf< - MessageWithContent - >().not.toExtend() - expectTypeOf< - MessageWithContent - >().not.toExtend() - }) - }) - describe('o3 (text + image + document)', () => { type Modalities = OpenAIModelInputModalitiesByName['o3'] type Message = ConstrainedModelMessage> diff --git a/packages/ai-openai/tests/model-sampling-support.test.ts b/packages/ai-openai/tests/model-sampling-support.test.ts index dbaf2abf5f..8bd2896348 100644 --- a/packages/ai-openai/tests/model-sampling-support.test.ts +++ b/packages/ai-openai/tests/model-sampling-support.test.ts @@ -17,12 +17,10 @@ describe('openAIModelRejectsSamplingParams', () => { 'gpt-5.2-pro', 'gpt-5.5', 'gpt-5.5-pro', - 'gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.1-codex-mini', - 'codex-mini-latest', ]) { expect(openAIModelRejectsSamplingParams(model), model).toBe(true) } @@ -34,7 +32,6 @@ describe('openAIModelRejectsSamplingParams', () => { 'gpt-5.1-chat-latest', 'gpt-5.2-chat-latest', 'gpt-chat-latest', - 'chatgpt-4o-latest', 'gpt-4.1', 'gpt-4o', 'gpt-4o-mini', diff --git a/packages/ai-openai/tests/openai-adapter.test.ts b/packages/ai-openai/tests/openai-adapter.test.ts index bee814a6f0..89b9ddb073 100644 --- a/packages/ai-openai/tests/openai-adapter.test.ts +++ b/packages/ai-openai/tests/openai-adapter.test.ts @@ -220,7 +220,7 @@ describe('OpenAI adapter option mapping', () => { type: 'response.created', response: { id: 'resp-reasoning-include', - model: 'gpt-5.6', + model: 'gpt-5.6-sol', status: 'in_progress', created_at: 1234567890, }, @@ -236,7 +236,7 @@ describe('OpenAI adapter option mapping', () => { ]) const responsesCreate = vi.fn().mockResolvedValueOnce(mockStream) - const adapter = new OpenAITextAdapter({ apiKey: 'test-key' }, 'gpt-5.6') + const adapter = new OpenAITextAdapter({ apiKey: 'test-key' }, 'gpt-5.6-sol') ;(adapter as any).client = { responses: { create: responsesCreate, @@ -306,7 +306,7 @@ describe('OpenAI adapter option mapping', () => { type: 'response.created', response: { id: 'resp-include', - model: 'gpt-5.6', + model: 'gpt-5.6-sol', status: 'in_progress', created_at: 1234567890, }, @@ -322,7 +322,7 @@ describe('OpenAI adapter option mapping', () => { ]) const responsesCreate = vi.fn().mockResolvedValueOnce(mockStream) - const adapter = new OpenAITextAdapter({ apiKey: 'test-key' }, 'gpt-5.6') + const adapter = new OpenAITextAdapter({ apiKey: 'test-key' }, 'gpt-5.6-sol') ;(adapter as any).client = { responses: { create: responsesCreate, diff --git a/packages/ai-openai/tests/prompt-cache.test.ts b/packages/ai-openai/tests/prompt-cache.test.ts index e2bf0bc7c5..41d0518d44 100644 --- a/packages/ai-openai/tests/prompt-cache.test.ts +++ b/packages/ai-openai/tests/prompt-cache.test.ts @@ -123,7 +123,6 @@ describe('clampPromptCacheKey', () => { describe('openAIModelUsesExplicitPromptCache', () => { it('is true for gpt-5.6 and later, and for gpt-6 and later', () => { for (const model of [ - 'gpt-5.6', 'gpt-5.6-sol', 'gpt-5.6-luna-pro', 'gpt-5.10', @@ -328,7 +327,7 @@ describe('OpenAI Responses adapter: chat({ promptCache })', () => { it('turns automatic caching off for none on gpt-5.6', async () => { expect( - await sendResponses('gpt-5.6', { retention: 'none', key: 'k-1' }), + await sendResponses('gpt-5.6-sol', { retention: 'none', key: 'k-1' }), ).toStrictEqual({ prompt_cache_options: { mode: 'explicit' } }) }) @@ -351,7 +350,7 @@ describe('OpenAI Responses adapter: chat({ promptCache })', () => { expect( await sendResponses( - 'gpt-5.6', + 'gpt-5.6-sol', { retention: 'long', key: 'k-1' }, callerOptions, ), @@ -362,7 +361,7 @@ describe('OpenAI Responses adapter: chat({ promptCache })', () => { }) it('sends no cache field without the option', async () => { - expect(await sendResponses('gpt-5.6', undefined)).toStrictEqual({}) + expect(await sendResponses('gpt-5.6-sol', undefined)).toStrictEqual({}) }) }) diff --git a/packages/ai-openai/tests/tools-per-model-type-safety.test.ts b/packages/ai-openai/tests/tools-per-model-type-safety.test.ts index cc89d94901..faab135bb4 100644 --- a/packages/ai-openai/tests/tools-per-model-type-safety.test.ts +++ b/packages/ai-openai/tests/tools-per-model-type-safety.test.ts @@ -64,12 +64,7 @@ describe('OpenAI per-model tool gating', () => { server_label: 'my-server', server_url: 'https://example.com/mcp', }), - computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), + computerUseTool({ type: 'computer' }), localShellTool(), shellTool(), applyPatchTool(), @@ -80,10 +75,6 @@ describe('OpenAI per-model tool gating', () => { assertFullToolSuperset(openaiText('gpt-5.2')) }) - it('gpt-5.6 accepts the full tool superset', () => { - assertFullToolSuperset(openaiText('gpt-5.6')) - }) - it('gpt-5.6-sol accepts the full tool superset', () => { assertFullToolSuperset(openaiText('gpt-5.6-sol')) }) @@ -128,12 +119,7 @@ describe('OpenAI per-model tool gating', () => { server_label: 'my-server', server_url: 'https://example.com/mcp', }), - computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), + computerUseTool({ type: 'computer' }), shellTool(), applyPatchTool(), // @ts-expect-error - gpt-6-astra does not list web_search_preview @@ -158,12 +144,7 @@ describe('OpenAI per-model tool gating', () => { server_label: 'my-server', server_url: 'https://example.com/mcp', }), - computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), + computerUseTool({ type: 'computer' }), shellTool(), applyPatchTool(), // @ts-expect-error - gpt-6-sol does not list web_search_preview @@ -188,12 +169,7 @@ describe('OpenAI per-model tool gating', () => { server_label: 'my-server', server_url: 'https://example.com/mcp', }), - computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), + computerUseTool({ type: 'computer' }), shellTool(), applyPatchTool(), // @ts-expect-error - gpt-6-luna does not list web_search_preview @@ -226,12 +202,7 @@ describe('OpenAI per-model tool gating', () => { server_url: 'https://example.com/mcp', }), // @ts-expect-error - gpt-3.5-turbo does not support computer_use - computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), + computerUseTool({ type: 'computer' }), // @ts-expect-error - gpt-3.5-turbo does not support local_shell localShellTool(), // @ts-expect-error - gpt-3.5-turbo does not support shell @@ -281,12 +252,7 @@ describe('OpenAI per-model tool gating', () => { // @ts-expect-error - gpt-4o does not support web_search_preview webSearchPreviewTool({ type: 'web_search_preview' }), // @ts-expect-error - gpt-4o does not support computer_use - computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), + computerUseTool({ type: 'computer' }), // @ts-expect-error - gpt-4o does not support local_shell localShellTool(), // @ts-expect-error - gpt-4o does not support shell diff --git a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md index 460e3c43f6..9019120718 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/SKILL.md +++ b/packages/ai/skills/ai-core/adapter-configuration/SKILL.md @@ -115,7 +115,7 @@ const adapter = openaiText('gpt-5.2') const adapter2 = anthropicText('claude-sonnet-4-6') const adapter3 = geminiText('gemini-2.5-pro') const adapter4 = grokText('grok-4.6') -const adapter5 = groqText('llama-3.3-70b-versatile') +const adapter5 = groqText('openai/gpt-oss-120b') const adapter6 = openRouterText('anthropic/claude-sonnet-4') const adapter7 = ollamaText('llama3.3:latest') const adapter8 = bedrockText('us.anthropic.claude-3-7-sonnet-20250219-v1:0') diff --git a/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md b/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md index 0c74d6bcb9..0b3250e8e6 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +++ b/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md @@ -26,7 +26,6 @@ import { anthropicText } from '@tanstack/ai-anthropic' | `claude-fable-5-1` | 1M | 128K | Newest; thinking always on (adaptive) | | `claude-fable-5` | 1M | 128K | Most capable; thinking always on (adaptive) | | `claude-opus-5` | 1M | 128K | Opus tier; budget thinking + sampling | -| `claude-opus-5-fast` | 1M | 128K | Fast-mode Opus 5; same options as opus-5 | | `claude-sonnet-5-5` | 1M | 128K | Sonnet tier; adaptive-only, no sampling | | `claude-haiku-5-5` | 1M | 128K | Fastest; adaptive thinking, no sampling | | `claude-sonnet-5` | 1M | 128K | Best balance; adaptive thinking by default | @@ -36,13 +35,12 @@ import { anthropicText } from '@tanstack/ai-anthropic' | `claude-sonnet-4-6` | 1M | 64K | Previous gen balanced, adaptive + budget | | `claude-sonnet-4-5` | 200K | 64K | Previous gen balanced | | `claude-opus-4-5` | 200K | 32K | Previous gen most capable | -| `claude-opus-4-1` | 200K | 64K | Deprecated (retires 2026-08-05) | | `claude-haiku-4-5` | 200K | 64K | Fast and affordable | Note: Model IDs use the format `claude-sonnet-5`, `claude-opus-4-8`, etc. -Retired models (Claude 3.x, Sonnet 3.7, Opus 4 / Sonnet 4) were removed — -every registered id resolves against the first-party Anthropic API. -`claude-opus-5-fast` is the only `-fast` id that remains. +Retired models (Claude 3.x, Sonnet 3.7, Opus 4 / Sonnet 4, Opus 4.1) and the +`-fast` ids were removed. Every registered id resolves against the first-party +Anthropic API. Thinking is set with `chat({ reasoning })`, not with `modelOptions`. The adapter sends adaptive thinking with `output_config.effort` on Claude 4.6 and diff --git a/packages/ai/skills/ai-core/adapter-configuration/references/gemini-adapter.md b/packages/ai/skills/ai-core/adapter-configuration/references/gemini-adapter.md index 032c337f2b..e46c44218c 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/references/gemini-adapter.md +++ b/packages/ai/skills/ai-core/adapter-configuration/references/gemini-adapter.md @@ -8,12 +8,12 @@ ## Adapter Factories -| Factory | Type | Description | -| ----------------- | --------- | ----------------------------- | -| `geminiText` | Text/Chat | Chat completions | -| `geminiImage` | Image | Image generation (Imagen) | -| `geminiSpeech` | TTS | Text-to-speech (experimental) | -| `geminiSummarize` | Summarize | Text summarization | +| Factory | Type | Description | +| ----------------- | --------- | ------------------------------ | +| `geminiText` | Text/Chat | Chat completions | +| `geminiImage` | Image | Image generation (Nano Banana) | +| `geminiSpeech` | TTS | Text-to-speech (experimental) | +| `geminiSummarize` | Summarize | Text summarization | ## Import diff --git a/packages/ai/skills/ai-core/adapter-configuration/references/groq-adapter.md b/packages/ai/skills/ai-core/adapter-configuration/references/groq-adapter.md index a0611cc3be..c56b5a2422 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/references/groq-adapter.md +++ b/packages/ai/skills/ai-core/adapter-configuration/references/groq-adapter.md @@ -22,18 +22,14 @@ import { groqText } from '@tanstack/ai-groq' ## Key Chat Models -| Model | Context Window | Notes | -| ----------------------------------------------- | -------------- | ------------------------- | -| `llama-3.3-70b-versatile` | 131K | General purpose | -| `meta-llama/llama-4-maverick-17b-128e-instruct` | 131K | Vision, JSON schema | -| `meta-llama/llama-4-scout-17b-16e-instruct` | 131K | Vision, tool calling | -| `openai/gpt-oss-120b` | 131K | Reasoning, browser search | -| `openai/gpt-oss-20b` | 131K | Budget reasoning | -| `qwen/qwen3-32b` | 131K | Reasoning, tool calling | -| `moonshotai/kimi-k2-instruct-0905` | 262K | Large context | -| `llama-3.1-8b-instant` | 131K | Ultra-fast, budget | - -Guard models: `meta-llama/llama-guard-4-12b`, `meta-llama/llama-prompt-guard-2-86m` +| Model | Context Window | Notes | +| --------------------- | -------------- | ------------------------- | +| `openai/gpt-oss-120b` | 131K | Reasoning, browser search | +| `openai/gpt-oss-20b` | 131K | Budget reasoning | +| `qwen/qwen3.8-27b` | 131K | Reasoning, vision, tools | + +Guard models: `openai/gpt-oss-safeguard-20b`, `meta-llama/llama-prompt-guard-2-86m`, +`meta-llama/llama-prompt-guard-2-22m` ## Provider-Specific modelOptions @@ -44,7 +40,7 @@ import { groqText } from '@tanstack/ai-groq' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: groqText('llama-3.3-70b-versatile'), + adapter: groqText('openai/gpt-oss-120b'), messages, threadId: 'thread-1', modelOptions: { @@ -96,12 +92,12 @@ GROQ_API_KEY ## Gotchas -- `chat({ reasoning })` sends `reasoning_effort`. qwen3 models only turn - thinking on (`high` sends `'default'`) or off (`off` sends `'none'`). - openai/gpt-oss models take `low`, `medium`, and `high`. +- `chat({ reasoning })` sends `reasoning_effort`. openai/gpt-oss and + qwen/qwen3.8-27b models take `low`, `medium`, and `high`. qwen/qwen3.8-27b also + takes `off` (sends `'none'`). - `include_reasoning` and `reasoning_format` are mutually exclusive. - Most models have `max_completion_tokens` of 8K-65K, not unlimited. - Groq specializes in inference speed; model selection is more limited than other providers. -- Guard models (`llama-guard-4-12b`, `llama-prompt-guard-2-*`) are for +- Guard models (`gpt-oss-safeguard-20b`, `llama-prompt-guard-2-*`) are for content moderation, not general chat. diff --git a/packages/ai/skills/ai-core/adapter-configuration/references/openai-adapter.md b/packages/ai/skills/ai-core/adapter-configuration/references/openai-adapter.md index 1dcd43fba7..e046419ccc 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/references/openai-adapter.md +++ b/packages/ai/skills/ai-core/adapter-configuration/references/openai-adapter.md @@ -8,15 +8,15 @@ ## Adapter Factories -| Factory | Type | Description | -| --------------------- | -------------- | ------------------------------------ | -| `openaiText` | Text/Chat | Chat completions (Responses API) | -| `openaiImage` | Image | Image generation (DALL-E, GPT Image) | -| `openaiSpeech` | TTS | Text-to-speech | -| `openaiTranscription` | Transcription | Speech-to-text | -| `openaiVideo` | Video | Video generation (experimental) | -| `openaiSummarize` | Summarize | Text summarization | -| `openaiRealtime` | Realtime/Voice | Realtime voice conversations | +| Factory | Type | Description | +| --------------------- | -------------- | -------------------------------- | +| `openaiText` | Text/Chat | Chat completions (Responses API) | +| `openaiImage` | Image | Image generation (GPT Image) | +| `openaiSpeech` | TTS | Text-to-speech | +| `openaiTranscription` | Transcription | Speech-to-text | +| `openaiVideo` | Video | Video generation (experimental) | +| `openaiSummarize` | Summarize | Text summarization | +| `openaiRealtime` | Realtime/Voice | Realtime voice conversations | ## Import @@ -31,7 +31,7 @@ import { openaiSpeech } from '@tanstack/ai-openai' | Model | Context Window | Max Output | Notes | | -------------- | -------------- | ---------- | ---------------------------------------------- | | `gpt-6-astra` | 1M | 128K | Newest; reasoning, tools, image input | -| `gpt-5.6` | 1M | 128K | Reasoning, tools, image input | +| `gpt-5.6-sol` | 1M | 128K | Reasoning, tools, image input | | `gpt-5.5` | 1M | 128K | Flagship used in examples; text/image/document | | `gpt-5.5-pro` | 1M | 128K | Higher reasoning tier | | `gpt-5.4-mini` | 400K | 128K | Cost-efficient (no bare `gpt-5.4` chat id) | @@ -39,7 +39,7 @@ import { openaiSpeech } from '@tanstack/ai-openai' | `gpt-5-mini` | 400K | 128K | Budget | `OPENAI_CHAT_MODELS` is the full list (also `gpt-6-astra-pro`, the -`gpt-5.6-luna/sol/terra` family, `gpt-5.4-nano`, `gpt-5.2-pro`, +`gpt-5.6-luna` and `gpt-5.6-terra`, `gpt-5.4-nano`, `gpt-5.2-pro`, `gpt-5.1`, `gpt-5`, the `o3`/`o4-mini` reasoning models, and `gpt-4.1`/`gpt-4o`). ## Provider-Specific modelOptions @@ -104,7 +104,7 @@ OPENAI_API_KEY - `chat({ reasoning })` takes only the levels of the selected model. A level that the model does not have moves to the nearest one. - `conversation` and `previous_response_id` cannot be used together. -- Reasoning models (`o*`, `gpt-5*` except `*-chat-latest`, `codex-mini-latest`) +- Reasoning models (`o*`, `gpt-5*` except `*-chat-latest`) pair each `function_call` with a `reasoning` item. The adapter requests `include: ['reasoning.encrypted_content']` for those models and replays that item on the next turn. Pre-5 chat models are left unchanged. If you persist diff --git a/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md b/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md index cf954204d1..dbe617e35b 100644 --- a/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md +++ b/packages/ai/skills/ai-core/ag-ui-protocol/SKILL.md @@ -29,7 +29,7 @@ import { openaiText } from '@tanstack/ai-openai' export async function POST(request: Request) { const { messages, threadId } = await request.json() const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId, }) @@ -65,7 +65,7 @@ export async function POST(req: Request) { } const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages: params.messages, threadId: params.threadId, tools: mergeAgentTools(serverTools, params.tools), @@ -102,7 +102,7 @@ const messages = [{ role: 'user' as const, content: 'Hello' }] // Option A: Get a ReadableStream (manual Response construction) const abortController = new AbortController() const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', abortController, @@ -159,7 +159,7 @@ const messages = [{ role: 'user' as const, content: 'Hello' }] // Option A: Get a ReadableStream const abortController = new AbortController() const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', abortController, @@ -246,7 +246,7 @@ import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', }) diff --git a/packages/ai/skills/ai-core/chat-experience/SKILL.md b/packages/ai/skills/ai-core/chat-experience/SKILL.md index 1414e2259f..5b4ee2af30 100644 --- a/packages/ai/skills/ai-core/chat-experience/SKILL.md +++ b/packages/ai/skills/ai-core/chat-experience/SKILL.md @@ -368,7 +368,7 @@ export async function POST(request: Request) { const abortController = new AbortController() const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId, abortController, @@ -453,7 +453,7 @@ export async function POST(request: Request) { }) const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId, mcp: { @@ -546,7 +546,7 @@ const client = await createMCPClient({ try { const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages: [{ role: 'user', content: 'Weather in Paris?' }], threadId: 'thread-1', tools: await client.tools(), @@ -742,7 +742,7 @@ import { streamText } from 'ai' import { openai } from '@ai-sdk/openai' const messages = [{ role: 'user' as const, content: 'Hello' }] -const result = streamText({ model: openai('gpt-5.6'), messages }) +const result = streamText({ model: openai('gpt-6.1-sol'), messages }) ``` ```typescript @@ -752,7 +752,7 @@ import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', }) @@ -767,7 +767,7 @@ import { createOpenAI } from '@ai-sdk/openai' const messages = [{ role: 'user' as const, content: 'Hello' }] const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY }) -streamText({ model: openai('gpt-5.6'), messages }) +streamText({ model: openai('gpt-6.1-sol'), messages }) ``` ```typescript @@ -776,7 +776,7 @@ import { openaiText } from '@tanstack/ai-openai' import { chat } from '@tanstack/ai' const messages = [{ role: 'user' as const, content: 'Hello' }] -chat({ adapter: openaiText('gpt-5.6'), messages, threadId: 'thread-1' }) +chat({ adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1' }) ``` ### c. CRITICAL: Using monolithic openai() instead of openaiText() @@ -784,7 +784,7 @@ chat({ adapter: openaiText('gpt-5.6'), messages, threadId: 'thread-1' }) ```typescript ignore // WRONG — `openai()` is no longer exported from @tanstack/ai-openai import { openai } from '@tanstack/ai-openai' -chat({ adapter: openai(), model: 'gpt-5.6', messages, threadId: 'thread-1' }) +chat({ adapter: openai(), model: 'gpt-6.1-sol', messages, threadId: 'thread-1' }) ``` ```typescript @@ -793,7 +793,7 @@ import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] -chat({ adapter: openaiText('gpt-5.6'), messages, threadId: 'thread-1' }) +chat({ adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1' }) ``` The monolithic `openai()` adapter no longer exists. Use tree-shakeable adapters: @@ -816,7 +816,7 @@ export async function POST(request: Request) { const { messages, threadId } = await request.json() const abortController = new AbortController() const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId, abortController, @@ -831,7 +831,7 @@ export async function POST(request: Request) { // WRONG chat({ adapter: openaiText(), - model: 'gpt-5.6', + model: 'gpt-6.1-sol', messages, threadId: 'thread-1', }) @@ -843,7 +843,7 @@ import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] -chat({ adapter: openaiText('gpt-5.6'), messages, threadId: 'thread-1' }) +chat({ adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1' }) ``` The model is passed to the adapter factory, not to `chat()`. @@ -881,7 +881,7 @@ import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', modelOptions: { temperature: 0.7, max_output_tokens: 1000 }, @@ -915,7 +915,7 @@ import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', modelOptions: { text: { format: { type: 'json_object' } } }, @@ -931,7 +931,7 @@ import { openaiText } from '@tanstack/ai-openai' export async function POST(request: Request) { const { messages, threadId } = await request.json() - const stream = chat({ adapter: openaiText('gpt-5.6'), messages, threadId }) + const stream = chat({ adapter: openaiText('gpt-6.1-sol'), messages, threadId }) const readable = new ReadableStream({ async start(controller) { @@ -958,7 +958,7 @@ export async function POST(request: Request) { const { messages, threadId } = await request.json() const abortController = new AbortController() const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId, abortController, @@ -1005,7 +1005,7 @@ const analytics: ChatMiddleware = { const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', middleware: [analytics], @@ -1044,7 +1044,7 @@ import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] const stream = chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', }) diff --git a/packages/ai/skills/ai-core/locks/SKILL.md b/packages/ai/skills/ai-core/locks/SKILL.md index 7cfbb2d5d3..2923b5584f 100644 --- a/packages/ai/skills/ai-core/locks/SKILL.md +++ b/packages/ai/skills/ai-core/locks/SKILL.md @@ -42,7 +42,7 @@ import { openaiText } from '@tanstack/ai-openai' const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', middleware: [ @@ -63,7 +63,7 @@ const persistence = memoryPersistence() const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', middleware: [ @@ -114,7 +114,7 @@ const locks = defineLock({ const messages = [{ role: 'user' as const, content: 'Hello' }] chat({ - adapter: openaiText('gpt-5.6'), + adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1', middleware: [withLocks(locks)], diff --git a/packages/ai/skills/ai-core/media-generation/SKILL.md b/packages/ai/skills/ai-core/media-generation/SKILL.md index 1228040426..a9e31a740d 100644 --- a/packages/ai/skills/ai-core/media-generation/SKILL.md +++ b/packages/ai/skills/ai-core/media-generation/SKILL.md @@ -176,10 +176,9 @@ function ImageGenerator() { ### 1. Image Generation -Supported adapters: `openaiImage` (dall-e-2, dall-e-3, gpt-image-1, -gpt-image-1-mini, gpt-image-2), `geminiImage` (gemini-3.1-flash-image, -gemini-3.1-flash-lite-image, gemini-nano-banana-2.1, gemini-3-pro-image, -imagen-4.0-generate-001, etc.) +Supported adapters: `openaiImage` (gpt-image-2.5-flare, gpt-image-2.5-sunburst, +gpt-image-2, gpt-image-1, gpt-image-1-mini), `geminiImage` (gemini-nano-banana-2.1, +gemini-3.1-flash-image, gemini-3.1-flash-lite-image, gemini-3-pro-image) and `byteplusImage` (Seedream — `seedream-4-0-250828`, `seedream-4-5-251128`, the 5.0 family). @@ -229,29 +228,21 @@ const edited = await generateImage({ prompt: 'Make the sky orange', modelOptions: { previous_interaction_id: geminiResult.id }, }) - -// Gemini Imagen model. Text-to-image only. No interaction id to chain. -const imagenResult = await generateImage({ - adapter: geminiImage('imagen-4.0-generate-001'), - threadId: 'thread-1', - prompt: 'A landscape photo', - modelOptions: { aspectRatio: '16:9' }, -}) ``` -Gemini-native image models (`gemini-3.1-flash-image`, +Gemini image models (`gemini-3.1-flash-image`, `gemini-3.1-flash-lite-image`, `gemini-nano-banana-2.1`, `gemini-3-pro-image`) use the Interactions API. `result.id` is the interaction id. Omit `modelOptions.store` to keep the API default (`true`). `store: false` cannot be chained. Gemini keeps a stored interaction for 1 day on the free tier and 55 days on a paid tier. Set `thinkingConfig.thinkingLevel` to `'minimal'`, `'low'`, `'medium'`, or -`'high'`. `thinkingConfig.thinkingBudget` throws. Imagen stays on -`generateImages`. +`'high'`. `thinkingConfig.thinkingBudget` throws. Result shape: `ImageGenerationResult` with `images` array where each entry -has `b64Json?`, `url?`, and `revisedPrompt?`. OpenAI image URLs expire -after 1 hour -- download or display immediately. +has `b64Json?`, `url?`, and `revisedPrompt?`. OpenAI gpt-image models +return `b64Json`. Seedream image URLs expire after 24 hours, so download or +display them immediately. #### Image-conditioned generation: multimodal `prompt` parts @@ -272,7 +263,7 @@ compile time via the adapter's input-modality map. import { generateImage } from '@tanstack/ai' import { openaiImage } from '@tanstack/ai-openai' -// Image-to-image (OpenAI gpt-image-2 / gpt-image-1, dall-e-2) +// Image-to-image (every OpenAI gpt-image model) await generateImage({ adapter: openaiImage('gpt-image-2'), threadId: 'thread-1', @@ -294,7 +285,7 @@ await generateImage({ ], }) -// Inpaint via metadata.role === 'mask' (OpenAI gpt-image models, dall-e-2; fal mask_url) +// Inpaint via metadata.role === 'mask' (OpenAI gpt-image models; fal mask_url) await generateImage({ adapter: openaiImage('gpt-image-2'), threadId: 'thread-1', @@ -354,21 +345,21 @@ with `allowUrlFetch: true` on the adapter config | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `'reference'` | fal `reference_image_urls`; OpenRouter video `input_references[]`; Gemini multimodal part; positional otherwise | | `'character'` | Same as `'reference'`; Veo `referenceImages`; OpenRouter `input_references[]` | -| `'mask'` | OpenAI `mask` (gpt-image-2, gpt-image-1, dall-e-2); fal `mask_url` | +| `'mask'` | OpenAI `mask` (gpt-image models); fal `mask_url` | | `'control'` | fal `control_image_url` (ControlNet / depth / pose) | | `'start_frame'` | fal `start_image_url` (or the endpoint's field, e.g. `image_url` on Kling i2v); OpenRouter `frame_images[]` `first_frame`; Veo `image` | | `'end_frame'` | fal `end_image_url` (or e.g. `tail_image_url` / `last_frame_url`); OpenRouter `frame_images[]` `last_frame`; Veo `lastFrame` | **Provider support matrix:** -| Provider | `generateImage` image parts | `generateVideo` image parts | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OpenAI | gpt-image-2 / gpt-image-1 / -mini → `images.edit()` (up to 16). dall-e-2 → edit (1). dall-e-3 throws. | Sora-2 / -pro → `input_reference` (single). Throws if >1. | -| Gemini | Native image models send prompt parts as Interactions content blocks. Pass the previous `result.id` as `modelOptions.previous_interaction_id` to edit without resending that image. Imagen throws on image parts and does not chain. | Veo → first un-roled / `'start_frame'` image is the input image; `'end_frame'` → `lastFrame`; `'reference'` / `'character'` → `referenceImages`. Omni Flash sends image/video parts as interaction content blocks (no role routing). | -| fal | Per-endpoint field names from a generated map (`pnpm generate:fal-image-fields`). Defaults: 1 input → `image_url`; >1 → `image_urls`; roles → `mask_url` / `control_image_url` / `reference_image_urls`. | Per-endpoint map (e.g. Kling i2v start frame → `image_url`). Defaults: 1 input → `image_url`; `start_frame`/`end_frame` → `start_image_url`/`end_image_url`; `reference` → `reference_image_urls`. | -| Grok | grok-imagine models → `/v1/images/edits` JSON endpoint (≤3 sources, addressed by xAI in request order; prompt sent verbatim; mask/control throw). | Un-roled / `'start_frame'` image → starting frame; `'reference'` / `'character'` → `reference_images` (1.5). On 1.5 a starting frame can be combined with reference inputs (it pins the first frame). A `video` part + `modelOptions.mode: 'edit' \| 'extend'` routes to `/videos/edits` / `/videos/extensions` on `grok-imagine-video` only. | -| OpenRouter | Prompt parts map 1:1 onto multimodal `text` / `image_url` content parts, preserving interleaved order. | Dedicated async API (`openRouterVideo`): `start_frame`/`end_frame` → `frame_images[]` (`first_frame`/`last_frame`); `reference`/`character` → `input_references[]`; an unroled image defaults to the start frame. Frame roles validated against the model's `supported_frame_images` metadata. | -| Anthropic | n/a (no image generation API). | n/a | +| Provider | `generateImage` image parts | `generateVideo` image parts | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OpenAI | Every gpt-image model → `images.edit()` (up to 16). | Sora-2 / -pro → `input_reference` (single). Throws if >1. | +| Gemini | Native image models send prompt parts as Interactions content blocks. Pass the previous `result.id` as `modelOptions.previous_interaction_id` to edit without resending that image. | Veo → first un-roled / `'start_frame'` image is the input image; `'end_frame'` → `lastFrame`; `'reference'` / `'character'` → `referenceImages`. Omni Flash sends image/video parts as interaction content blocks (no role routing). | +| fal | Per-endpoint field names from a generated map (`pnpm generate:fal-image-fields`). Defaults: 1 input → `image_url`; >1 → `image_urls`; roles → `mask_url` / `control_image_url` / `reference_image_urls`. | Per-endpoint map (e.g. Kling i2v start frame → `image_url`). Defaults: 1 input → `image_url`; `start_frame`/`end_frame` → `start_image_url`/`end_image_url`; `reference` → `reference_image_urls`. | +| Grok | grok-imagine models → `/v1/images/edits` JSON endpoint (≤3 sources, addressed by xAI in request order; prompt sent verbatim; mask/control throw). | Un-roled / `'start_frame'` image → starting frame; `'reference'` / `'character'` → `reference_images` (1.5). On 1.5 a starting frame can be combined with reference inputs (it pins the first frame). A `video` part + `modelOptions.mode: 'edit' \| 'extend'` routes to `/videos/edits` / `/videos/extensions` on `grok-imagine-video` only. | +| OpenRouter | Prompt parts map 1:1 onto multimodal `text` / `image_url` content parts, preserving interleaved order. | Dedicated async API (`openRouterVideo`): `start_frame`/`end_frame` → `frame_images[]` (`first_frame`/`last_frame`); `reference`/`character` → `input_references[]`; an unroled image defaults to the start frame. Frame roles validated against the model's `supported_frame_images` metadata. | +| Anthropic | n/a (no image generation API). | n/a | Video and audio prompt parts follow the same `metadata.role` convention for video-to-video and lipsync flows on fal. Grok accepts one source @@ -412,7 +403,7 @@ const { generate, result, isLoading } = useGenerateAudio({ ### 3. Text-to-Speech -Adapters include `openaiSpeech` (tts-1, tts-1-hd, gpt-4o-audio-preview), +Adapters include `openaiSpeech` (tts-1, tts-1-hd, gpt-4o-mini-tts), `byteplusSpeech` (`seed-audio-1.0`), and `elevenlabsSpeech` (`eleven_v4`). `elevenlabsSpeech` accepts `format: 'mp3' | 'pcm' | 'opus' | 'wav'`. @@ -1165,18 +1156,18 @@ export const generateImageStreamFn = createServerFn({ method: 'POST' }) > Source: maintainer interview. -### c. MEDIUM: Not downloading OpenAI image URLs before they expire +### c. MEDIUM: Not downloading Seedream image URLs before they expire -OpenAI image URLs expire after 1 hour. If you store the URL and display it +BytePlus Seedream image URLs expire after 24 hours. If you store the URL and display it later, the image will silently break. Always download or display the image immediately, or convert to base64 for persistence. ```typescript import { generateImage } from '@tanstack/ai' -import { openaiImage } from '@tanstack/ai-openai' +import { byteplusImage } from '@tanstack/ai-byteplus' const result = await generateImage({ - adapter: openaiImage('dall-e-3'), + adapter: byteplusImage('dola-seedream-5-0-pro-260628'), threadId: 'thread-1', prompt: 'A mountain landscape', }) @@ -1190,8 +1181,8 @@ for (const img of result.images) { } } -// GOOD: use b64Json when available (no expiration) -// gpt-image-1 returns b64Json by default +// GOOD: ask for b64Json (no expiration) with +// modelOptions: { response_format: 'b64_json' } ``` > Source: docs/media/image-generation.md. @@ -1314,32 +1305,10 @@ generateSpeech({ ### h. HIGH: Passing image prompt parts to a model that doesn't support image-conditioned generation Not every model accepts image-conditioned prompts. The `prompt` type is -narrowed per model, so passing an image part to a text-only model -(dall-e-3, Imagen) is a **compile-time error**; adapters -also throw a clear runtime error as a backstop, so users learn at call -time rather than getting silently wrong output. - -```typescript ignore -// WRONG — dall-e-3 has no edit/inputs API; image parts are a type error -generateImage({ - adapter: openaiImage('dall-e-3'), - threadId: 'thread-1', - prompt: [ - { type: 'text', content: 'Edit this' }, - { type: 'image', source: { type: 'url', value: url } }, // ❌ type error - ], -}) - -// WRONG — Imagen is text-to-image only; same compile-time rejection -generateImage({ - adapter: geminiImage('imagen-4.0-generate-001'), - threadId: 'thread-1', - prompt: [ - { type: 'text', content: 'Edit this' }, - { type: 'image', source: { type: 'url', value: url } }, // ❌ type error - ], -}) -``` +narrowed per model, so passing an image part to a model with no image +input is a **compile-time error**; adapters also throw a clear runtime error +as a backstop, so users learn at call time rather than getting silently wrong +output. ```typescript import { generateImage } from '@tanstack/ai' diff --git a/packages/ai/src/activities/chat/agents/define-agent.ts b/packages/ai/src/activities/chat/agents/define-agent.ts index 41534c5d38..21cac9548d 100644 --- a/packages/ai/src/activities/chat/agents/define-agent.ts +++ b/packages/ai/src/activities/chat/agents/define-agent.ts @@ -157,7 +157,7 @@ export type SubagentChoiceOptions> = * description: 'Looks up facts', * run: (ctx) => * chat({ - * adapter: openaiText('gpt-5.6'), + * adapter: openaiText('gpt-6.1-sol'), * messages: ctx.messages, * threadId: ctx.threadId, * runId: ctx.runId, diff --git a/packages/ai/src/activities/generateImage/adapter.ts b/packages/ai/src/activities/generateImage/adapter.ts index f348cac676..59e8bbc848 100644 --- a/packages/ai/src/activities/generateImage/adapter.ts +++ b/packages/ai/src/activities/generateImage/adapter.ts @@ -29,7 +29,7 @@ export interface ImageAdapterConfig { * All type resolution happens at the provider call site, not in this interface. * * Generic parameters: - * - TModel: The specific model name (e.g., 'dall-e-3') + * - TModel: The specific model name (e.g., 'gpt-image-2.5-flare') * - TProviderOptions: Base provider-specific options (already resolved) * - TModelProviderOptionsByName: Map from model name to its specific provider options * - TModelSizeByName: Map from model name to its supported sizes diff --git a/packages/ai/src/activities/generateImage/index.ts b/packages/ai/src/activities/generateImage/index.ts index 8465c6eaac..33c5258fc2 100644 --- a/packages/ai/src/activities/generateImage/index.ts +++ b/packages/ai/src/activities/generateImage/index.ts @@ -209,39 +209,39 @@ function createId(prefix: string): string { * import { openaiImage } from '@tanstack/ai-openai' * * const result = await generateImage({ - * adapter: openaiImage('dall-e-3'), + * adapter: openaiImage('gpt-image-2.5-flare'), * prompt: 'A serene mountain landscape at sunset', * threadId, * }) * - * console.log(result.images[0].url) + * console.log(result.images[0].b64Json) * ``` * * @example Generate multiple images * ```ts * const result = await generateImage({ - * adapter: openaiImage('dall-e-2'), + * adapter: openaiImage('gpt-image-1-mini'), * prompt: 'A cute robot mascot', * threadId, * numberOfImages: 4, - * size: '512x512' + * size: '1024x1024' * }) * * result.images.forEach((image, i) => { - * console.log(`Image ${i + 1}: ${image.url}`) + * console.log(`Image ${i + 1}: ${image.b64Json}`) * }) * ``` * * @example With provider-specific options * ```ts * const result = await generateImage({ - * adapter: openaiImage('dall-e-3'), + * adapter: openaiImage('gpt-image-2.5-flare'), * prompt: 'A professional headshot photo', * threadId, * size: '1024x1024', * modelOptions: { - * quality: 'hd', - * style: 'natural' + * quality: 'high', + * background: 'transparent' * } * }) * ``` diff --git a/packages/ai/tests/summarize-max-length.test.ts b/packages/ai/tests/summarize-max-length.test.ts index 2f289fa83e..c240e9de1d 100644 --- a/packages/ai/tests/summarize-max-length.test.ts +++ b/packages/ai/tests/summarize-max-length.test.ts @@ -81,13 +81,13 @@ describe('ChatStreamSummarizeAdapter — maxLength reaches the wrapped adapter u const { textAdapter, lastModelOptions } = createRecordingTextAdapter() const adapter = new ChatStreamSummarizeAdapter( textAdapter, - 'llama-3.3-70b-versatile', + 'openai/gpt-oss-120b', 'groq', ) await adapter.summarize({ threadId: 'thread-1', - model: 'llama-3.3-70b-versatile', + model: 'openai/gpt-oss-120b', text: 'hi', maxLength: 256, logger, diff --git a/packages/openai-base/src/adapters/responses-text.ts b/packages/openai-base/src/adapters/responses-text.ts index 6b1f61b060..1c485a75c3 100644 --- a/packages/openai-base/src/adapters/responses-text.ts +++ b/packages/openai-base/src/adapters/responses-text.ts @@ -521,14 +521,12 @@ export abstract class OpenAIBaseResponsesTextAdapter< async *chatStream( chatOptions: TextOptions, ): AsyncIterable { - // A portable computer tool becomes the native computer tool of the model. + // A portable computer tool becomes the native computer tool. // The request and the stream both read the native tool. const options = chatOptions.tools ? { ...chatOptions, - tools: chatOptions.tools.map((tool) => - resolveComputerUseTool(tool, chatOptions.model), - ), + tools: chatOptions.tools.map((tool) => resolveComputerUseTool(tool)), } : chatOptions // Key streamed state by output item ID because argument deltas reference @@ -1381,14 +1379,6 @@ export abstract class OpenAIBaseResponsesTextAdapter< hasEmittedRunStarted: boolean }, ): AsyncIterable { - // A `computer_call` is a call to the computer tool of this request. - const computerTool = options.tools?.some( - (tool) => - tool.name === 'computer' && - getOpenAIProviderToolKind(tool) === 'computer_use', - ) - ? 'computer' - : 'computer_use_preview' const normalizeToolInput = createToolInputNormalizer( options.tools, (schema, required) => @@ -1592,7 +1582,7 @@ export abstract class OpenAIBaseResponsesTextAdapter< outputIndex: number, bareShell: boolean, ): Array => { - const call = readUserExecutedCall(item, { bareShell, computerTool }) + const call = readUserExecutedCall(item, { bareShell }) if (!call || toolCallMetadata.get(call.callId)?.ended) return [] const tracked = toolCallMetadata.get(call.callId) ?? { callId: call.callId, diff --git a/packages/openai-base/src/adapters/responses-user-tools.ts b/packages/openai-base/src/adapters/responses-user-tools.ts index 2105d8ee69..8539b2dee5 100644 --- a/packages/openai-base/src/adapters/responses-user-tools.ts +++ b/packages/openai-base/src/adapters/responses-user-tools.ts @@ -8,17 +8,14 @@ import type { /** * OpenAI Responses tools the app must run. * A container `shell` runs on the provider. `apply_patch`, `local_shell`, - * `computer`, `computer_use_preview`, and `shell` with + * `computer`, and `shell` with * `environment.type: "local"` (or no environment) do not. */ export type OpenAIUserToolName = | 'shell' | 'apply_patch' | 'local_shell' - | OpenAIComputerToolName - -/** The names of the computer tool: its two tool types. */ -export type OpenAIComputerToolName = 'computer' | 'computer_use_preview' + | 'computer' export interface OpenAIUserExecutedCall { name: OpenAIUserToolName @@ -88,8 +85,7 @@ export function readUserToolName(metadata: unknown): OpenAIUserToolName | null { name === 'shell' || name === 'apply_patch' || name === 'local_shell' || - name === 'computer' || - name === 'computer_use_preview' + name === 'computer' ) { return name } @@ -176,12 +172,10 @@ function applyPatchOperation( * `bareShell` is true only once the full response is known. A shell call * with no environment waits for that pass, so a hosted call that later * carries `shell_call_output` is not asked of the app. - * `computerTool` is the name of the computer tool of the request. A - * `computer_call` is a call to it. */ export function readUserExecutedCall( item: unknown, - options: { bareShell: boolean; computerTool?: OpenAIComputerToolName }, + options: { bareShell: boolean }, ): OpenAIUserExecutedCall | null { if (!isRecord(item) || typeof item.type !== 'string') return null const callId = typeof item.call_id === 'string' ? item.call_id : '' @@ -205,7 +199,7 @@ export function readUserExecutedCall( if (!callId || (!hasAction && !hasActions)) return null const checks = safetyChecks(item.pending_safety_checks) return { - name: options.computerTool ?? 'computer_use_preview', + name: 'computer', callId, ...(itemId ? { itemId } : {}), input: { @@ -316,7 +310,7 @@ export function userToolRequestItem( } } - if (name === 'computer' || name === 'computer_use_preview') { + if (name === 'computer') { const { action, actions } = args const hasAction = isComputerAction(action) const hasActions = isComputerActionList(actions) @@ -410,7 +404,7 @@ export function userToolResultItem( const name = readUserToolName(toolCall.metadata) if (!name) return null - if (name === 'computer' || name === 'computer_use_preview') { + if (name === 'computer') { const checks = computer.failed ? [] : (safetyChecks( diff --git a/packages/openai-base/src/tools/computer-use-tool.ts b/packages/openai-base/src/tools/computer-use-tool.ts index bb8b246714..b5b9d36bba 100644 --- a/packages/openai-base/src/tools/computer-use-tool.ts +++ b/packages/openai-base/src/tools/computer-use-tool.ts @@ -1,39 +1,25 @@ -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' +import { openAIProviderTool } from './openai-provider-tool' import { getComputerUseToolMetadata } from '@tanstack/ai/adapter-internals' -import type { - ComputerTool, - ComputerUsePreviewTool, -} from 'openai/resources/responses/responses' +import type { ComputerTool } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' /** - * The computer tool: `{ type: 'computer' }`, or the `computer_use_preview` - * tool of the `computer-use-preview` model. + * The computer tool: `{ type: 'computer' }`. */ -export type ComputerUseToolConfig = ComputerTool | ComputerUsePreviewTool +export type ComputerUseToolConfig = ComputerTool /** * Converts a standard Tool to OpenAI ComputerUseTool format */ export function convertComputerUseToolToAdapterFormat( - tool: Tool, + _tool: Tool, ): ComputerUseToolConfig { - const metadata = getOpenAIProviderToolMetadata(tool) as ComputerUseToolConfig - if (metadata.type === 'computer') return { type: 'computer' } - return { - type: 'computer_use_preview', - display_height: metadata.display_height, - display_width: metadata.display_width, - environment: metadata.environment, - } + return { type: 'computer' } } /** * Creates a standard Tool from ComputerUseTool parameters. The tool name is - * the tool type: `computer` or `computer_use_preview`. + * the tool type: `computer`. * * Base (non-branded) factory. Providers that need branded return types should * re-wrap this in their own package. @@ -52,29 +38,14 @@ export function computerUseTool(toolData: ComputerUseToolConfig): Tool { } /** - * Swaps a portable computer-use tool for the native OpenAI computer tool of - * the model. The `computer-use-preview` models get `computer_use_preview` - * with the display and environment of the tool. Other models get + * Swaps a portable computer-use tool for the native OpenAI computer tool, * `{ type: 'computer' }`. The result keeps the name `computer` and the * `execute` of the portable tool. Other tools come back unchanged. * * @param tool - A tool from the chat tool list. - * @param model - The model of the request. */ -export function resolveComputerUseTool(tool: Tool, model: string): Tool { - const metadata = getComputerUseToolMetadata(tool) - if (!metadata) return tool - // Dated ids such as `computer-use-preview-2025-03-11` match too. - const isPreviewModel = model.startsWith('computer-use-preview') - const native = computerUseTool( - isPreviewModel - ? { - type: 'computer_use_preview', - display_width: metadata.display.width, - display_height: metadata.display.height, - environment: metadata.environment, - } - : { type: 'computer' }, - ) +export function resolveComputerUseTool(tool: Tool): Tool { + if (!getComputerUseToolMetadata(tool)) return tool + const native = computerUseTool({ type: 'computer' }) return { ...native, name: 'computer', execute: tool.execute } } diff --git a/packages/openai-base/tests/computer-use-generic.test.ts b/packages/openai-base/tests/computer-use-generic.test.ts index 31d58e2bdb..03b44e19ce 100644 --- a/packages/openai-base/tests/computer-use-generic.test.ts +++ b/packages/openai-base/tests/computer-use-generic.test.ts @@ -99,38 +99,7 @@ async function runLoop(model: string, call: Record) { } describe('portable computer tool on the Responses API', () => { - it('sends computer_use_preview to computer-use-preview and runs the single action', async () => { - const action = { type: 'click', button: 'left', x: 10, y: 20 } - const { first, second, execute } = await runLoop('computer-use-preview', { - type: 'computer_call', - id: 'cu_1', - call_id: 'call_cu', - status: 'completed', - action, - pending_safety_checks: [safetyCheck], - }) - - expect(first.tools).toEqual([ - { - type: 'computer_use_preview', - display_width: 1280, - display_height: 800, - environment: 'linux', - }, - ]) - expect(execute.mock.calls[0]?.[0]).toEqual({ - action, - pending_safety_checks: [safetyCheck], - }) - expect(second.input).toContainEqual({ - type: 'computer_call_output', - call_id: 'call_cu', - output: screenshotOutput, - acknowledged_safety_checks: [safetyCheck], - }) - }) - - it('sends the computer tool to other models and runs the batch', async () => { + it('sends the computer tool and runs the batch', async () => { const actions = [ { type: 'click', button: 'left', x: 10, y: 20 }, { type: 'type', text: 'hello' }, diff --git a/packages/openai-base/tests/computer-use-loop.test.ts b/packages/openai-base/tests/computer-use-loop.test.ts index 40af7b12a5..d50de70d54 100644 --- a/packages/openai-base/tests/computer-use-loop.test.ts +++ b/packages/openai-base/tests/computer-use-loop.test.ts @@ -18,7 +18,7 @@ async function* completed(output: Array>) { type: 'response.created', response: { id: 'resp_1', - model: 'computer-use-preview', + model: 'gpt-6.1-sol', status: 'in_progress', }, }, @@ -26,7 +26,7 @@ async function* completed(output: Array>) { type: 'response.completed', response: { id: 'resp_1', - model: 'computer-use-preview', + model: 'gpt-6.1-sol', status: 'completed', output, usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, @@ -63,12 +63,7 @@ class FileAdapter extends TestAdapter { override readonly supportsFileSources = true } -const previewTool = computerUseTool({ - type: 'computer_use_preview', - display_width: 1024, - display_height: 768, - environment: 'browser', -}) +const computerTool = computerUseTool({ type: 'computer' }) const screenshot = [ { @@ -89,7 +84,7 @@ function failureMessage(text: string) { async function runLoop( execute: (input: unknown) => Promise, { - computer = previewTool, + computer = computerTool, calls = [clickCall], }: { computer?: Tool; calls?: Array> } = {}, ) { @@ -99,7 +94,7 @@ async function runLoop( const client = { responses: { create: (params: unknown) => create(params) }, } as unknown as OpenAI - const adapter = new TestAdapter('computer-use-preview', 'openai', client) + const adapter = new TestAdapter('gpt-6.1-sol', 'openai', client) const tool = { ...computer, execute: vi.fn(execute) } const chunks: Array = [] for await (const chunk of chat({ @@ -153,7 +148,6 @@ describe('computer use loop', () => { { type: 'type', text: 'hello' }, ] const { create, execute } = await runLoop(async () => screenshot, { - computer: computerUseTool({ type: 'computer' }), calls: [ { type: 'computer_call', diff --git a/packages/openai-base/tests/provider-tool-dispatch.test.ts b/packages/openai-base/tests/provider-tool-dispatch.test.ts index e7292ae5a5..cf90c8a0f0 100644 --- a/packages/openai-base/tests/provider-tool-dispatch.test.ts +++ b/packages/openai-base/tests/provider-tool-dispatch.test.ts @@ -31,7 +31,6 @@ const PROVIDER_TOOL_NAMES = [ 'apply_patch', 'code_interpreter', 'computer', - 'computer_use_preview', 'custom', 'file_search', 'image_generation', @@ -64,22 +63,6 @@ const FACTORY_CASES = [ tool: computerUseTool({ type: 'computer' }), wire: { type: 'computer' }, }, - { - name: 'computer_use_preview', - kind: 'openai.computer_use', - tool: computerUseTool({ - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }), - wire: { - type: 'computer_use_preview', - display_height: 768, - display_width: 1024, - environment: 'linux', - }, - }, { name: 'custom', kind: 'openai.custom', diff --git a/scripts/sync-provider-models.ts b/scripts/sync-provider-models.ts index a7897d19d7..0d1536a529 100644 --- a/scripts/sync-provider-models.ts +++ b/scripts/sync-provider-models.ts @@ -98,8 +98,8 @@ interface ProviderConfig { /** * Runtime set of models that accept tools plus `output_config.format` in * one request. New Anthropic ids are inserted here. `-fast` variants stay - * out: fast mode is a request parameter, and the model-meta test pins - * `claude-opus-5-fast` as excluded. + * out: fast mode is a request parameter, not a model id, and the model-meta + * test expects every registered model in the set. */ combinedToolsAndSchemaSetName?: string /** Provider key for conservative supports generation */ diff --git a/testing/e2e/global-setup.ts b/testing/e2e/global-setup.ts index cb4973bffe..a6ad6292e3 100644 --- a/testing/e2e/global-setup.ts +++ b/testing/e2e/global-setup.ts @@ -71,9 +71,9 @@ export default async function globalSetup() { // `chat({ reasoning })` wire check for Gemini: aimock's journal normalizes // the body, so this mount checks `generationConfig.thinkingConfig` itself. mock.mount('/reasoning-wire-gemini', geminiReasoningWireMount()) - // Gemini Veo video generation. aimock 1.29 mocks Gemini's `:predict` - // (Imagen) endpoint but not the long-running `:predictLongRunning` + - // operations-polling pair Veo uses, so mount both here. Non-Veo paths + // Gemini Veo video generation. aimock does not mock the long-running + // `:predictLongRunning` + operations-polling pair Veo uses, so mount both + // here. Non-Veo paths // under /v1beta/models (chat, images) return false and fall through to // aimock's native Gemini handlers. mock.mount('/v1beta/models', geminiVeoMount()) @@ -175,16 +175,6 @@ export default async function globalSetup() { // Bedrock's cache usage counters; the companion spec asserts both. mock.mount('/bedrock-converse-cache', bedrockConverseCacheMount()) - // Anthropic structured-output fallback usage (#758). The Anthropic text - // adapter has no native `structuredOutputStream`, so streaming structured - // output runs through the activity layer's `fallbackStructuredOutputStream`, - // which wraps the non-streaming `structuredOutput()`. aimock's native - // Anthropic helper doesn't synthesize a tool-forced `structured_output` - // response with usage, so this mount hand-crafts the non-streaming - // `/v1/messages` JSON the adapter expects. The companion spec asserts the - // `usage` survives onto `RUN_FINISHED.usage` on the fallback path. - mock.mount('/anthropic-structured-usage', anthropicStructuredUsageMount()) - // decide() usage on a failed answer mapping. aimock has no OpenAI Decisions // endpoint, so this mount hand-crafts a `/v1/decisions` response with valid // usage and a choice with no entry in `probabilities`. The companion spec @@ -762,7 +752,8 @@ function geminiNativeImageMount(): Mountable { const CREATE_ID = 'int_e2e_create' const EDIT_ID = 'int_e2e_edit' - // CamelCase generateContent / Imagen fields must never reach this body. + // The body takes Interactions fields only. These camelCase fields of the + // older generateContent and predict request shapes must never reach it. const ROOT_FORBIDDEN = [ 'personGeneration', 'safetyFilterLevel', @@ -2189,60 +2180,6 @@ function geminiProviderSearchMount(): Mountable { } } -/** - * Mounts the non-streaming Anthropic `/v1/messages` response the text adapter's - * `structuredOutput()` expects: a tool-forced `structured_output` `tool_use` - * block plus a `usage` object carrying `input_tokens` / `output_tokens` / - * `cache_read_input_tokens`. `buildAnthropicUsage` normalizes those into - * `promptTokens` / `completionTokens` / `promptTokensDetails.cachedTokens`. - * Drives the #758 fallback-path usage regression. - */ -function anthropicStructuredUsageMount(): Mountable { - return { - async handleRequest( - req: http.IncomingMessage, - res: http.ServerResponse, - // The mount prefix (/anthropic-structured-usage) is stripped before - // dispatch; the Anthropic SDK posts to /v1/messages and aimock - // strips the ?beta=... query string from `pathname`. - pathname: string, - ): Promise { - if (req.method !== 'POST' || !pathname.startsWith('/v1/messages')) { - return false - } - // structuredOutput() makes a non-streaming request (stream: false), so - // respond with a single JSON message rather than an SSE stream. - await drainBody(req) - res.statusCode = 200 - res.setHeader('Content-Type', 'application/json') - res.end( - JSON.stringify({ - id: 'msg_structured_usage_e2e', - type: 'message', - role: 'assistant', - model: 'claude-opus-4-1', - content: [ - { - type: 'tool_use', - id: 'toolu_structured_output', - name: 'structured_output', - input: { recommendation: 'Fender Stratocaster', price: 1299 }, - }, - ], - stop_reason: 'tool_use', - stop_sequence: null, - usage: { - input_tokens: 125, - output_tokens: 1346, - cache_read_input_tokens: 5760, - }, - }), - ) - return true - }, - } -} - /** * Mounts the OpenAI Decisions `/v1/decisions` response for the * `evaluate-usage-on-failure` spec: valid usage, but a `queue` answer whose diff --git a/testing/e2e/src/lib/feature-support.ts b/testing/e2e/src/lib/feature-support.ts index 7ecc6a1302..607fbb2f5b 100644 --- a/testing/e2e/src/lib/feature-support.ts +++ b/testing/e2e/src/lib/feature-support.ts @@ -389,7 +389,8 @@ export const matrix: Record> = { 'vercel-gateway', 'lovable', ]), - // Gemini excluded: aimock doesn't mock Gemini's Imagen predict endpoint format + // Gemini excluded: its image adapter uses the Interactions API, which aimock + // doesn't mock. gemini-native-image-wire and gemini-image-ga-models cover it. // vercel-gateway uses POST /v1/images/generations, the same path aimock // already mocks for openai. Drop this entry if the first run has no fixture. 'image-gen': new Set([ diff --git a/testing/e2e/src/lib/providers.ts b/testing/e2e/src/lib/providers.ts index bac63a8b25..40d2453348 100644 --- a/testing/e2e/src/lib/providers.ts +++ b/testing/e2e/src/lib/providers.ts @@ -89,7 +89,7 @@ const defaultModels: Record = { 'vertex-grok': 'grok-4.3', 'vertex-mistral': 'mistral-medium-3', ollama: 'mistral', - groq: 'llama-3.3-70b-versatile', + groq: 'openai/gpt-oss-120b', grok: 'grok-build-0.1', bedrock: 'openai.gpt-oss-120b-1:0', 'bedrock-responses': 'openai.gpt-oss-120b-1:0', @@ -237,7 +237,7 @@ export function createTextAdapter( }), groq: () => createChatOptions({ - adapter: createGroqText(model as 'llama-3.3-70b-versatile', DUMMY_KEY, { + adapter: createGroqText(model as 'openai/gpt-oss-120b', DUMMY_KEY, { baseURL: base, defaultHeaders: testHeaders, }), diff --git a/testing/e2e/src/routeTree.gen.ts b/testing/e2e/src/routeTree.gen.ts index cd900cf5d0..1c835ffeca 100644 --- a/testing/e2e/src/routeTree.gen.ts +++ b/testing/e2e/src/routeTree.gen.ts @@ -63,7 +63,6 @@ import { Route as ApiAnthropicOpus5CombinedWireRouteImport } from './routes/api. import { Route as ApiAnthropicRedactedThinkingWireRouteImport } from './routes/api.anthropic-redacted-thinking-wire' import { Route as ApiAnthropicSkillsWireRouteImport } from './routes/api.anthropic-skills-wire' import { Route as ApiAnthropicSonnet55WireRouteImport } from './routes/api.anthropic-sonnet-5-5-wire' -import { Route as ApiAnthropicStructuredUsageRouteImport } from './routes/api.anthropic-structured-usage' import { Route as ApiAnthropicThinkingOrderWireRouteImport } from './routes/api.anthropic-thinking-order-wire' import { Route as ApiAnthropicTruncatedToolInputWireRouteImport } from './routes/api.anthropic-truncated-tool-input-wire' import { Route as ApiArktypeToolWireRouteImport } from './routes/api.arktype-tool-wire' @@ -468,12 +467,6 @@ const ApiAnthropicSonnet55WireRoute = path: '/api/anthropic-sonnet-5-5-wire', getParentRoute: () => rootRouteImport, } as any) -const ApiAnthropicStructuredUsageRoute = - ApiAnthropicStructuredUsageRouteImport.update({ - id: '/api/anthropic-structured-usage', - path: '/api/anthropic-structured-usage', - getParentRoute: () => rootRouteImport, - } as any) const ApiAnthropicThinkingOrderWireRoute = ApiAnthropicThinkingOrderWireRouteImport.update({ id: '/api/anthropic-thinking-order-wire', @@ -1173,7 +1166,6 @@ export interface FileRoutesByFullPath { '/api/anthropic-redacted-thinking-wire': typeof ApiAnthropicRedactedThinkingWireRoute '/api/anthropic-skills-wire': typeof ApiAnthropicSkillsWireRoute '/api/anthropic-sonnet-5-5-wire': typeof ApiAnthropicSonnet55WireRoute - '/api/anthropic-structured-usage': typeof ApiAnthropicStructuredUsageRoute '/api/anthropic-thinking-order-wire': typeof ApiAnthropicThinkingOrderWireRoute '/api/anthropic-truncated-tool-input-wire': typeof ApiAnthropicTruncatedToolInputWireRoute '/api/arktype-tool-wire': typeof ApiArktypeToolWireRoute @@ -1352,7 +1344,6 @@ export interface FileRoutesByTo { '/api/anthropic-redacted-thinking-wire': typeof ApiAnthropicRedactedThinkingWireRoute '/api/anthropic-skills-wire': typeof ApiAnthropicSkillsWireRoute '/api/anthropic-sonnet-5-5-wire': typeof ApiAnthropicSonnet55WireRoute - '/api/anthropic-structured-usage': typeof ApiAnthropicStructuredUsageRoute '/api/anthropic-thinking-order-wire': typeof ApiAnthropicThinkingOrderWireRoute '/api/anthropic-truncated-tool-input-wire': typeof ApiAnthropicTruncatedToolInputWireRoute '/api/arktype-tool-wire': typeof ApiArktypeToolWireRoute @@ -1532,7 +1523,6 @@ export interface FileRoutesById { '/api/anthropic-redacted-thinking-wire': typeof ApiAnthropicRedactedThinkingWireRoute '/api/anthropic-skills-wire': typeof ApiAnthropicSkillsWireRoute '/api/anthropic-sonnet-5-5-wire': typeof ApiAnthropicSonnet55WireRoute - '/api/anthropic-structured-usage': typeof ApiAnthropicStructuredUsageRoute '/api/anthropic-thinking-order-wire': typeof ApiAnthropicThinkingOrderWireRoute '/api/anthropic-truncated-tool-input-wire': typeof ApiAnthropicTruncatedToolInputWireRoute '/api/arktype-tool-wire': typeof ApiArktypeToolWireRoute @@ -1713,7 +1703,6 @@ export interface FileRouteTypes { | '/api/anthropic-redacted-thinking-wire' | '/api/anthropic-skills-wire' | '/api/anthropic-sonnet-5-5-wire' - | '/api/anthropic-structured-usage' | '/api/anthropic-thinking-order-wire' | '/api/anthropic-truncated-tool-input-wire' | '/api/arktype-tool-wire' @@ -1892,7 +1881,6 @@ export interface FileRouteTypes { | '/api/anthropic-redacted-thinking-wire' | '/api/anthropic-skills-wire' | '/api/anthropic-sonnet-5-5-wire' - | '/api/anthropic-structured-usage' | '/api/anthropic-thinking-order-wire' | '/api/anthropic-truncated-tool-input-wire' | '/api/arktype-tool-wire' @@ -2071,7 +2059,6 @@ export interface FileRouteTypes { | '/api/anthropic-redacted-thinking-wire' | '/api/anthropic-skills-wire' | '/api/anthropic-sonnet-5-5-wire' - | '/api/anthropic-structured-usage' | '/api/anthropic-thinking-order-wire' | '/api/anthropic-truncated-tool-input-wire' | '/api/arktype-tool-wire' @@ -2251,7 +2238,6 @@ export interface RootRouteChildren { ApiAnthropicRedactedThinkingWireRoute: typeof ApiAnthropicRedactedThinkingWireRoute ApiAnthropicSkillsWireRoute: typeof ApiAnthropicSkillsWireRoute ApiAnthropicSonnet55WireRoute: typeof ApiAnthropicSonnet55WireRoute - ApiAnthropicStructuredUsageRoute: typeof ApiAnthropicStructuredUsageRoute ApiAnthropicThinkingOrderWireRoute: typeof ApiAnthropicThinkingOrderWireRoute ApiAnthropicTruncatedToolInputWireRoute: typeof ApiAnthropicTruncatedToolInputWireRoute ApiArktypeToolWireRoute: typeof ApiArktypeToolWireRoute @@ -2752,13 +2738,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ApiAnthropicSonnet55WireRouteImport parentRoute: typeof rootRouteImport } - '/api/anthropic-structured-usage': { - id: '/api/anthropic-structured-usage' - path: '/api/anthropic-structured-usage' - fullPath: '/api/anthropic-structured-usage' - preLoaderRoute: typeof ApiAnthropicStructuredUsageRouteImport - parentRoute: typeof rootRouteImport - } '/api/anthropic-thinking-order-wire': { id: '/api/anthropic-thinking-order-wire' path: '/api/anthropic-thinking-order-wire' @@ -3729,7 +3708,6 @@ const rootRouteChildren: RootRouteChildren = { ApiAnthropicRedactedThinkingWireRoute: ApiAnthropicRedactedThinkingWireRoute, ApiAnthropicSkillsWireRoute: ApiAnthropicSkillsWireRoute, ApiAnthropicSonnet55WireRoute: ApiAnthropicSonnet55WireRoute, - ApiAnthropicStructuredUsageRoute: ApiAnthropicStructuredUsageRoute, ApiAnthropicThinkingOrderWireRoute: ApiAnthropicThinkingOrderWireRoute, ApiAnthropicTruncatedToolInputWireRoute: ApiAnthropicTruncatedToolInputWireRoute, diff --git a/testing/e2e/src/routes/api.anthropic-structured-usage.ts b/testing/e2e/src/routes/api.anthropic-structured-usage.ts deleted file mode 100644 index ce0a89de78..0000000000 --- a/testing/e2e/src/routes/api.anthropic-structured-usage.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router' -import { chat, createChatOptions } from '@tanstack/ai' -import { createAnthropicChat } from '@tanstack/ai-anthropic' -import { z } from 'zod' - -const LLMOCK_DEFAULT_BASE = process.env.LLMOCK_URL || 'http://127.0.0.1:4010' -const DUMMY_KEY = 'sk-e2e-test-dummy-key' - -/** - * Drives the Anthropic text adapter (`AnthropicTextAdapter`) through the - * streaming structured-output path. That adapter has no native - * `structuredOutputStream`, so `chat({ outputSchema, stream: true })` routes - * through the activity layer's `fallbackStructuredOutputStream`, which wraps the - * non-streaming `structuredOutput()`. The mounted `/anthropic-structured-usage` - * aimock path returns a tool-forced `structured_output` response whose `usage` - * carries `input_tokens` / `output_tokens` / `cache_read_input_tokens`. - * - * Regressions for #758 and #1125: the companion spec asserts that usage reaches - * `RUN_FINISHED.usage` and fallback timestamps follow stream order. - */ -export const Route = createFileRoute('/api/anthropic-structured-usage')({ - server: { - handlers: { - POST: async () => { - // `claude-opus-4-1` is intentionally a *pre-4.5* model: it is NOT in - // `ANTHROPIC_COMBINED_TOOLS_AND_SCHEMA_MODELS`, so - // `supportsCombinedToolsAndSchema()` is false and the engine routes - // `chat({ outputSchema, stream: true })` through the non-streaming - // `structuredOutput()` wrapped by `fallbackStructuredOutputStream` — - // exactly the path #758 fixes. A 4.5+ model would use the native - // combined path and never touch the fallback. - const adapter = createAnthropicChat('claude-opus-4-1', DUMMY_KEY, { - baseURL: `${LLMOCK_DEFAULT_BASE}/anthropic-structured-usage`, - }) - - const options = createChatOptions({ - adapter, - outputSchema: z.object({ - recommendation: z.string(), - price: z.number(), - }), - stream: true, - }) - - let usage: Record | undefined - const timestamps: Record = {} - try { - for await (const chunk of chat({ - threadId: 'anthropic-structured-usage', - ...options, - messages: [{ role: 'user', content: 'recommend a guitar as json' }], - })) { - if ( - chunk.type === 'RUN_STARTED' && - typeof chunk.timestamp === 'number' - ) { - timestamps.runStarted = chunk.timestamp - } - if ( - chunk.type === 'CUSTOM' && - chunk.name === 'structured-output.start' && - typeof chunk.timestamp === 'number' - ) { - timestamps.structuredOutputStart = chunk.timestamp - } - if ( - chunk.type === 'CUSTOM' && - chunk.name === 'structured-output.complete' && - typeof chunk.timestamp === 'number' - ) { - timestamps.structuredOutputComplete = chunk.timestamp - } - if (chunk.type === 'RUN_FINISHED') { - usage = chunk.usage as Record | undefined - if (typeof chunk.timestamp === 'number') { - timestamps.runFinished = chunk.timestamp - } - } - } - } catch (error) { - return new Response( - JSON.stringify({ - ok: false, - error: error instanceof Error ? error.message : String(error), - }), - { status: 200, headers: { 'Content-Type': 'application/json' } }, - ) - } - - return new Response(JSON.stringify({ ok: true, usage, timestamps }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }) - }, - }, - }, -}) diff --git a/testing/e2e/src/routes/api.chat-completions-incomplete-stream.ts b/testing/e2e/src/routes/api.chat-completions-incomplete-stream.ts index 03ad3edc57..75823523c7 100644 --- a/testing/e2e/src/routes/api.chat-completions-incomplete-stream.ts +++ b/testing/e2e/src/routes/api.chat-completions-incomplete-stream.ts @@ -12,7 +12,7 @@ export const Route = createFileRoute('/api/chat-completions-incomplete-stream')( id: 'chatcmpl-eof', object: 'chat.completion.chunk', created: 1, - model: 'gpt-5.6', + model: 'gpt-5.6-sol', } const chunks: Array> = [ { @@ -77,7 +77,7 @@ export const Route = createFileRoute('/api/chat-completions-incomplete-stream')( ] let requests = 0 const adapter = createOpenaiChatCompletions( - 'gpt-5.6', + 'gpt-5.6-sol', 'test-placeholder', { maxRetries: 0, diff --git a/testing/e2e/src/routes/api.copilot-wire.ts b/testing/e2e/src/routes/api.copilot-wire.ts index 9c41e7fc66..d2fabd402b 100644 --- a/testing/e2e/src/routes/api.copilot-wire.ts +++ b/testing/e2e/src/routes/api.copilot-wire.ts @@ -69,7 +69,7 @@ const copilotStreams = { type: 'response.completed', response: { id: 'r-1', - model: 'gpt-5.6', + model: 'gpt-5.6-sol', status: 'completed', output: [{ ...call, id: 'fc_f' }], }, @@ -87,7 +87,7 @@ const copilotStreams = { type: 'response.completed', response: { id: 'r-2', - model: 'gpt-5.6', + model: 'gpt-5.6-sol', status: 'completed', output: [], }, @@ -118,7 +118,7 @@ const copilotStreams = { type: 'response.completed', response: { id: 'r-1', - model: 'gpt-5.6', + model: 'gpt-5.6-sol', status: 'completed', output: [], }, @@ -136,7 +136,7 @@ const copilotStreams = { type: 'response.completed', response: { id: 'r-2', - model: 'gpt-5.6', + model: 'gpt-5.6-sol', status: 'completed', output: [], }, @@ -179,7 +179,7 @@ export const Route = createFileRoute('/api/copilot-wire')({ let turn = 0 const stream = chat({ threadId: 'copilot-wire', - adapter: openaiCompatibleText('gpt-5.6', { + adapter: openaiCompatibleText('gpt-5.6-sol', { name: 'github-copilot', baseURL: 'https://api.githubcopilot.com', apiKey: 'copilot-token', @@ -246,7 +246,7 @@ export const Route = createFileRoute('/api/copilot-wire')({ let sent: unknown const { text } = await chat({ threadId: 'copilot-wire', - adapter: openaiCompatibleText('gpt-5.6', { + adapter: openaiCompatibleText('gpt-5.6-sol', { name: 'github-copilot', baseURL: `${LLMOCK_BASE}/v1`, apiKey: 'copilot-token', diff --git a/testing/e2e/src/routes/api.gemini-image-ga-models.ts b/testing/e2e/src/routes/api.gemini-image-ga-models.ts index e40e33837a..f70f5d3372 100644 --- a/testing/e2e/src/routes/api.gemini-image-ga-models.ts +++ b/testing/e2e/src/routes/api.gemini-image-ga-models.ts @@ -65,9 +65,8 @@ export const Route = createFileRoute('/api/gemini-image-ga-models')({ }), ) - // #1640: `gemini-nano-banana-2.1` is a native image model only - // because it is in `GEMINI_NATIVE_IMAGE_MODELS`. Otherwise it goes - // to the Imagen endpoint, which the mock does not serve. + // #1640: `gemini-nano-banana-2.1` goes to the Interactions API with + // `aspect_ratio` and `image_size`, like every Gemini image model. const nanoBanana21 = await runStage(() => generateImage({ threadId: 'gemini-image-ga-models', diff --git a/testing/e2e/src/routes/api.gemini-native-image-wire.ts b/testing/e2e/src/routes/api.gemini-native-image-wire.ts index bd2141ca90..dd34df05c8 100644 --- a/testing/e2e/src/routes/api.gemini-native-image-wire.ts +++ b/testing/e2e/src/routes/api.gemini-native-image-wire.ts @@ -8,7 +8,7 @@ import { createImageAdapter } from '@/lib/media-providers' * `geminiNativeImageMount` reads the Interactions request and 400s unless * `safety_settings` is `[{ type: "dangerous_content", threshold: * "block_only_high" }]`, `generation_config.thinking_level` is `"low"`, and - * no Imagen or generateContent field is present. A second call chains + * no camelCase field of an older request shape is present. A second call chains * `previous_interaction_id` from the first result and must not resend an * image. The mount answers `int_e2e_create` then `int_e2e_edit`. * diff --git a/testing/e2e/src/routes/api.openai-computer-use-wire.ts b/testing/e2e/src/routes/api.openai-computer-use-wire.ts index 86b6bd07e6..38be55f24d 100644 --- a/testing/e2e/src/routes/api.openai-computer-use-wire.ts +++ b/testing/e2e/src/routes/api.openai-computer-use-wire.ts @@ -12,16 +12,7 @@ const safetyCheck = { message: 'The page asks the model to do something else.', } -const computerCall = { - type: 'computer_call', - id: 'cu_e2e', - call_id: 'call_cu_e2e', - status: 'completed', - action: { type: 'click', button: 'left', x: 10, y: 20 }, - pending_safety_checks: [safetyCheck], -} - -// The computer tool of the newer models sends a batch of actions. +// The computer tool sends a batch of actions. const batchCall = { type: 'computer_call', id: 'cu_e2e_batch', @@ -31,7 +22,7 @@ const batchCall = { { type: 'click', button: 'left', x: 10, y: 20 }, { type: 'type', text: 'hello' }, ], - pending_safety_checks: [], + pending_safety_checks: [safetyCheck], } const screenshot = [ @@ -86,16 +77,12 @@ function responseStream( * custom `fetch` answers the first request with a `computer_call` and the * second with text, and records each request body. aimock cannot send a * `computer_call`, so this route serves the stream itself. - * - * `?tool=computer` uses the `computer` tool of `gpt-6.1-sol`, with a batch - * of actions. Anything else uses `computer_use_preview`. + * It uses the `computer` tool of `gpt-6.1-sol`, with a batch of actions. */ export const Route = createFileRoute('/api/openai-computer-use-wire')({ server: { handlers: { - POST: async ({ request }) => { - const batch = - new URL(request.url).searchParams.get('tool') === 'computer' + POST: async () => { const requestBodies: Array = [] const mockFetch: typeof fetch = async (input, init) => { const req = @@ -103,9 +90,7 @@ export const Route = createFileRoute('/api/openai-computer-use-wire')({ requestBodies.push(JSON.parse(await req.text())) const stream = requestBodies.length === 1 - ? responseStream('resp_cu_e2e_1', [ - batch ? batchCall : computerCall, - ]) + ? responseStream('resp_cu_e2e_1', [batchCall]) : responseStream('resp_cu_e2e_2', [answer]) return new Response(stream, { status: 200, @@ -122,37 +107,16 @@ export const Route = createFileRoute('/api/openai-computer-use-wire')({ { role: 'user' as const, content: 'Click the button.' }, ] try { - const stream = batch - ? chat({ - threadId: 'openai-computer-use-wire', - ...createChatOptions({ - adapter: createOpenaiChat('gpt-6.1-sol', DUMMY_KEY, { - fetch: mockFetch, - }), - }), - messages, - tools: [{ ...computerUseTool({ type: 'computer' }), execute }], - }) - : chat({ - threadId: 'openai-computer-use-wire', - ...createChatOptions({ - adapter: createOpenaiChat('computer-use-preview', DUMMY_KEY, { - fetch: mockFetch, - }), - }), - messages, - tools: [ - { - ...computerUseTool({ - type: 'computer_use_preview', - display_width: 1024, - display_height: 768, - environment: 'browser', - }), - execute, - }, - ], - }) + const stream = chat({ + threadId: 'openai-computer-use-wire', + ...createChatOptions({ + adapter: createOpenaiChat('gpt-6.1-sol', DUMMY_KEY, { + fetch: mockFetch, + }), + }), + messages, + tools: [{ ...computerUseTool({ type: 'computer' }), execute }], + }) for await (const _ of stream) { // Drain the stream. } diff --git a/testing/e2e/src/routes/api.structured-output-length-wire.ts b/testing/e2e/src/routes/api.structured-output-length-wire.ts index e8e2ad28d4..89ff9c227f 100644 --- a/testing/e2e/src/routes/api.structured-output-length-wire.ts +++ b/testing/e2e/src/routes/api.structured-output-length-wire.ts @@ -1,7 +1,6 @@ import { createServer } from 'node:http' import { createFileRoute } from '@tanstack/react-router' import { chat, createChatOptions } from '@tanstack/ai' -import { createAnthropicChat } from '@tanstack/ai-anthropic' import { createMistralText } from '@tanstack/ai-mistral' import { createOllamaChat } from '@tanstack/ai-ollama' import { openaiCompatible } from '@tanstack/ai-openai/compatible' @@ -72,7 +71,7 @@ async function withServer( * (`finish_reason: "length"`). For OpenAI-compatible and OpenRouter, * `chat()` takes the adapter's streaming structured-output path. A scripted * `fetch` returns a truncated JSON document and, for a reasoning model that - * spent the budget, empty content. Anthropic, Mistral and Ollama have no + * spent the budget, empty content. Mistral and Ollama have no * streaming structured output, so `chat()` calls their `structuredOutput()`; * Mistral and Ollama take no custom `fetch` and get a local server instead. * The companion spec checks that each adapter reports the token limit @@ -119,38 +118,6 @@ export const Route = createFileRoute('/api/structured-output-length-wire')({ ) } - // A pre-4.5 model takes the forced-tool `structuredOutput()` path - // (4.5+ use the native combined path). The forced tool call stopped - // at max_tokens, with partial input. - const anthropic = createAnthropicChat('claude-opus-4-1', DUMMY_KEY, { - fetch: async () => - Response.json({ - id: 'msg_length', - type: 'message', - role: 'assistant', - model: 'claude-opus-4-1', - content: [ - { - type: 'tool_use', - id: 'toolu_length', - name: 'structured_output', - input: {}, - }, - ], - stop_reason: 'max_tokens', - stop_sequence: null, - usage: { input_tokens: 10, output_tokens: 5 }, - }), - }) - errors['anthropic-truncated'] = await errorOf(() => - chat({ - threadId: 'structured-output-length-wire', - ...createChatOptions({ adapter: anthropic }), - messages: [{ role: 'user', content: 'Give me a title' }], - outputSchema, - }), - ) - errors['mistral-truncated'] = await withServer( { id: 'cmpl-length', diff --git a/testing/e2e/src/routes/api.summarize.ts b/testing/e2e/src/routes/api.summarize.ts index 96940c40e1..5228ba504a 100644 --- a/testing/e2e/src/routes/api.summarize.ts +++ b/testing/e2e/src/routes/api.summarize.ts @@ -94,7 +94,7 @@ function createSummarizeAdapter( ), ollama: () => createOllamaSummarize('mistral', llmockBase(aimockPort)), groq: () => - createGroqSummarize('llama-3.3-70b-versatile', DUMMY_KEY, { + createGroqSummarize('openai/gpt-oss-120b', DUMMY_KEY, { baseURL: `${llmockBase(aimockPort)}/openai/v1`, defaultHeaders: headers, }), diff --git a/testing/e2e/tests/anthropic-structured-usage.spec.ts b/testing/e2e/tests/anthropic-structured-usage.spec.ts deleted file mode 100644 index 023230bfd2..0000000000 --- a/testing/e2e/tests/anthropic-structured-usage.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { test, expect } from './fixtures' - -/** - * Regressions for #758 and #1125. `AnthropicTextAdapter` has no native - * `structuredOutputStream`, so `chat({ outputSchema, stream: true })` runs - * through the activity layer's `fallbackStructuredOutputStream`. That wrapper - * used to drop the `usage` returned by `structuredOutput()`, so consumers - * reading `RUN_FINISHED.usage` (and the `runOnUsage` middleware hook) saw - * `undefined` on every fallback-path provider. - * - * The `/api/anthropic-structured-usage` route drives the adapter against an - * aimock mount whose tool-forced `structured_output` response carries - * `input_tokens` / `output_tokens` / `cache_read_input_tokens`. This is the - * end-to-end proof that usage survives the fallback path and its timestamps - * follow stream order. - */ -test.describe('anthropic — structured-output fallback', () => { - test('preserves usage and timestamp ordering', async ({ request }) => { - const res = await request.post('/api/anthropic-structured-usage') - expect(res.ok()).toBe(true) - - const { ok, usage, timestamps, error } = (await res.json()) as { - ok: boolean - error?: string - usage?: { - promptTokens?: number - completionTokens?: number - totalTokens?: number - promptTokensDetails?: { cachedTokens?: number } - } - timestamps: { - runStarted: number - structuredOutputStart: number - structuredOutputComplete: number - runFinished: number - } - } - - expect(error ?? null).toBeNull() - expect(ok).toBe(true) - // promptTokens is the total input: 125 uncached + 5760 cache read. The - // mount sends no cache write. - expect(usage).toMatchObject({ - promptTokens: 5885, - completionTokens: 1346, - totalTokens: 7231, - promptTokensDetails: { cachedTokens: 5760 }, - }) - expect(timestamps.structuredOutputStart).toBeGreaterThanOrEqual( - timestamps.runStarted, - ) - expect(timestamps.structuredOutputComplete).toBeGreaterThanOrEqual( - timestamps.structuredOutputStart, - ) - expect(timestamps.runFinished).toBeGreaterThanOrEqual( - timestamps.structuredOutputComplete, - ) - }) -}) diff --git a/testing/e2e/tests/gemini-image-ga-models.spec.ts b/testing/e2e/tests/gemini-image-ga-models.spec.ts index 1ad67b5aab..19c23032ad 100644 --- a/testing/e2e/tests/gemini-image-ga-models.spec.ts +++ b/testing/e2e/tests/gemini-image-ga-models.spec.ts @@ -33,7 +33,7 @@ test.describe('gemini — GA image model ids and per-model sizes (#1104)', () => // Genuine runtime data, not a type check: `GeminiImageModels` is the // package's exported `as const` array of supported image model ids. // Nothing at runtime gates request routing on this array (the adapter - // dispatches on `GEMINI_NATIVE_IMAGE_MODELS` membership), so checking + // sends every model to the Interactions API), so checking // its contents is the only way to observe the id addition without a // TypeScript compile step. Before #1104 the array only carried the // shut-down `gemini-3.1-flash-image-preview` alias. diff --git a/testing/e2e/tests/openai-computer-use-wire.spec.ts b/testing/e2e/tests/openai-computer-use-wire.spec.ts index b700e7023f..b9be74518c 100644 --- a/testing/e2e/tests/openai-computer-use-wire.spec.ts +++ b/testing/e2e/tests/openai-computer-use-wire.spec.ts @@ -9,16 +9,14 @@ import { test, expect } from './fixtures' * `fetch` and returns the tool inputs and the request bodies as JSON. */ test.describe('openai — computer use wire format', () => { - test('runs the computer call and sends the screenshot back', async ({ - request, - }) => { + test('runs the batched actions of the computer tool', async ({ request }) => { const res = await request.post('/api/openai-computer-use-wire') expect(res.ok()).toBe(true) const { ok, error, toolInputs, requestBodies } = (await res.json()) as { ok: boolean error?: string toolInputs: Array - requestBodies: Array<{ input: Array }> + requestBodies: Array<{ tools?: Array; input: Array }> } if (!ok) { @@ -30,55 +28,13 @@ test.describe('openai — computer use wire format', () => { code: 'malicious_instructions', message: 'The page asks the model to do something else.', } - expect(toolInputs).toEqual([ - { - action: { type: 'click', button: 'left', x: 10, y: 20 }, - pending_safety_checks: [safetyCheck], - }, - ]) - expect(requestBodies).toHaveLength(2) - expect(requestBodies[1]!.input).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - type: 'computer_call', - call_id: 'call_cu_e2e', - action: { type: 'click', button: 'left', x: 10, y: 20 }, - }), - { - type: 'computer_call_output', - call_id: 'call_cu_e2e', - output: { - type: 'computer_screenshot', - image_url: 'data:image/png;base64,iVBORw0KGgo=', - detail: 'original', - }, - acknowledged_safety_checks: [safetyCheck], - }, - ]), - ) - }) - - test('runs the batched actions of the computer tool', async ({ request }) => { - const res = await request.post( - '/api/openai-computer-use-wire?tool=computer', - ) - expect(res.ok()).toBe(true) - const { ok, error, toolInputs, requestBodies } = (await res.json()) as { - ok: boolean - error?: string - toolInputs: Array - requestBodies: Array<{ tools?: Array; input: Array }> - } - - if (!ok) { - throw new Error(`Route failed: ${error}`) - } - const actions = [ { type: 'click', button: 'left', x: 10, y: 20 }, { type: 'type', text: 'hello' }, ] - expect(toolInputs).toEqual([{ actions, pending_safety_checks: [] }]) + expect(toolInputs).toEqual([ + { actions, pending_safety_checks: [safetyCheck] }, + ]) expect(requestBodies).toHaveLength(2) expect(requestBodies[0]!.tools).toEqual([{ type: 'computer' }]) expect(requestBodies[1]!.input).toEqual( @@ -96,6 +52,7 @@ test.describe('openai — computer use wire format', () => { image_url: 'data:image/png;base64,iVBORw0KGgo=', detail: 'original', }, + acknowledged_safety_checks: [safetyCheck], }, ]), ) diff --git a/testing/e2e/tests/structured-output-length-wire.spec.ts b/testing/e2e/tests/structured-output-length-wire.spec.ts index a4cbe00e5a..3a8e3ffec2 100644 --- a/testing/e2e/tests/structured-output-length-wire.spec.ts +++ b/testing/e2e/tests/structured-output-length-wire.spec.ts @@ -14,7 +14,6 @@ test.describe('structured output — finish_reason=length', () => { } expect(Object.keys(errors).sort()).toEqual([ - 'anthropic-truncated', 'compatible-empty', 'compatible-truncated', 'mistral-truncated', diff --git a/testing/panel/src/routes/tts.tsx b/testing/panel/src/routes/tts.tsx index 6af7a593d7..64051f7632 100644 --- a/testing/panel/src/routes/tts.tsx +++ b/testing/panel/src/routes/tts.tsx @@ -14,7 +14,7 @@ type Voice = | 'sage' | 'shimmer' | 'verse' -type Model = 'tts-1' | 'tts-1-hd' | 'gpt-4o-audio-preview' +type Model = 'tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts' type Format = 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm' interface TTSResult { @@ -43,7 +43,7 @@ const VOICES: Array<{ value: Voice; label: string }> = [ const MODELS: Array<{ value: Model; label: string }> = [ { value: 'tts-1', label: 'TTS-1 (Fast)' }, { value: 'tts-1-hd', label: 'TTS-1-HD (High Quality)' }, - { value: 'gpt-4o-audio-preview', label: 'GPT-4o Audio (Preview)' }, + { value: 'gpt-4o-mini-tts', label: 'GPT-4o mini TTS' }, ] const FORMATS: Array<{ value: Format; label: string }> = [ diff --git a/tools/workspace-plugin/src/generators/react-app/files/src/lib/model-selection.ts__tmpl__ b/tools/workspace-plugin/src/generators/react-app/files/src/lib/model-selection.ts__tmpl__ index 7963dd720d..2297b99520 100644 --- a/tools/workspace-plugin/src/generators/react-app/files/src/lib/model-selection.ts__tmpl__ +++ b/tools/workspace-plugin/src/generators/react-app/files/src/lib/model-selection.ts__tmpl__ @@ -19,7 +19,7 @@ export interface ModelOption { export const MODEL_OPTIONS: Array = [ // OpenAI - { provider: 'openai', model: 'gpt-5.6', label: 'OpenAI - GPT-5.6' }, + { provider: 'openai', model: 'gpt-6.1-sol', label: 'OpenAI - GPT-6.1 Sol' }, { provider: 'openai', model: 'gpt-5.6-sol', label: 'OpenAI - GPT-5.6 Sol' }, { provider: 'openai', @@ -248,18 +248,8 @@ export const MODEL_OPTIONS: Array = [ }, { provider: 'groq', - model: 'moonshotai/kimi-k2-instruct-0905', - label: 'Groq - Kimi K2 Instruct', - }, - { - provider: 'groq', - model: 'meta-llama/llama-4-maverick-17b-128e-instruct', - label: 'Groq - Llama 4 Maverick', - }, - { - provider: 'groq', - model: 'qwen/qwen3-32b', - label: 'Groq - Qwen3 32B', + model: 'qwen/qwen3.8-27b', + label: 'Groq - Qwen3.8 27B', }, // Grok From 520d36a47aabe084f9cc01cb0e7a0b358471e96d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:04:39 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../references/anthropic-adapter.md | 30 +++++++++---------- .../skills/ai-core/chat-experience/SKILL.md | 13 ++++++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md b/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md index 0b3250e8e6..36afecb945 100644 --- a/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +++ b/packages/ai/skills/ai-core/adapter-configuration/references/anthropic-adapter.md @@ -21,21 +21,21 @@ import { anthropicText } from '@tanstack/ai-anthropic' ## Key Chat Models -| Model | Context Window | Max Output | Notes | -| -------------------- | -------------- | ---------- | ------------------------------------------- | -| `claude-fable-5-1` | 1M | 128K | Newest; thinking always on (adaptive) | -| `claude-fable-5` | 1M | 128K | Most capable; thinking always on (adaptive) | -| `claude-opus-5` | 1M | 128K | Opus tier; budget thinking + sampling | -| `claude-sonnet-5-5` | 1M | 128K | Sonnet tier; adaptive-only, no sampling | -| `claude-haiku-5-5` | 1M | 128K | Fastest; adaptive thinking, no sampling | -| `claude-sonnet-5` | 1M | 128K | Best balance; adaptive thinking by default | -| `claude-opus-4-8` | 1M | 128K | Opus tier; adaptive thinking, no sampling | -| `claude-opus-4-7` | 1M | 128K | Older Opus; adaptive thinking, no sampling | -| `claude-opus-4-6` | 200K | 128K | Older Opus, adaptive + budget thinking | -| `claude-sonnet-4-6` | 1M | 64K | Previous gen balanced, adaptive + budget | -| `claude-sonnet-4-5` | 200K | 64K | Previous gen balanced | -| `claude-opus-4-5` | 200K | 32K | Previous gen most capable | -| `claude-haiku-4-5` | 200K | 64K | Fast and affordable | +| Model | Context Window | Max Output | Notes | +| ------------------- | -------------- | ---------- | ------------------------------------------- | +| `claude-fable-5-1` | 1M | 128K | Newest; thinking always on (adaptive) | +| `claude-fable-5` | 1M | 128K | Most capable; thinking always on (adaptive) | +| `claude-opus-5` | 1M | 128K | Opus tier; budget thinking + sampling | +| `claude-sonnet-5-5` | 1M | 128K | Sonnet tier; adaptive-only, no sampling | +| `claude-haiku-5-5` | 1M | 128K | Fastest; adaptive thinking, no sampling | +| `claude-sonnet-5` | 1M | 128K | Best balance; adaptive thinking by default | +| `claude-opus-4-8` | 1M | 128K | Opus tier; adaptive thinking, no sampling | +| `claude-opus-4-7` | 1M | 128K | Older Opus; adaptive thinking, no sampling | +| `claude-opus-4-6` | 200K | 128K | Older Opus, adaptive + budget thinking | +| `claude-sonnet-4-6` | 1M | 64K | Previous gen balanced, adaptive + budget | +| `claude-sonnet-4-5` | 200K | 64K | Previous gen balanced | +| `claude-opus-4-5` | 200K | 32K | Previous gen most capable | +| `claude-haiku-4-5` | 200K | 64K | Fast and affordable | Note: Model IDs use the format `claude-sonnet-5`, `claude-opus-4-8`, etc. Retired models (Claude 3.x, Sonnet 3.7, Opus 4 / Sonnet 4, Opus 4.1) and the diff --git a/packages/ai/skills/ai-core/chat-experience/SKILL.md b/packages/ai/skills/ai-core/chat-experience/SKILL.md index 5b4ee2af30..a6308ef1e5 100644 --- a/packages/ai/skills/ai-core/chat-experience/SKILL.md +++ b/packages/ai/skills/ai-core/chat-experience/SKILL.md @@ -784,7 +784,12 @@ chat({ adapter: openaiText('gpt-6.1-sol'), messages, threadId: 'thread-1' }) ```typescript ignore // WRONG — `openai()` is no longer exported from @tanstack/ai-openai import { openai } from '@tanstack/ai-openai' -chat({ adapter: openai(), model: 'gpt-6.1-sol', messages, threadId: 'thread-1' }) +chat({ + adapter: openai(), + model: 'gpt-6.1-sol', + messages, + threadId: 'thread-1', +}) ``` ```typescript @@ -931,7 +936,11 @@ import { openaiText } from '@tanstack/ai-openai' export async function POST(request: Request) { const { messages, threadId } = await request.json() - const stream = chat({ adapter: openaiText('gpt-6.1-sol'), messages, threadId }) + const stream = chat({ + adapter: openaiText('gpt-6.1-sol'), + messages, + threadId, + }) const readable = new ReadableStream({ async start(controller) {