diff --git a/.changeset/provider-tools-ai-anthropic.md b/.changeset/provider-tools-ai-anthropic.md new file mode 100644 index 000000000..4ec2e77b4 --- /dev/null +++ b/.changeset/provider-tools-ai-anthropic.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-anthropic': minor +--- + +Expose provider-tool factories (`webSearchTool`, `codeExecutionTool`, `computerUseTool`, `bashTool`, `textEditorTool`, `webFetchTool`, `memoryTool`, `customTool`) on a new `/tools` subpath. Each factory now returns a branded type (e.g. `AnthropicWebSearchTool`) that is gated against the selected model's `supports.tools` list. Existing factory signatures and runtime behavior are unchanged; old config-type aliases (`WebSearchTool`, `BashTool`, etc.) remain as `@deprecated` aliases pointing at the renamed `*ToolConfig` types. diff --git a/.changeset/provider-tools-ai-core.md b/.changeset/provider-tools-ai-core.md new file mode 100644 index 000000000..866f37e23 --- /dev/null +++ b/.changeset/provider-tools-ai-core.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai': minor +--- + +Add `ProviderTool` phantom-branded tool subtype and a `toolCapabilities` channel on `TextAdapter['~types']`. `TextActivityOptions['tools']` is now typed so that adapter-exported provider tools are gated against the selected model's `supports.tools` list. User tools from `toolDefinition()` remain unaffected. diff --git a/.changeset/provider-tools-ai-gemini.md b/.changeset/provider-tools-ai-gemini.md new file mode 100644 index 000000000..37e5a8b51 --- /dev/null +++ b/.changeset/provider-tools-ai-gemini.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai-gemini': minor +--- + +Expose provider-tool factories (`codeExecutionTool`, `fileSearchTool`, `googleSearchTool`, `googleSearchRetrievalTool`, `googleMapsTool`, `urlContextTool`, `computerUseTool`) on a new `/tools` subpath, each returning a branded type gated against the selected model's `supports.tools` list. + +Note: `supports.capabilities` entries that described tools (`code_execution`, `file_search`, `grounding_with_gmaps` → renamed `google_maps`, `search_grounding` → renamed `google_search`, `url_context`) have been relocated to the new `supports.tools` field. The `capabilities` array loses those entries. This is a model-meta shape change but not a runtime break. diff --git a/.changeset/provider-tools-ai-grok.md b/.changeset/provider-tools-ai-grok.md new file mode 100644 index 000000000..f3fbc58e8 --- /dev/null +++ b/.changeset/provider-tools-ai-grok.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-grok': patch +--- + +Expose the `/tools` subpath and add an empty `supports.tools: []` channel per model so Grok adapters participate in the core tool-capability type gating. No provider-specific tool factories are exposed yet — define your own tools with `toolDefinition()` from `@tanstack/ai`. diff --git a/.changeset/provider-tools-ai-groq.md b/.changeset/provider-tools-ai-groq.md new file mode 100644 index 000000000..616267f6f --- /dev/null +++ b/.changeset/provider-tools-ai-groq.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-groq': patch +--- + +Expose the `/tools` subpath and add an empty `supports.tools: []` channel per model so Groq adapters participate in the core tool-capability type gating. No provider-specific tool factories are exposed yet — define your own tools with `toolDefinition()` from `@tanstack/ai`. diff --git a/.changeset/provider-tools-ai-openai.md b/.changeset/provider-tools-ai-openai.md new file mode 100644 index 000000000..9f87ca867 --- /dev/null +++ b/.changeset/provider-tools-ai-openai.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-openai': minor +--- + +Expose provider-tool factories (`webSearchTool`, `webSearchPreviewTool`, `fileSearchTool`, `imageGenerationTool`, `codeInterpreterTool`, `mcpTool`, `computerUseTool`, `localShellTool`, `shellTool`, `applyPatchTool`, `customTool`) on a new `/tools` subpath. Each factory returns a branded type (e.g. `OpenAIWebSearchTool`) gated against the selected model's `supports.tools` list. `supports.tools` was expanded to include `web_search_preview`, `local_shell`, `shell`, `apply_patch`. Existing factory signatures and runtime behavior are unchanged. diff --git a/.changeset/provider-tools-ai-openrouter.md b/.changeset/provider-tools-ai-openrouter.md new file mode 100644 index 000000000..9f39aa8d1 --- /dev/null +++ b/.changeset/provider-tools-ai-openrouter.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai-openrouter': minor +--- + +**Breaking export change.** `createWebSearchTool` has been removed from the package root. Import `webSearchTool` from `@tanstack/ai-openrouter/tools` instead. See Migration Guide §6 for the before/after snippet. + +Alongside: the new `/tools` subpath exposes `webSearchTool` (branded `OpenRouterWebSearchTool`) and the existing `convertToolsToProviderFormat`. A new `supports.tools` channel on each chat model gates provider tools at the type level. diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index 1c1a63b9d..50e7ef155 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -237,3 +237,198 @@ Creates an Anthropic summarization adapter with an explicit API key. - [Getting Started](../getting-started/quick-start) - Learn the basics - [Tools Guide](../tools/tools) - Learn about tools - [Other Adapters](./openai) - Explore other providers + +## Provider Tools + +Anthropic exposes several native tools beyond user-defined function calls. +Import them from `@tanstack/ai-anthropic/tools` and pass them into +`chat({ tools: [...] })`. + +> For the full concept, a comparison matrix, and type-gating details, see +> [Provider Tools](../tools/provider-tools.md). + +### `webSearchTool` + +Enables Claude to run Anthropic's native web search with inline citations. +Scope the search with `allowed_domains` or `blocked_domains` (mutually +exclusive); set `max_uses` to cap per-turn cost. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { webSearchTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-opus-4-6"), + messages: [{ role: "user", content: "What's new in AI this week?" }], + tools: [ + webSearchTool({ + name: "web_search", + type: "web_search_20250305", + max_uses: 2, + }), + ], +}); +``` + +**Supported models:** every current Claude model. `claude-3-haiku` supports +only `web_search` (not `web_fetch`). See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `webFetchTool` + +Lets Claude fetch the contents of a URL directly, useful when you want the +model to read a specific page rather than run a search. Takes no required +arguments — pass an optional config object to override defaults. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { webFetchTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "Summarise https://example.com" }], + tools: [webFetchTool()], +}); +``` + +**Supported models:** Claude Sonnet 4.x and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `codeExecutionTool` + +Gives Claude a sandboxed code-execution environment so it can run Python +snippets, analyse data, and return results inline. Choose the version string +that matches your desired API revision. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { codeExecutionTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "Plot a histogram of [1,2,2,3,3,3]" }], + tools: [ + codeExecutionTool({ name: "code_execution", type: "code_execution_20250825" }), + ], +}); +``` + +**Supported models:** Claude Sonnet 4.x and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `computerUseTool` + +Allows Claude to observe a virtual desktop (screenshots) and interact with it +via keyboard and mouse events. Provide the screen resolution so Claude can +calculate accurate coordinates. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { computerUseTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "Open the browser and go to example.com" }], + tools: [ + computerUseTool({ + type: "computer_20250124", + name: "computer", + display_width_px: 1024, + display_height_px: 768, + }), + ], +}); +``` + +**Supported models:** Claude Sonnet 3.5 and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `bashTool` + +Provides Claude with a persistent bash shell session, letting it run arbitrary +commands, install packages, or manipulate files on the host. Choose the type +string that matches your API revision. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { bashTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "List all TypeScript files in src/" }], + tools: [bashTool({ name: "bash", type: "bash_20250124" })], +}); +``` + +**Supported models:** Claude Sonnet 3.5 and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `textEditorTool` + +Gives Claude a structured text-editor interface for viewing and modifying files +using `str_replace`, `create`, `view`, and `undo_edit` commands. Choose the +type string for the API revision you target. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { textEditorTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "Fix the bug in src/index.ts" }], + tools: [ + textEditorTool({ type: "text_editor_20250124", name: "str_replace_editor" }), + ], +}); +``` + +**Supported models:** Claude Sonnet 3.5 and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `memoryTool` + +Enables Claude to store and retrieve information across conversation turns +using Anthropic's managed memory service. Call with no arguments to use +default configuration. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { memoryTool } from "@tanstack/ai-anthropic/tools"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "Remember that I prefer metric units" }], + tools: [memoryTool()], +}); +``` + +**Supported models:** Claude Sonnet 4.x and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `customTool` + +Creates a tool with an inline JSON Schema input definition instead of going +through `toolDefinition()`. Useful when you need fine-grained control over the +schema shape or want to add `cache_control`. Unlike branded provider tools, +`customTool` returns a plain `Tool` and is accepted by any chat model. + +```typescript +import { chat } from "@tanstack/ai"; +import { anthropicText } from "@tanstack/ai-anthropic"; +import { customTool } from "@tanstack/ai-anthropic/tools"; +import { z } from "zod"; + +const stream = chat({ + adapter: anthropicText("claude-sonnet-4-5"), + messages: [{ role: "user", content: "Look up user 42" }], + tools: [ + customTool( + "lookup_user", + "Look up a user by ID and return their profile", + z.object({ userId: z.number() }), + ), + ], +}); +``` + +**Supported models:** all current Claude models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index 5c3925e61..a3e4ff7e4 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -395,3 +395,162 @@ Creates a Gemini TTS adapter with an explicit API key. - [Getting Started](../getting-started/quick-start) - Learn the basics - [Tools Guide](../tools/tools) - Learn about tools - [Other Adapters](./openai) - Explore other providers + +## Provider Tools + +Google Gemini exposes several native tools beyond user-defined function calls. +Import them from `@tanstack/ai-gemini/tools` and pass them into +`chat({ tools: [...] })`. + +> For the full concept, a comparison matrix, and type-gating details, see +> [Provider Tools](../tools/provider-tools.md). + +### `codeExecutionTool` + +Enables Gemini to execute Python code in a sandboxed environment and return +results inline. Takes no arguments — include it in the `tools` array to +activate code execution. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { codeExecutionTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "Calculate the first 10 Fibonacci numbers" }], + tools: [codeExecutionTool()], +}); +``` + +**Supported models:** Gemini 1.5 Pro, Gemini 2.x, Gemini 2.5 and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `fileSearchTool` + +Searches files that have been uploaded to the Gemini File API. Pass a +`FileSearch` config object with the corpus and file IDs to scope the search. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { fileSearchTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "Find the quarterly revenue figures" }], + tools: [ + fileSearchTool({ + fileSearchStoreNames: ["fileSearchStores/my-file-search-store-123"], + }), + ], +}); +``` + +**Supported models:** Gemini 2.x and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `googleSearchTool` + +Enables Gemini to query Google Search and incorporate grounded search results +into its response. Pass an optional `GoogleSearch` config or call with no +arguments to use defaults. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { googleSearchTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "What's the weather in Tokyo right now?" }], + tools: [googleSearchTool()], +}); +``` + +**Supported models:** Gemini 1.5 Pro, Gemini 2.x, Gemini 2.5. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `googleSearchRetrievalTool` + +A retrieval-augmented variant of Google Search that returns ranked passages +from the web with configurable dynamic retrieval mode. Pass an optional +`GoogleSearchRetrieval` config. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { googleSearchRetrievalTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "Explain the latest JavaScript proposals" }], + tools: [ + googleSearchRetrievalTool({ + dynamicRetrievalConfig: { mode: "MODE_DYNAMIC", dynamicThreshold: 0.7 }, + }), + ], +}); +``` + +**Supported models:** Gemini 1.5 Pro and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `googleMapsTool` + +Connects Gemini to the Google Maps API for location-aware queries such as +directions, place search, and geocoding. Pass an optional `GoogleMaps` config +or call with no arguments. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { googleMapsTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "Find coffee shops near Union Square, SF" }], + tools: [googleMapsTool()], +}); +``` + +**Supported models:** Gemini 2.5 and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `urlContextTool` + +Fetches and includes the content of URLs mentioned in the conversation so +Gemini can reason over live web pages. Takes no arguments. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { urlContextTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "Summarise https://example.com/article" }], + tools: [urlContextTool()], +}); +``` + +**Supported models:** Gemini 2.x and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `computerUseTool` + +Allows Gemini to observe a virtual desktop via screenshots and interact with +it using predefined computer-use functions. Provide the `environment` and +optionally restrict callable functions via `excludedPredefinedFunctions`. + +```typescript +import { chat } from "@tanstack/ai"; +import { geminiText } from "@tanstack/ai-gemini"; +import { computerUseTool } from "@tanstack/ai-gemini/tools"; + +const stream = chat({ + adapter: geminiText("gemini-2.5-pro"), + messages: [{ role: "user", content: "Navigate to example.com in the browser" }], + tools: [ + computerUseTool({ + environment: "browser", + }), + ], +}); +``` + +**Supported models:** Gemini 2.5 and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). diff --git a/docs/adapters/grok.md b/docs/adapters/grok.md index e38480896..b08cf4091 100644 --- a/docs/adapters/grok.md +++ b/docs/adapters/grok.md @@ -239,3 +239,12 @@ Creates a Grok image generation adapter with an explicit API key. - [Getting Started](../getting-started/quick-start) - Learn the basics - [Tools Guide](../tools/tools) - Learn about tools - [Other Adapters](./openai) - Explore other providers + +## Provider Tools + +Grok does not currently expose provider-specific tool factories. +Define your own tools with `toolDefinition()` from `@tanstack/ai`. + +See [Tools](../tools/tools.md) for the general tool-definition flow, or +[Provider Tools](../tools/provider-tools.md) for other providers' +native-tool offerings. diff --git a/docs/adapters/groq.md b/docs/adapters/groq.md index 40788d5f1..b6ab11530 100644 --- a/docs/adapters/groq.md +++ b/docs/adapters/groq.md @@ -279,3 +279,12 @@ Creates a Groq TTS adapter with an explicit API key. - [Getting Started](../getting-started/quick-start) - Learn the basics - [Tools Guide](../tools/tools) - Learn about tools - [Other Adapters](./openai) - Explore other providers + +## Provider Tools + +Groq does not currently expose provider-specific tool factories. +Define your own tools with `toolDefinition()` from `@tanstack/ai`. + +See [Tools](../tools/tools.md) for the general tool-definition flow, or +[Provider Tools](../tools/provider-tools.md) for other providers' +native-tool offerings. diff --git a/docs/adapters/openai.md b/docs/adapters/openai.md index 7fa6fd126..122aaf520 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -342,3 +342,265 @@ Creates an OpenAI transcription adapter with an explicit API key. - [Getting Started](../getting-started/quick-start) - Learn the basics - [Tools Guide](../tools/tools) - Learn about tools - [Other Adapters](./anthropic) - Explore other providers + +## Provider Tools + +OpenAI exposes several native tools beyond user-defined function calls. +Import them from `@tanstack/ai-openai/tools` and pass them into +`chat({ tools: [...] })`. + +> For the full concept, a comparison matrix, and type-gating details, see +> [Provider Tools](../tools/provider-tools.md). + +### `webSearchTool` + +Enables the model to run a web search and return grounded results with +citations. Pass a `WebSearchToolConfig` object (typed from the OpenAI SDK) +to configure the tool. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { webSearchTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "What's new in AI this week?" }], + tools: [webSearchTool({ type: "web_search" })], +}); +``` + +**Supported models:** GPT-4o, GPT-5, and Responses API-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `webSearchPreviewTool` + +The preview variant of web search with additional options for controlling +search context size and user location. Use this when you want fine-grained +control over the search context sent to the model. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { webSearchPreviewTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Latest news about TypeScript" }], + tools: [ + webSearchPreviewTool({ + type: "web_search_preview_2025_03_11", + search_context_size: "high", + }), + ], +}); +``` + +**Supported models:** GPT-4o and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `fileSearchTool` + +Searches OpenAI vector stores that you have pre-populated, letting the model +retrieve relevant document chunks. Provide the `vector_store_ids` to search +and optionally limit results with `max_num_results` (1–50). + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { fileSearchTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "What does the handbook say about PTO?" }], + tools: [ + fileSearchTool({ + type: "file_search", + vector_store_ids: ["vs_abc123"], + max_num_results: 5, + }), + ], +}); +``` + +**Supported models:** GPT-4o and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `imageGenerationTool` + +Allows the model to generate images inline during a conversation using +DALL-E/GPT-Image. Pass quality, size, and style options via the config object. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { imageGenerationTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Draw a logo for my app" }], + tools: [ + imageGenerationTool({ + quality: "high", + size: "1024x1024", + }), + ], +}); +``` + +**Supported models:** GPT-5 and GPT-Image-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `codeInterpreterTool` + +Gives the model a sandboxed Python execution environment. The `container` +field configures the execution environment; pass the full +`CodeInterpreterToolConfig` object. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { codeInterpreterTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Analyse this CSV and plot a chart" }], + tools: [ + codeInterpreterTool({ type: "code_interpreter", container: { type: "auto" } }), + ], +}); +``` + +**Supported models:** GPT-4o and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `mcpTool` + +Connects the model to a remote MCP (Model Context Protocol) server, exposing +all its capabilities as callable tools. Provide either `server_url` or +`connector_id` — not both. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { mcpTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "List my GitHub issues" }], + tools: [ + mcpTool({ + server_url: "https://mcp.example.com", + server_label: "github", + }), + ], +}); +``` + +**Supported models:** GPT-4o and above. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `computerUseTool` + +Lets the model observe a virtual desktop via screenshots and interact with +it using keyboard and mouse events. Provide the display dimensions and the +execution environment type. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { computerUseTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("computer-use-preview"), + messages: [{ role: "user", content: "Open Chrome and navigate to example.com" }], + tools: [ + computerUseTool({ + type: "computer_use_preview", + display_width: 1024, + display_height: 768, + environment: "browser", + }), + ], +}); +``` + +**Supported models:** `computer-use-preview`. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `localShellTool` + +Provides the model with a local shell for executing system commands. Takes no +arguments — the tool is enabled simply by including it in the `tools` array. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { localShellTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Run the test suite and summarise failures" }], + tools: [localShellTool()], +}); +``` + +**Supported models:** GPT-5.x and other agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `shellTool` + +A function-style shell tool that exposes shell execution as a structured +function call. Takes no arguments. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { shellTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Count lines in all JS files" }], + tools: [shellTool()], +}); +``` + +**Supported models:** GPT-5.x and other agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `applyPatchTool` + +Lets the model apply unified-diff patches to modify files directly. Takes no +arguments — include it in the `tools` array to enable patch application. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { applyPatchTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Fix the import paths in src/index.ts" }], + tools: [applyPatchTool()], +}); +``` + +**Supported models:** GPT-5.x and other agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `customTool` + +Defines a custom Responses API tool with an explicit name, description, and +format. Use this when none of the structured tool types fits your use case. +Unlike branded provider tools, `customTool` returns a plain `Tool` and is +accepted by any chat model. + +```typescript +import { chat } from "@tanstack/ai"; +import { openaiText } from "@tanstack/ai-openai"; +import { customTool } from "@tanstack/ai-openai/tools"; + +const stream = chat({ + adapter: openaiText("gpt-5.2"), + messages: [{ role: "user", content: "Look up order #1234" }], + tools: [ + customTool({ + type: "custom", + name: "lookup_order", + description: "Look up the status of a customer order by order ID", + }), + ], +}); +``` + +**Supported models:** all Responses API models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). diff --git a/docs/adapters/openrouter.md b/docs/adapters/openrouter.md index 07459cdb0..c61fcff96 100644 --- a/docs/adapters/openrouter.md +++ b/docs/adapters/openrouter.md @@ -137,5 +137,44 @@ const stream = chat({ ## Next Steps - [Getting Started](../getting-started/quick-start) - Learn the basics -- [Tools Guide](../tools/tools) - Learn about tools +- [Tools Guide](../tools/tools) - Learn about tools + +## Provider Tools + +> **Migrated from `createWebSearchTool`?** This factory was renamed to +> `webSearchTool` and moved to the `/tools` subpath in this release. +> See [Migration Guide §6](../migration/migration.md#6-provider-tools-moved-to-tools-subpath) +> for the exact before/after. + +OpenRouter's gateway exposes web search via a plugin that works across +any proxied chat model. Import it from `@tanstack/ai-openrouter/tools`. + +> For the full concept, a comparison matrix, and type-gating details, see +> [Provider Tools](../tools/provider-tools.md). + +### `webSearchTool` + +Adds web search capability to any OpenRouter-proxied chat model. Choose the +search `engine` (`native` or `exa`), cap results with `maxResults`, and +optionally provide a `searchPrompt` to guide query formation. + +```typescript +import { chat } from "@tanstack/ai"; +import { openRouterText } from "@tanstack/ai-openrouter"; +import { webSearchTool } from "@tanstack/ai-openrouter/tools"; + +const stream = chat({ + adapter: openRouterText("openai/gpt-5"), + messages: [{ role: "user", content: "What's new in AI this week?" }], + tools: [ + webSearchTool({ + engine: "exa", + maxResults: 5, + searchPrompt: "Recent AI news and research papers", + }), + ], +}); +``` + +**Supported models:** all OpenRouter chat models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). diff --git a/docs/config.json b/docs/config.json index 698d18545..ef3e6ccab 100644 --- a/docs/config.json +++ b/docs/config.json @@ -55,6 +55,10 @@ "label": "Tools", "to": "tools/tools" }, + { + "label": "Provider Tools", + "to": "tools/provider-tools" + }, { "label": "Tool Architecture", "to": "tools/tool-architecture" diff --git a/docs/migration/migration.md b/docs/migration/migration.md index 2c8db8391..72c077fe7 100644 --- a/docs/migration/migration.md +++ b/docs/migration/migration.md @@ -364,6 +364,56 @@ const result = await openai.embeddings.create({ - **Direct provider access** - You can use the provider SDK directly for embeddings - **Focused scope** - TanStack AI focuses on chat, tools, and agentic workflows +## 6. Provider Tools Moved to `/tools` Subpath + +Provider-specific tools (web search, code execution, computer use, etc.) are now +exported from a dedicated `/tools` subpath on every adapter package. This keeps +tool imports tree-shakeable and avoids name collisions between providers. + +The only breaking change is in `@tanstack/ai-openrouter`: +`createWebSearchTool` has been removed from the package root, renamed to +`webSearchTool`, and moved to `@tanstack/ai-openrouter/tools`. Every other +provider tool (Anthropic, OpenAI, Gemini) is newly exported — no existing +import breaks. + +### Before + +```typescript +import { createWebSearchTool } from '@tanstack/ai-openrouter' + +const tools = [ + createWebSearchTool({ engine: 'native', maxResults: 5 }), +] +``` + +### After + +```typescript +import { webSearchTool } from '@tanstack/ai-openrouter/tools' + +const tools = [ + webSearchTool({ engine: 'native', maxResults: 5 }), +] +``` + +### Key Changes + +- **Import path is now `/tools`** — matches the existing `/adapters` subpath + pattern used elsewhere in each provider package. +- **Factory renamed** — `createWebSearchTool` → `webSearchTool`. The `create*` + prefix has been dropped to align with every other provider + (`webSearchTool` in `@tanstack/ai-anthropic/tools`, + `@tanstack/ai-openai/tools`, etc.). +- **Runtime behavior is unchanged** — the factory accepts the same config + object and returns a tool that works identically in `chat({ tools: [...] })`. +- **Type-level gating is new** — if you pass a provider tool to a model that + doesn't support it (per the model's `supports.tools` array), you now get a + type error on the `tools` array. User-defined `toolDefinition()` tools are + unaffected. + +For the full list of available provider tools and which models support each +one, see [Provider Tools](../tools/provider-tools.md). + ## Complete Migration Example Here's a complete example showing all the changes together: diff --git a/docs/tools/provider-tools.md b/docs/tools/provider-tools.md new file mode 100644 index 000000000..865f266f8 --- /dev/null +++ b/docs/tools/provider-tools.md @@ -0,0 +1,93 @@ +--- +title: Provider Tools +id: provider-tools +order: 2 +--- + +Most providers expose native tools beyond user-defined function calls: web +search, code execution, computer use, hosted retrieval, and more. TanStack AI +exports each provider's native tools from a dedicated `/tools` subpath per +adapter package. + +You have an adapter already wired up. You want to give the model access to a +provider-native capability (e.g. Anthropic web search) and be sure you never +pair a tool with a model that doesn't support it. By the end of this page, +you'll have imported the factory, added it to `chat({ tools: [...] })`, and +understood the compile-time guard that will catch unsupported combinations. + +## Import + +Every adapter ships provider tools on a `/tools` subpath: + +```typescript +import { webSearchTool } from '@tanstack/ai-anthropic/tools' +import { codeInterpreterTool } from '@tanstack/ai-openai/tools' +import { googleSearchTool } from '@tanstack/ai-gemini/tools' +``` + +## Use in `chat({ tools })` + +```typescript +import { chat } from '@tanstack/ai' +import { anthropicText } from '@tanstack/ai-anthropic' +import { webSearchTool } from '@tanstack/ai-anthropic/tools' + +const stream = chat({ + adapter: anthropicText('claude-opus-4-6'), + messages: [{ role: 'user', content: "Summarize today's AI news." }], + tools: [ + webSearchTool({ + name: 'web_search', + type: 'web_search_20250305', + max_uses: 3, + }), + ], +}) +``` + +## Type-level guard + +Every provider-specific tool factory (e.g. `webSearchTool`, `computerUseTool`) +returns a `ProviderTool` brand. The adapter's +`toolCapabilities` (derived from each model's `supports.tools` list) gates +which brands are assignable to `tools`. + +Paste a `computerUseTool(...)` into a model that doesn't expose it, and +TypeScript reports an error on that array element — not on the factory call, +not at runtime. User-defined `toolDefinition()` tools stay unbranded and +always assignable. The `customTool` factories exported from `ai-anthropic` and +`ai-openai` also return a plain `Tool` (not a `ProviderTool` brand) and are +therefore universally accepted by any chat model, just like `toolDefinition()`. + +## Available tools + +| Provider | Tools | +|---|---| +| Anthropic | `webSearchTool`, `webFetchTool`, `codeExecutionTool`, `computerUseTool`, `bashTool`, `textEditorTool`, `memoryTool` — see [Anthropic adapter](../adapters/anthropic.md#provider-tools). | +| OpenAI | `webSearchTool`, `webSearchPreviewTool`, `fileSearchTool`, `imageGenerationTool`, `codeInterpreterTool`, `mcpTool`, `computerUseTool`, `localShellTool`, `shellTool`, `applyPatchTool` — see [OpenAI adapter](../adapters/openai.md#provider-tools). | +| Gemini | `codeExecutionTool`, `fileSearchTool`, `googleSearchTool`, `googleSearchRetrievalTool`, `googleMapsTool`, `urlContextTool`, `computerUseTool` — see [Gemini adapter](../adapters/gemini.md#provider-tools). | +| OpenRouter | `webSearchTool` — see [OpenRouter adapter](../adapters/openrouter.md#provider-tools). | +| Grok | function tools only (no provider-specific tools). | +| Groq | function tools only (no provider-specific tools). | + +## Which models support which tools? + +Each adapter's `supports.tools` array is the source of truth. The comparison +matrix is maintained alongside `model-meta.ts` and reflected here: + +- **Anthropic**: every current model except `claude-3-haiku` (web_search only) + and `claude-3-5-haiku` (web tools only). +- **OpenAI**: GPT-5 family and reasoning models (O-series) support the full + superset. GPT-4-series supports web/file/image/code/mcp but not + preview/shell variants. GPT-3.5 and audio-focused models: none. +- **Gemini**: 3.x Pro/Flash models support the full tool set. Lite and + image/video variants have narrower support. +- **OpenRouter**: every chat model supports `webSearchTool` via the gateway. + +For the exact per-model list, open the adapter page or read the model's +`supports.tools` array directly from `model-meta.ts`. + +## Migrating from earlier versions + +If you were using `createWebSearchTool` from `@tanstack/ai-openrouter`, see +[Migration Guide §6](../migration/migration.md#6-provider-tools-moved-to-tools-subpath). diff --git a/docs/tools/tools.md b/docs/tools/tools.md index cba08c5ad..3d4ffdd56 100644 --- a/docs/tools/tools.md +++ b/docs/tools/tools.md @@ -24,6 +24,9 @@ Tools enable your AI application to: - **Execute client-side operations** like updating UI or local storage - **Create hybrid tools** that execute in both server and client contexts +> Looking for provider-native tools like Anthropic web search, OpenAI code +> interpreter, or Gemini URL context? See [Provider Tools](./provider-tools.md). + ## Framework Support TanStack AI works with **any** JavaScript framework: diff --git a/packages/typescript/ai-anthropic/package.json b/packages/typescript/ai-anthropic/package.json index 400a328a6..31d65ea46 100644 --- a/packages/typescript/ai-anthropic/package.json +++ b/packages/typescript/ai-anthropic/package.json @@ -23,6 +23,10 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" + }, + "./tools": { + "types": "./dist/esm/tools/index.d.ts", + "import": "./dist/esm/tools/index.js" } }, "files": [ diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts index c88c1159f..a8510c167 100644 --- a/packages/typescript/ai-anthropic/src/adapters/text.ts +++ b/packages/typescript/ai-anthropic/src/adapters/text.ts @@ -9,6 +9,7 @@ import { import type { ANTHROPIC_MODELS, AnthropicChatModelProviderOptionsByName, + AnthropicChatModelToolCapabilitiesByName, AnthropicModelInputModalitiesByName, } from '../model-meta' import type { @@ -89,6 +90,11 @@ type ResolveInputModalities = ? AnthropicModelInputModalitiesByName[TModel] : readonly ['text', 'image', 'document'] +type ResolveToolCapabilities = + TModel extends keyof AnthropicChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + // =========================== // Adapter Implementation // =========================== @@ -101,14 +107,17 @@ type ResolveInputModalities = */ export class AnthropicTextAdapter< TModel extends (typeof ANTHROPIC_MODELS)[number], - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, TProviderOptions, TInputModalities, - AnthropicMessageMetadataByModality + AnthropicMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const readonly name = 'anthropic' as const @@ -121,7 +130,7 @@ export class AnthropicTextAdapter< } async *chatStream( - options: TextOptions, + options: TextOptions, ): AsyncIterable { try { const requestParams = this.mapCommonOptionsToAnthropic(options) @@ -160,7 +169,7 @@ export class AnthropicTextAdapter< * The outputSchema is already JSON Schema (converted in the ai layer). */ async structuredOutput( - options: StructuredOutputOptions, + options: StructuredOutputOptions, ): Promise> { const { chatOptions, outputSchema } = options diff --git a/packages/typescript/ai-anthropic/src/index.ts b/packages/typescript/ai-anthropic/src/index.ts index a5d7bb4a7..4100ec183 100644 --- a/packages/typescript/ai-anthropic/src/index.ts +++ b/packages/typescript/ai-anthropic/src/index.ts @@ -24,9 +24,10 @@ export { // ============================================================================ export type { + AnthropicChatModel, AnthropicChatModelProviderOptionsByName, + AnthropicChatModelToolCapabilitiesByName, AnthropicModelInputModalitiesByName, - AnthropicChatModel, } from './model-meta' export { ANTHROPIC_MODELS } from './model-meta' export type { diff --git a/packages/typescript/ai-anthropic/src/model-meta.ts b/packages/typescript/ai-anthropic/src/model-meta.ts index e0781c3d0..768be1c42 100644 --- a/packages/typescript/ai-anthropic/src/model-meta.ts +++ b/packages/typescript/ai-anthropic/src/model-meta.ts @@ -21,6 +21,15 @@ interface ModelMeta< extended_thinking?: boolean adaptive_thinking?: boolean priority_tier?: boolean + tools?: Array< + | 'web_search' + | 'web_fetch' + | 'code_execution' + | 'computer_use' + | 'bash' + | 'text_editor' + | 'memory' + > } context_window?: number max_output_tokens?: number @@ -66,6 +75,15 @@ const CLAUDE_OPUS_4_6 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -96,6 +114,15 @@ const CLAUDE_OPUS_4_5 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -127,6 +154,15 @@ const CLAUDE_SONNET_4_6 = { extended_thinking: true, adaptive_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -157,6 +193,15 @@ const CLAUDE_SONNET_4_5 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -187,6 +232,15 @@ const CLAUDE_HAIKU_4_5 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -217,6 +271,15 @@ const CLAUDE_OPUS_4_1 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -247,6 +310,15 @@ const CLAUDE_SONNET_4 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -276,6 +348,15 @@ const CLAUDE_SONNET_3_7 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -306,6 +387,15 @@ const CLAUDE_OPUS_4 = { input: ['text', 'image', 'document'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -336,6 +426,7 @@ const CLAUDE_HAIKU_3_5 = { input: ['text', 'image', 'document'], extended_thinking: false, priority_tier: true, + tools: ['web_search', 'web_fetch'], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -366,6 +457,7 @@ const CLAUDE_HAIKU_3 = { input: ['text', 'image', 'document'], extended_thinking: false, priority_tier: false, + tools: ['web_search'], }, } as const satisfies ModelMeta< AnthropicContainerOptions & @@ -432,6 +524,15 @@ const CLAUDE_OPUS_4_6_FAST = { input: ['text', 'image'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, pricing: { input: { @@ -462,6 +563,15 @@ const CLAUDE_OPUS_4_7 = { input: ['text', 'image'], extended_thinking: true, priority_tier: true, + tools: [ + 'web_search', + 'web_fetch', + 'code_execution', + 'computer_use', + 'bash', + 'text_editor', + 'memory', + ], }, pricing: { input: { @@ -619,6 +729,22 @@ export type AnthropicChatModelProviderOptionsByName = { AnthropicSamplingOptions } +export type AnthropicChatModelToolCapabilitiesByName = { + [CLAUDE_OPUS_4_6.id]: typeof CLAUDE_OPUS_4_6.supports.tools + [CLAUDE_OPUS_4_5.id]: typeof CLAUDE_OPUS_4_5.supports.tools + [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_SONNET_4.id]: typeof CLAUDE_SONNET_4.supports.tools + [CLAUDE_SONNET_3_7.id]: typeof CLAUDE_SONNET_3_7.supports.tools + [CLAUDE_OPUS_4.id]: typeof CLAUDE_OPUS_4.supports.tools + [CLAUDE_HAIKU_3_5.id]: typeof CLAUDE_HAIKU_3_5.supports.tools + [CLAUDE_HAIKU_3.id]: typeof CLAUDE_HAIKU_3.supports.tools + [CLAUDE_OPUS_4_6_FAST.id]: typeof CLAUDE_OPUS_4_6_FAST.supports.tools + [CLAUDE_OPUS_4_7.id]: typeof CLAUDE_OPUS_4_7.supports.tools +} + /** * Type-only map from chat model name to its supported input modalities. * All Anthropic Claude models support text, image, and document (PDF) input. diff --git a/packages/typescript/ai-anthropic/src/tools/bash-tool.ts b/packages/typescript/ai-anthropic/src/tools/bash-tool.ts index b1b6abeab..1dee32253 100644 --- a/packages/typescript/ai-anthropic/src/tools/bash-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/bash-tool.ts @@ -2,18 +2,25 @@ import type { BetaToolBash20241022, BetaToolBash20250124, } from '@anthropic-ai/sdk/resources/beta' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type BashTool = BetaToolBash20241022 | BetaToolBash20250124 +export type BashToolConfig = BetaToolBash20241022 | BetaToolBash20250124 -export function convertBashToolToAdapterFormat(tool: Tool): BashTool { - const metadata = tool.metadata as BashTool +/** @deprecated Renamed to `BashToolConfig`. Will be removed in a future release. */ +export type BashTool = BashToolConfig + +export type AnthropicBashTool = ProviderTool<'anthropic', 'bash'> + +export function convertBashToolToAdapterFormat(tool: Tool): BashToolConfig { + const metadata = tool.metadata as BashToolConfig return metadata } -export function bashTool(config: BashTool): Tool { + +export function bashTool(config: BashToolConfig): AnthropicBashTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'bash', description: '', metadata: config, - } + } as unknown as AnthropicBashTool } diff --git a/packages/typescript/ai-anthropic/src/tools/code-execution-tool.ts b/packages/typescript/ai-anthropic/src/tools/code-execution-tool.ts index 7fdb6f84f..17241d60e 100644 --- a/packages/typescript/ai-anthropic/src/tools/code-execution-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/code-execution-tool.ts @@ -2,23 +2,34 @@ import type { BetaCodeExecutionTool20250522, BetaCodeExecutionTool20250825, } from '@anthropic-ai/sdk/resources/beta' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type CodeExecutionTool = +export type CodeExecutionToolConfig = | BetaCodeExecutionTool20250522 | BetaCodeExecutionTool20250825 +/** @deprecated Renamed to `CodeExecutionToolConfig`. Will be removed in a future release. */ +export type CodeExecutionTool = CodeExecutionToolConfig + +export type AnthropicCodeExecutionTool = ProviderTool< + 'anthropic', + 'code_execution' +> + export function convertCodeExecutionToolToAdapterFormat( tool: Tool, -): CodeExecutionTool { - const metadata = tool.metadata as CodeExecutionTool +): CodeExecutionToolConfig { + const metadata = tool.metadata as CodeExecutionToolConfig return metadata } -export function codeExecutionTool(config: CodeExecutionTool): Tool { +export function codeExecutionTool( + config: CodeExecutionToolConfig, +): AnthropicCodeExecutionTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'code_execution', description: '', metadata: config, - } + } as unknown as AnthropicCodeExecutionTool } diff --git a/packages/typescript/ai-anthropic/src/tools/computer-use-tool.ts b/packages/typescript/ai-anthropic/src/tools/computer-use-tool.ts index 18c2b7e37..0788f36ad 100644 --- a/packages/typescript/ai-anthropic/src/tools/computer-use-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/computer-use-tool.ts @@ -2,23 +2,31 @@ import type { BetaToolComputerUse20241022, BetaToolComputerUse20250124, } from '@anthropic-ai/sdk/resources/beta' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type ComputerUseTool = +export type ComputerUseToolConfig = | BetaToolComputerUse20241022 | BetaToolComputerUse20250124 +/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */ +export type ComputerUseTool = ComputerUseToolConfig + +export type AnthropicComputerUseTool = ProviderTool<'anthropic', 'computer_use'> + export function convertComputerUseToolToAdapterFormat( tool: Tool, -): ComputerUseTool { - const metadata = tool.metadata as ComputerUseTool +): ComputerUseToolConfig { + const metadata = tool.metadata as ComputerUseToolConfig return metadata } -export function computerUseTool(config: ComputerUseTool): Tool { +export function computerUseTool( + config: ComputerUseToolConfig, +): AnthropicComputerUseTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'computer', description: '', metadata: config, - } + } as unknown as AnthropicComputerUseTool } diff --git a/packages/typescript/ai-anthropic/src/tools/custom-tool.ts b/packages/typescript/ai-anthropic/src/tools/custom-tool.ts index 3b36a9ba5..582b7cc03 100644 --- a/packages/typescript/ai-anthropic/src/tools/custom-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/custom-tool.ts @@ -1,7 +1,7 @@ import type { JSONSchema, SchemaInput, Tool } from '@tanstack/ai' import type { CacheControl } from '../text/text-provider-options' -export interface CustomTool { +export interface CustomToolConfig { /** * The name of the tool. */ @@ -23,11 +23,13 @@ export interface CustomTool { cache_control?: CacheControl | null } -export function convertCustomToolToAdapterFormat(tool: Tool): CustomTool { +/** @deprecated Renamed to `CustomToolConfig`. Will be removed in a future release. */ +export type CustomTool = CustomToolConfig + +export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { const metadata = (tool.metadata as { cacheControl?: CacheControl | null } | undefined) || {} - // Tool schemas are already converted to JSON Schema in the ai layer const jsonSchema = (tool.inputSchema ?? { type: 'object', properties: {}, diff --git a/packages/typescript/ai-anthropic/src/tools/index.ts b/packages/typescript/ai-anthropic/src/tools/index.ts index 5012b2b5f..7e3beeede 100644 --- a/packages/typescript/ai-anthropic/src/tools/index.ts +++ b/packages/typescript/ai-anthropic/src/tools/index.ts @@ -7,6 +7,55 @@ import type { TextEditorTool } from './text-editor-tool' import type { WebFetchTool } from './web-fetch-tool' import type { WebSearchTool } from './web-search-tool' +export { + bashTool, + type AnthropicBashTool, + type BashToolConfig, + type BashTool, +} from './bash-tool' +export { + codeExecutionTool, + type AnthropicCodeExecutionTool, + type CodeExecutionToolConfig, + type CodeExecutionTool, +} from './code-execution-tool' +export { + computerUseTool, + type AnthropicComputerUseTool, + type ComputerUseToolConfig, + type ComputerUseTool, +} from './computer-use-tool' +export { + customTool, + type CustomToolConfig, + type CustomTool, +} from './custom-tool' +export { + memoryTool, + type AnthropicMemoryTool, + type MemoryToolConfig, + type MemoryTool, +} from './memory-tool' +export { + textEditorTool, + type AnthropicTextEditorTool, + type TextEditorToolConfig, + type TextEditorTool, +} from './text-editor-tool' +export { + webFetchTool, + type AnthropicWebFetchTool, + type WebFetchToolConfig, + type WebFetchTool, +} from './web-fetch-tool' +export { + webSearchTool, + type AnthropicWebSearchTool, + type WebSearchToolConfig, + type WebSearchTool, +} from './web-search-tool' + +// Keep the discriminated union defined inline (no barrel exports). export type AnthropicTool = | BashTool | CodeExecutionTool @@ -16,15 +65,3 @@ export type AnthropicTool = | TextEditorTool | WebFetchTool | WebSearchTool - -// Export individual tool types -export type { - // BashTool, - // CodeExecutionTool, - // ComputerUseTool, - CustomTool, - // MemoryTool, - // TextEditorTool, - // WebFetchTool, - // WebSearchTool, -} diff --git a/packages/typescript/ai-anthropic/src/tools/memory-tool.ts b/packages/typescript/ai-anthropic/src/tools/memory-tool.ts index f4fca3079..2eddf9257 100644 --- a/packages/typescript/ai-anthropic/src/tools/memory-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/memory-tool.ts @@ -1,20 +1,26 @@ import type { BetaMemoryTool20250818 } from '@anthropic-ai/sdk/resources/beta' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type MemoryTool = BetaMemoryTool20250818 +export type MemoryToolConfig = BetaMemoryTool20250818 -export function convertMemoryToolToAdapterFormat(tool: Tool): MemoryTool { - const metadata = tool.metadata as Omit +/** @deprecated Renamed to `MemoryToolConfig`. Will be removed in a future release. */ +export type MemoryTool = MemoryToolConfig + +export type AnthropicMemoryTool = ProviderTool<'anthropic', 'memory'> + +export function convertMemoryToolToAdapterFormat(tool: Tool): MemoryToolConfig { + const metadata = tool.metadata as Omit return { type: 'memory_20250818', ...metadata, } } -export function memoryTool(config?: MemoryTool): Tool { +export function memoryTool(config?: MemoryToolConfig): AnthropicMemoryTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'memory', description: '', metadata: config, - } + } as unknown as AnthropicMemoryTool } diff --git a/packages/typescript/ai-anthropic/src/tools/text-editor-tool.ts b/packages/typescript/ai-anthropic/src/tools/text-editor-tool.ts index 5ac361224..714888dd3 100644 --- a/packages/typescript/ai-anthropic/src/tools/text-editor-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/text-editor-tool.ts @@ -3,26 +3,34 @@ import type { ToolTextEditor20250429, ToolTextEditor20250728, } from '@anthropic-ai/sdk/resources/messages' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type TextEditorTool = +export type TextEditorToolConfig = | ToolTextEditor20250124 | ToolTextEditor20250429 | ToolTextEditor20250728 +/** @deprecated Renamed to `TextEditorToolConfig`. Will be removed in a future release. */ +export type TextEditorTool = TextEditorToolConfig + +export type AnthropicTextEditorTool = ProviderTool<'anthropic', 'text_editor'> + export function convertTextEditorToolToAdapterFormat( tool: Tool, -): TextEditorTool { - const metadata = tool.metadata as TextEditorTool +): TextEditorToolConfig { + const metadata = tool.metadata as TextEditorToolConfig return { ...metadata, } } -export function textEditorTool(config: T): Tool { +export function textEditorTool( + config: T, +): AnthropicTextEditorTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'str_replace_editor', description: '', metadata: config, - } + } as unknown as AnthropicTextEditorTool } diff --git a/packages/typescript/ai-anthropic/src/tools/web-fetch-tool.ts b/packages/typescript/ai-anthropic/src/tools/web-fetch-tool.ts index e0a03fcc1..81bb523a7 100644 --- a/packages/typescript/ai-anthropic/src/tools/web-fetch-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/web-fetch-tool.ts @@ -1,10 +1,17 @@ import type { BetaWebFetchTool20250910 } from '@anthropic-ai/sdk/resources/beta' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type WebFetchTool = BetaWebFetchTool20250910 +export type WebFetchToolConfig = BetaWebFetchTool20250910 -export function convertWebFetchToolToAdapterFormat(tool: Tool): WebFetchTool { - const metadata = tool.metadata as Omit +/** @deprecated Renamed to `WebFetchToolConfig`. Will be removed in a future release. */ +export type WebFetchTool = WebFetchToolConfig + +export type AnthropicWebFetchTool = ProviderTool<'anthropic', 'web_fetch'> + +export function convertWebFetchToolToAdapterFormat( + tool: Tool, +): WebFetchToolConfig { + const metadata = tool.metadata as Omit return { name: 'web_fetch', type: 'web_fetch_20250910', @@ -13,11 +20,12 @@ export function convertWebFetchToolToAdapterFormat(tool: Tool): WebFetchTool { } export function webFetchTool( - config?: Omit, -): Tool { + config?: Omit, +): AnthropicWebFetchTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'web_fetch', description: '', metadata: config, - } + } as unknown as AnthropicWebFetchTool } diff --git a/packages/typescript/ai-anthropic/src/tools/web-search-tool.ts b/packages/typescript/ai-anthropic/src/tools/web-search-tool.ts index 27f306957..cad276b3d 100644 --- a/packages/typescript/ai-anthropic/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/web-search-tool.ts @@ -1,10 +1,15 @@ import type { WebSearchTool20250305 } from '@anthropic-ai/sdk/resources/messages' import type { CacheControl } from '../text/text-provider-options' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type WebSearchTool = WebSearchTool20250305 +export type WebSearchToolConfig = WebSearchTool20250305 -const validateDomains = (tool: WebSearchTool) => { +/** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */ +export type WebSearchTool = WebSearchToolConfig + +export type AnthropicWebSearchTool = ProviderTool<'anthropic', 'web_search'> + +const validateDomains = (tool: WebSearchToolConfig) => { if (tool.allowed_domains && tool.blocked_domains) { throw new Error( 'allowed_domains and blocked_domains cannot be used together.', @@ -12,7 +17,7 @@ const validateDomains = (tool: WebSearchTool) => { } } -const validateUserLocation = (tool: WebSearchTool) => { +const validateUserLocation = (tool: WebSearchToolConfig) => { const userLocation = tool.user_location if (userLocation) { if ( @@ -45,7 +50,9 @@ const validateUserLocation = (tool: WebSearchTool) => { } } -export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchTool { +export function convertWebSearchToolToAdapterFormat( + tool: Tool, +): WebSearchToolConfig { const metadata = tool.metadata as { allowedDomains?: Array | null blockedDomains?: Array | null @@ -70,12 +77,15 @@ export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchTool { } } -export function webSearchTool(config: WebSearchTool): Tool { +export function webSearchTool( + config: WebSearchToolConfig, +): AnthropicWebSearchTool { validateDomains(config) validateUserLocation(config) + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'web_search', description: '', metadata: config, - } + } as unknown as AnthropicWebSearchTool } diff --git a/packages/typescript/ai-anthropic/tests/provider-tools-smoke.test.ts b/packages/typescript/ai-anthropic/tests/provider-tools-smoke.test.ts new file mode 100644 index 000000000..89c93f389 --- /dev/null +++ b/packages/typescript/ai-anthropic/tests/provider-tools-smoke.test.ts @@ -0,0 +1,118 @@ +/** + * Runtime smoke test for provider tool factories. + * + * Verifies: + * 1. Factories are importable from the package-internal tools path + * (mirroring the public `/tools` subpath consumers will use). + * 2. Each factory produces a runtime shape with `name`, `description`, `metadata`. + * 3. `convertToolsToProviderFormat` transforms those outputs into the SDK shape. + */ +import { describe, it, expect } from 'vitest' +import { + bashTool, + codeExecutionTool, + computerUseTool, + memoryTool, + textEditorTool, + webFetchTool, + webSearchTool, +} from '../src/tools' +import { convertToolsToProviderFormat } from '../src/tools/tool-converter' +import type { Tool } from '@tanstack/ai' + +describe('Anthropic provider tool factories — runtime shape', () => { + it('webSearchTool produces a Tool-shaped object', () => { + const tool = webSearchTool({ + name: 'web_search', + type: 'web_search_20250305', + }) + expect(tool.name).toBe('web_search') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) + + it('codeExecutionTool produces a Tool-shaped object', () => { + const tool = codeExecutionTool({ + name: 'code_execution', + type: 'code_execution_20250825', + }) + expect(tool.name).toBe('code_execution') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) + + it('computerUseTool produces a Tool-shaped object', () => { + const tool = computerUseTool({ + type: 'computer_20250124', + name: 'computer', + display_width_px: 1024, + display_height_px: 768, + }) + expect(tool.name).toBe('computer') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) + + it('bashTool produces a Tool-shaped object', () => { + const tool = bashTool({ name: 'bash', type: 'bash_20250124' }) + expect(tool.name).toBe('bash') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) + + it('textEditorTool produces a Tool-shaped object', () => { + const tool = textEditorTool({ + type: 'text_editor_20250124', + name: 'str_replace_editor', + }) + expect(tool.name).toBe('str_replace_editor') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) + + it('webFetchTool produces a Tool-shaped object', () => { + const tool = webFetchTool() + expect(tool.name).toBe('web_fetch') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) + + it('memoryTool produces a Tool-shaped object', () => { + const tool = memoryTool() + expect(tool.name).toBe('memory') + expect(tool).toHaveProperty('description') + expect(tool).toHaveProperty('metadata') + }) +}) + +describe('convertToolsToProviderFormat — end-to-end shape', () => { + it('converts webSearchTool output to the SDK web_search shape', () => { + const [converted] = convertToolsToProviderFormat([ + webSearchTool({ + name: 'web_search', + type: 'web_search_20250305', + max_uses: 2, + }) as unknown as Tool, + ]) + expect(converted).toMatchObject({ + name: 'web_search', + type: 'web_search_20250305', + }) + }) + + it('converts multiple provider tools in one call', () => { + const converted = convertToolsToProviderFormat([ + webSearchTool({ name: 'web_search', type: 'web_search_20250305' }), + codeExecutionTool({ + name: 'code_execution', + type: 'code_execution_20250825', + }), + bashTool({ name: 'bash', type: 'bash_20250124' }), + ] as unknown as Tool[]) + expect(converted).toHaveLength(3) + const names = converted.map((t) => ('name' in t ? t.name : undefined)) + expect(names).toContain('web_search') + expect(names).toContain('code_execution') + expect(names).toContain('bash') + }) +}) diff --git a/packages/typescript/ai-anthropic/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai-anthropic/tests/tools-per-model-type-safety.test.ts new file mode 100644 index 000000000..c7e01f3af --- /dev/null +++ b/packages/typescript/ai-anthropic/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,152 @@ +/** + * Per-model type-safety tests for Anthropic provider tools. + * + * Positive cases: each supported (model, tool) pair compiles cleanly. + * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. + */ +import { beforeAll, describe, it } from 'vitest' +import { z } from 'zod' +import { toolDefinition } from '@tanstack/ai' +import { anthropicText } from '../src' +import { + bashTool, + codeExecutionTool, + computerUseTool, + customTool, + memoryTool, + textEditorTool, + webFetchTool, + webSearchTool, +} from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], +) { + return { adapter, tools } +} + +// Set a dummy API key so adapter construction does not throw at runtime. +// These tests only exercise compile-time type gating; no network calls are made. +beforeAll(() => { + process.env['ANTHROPIC_API_KEY'] = 'sk-test-dummy' +}) + +// Minimal user tool — always assignable regardless of model. +const userTool = toolDefinition({ + name: 'echo', + description: 'echoes input', + inputSchema: z.object({ msg: z.string() }), +}).server(async ({ msg }) => msg) + +describe('Anthropic per-model tool gating', () => { + it('claude-opus-4-6 accepts the full tool superset', () => { + const adapter = anthropicText('claude-opus-4-6') + typedTools(adapter, [ + userTool, + webSearchTool({ name: 'web_search', type: 'web_search_20250305' }), + webFetchTool(), + codeExecutionTool({ + name: 'code_execution', + type: 'code_execution_20250825', + }), + computerUseTool({ + type: 'computer_20250124', + name: 'computer', + display_width_px: 1024, + display_height_px: 768, + }), + bashTool({ name: 'bash', type: 'bash_20250124' }), + textEditorTool({ + type: 'text_editor_20250124', + name: 'str_replace_editor', + }), + memoryTool(), + ]) + }) + + it('claude-3-haiku rejects every tool except web_search', () => { + const adapter = anthropicText('claude-3-haiku') + typedTools(adapter, [ + userTool, + webSearchTool({ name: 'web_search', type: 'web_search_20250305' }), + // @ts-expect-error - claude-3-haiku does not support web_fetch + webFetchTool(), + // @ts-expect-error - claude-3-haiku does not support code_execution + codeExecutionTool({ + name: 'code_execution', + type: 'code_execution_20250825', + }), + // @ts-expect-error - claude-3-haiku does not support computer_use + computerUseTool({ + type: 'computer_20250124', + name: 'computer', + display_width_px: 1024, + display_height_px: 768, + }), + // @ts-expect-error - claude-3-haiku does not support bash + bashTool({ name: 'bash', type: 'bash_20250124' }), + // @ts-expect-error - claude-3-haiku does not support text_editor + textEditorTool({ + type: 'text_editor_20250124', + name: 'str_replace_editor', + }), + // @ts-expect-error - claude-3-haiku does not support memory + memoryTool(), + ]) + }) + + it('customTool is accepted on any model (returns plain Tool, not a branded ProviderTool)', () => { + // Full-featured model + const fullAdapter = anthropicText('claude-opus-4-6') + typedTools(fullAdapter, [ + customTool( + 'lookup_user', + 'Look up a user by ID', + z.object({ userId: z.number() }), + ), + ]) + + // Restricted model — customTool must still compile without @ts-expect-error + const restrictedAdapter = anthropicText('claude-3-haiku') + typedTools(restrictedAdapter, [ + customTool( + 'lookup_user', + 'Look up a user by ID', + z.object({ userId: z.number() }), + ), + ]) + }) + + it('claude-3-5-haiku accepts only web tools', () => { + const adapter = anthropicText('claude-3-5-haiku') + typedTools(adapter, [ + userTool, + webSearchTool({ name: 'web_search', type: 'web_search_20250305' }), + webFetchTool(), + // @ts-expect-error - claude-3-5-haiku does not support code_execution + codeExecutionTool({ + name: 'code_execution', + type: 'code_execution_20250825', + }), + // @ts-expect-error - claude-3-5-haiku does not support computer_use + computerUseTool({ + type: 'computer_20250124', + name: 'computer', + display_width_px: 1024, + display_height_px: 768, + }), + // @ts-expect-error - claude-3-5-haiku does not support bash + bashTool({ name: 'bash', type: 'bash_20250124' }), + // @ts-expect-error - claude-3-5-haiku does not support text_editor + textEditorTool({ + type: 'text_editor_20250124', + name: 'str_replace_editor', + }), + // @ts-expect-error - claude-3-5-haiku does not support memory + memoryTool(), + ]) + }) +}) diff --git a/packages/typescript/ai-anthropic/tsconfig.json b/packages/typescript/ai-anthropic/tsconfig.json index e5e872741..0c50acadb 100644 --- a/packages/typescript/ai-anthropic/tsconfig.json +++ b/packages/typescript/ai-anthropic/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["vite.config.ts", "./src"], + "include": [ + "vite.config.ts", + "./src", + "tests/tools-per-model-type-safety.test.ts" + ], "exclude": ["node_modules", "dist", "**/*.config.ts"] } diff --git a/packages/typescript/ai-anthropic/vite.config.ts b/packages/typescript/ai-anthropic/vite.config.ts index 11f5b20b7..8e2299108 100644 --- a/packages/typescript/ai-anthropic/vite.config.ts +++ b/packages/typescript/ai-anthropic/vite.config.ts @@ -30,7 +30,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/tools/index.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/ai-gemini/package.json b/packages/typescript/ai-gemini/package.json index 7923ad8f4..df780448a 100644 --- a/packages/typescript/ai-gemini/package.json +++ b/packages/typescript/ai-gemini/package.json @@ -16,6 +16,10 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" + }, + "./tools": { + "types": "./dist/esm/tools/index.d.ts", + "import": "./dist/esm/tools/index.js" } }, "files": [ @@ -48,6 +52,7 @@ "devDependencies": { "@tanstack/ai": "workspace:*", "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.7" + "vite": "^7.2.7", + "zod": "^4.2.0" } } diff --git a/packages/typescript/ai-gemini/src/adapters/text.ts b/packages/typescript/ai-gemini/src/adapters/text.ts index 0ed39cebf..6b265122f 100644 --- a/packages/typescript/ai-gemini/src/adapters/text.ts +++ b/packages/typescript/ai-gemini/src/adapters/text.ts @@ -9,6 +9,7 @@ import { import type { GEMINI_MODELS, GeminiChatModelProviderOptionsByName, + GeminiChatModelToolCapabilitiesByName, GeminiModelInputModalitiesByName, } from '../model-meta' import type { @@ -71,6 +72,15 @@ type ResolveInputModalities = ? GeminiModelInputModalitiesByName[TModel] : readonly ['text', 'image', 'audio', 'video', 'document'] +/** + * Resolve tool capabilities for a specific model. + * If the model has explicit tools in the map, use those; otherwise use empty tuple. + */ +type ResolveToolCapabilities = + TModel extends keyof GeminiChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + // =========================== // Adapter Implementation // =========================== @@ -83,14 +93,17 @@ type ResolveInputModalities = */ export class GeminiTextAdapter< TModel extends (typeof GEMINI_MODELS)[number], - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, TProviderOptions, TInputModalities, - GeminiMessageMetadataByModality + GeminiMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const readonly name = 'gemini' as const @@ -810,7 +823,8 @@ export function createGeminiChat( ): GeminiTextAdapter< TModel, ResolveProviderOptions, - ResolveInputModalities + ResolveInputModalities, + ResolveToolCapabilities > { return new GeminiTextAdapter({ apiKey, ...config }, model) } @@ -825,7 +839,8 @@ export function geminiText( ): GeminiTextAdapter< TModel, ResolveProviderOptions, - ResolveInputModalities + ResolveInputModalities, + ResolveToolCapabilities > { const apiKey = getGeminiApiKeyFromEnv() return createGeminiChat(model, apiKey, config) diff --git a/packages/typescript/ai-gemini/src/index.ts b/packages/typescript/ai-gemini/src/index.ts index 05c46547a..b7ea427b5 100644 --- a/packages/typescript/ai-gemini/src/index.ts +++ b/packages/typescript/ai-gemini/src/index.ts @@ -67,6 +67,7 @@ export type { GeminiTTSVoice } from './model-meta' export type { GeminiChatModelProviderOptionsByName, + GeminiChatModelToolCapabilitiesByName, GeminiModelInputModalitiesByName, } from './model-meta' export type { diff --git a/packages/typescript/ai-gemini/src/model-meta.ts b/packages/typescript/ai-gemini/src/model-meta.ts index 5b4c3b92c..219c274fc 100644 --- a/packages/typescript/ai-gemini/src/model-meta.ts +++ b/packages/typescript/ai-gemini/src/model-meta.ts @@ -17,16 +17,19 @@ interface ModelMeta { | 'audio_generation' | 'batch_api' | 'caching' - | 'code_execution' - | 'file_search' | 'function_calling' - | 'grounding_with_gmaps' - | 'image_generation' | 'live_api' - | 'search_grounding' | 'structured_output' | 'thinking' + > + tools?: Array< + | 'code_execution' + | 'file_search' + | 'google_search' + | 'google_search_retrieval' + | 'google_maps' | 'url_context' + | 'computer_use' > } max_input_tokens?: number @@ -58,14 +61,11 @@ const GEMINI_3_1_PRO = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -96,14 +96,11 @@ const GEMINI_3_PRO = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -134,14 +131,11 @@ const GEMINI_3_FLASH = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -169,13 +163,8 @@ const GEMINI_3_PRO_IMAGE = { supports: { input: ['text', 'image'], output: ['text', 'image'], - capabilities: [ - 'batch_api', - 'image_generation', - 'search_grounding', - 'structured_output', - 'thinking', - ], + capabilities: ['batch_api', 'structured_output', 'thinking'], + tools: ['google_search'], }, pricing: { input: { @@ -203,13 +192,8 @@ const GEMINI_3_1_FLASH_IMAGE = { supports: { input: ['text', 'image'], output: ['text', 'image'], - capabilities: [ - 'batch_api', - 'image_generation', - 'search_grounding', - 'structured_output', - 'thinking', - ], + capabilities: ['batch_api', 'structured_output', 'thinking'], + tools: ['google_search'], }, pricing: { input: { @@ -239,14 +223,11 @@ const GEMINI_3_1_FLASH_LITE = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -276,13 +257,15 @@ const GEMINI_2_5_PRO = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'grounding_with_gmaps', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_maps', + 'google_search', 'url_context', ], }, @@ -311,7 +294,8 @@ const GEMINI_2_5_PRO_TTS = { supports: { input: ['text'], output: ['audio'], - capabilities: ['audio_generation', 'file_search'], + capabilities: ['audio_generation'], + tools: ['file_search'], }, pricing: { input: { @@ -339,13 +323,15 @@ const GEMINI_2_5_FLASH = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'grounding_with_gmaps', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_maps', + 'google_search', 'url_context', ], }, @@ -377,14 +363,11 @@ const GEMINI_2_5_FLASH_PREVIEW = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -411,13 +394,8 @@ const GEMINI_2_5_FLASH_IMAGE = { supports: { input: ['text', 'image'], output: ['text', 'image'], - capabilities: [ - 'batch_api', - 'caching', - 'file_search', - 'image_generation', - 'structured_output', - ], + capabilities: ['batch_api', 'caching', 'structured_output'], + tools: ['file_search'], }, pricing: { input: { @@ -476,7 +454,8 @@ const GEMINI_2_5_FLASH_TTS = { supports: { input: ['text'], output: ['audio'], - capabilities: ['audio_generation', 'batch_api', 'file_search'], + capabilities: ['audio_generation', 'batch_api'], + tools: ['file_search'], }, pricing: { input: { @@ -504,14 +483,11 @@ const GEMINI_2_5_FLASH_LITE = { capabilities: [ 'batch_api', 'caching', - 'code_execution', 'function_calling', - 'grounding_with_gmaps', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'google_maps', 'google_search', 'url_context'], }, pricing: { input: { @@ -541,13 +517,11 @@ const GEMINI_2_5_FLASH_LITE_PREVIEW = { capabilities: [ 'batch_api', 'caching', - 'code_execution', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', - 'url_context', ], + tools: ['code_execution', 'google_search', 'url_context'], }, pricing: { input: { @@ -577,13 +551,11 @@ const GEMINI_2_FLASH = { capabilities: [ 'batch_api', 'caching', - 'code_execution', 'function_calling', - 'grounding_with_gmaps', 'live_api', - 'search_grounding', 'structured_output', ], + tools: ['code_execution', 'google_maps', 'google_search'], }, pricing: { input: { @@ -609,12 +581,8 @@ const GEMINI_2_FLASH_IMAGE = { supports: { input: ['text', 'image', 'audio', 'video'], output: ['text'], - capabilities: [ - 'batch_api', - 'caching', - 'image_generation', - 'structured_output', - ], + capabilities: ['batch_api', 'caching', 'structured_output'], + tools: [], }, pricing: { input: { @@ -679,6 +647,7 @@ const GEMINI_2_FLASH_LITE = { 'function_calling', 'structured_output', ], + tools: [], }, pricing: { input: { @@ -1092,6 +1061,24 @@ export type GeminiChatModelProviderOptionsByName = { GeminiStructuredOutputOptions } +/** + * Type-only map from chat model name to its supported tool capabilities. + * Based on the 'supports.tools' arrays defined for each model. + */ +export type GeminiChatModelToolCapabilitiesByName = { + [GEMINI_3_1_PRO.name]: typeof GEMINI_3_1_PRO.supports.tools + [GEMINI_3_PRO.name]: typeof GEMINI_3_PRO.supports.tools + [GEMINI_3_FLASH.name]: typeof GEMINI_3_FLASH.supports.tools + [GEMINI_3_1_FLASH_LITE.name]: typeof GEMINI_3_1_FLASH_LITE.supports.tools + [GEMINI_2_5_PRO.name]: typeof GEMINI_2_5_PRO.supports.tools + [GEMINI_2_5_FLASH.name]: typeof GEMINI_2_5_FLASH.supports.tools + [GEMINI_2_5_FLASH_PREVIEW.name]: typeof GEMINI_2_5_FLASH_PREVIEW.supports.tools + [GEMINI_2_5_FLASH_LITE.name]: typeof GEMINI_2_5_FLASH_LITE.supports.tools + [GEMINI_2_5_FLASH_LITE_PREVIEW.name]: typeof GEMINI_2_5_FLASH_LITE_PREVIEW.supports.tools + [GEMINI_2_FLASH.name]: typeof GEMINI_2_FLASH.supports.tools + [GEMINI_2_FLASH_LITE.name]: typeof GEMINI_2_FLASH_LITE.supports.tools +} + /** * Type-only map from chat model name to its supported input modalities. * Based on the 'supports.input' arrays defined for each model. diff --git a/packages/typescript/ai-gemini/src/tools/code-execution-tool.ts b/packages/typescript/ai-gemini/src/tools/code-execution-tool.ts index 04a2ebba8..a29645f3c 100644 --- a/packages/typescript/ai-gemini/src/tools/code-execution-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/code-execution-tool.ts @@ -1,6 +1,11 @@ -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export interface CodeExecutionTool {} +export interface CodeExecutionToolConfig {} + +/** @deprecated Renamed to `CodeExecutionToolConfig`. Will be removed in a future release. */ +export type CodeExecutionTool = CodeExecutionToolConfig + +export type GeminiCodeExecutionTool = ProviderTool<'gemini', 'code_execution'> export function convertCodeExecutionToolToAdapterFormat(_tool: Tool) { return { @@ -8,10 +13,11 @@ export function convertCodeExecutionToolToAdapterFormat(_tool: Tool) { } } -export function codeExecutionTool(): Tool { +export function codeExecutionTool(): GeminiCodeExecutionTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'code_execution', description: '', metadata: {}, - } + } as unknown as GeminiCodeExecutionTool } diff --git a/packages/typescript/ai-gemini/src/tools/computer-use-tool.ts b/packages/typescript/ai-gemini/src/tools/computer-use-tool.ts index b4a49f0e7..7a908d499 100644 --- a/packages/typescript/ai-gemini/src/tools/computer-use-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/computer-use-tool.ts @@ -1,10 +1,15 @@ import type { ComputerUse } from '@google/genai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type ComputerUseTool = ComputerUse +export type ComputerUseToolConfig = ComputerUse + +/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */ +export type ComputerUseTool = ComputerUseToolConfig + +export type GeminiComputerUseTool = ProviderTool<'gemini', 'computer_use'> export function convertComputerUseToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as ComputerUseTool + const metadata = tool.metadata as ComputerUseToolConfig return { computerUse: { environment: metadata.environment, @@ -13,7 +18,10 @@ export function convertComputerUseToolToAdapterFormat(tool: Tool) { } } -export function computerUseTool(config: ComputerUseTool): Tool { +export function computerUseTool( + config: ComputerUseToolConfig, +): GeminiComputerUseTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'computer_use', description: '', @@ -21,5 +29,5 @@ export function computerUseTool(config: ComputerUseTool): Tool { environment: config.environment, excludedPredefinedFunctions: config.excludedPredefinedFunctions, }, - } + } as unknown as GeminiComputerUseTool } diff --git a/packages/typescript/ai-gemini/src/tools/file-search-tool.ts b/packages/typescript/ai-gemini/src/tools/file-search-tool.ts index 87b9286b2..a7a21a624 100644 --- a/packages/typescript/ai-gemini/src/tools/file-search-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/file-search-tool.ts @@ -1,19 +1,27 @@ -import type { Tool } from '@tanstack/ai' import type { FileSearch } from '@google/genai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type FileSearchTool = FileSearch +export type FileSearchToolConfig = FileSearch + +/** @deprecated Renamed to `FileSearchToolConfig`. Will be removed in a future release. */ +export type FileSearchTool = FileSearchToolConfig + +export type GeminiFileSearchTool = ProviderTool<'gemini', 'file_search'> export function convertFileSearchToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as FileSearchTool + const metadata = tool.metadata as FileSearchToolConfig return { fileSearch: metadata, } } -export function fileSearchTool(config: FileSearchTool): Tool { +export function fileSearchTool( + config: FileSearchToolConfig, +): GeminiFileSearchTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'file_search', description: '', metadata: config, - } + } as unknown as GeminiFileSearchTool } diff --git a/packages/typescript/ai-gemini/src/tools/google-maps-tool.ts b/packages/typescript/ai-gemini/src/tools/google-maps-tool.ts index 42e3c27cb..b94bb4f4a 100644 --- a/packages/typescript/ai-gemini/src/tools/google-maps-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/google-maps-tool.ts @@ -1,19 +1,27 @@ import type { GoogleMaps } from '@google/genai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type GoogleMapsTool = GoogleMaps +export type GoogleMapsToolConfig = GoogleMaps + +/** @deprecated Renamed to `GoogleMapsToolConfig`. Will be removed in a future release. */ +export type GoogleMapsTool = GoogleMapsToolConfig + +export type GeminiGoogleMapsTool = ProviderTool<'gemini', 'google_maps'> export function convertGoogleMapsToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as GoogleMapsTool + const metadata = tool.metadata as GoogleMapsToolConfig return { googleMaps: metadata, } } -export function googleMapsTool(config?: GoogleMapsTool): Tool { +export function googleMapsTool( + config?: GoogleMapsToolConfig, +): GeminiGoogleMapsTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'google_maps', description: '', metadata: config, - } + } as unknown as GeminiGoogleMapsTool } diff --git a/packages/typescript/ai-gemini/src/tools/google-search-retriveal-tool.ts b/packages/typescript/ai-gemini/src/tools/google-search-retriveal-tool.ts index 24611c6e9..b8976fd85 100644 --- a/packages/typescript/ai-gemini/src/tools/google-search-retriveal-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/google-search-retriveal-tool.ts @@ -1,21 +1,30 @@ import type { GoogleSearchRetrieval } from '@google/genai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type GoogleSearchRetrievalTool = GoogleSearchRetrieval +export type GoogleSearchRetrievalToolConfig = GoogleSearchRetrieval + +/** @deprecated Renamed to `GoogleSearchRetrievalToolConfig`. Will be removed in a future release. */ +export type GoogleSearchRetrievalTool = GoogleSearchRetrievalToolConfig + +export type GeminiGoogleSearchRetrievalTool = ProviderTool< + 'gemini', + 'google_search_retrieval' +> export function convertGoogleSearchRetrievalToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as GoogleSearchRetrievalTool + const metadata = tool.metadata as GoogleSearchRetrievalToolConfig return { googleSearchRetrieval: metadata, } } export function googleSearchRetrievalTool( - config?: GoogleSearchRetrievalTool, -): Tool { + config?: GoogleSearchRetrievalToolConfig, +): GeminiGoogleSearchRetrievalTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'google_search_retrieval', description: '', metadata: config, - } + } as unknown as GeminiGoogleSearchRetrievalTool } diff --git a/packages/typescript/ai-gemini/src/tools/google-search-tool.ts b/packages/typescript/ai-gemini/src/tools/google-search-tool.ts index cd72f42dd..0d7db3d06 100644 --- a/packages/typescript/ai-gemini/src/tools/google-search-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/google-search-tool.ts @@ -1,19 +1,27 @@ import type { GoogleSearch } from '@google/genai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type GoogleSearchTool = GoogleSearch +export type GoogleSearchToolConfig = GoogleSearch + +/** @deprecated Renamed to `GoogleSearchToolConfig`. Will be removed in a future release. */ +export type GoogleSearchTool = GoogleSearchToolConfig + +export type GeminiGoogleSearchTool = ProviderTool<'gemini', 'google_search'> export function convertGoogleSearchToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as GoogleSearchTool + const metadata = tool.metadata as GoogleSearchToolConfig return { googleSearch: metadata, } } -export function googleSearchTool(config?: GoogleSearchTool): Tool { +export function googleSearchTool( + config?: GoogleSearchToolConfig, +): GeminiGoogleSearchTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'google_search', description: '', metadata: config, - } + } as unknown as GeminiGoogleSearchTool } diff --git a/packages/typescript/ai-gemini/src/tools/index.ts b/packages/typescript/ai-gemini/src/tools/index.ts index 442a9bd70..d39a396d7 100644 --- a/packages/typescript/ai-gemini/src/tools/index.ts +++ b/packages/typescript/ai-gemini/src/tools/index.ts @@ -7,6 +7,55 @@ import type { GoogleSearchRetrievalTool } from './google-search-retriveal-tool' import type { GoogleSearchTool } from './google-search-tool' import type { UrlContextTool } from './url-context-tool' +export { + codeExecutionTool, + type GeminiCodeExecutionTool, + type CodeExecutionToolConfig, + type CodeExecutionTool, +} from './code-execution-tool' +export { + computerUseTool, + type GeminiComputerUseTool, + type ComputerUseToolConfig, + type ComputerUseTool, +} from './computer-use-tool' +export { + fileSearchTool, + type GeminiFileSearchTool, + type FileSearchToolConfig, + type FileSearchTool, +} from './file-search-tool' +export { + functionDeclarationTools, + type FunctionDeclarationTool, +} from './function-declaration-tool' +export { + googleMapsTool, + type GeminiGoogleMapsTool, + type GoogleMapsToolConfig, + type GoogleMapsTool, +} from './google-maps-tool' +export { + googleSearchRetrievalTool, + type GeminiGoogleSearchRetrievalTool, + type GoogleSearchRetrievalToolConfig, + type GoogleSearchRetrievalTool, +} from './google-search-retriveal-tool' +export { + googleSearchTool, + type GeminiGoogleSearchTool, + type GoogleSearchToolConfig, + type GoogleSearchTool, +} from './google-search-tool' +export { + urlContextTool, + type GeminiUrlContextTool, + type UrlContextToolConfig, + type UrlContextTool, +} from './url-context-tool' + +// Keep the existing discriminated union defined inline (no barrel exports). +// Built from the deprecated config-type aliases — matches the SDK-adapter shape. export type GoogleGeminiTool = | CodeExecutionTool | ComputerUseTool @@ -16,3 +65,5 @@ export type GoogleGeminiTool = | GoogleSearchRetrievalTool | GoogleSearchTool | UrlContextTool + +export { convertToolsToProviderFormat } from './tool-converter' diff --git a/packages/typescript/ai-gemini/src/tools/url-context-tool.ts b/packages/typescript/ai-gemini/src/tools/url-context-tool.ts index 3518dd7f3..38c1d8478 100644 --- a/packages/typescript/ai-gemini/src/tools/url-context-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/url-context-tool.ts @@ -1,6 +1,11 @@ -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export interface UrlContextTool {} +export interface UrlContextToolConfig {} + +/** @deprecated Renamed to `UrlContextToolConfig`. Will be removed in a future release. */ +export type UrlContextTool = UrlContextToolConfig + +export type GeminiUrlContextTool = ProviderTool<'gemini', 'url_context'> export function convertUrlContextToolToAdapterFormat(_tool: Tool) { return { @@ -8,10 +13,11 @@ export function convertUrlContextToolToAdapterFormat(_tool: Tool) { } } -export function urlContextTool(): Tool { +export function urlContextTool(): GeminiUrlContextTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'url_context', description: '', metadata: {}, - } + } as unknown as GeminiUrlContextTool } diff --git a/packages/typescript/ai-gemini/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai-gemini/tests/tools-per-model-type-safety.test.ts new file mode 100644 index 000000000..e32d28c2e --- /dev/null +++ b/packages/typescript/ai-gemini/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,88 @@ +/** + * Per-model type-safety tests for Gemini provider tools. + * + * Positive cases: each supported (model, tool) pair compiles cleanly. + * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. + */ +import { beforeAll, describe, it } from 'vitest' +import { z } from 'zod' +import { Environment } from '@google/genai' +import { toolDefinition } from '@tanstack/ai' +import { geminiText } from '../src' +import { + codeExecutionTool, + computerUseTool, + fileSearchTool, + googleMapsTool, + googleSearchRetrievalTool, + googleSearchTool, + urlContextTool, +} from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], +) { + return { adapter, tools } +} + +// Set a dummy API key so adapter construction does not throw at runtime. +// These tests only exercise compile-time type gating; no network calls are made. +beforeAll(() => { + process.env['GOOGLE_API_KEY'] = 'sk-test-dummy' +}) + +// Minimal user tool — always assignable regardless of model. +const userTool = toolDefinition({ + name: 'echo', + description: 'echoes input', + inputSchema: z.object({ msg: z.string() }), +}).server(async ({ msg }) => msg) + +describe('Gemini per-model tool gating', () => { + it('gemini-3.1-pro-preview accepts code_execution, file_search, google_search, url_context', () => { + const adapter = geminiText('gemini-3.1-pro-preview') + const fileSearchConfig: Parameters[0] = { + fileSearchStoreNames: [], + } + typedTools(adapter, [ + userTool, + codeExecutionTool(), + fileSearchTool(fileSearchConfig), + googleSearchTool(), + urlContextTool(), + // @ts-expect-error - gemini-3.1-pro-preview does not support computer_use + computerUseTool({ + environment: Environment.ENVIRONMENT_BROWSER, + excludedPredefinedFunctions: [], + }), + // @ts-expect-error - gemini-3.1-pro-preview does not support google_maps + googleMapsTool(), + // @ts-expect-error - gemini-3.1-pro-preview does not support google_search_retrieval + googleSearchRetrievalTool(), + ]) + }) + + it('gemini-2.0-flash-lite rejects all provider tools', () => { + const adapter = geminiText('gemini-2.0-flash-lite') + typedTools(adapter, [ + userTool, + // @ts-expect-error - gemini-2.0-flash-lite does not support code_execution + codeExecutionTool(), + // @ts-expect-error - gemini-2.0-flash-lite does not support computer_use + computerUseTool({}), + // @ts-expect-error - gemini-2.0-flash-lite does not support file_search + fileSearchTool({ fileSearchStoreNames: [] }), + // @ts-expect-error - gemini-2.0-flash-lite does not support google_maps + googleMapsTool(), + // @ts-expect-error - gemini-2.0-flash-lite does not support google_search + googleSearchTool(), + // @ts-expect-error - gemini-2.0-flash-lite does not support google_search_retrieval + googleSearchRetrievalTool(), + // @ts-expect-error - gemini-2.0-flash-lite does not support url_context + urlContextTool(), + ]) + }) +}) diff --git a/packages/typescript/ai-gemini/tsconfig.json b/packages/typescript/ai-gemini/tsconfig.json index ea11c1096..0c50acadb 100644 --- a/packages/typescript/ai-gemini/tsconfig.json +++ b/packages/typescript/ai-gemini/tsconfig.json @@ -1,9 +1,12 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { - "outDir": "dist", - "rootDir": "src" + "outDir": "dist" }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": [ + "vite.config.ts", + "./src", + "tests/tools-per-model-type-safety.test.ts" + ], "exclude": ["node_modules", "dist", "**/*.config.ts"] } diff --git a/packages/typescript/ai-gemini/vite.config.ts b/packages/typescript/ai-gemini/vite.config.ts index 77bcc2e60..0e7e7eaea 100644 --- a/packages/typescript/ai-gemini/vite.config.ts +++ b/packages/typescript/ai-gemini/vite.config.ts @@ -29,7 +29,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/tools/index.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/ai-grok/package.json b/packages/typescript/ai-grok/package.json index ae861eee4..9432e5978 100644 --- a/packages/typescript/ai-grok/package.json +++ b/packages/typescript/ai-grok/package.json @@ -16,6 +16,10 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" + }, + "./tools": { + "types": "./dist/esm/tools/index.d.ts", + "import": "./dist/esm/tools/index.js" } }, "files": [ diff --git a/packages/typescript/ai-grok/src/adapters/text.ts b/packages/typescript/ai-grok/src/adapters/text.ts index 9902354f3..6d354fe10 100644 --- a/packages/typescript/ai-grok/src/adapters/text.ts +++ b/packages/typescript/ai-grok/src/adapters/text.ts @@ -10,6 +10,7 @@ import { } from '../utils' import type { GROK_CHAT_MODELS, + GrokChatModelToolCapabilitiesByName, ResolveInputModalities, ResolveProviderOptions, } from '../model-meta' @@ -20,17 +21,26 @@ import type { import type OpenAI_SDK from 'openai' import type { ContentPart, + Modality, ModelMessage, StreamChunk, TextOptions, } from '@tanstack/ai' -import type { InternalTextProviderOptions } from '../text/text-provider-options' +import type { + ExternalTextProviderOptions as GrokTextProviderOptions, + InternalTextProviderOptions, +} from '../text/text-provider-options' import type { GrokImageMetadata, GrokMessageMetadataByModality, } from '../message-types' import type { GrokClientConfig } from '../utils' +type ResolveToolCapabilities = + TModel extends keyof GrokChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + /** Cast an event object to StreamChunk. Adapters construct events with string * literal types which are structurally compatible with the EventType enum. */ const asChunk = (chunk: Record) => @@ -54,11 +64,17 @@ export type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../ */ export class GrokTextAdapter< TModel extends (typeof GROK_CHAT_MODELS)[number], + TProviderOptions extends Record = ResolveProviderOptions, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, - ResolveProviderOptions, - ResolveInputModalities, - GrokMessageMetadataByModality + TProviderOptions, + TInputModalities, + GrokMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const readonly name = 'grok' as const @@ -71,7 +87,7 @@ export class GrokTextAdapter< } async *chatStream( - options: TextOptions>, + options: TextOptions, ): AsyncIterable { const requestParams = this.mapTextOptionsToGrok(options) const timestamp = Date.now() @@ -141,7 +157,7 @@ export class GrokTextAdapter< * We apply Grok-specific transformations for structured output compatibility. */ async structuredOutput( - options: StructuredOutputOptions>, + options: StructuredOutputOptions, ): Promise> { const { chatOptions, outputSchema } = options const requestParams = this.mapTextOptionsToGrok(chatOptions) diff --git a/packages/typescript/ai-grok/src/index.ts b/packages/typescript/ai-grok/src/index.ts index a5deb0997..4a27ef8e5 100644 --- a/packages/typescript/ai-grok/src/index.ts +++ b/packages/typescript/ai-grok/src/index.ts @@ -39,6 +39,7 @@ export type { export type { GrokChatModelProviderOptionsByName, + GrokChatModelToolCapabilitiesByName, GrokModelInputModalitiesByName, ResolveProviderOptions, ResolveInputModalities, diff --git a/packages/typescript/ai-grok/src/model-meta.ts b/packages/typescript/ai-grok/src/model-meta.ts index 1b1e0f823..682504661 100644 --- a/packages/typescript/ai-grok/src/model-meta.ts +++ b/packages/typescript/ai-grok/src/model-meta.ts @@ -7,6 +7,7 @@ interface ModelMeta { input: Array<'text' | 'image' | 'audio' | 'video' | 'document'> output: Array<'text' | 'image' | 'audio' | 'video'> capabilities?: Array<'reasoning' | 'tool_calling' | 'structured_outputs'> + tools?: ReadonlyArray } max_input_tokens?: number max_output_tokens?: number @@ -30,6 +31,7 @@ const GROK_4_1_FAST_REASONING = { input: ['text', 'image'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -49,6 +51,7 @@ const GROK_4_1_FAST_NON_REASONING = { input: ['text', 'image'], output: ['text'], capabilities: ['structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -68,6 +71,7 @@ const GROK_CODE_FAST_1 = { input: ['text'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -87,6 +91,7 @@ const GROK_4_FAST_REASONING = { input: ['text', 'image'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -106,6 +111,7 @@ const GROK_4_FAST_NON_REASONING = { input: ['text', 'image'], output: ['text'], capabilities: ['structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -125,6 +131,7 @@ const GROK_4 = { input: ['text', 'image'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -144,6 +151,7 @@ const GROK_3_MINI = { input: ['text'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -163,6 +171,7 @@ const GROK_3 = { input: ['text'], output: ['text'], capabilities: ['structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -182,6 +191,7 @@ const GROK_2_VISION = { input: ['text', 'image'], output: ['text'], capabilities: ['structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -220,6 +230,7 @@ const GROK_4_20 = { input: ['text', 'image', 'document'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -239,6 +250,7 @@ const GROK_4_20_MULTI_AGENT = { input: ['text', 'image', 'document'], output: ['text'], capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [] as const, }, pricing: { input: { @@ -300,6 +312,26 @@ export type GrokChatModelProviderOptionsByName = { [K in (typeof GROK_CHAT_MODELS)[number]]: GrokProviderOptions } +/** + * Type-only map from Grok chat model name to its supported provider tools. + * Grok exposes no provider-specific tool factories, so every model gets an + * empty tuple. This ensures that passing an Anthropic/OpenAI ProviderTool to + * a Grok adapter produces a compile-time type error. + */ +export type GrokChatModelToolCapabilitiesByName = { + [GROK_4_1_FAST_REASONING.name]: typeof GROK_4_1_FAST_REASONING.supports.tools + [GROK_4_1_FAST_NON_REASONING.name]: typeof GROK_4_1_FAST_NON_REASONING.supports.tools + [GROK_CODE_FAST_1.name]: typeof GROK_CODE_FAST_1.supports.tools + [GROK_4_FAST_REASONING.name]: typeof GROK_4_FAST_REASONING.supports.tools + [GROK_4_FAST_NON_REASONING.name]: typeof GROK_4_FAST_NON_REASONING.supports.tools + [GROK_4.name]: typeof GROK_4.supports.tools + [GROK_3.name]: typeof GROK_3.supports.tools + [GROK_3_MINI.name]: typeof GROK_3_MINI.supports.tools + [GROK_2_VISION.name]: typeof GROK_2_VISION.supports.tools + [GROK_4_20.name]: typeof GROK_4_20.supports.tools + [GROK_4_20_MULTI_AGENT.name]: typeof GROK_4_20_MULTI_AGENT.supports.tools +} + /** * Grok-specific provider options * Based on OpenAI-compatible API options diff --git a/packages/typescript/ai-grok/vite.config.ts b/packages/typescript/ai-grok/vite.config.ts index 77bcc2e60..0e7e7eaea 100644 --- a/packages/typescript/ai-grok/vite.config.ts +++ b/packages/typescript/ai-grok/vite.config.ts @@ -29,7 +29,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/tools/index.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/ai-groq/package.json b/packages/typescript/ai-groq/package.json index c5b7a7406..1c6385655 100644 --- a/packages/typescript/ai-groq/package.json +++ b/packages/typescript/ai-groq/package.json @@ -16,6 +16,10 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" + }, + "./tools": { + "types": "./dist/esm/tools/index.d.ts", + "import": "./dist/esm/tools/index.js" } }, "files": [ diff --git a/packages/typescript/ai-groq/src/adapters/text.ts b/packages/typescript/ai-groq/src/adapters/text.ts index 7a170b80c..b14879c96 100644 --- a/packages/typescript/ai-groq/src/adapters/text.ts +++ b/packages/typescript/ai-groq/src/adapters/text.ts @@ -10,6 +10,7 @@ import { } from '../utils' import type { GROQ_CHAT_MODELS, + GroqChatModelToolCapabilitiesByName, ResolveInputModalities, ResolveProviderOptions, } from '../model-meta' @@ -21,11 +22,15 @@ import type GROQ_SDK from 'groq-sdk' import type { ChatCompletionCreateParamsStreaming } from 'groq-sdk/resources/chat/completions' import type { ContentPart, + Modality, ModelMessage, StreamChunk, TextOptions, } from '@tanstack/ai' -import type { InternalTextProviderOptions } from '../text/text-provider-options' +import type { + ExternalTextProviderOptions, + InternalTextProviderOptions, +} from '../text/text-provider-options' import type { ChatCompletionContentPart, ChatCompletionMessageParam, @@ -34,6 +39,13 @@ import type { } from '../message-types' import type { GroqClientConfig } from '../utils' +type GroqTextProviderOptions = ExternalTextProviderOptions + +type ResolveToolCapabilities = + TModel extends keyof GroqChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + /** Cast an event object to StreamChunk. Adapters construct events with string * literal types which are structurally compatible with the EventType enum. */ const asChunk = (chunk: Record) => @@ -57,11 +69,17 @@ export type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../ */ export class GroqTextAdapter< TModel extends (typeof GROQ_CHAT_MODELS)[number], + TProviderOptions extends Record = ResolveProviderOptions, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, - ResolveProviderOptions, - ResolveInputModalities, - GroqMessageMetadataByModality + TProviderOptions, + TInputModalities, + GroqMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const readonly name = 'groq' as const @@ -74,7 +92,7 @@ export class GroqTextAdapter< } async *chatStream( - options: TextOptions>, + options: TextOptions, ): AsyncIterable { const requestParams = this.mapTextOptionsToGroq(options) const timestamp = Date.now() @@ -141,7 +159,7 @@ export class GroqTextAdapter< * We apply Groq-specific transformations for structured output compatibility. */ async structuredOutput( - options: StructuredOutputOptions>, + options: StructuredOutputOptions, ): Promise> { const { chatOptions, outputSchema } = options const requestParams = this.mapTextOptionsToGroq(chatOptions) diff --git a/packages/typescript/ai-groq/src/index.ts b/packages/typescript/ai-groq/src/index.ts index ff2d02872..034ff38d0 100644 --- a/packages/typescript/ai-groq/src/index.ts +++ b/packages/typescript/ai-groq/src/index.ts @@ -17,6 +17,7 @@ export { // Types export type { GroqChatModelProviderOptionsByName, + GroqChatModelToolCapabilitiesByName, GroqModelInputModalitiesByName, ResolveProviderOptions, ResolveInputModalities, diff --git a/packages/typescript/ai-groq/src/model-meta.ts b/packages/typescript/ai-groq/src/model-meta.ts index 83ce38800..70eae1cde 100644 --- a/packages/typescript/ai-groq/src/model-meta.ts +++ b/packages/typescript/ai-groq/src/model-meta.ts @@ -27,6 +27,7 @@ interface ModelMeta { | 'json_schema' | 'vision' > + tools?: ReadonlyArray } /** * Type-level description of which provider options this model supports. @@ -51,6 +52,7 @@ const LLAMA_3_3_70B_VERSATILE = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'json_object'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -71,6 +73,7 @@ const LLAMA_4_MAVERICK_17B_128E_INSTRUCT = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'json_object', 'json_schema', 'vision'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -91,6 +94,7 @@ const LLAMA_4_SCOUT_17B_16E_INSTRUCT = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'json_object'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -111,6 +115,7 @@ const LLAMA_GUARD_4_12B = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'json_object', 'content_moderation', 'vision'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -131,6 +136,7 @@ const LLAMA_PROMPT_GUARD_2_86M = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'content_moderation', 'json_object'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -151,6 +157,7 @@ const LLAMA_3_1_8B_INSTANT = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'json_object', 'tools'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -171,6 +178,7 @@ const LLAMA_PROMPT_GUARD_2_22M = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'content_moderation'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -200,6 +208,7 @@ const GPT_OSS_120B = { 'code_execution', 'reasoning', ], + tools: [] as const, }, } as const satisfies ModelMeta @@ -230,6 +239,7 @@ const GPT_OSS_SAFEGUARD_20B = { 'reasoning', 'content_moderation', ], + tools: [] as const, }, } as const satisfies ModelMeta @@ -259,6 +269,7 @@ const GPT_OSS_20B = { 'reasoning', 'tools', ], + tools: [] as const, }, } as const satisfies ModelMeta @@ -280,6 +291,7 @@ const KIMI_K2_INSTRUCT_0905 = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'tools', 'json_object', 'json_schema'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -300,6 +312,7 @@ const QWEN3_32B = { output: ['text'], endpoints: ['chat'], features: ['streaming', 'json_object', 'tools', 'reasoning'], + tools: [] as const, }, } as const satisfies ModelMeta @@ -351,6 +364,27 @@ export type GroqChatModelProviderOptionsByName = { [K in (typeof GROQ_CHAT_MODELS)[number]]: GroqTextProviderOptions } +/** + * Type-only map from Groq chat model name to its supported provider tools. + * Groq exposes no provider-specific tool factories, so every model gets an + * empty tuple. This ensures that passing an Anthropic/OpenAI ProviderTool to + * 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 +} + /** * Resolves the provider options type for a specific Groq model. * Falls back to generic GroqTextProviderOptions for unknown models. diff --git a/packages/typescript/ai-groq/vite.config.ts b/packages/typescript/ai-groq/vite.config.ts index 77bcc2e60..0e7e7eaea 100644 --- a/packages/typescript/ai-groq/vite.config.ts +++ b/packages/typescript/ai-groq/vite.config.ts @@ -29,7 +29,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/tools/index.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/ai-openai/package.json b/packages/typescript/ai-openai/package.json index 40a464bd3..b2f803ec1 100644 --- a/packages/typescript/ai-openai/package.json +++ b/packages/typescript/ai-openai/package.json @@ -16,6 +16,10 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" + }, + "./tools": { + "types": "./dist/esm/tools/index.d.ts", + "import": "./dist/esm/tools/index.js" } }, "files": [ diff --git a/packages/typescript/ai-openai/src/adapters/text.ts b/packages/typescript/ai-openai/src/adapters/text.ts index ad9025fd4..7ccdbfe7d 100644 --- a/packages/typescript/ai-openai/src/adapters/text.ts +++ b/packages/typescript/ai-openai/src/adapters/text.ts @@ -14,6 +14,7 @@ import type { OPENAI_CHAT_MODELS, OpenAIChatModel, OpenAIChatModelProviderOptionsByName, + OpenAIChatModelToolCapabilitiesByName, OpenAIModelInputModalitiesByName, } from '../model-meta' import type { @@ -24,6 +25,7 @@ import type OpenAI_SDK from 'openai' import type { Responses } from 'openai/resources' import type { ContentPart, + Modality, ModelMessage, StreamChunk, TextOptions, @@ -76,6 +78,15 @@ type ResolveInputModalities = ? OpenAIModelInputModalitiesByName[TModel] : readonly ['text', 'image', 'audio'] +/** + * Resolve tool capabilities for a specific model. + * If the model has explicit tools in the map, use those; otherwise use empty tuple. + */ +type ResolveToolCapabilities = + TModel extends keyof OpenAIChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + // =========================== // Adapter Implementation // =========================== @@ -88,11 +99,17 @@ type ResolveInputModalities = */ export class OpenAITextAdapter< TModel extends OpenAIChatModel, + TProviderOptions extends Record = ResolveProviderOptions, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, - ResolveProviderOptions, - ResolveInputModalities, - OpenAIMessageMetadataByModality + TProviderOptions, + TInputModalities, + OpenAIMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const readonly name = 'openai' as const @@ -105,7 +122,7 @@ export class OpenAITextAdapter< } async *chatStream( - options: TextOptions>, + options: TextOptions, ): AsyncIterable { // Track tool call metadata by unique ID // OpenAI streams tool calls with deltas - first chunk has ID/name, subsequent chunks only have args @@ -158,7 +175,7 @@ export class OpenAITextAdapter< * We apply OpenAI-specific transformations for structured output compatibility. */ async structuredOutput( - options: StructuredOutputOptions>, + options: StructuredOutputOptions, ): Promise> { const { chatOptions, outputSchema } = options const requestArguments = this.mapTextOptionsToOpenAI(chatOptions) diff --git a/packages/typescript/ai-openai/src/index.ts b/packages/typescript/ai-openai/src/index.ts index afadc4529..b2d6a1d26 100644 --- a/packages/typescript/ai-openai/src/index.ts +++ b/packages/typescript/ai-openai/src/index.ts @@ -77,6 +77,7 @@ export type { OpenAITranscriptionProviderOptions } from './audio/transcription-p export type { OpenAIChatModelProviderOptionsByName, + OpenAIChatModelToolCapabilitiesByName, OpenAIModelInputModalitiesByName, OpenAIChatModel, OpenAIImageModel, diff --git a/packages/typescript/ai-openai/src/model-meta.ts b/packages/typescript/ai-openai/src/model-meta.ts index 3b6d5748e..1772171fe 100644 --- a/packages/typescript/ai-openai/src/model-meta.ts +++ b/packages/typescript/ai-openai/src/model-meta.ts @@ -39,11 +39,15 @@ interface ModelMeta { > tools?: Array< | 'web_search' + | 'web_search_preview' | 'file_search' | 'image_generation' | 'code_interpreter' | 'mcp' | 'computer_use' + | 'local_shell' + | 'shell' + | 'apply_patch' > } context_window?: number @@ -81,10 +85,15 @@ const GPT5_2 = { ], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, pricing: { @@ -114,7 +123,18 @@ const GPT5_2_PRO = { output: ['text'], endpoints: ['chat', 'chat-completions'], features: ['streaming', 'function_calling'], - tools: ['web_search', 'file_search', 'image_generation', 'mcp'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, pricing: { input: { @@ -142,7 +162,14 @@ const GPT5_2_CHAT = { output: ['text'], endpoints: ['chat', 'chat-completions'], features: ['streaming', 'function_calling', 'structured_outputs'], - tools: [], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + ], }, pricing: { input: { @@ -178,10 +205,15 @@ const GPT5_1 = { ], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, pricing: { @@ -212,6 +244,14 @@ const GPT5_1_CODEX = { output: ['text', 'image'], endpoints: ['chat'], features: ['streaming', 'function_calling', 'structured_outputs'], + tools: [ + 'file_search', + 'code_interpreter', + 'mcp', + 'local_shell', + 'shell', + 'apply_patch', + ], }, pricing: { input: { @@ -248,10 +288,15 @@ const GPT5 = { ], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, pricing: { @@ -282,7 +327,18 @@ const GPT5_MINI = { output: ['text'], endpoints: ['chat', 'chat-completions', 'batch'], features: ['streaming', 'structured_outputs', 'function_calling'], - tools: ['web_search', 'file_search', 'mcp', 'code_interpreter'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, pricing: { input: { @@ -323,10 +379,15 @@ const GPT5_NANO = { features: ['streaming', 'structured_outputs', 'function_calling'], tools: [ 'web_search', + 'web_search_preview', 'file_search', - 'mcp', 'image_generation', 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, } as const satisfies ModelMeta< @@ -356,7 +417,18 @@ const GPT5_PRO = { output: ['text'], endpoints: ['chat', 'batch'], features: ['streaming', 'structured_outputs', 'function_calling'], - tools: ['web_search', 'file_search', 'image_generation', 'mcp'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -386,6 +458,14 @@ const GPT5_CODEX = { output: ['text', 'image'], endpoints: ['chat'], features: ['streaming', 'structured_outputs', 'function_calling'], + tools: [ + 'file_search', + 'code_interpreter', + 'mcp', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -511,6 +591,18 @@ const O3_DEEP_RESEARCH = { output: ['text'], endpoints: ['chat', 'batch'], features: ['streaming'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -538,6 +630,18 @@ const O4_MINI_DEEP_RESEARCH = { output: ['text'], endpoints: ['chat', 'batch'], features: ['streaming'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -564,6 +668,18 @@ const O3_PRO = { output: ['text'], endpoints: ['chat', 'batch'], features: ['function_calling', 'structured_outputs'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -593,6 +709,7 @@ const GPT_AUDIO = { output: ['text', 'audio'], endpoints: ['chat-completions'], features: ['function_calling'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -676,6 +793,7 @@ const GPT_AUDIO_MINI = { output: ['text', 'audio'], endpoints: ['chat-completions'], features: ['function_calling'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -703,6 +821,18 @@ const O3 = { output: ['text'], endpoints: ['chat', 'batch', 'chat-completions'], features: ['function_calling', 'structured_outputs', 'streaming'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -737,6 +867,18 @@ const O4_MINI = { 'streaming', 'fine_tuning', ], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -779,10 +921,15 @@ const GPT4_1 = { ], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, } as const satisfies ModelMeta< @@ -824,6 +971,18 @@ const GPT4_1_MINI = { 'structured_outputs', 'fine_tuning', ], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -864,6 +1023,18 @@ const GPT4_1_NANO = { 'fine_tuning', 'predicted_outcomes', ], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -891,6 +1062,7 @@ const O1_PRO = { output: ['text'], endpoints: ['chat', 'batch'], features: ['function_calling', 'structured_outputs'], + tools: ['file_search', 'code_interpreter', 'mcp'], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -919,6 +1091,7 @@ const COMPUTER_USE_PREVIEW = { output: ['text'], endpoints: ['chat', 'batch'], features: ['function_calling'], + tools: ['computer_use'], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -946,6 +1119,7 @@ const GPT_4O_MINI_SEARCH_PREVIEW = { output: ['text'], endpoints: ['chat-completions'], features: ['streaming', 'structured_outputs'], + tools: ['web_search_preview'], }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions @@ -969,6 +1143,7 @@ const GPT_4O_SEARCH_PREVIEW = { output: ['text'], endpoints: ['chat-completions'], features: ['streaming', 'structured_outputs'], + tools: ['web_search_preview'], }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions @@ -993,6 +1168,18 @@ const O3_MINI = { output: ['text'], endpoints: ['chat', 'batch', 'chat-completions', 'assistants'], features: ['function_calling', 'structured_outputs', 'streaming'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1022,6 +1209,7 @@ const GPT_4O_MINI_AUDIO = { output: ['text', 'audio'], endpoints: ['chat-completions'], features: ['function_calling', 'streaming'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1077,6 +1265,7 @@ const O1 = { output: ['text'], endpoints: ['chat', 'batch', 'chat-completions', 'assistants'], features: ['function_calling', 'structured_outputs', 'streaming'], + tools: ['file_search', 'code_interpreter', 'mcp'], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1139,6 +1328,13 @@ const GPT_4O = { 'fine_tuning', 'predicted_outcomes', ], + tools: [ + 'web_search', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1168,6 +1364,7 @@ const GPT_4O_AUDIO = { output: ['text', 'audio'], endpoints: ['chat-completions'], features: ['streaming', 'function_calling'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1207,6 +1404,13 @@ const GPT_4O_MINI = { 'fine_tuning', 'predicted_outcomes', ], + tools: [ + 'web_search', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1262,6 +1466,7 @@ const GPT_4_TURBO = { output: ['text'], endpoints: ['chat', 'chat-completions', 'assistants', 'batch'], features: ['function_calling', 'streaming'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1288,6 +1493,14 @@ const CHATGPT_40 = { 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 @@ -1312,6 +1525,14 @@ const GPT_5_1_CODEX_MINI = { output: ['text', 'image'], endpoints: ['chat'], features: ['streaming', 'function_calling', 'structured_outputs'], + tools: [ + 'file_search', + 'code_interpreter', + 'mcp', + 'local_shell', + 'shell', + 'apply_patch', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1341,6 +1562,14 @@ const CODEX_MINI_LATEST = { 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 & @@ -1411,6 +1640,7 @@ const GPT_3_5_TURBO = { output: ['text'], endpoints: ['chat', 'chat-completions', 'batch', 'fine-tuning'], features: ['fine_tuning'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions @@ -1440,6 +1670,7 @@ const GPT_4 = { 'assistants', ], features: ['fine_tuning', 'streaming'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions @@ -1557,6 +1788,14 @@ const GPT_5_1_CHAT = { output: ['text'], endpoints: ['chat', 'chat-completions'], features: ['streaming', 'function_calling', 'structured_outputs'], + tools: [ + 'web_search', + 'web_search_preview', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1588,6 +1827,7 @@ const GPT_5_CHAT = { features: ['streaming', 'function_calling', 'structured_outputs'], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', @@ -1662,10 +1902,15 @@ const GPT_5_4_MINI = { ], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, pricing: { @@ -1702,10 +1947,15 @@ const GPT_5_4_NANO = { ], tools: [ 'web_search', + 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', + 'computer_use', + 'local_shell', + 'shell', + 'apply_patch', ], }, pricing: { @@ -2044,6 +2294,54 @@ export type OpenAIChatModelProviderOptionsByName = { OpenAIMetadataOptions } +/** + * Type-only map from chat model name to its supported provider tools. + * Keyed on each model's `.name` field. Value is the `typeof supports.tools` + * tuple from each model constant. + */ +export type OpenAIChatModelToolCapabilitiesByName = { + [GPT5_2.name]: typeof GPT5_2.supports.tools + [GPT5_2_PRO.name]: typeof GPT5_2_PRO.supports.tools + [GPT5_2_CHAT.name]: typeof GPT5_2_CHAT.supports.tools + [GPT5_1.name]: typeof GPT5_1.supports.tools + [GPT5_1_CODEX.name]: typeof GPT5_1_CODEX.supports.tools + [GPT5.name]: typeof GPT5.supports.tools + [GPT5_MINI.name]: typeof GPT5_MINI.supports.tools + [GPT5_NANO.name]: typeof GPT5_NANO.supports.tools + [GPT5_PRO.name]: typeof GPT5_PRO.supports.tools + [GPT5_CODEX.name]: typeof GPT5_CODEX.supports.tools + [O3.name]: typeof O3.supports.tools + [O3_PRO.name]: typeof O3_PRO.supports.tools + [O3_MINI.name]: typeof O3_MINI.supports.tools + [O4_MINI.name]: typeof O4_MINI.supports.tools + [O3_DEEP_RESEARCH.name]: typeof O3_DEEP_RESEARCH.supports.tools + [O4_MINI_DEEP_RESEARCH.name]: typeof O4_MINI_DEEP_RESEARCH.supports.tools + [GPT4_1.name]: typeof GPT4_1.supports.tools + [GPT4_1_MINI.name]: typeof GPT4_1_MINI.supports.tools + [GPT4_1_NANO.name]: typeof GPT4_1_NANO.supports.tools + [GPT_4.name]: typeof GPT_4.supports.tools + [GPT_4_TURBO.name]: typeof GPT_4_TURBO.supports.tools + [GPT_4O.name]: typeof GPT_4O.supports.tools + [GPT_4O_MINI.name]: typeof GPT_4O_MINI.supports.tools + [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 +} + /** * Type-only map from chat model name to its supported input modalities. * Based on the 'supports.input' arrays defined for each model. diff --git a/packages/typescript/ai-openai/src/tools/apply-patch-tool.ts b/packages/typescript/ai-openai/src/tools/apply-patch-tool.ts index 8e73cc898..486841f75 100644 --- a/packages/typescript/ai-openai/src/tools/apply-patch-tool.ts +++ b/packages/typescript/ai-openai/src/tools/apply-patch-tool.ts @@ -1,14 +1,19 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type ApplyPatchTool = OpenAI.Responses.ApplyPatchTool +export type ApplyPatchToolConfig = OpenAI.Responses.ApplyPatchTool + +/** @deprecated Renamed to `ApplyPatchToolConfig`. Will be removed in a future release. */ +export type ApplyPatchTool = ApplyPatchToolConfig + +export type OpenAIApplyPatchTool = ProviderTool<'openai', 'apply_patch'> /** * Converts a standard Tool to OpenAI ApplyPatchTool format */ export function convertApplyPatchToolToAdapterFormat( _tool: Tool, -): ApplyPatchTool { +): ApplyPatchToolConfig { return { type: 'apply_patch', } @@ -17,10 +22,11 @@ export function convertApplyPatchToolToAdapterFormat( /** * Creates a standard Tool from ApplyPatchTool parameters */ -export function applyPatchTool(): Tool { +export function applyPatchTool(): OpenAIApplyPatchTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'apply_patch', description: 'Apply a patch to modify files', metadata: {}, - } + } as unknown as OpenAIApplyPatchTool } diff --git a/packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts b/packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts index 15bd8e429..357b47c64 100644 --- a/packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts +++ b/packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts @@ -1,15 +1,23 @@ -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' import type OpenAI from 'openai' -export type CodeInterpreterTool = OpenAI.Responses.Tool.CodeInterpreter +export type CodeInterpreterToolConfig = OpenAI.Responses.Tool.CodeInterpreter + +/** @deprecated Renamed to `CodeInterpreterToolConfig`. Will be removed in a future release. */ +export type CodeInterpreterTool = CodeInterpreterToolConfig + +export type OpenAICodeInterpreterTool = ProviderTool< + 'openai', + 'code_interpreter' +> /** * Converts a standard Tool to OpenAI CodeInterpreterTool format */ export function convertCodeInterpreterToolToAdapterFormat( tool: Tool, -): CodeInterpreterTool { - const metadata = tool.metadata as CodeInterpreterTool +): CodeInterpreterToolConfig { + const metadata = tool.metadata as CodeInterpreterToolConfig return { type: 'code_interpreter', container: metadata.container, @@ -19,7 +27,10 @@ export function convertCodeInterpreterToolToAdapterFormat( /** * Creates a standard Tool from CodeInterpreterTool parameters */ -export function codeInterpreterTool(container: CodeInterpreterTool): Tool { +export function codeInterpreterTool( + container: CodeInterpreterToolConfig, +): OpenAICodeInterpreterTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'code_interpreter', description: 'Execute code in a sandboxed environment', @@ -27,5 +38,5 @@ export function codeInterpreterTool(container: CodeInterpreterTool): Tool { type: 'code_interpreter', container, }, - } + } as unknown as OpenAICodeInterpreterTool } diff --git a/packages/typescript/ai-openai/src/tools/computer-use-tool.ts b/packages/typescript/ai-openai/src/tools/computer-use-tool.ts index 1a19b573b..72e3a9399 100644 --- a/packages/typescript/ai-openai/src/tools/computer-use-tool.ts +++ b/packages/typescript/ai-openai/src/tools/computer-use-tool.ts @@ -1,14 +1,20 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' + +export type ComputerUseToolConfig = OpenAI.Responses.ComputerTool + +/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */ +export type ComputerUseTool = ComputerUseToolConfig + +export type OpenAIComputerUseTool = ProviderTool<'openai', 'computer_use'> -export type ComputerUseTool = OpenAI.Responses.ComputerTool /** * Converts a standard Tool to OpenAI ComputerUseTool format */ export function convertComputerUseToolToAdapterFormat( tool: Tool, -): ComputerUseTool { - const metadata = tool.metadata as ComputerUseTool +): ComputerUseToolConfig { + const metadata = tool.metadata as ComputerUseToolConfig return { type: 'computer_use_preview', display_height: metadata.display_height, @@ -20,12 +26,15 @@ export function convertComputerUseToolToAdapterFormat( /** * Creates a standard Tool from ComputerUseTool parameters */ -export function computerUseTool(toolData: ComputerUseTool): Tool { +export function computerUseTool( + toolData: ComputerUseToolConfig, +): OpenAIComputerUseTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'computer_use_preview', description: 'Control a virtual computer', metadata: { ...toolData, }, - } + } as unknown as OpenAIComputerUseTool } diff --git a/packages/typescript/ai-openai/src/tools/custom-tool.ts b/packages/typescript/ai-openai/src/tools/custom-tool.ts index ad7de4d25..1bbd543b7 100644 --- a/packages/typescript/ai-openai/src/tools/custom-tool.ts +++ b/packages/typescript/ai-openai/src/tools/custom-tool.ts @@ -1,13 +1,16 @@ import type OpenAI from 'openai' import type { Tool } from '@tanstack/ai' -export type CustomTool = OpenAI.Responses.CustomTool +export type CustomToolConfig = OpenAI.Responses.CustomTool + +/** @deprecated Renamed to `CustomToolConfig`. Will be removed in a future release. */ +export type CustomTool = CustomToolConfig /** * Converts a standard Tool to OpenAI CustomTool format */ -export function convertCustomToolToAdapterFormat(tool: Tool): CustomTool { - const metadata = tool.metadata as CustomTool +export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { + const metadata = tool.metadata as CustomToolConfig return { type: 'custom', name: metadata.name, @@ -19,7 +22,7 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomTool { /** * Creates a standard Tool from CustomTool parameters */ -export function customTool(toolData: CustomTool): Tool { +export function customTool(toolData: CustomToolConfig): Tool { return { name: 'custom', description: toolData.description || 'A custom tool', diff --git a/packages/typescript/ai-openai/src/tools/file-search-tool.ts b/packages/typescript/ai-openai/src/tools/file-search-tool.ts index 0fc85f06e..ee109e10f 100644 --- a/packages/typescript/ai-openai/src/tools/file-search-tool.ts +++ b/packages/typescript/ai-openai/src/tools/file-search-tool.ts @@ -1,5 +1,5 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' const validateMaxNumResults = (maxNumResults: number | undefined) => { if (maxNumResults && (maxNumResults < 1 || maxNumResults > 50)) { @@ -7,7 +7,12 @@ const validateMaxNumResults = (maxNumResults: number | undefined) => { } } -export type FileSearchTool = OpenAI.Responses.FileSearchTool +export type FileSearchToolConfig = OpenAI.Responses.FileSearchTool + +/** @deprecated Renamed to `FileSearchToolConfig`. Will be removed in a future release. */ +export type FileSearchTool = FileSearchToolConfig + +export type OpenAIFileSearchTool = ProviderTool<'openai', 'file_search'> /** * Converts a standard Tool to OpenAI FileSearchTool format @@ -30,13 +35,14 @@ export function convertFileSearchToolToAdapterFormat( */ export function fileSearchTool( toolData: OpenAI.Responses.FileSearchTool, -): Tool { +): OpenAIFileSearchTool { validateMaxNumResults(toolData.max_num_results) + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'file_search', description: 'Search files in vector stores', metadata: { ...toolData, }, - } + } as unknown as OpenAIFileSearchTool } diff --git a/packages/typescript/ai-openai/src/tools/function-tool.ts b/packages/typescript/ai-openai/src/tools/function-tool.ts index 6bcce9cde..6b4630f3f 100644 --- a/packages/typescript/ai-openai/src/tools/function-tool.ts +++ b/packages/typescript/ai-openai/src/tools/function-tool.ts @@ -2,7 +2,10 @@ import { makeOpenAIStructuredOutputCompatible } from '../utils/schema-converter' import type { JSONSchema, Tool } from '@tanstack/ai' import type OpenAI from 'openai' -export type FunctionTool = OpenAI.Responses.FunctionTool +export type FunctionToolConfig = OpenAI.Responses.FunctionTool + +/** @deprecated Renamed to `FunctionToolConfig`. Will be removed in a future release. */ +export type FunctionTool = FunctionToolConfig /** * Converts a standard Tool to OpenAI FunctionTool format. @@ -15,7 +18,9 @@ export type FunctionTool = OpenAI.Responses.FunctionTool * * This enables strict mode for all tools automatically. */ -export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionTool { +export function convertFunctionToolToAdapterFormat( + tool: Tool, +): FunctionToolConfig { // Tool schemas are already converted to JSON Schema in the ai layer // Apply OpenAI-specific transformations for strict mode const inputSchema = (tool.inputSchema ?? { @@ -38,5 +43,5 @@ export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionTool { description: tool.description, parameters: jsonSchema, strict: true, // Always use strict mode since our schema converter handles the requirements - } satisfies FunctionTool + } satisfies FunctionToolConfig } diff --git a/packages/typescript/ai-openai/src/tools/image-generation-tool.ts b/packages/typescript/ai-openai/src/tools/image-generation-tool.ts index c48ff1e0e..9b3abb395 100644 --- a/packages/typescript/ai-openai/src/tools/image-generation-tool.ts +++ b/packages/typescript/ai-openai/src/tools/image-generation-tool.ts @@ -1,7 +1,15 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type ImageGenerationTool = OpenAI.Responses.Tool.ImageGeneration +export type ImageGenerationToolConfig = OpenAI.Responses.Tool.ImageGeneration + +/** @deprecated Renamed to `ImageGenerationToolConfig`. Will be removed in a future release. */ +export type ImageGenerationTool = ImageGenerationToolConfig + +export type OpenAIImageGenerationTool = ProviderTool< + 'openai', + 'image_generation' +> const validatePartialImages = (value: number | undefined) => { if (value !== undefined && (value < 0 || value > 3)) { @@ -14,8 +22,8 @@ const validatePartialImages = (value: number | undefined) => { */ export function convertImageGenerationToolToAdapterFormat( tool: Tool, -): ImageGenerationTool { - const metadata = tool.metadata as Omit +): ImageGenerationToolConfig { + const metadata = tool.metadata as Omit return { type: 'image_generation', ...metadata, @@ -26,14 +34,15 @@ export function convertImageGenerationToolToAdapterFormat( * Creates a standard Tool from ImageGenerationTool parameters */ export function imageGenerationTool( - toolData: Omit, -): Tool { + toolData: Omit, +): OpenAIImageGenerationTool { validatePartialImages(toolData.partial_images) + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'image_generation', description: 'Generate images based on text descriptions', metadata: { ...toolData, }, - } + } as unknown as OpenAIImageGenerationTool } diff --git a/packages/typescript/ai-openai/src/tools/index.ts b/packages/typescript/ai-openai/src/tools/index.ts index 1795d7fce..918f222e6 100644 --- a/packages/typescript/ai-openai/src/tools/index.ts +++ b/packages/typescript/ai-openai/src/tools/index.ts @@ -1,3 +1,6 @@ +// Keep the existing discriminated union defined inline. +// Built from the deprecated config-type aliases — matches the SDK shape that +// `convertToolsToProviderFormat` emits. import type { ApplyPatchTool } from './apply-patch-tool' import type { CodeInterpreterTool } from './code-interpreter-tool' import type { ComputerUseTool } from './computer-use-tool' @@ -25,17 +28,88 @@ export type OpenAITool = | WebSearchPreviewTool | WebSearchTool -export * from './apply-patch-tool' -export * from './code-interpreter-tool' -export * from './computer-use-tool' -export * from './custom-tool' -export * from './file-search-tool' -export * from './function-tool' -export * from './image-generation-tool' -export * from './local-shell-tool' -export * from './mcp-tool' -export * from './shell-tool' -export * from './tool-choice' -export * from './tool-converter' -export * from './web-search-preview-tool' -export * from './web-search-tool' +export { + applyPatchTool, + convertApplyPatchToolToAdapterFormat, + type OpenAIApplyPatchTool, + type ApplyPatchToolConfig, + type ApplyPatchTool, +} from './apply-patch-tool' +export { + codeInterpreterTool, + convertCodeInterpreterToolToAdapterFormat, + type OpenAICodeInterpreterTool, + type CodeInterpreterToolConfig, + type CodeInterpreterTool, +} from './code-interpreter-tool' +export { + computerUseTool, + convertComputerUseToolToAdapterFormat, + type OpenAIComputerUseTool, + type ComputerUseToolConfig, + type ComputerUseTool, +} from './computer-use-tool' +export { + customTool, + convertCustomToolToAdapterFormat, + type CustomToolConfig, + type CustomTool, +} from './custom-tool' +export { + fileSearchTool, + convertFileSearchToolToAdapterFormat, + type OpenAIFileSearchTool, + type FileSearchToolConfig, + type FileSearchTool, +} from './file-search-tool' +export { + convertFunctionToolToAdapterFormat, + type FunctionToolConfig, + type FunctionTool, +} from './function-tool' +export { + imageGenerationTool, + convertImageGenerationToolToAdapterFormat, + type OpenAIImageGenerationTool, + type ImageGenerationToolConfig, + type ImageGenerationTool, +} from './image-generation-tool' +export { + localShellTool, + convertLocalShellToolToAdapterFormat, + type OpenAILocalShellTool, + type LocalShellToolConfig, + type LocalShellTool, +} from './local-shell-tool' +export { + mcpTool, + validateMCPtool, + convertMCPToolToAdapterFormat, + type OpenAIMCPTool, + type MCPToolConfig, + type MCPTool, +} from './mcp-tool' +export { + shellTool, + convertShellToolToAdapterFormat, + type OpenAIShellTool, + type ShellToolConfig, + type ShellTool, +} from './shell-tool' +export { + webSearchPreviewTool, + convertWebSearchPreviewToolToAdapterFormat, + type OpenAIWebSearchPreviewTool, + type WebSearchPreviewToolConfig, + type WebSearchPreviewTool, +} from './web-search-preview-tool' +export { + webSearchTool, + convertWebSearchToolToAdapterFormat, + type OpenAIWebSearchTool, + type WebSearchToolConfig, + type WebSearchTool, +} from './web-search-tool' + +export { type ToolChoice } from './tool-choice' +export { convertToolsToProviderFormat } from './tool-converter' diff --git a/packages/typescript/ai-openai/src/tools/local-shell-tool.ts b/packages/typescript/ai-openai/src/tools/local-shell-tool.ts index ed829cb28..ce388ca3b 100644 --- a/packages/typescript/ai-openai/src/tools/local-shell-tool.ts +++ b/packages/typescript/ai-openai/src/tools/local-shell-tool.ts @@ -1,14 +1,19 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type LocalShellTool = OpenAI.Responses.Tool.LocalShell +export type LocalShellToolConfig = OpenAI.Responses.Tool.LocalShell + +/** @deprecated Renamed to `LocalShellToolConfig`. Will be removed in a future release. */ +export type LocalShellTool = LocalShellToolConfig + +export type OpenAILocalShellTool = ProviderTool<'openai', 'local_shell'> /** * Converts a standard Tool to OpenAI LocalShellTool format */ export function convertLocalShellToolToAdapterFormat( _tool: Tool, -): LocalShellTool { +): LocalShellToolConfig { return { type: 'local_shell', } @@ -17,10 +22,11 @@ export function convertLocalShellToolToAdapterFormat( /** * Creates a standard Tool from LocalShellTool parameters */ -export function localShellTool(): Tool { +export function localShellTool(): OpenAILocalShellTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'local_shell', description: 'Execute local shell commands', metadata: {}, - } + } as unknown as OpenAILocalShellTool } diff --git a/packages/typescript/ai-openai/src/tools/mcp-tool.ts b/packages/typescript/ai-openai/src/tools/mcp-tool.ts index 64b94357f..4f224c108 100644 --- a/packages/typescript/ai-openai/src/tools/mcp-tool.ts +++ b/packages/typescript/ai-openai/src/tools/mcp-tool.ts @@ -1,9 +1,14 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type MCPTool = OpenAI.Responses.Tool.Mcp +export type MCPToolConfig = OpenAI.Responses.Tool.Mcp -export function validateMCPtool(tool: MCPTool) { +/** @deprecated Renamed to `MCPToolConfig`. Will be removed in a future release. */ +export type MCPTool = MCPToolConfig + +export type OpenAIMCPTool = ProviderTool<'openai', 'mcp'> + +export function validateMCPtool(tool: MCPToolConfig) { if (!tool.server_url && !tool.connector_id) { throw new Error('Either server_url or connector_id must be provided.') } @@ -15,10 +20,10 @@ export function validateMCPtool(tool: MCPTool) { /** * Converts a standard Tool to OpenAI MCPTool format */ -export function convertMCPToolToAdapterFormat(tool: Tool): MCPTool { - const metadata = tool.metadata as Omit +export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig { + const metadata = tool.metadata as Omit - const mcpTool: MCPTool = { + const mcpTool: MCPToolConfig = { type: 'mcp', ...metadata, } @@ -30,12 +35,13 @@ export function convertMCPToolToAdapterFormat(tool: Tool): MCPTool { /** * Creates a standard Tool from MCPTool parameters */ -export function mcpTool(toolData: Omit): Tool { +export function mcpTool(toolData: Omit): OpenAIMCPTool { validateMCPtool({ ...toolData, type: 'mcp' }) + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'mcp', description: toolData.server_description || '', metadata: toolData, - } + } as unknown as OpenAIMCPTool } diff --git a/packages/typescript/ai-openai/src/tools/shell-tool.ts b/packages/typescript/ai-openai/src/tools/shell-tool.ts index 83b301a23..5fc4bdf65 100644 --- a/packages/typescript/ai-openai/src/tools/shell-tool.ts +++ b/packages/typescript/ai-openai/src/tools/shell-tool.ts @@ -1,12 +1,17 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type ShellTool = OpenAI.Responses.FunctionShellTool +export type ShellToolConfig = OpenAI.Responses.FunctionShellTool + +/** @deprecated Renamed to `ShellToolConfig`. Will be removed in a future release. */ +export type ShellTool = ShellToolConfig + +export type OpenAIShellTool = ProviderTool<'openai', 'shell'> /** * Converts a standard Tool to OpenAI ShellTool format */ -export function convertShellToolToAdapterFormat(_tool: Tool): ShellTool { +export function convertShellToolToAdapterFormat(_tool: Tool): ShellToolConfig { return { type: 'shell', } @@ -15,10 +20,11 @@ export function convertShellToolToAdapterFormat(_tool: Tool): ShellTool { /** * Creates a standard Tool from ShellTool parameters */ -export function shellTool(): Tool { +export function shellTool(): OpenAIShellTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'shell', description: 'Execute shell commands', metadata: {}, - } + } as unknown as OpenAIShellTool } diff --git a/packages/typescript/ai-openai/src/tools/web-search-preview-tool.ts b/packages/typescript/ai-openai/src/tools/web-search-preview-tool.ts index 48942d436..fb5163b5e 100644 --- a/packages/typescript/ai-openai/src/tools/web-search-preview-tool.ts +++ b/packages/typescript/ai-openai/src/tools/web-search-preview-tool.ts @@ -1,15 +1,23 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type WebSearchPreviewTool = OpenAI.Responses.WebSearchPreviewTool +export type WebSearchPreviewToolConfig = OpenAI.Responses.WebSearchPreviewTool + +/** @deprecated Renamed to `WebSearchPreviewToolConfig`. Will be removed in a future release. */ +export type WebSearchPreviewTool = WebSearchPreviewToolConfig + +export type OpenAIWebSearchPreviewTool = ProviderTool< + 'openai', + 'web_search_preview' +> /** * Converts a standard Tool to OpenAI WebSearchPreviewTool format */ export function convertWebSearchPreviewToolToAdapterFormat( tool: Tool, -): WebSearchPreviewTool { - const metadata = tool.metadata as WebSearchPreviewTool +): WebSearchPreviewToolConfig { + const metadata = tool.metadata as WebSearchPreviewToolConfig return { type: metadata.type, search_context_size: metadata.search_context_size, @@ -20,10 +28,13 @@ export function convertWebSearchPreviewToolToAdapterFormat( /** * Creates a standard Tool from WebSearchPreviewTool parameters */ -export function webSearchPreviewTool(toolData: WebSearchPreviewTool): Tool { +export function webSearchPreviewTool( + toolData: WebSearchPreviewToolConfig, +): OpenAIWebSearchPreviewTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'web_search_preview', description: 'Search the web (preview version)', metadata: toolData, - } + } as unknown as OpenAIWebSearchPreviewTool } diff --git a/packages/typescript/ai-openai/src/tools/web-search-tool.ts b/packages/typescript/ai-openai/src/tools/web-search-tool.ts index c7d5aef68..83991e9d3 100644 --- a/packages/typescript/ai-openai/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openai/src/tools/web-search-tool.ts @@ -1,23 +1,33 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' -export type WebSearchTool = OpenAI.Responses.WebSearchTool +export type WebSearchToolConfig = OpenAI.Responses.WebSearchTool + +/** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */ +export type WebSearchTool = WebSearchToolConfig + +export type OpenAIWebSearchTool = ProviderTool<'openai', 'web_search'> /** * Converts a standard Tool to OpenAI WebSearchTool format */ -export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchTool { - const metadata = tool.metadata as WebSearchTool +export function convertWebSearchToolToAdapterFormat( + tool: Tool, +): WebSearchToolConfig { + const metadata = tool.metadata as WebSearchToolConfig return metadata } /** * Creates a standard Tool from WebSearchTool parameters */ -export function webSearchTool(toolData: WebSearchTool): Tool { +export function webSearchTool( + toolData: WebSearchToolConfig, +): OpenAIWebSearchTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'web_search', description: 'Search the web', metadata: toolData, - } + } as unknown as OpenAIWebSearchTool } diff --git a/packages/typescript/ai-openai/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai-openai/tests/tools-per-model-type-safety.test.ts new file mode 100644 index 000000000..848a5ad37 --- /dev/null +++ b/packages/typescript/ai-openai/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,168 @@ +/** + * Per-model type-safety tests for OpenAI provider tools. + * + * Positive cases: each supported (model, tool) pair compiles cleanly. + * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. + */ +import { beforeAll, describe, it } from 'vitest' +import { z } from 'zod' +import { toolDefinition } from '@tanstack/ai' +import { openaiText } from '../src' +import { + applyPatchTool, + codeInterpreterTool, + computerUseTool, + customTool, + fileSearchTool, + imageGenerationTool, + localShellTool, + mcpTool, + shellTool, + webSearchPreviewTool, + webSearchTool, +} from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], +) { + return { adapter, tools } +} + +// Set a dummy API key so adapter construction does not throw at runtime. +// These tests only exercise compile-time type gating; no network calls are made. +beforeAll(() => { + process.env['OPENAI_API_KEY'] = 'sk-test-dummy' +}) + +// Minimal user tool — always assignable regardless of model. +const userTool = toolDefinition({ + name: 'echo', + description: 'echoes input', + inputSchema: z.object({ msg: z.string() }), +}).server(async ({ msg }) => msg) + +describe('OpenAI per-model tool gating', () => { + it('gpt-5.2 accepts the full tool superset', () => { + const adapter = openaiText('gpt-5.2') + typedTools(adapter, [ + userTool, + webSearchTool({ type: 'web_search' }), + webSearchPreviewTool({ type: 'web_search_preview' }), + fileSearchTool({ type: 'file_search', vector_store_ids: ['vs_123'] }), + imageGenerationTool({}), + codeInterpreterTool({ + type: 'code_interpreter', + container: { type: 'auto' }, + }), + mcpTool({ + server_label: 'my-server', + server_url: 'https://example.com/mcp', + }), + computerUseTool({ + type: 'computer_use_preview', + display_height: 768, + display_width: 1024, + environment: 'linux', + }), + localShellTool(), + shellTool(), + applyPatchTool(), + ]) + }) + + it('gpt-3.5-turbo rejects every provider tool; user-defined tool is still accepted', () => { + const adapter = openaiText('gpt-3.5-turbo') + typedTools(adapter, [ + userTool, + // @ts-expect-error - gpt-3.5-turbo does not support web_search + webSearchTool({ type: 'web_search' }), + // @ts-expect-error - gpt-3.5-turbo does not support web_search_preview + webSearchPreviewTool({ type: 'web_search_preview' }), + // @ts-expect-error - gpt-3.5-turbo does not support file_search + fileSearchTool({ type: 'file_search', vector_store_ids: ['vs_123'] }), + // @ts-expect-error - gpt-3.5-turbo does not support image_generation + imageGenerationTool({}), + // @ts-expect-error - gpt-3.5-turbo does not support code_interpreter + codeInterpreterTool({ + type: 'code_interpreter', + container: { type: 'auto' }, + }), + // @ts-expect-error - gpt-3.5-turbo does not support mcp + mcpTool({ + server_label: 'my-server', + 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', + }), + // @ts-expect-error - gpt-3.5-turbo does not support local_shell + localShellTool(), + // @ts-expect-error - gpt-3.5-turbo does not support shell + shellTool(), + // @ts-expect-error - gpt-3.5-turbo does not support apply_patch + applyPatchTool(), + ]) + }) + + it('customTool is accepted on any model (returns plain Tool, not a branded ProviderTool)', () => { + // Full-featured model + const fullAdapter = openaiText('gpt-5.2') + typedTools(fullAdapter, [ + customTool({ + type: 'custom', + name: 'lookup_order', + description: 'Look up an order', + }), + ]) + + // Restricted model — customTool must still compile without @ts-expect-error + const restrictedAdapter = openaiText('gpt-3.5-turbo') + typedTools(restrictedAdapter, [ + customTool({ + type: 'custom', + name: 'lookup_order', + description: 'Look up an order', + }), + ]) + }) + + it('gpt-4o accepts web_search, file_search, image_generation, code_interpreter, mcp but rejects the rest', () => { + const adapter = openaiText('gpt-4o') + typedTools(adapter, [ + userTool, + webSearchTool({ type: 'web_search' }), + fileSearchTool({ type: 'file_search', vector_store_ids: ['vs_456'] }), + imageGenerationTool({}), + codeInterpreterTool({ + type: 'code_interpreter', + container: { type: 'auto' }, + }), + mcpTool({ + server_label: 'my-server', + server_url: 'https://example.com/mcp', + }), + // @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', + }), + // @ts-expect-error - gpt-4o does not support local_shell + localShellTool(), + // @ts-expect-error - gpt-4o does not support shell + shellTool(), + // @ts-expect-error - gpt-4o does not support apply_patch + applyPatchTool(), + ]) + }) +}) diff --git a/packages/typescript/ai-openai/tsconfig.json b/packages/typescript/ai-openai/tsconfig.json index ea11c1096..0c50acadb 100644 --- a/packages/typescript/ai-openai/tsconfig.json +++ b/packages/typescript/ai-openai/tsconfig.json @@ -1,9 +1,12 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { - "outDir": "dist", - "rootDir": "src" + "outDir": "dist" }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": [ + "vite.config.ts", + "./src", + "tests/tools-per-model-type-safety.test.ts" + ], "exclude": ["node_modules", "dist", "**/*.config.ts"] } diff --git a/packages/typescript/ai-openai/vite.config.ts b/packages/typescript/ai-openai/vite.config.ts index 77bcc2e60..0e7e7eaea 100644 --- a/packages/typescript/ai-openai/vite.config.ts +++ b/packages/typescript/ai-openai/vite.config.ts @@ -29,7 +29,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/tools/index.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/ai-openrouter/package.json b/packages/typescript/ai-openrouter/package.json index 301adb549..30d0fb8ba 100644 --- a/packages/typescript/ai-openrouter/package.json +++ b/packages/typescript/ai-openrouter/package.json @@ -16,6 +16,10 @@ ".": { "types": "./dist/esm/index.d.ts", "import": "./dist/esm/index.js" + }, + "./tools": { + "types": "./dist/esm/tools/index.d.ts", + "import": "./dist/esm/tools/index.js" } }, "files": [ @@ -44,7 +48,8 @@ }, "devDependencies": { "@vitest/coverage-v8": "4.0.14", - "vite": "^7.2.7" + "vite": "^7.2.7", + "zod": "^4.2.0" }, "peerDependencies": { "@tanstack/ai": "workspace:*" diff --git a/packages/typescript/ai-openrouter/src/adapters/text.ts b/packages/typescript/ai-openrouter/src/adapters/text.ts index 15d0768f3..34be7df3c 100644 --- a/packages/typescript/ai-openrouter/src/adapters/text.ts +++ b/packages/typescript/ai-openrouter/src/adapters/text.ts @@ -10,6 +10,7 @@ import { import type { SDKOptions } from '@openrouter/sdk' import type { OPENROUTER_CHAT_MODELS, + OpenRouterChatModelToolCapabilitiesByName, OpenRouterModelInputModalitiesByName, OpenRouterModelOptionsByName, } from '../model-meta' @@ -56,6 +57,11 @@ type ResolveInputModalities = ? OpenRouterModelInputModalitiesByName[TModel] : readonly ['text', 'image'] +type ResolveToolCapabilities = + TModel extends keyof OpenRouterChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + // Internal buffer for accumulating streamed tool calls interface ToolCallBuffer { id: string @@ -85,11 +91,14 @@ interface AGUIState { export class OpenRouterTextAdapter< TModel extends OpenRouterTextModels, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, ResolveProviderOptions, ResolveInputModalities, - OpenRouterMessageMetadataByModality + OpenRouterMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const readonly name = 'openrouter' as const @@ -766,14 +775,14 @@ export function createOpenRouterText( model: TModel, apiKey: string, config?: Omit, -): OpenRouterTextAdapter { +): OpenRouterTextAdapter> { return new OpenRouterTextAdapter({ apiKey, ...config }, model) } export function openRouterText( model: TModel, config?: Omit, -): OpenRouterTextAdapter { +): OpenRouterTextAdapter> { const apiKey = getOpenRouterApiKeyFromEnv() return createOpenRouterText(model, apiKey, config) } diff --git a/packages/typescript/ai-openrouter/src/index.ts b/packages/typescript/ai-openrouter/src/index.ts index 8823c73b3..e17844743 100644 --- a/packages/typescript/ai-openrouter/src/index.ts +++ b/packages/typescript/ai-openrouter/src/index.ts @@ -40,6 +40,7 @@ export type { export type { OpenRouterModelOptionsByName, OpenRouterModelInputModalitiesByName, + OpenRouterChatModelToolCapabilitiesByName, } from './model-meta' export type { OpenRouterTextMetadata, @@ -79,6 +80,5 @@ export { // ============================================================================ export { convertToolsToProviderFormat } from './tools/tool-converter' -export { createWebSearchTool } from './tools/web-search-tool' -export type { OpenRouterTool, FunctionTool, WebSearchTool } from './tools' +export type { OpenRouterTool, FunctionTool } from './tools' diff --git a/packages/typescript/ai-openrouter/src/model-meta.ts b/packages/typescript/ai-openrouter/src/model-meta.ts index d08296983..3961ce794 100644 --- a/packages/typescript/ai-openrouter/src/model-meta.ts +++ b/packages/typescript/ai-openrouter/src/model-meta.ts @@ -15606,6 +15606,13 @@ export const OPENROUTER_CHAT_MODELS = [ Z_AI_GLM_5V_TURBO.id, ] as const +// OpenRouter's web_search plugin works across all chat models via the gateway. +// A mapped type assigns the capability uniformly without touching each of the +// 345 model constants. +export type OpenRouterChatModelToolCapabilitiesByName = { + [K in (typeof OPENROUTER_CHAT_MODELS)[number]]: readonly ['web_search'] +} + export const OPENROUTER_IMAGE_MODELS = [ GOOGLE_GEMINI_2_5_FLASH_IMAGE.id, GOOGLE_GEMINI_3_PRO_IMAGE_PREVIEW.id, diff --git a/packages/typescript/ai-openrouter/src/tools/index.ts b/packages/typescript/ai-openrouter/src/tools/index.ts index 497823f61..a312565cd 100644 --- a/packages/typescript/ai-openrouter/src/tools/index.ts +++ b/packages/typescript/ai-openrouter/src/tools/index.ts @@ -1,8 +1,13 @@ -export type { OpenRouterTool } from './tool-converter' +export { + webSearchTool, + convertWebSearchToolToAdapterFormat, + type OpenRouterWebSearchTool, + type WebSearchToolConfig, + type WebSearchTool, +} from './web-search-tool' +export { + type FunctionTool, + convertFunctionToolToAdapterFormat, +} from './function-tool' +export { type OpenRouterTool } from './tool-converter' export { convertToolsToProviderFormat } from './tool-converter' - -export type { FunctionTool } from './function-tool' -export { convertFunctionToolToAdapterFormat } from './function-tool' - -export type { WebSearchTool } from './web-search-tool' -export { createWebSearchTool } from './web-search-tool' diff --git a/packages/typescript/ai-openrouter/src/tools/tool-converter.ts b/packages/typescript/ai-openrouter/src/tools/tool-converter.ts index 8bed413f0..0631330b9 100644 --- a/packages/typescript/ai-openrouter/src/tools/tool-converter.ts +++ b/packages/typescript/ai-openrouter/src/tools/tool-converter.ts @@ -1,11 +1,23 @@ import { convertFunctionToolToAdapterFormat } from './function-tool' +import { + convertWebSearchToolToAdapterFormat, + isWebSearchTool, +} from './web-search-tool' import type { Tool } from '@tanstack/ai' import type { FunctionTool } from './function-tool' +import type { WebSearchToolConfig } from './web-search-tool' -export type OpenRouterTool = FunctionTool +export type OpenRouterTool = FunctionTool | WebSearchToolConfig export function convertToolsToProviderFormat( tools: Array, ): Array { - return tools.map((tool) => convertFunctionToolToAdapterFormat(tool)) + return tools.map((tool) => { + // Dispatch on the stable `__kind` brand set by webSearchTool() — not on + // `tool.name`, which a user can reuse with toolDefinition(). + if (isWebSearchTool(tool)) { + return convertWebSearchToolToAdapterFormat(tool) + } + return convertFunctionToolToAdapterFormat(tool) + }) } diff --git a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts index 8b0e32985..59bc27a74 100644 --- a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts @@ -1,4 +1,13 @@ -export interface WebSearchTool { +import type { ProviderTool, Tool } from '@tanstack/ai' + +/** + * Stable runtime marker used to identify a `webSearchTool()`-created tool so + * `convertToolsToProviderFormat` can route it without relying on the mutable + * public `tool.name`. + */ +export const WEB_SEARCH_TOOL_KIND = 'openrouter.web_search' + +export interface WebSearchToolConfig { type: 'web_search' web_search: { engine?: 'native' | 'exa' @@ -7,17 +16,74 @@ export interface WebSearchTool { } } -export function createWebSearchTool(options?: { +/** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */ +export type WebSearchTool = WebSearchToolConfig + +export type OpenRouterWebSearchTool = ProviderTool<'openrouter', 'web_search'> + +/** A tool is a webSearchTool() output iff its metadata carries our branded kind marker. */ +export function isWebSearchTool(tool: Tool): boolean { + const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind + return kind === WEB_SEARCH_TOOL_KIND +} + +/** + * Converts a branded web-search tool to OpenRouter's wire format. Throws if + * the metadata doesn't match the expected shape — callers must gate on + * `isWebSearchTool()` first. + */ +export function convertWebSearchToolToAdapterFormat( + tool: Tool, +): WebSearchToolConfig { + const metadata = tool.metadata as + | { + __kind?: unknown + type?: unknown + web_search?: unknown + } + | undefined + if ( + !metadata || + metadata.__kind !== WEB_SEARCH_TOOL_KIND || + metadata.type !== 'web_search' || + typeof metadata.web_search !== 'object' || + metadata.web_search === null || + Array.isArray(metadata.web_search) + ) { + throw new Error( + `convertWebSearchToolToAdapterFormat: tool "${tool.name}" is not a valid webSearchTool() output (missing branded metadata).`, + ) + } + return { + type: 'web_search', + web_search: metadata.web_search as WebSearchToolConfig['web_search'], + } +} + +/** + * Creates a branded web search tool for use with OpenRouter models. + * + * The web search tool is available across all OpenRouter chat models via the + * OpenRouter gateway. Pass the returned value in the `tools` array when calling + * a chat function. + */ +export function webSearchTool(options?: { engine?: 'native' | 'exa' maxResults?: number searchPrompt?: string -}): WebSearchTool { +}): OpenRouterWebSearchTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { - type: 'web_search', - web_search: { - engine: options?.engine, - max_results: options?.maxResults, - search_prompt: options?.searchPrompt, + name: 'web_search', + description: '', + metadata: { + __kind: WEB_SEARCH_TOOL_KIND, + type: 'web_search' as const, + web_search: { + engine: options?.engine, + max_results: options?.maxResults, + search_prompt: options?.searchPrompt, + }, }, - } + } as unknown as OpenRouterWebSearchTool } diff --git a/packages/typescript/ai-openrouter/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai-openrouter/tests/tools-per-model-type-safety.test.ts new file mode 100644 index 000000000..774326c67 --- /dev/null +++ b/packages/typescript/ai-openrouter/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,65 @@ +/** + * Per-model type-safety tests for OpenRouter provider tools. + * + * Positive cases: each supported (model, tool) pair compiles cleanly. + * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. + */ +import { beforeAll, describe, it } from 'vitest' +import { z } from 'zod' +import { toolDefinition } from '@tanstack/ai' +import { openRouterText } from '../src' +import { webSearchTool } from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' +import type { ProviderTool } from '@tanstack/ai' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], +) { + return { adapter, tools } +} + +// Set a dummy API key so adapter construction does not throw at runtime. +// These tests only exercise compile-time type gating; no network calls are made. +beforeAll(() => { + process.env['OPENROUTER_API_KEY'] = 'sk-or-test-dummy' +}) + +// Minimal user tool — always assignable regardless of model. +const userTool = toolDefinition({ + name: 'echo', + description: 'echoes input', + inputSchema: z.object({ msg: z.string() }), +}).server(async ({ msg }) => msg) + +describe('OpenRouter per-model tool gating', () => { + it('anthropic/claude-opus-4.6 accepts webSearchTool and user-defined tools', () => { + const adapter = openRouterText('anthropic/claude-opus-4.6') + typedTools(adapter, [ + userTool, + webSearchTool({ engine: 'native', maxResults: 5 }), + ]) + }) + + it('openai/gpt-4o accepts webSearchTool and user-defined tools', () => { + const adapter = openRouterText('openai/gpt-4o') + typedTools(adapter, [ + userTool, + webSearchTool({ engine: 'exa', searchPrompt: 'latest news' }), + ]) + }) + + it('rejects provider tools with kinds not in supports.tools', () => { + const adapter = openRouterText('anthropic/claude-opus-4.6') + const fakeTool = { + name: 'code_execution', + description: '', + metadata: {}, + } as ProviderTool<'openrouter', 'code_execution'> + typedTools(adapter, [ + // @ts-expect-error - 'code_execution' is not in openrouter's supports.tools + fakeTool, + ]) + }) +}) diff --git a/packages/typescript/ai-openrouter/vite.config.ts b/packages/typescript/ai-openrouter/vite.config.ts index 77bcc2e60..0e7e7eaea 100644 --- a/packages/typescript/ai-openrouter/vite.config.ts +++ b/packages/typescript/ai-openrouter/vite.config.ts @@ -29,7 +29,7 @@ const config = defineConfig({ export default mergeConfig( config, tanstackViteConfig({ - entry: ['./src/index.ts'], + entry: ['./src/index.ts', './src/tools/index.ts'], srcDir: './src', cjs: false, }), diff --git a/packages/typescript/ai/src/activities/chat/adapter.ts b/packages/typescript/ai/src/activities/chat/adapter.ts index ac7e96e33..7794314a6 100644 --- a/packages/typescript/ai/src/activities/chat/adapter.ts +++ b/packages/typescript/ai/src/activities/chat/adapter.ts @@ -48,12 +48,14 @@ export interface StructuredOutputResult { * - TProviderOptions: Provider-specific options for this model (already resolved) * - TInputModalities: Supported input modalities for this model (already resolved) * - TMessageMetadata: Metadata types for content parts (already resolved) + * - TToolCapabilities: Tuple of tool-kind strings supported by this model, resolved from `supports.tools` */ export interface TextAdapter< TModel extends string, TProviderOptions extends Record, TInputModalities extends ReadonlyArray, TMessageMetadataByModality extends DefaultMessageMetadataByModality, + TToolCapabilities extends ReadonlyArray = ReadonlyArray, > { /** Discriminator for adapter kind */ readonly kind: 'text' @@ -69,6 +71,7 @@ export interface TextAdapter< providerOptions: TProviderOptions inputModalities: TInputModalities messageMetadataByModality: TMessageMetadataByModality + toolCapabilities: TToolCapabilities } /** @@ -95,7 +98,7 @@ export interface TextAdapter< * A TextAdapter with any/unknown type parameters. * Useful as a constraint in generic functions and interfaces. */ -export type AnyTextAdapter = TextAdapter +export type AnyTextAdapter = TextAdapter /** * Abstract base class for text adapters. @@ -108,11 +111,13 @@ export abstract class BaseTextAdapter< TProviderOptions extends Record, TInputModalities extends ReadonlyArray, TMessageMetadataByModality extends DefaultMessageMetadataByModality, + TToolCapabilities extends ReadonlyArray = ReadonlyArray, > implements TextAdapter< TModel, TProviderOptions, TInputModalities, - TMessageMetadataByModality + TMessageMetadataByModality, + TToolCapabilities > { readonly kind = 'text' as const abstract readonly name: string @@ -123,6 +128,7 @@ export abstract class BaseTextAdapter< providerOptions: TProviderOptions inputModalities: TInputModalities messageMetadataByModality: TMessageMetadataByModality + toolCapabilities: TToolCapabilities } protected config: TextAdapterConfig diff --git a/packages/typescript/ai/src/activities/chat/index.ts b/packages/typescript/ai/src/activities/chat/index.ts index c87856630..574c7186d 100644 --- a/packages/typescript/ai/src/activities/chat/index.ts +++ b/packages/typescript/ai/src/activities/chat/index.ts @@ -51,6 +51,7 @@ import type { ChatMiddlewareContext, ChatMiddlewarePhase, } from './middleware/types' +import type { ProviderTool } from '../../tools/provider-tool' // =========================== // Activity Kind @@ -87,8 +88,20 @@ export interface TextActivityOptions< > /** System prompts to prepend to the conversation */ systemPrompts?: TextOptions['systemPrompts'] - /** Tools for function calling (auto-executed when called) */ - tools?: TextOptions['tools'] + /** + * Tools for function calling (auto-executed when called). + * + * Accepts two shapes: + * - User-defined tools via `toolDefinition()` — plain `Tool`, always assignable. + * - Provider tools from `@tanstack/ai-/tools` (e.g. `webSearchTool`) + * — branded and type-checked against the selected model's + * `supports.tools` list. Passing an unsupported tool produces a + * compile-time error on the array element. + */ + tools?: Array< + | (Tool & { readonly '~toolKind'?: never }) + | ProviderTool + > /** Controls the randomness of the output. Higher values make output more random. Range: [0.0, 2.0] */ temperature?: TextOptions['temperature'] /** Nucleus sampling parameter. The model considers tokens with topP probability mass. */ diff --git a/packages/typescript/ai/src/index.ts b/packages/typescript/ai/src/index.ts index 34e1b5922..2a99a8493 100644 --- a/packages/typescript/ai/src/index.ts +++ b/packages/typescript/ai/src/index.ts @@ -63,6 +63,9 @@ export { // Tool call management export { ToolCallManager } from './activities/chat/tools/tool-calls' +// Provider tool type +export type { ProviderTool } from './tools/provider-tool' + // Agent loop strategies export { maxIterations, diff --git a/packages/typescript/ai/src/tools/provider-tool.ts b/packages/typescript/ai/src/tools/provider-tool.ts new file mode 100644 index 000000000..780ee106c --- /dev/null +++ b/packages/typescript/ai/src/tools/provider-tool.ts @@ -0,0 +1,25 @@ +import type { Tool } from '../types' + +/** + * A provider-specific tool produced by an adapter-package factory + * (e.g. `webSearchTool` from `@tanstack/ai-anthropic/tools`). + * + * The two `~`-prefixed fields are type-only phantom brands — they are never + * assigned at runtime. They allow the core type system to match a factory's + * output against the selected model's `supports.tools` list and surface a + * compile-time error when the combination is unsupported. + * + * User-defined tools (via `toolDefinition()`) remain plain `Tool` and stay + * assignable to any model. + * + * @template TProvider - Provider identifier (e.g. `'anthropic'`, `'openai'`). + * @template TKind - Canonical tool-kind string matching the provider's + * `supports.tools` entries (e.g. `'web_search'`, `'code_execution'`). + */ +export interface ProviderTool< + TProvider extends string, + TKind extends string, +> extends Tool { + readonly '~provider': TProvider + readonly '~toolKind': TKind +} diff --git a/packages/typescript/ai/tests/test-utils.ts b/packages/typescript/ai/tests/test-utils.ts index 879db40f8..523480648 100644 --- a/packages/typescript/ai/tests/test-utils.ts +++ b/packages/typescript/ai/tests/test-utils.ts @@ -107,6 +107,7 @@ export function createMockAdapter(options: { video: undefined as unknown, document: undefined as unknown, }, + toolCapabilities: [] as ReadonlyArray, }, chatStream: (opts: any) => { calls.push(opts) diff --git a/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts new file mode 100644 index 000000000..7e7900c18 --- /dev/null +++ b/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,82 @@ +/** + * Type-safety tests for TextActivityOptions['tools'] gating. + * + * Mirrors the pattern in image-per-model-type-safety.test.ts — uses + * `@ts-expect-error` to assert compile-time rejections and `expectTypeOf` + * for positive inference checks. + */ +import { describe, expectTypeOf, it } from 'vitest' +import { z } from 'zod' +import { toolDefinition } from '../src/index' +import type { ProviderTool } from '../src/index' +import type { TextActivityOptions } from '../src/activities/chat/index' +import type { TextAdapter } from '../src/activities/chat/adapter' + +// ---- Mock adapter wired with a fixed toolCapabilities union ---- + +type MockToolCapabilities = readonly ['web_search', 'code_execution'] + +type MockAdapter = TextAdapter< + 'mock-model', + Record, + readonly ['text'], + { text: {}; image: {}; audio: {}; video: {}; document: {} }, + MockToolCapabilities +> + +// Helper that exposes the gated `tools` element type. +type MockToolsOption = NonNullable< + TextActivityOptions['tools'] +>[number] + +// ---- Fixtures ---- + +const userTool = toolDefinition({ + name: 'user_tool', + description: 'A plain user-defined tool', + inputSchema: z.object({ query: z.string() }), +}).server(async ({ query }) => query.toUpperCase()) + +const supportedProviderTool = { + name: 'web_search', + description: '', + metadata: {}, +} as ProviderTool<'mock', 'web_search'> + +const unsupportedProviderTool = { + name: 'computer_use', + description: '', + metadata: {}, +} as ProviderTool<'mock', 'computer_use'> + +describe('TextActivityOptions["tools"] type gating', () => { + it('accepts user-defined tools from toolDefinition()', () => { + expectTypeOf(userTool).toMatchTypeOf() + }) + + it('accepts provider tools whose kind appears in supports.tools', () => { + expectTypeOf(supportedProviderTool).toMatchTypeOf() + }) + + it('rejects provider tools whose kind is not in supports.tools', () => { + // @ts-expect-error - 'computer_use' is not in MockToolCapabilities + const _tools: Array = [unsupportedProviderTool] + void _tools + }) + + it('rejects provider tools with broad string TKind', () => { + const broadTool = { + name: 'x', + description: '', + metadata: {}, + } as ProviderTool<'x', string> + // @ts-expect-error - broad `string` TKind is not assignable to the model's specific toolCapabilities union + const _tools: Array = [broadTool] + void _tools + }) + + it('accepts a mixed array of user tools and supported provider tools', () => { + const tools: Array = [userTool, supportedProviderTool] + expectTypeOf(tools).items.toMatchTypeOf() + }) +}) diff --git a/packages/typescript/ai/tests/type-check.test.ts b/packages/typescript/ai/tests/type-check.test.ts index 124710beb..acb064216 100644 --- a/packages/typescript/ai/tests/type-check.test.ts +++ b/packages/typescript/ai/tests/type-check.test.ts @@ -35,6 +35,7 @@ const mockAdapter = { video: undefined as unknown, document: undefined as unknown, }, + toolCapabilities: [] as ReadonlyArray, }, chatStream: async function* () {}, structuredOutput: async () => ({ data: {}, rawText: '{}' }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e5732b0c..447f731e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1055,6 +1055,9 @@ importers: vite: specifier: ^7.2.7 version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + zod: + specifier: ^4.2.0 + version: 4.3.6 packages/typescript/ai-grok: dependencies: @@ -1195,6 +1198,9 @@ importers: vite: specifier: ^7.2.7 version: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + zod: + specifier: ^4.2.0 + version: 4.3.6 packages/typescript/ai-preact: dependencies: @@ -15253,7 +15259,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.0.1)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.3.1(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + vitest: 4.1.4(@types/node@25.0.1)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -15283,13 +15289,13 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@4.1.4(vite@7.3.1(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.4(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.14': dependencies: @@ -21289,10 +21295,10 @@ snapshots: - tsx - yaml - vitest@4.1.4(@types/node@25.0.1)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.3.1(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): + vitest@4.1.4(@types/node@25.0.1)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.9))(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@7.3.1(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 4.1.4(vite@7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -21309,7 +21315,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 7.3.1(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.2.7(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.0.1 diff --git a/scripts/sync-provider-models.ts b/scripts/sync-provider-models.ts index 04afa1525..5f0199cc7 100644 --- a/scripts/sync-provider-models.ts +++ b/scripts/sync-provider-models.ts @@ -72,7 +72,7 @@ const PROVIDER_MAP: Record = { referenceSupportsBody: ` output: ['text'], endpoints: ['chat', 'chat-completions'], features: ['streaming', 'function_calling', 'structured_outputs', 'distillation'], - tools: ['web_search', 'file_search', 'image_generation', 'code_interpreter', 'mcp'],`, + tools: ['web_search', 'web_search_preview', 'file_search', 'image_generation', 'code_interpreter', 'mcp', 'computer_use', 'local_shell', 'shell', 'apply_patch'],`, referenceSatisfies: 'ModelMeta', referenceProviderOptionsEntry: @@ -100,7 +100,8 @@ const PROVIDER_MAP: Record = { inputModalitiesTypeName: 'AnthropicModelInputModalitiesByName', validInputModalities: ['text', 'image', 'audio', 'video', 'document'], referenceSupportsBody: ` extended_thinking: true, - priority_tier: true,`, + priority_tier: true, + tools: ['web_search', 'web_fetch', 'code_execution', 'computer_use', 'bash', 'text_editor', 'memory'],`, referenceSatisfies: 'ModelMeta', referenceProviderOptionsEntry: @@ -119,7 +120,8 @@ const PROVIDER_MAP: Record = { inputModalitiesTypeName: 'GeminiModelInputModalitiesByName', validInputModalities: ['text', 'image', 'audio', 'video', 'document'], referenceSupportsBody: ` output: ['text'], - capabilities: ['batch_api', 'caching', 'code_execution', 'file_search', 'function_calling', 'search_grounding', 'structured_output', 'thinking', 'url_context'],`, + capabilities: ['batch_api', 'caching', 'function_calling', 'structured_output', 'thinking'], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'],`, referenceSatisfies: 'ModelMeta', referenceProviderOptionsEntry: @@ -140,7 +142,8 @@ const PROVIDER_MAP: Record = { inputModalitiesTypeName: 'GrokModelInputModalitiesByName', validInputModalities: ['text', 'image', 'audio', 'video', 'document'], referenceSupportsBody: ` output: ['text'], - capabilities: ['reasoning', 'structured_outputs', 'tool_calling'],`, + capabilities: ['reasoning', 'structured_outputs', 'tool_calling'], + tools: [],`, referenceSatisfies: 'ModelMeta', referenceProviderOptionsEntry: 'GrokProviderOptions', hasBothNameAndId: false,