From 3887fb5b051bb0331fd5e4d3c097278abfe2a1cf Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 12:34:46 +0200 Subject: [PATCH 01/49] feat(ai): add ProviderTool phantom-branded subtype --- packages/typescript/ai/src/index.ts | 3 +++ .../typescript/ai/src/tools/provider-tool.ts | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 packages/typescript/ai/src/tools/provider-tool.ts 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 +} From de743dc9cb6b281cddc08b4b27de6f920d8c0fe3 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 12:40:45 +0200 Subject: [PATCH 02/49] feat(ai): add toolCapabilities to TextAdapter type channel --- packages/typescript/ai/src/activities/chat/adapter.ts | 10 ++++++++-- packages/typescript/ai/tests/test-utils.ts | 1 + packages/typescript/ai/tests/type-check.test.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) 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/tests/test-utils.ts b/packages/typescript/ai/tests/test-utils.ts index 380ca3ac9..d9129461f 100644 --- a/packages/typescript/ai/tests/test-utils.ts +++ b/packages/typescript/ai/tests/test-utils.ts @@ -92,6 +92,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/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: '{}' }), From 561aa9f34948672472d7db29b6c076f219d50834 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 12:47:06 +0200 Subject: [PATCH 03/49] feat(ai): gate TextActivityOptions.tools on model toolCapabilities --- .../typescript/ai/src/activities/chat/index.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/typescript/ai/src/activities/chat/index.ts b/packages/typescript/ai/src/activities/chat/index.ts index 24cc41529..ed9e6879a 100644 --- a/packages/typescript/ai/src/activities/chat/index.ts +++ b/packages/typescript/ai/src/activities/chat/index.ts @@ -50,6 +50,7 @@ import type { ChatMiddlewareContext, ChatMiddlewarePhase, } from './middleware/types' +import type { ProviderTool } from '../../tools/provider-tool' // =========================== // Activity Kind @@ -86,8 +87,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 + | 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. */ From bec006fcdbefe014ceedb03ab342ec4969f0872d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 12:59:08 +0200 Subject: [PATCH 04/49] fix(ai): prevent ProviderTool subsumption in TextActivityOptions.tools union --- packages/typescript/ai/src/activities/chat/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript/ai/src/activities/chat/index.ts b/packages/typescript/ai/src/activities/chat/index.ts index ed9e6879a..d6a5542f0 100644 --- a/packages/typescript/ai/src/activities/chat/index.ts +++ b/packages/typescript/ai/src/activities/chat/index.ts @@ -98,7 +98,7 @@ export interface TextActivityOptions< * compile-time error on the array element. */ tools?: Array< - | Tool + | (Tool & { readonly '~toolKind'?: never }) | ProviderTool > /** Controls the randomness of the output. Higher values make output more random. Range: [0.0, 2.0] */ From 49b39da26c2bfeb10ced4111b72d74327e581435 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:00:53 +0200 Subject: [PATCH 05/49] test(ai): type tests for TextActivityOptions.tools gating --- .../tests/tools-per-model-type-safety.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/typescript/ai/tests/tools-per-model-type-safety.test.ts 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..aa051badf --- /dev/null +++ b/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,71 @@ +/** + * 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, it, expectTypeOf } from 'vitest' +import type { ProviderTool } from '../src/index' +import type { TextActivityOptions } from '../src/activities/chat/index' +import type { TextAdapter } from '../src/activities/chat/adapter' +import { toolDefinition } from '../src/index' +import { z } from 'zod' + +// ---- 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('accepts a mixed array of user tools and supported provider tools', () => { + const tools: Array = [userTool, supportedProviderTool] + expectTypeOf(tools).items.toMatchTypeOf() + }) +}) From cf8ff8e0694efaeca6056ccc823150c96e7fffdd Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:08:20 +0200 Subject: [PATCH 06/49] test(ai): fix import order and cover broad-TKind ProviderTool rejection --- .../ai/tests/tools-per-model-type-safety.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 index aa051badf..582cad847 100644 --- a/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts +++ b/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts @@ -6,11 +6,11 @@ * for positive inference checks. */ import { describe, it, expectTypeOf } from 'vitest' +import { z } from 'zod' import type { ProviderTool } from '../src/index' import type { TextActivityOptions } from '../src/activities/chat/index' import type { TextAdapter } from '../src/activities/chat/adapter' import { toolDefinition } from '../src/index' -import { z } from 'zod' // ---- Mock adapter wired with a fixed toolCapabilities union ---- @@ -64,6 +64,17 @@ describe('TextActivityOptions["tools"] type gating', () => { 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() From 6f86c1c124eae76d74f57bf380f03d8f1fbfb2b5 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:22:32 +0200 Subject: [PATCH 07/49] feat(ai-anthropic): add supports.tools and ToolCapabilitiesByName map --- .../typescript/ai-anthropic/src/model-meta.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/packages/typescript/ai-anthropic/src/model-meta.ts b/packages/typescript/ai-anthropic/src/model-meta.ts index e0781c3d0..2a86b1e07 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?: ReadonlyArray< + | '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. From e7b0565d52f8805f61744a80876f1b47f9d41357 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:28:11 +0200 Subject: [PATCH 08/49] refactor(ai-anthropic): use Array for supports.tools to match supports.input --- packages/typescript/ai-anthropic/src/model-meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript/ai-anthropic/src/model-meta.ts b/packages/typescript/ai-anthropic/src/model-meta.ts index 2a86b1e07..768be1c42 100644 --- a/packages/typescript/ai-anthropic/src/model-meta.ts +++ b/packages/typescript/ai-anthropic/src/model-meta.ts @@ -21,7 +21,7 @@ interface ModelMeta< extended_thinking?: boolean adaptive_thinking?: boolean priority_tier?: boolean - tools?: ReadonlyArray< + tools?: Array< | 'web_search' | 'web_fetch' | 'code_execution' From 5e679b039497e314a0e250046b7db4600038bb6d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:32:46 +0200 Subject: [PATCH 09/49] feat(ai-anthropic): brand provider tool factory return types --- .../ai-anthropic/src/tools/bash-tool.ts | 19 ++++++++++++------ .../src/tools/code-execution-tool.ts | 18 +++++++++++------ .../src/tools/computer-use-tool.ts | 18 +++++++++++------ .../ai-anthropic/src/tools/custom-tool.ts | 17 ++++++++++------ .../ai-anthropic/src/tools/memory-tool.ts | 18 +++++++++++------ .../src/tools/text-editor-tool.ts | 18 +++++++++++------ .../ai-anthropic/src/tools/web-fetch-tool.ts | 20 ++++++++++++------- .../ai-anthropic/src/tools/web-search-tool.ts | 20 ++++++++++++------- 8 files changed, 98 insertions(+), 50 deletions(-) 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..5f52205b0 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,29 @@ 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..7cdddf025 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,29 @@ 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..de259bd6c 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 { JSONSchema, ProviderTool, 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,15 @@ 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 type AnthropicCustomTool = ProviderTool<'anthropic', 'custom'> + +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: {}, @@ -54,7 +58,8 @@ export function customTool( description: string, inputSchema: SchemaInput, cacheControl?: CacheControl | null, -): Tool { +): AnthropicCustomTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name, description, @@ -62,5 +67,5 @@ export function customTool( metadata: { cacheControl, }, - } + } as unknown as AnthropicCustomTool } 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..59ad6ed98 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,32 @@ 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..7ca4173c4 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,15 @@ 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 +18,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..ea7722021 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,7 @@ 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 +75,13 @@ 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 } From 401a542efb81c52ffb95b31d50abc66ecd1513a4 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:44:09 +0200 Subject: [PATCH 10/49] refactor(ai-anthropic): replace tools barrel with named re-exports --- .../ai-anthropic/src/tools/index.ts | 62 +++++++++++++++---- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/packages/typescript/ai-anthropic/src/tools/index.ts b/packages/typescript/ai-anthropic/src/tools/index.ts index 5012b2b5f..4202cfecc 100644 --- a/packages/typescript/ai-anthropic/src/tools/index.ts +++ b/packages/typescript/ai-anthropic/src/tools/index.ts @@ -7,6 +7,56 @@ 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 AnthropicCustomTool, + 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 +66,3 @@ export type AnthropicTool = | TextEditorTool | WebFetchTool | WebSearchTool - -// Export individual tool types -export type { - // BashTool, - // CodeExecutionTool, - // ComputerUseTool, - CustomTool, - // MemoryTool, - // TextEditorTool, - // WebFetchTool, - // WebSearchTool, -} From 801255b72db6e20a4e5648643c276501c0202b74 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:48:30 +0200 Subject: [PATCH 11/49] fix(ai-anthropic): build AnthropicTool union from branded types --- .../ai-anthropic/src/adapters/text.ts | 14 ++++++-- .../ai-anthropic/src/tools/index.ts | 32 +++++++++---------- .../ai-anthropic/src/tools/tool-converter.ts | 16 +++++----- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts index 235d9f5b5..e4a869efd 100644 --- a/packages/typescript/ai-anthropic/src/adapters/text.ts +++ b/packages/typescript/ai-anthropic/src/adapters/text.ts @@ -122,7 +122,15 @@ export class AnthropicTextAdapter< const requestParams = this.mapCommonOptionsToAnthropic(options) const stream = await this.client.beta.messages.create( - { ...requestParams, stream: true }, + { + ...requestParams, + stream: true, + tools: requestParams.tools + ? (requestParams.tools as unknown as Parameters< + typeof this.client.beta.messages.create + >[0]['tools']) + : undefined, + }, { signal: options.request?.signal, headers: options.request?.headers, @@ -178,7 +186,9 @@ export class AnthropicTextAdapter< { ...requestParams, stream: false, - tools: [structuredOutputTool], + tools: [structuredOutputTool] as unknown as Parameters< + typeof this.client.messages.create + >[0]['tools'], tool_choice: { type: 'tool', name: 'structured_output' }, }, { diff --git a/packages/typescript/ai-anthropic/src/tools/index.ts b/packages/typescript/ai-anthropic/src/tools/index.ts index 4202cfecc..fa8ba7679 100644 --- a/packages/typescript/ai-anthropic/src/tools/index.ts +++ b/packages/typescript/ai-anthropic/src/tools/index.ts @@ -1,11 +1,11 @@ -import type { BashTool } from './bash-tool' -import type { CodeExecutionTool } from './code-execution-tool' -import type { ComputerUseTool } from './computer-use-tool' -import type { CustomTool } from './custom-tool' -import type { MemoryTool } from './memory-tool' -import type { TextEditorTool } from './text-editor-tool' -import type { WebFetchTool } from './web-fetch-tool' -import type { WebSearchTool } from './web-search-tool' +import type { AnthropicBashTool } from './bash-tool' +import type { AnthropicCodeExecutionTool } from './code-execution-tool' +import type { AnthropicComputerUseTool } from './computer-use-tool' +import type { AnthropicCustomTool } from './custom-tool' +import type { AnthropicMemoryTool } from './memory-tool' +import type { AnthropicTextEditorTool } from './text-editor-tool' +import type { AnthropicWebFetchTool } from './web-fetch-tool' +import type { AnthropicWebSearchTool } from './web-search-tool' export { bashTool, @@ -58,11 +58,11 @@ export { // Keep the discriminated union defined inline (no barrel exports). export type AnthropicTool = - | BashTool - | CodeExecutionTool - | ComputerUseTool - | CustomTool - | MemoryTool - | TextEditorTool - | WebFetchTool - | WebSearchTool + | AnthropicBashTool + | AnthropicCodeExecutionTool + | AnthropicComputerUseTool + | AnthropicCustomTool + | AnthropicMemoryTool + | AnthropicTextEditorTool + | AnthropicWebFetchTool + | AnthropicWebSearchTool diff --git a/packages/typescript/ai-anthropic/src/tools/tool-converter.ts b/packages/typescript/ai-anthropic/src/tools/tool-converter.ts index 4ca43c389..4d434cafe 100644 --- a/packages/typescript/ai-anthropic/src/tools/tool-converter.ts +++ b/packages/typescript/ai-anthropic/src/tools/tool-converter.ts @@ -41,21 +41,21 @@ export function convertToolsToProviderFormat( switch (name) { case 'bash': - return convertBashToolToAdapterFormat(tool) + return convertBashToolToAdapterFormat(tool) as unknown as AnthropicTool case 'code_execution': - return convertCodeExecutionToolToAdapterFormat(tool) + return convertCodeExecutionToolToAdapterFormat(tool) as unknown as AnthropicTool case 'computer': - return convertComputerUseToolToAdapterFormat(tool) + return convertComputerUseToolToAdapterFormat(tool) as unknown as AnthropicTool case 'memory': - return convertMemoryToolToAdapterFormat(tool) + return convertMemoryToolToAdapterFormat(tool) as unknown as AnthropicTool case 'str_replace_editor': - return convertTextEditorToolToAdapterFormat(tool) + return convertTextEditorToolToAdapterFormat(tool) as unknown as AnthropicTool case 'web_fetch': - return convertWebFetchToolToAdapterFormat(tool) + return convertWebFetchToolToAdapterFormat(tool) as unknown as AnthropicTool case 'web_search': - return convertWebSearchToolToAdapterFormat(tool) + return convertWebSearchToolToAdapterFormat(tool) as unknown as AnthropicTool default: - return convertCustomToolToAdapterFormat(tool) + return convertCustomToolToAdapterFormat(tool) as unknown as AnthropicTool } }) } From b996e0791841c7fa6e90b5e268386b064d4c6d58 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:49:59 +0200 Subject: [PATCH 12/49] Revert "fix(ai-anthropic): build AnthropicTool union from branded types" This reverts commit 801255b72db6e20a4e5648643c276501c0202b74. --- .../ai-anthropic/src/adapters/text.ts | 14 ++------ .../ai-anthropic/src/tools/index.ts | 32 +++++++++---------- .../ai-anthropic/src/tools/tool-converter.ts | 16 +++++----- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts index e4a869efd..235d9f5b5 100644 --- a/packages/typescript/ai-anthropic/src/adapters/text.ts +++ b/packages/typescript/ai-anthropic/src/adapters/text.ts @@ -122,15 +122,7 @@ export class AnthropicTextAdapter< const requestParams = this.mapCommonOptionsToAnthropic(options) const stream = await this.client.beta.messages.create( - { - ...requestParams, - stream: true, - tools: requestParams.tools - ? (requestParams.tools as unknown as Parameters< - typeof this.client.beta.messages.create - >[0]['tools']) - : undefined, - }, + { ...requestParams, stream: true }, { signal: options.request?.signal, headers: options.request?.headers, @@ -186,9 +178,7 @@ export class AnthropicTextAdapter< { ...requestParams, stream: false, - tools: [structuredOutputTool] as unknown as Parameters< - typeof this.client.messages.create - >[0]['tools'], + tools: [structuredOutputTool], tool_choice: { type: 'tool', name: 'structured_output' }, }, { diff --git a/packages/typescript/ai-anthropic/src/tools/index.ts b/packages/typescript/ai-anthropic/src/tools/index.ts index fa8ba7679..4202cfecc 100644 --- a/packages/typescript/ai-anthropic/src/tools/index.ts +++ b/packages/typescript/ai-anthropic/src/tools/index.ts @@ -1,11 +1,11 @@ -import type { AnthropicBashTool } from './bash-tool' -import type { AnthropicCodeExecutionTool } from './code-execution-tool' -import type { AnthropicComputerUseTool } from './computer-use-tool' -import type { AnthropicCustomTool } from './custom-tool' -import type { AnthropicMemoryTool } from './memory-tool' -import type { AnthropicTextEditorTool } from './text-editor-tool' -import type { AnthropicWebFetchTool } from './web-fetch-tool' -import type { AnthropicWebSearchTool } from './web-search-tool' +import type { BashTool } from './bash-tool' +import type { CodeExecutionTool } from './code-execution-tool' +import type { ComputerUseTool } from './computer-use-tool' +import type { CustomTool } from './custom-tool' +import type { MemoryTool } from './memory-tool' +import type { TextEditorTool } from './text-editor-tool' +import type { WebFetchTool } from './web-fetch-tool' +import type { WebSearchTool } from './web-search-tool' export { bashTool, @@ -58,11 +58,11 @@ export { // Keep the discriminated union defined inline (no barrel exports). export type AnthropicTool = - | AnthropicBashTool - | AnthropicCodeExecutionTool - | AnthropicComputerUseTool - | AnthropicCustomTool - | AnthropicMemoryTool - | AnthropicTextEditorTool - | AnthropicWebFetchTool - | AnthropicWebSearchTool + | BashTool + | CodeExecutionTool + | ComputerUseTool + | CustomTool + | MemoryTool + | TextEditorTool + | WebFetchTool + | WebSearchTool diff --git a/packages/typescript/ai-anthropic/src/tools/tool-converter.ts b/packages/typescript/ai-anthropic/src/tools/tool-converter.ts index 4d434cafe..4ca43c389 100644 --- a/packages/typescript/ai-anthropic/src/tools/tool-converter.ts +++ b/packages/typescript/ai-anthropic/src/tools/tool-converter.ts @@ -41,21 +41,21 @@ export function convertToolsToProviderFormat( switch (name) { case 'bash': - return convertBashToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertBashToolToAdapterFormat(tool) case 'code_execution': - return convertCodeExecutionToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertCodeExecutionToolToAdapterFormat(tool) case 'computer': - return convertComputerUseToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertComputerUseToolToAdapterFormat(tool) case 'memory': - return convertMemoryToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertMemoryToolToAdapterFormat(tool) case 'str_replace_editor': - return convertTextEditorToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertTextEditorToolToAdapterFormat(tool) case 'web_fetch': - return convertWebFetchToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertWebFetchToolToAdapterFormat(tool) case 'web_search': - return convertWebSearchToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertWebSearchToolToAdapterFormat(tool) default: - return convertCustomToolToAdapterFormat(tool) as unknown as AnthropicTool + return convertCustomToolToAdapterFormat(tool) } }) } From 29dc52f99bf1f4e125ff70023be74152eb98ab79 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 13:56:16 +0200 Subject: [PATCH 13/49] feat(ai-anthropic): thread toolCapabilities through text adapter --- packages/typescript/ai-anthropic/src/adapters/text.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts index 235d9f5b5..e0ab34701 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 { @@ -84,6 +85,11 @@ type ResolveInputModalities = ? AnthropicModelInputModalitiesByName[TModel] : readonly ['text', 'image', 'document'] +type ResolveToolCapabilities = + TModel extends keyof AnthropicChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + // =========================== // Adapter Implementation // =========================== @@ -99,11 +105,14 @@ export class AnthropicTextAdapter< TProviderOptions extends object = 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 From 2bc673a3d77694ec1251afe32cd67dcae7132d9c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:00:05 +0200 Subject: [PATCH 14/49] feat(ai-anthropic): export AnthropicChatModelToolCapabilitiesByName --- packages/typescript/ai-anthropic/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 { From 628f20f8e21ebeb36aa53d113dda84095db36cb9 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:01:29 +0200 Subject: [PATCH 15/49] feat(ai-anthropic): add /tools subpath export --- packages/typescript/ai-anthropic/package.json | 4 ++++ packages/typescript/ai-anthropic/vite.config.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-anthropic/package.json b/packages/typescript/ai-anthropic/package.json index 44a27492b..67e56f905 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/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, }), From 86a2df5b711062a0765a65096bcd84d1fbc41d3e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:13:29 +0200 Subject: [PATCH 16/49] test(ai-anthropic): per-model type tests for provider tools --- .../tests/tools-per-model-type-safety.test.ts | 114 ++++++++++++++++++ .../typescript/ai-anthropic/tsconfig.json | 2 +- 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/typescript/ai-anthropic/tests/tools-per-model-type-safety.test.ts 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..0673ce0f7 --- /dev/null +++ b/packages/typescript/ai-anthropic/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,114 @@ +/** + * 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 { describe, it, beforeAll } from 'vitest' +import { anthropicText } from '../src' +import { + bashTool, + codeExecutionTool, + computerUseTool, + memoryTool, + textEditorTool, + webFetchTool, + webSearchTool, +} from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' +import { toolDefinition } from '@tanstack/ai' +import { z } from 'zod' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: A, + 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('claude-3-5-haiku accepts web tools, rejects code/shell/memory', () => { + 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 bash + bashTool({ name: 'bash', type: 'bash_20250124' }), + // @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..e85be5eaf 100644 --- a/packages/typescript/ai-anthropic/tsconfig.json +++ b/packages/typescript/ai-anthropic/tsconfig.json @@ -3,6 +3,6 @@ "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"] } From e3cba77ea749a220adc6d7b4b4b7f65b9eebff20 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:22:49 +0200 Subject: [PATCH 17/49] test(ai-anthropic): cover all unsupported tools in haiku-3-5 negative case --- .../tests/tools-per-model-type-safety.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 index 0673ce0f7..6247b9ce9 100644 --- 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 @@ -94,7 +94,7 @@ describe('Anthropic per-model tool gating', () => { ]) }) - it('claude-3-5-haiku accepts web tools, rejects code/shell/memory', () => { + it('claude-3-5-haiku accepts only web tools', () => { const adapter = anthropicText('claude-3-5-haiku') typedTools(adapter, [ userTool, @@ -105,8 +105,17 @@ describe('Anthropic per-model tool gating', () => { 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(), ]) From 0e4ad1c51ec7a9586eea53b022726c2d551f8b52 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:50:22 +0200 Subject: [PATCH 18/49] feat(ai-openai): add ToolCapabilitiesByName and expanded supports.tools --- .../typescript/ai-openai/src/adapters/text.ts | 27 +- packages/typescript/ai-openai/src/index.ts | 1 + .../typescript/ai-openai/src/model-meta.ts | 296 +++++++++++++++++- 3 files changed, 314 insertions(+), 10 deletions(-) diff --git a/packages/typescript/ai-openai/src/adapters/text.ts b/packages/typescript/ai-openai/src/adapters/text.ts index 09dd1472f..4546f8fd6 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, @@ -71,6 +73,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 // =========================== @@ -83,11 +94,17 @@ type ResolveInputModalities = */ export class OpenAITextAdapter< TModel extends OpenAIChatModel, + TProviderOptions extends object = 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 @@ -100,7 +117,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 @@ -153,7 +170,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..6112a392f 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,7 @@ const GPT_4O = { 'fine_tuning', 'predicted_outcomes', ], + tools: ['web_search', 'file_search', 'image_generation', 'code_interpreter', 'mcp'], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1168,6 +1358,7 @@ const GPT_4O_AUDIO = { output: ['text', 'audio'], endpoints: ['chat-completions'], features: ['streaming', 'function_calling'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1207,6 +1398,7 @@ 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 +1454,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 +1481,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 +1513,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 +1550,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 +1628,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 +1658,7 @@ const GPT_4 = { 'assistants', ], features: ['fine_tuning', 'streaming'], + tools: [], }, } as const satisfies ModelMeta< OpenAIBaseOptions & OpenAIStreamingOptions & OpenAIMetadataOptions @@ -1557,6 +1776,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 +1815,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 +1890,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 +1935,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 +2282,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. From 32a18cea68c16c46cdba1bd2fc9bbb1c4e76eef9 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:55:15 +0200 Subject: [PATCH 19/49] feat(ai-openai): brand provider tool factories and replace barrel export --- .../ai-openai/src/tools/apply-patch-tool.ts | 16 ++- .../src/tools/code-interpreter-tool.ts | 18 ++- .../ai-openai/src/tools/computer-use-tool.ts | 19 +++- .../ai-openai/src/tools/custom-tool.ts | 18 ++- .../ai-openai/src/tools/file-search-tool.ts | 14 ++- .../ai-openai/src/tools/function-tool.ts | 13 ++- .../src/tools/image-generation-tool.ts | 20 ++-- .../typescript/ai-openai/src/tools/index.ts | 104 +++++++++++++++--- .../ai-openai/src/tools/local-shell-tool.ts | 16 ++- .../ai-openai/src/tools/mcp-tool.ts | 22 ++-- .../ai-openai/src/tools/shell-tool.ts | 16 ++- .../src/tools/web-search-preview-tool.ts | 18 ++- .../ai-openai/src/tools/web-search-tool.ts | 18 ++- 13 files changed, 230 insertions(+), 82 deletions(-) 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..89d9392e7 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,20 @@ -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 +24,8 @@ 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 +33,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..ea42c0417 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,13 @@ 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..37af35681 100644 --- a/packages/typescript/ai-openai/src/tools/custom-tool.ts +++ b/packages/typescript/ai-openai/src/tools/custom-tool.ts @@ -1,13 +1,18 @@ import type OpenAI from 'openai' -import type { Tool } from '@tanstack/ai' +import type { ProviderTool, 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 + +export type OpenAICustomTool = ProviderTool<'openai', 'custom'> /** * 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,12 +24,13 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomTool { /** * Creates a standard Tool from CustomTool parameters */ -export function customTool(toolData: CustomTool): Tool { +export function customTool(toolData: CustomToolConfig): OpenAICustomTool { + // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'custom', description: toolData.description || 'A custom tool', metadata: { ...toolData, }, - } + } as unknown as OpenAICustomTool } 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..6e2fff740 100644 --- a/packages/typescript/ai-openai/src/tools/function-tool.ts +++ b/packages/typescript/ai-openai/src/tools/function-tool.ts @@ -1,8 +1,13 @@ import { makeOpenAIStructuredOutputCompatible } from '../utils/schema-converter' -import type { JSONSchema, Tool } from '@tanstack/ai' +import type { JSONSchema, ProviderTool, 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 + +export type OpenAIFunctionTool = ProviderTool<'openai', 'function'> /** * Converts a standard Tool to OpenAI FunctionTool format. @@ -15,7 +20,7 @@ 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..2afb8bb5e 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,12 @@ 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 +19,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 +31,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..0319f97c3 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,90 @@ 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 OpenAICustomTool, + type CustomToolConfig, + type CustomTool, +} from './custom-tool' +export { + fileSearchTool, + convertFileSearchToolToAdapterFormat, + type OpenAIFileSearchTool, + type FileSearchToolConfig, + type FileSearchTool, +} from './file-search-tool' +export { + convertFunctionToolToAdapterFormat, + type OpenAIFunctionTool, + 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..d6a19fb31 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,20 @@ 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 +25,11 @@ 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..7a10828fa 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,29 @@ 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 } From 1fd3c3892f2fa9c870e22ee8d1834c09a1e75682 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 14:57:15 +0200 Subject: [PATCH 20/49] feat(ai-openai): add /tools subpath export --- packages/typescript/ai-openai/package.json | 4 ++++ packages/typescript/ai-openai/vite.config.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-openai/package.json b/packages/typescript/ai-openai/package.json index 0fdbc8d99..1095613cd 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/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, }), From 7d82f69c851d09b5634988318f3b17fb61906327 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:02:14 +0200 Subject: [PATCH 21/49] test(ai-openai): per-model type tests for provider tools --- .../tests/tools-per-model-type-safety.test.ts | 108 ++++++++++++++++++ packages/typescript/ai-openai/tsconfig.json | 5 +- 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 packages/typescript/ai-openai/tests/tools-per-model-type-safety.test.ts 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..caa386a5e --- /dev/null +++ b/packages/typescript/ai-openai/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,108 @@ +/** + * 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 { describe, it, beforeAll } from 'vitest' +import { openaiText } from '../src' +import { + webSearchTool, + webSearchPreviewTool, + fileSearchTool, + imageGenerationTool, + codeInterpreterTool, + mcpTool, + computerUseTool, + localShellTool, + shellTool, + applyPatchTool, +} from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' +import { toolDefinition } from '@tanstack/ai' +import { z } from 'zod' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: A, + 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 code_interpreter + codeInterpreterTool({ type: 'code_interpreter', container: { type: 'auto' } }), + // @ts-expect-error - gpt-3.5-turbo does not support file_search + fileSearchTool({ type: 'file_search', vector_store_ids: ['vs_123'] }), + ]) + }) + + 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..e85be5eaf 100644 --- a/packages/typescript/ai-openai/tsconfig.json +++ b/packages/typescript/ai-openai/tsconfig.json @@ -1,9 +1,8 @@ { "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"] } From 65ce6589eb57b8e82361aec0fa72d28b53bc4ded Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:07:45 +0200 Subject: [PATCH 22/49] feat(ai-gemini): split capabilities into capabilities + tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separates tool-type entries (code_execution, file_search, search_grounding, grounding_with_gmaps, url_context, image_generation) from general capability flags in ModelMeta.supports. Adds a new tools field to the supports shape, renames grounding_with_gmaps → google_maps and search_grounding → google_search, drops image_generation (no tool factory yet), and introduces GeminiChatModelToolCapabilitiesByName. Threads a fifth TToolCapabilities generic through GeminiTextAdapter and exports the new type map from the root. --- .../typescript/ai-gemini/src/adapters/text.ts | 21 ++- packages/typescript/ai-gemini/src/index.ts | 1 + .../typescript/ai-gemini/src/model-meta.ts | 137 ++++++++++++------ 3 files changed, 111 insertions(+), 48 deletions(-) diff --git a/packages/typescript/ai-gemini/src/adapters/text.ts b/packages/typescript/ai-gemini/src/adapters/text.ts index 5dc441c98..ff55a491b 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 { @@ -66,6 +67,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 // =========================== @@ -81,11 +91,14 @@ export class GeminiTextAdapter< TProviderOptions extends object = 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 @@ -723,7 +736,8 @@ export function createGeminiChat( ): GeminiTextAdapter< TModel, ResolveProviderOptions, - ResolveInputModalities + ResolveInputModalities, + ResolveToolCapabilities > { return new GeminiTextAdapter({ apiKey, ...config }, model) } @@ -738,7 +752,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..e13c04aec 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,12 +61,14 @@ const GEMINI_3_1_PRO = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_search', 'url_context', ], }, @@ -96,12 +101,14 @@ const GEMINI_3_PRO = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_search', 'url_context', ], }, @@ -134,12 +141,14 @@ const GEMINI_3_FLASH = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_search', 'url_context', ], }, @@ -171,11 +180,12 @@ const GEMINI_3_PRO_IMAGE = { output: ['text', 'image'], capabilities: [ 'batch_api', - 'image_generation', - 'search_grounding', 'structured_output', 'thinking', ], + tools: [ + 'google_search', + ], }, pricing: { input: { @@ -205,11 +215,12 @@ const GEMINI_3_1_FLASH_IMAGE = { output: ['text', 'image'], capabilities: [ 'batch_api', - 'image_generation', - 'search_grounding', 'structured_output', 'thinking', ], + tools: [ + 'google_search', + ], }, pricing: { input: { @@ -239,12 +250,14 @@ const GEMINI_3_1_FLASH_LITE = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_search', 'url_context', ], }, @@ -276,13 +289,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 +326,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 +355,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,12 +395,14 @@ const GEMINI_2_5_FLASH_PREVIEW = { capabilities: [ 'batch_api', 'caching', - 'code_execution', - 'file_search', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'file_search', + 'google_search', 'url_context', ], }, @@ -414,10 +434,11 @@ const GEMINI_2_5_FLASH_IMAGE = { capabilities: [ 'batch_api', 'caching', - 'file_search', - 'image_generation', 'structured_output', ], + tools: [ + 'file_search', + ], }, pricing: { input: { @@ -476,7 +497,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,12 +526,14 @@ const GEMINI_2_5_FLASH_LITE = { capabilities: [ 'batch_api', 'caching', - 'code_execution', 'function_calling', - 'grounding_with_gmaps', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'google_maps', + 'google_search', 'url_context', ], }, @@ -541,11 +565,13 @@ const GEMINI_2_5_FLASH_LITE_PREVIEW = { capabilities: [ 'batch_api', 'caching', - 'code_execution', 'function_calling', - 'search_grounding', 'structured_output', 'thinking', + ], + tools: [ + 'code_execution', + 'google_search', 'url_context', ], }, @@ -577,13 +603,15 @@ 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: { @@ -612,9 +640,9 @@ const GEMINI_2_FLASH_IMAGE = { capabilities: [ 'batch_api', 'caching', - 'image_generation', 'structured_output', ], + tools: [], }, pricing: { input: { @@ -679,6 +707,7 @@ const GEMINI_2_FLASH_LITE = { 'function_calling', 'structured_output', ], + tools: [], }, pricing: { input: { @@ -1092,6 +1121,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. From 868b71f21fa5b1b59b4a469c6d43cae05b553755 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:11:51 +0200 Subject: [PATCH 23/49] feat(ai-gemini): brand provider tool factories and replace barrel export --- .../src/tools/code-execution-tool.ts | 14 ++++-- .../ai-gemini/src/tools/computer-use-tool.ts | 16 ++++-- .../ai-gemini/src/tools/file-search-tool.ts | 16 ++++-- .../ai-gemini/src/tools/google-maps-tool.ts | 16 ++++-- .../src/tools/google-search-retriveal-tool.ts | 18 ++++--- .../ai-gemini/src/tools/google-search-tool.ts | 16 ++++-- .../typescript/ai-gemini/src/tools/index.ts | 49 +++++++++++++++++++ .../ai-gemini/src/tools/url-context-tool.ts | 14 ++++-- 8 files changed, 125 insertions(+), 34 deletions(-) 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..97c9e5793 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,8 @@ 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 +27,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..c46553735 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,25 @@ -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..dea44248a 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,25 @@ 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..4863c99c8 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,27 @@ 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..acd5f2f16 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,25 @@ 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..4361444d1 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 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 } From 269d281f3b42fc77e5fbbac454bd600efa7dd30a Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:17:42 +0200 Subject: [PATCH 24/49] feat(ai-gemini): add /tools subpath export and type tests --- packages/typescript/ai-gemini/package.json | 7 +- .../tests/tools-per-model-type-safety.test.ts | 75 +++++++++++++++++++ packages/typescript/ai-gemini/tsconfig.json | 5 +- packages/typescript/ai-gemini/vite.config.ts | 2 +- pnpm-lock.yaml | 15 ++-- 5 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 packages/typescript/ai-gemini/tests/tools-per-model-type-safety.test.ts diff --git a/packages/typescript/ai-gemini/package.json b/packages/typescript/ai-gemini/package.json index c7ad4ce23..3b072f049 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/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..2f530f0dc --- /dev/null +++ b/packages/typescript/ai-gemini/tests/tools-per-model-type-safety.test.ts @@ -0,0 +1,75 @@ +/** + * 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 { describe, it, beforeAll } from 'vitest' +import { geminiText } from '../src' +import { + codeExecutionTool, + computerUseTool, + fileSearchTool, + googleMapsTool, + googleSearchTool, + urlContextTool, +} from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' +import { toolDefinition } from '@tanstack/ai' +import { z } from 'zod' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: A, + 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(), + ]) + }) + + 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 url_context + urlContextTool(), + ]) + }) +}) diff --git a/packages/typescript/ai-gemini/tsconfig.json b/packages/typescript/ai-gemini/tsconfig.json index ea11c1096..e85be5eaf 100644 --- a/packages/typescript/ai-gemini/tsconfig.json +++ b/packages/typescript/ai-gemini/tsconfig.json @@ -1,9 +1,8 @@ { "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/pnpm-lock.yaml b/pnpm-lock.yaml index 33532fa85..393bab0f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1052,6 +1052,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: @@ -15243,7 +15246,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 @@ -15273,13 +15276,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: @@ -21279,10 +21282,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 @@ -21299,7 +21302,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 From 31aa38f2d50f0665e7f0928bce796e31648bc7d9 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:39:36 +0200 Subject: [PATCH 25/49] feat(ai-openrouter)!: move and rename createWebSearchTool to webSearchTool on /tools subpath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames createWebSearchTool → webSearchTool, brands its return type as OpenRouterWebSearchTool (ProviderTool<'openrouter', 'web_search'>), moves web_search exports to the new ./tools subpath, adds OpenRouterChatModelToolCapabilitiesByName mapped type (all chat models support web_search via the gateway), threads TToolCapabilities through the text adapter, and adds per-model type tests. --- .../typescript/ai-openrouter/package.json | 7 +- .../ai-openrouter/src/adapters/text.ts | 14 +++- .../typescript/ai-openrouter/src/index.ts | 4 +- .../ai-openrouter/src/model-meta.ts | 7 ++ .../ai-openrouter/src/tools/index.ts | 19 ++++-- .../ai-openrouter/src/tools/tool-converter.ts | 11 +++- .../src/tools/web-search-tool.ts | 45 ++++++++++--- .../tests/tools-per-model-type-safety.test.ts | 65 +++++++++++++++++++ .../typescript/ai-openrouter/vite.config.ts | 2 +- pnpm-lock.yaml | 3 + 10 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 packages/typescript/ai-openrouter/tests/tools-per-model-type-safety.test.ts diff --git a/packages/typescript/ai-openrouter/package.json b/packages/typescript/ai-openrouter/package.json index 772eb523c..fa2e6c594 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 4a19d1c9a..2a995b2fc 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' @@ -51,6 +52,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 @@ -71,11 +77,13 @@ 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 @@ -623,14 +631,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 b385308b0..dc3338e32 100644 --- a/packages/typescript/ai-openrouter/src/model-meta.ts +++ b/packages/typescript/ai-openrouter/src/model-meta.ts @@ -15758,6 +15758,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..66233cbb5 100644 --- a/packages/typescript/ai-openrouter/src/tools/tool-converter.ts +++ b/packages/typescript/ai-openrouter/src/tools/tool-converter.ts @@ -1,11 +1,18 @@ import { convertFunctionToolToAdapterFormat } from './function-tool' +import { convertWebSearchToolToAdapterFormat } 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) => { + if (tool.name === 'web_search') { + 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..ed3798e39 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,6 @@ -export interface WebSearchTool { +import type { ProviderTool, Tool } from '@tanstack/ai' + +export interface WebSearchToolConfig { type: 'web_search' web_search: { engine?: 'native' | 'exa' @@ -7,17 +9,42 @@ export interface WebSearchTool { } } -export function createWebSearchTool(options?: { +/** @deprecated Renamed to `WebSearchToolConfig`. */ +export type WebSearchTool = WebSearchToolConfig + +export type OpenRouterWebSearchTool = ProviderTool<'openrouter', 'web_search'> + +/** + * Converts a standard Tool to OpenRouter WebSearchTool format. + */ +export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchToolConfig { + const metadata = tool.metadata as WebSearchToolConfig + return metadata +} + +/** + * 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: { + 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..bac6bfcf9 --- /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 { describe, it, beforeAll } from 'vitest' +import { openRouterText } from '../src' +import { webSearchTool } from '../src/tools' +import type { TextActivityOptions } from '@tanstack/ai/adapters' +import type { ProviderTool } from '@tanstack/ai' +import { toolDefinition } from '@tanstack/ai' +import { z } from 'zod' + +// Helper — keeps each `it` body to one call (test-hygiene Rule 1). +function typedTools>( + adapter: A, + 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/pnpm-lock.yaml b/pnpm-lock.yaml index 393bab0f4..9c455b721 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1195,6 +1195,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: From cff729e667052fda0c410152da5091fb1155555c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:51:44 +0200 Subject: [PATCH 26/49] feat(ai-grok, ai-groq): add supports.tools, /tools subpath, and thread toolCapabilities - Add tools?: ReadonlyArray to ModelMeta.supports interface in both packages - Add tools: [] as const to every chat model constant - Export GrokChatModelToolCapabilitiesByName / GroqChatModelToolCapabilitiesByName type maps - Add 5th TToolCapabilities generic to GrokTextAdapter / GroqTextAdapter via ResolveToolCapabilities - Add ./tools subpath to package.json exports and vite.config.ts entry for both packages - Re-export new ToolCapabilitiesByName types from root index.ts in both packages --- packages/typescript/ai-grok/package.json | 4 +++ .../typescript/ai-grok/src/adapters/text.ts | 26 ++++++++++---- packages/typescript/ai-grok/src/index.ts | 1 + packages/typescript/ai-grok/src/model-meta.ts | 32 +++++++++++++++++ packages/typescript/ai-grok/vite.config.ts | 2 +- packages/typescript/ai-groq/package.json | 4 +++ .../typescript/ai-groq/src/adapters/text.ts | 28 +++++++++++---- packages/typescript/ai-groq/src/index.ts | 1 + packages/typescript/ai-groq/src/model-meta.ts | 34 +++++++++++++++++++ packages/typescript/ai-groq/vite.config.ts | 2 +- 10 files changed, 120 insertions(+), 14 deletions(-) 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 c0204ab53..f82c8761a 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 [] + /** * Configuration for Grok text adapter */ @@ -49,11 +59,15 @@ export type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../ */ export class GrokTextAdapter< TModel extends (typeof GROK_CHAT_MODELS)[number], + TProviderOptions extends object = 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 @@ -66,7 +80,7 @@ export class GrokTextAdapter< } async *chatStream( - options: TextOptions>, + options: TextOptions, ): AsyncIterable { const requestParams = this.mapTextOptionsToGrok(options) const timestamp = Date.now() @@ -132,7 +146,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 6e6465f74..236edae41 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,17 @@ 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' + +type GroqTextProviderOptions = ExternalTextProviderOptions import type { ChatCompletionContentPart, ChatCompletionMessageParam, @@ -34,6 +41,11 @@ import type { } from '../message-types' import type { GroqClientConfig } from '../utils' +type ResolveToolCapabilities = + TModel extends keyof GroqChatModelToolCapabilitiesByName + ? NonNullable + : readonly [] + /** * Configuration for Groq text adapter */ @@ -52,11 +64,15 @@ export type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../ */ export class GroqTextAdapter< TModel extends (typeof GROQ_CHAT_MODELS)[number], + TProviderOptions extends object = 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 @@ -69,7 +85,7 @@ export class GroqTextAdapter< } async *chatStream( - options: TextOptions>, + options: TextOptions, ): AsyncIterable { const requestParams = this.mapTextOptionsToGroq(options) const timestamp = Date.now() @@ -132,7 +148,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, }), From 782554def570713246877fd8cc921ed1d4828ed1 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 15:53:27 +0200 Subject: [PATCH 27/49] chore(scripts): update sync-models templates to include supports.tools --- scripts/sync-provider-models.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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, From cdcfe6d694ad4d0d344f71cfbe50169a52cbc07c Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:06:31 +0200 Subject: [PATCH 28/49] test(ai-anthropic): runtime smoke test for provider tool factories --- .../tests/provider-tools-smoke.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 packages/typescript/ai-anthropic/tests/provider-tools-smoke.test.ts 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') + }) +}) From 95a1ba9166e0194608d7f44e5f96c0cea474b92e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:10:15 +0200 Subject: [PATCH 29/49] docs: add provider tools concept page --- docs/tools/provider-tools.md | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/tools/provider-tools.md diff --git a/docs/tools/provider-tools.md b/docs/tools/provider-tools.md new file mode 100644 index 000000000..1f44b3f7b --- /dev/null +++ b/docs/tools/provider-tools.md @@ -0,0 +1,90 @@ +--- +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 todays AI news.' }], + tools: [ + webSearchTool({ + name: 'web_search', + type: 'web_search_20250305', + max_uses: 3, + }), + ], +}) +``` + +## Type-level guard + +Every provider-tool factory 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. + +## Available tools + +| Provider | Tools | +|---|---| +| Anthropic | `webSearchTool`, `webFetchTool`, `codeExecutionTool`, `computerUseTool`, `bashTool`, `textEditorTool`, `memoryTool`, `customTool` — see [Anthropic adapter](../adapters/anthropic.md#provider-tools). | +| OpenAI | `webSearchTool`, `webSearchPreviewTool`, `fileSearchTool`, `imageGenerationTool`, `codeInterpreterTool`, `mcpTool`, `computerUseTool`, `localShellTool`, `shellTool`, `applyPatchTool`, `customTool` — 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). From 2fe3d04fdd7dac48fdd534950cf657bc7897a0f2 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:14:50 +0200 Subject: [PATCH 30/49] docs(adapters): add Provider Tools sections for every adapter --- docs/adapters/anthropic.md | 194 +++++++++++++++++++++++++++ docs/adapters/gemini.md | 159 ++++++++++++++++++++++ docs/adapters/grok.md | 9 ++ docs/adapters/groq.md | 9 ++ docs/adapters/openai.md | 260 ++++++++++++++++++++++++++++++++++++ docs/adapters/openrouter.md | 41 +++++- 6 files changed, 671 insertions(+), 1 deletion(-) diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index 2e8f36b2f..48991603f 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -228,3 +228,197 @@ 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 fully custom Anthropic-native 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`. + +```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 84b5e9515..98ec41300 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -385,3 +385,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({ + fileIds: ["file-abc123"], + }), + ], +}); +``` + +**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 b73662b9c..acf571090 100644 --- a/docs/adapters/grok.md +++ b/docs/adapters/grok.md @@ -230,3 +230,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 b6fcf4991..4cc38c47a 100644 --- a/docs/adapters/groq.md +++ b/docs/adapters/groq.md @@ -270,3 +270,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 eda51463d..821ff0b68 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -331,3 +331,263 @@ 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_preview" })], +}); +``` + +**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:** Codex CLI and 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:** Codex CLI and 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:** Codex CLI and agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). + +### `customTool` + +Defines a fully custom Responses API tool with an explicit name, description, +and format. Use this when none of the structured tool types fits your use case. + +```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 b51c18ca4..18074249f 100644 --- a/docs/adapters/openrouter.md +++ b/docs/adapters/openrouter.md @@ -128,5 +128,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). From 24b3b496bf407fa7dc8115028f94c813d22d1fdf Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:16:37 +0200 Subject: [PATCH 31/49] docs: cross-link provider tools concept page and migration section 6 --- docs/config.json | 4 +++ docs/migration/migration.md | 50 +++++++++++++++++++++++++++++++++++++ docs/tools/tools.md | 3 +++ 3 files changed, 57 insertions(+) diff --git a/docs/config.json b/docs/config.json index a4377a9af..79b29bc3c 100644 --- a/docs/config.json +++ b/docs/config.json @@ -51,6 +51,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 2a1093b47..3966a3ec3 100644 --- a/docs/migration/migration.md +++ b/docs/migration/migration.md @@ -355,6 +355,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/tools.md b/docs/tools/tools.md index c0a651a95..1719bbc4d 100644 --- a/docs/tools/tools.md +++ b/docs/tools/tools.md @@ -14,6 +14,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: From cd4d7fba3da5e527820a17cd336ff7898467b397 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:18:38 +0200 Subject: [PATCH 32/49] chore: changesets for provider-tools surface --- .changeset/provider-tools-ai-anthropic.md | 5 +++++ .changeset/provider-tools-ai-core.md | 5 +++++ .changeset/provider-tools-ai-gemini.md | 7 +++++++ .changeset/provider-tools-ai-grok.md | 5 +++++ .changeset/provider-tools-ai-groq.md | 5 +++++ .changeset/provider-tools-ai-openai.md | 5 +++++ .changeset/provider-tools-ai-openrouter.md | 7 +++++++ 7 files changed, 39 insertions(+) create mode 100644 .changeset/provider-tools-ai-anthropic.md create mode 100644 .changeset/provider-tools-ai-core.md create mode 100644 .changeset/provider-tools-ai-gemini.md create mode 100644 .changeset/provider-tools-ai-grok.md create mode 100644 .changeset/provider-tools-ai-groq.md create mode 100644 .changeset/provider-tools-ai-openai.md create mode 100644 .changeset/provider-tools-ai-openrouter.md diff --git a/.changeset/provider-tools-ai-anthropic.md b/.changeset/provider-tools-ai-anthropic.md new file mode 100644 index 000000000..102f61afe --- /dev/null +++ b/.changeset/provider-tools-ai-anthropic.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-anthropic': patch +--- + +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..0546b44ca --- /dev/null +++ b/.changeset/provider-tools-ai-gemini.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai-gemini': patch +--- + +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..7fa39c942 --- /dev/null +++ b/.changeset/provider-tools-ai-openai.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-openai': patch +--- + +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..84800044d --- /dev/null +++ b/.changeset/provider-tools-ai-openrouter.md @@ -0,0 +1,7 @@ +--- +'@tanstack/ai-openrouter': patch +--- + +**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. From 66aece462d82951e1dd192599c3cbd5767228cb8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:24:36 +0200 Subject: [PATCH 33/49] fix(ai-groq): reorder imports to satisfy import/first ESLint rule --- packages/typescript/ai-groq/src/adapters/text.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript/ai-groq/src/adapters/text.ts b/packages/typescript/ai-groq/src/adapters/text.ts index 236edae41..d3af424ed 100644 --- a/packages/typescript/ai-groq/src/adapters/text.ts +++ b/packages/typescript/ai-groq/src/adapters/text.ts @@ -31,8 +31,6 @@ import type { ExternalTextProviderOptions, InternalTextProviderOptions, } from '../text/text-provider-options' - -type GroqTextProviderOptions = ExternalTextProviderOptions import type { ChatCompletionContentPart, ChatCompletionMessageParam, @@ -41,6 +39,8 @@ import type { } from '../message-types' import type { GroqClientConfig } from '../utils' +type GroqTextProviderOptions = ExternalTextProviderOptions + type ResolveToolCapabilities = TModel extends keyof GroqChatModelToolCapabilitiesByName ? NonNullable From c78f146309d16eb708f4d9dc0ee408fe205f6298 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:48:23 +0200 Subject: [PATCH 34/49] fix(changesets): bump adapters to minor for new /tools subpath + openrouter breaking --- .changeset/provider-tools-ai-anthropic.md | 2 +- .changeset/provider-tools-ai-gemini.md | 2 +- .changeset/provider-tools-ai-openai.md | 2 +- .changeset/provider-tools-ai-openrouter.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/provider-tools-ai-anthropic.md b/.changeset/provider-tools-ai-anthropic.md index 102f61afe..4ec2e77b4 100644 --- a/.changeset/provider-tools-ai-anthropic.md +++ b/.changeset/provider-tools-ai-anthropic.md @@ -1,5 +1,5 @@ --- -'@tanstack/ai-anthropic': patch +'@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-gemini.md b/.changeset/provider-tools-ai-gemini.md index 0546b44ca..37e5a8b51 100644 --- a/.changeset/provider-tools-ai-gemini.md +++ b/.changeset/provider-tools-ai-gemini.md @@ -1,5 +1,5 @@ --- -'@tanstack/ai-gemini': patch +'@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. diff --git a/.changeset/provider-tools-ai-openai.md b/.changeset/provider-tools-ai-openai.md index 7fa39c942..9f87ca867 100644 --- a/.changeset/provider-tools-ai-openai.md +++ b/.changeset/provider-tools-ai-openai.md @@ -1,5 +1,5 @@ --- -'@tanstack/ai-openai': patch +'@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 index 84800044d..9f39aa8d1 100644 --- a/.changeset/provider-tools-ai-openrouter.md +++ b/.changeset/provider-tools-ai-openrouter.md @@ -1,5 +1,5 @@ --- -'@tanstack/ai-openrouter': patch +'@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. From ec01981bff8523157aac94df7a62ceee97d962a4 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:48:33 +0200 Subject: [PATCH 35/49] fix(ai-openrouter): match @deprecated tag wording across adapters --- packages/typescript/ai-openrouter/src/tools/web-search-tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ed3798e39..9124b2797 100644 --- a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts @@ -9,7 +9,7 @@ export interface WebSearchToolConfig { } } -/** @deprecated Renamed to `WebSearchToolConfig`. */ +/** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */ export type WebSearchTool = WebSearchToolConfig export type OpenRouterWebSearchTool = ProviderTool<'openrouter', 'web_search'> From b700eb1aedb01cce4f47eb8752ab408b84ef04f0 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:48:59 +0200 Subject: [PATCH 36/49] fix(changesets): confirm gemini relocation claim is accurate for all active models Investigated file_search and search_grounding entries in model-meta.ts: - All active (exported) models: correctly have these entries in tools: array - Commented-out preview models (GEMINI_2_5_FLASH_LIVE, GEMINI_2_FLASH_LIVE): still have them in capabilities: but are not part of the released API - Changeset claim is accurate: relocation is complete for all models that matter From 6861e55e1df23b87167455ebbb9e2e6ea19227f6 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:54:35 +0200 Subject: [PATCH 37/49] fix(ai-gemini): export convertToolsToProviderFormat from /tools --- packages/typescript/ai-gemini/src/tools/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/typescript/ai-gemini/src/tools/index.ts b/packages/typescript/ai-gemini/src/tools/index.ts index 4361444d1..d39a396d7 100644 --- a/packages/typescript/ai-gemini/src/tools/index.ts +++ b/packages/typescript/ai-gemini/src/tools/index.ts @@ -65,3 +65,5 @@ export type GoogleGeminiTool = | GoogleSearchRetrievalTool | GoogleSearchTool | UrlContextTool + +export { convertToolsToProviderFormat } from './tool-converter' From 29ba0a80e11cda95ad9c37a7bdedf819c33ffd79 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:54:47 +0200 Subject: [PATCH 38/49] test(ai-openai): cover all 10 unsupported tools in gpt-3.5-turbo --- .../tests/tools-per-model-type-safety.test.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) 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 index caa386a5e..d0d209586 100644 --- 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 @@ -7,6 +7,7 @@ import { describe, it, beforeAll } from 'vitest' import { openaiText } from '../src' import { + customTool, webSearchTool, webSearchPreviewTool, fileSearchTool, @@ -72,10 +73,43 @@ describe('OpenAI per-model tool gating', () => { 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 code_interpreter - codeInterpreterTool({ type: 'code_interpreter', container: { type: 'auto' } }), + // @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' }), ]) }) From a02e67dce27915ab6d1f34d1e86f7770a7e82c41 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:54:58 +0200 Subject: [PATCH 39/49] test(ai-gemini): add negative cases for gemini-3.1-pro-preview --- .../ai-gemini/tests/tools-per-model-type-safety.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 index 2f530f0dc..13f857bbf 100644 --- 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 @@ -11,6 +11,7 @@ import { computerUseTool, fileSearchTool, googleMapsTool, + googleSearchRetrievalTool, googleSearchTool, urlContextTool, } from '../src/tools' @@ -51,6 +52,12 @@ describe('Gemini per-model tool gating', () => { fileSearchTool(fileSearchConfig), googleSearchTool(), urlContextTool(), + // @ts-expect-error - gemini-3.1-pro-preview does not support computer_use + computerUseTool({ 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(), ]) }) From 8be483542b49b06a6fba5a962b5df84e96188f49 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:57:22 +0200 Subject: [PATCH 40/49] =?UTF-8?q?fix(ai-anthropic):=20debrand=20customTool?= =?UTF-8?q?=20=E2=80=94=20return=20plain=20Tool=20(universal)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../typescript/ai-anthropic/src/tools/custom-tool.ts | 9 +++------ packages/typescript/ai-anthropic/src/tools/index.ts | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/typescript/ai-anthropic/src/tools/custom-tool.ts b/packages/typescript/ai-anthropic/src/tools/custom-tool.ts index de259bd6c..582b7cc03 100644 --- a/packages/typescript/ai-anthropic/src/tools/custom-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/custom-tool.ts @@ -1,4 +1,4 @@ -import type { JSONSchema, ProviderTool, SchemaInput, Tool } from '@tanstack/ai' +import type { JSONSchema, SchemaInput, Tool } from '@tanstack/ai' import type { CacheControl } from '../text/text-provider-options' export interface CustomToolConfig { @@ -26,8 +26,6 @@ export interface CustomToolConfig { /** @deprecated Renamed to `CustomToolConfig`. Will be removed in a future release. */ export type CustomTool = CustomToolConfig -export type AnthropicCustomTool = ProviderTool<'anthropic', 'custom'> - export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { const metadata = (tool.metadata as { cacheControl?: CacheControl | null } | undefined) || {} @@ -58,8 +56,7 @@ export function customTool( description: string, inputSchema: SchemaInput, cacheControl?: CacheControl | null, -): AnthropicCustomTool { - // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. +): Tool { return { name, description, @@ -67,5 +64,5 @@ export function customTool( metadata: { cacheControl, }, - } as unknown as AnthropicCustomTool + } } diff --git a/packages/typescript/ai-anthropic/src/tools/index.ts b/packages/typescript/ai-anthropic/src/tools/index.ts index 4202cfecc..7e3beeede 100644 --- a/packages/typescript/ai-anthropic/src/tools/index.ts +++ b/packages/typescript/ai-anthropic/src/tools/index.ts @@ -27,7 +27,6 @@ export { } from './computer-use-tool' export { customTool, - type AnthropicCustomTool, type CustomToolConfig, type CustomTool, } from './custom-tool' From 1f4cc44adef1f54ae8dbae706efbd7b957115455 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:57:36 +0200 Subject: [PATCH 41/49] =?UTF-8?q?fix(ai-openai):=20debrand=20customTool/fu?= =?UTF-8?q?nctionTool=20=E2=80=94=20remove=20unused=20brand=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/typescript/ai-openai/src/tools/custom-tool.ts | 9 +++------ packages/typescript/ai-openai/src/tools/function-tool.ts | 4 +--- packages/typescript/ai-openai/src/tools/index.ts | 2 -- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/typescript/ai-openai/src/tools/custom-tool.ts b/packages/typescript/ai-openai/src/tools/custom-tool.ts index 37af35681..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,11 @@ import type OpenAI from 'openai' -import type { ProviderTool, Tool } from '@tanstack/ai' +import type { Tool } from '@tanstack/ai' export type CustomToolConfig = OpenAI.Responses.CustomTool /** @deprecated Renamed to `CustomToolConfig`. Will be removed in a future release. */ export type CustomTool = CustomToolConfig -export type OpenAICustomTool = ProviderTool<'openai', 'custom'> - /** * Converts a standard Tool to OpenAI CustomTool format */ @@ -24,13 +22,12 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { /** * Creates a standard Tool from CustomTool parameters */ -export function customTool(toolData: CustomToolConfig): OpenAICustomTool { - // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. +export function customTool(toolData: CustomToolConfig): Tool { return { name: 'custom', description: toolData.description || 'A custom tool', metadata: { ...toolData, }, - } as unknown as OpenAICustomTool + } } diff --git a/packages/typescript/ai-openai/src/tools/function-tool.ts b/packages/typescript/ai-openai/src/tools/function-tool.ts index 6e2fff740..989867cb6 100644 --- a/packages/typescript/ai-openai/src/tools/function-tool.ts +++ b/packages/typescript/ai-openai/src/tools/function-tool.ts @@ -1,5 +1,5 @@ import { makeOpenAIStructuredOutputCompatible } from '../utils/schema-converter' -import type { JSONSchema, ProviderTool, Tool } from '@tanstack/ai' +import type { JSONSchema, Tool } from '@tanstack/ai' import type OpenAI from 'openai' export type FunctionToolConfig = OpenAI.Responses.FunctionTool @@ -7,8 +7,6 @@ export type FunctionToolConfig = OpenAI.Responses.FunctionTool /** @deprecated Renamed to `FunctionToolConfig`. Will be removed in a future release. */ export type FunctionTool = FunctionToolConfig -export type OpenAIFunctionTool = ProviderTool<'openai', 'function'> - /** * Converts a standard Tool to OpenAI FunctionTool format. * diff --git a/packages/typescript/ai-openai/src/tools/index.ts b/packages/typescript/ai-openai/src/tools/index.ts index 0319f97c3..918f222e6 100644 --- a/packages/typescript/ai-openai/src/tools/index.ts +++ b/packages/typescript/ai-openai/src/tools/index.ts @@ -52,7 +52,6 @@ export { export { customTool, convertCustomToolToAdapterFormat, - type OpenAICustomTool, type CustomToolConfig, type CustomTool, } from './custom-tool' @@ -65,7 +64,6 @@ export { } from './file-search-tool' export { convertFunctionToolToAdapterFormat, - type OpenAIFunctionTool, type FunctionToolConfig, type FunctionTool, } from './function-tool' From 6970466f817465f2033a0df51d821798489400e8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 18:57:51 +0200 Subject: [PATCH 42/49] docs: remove customTool from branded-factories matrix --- docs/adapters/anthropic.md | 7 ++++--- docs/adapters/openai.md | 6 ++++-- docs/tools/provider-tools.md | 15 +++++++++------ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/adapters/anthropic.md b/docs/adapters/anthropic.md index 48991603f..03afdc9a5 100644 --- a/docs/adapters/anthropic.md +++ b/docs/adapters/anthropic.md @@ -398,9 +398,10 @@ const stream = chat({ ### `customTool` -Creates a fully custom Anthropic-native 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`. +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"; diff --git a/docs/adapters/openai.md b/docs/adapters/openai.md index 821ff0b68..2606c0595 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -569,8 +569,10 @@ const stream = chat({ ### `customTool` -Defines a fully custom Responses API tool with an explicit name, description, -and format. Use this when none of the structured tool types fits your use case. +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"; diff --git a/docs/tools/provider-tools.md b/docs/tools/provider-tools.md index 1f44b3f7b..f2ef8d96b 100644 --- a/docs/tools/provider-tools.md +++ b/docs/tools/provider-tools.md @@ -47,21 +47,24 @@ const stream = chat({ ## Type-level guard -Every provider-tool factory returns a `ProviderTool` brand. -The adapter's `toolCapabilities` (derived from each model's `supports.tools` -list) gates which brands are assignable to `tools`. +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. +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`, `customTool` — see [Anthropic adapter](../adapters/anthropic.md#provider-tools). | -| OpenAI | `webSearchTool`, `webSearchPreviewTool`, `fileSearchTool`, `imageGenerationTool`, `codeInterpreterTool`, `mcpTool`, `computerUseTool`, `localShellTool`, `shellTool`, `applyPatchTool`, `customTool` — see [OpenAI adapter](../adapters/openai.md#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). | From 1e25129664e0e6664a78af8d1c982badc5c083dd Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 17 Apr 2026 19:27:41 +0200 Subject: [PATCH 43/49] test(ai-anthropic): cover debranded customTool acceptance on any model --- .../tests/tools-per-model-type-safety.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 index 6247b9ce9..a943f94f8 100644 --- 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 @@ -10,6 +10,7 @@ import { bashTool, codeExecutionTool, computerUseTool, + customTool, memoryTool, textEditorTool, webFetchTool, @@ -94,6 +95,20 @@ describe('Anthropic per-model tool gating', () => { ]) }) + 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, [ From 0c1bd22be0b8b8426d50b4b8254485dd37680f3c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:29:39 +0000 Subject: [PATCH 44/49] ci: apply automated fixes --- .../src/tools/code-execution-tool.ts | 9 +- .../src/tools/computer-use-tool.ts | 4 +- .../src/tools/text-editor-tool.ts | 4 +- .../ai-anthropic/src/tools/web-fetch-tool.ts | 4 +- .../ai-anthropic/src/tools/web-search-tool.ts | 8 +- .../tests/tools-per-model-type-safety.test.ts | 22 ++++- .../typescript/ai-anthropic/tsconfig.json | 6 +- .../typescript/ai-gemini/src/model-meta.ts | 90 ++++--------------- .../ai-gemini/src/tools/computer-use-tool.ts | 4 +- .../ai-gemini/src/tools/file-search-tool.ts | 4 +- .../ai-gemini/src/tools/google-maps-tool.ts | 4 +- .../src/tools/google-search-retriveal-tool.ts | 5 +- .../ai-gemini/src/tools/google-search-tool.ts | 4 +- .../tests/tools-per-model-type-safety.test.ts | 5 +- packages/typescript/ai-gemini/tsconfig.json | 6 +- .../typescript/ai-grok/src/adapters/text.ts | 6 +- .../typescript/ai-groq/src/adapters/text.ts | 6 +- .../typescript/ai-openai/src/model-meta.ts | 16 +++- .../src/tools/code-interpreter-tool.ts | 9 +- .../ai-openai/src/tools/computer-use-tool.ts | 4 +- .../ai-openai/src/tools/function-tool.ts | 4 +- .../src/tools/image-generation-tool.ts | 5 +- .../src/tools/web-search-preview-tool.ts | 9 +- .../ai-openai/src/tools/web-search-tool.ts | 8 +- .../tests/tools-per-model-type-safety.test.ts | 42 +++++++-- packages/typescript/ai-openai/tsconfig.json | 6 +- .../ai-openrouter/src/adapters/text.ts | 3 +- .../src/tools/web-search-tool.ts | 4 +- 28 files changed, 181 insertions(+), 120 deletions(-) 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 5f52205b0..17241d60e 100644 --- a/packages/typescript/ai-anthropic/src/tools/code-execution-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/code-execution-tool.ts @@ -11,7 +11,10 @@ export type CodeExecutionToolConfig = /** @deprecated Renamed to `CodeExecutionToolConfig`. Will be removed in a future release. */ export type CodeExecutionTool = CodeExecutionToolConfig -export type AnthropicCodeExecutionTool = ProviderTool<'anthropic', 'code_execution'> +export type AnthropicCodeExecutionTool = ProviderTool< + 'anthropic', + 'code_execution' +> export function convertCodeExecutionToolToAdapterFormat( tool: Tool, @@ -20,7 +23,9 @@ export function convertCodeExecutionToolToAdapterFormat( return metadata } -export function codeExecutionTool(config: CodeExecutionToolConfig): AnthropicCodeExecutionTool { +export function codeExecutionTool( + config: CodeExecutionToolConfig, +): AnthropicCodeExecutionTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'code_execution', 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 7cdddf025..0788f36ad 100644 --- a/packages/typescript/ai-anthropic/src/tools/computer-use-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/computer-use-tool.ts @@ -20,7 +20,9 @@ export function convertComputerUseToolToAdapterFormat( return metadata } -export function computerUseTool(config: ComputerUseToolConfig): AnthropicComputerUseTool { +export function computerUseTool( + config: ComputerUseToolConfig, +): AnthropicComputerUseTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'computer', 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 59ad6ed98..714888dd3 100644 --- a/packages/typescript/ai-anthropic/src/tools/text-editor-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/text-editor-tool.ts @@ -24,7 +24,9 @@ export function convertTextEditorToolToAdapterFormat( } } -export function textEditorTool(config: T): AnthropicTextEditorTool { +export function textEditorTool( + config: T, +): AnthropicTextEditorTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'str_replace_editor', 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 7ca4173c4..81bb523a7 100644 --- a/packages/typescript/ai-anthropic/src/tools/web-fetch-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/web-fetch-tool.ts @@ -8,7 +8,9 @@ export type WebFetchTool = WebFetchToolConfig export type AnthropicWebFetchTool = ProviderTool<'anthropic', 'web_fetch'> -export function convertWebFetchToolToAdapterFormat(tool: Tool): WebFetchToolConfig { +export function convertWebFetchToolToAdapterFormat( + tool: Tool, +): WebFetchToolConfig { const metadata = tool.metadata as Omit return { name: 'web_fetch', 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 ea7722021..cad276b3d 100644 --- a/packages/typescript/ai-anthropic/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-anthropic/src/tools/web-search-tool.ts @@ -50,7 +50,9 @@ const validateUserLocation = (tool: WebSearchToolConfig) => { } } -export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchToolConfig { +export function convertWebSearchToolToAdapterFormat( + tool: Tool, +): WebSearchToolConfig { const metadata = tool.metadata as { allowedDomains?: Array | null blockedDomains?: Array | null @@ -75,7 +77,9 @@ export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchToolCo } } -export function webSearchTool(config: WebSearchToolConfig): AnthropicWebSearchTool { +export function webSearchTool( + config: WebSearchToolConfig, +): AnthropicWebSearchTool { validateDomains(config) validateUserLocation(config) // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. 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 index a943f94f8..c97557690 100644 --- 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 @@ -59,7 +59,10 @@ describe('Anthropic per-model tool gating', () => { display_height_px: 768, }), bashTool({ name: 'bash', type: 'bash_20250124' }), - textEditorTool({ type: 'text_editor_20250124', name: 'str_replace_editor' }), + textEditorTool({ + type: 'text_editor_20250124', + name: 'str_replace_editor', + }), memoryTool(), ]) }) @@ -99,13 +102,21 @@ describe('Anthropic per-model tool gating', () => { // 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() })), + 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() })), + customTool( + 'lookup_user', + 'Look up a user by ID', + z.object({ userId: z.number() }), + ), ]) }) @@ -130,7 +141,10 @@ describe('Anthropic per-model tool gating', () => { // @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' }), + 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 e85be5eaf..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", "tests/tools-per-model-type-safety.test.ts"], + "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/src/model-meta.ts b/packages/typescript/ai-gemini/src/model-meta.ts index e13c04aec..219c274fc 100644 --- a/packages/typescript/ai-gemini/src/model-meta.ts +++ b/packages/typescript/ai-gemini/src/model-meta.ts @@ -65,12 +65,7 @@ const GEMINI_3_1_PRO = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'file_search', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -105,12 +100,7 @@ const GEMINI_3_PRO = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'file_search', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -145,12 +135,7 @@ const GEMINI_3_FLASH = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'file_search', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -178,14 +163,8 @@ const GEMINI_3_PRO_IMAGE = { supports: { input: ['text', 'image'], output: ['text', 'image'], - capabilities: [ - 'batch_api', - 'structured_output', - 'thinking', - ], - tools: [ - 'google_search', - ], + capabilities: ['batch_api', 'structured_output', 'thinking'], + tools: ['google_search'], }, pricing: { input: { @@ -213,14 +192,8 @@ const GEMINI_3_1_FLASH_IMAGE = { supports: { input: ['text', 'image'], output: ['text', 'image'], - capabilities: [ - 'batch_api', - 'structured_output', - 'thinking', - ], - tools: [ - 'google_search', - ], + capabilities: ['batch_api', 'structured_output', 'thinking'], + tools: ['google_search'], }, pricing: { input: { @@ -254,12 +227,7 @@ const GEMINI_3_1_FLASH_LITE = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'file_search', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -399,12 +367,7 @@ const GEMINI_2_5_FLASH_PREVIEW = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'file_search', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'file_search', 'google_search', 'url_context'], }, pricing: { input: { @@ -431,14 +394,8 @@ const GEMINI_2_5_FLASH_IMAGE = { supports: { input: ['text', 'image'], output: ['text', 'image'], - capabilities: [ - 'batch_api', - 'caching', - 'structured_output', - ], - tools: [ - 'file_search', - ], + capabilities: ['batch_api', 'caching', 'structured_output'], + tools: ['file_search'], }, pricing: { input: { @@ -530,12 +487,7 @@ const GEMINI_2_5_FLASH_LITE = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'google_maps', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'google_maps', 'google_search', 'url_context'], }, pricing: { input: { @@ -569,11 +521,7 @@ const GEMINI_2_5_FLASH_LITE_PREVIEW = { 'structured_output', 'thinking', ], - tools: [ - 'code_execution', - 'google_search', - 'url_context', - ], + tools: ['code_execution', 'google_search', 'url_context'], }, pricing: { input: { @@ -607,11 +555,7 @@ const GEMINI_2_FLASH = { 'live_api', 'structured_output', ], - tools: [ - 'code_execution', - 'google_maps', - 'google_search', - ], + tools: ['code_execution', 'google_maps', 'google_search'], }, pricing: { input: { @@ -637,11 +581,7 @@ const GEMINI_2_FLASH_IMAGE = { supports: { input: ['text', 'image', 'audio', 'video'], output: ['text'], - capabilities: [ - 'batch_api', - 'caching', - 'structured_output', - ], + capabilities: ['batch_api', 'caching', 'structured_output'], tools: [], }, pricing: { 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 97c9e5793..7a908d499 100644 --- a/packages/typescript/ai-gemini/src/tools/computer-use-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/computer-use-tool.ts @@ -18,7 +18,9 @@ export function convertComputerUseToolToAdapterFormat(tool: Tool) { } } -export function computerUseTool(config: ComputerUseToolConfig): GeminiComputerUseTool { +export function computerUseTool( + config: ComputerUseToolConfig, +): GeminiComputerUseTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'computer_use', 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 c46553735..a7a21a624 100644 --- a/packages/typescript/ai-gemini/src/tools/file-search-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/file-search-tool.ts @@ -15,7 +15,9 @@ export function convertFileSearchToolToAdapterFormat(tool: Tool) { } } -export function fileSearchTool(config: FileSearchToolConfig): GeminiFileSearchTool { +export function fileSearchTool( + config: FileSearchToolConfig, +): GeminiFileSearchTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'file_search', 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 dea44248a..b94bb4f4a 100644 --- a/packages/typescript/ai-gemini/src/tools/google-maps-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/google-maps-tool.ts @@ -15,7 +15,9 @@ export function convertGoogleMapsToolToAdapterFormat(tool: Tool) { } } -export function googleMapsTool(config?: GoogleMapsToolConfig): GeminiGoogleMapsTool { +export function googleMapsTool( + config?: GoogleMapsToolConfig, +): GeminiGoogleMapsTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'google_maps', 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 4863c99c8..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 @@ -6,7 +6,10 @@ 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 type GeminiGoogleSearchRetrievalTool = ProviderTool< + 'gemini', + 'google_search_retrieval' +> export function convertGoogleSearchRetrievalToolToAdapterFormat(tool: Tool) { const metadata = tool.metadata as GoogleSearchRetrievalToolConfig 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 acd5f2f16..0d7db3d06 100644 --- a/packages/typescript/ai-gemini/src/tools/google-search-tool.ts +++ b/packages/typescript/ai-gemini/src/tools/google-search-tool.ts @@ -15,7 +15,9 @@ export function convertGoogleSearchToolToAdapterFormat(tool: Tool) { } } -export function googleSearchTool(config?: GoogleSearchToolConfig): GeminiGoogleSearchTool { +export function googleSearchTool( + config?: GoogleSearchToolConfig, +): GeminiGoogleSearchTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'google_search', 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 index 13f857bbf..4f0f14a93 100644 --- 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 @@ -53,7 +53,10 @@ describe('Gemini per-model tool gating', () => { googleSearchTool(), urlContextTool(), // @ts-expect-error - gemini-3.1-pro-preview does not support computer_use - computerUseTool({ environment: 'ENVIRONMENT_BROWSER', excludedPredefinedFunctions: [] }), + computerUseTool({ + 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 diff --git a/packages/typescript/ai-gemini/tsconfig.json b/packages/typescript/ai-gemini/tsconfig.json index e85be5eaf..0c50acadb 100644 --- a/packages/typescript/ai-gemini/tsconfig.json +++ b/packages/typescript/ai-gemini/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["vite.config.ts", "./src", "tests/tools-per-model-type-safety.test.ts"], + "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-grok/src/adapters/text.ts b/packages/typescript/ai-grok/src/adapters/text.ts index f82c8761a..bd207a634 100644 --- a/packages/typescript/ai-grok/src/adapters/text.ts +++ b/packages/typescript/ai-grok/src/adapters/text.ts @@ -60,8 +60,10 @@ export type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../ export class GrokTextAdapter< TModel extends (typeof GROK_CHAT_MODELS)[number], TProviderOptions extends object = ResolveProviderOptions, - TInputModalities extends ReadonlyArray = ResolveInputModalities, - TToolCapabilities extends ReadonlyArray = ResolveToolCapabilities, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, TProviderOptions, diff --git a/packages/typescript/ai-groq/src/adapters/text.ts b/packages/typescript/ai-groq/src/adapters/text.ts index d3af424ed..66e6b0c1f 100644 --- a/packages/typescript/ai-groq/src/adapters/text.ts +++ b/packages/typescript/ai-groq/src/adapters/text.ts @@ -65,8 +65,10 @@ export type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../ export class GroqTextAdapter< TModel extends (typeof GROQ_CHAT_MODELS)[number], TProviderOptions extends object = ResolveProviderOptions, - TInputModalities extends ReadonlyArray = ResolveInputModalities, - TToolCapabilities extends ReadonlyArray = ResolveToolCapabilities, + TInputModalities extends ReadonlyArray = + ResolveInputModalities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, TProviderOptions, diff --git a/packages/typescript/ai-openai/src/model-meta.ts b/packages/typescript/ai-openai/src/model-meta.ts index 6112a392f..1772171fe 100644 --- a/packages/typescript/ai-openai/src/model-meta.ts +++ b/packages/typescript/ai-openai/src/model-meta.ts @@ -1328,7 +1328,13 @@ const GPT_4O = { 'fine_tuning', 'predicted_outcomes', ], - tools: ['web_search', 'file_search', 'image_generation', 'code_interpreter', 'mcp'], + tools: [ + 'web_search', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & @@ -1398,7 +1404,13 @@ const GPT_4O_MINI = { 'fine_tuning', 'predicted_outcomes', ], - tools: ['web_search', 'file_search', 'image_generation', 'code_interpreter', 'mcp'], + tools: [ + 'web_search', + 'file_search', + 'image_generation', + 'code_interpreter', + 'mcp', + ], }, } as const satisfies ModelMeta< OpenAIBaseOptions & 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 89d9392e7..357b47c64 100644 --- a/packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts +++ b/packages/typescript/ai-openai/src/tools/code-interpreter-tool.ts @@ -6,7 +6,10 @@ 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'> +export type OpenAICodeInterpreterTool = ProviderTool< + 'openai', + 'code_interpreter' +> /** * Converts a standard Tool to OpenAI CodeInterpreterTool format @@ -24,7 +27,9 @@ export function convertCodeInterpreterToolToAdapterFormat( /** * Creates a standard Tool from CodeInterpreterTool parameters */ -export function codeInterpreterTool(container: CodeInterpreterToolConfig): OpenAICodeInterpreterTool { +export function codeInterpreterTool( + container: CodeInterpreterToolConfig, +): OpenAICodeInterpreterTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'code_interpreter', 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 ea42c0417..72e3a9399 100644 --- a/packages/typescript/ai-openai/src/tools/computer-use-tool.ts +++ b/packages/typescript/ai-openai/src/tools/computer-use-tool.ts @@ -26,7 +26,9 @@ export function convertComputerUseToolToAdapterFormat( /** * Creates a standard Tool from ComputerUseTool parameters */ -export function computerUseTool(toolData: ComputerUseToolConfig): OpenAIComputerUseTool { +export function computerUseTool( + toolData: ComputerUseToolConfig, +): OpenAIComputerUseTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'computer_use_preview', diff --git a/packages/typescript/ai-openai/src/tools/function-tool.ts b/packages/typescript/ai-openai/src/tools/function-tool.ts index 989867cb6..6b4630f3f 100644 --- a/packages/typescript/ai-openai/src/tools/function-tool.ts +++ b/packages/typescript/ai-openai/src/tools/function-tool.ts @@ -18,7 +18,9 @@ export type FunctionTool = FunctionToolConfig * * This enables strict mode for all tools automatically. */ -export function convertFunctionToolToAdapterFormat(tool: Tool): FunctionToolConfig { +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 ?? { 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 2afb8bb5e..9b3abb395 100644 --- a/packages/typescript/ai-openai/src/tools/image-generation-tool.ts +++ b/packages/typescript/ai-openai/src/tools/image-generation-tool.ts @@ -6,7 +6,10 @@ 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'> +export type OpenAIImageGenerationTool = ProviderTool< + 'openai', + 'image_generation' +> const validatePartialImages = (value: number | undefined) => { if (value !== undefined && (value < 0 || value > 3)) { 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 d6a19fb31..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 @@ -6,7 +6,10 @@ 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'> +export type OpenAIWebSearchPreviewTool = ProviderTool< + 'openai', + 'web_search_preview' +> /** * Converts a standard Tool to OpenAI WebSearchPreviewTool format @@ -25,7 +28,9 @@ export function convertWebSearchPreviewToolToAdapterFormat( /** * Creates a standard Tool from WebSearchPreviewTool parameters */ -export function webSearchPreviewTool(toolData: WebSearchPreviewToolConfig): OpenAIWebSearchPreviewTool { +export function webSearchPreviewTool( + toolData: WebSearchPreviewToolConfig, +): OpenAIWebSearchPreviewTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'web_search_preview', 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 7a10828fa..83991e9d3 100644 --- a/packages/typescript/ai-openai/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openai/src/tools/web-search-tool.ts @@ -11,7 +11,9 @@ export type OpenAIWebSearchTool = ProviderTool<'openai', 'web_search'> /** * Converts a standard Tool to OpenAI WebSearchTool format */ -export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchToolConfig { +export function convertWebSearchToolToAdapterFormat( + tool: Tool, +): WebSearchToolConfig { const metadata = tool.metadata as WebSearchToolConfig return metadata } @@ -19,7 +21,9 @@ export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchToolCo /** * Creates a standard Tool from WebSearchTool parameters */ -export function webSearchTool(toolData: WebSearchToolConfig): OpenAIWebSearchTool { +export function webSearchTool( + toolData: WebSearchToolConfig, +): OpenAIWebSearchTool { // Phantom-brand cast: '~provider'/'~toolKind' are type-only and never assigned at runtime. return { name: 'web_search', 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 index d0d209586..185a85d40 100644 --- 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 @@ -53,8 +53,14 @@ describe('OpenAI per-model tool gating', () => { 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' }), + 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, @@ -80,9 +86,15 @@ describe('OpenAI per-model tool gating', () => { // @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' } }), + 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' }), + 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', @@ -103,13 +115,21 @@ describe('OpenAI per-model tool gating', () => { // Full-featured model const fullAdapter = openaiText('gpt-5.2') typedTools(fullAdapter, [ - customTool({ type: 'custom', name: 'lookup_order', description: 'Look up an order' }), + 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' }), + customTool({ + type: 'custom', + name: 'lookup_order', + description: 'Look up an order', + }), ]) }) @@ -120,8 +140,14 @@ describe('OpenAI per-model tool gating', () => { 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' }), + 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 diff --git a/packages/typescript/ai-openai/tsconfig.json b/packages/typescript/ai-openai/tsconfig.json index e85be5eaf..0c50acadb 100644 --- a/packages/typescript/ai-openai/tsconfig.json +++ b/packages/typescript/ai-openai/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["vite.config.ts", "./src", "tests/tools-per-model-type-safety.test.ts"], + "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-openrouter/src/adapters/text.ts b/packages/typescript/ai-openrouter/src/adapters/text.ts index 2a995b2fc..0ef701ed8 100644 --- a/packages/typescript/ai-openrouter/src/adapters/text.ts +++ b/packages/typescript/ai-openrouter/src/adapters/text.ts @@ -77,7 +77,8 @@ interface AGUIState { export class OpenRouterTextAdapter< TModel extends OpenRouterTextModels, - TToolCapabilities extends ReadonlyArray = ResolveToolCapabilities, + TToolCapabilities extends ReadonlyArray = + ResolveToolCapabilities, > extends BaseTextAdapter< TModel, ResolveProviderOptions, 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 9124b2797..e697307fd 100644 --- a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts @@ -17,7 +17,9 @@ export type OpenRouterWebSearchTool = ProviderTool<'openrouter', 'web_search'> /** * Converts a standard Tool to OpenRouter WebSearchTool format. */ -export function convertWebSearchToolToAdapterFormat(tool: Tool): WebSearchToolConfig { +export function convertWebSearchToolToAdapterFormat( + tool: Tool, +): WebSearchToolConfig { const metadata = tool.metadata as WebSearchToolConfig return metadata } From 416d8b0406978a7ac018c99b3afb306ee521717e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 20 Apr 2026 12:34:31 +0200 Subject: [PATCH 45/49] review: address CodeRabbit PR feedback - Tighten TProviderOptions constraint from 'extends object' to 'extends Record' across all five text adapters to match BaseTextAdapter (openai, anthropic, gemini, grok, groq). - OpenAI & Anthropic: wire chatStream / structuredOutput to the TProviderOptions generic so per-model provider options are enforced at the call site, matching the openrouter/gemini pattern. - OpenRouter webSearchTool: brand via a stable __kind marker and gate the converter on that; reject malformed metadata explicitly instead of trusting tool.name === 'web_search' (users can collide on name). - Docs: fix fileSearchTool example to use the real fileSearchStoreNames shape; switch webSearchTool example back to type: 'web_search' (preview variant is a separate factory); update local_shell/shell/apply_patch supported-models copy to match the actual supports.tools gating; drop 'todays' typo. - Tests: apply ESLint import/order + sort-imports to the five tools-per-model type-safety specs. --- docs/adapters/gemini.md | 2 +- docs/adapters/openai.md | 8 ++-- docs/tools/provider-tools.md | 2 +- .../ai-anthropic/src/adapters/text.ts | 6 +-- .../tests/tools-per-model-type-safety.test.ts | 6 +-- .../typescript/ai-gemini/src/adapters/text.ts | 2 +- .../tests/tools-per-model-type-safety.test.ts | 6 +-- .../typescript/ai-grok/src/adapters/text.ts | 2 +- .../typescript/ai-groq/src/adapters/text.ts | 2 +- .../typescript/ai-openai/src/adapters/text.ts | 6 +-- .../tests/tools-per-model-type-safety.test.ts | 18 ++++----- .../ai-openrouter/src/tools/tool-converter.ts | 9 ++++- .../src/tools/web-search-tool.ts | 38 +++++++++++++++++-- .../tests/tools-per-model-type-safety.test.ts | 6 +-- .../tests/tools-per-model-type-safety.test.ts | 4 +- 15 files changed, 77 insertions(+), 40 deletions(-) diff --git a/docs/adapters/gemini.md b/docs/adapters/gemini.md index 74a88cfbb..a3e4ff7e4 100644 --- a/docs/adapters/gemini.md +++ b/docs/adapters/gemini.md @@ -440,7 +440,7 @@ const stream = chat({ messages: [{ role: "user", content: "Find the quarterly revenue figures" }], tools: [ fileSearchTool({ - fileIds: ["file-abc123"], + fileSearchStoreNames: ["fileSearchStores/my-file-search-store-123"], }), ], }); diff --git a/docs/adapters/openai.md b/docs/adapters/openai.md index 41159202e..122aaf520 100644 --- a/docs/adapters/openai.md +++ b/docs/adapters/openai.md @@ -366,7 +366,7 @@ 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_preview" })], + tools: [webSearchTool({ type: "web_search" })], }); ``` @@ -538,7 +538,7 @@ const stream = chat({ }); ``` -**Supported models:** Codex CLI and agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). +**Supported models:** GPT-5.x and other agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). ### `shellTool` @@ -557,7 +557,7 @@ const stream = chat({ }); ``` -**Supported models:** Codex CLI and agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). +**Supported models:** GPT-5.x and other agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). ### `applyPatchTool` @@ -576,7 +576,7 @@ const stream = chat({ }); ``` -**Supported models:** Codex CLI and agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). +**Supported models:** GPT-5.x and other agent-capable models. See [Provider Tools](../tools/provider-tools.md#which-models-support-which-tools). ### `customTool` diff --git a/docs/tools/provider-tools.md b/docs/tools/provider-tools.md index f2ef8d96b..865f266f8 100644 --- a/docs/tools/provider-tools.md +++ b/docs/tools/provider-tools.md @@ -34,7 +34,7 @@ import { webSearchTool } from '@tanstack/ai-anthropic/tools' const stream = chat({ adapter: anthropicText('claude-opus-4-6'), - messages: [{ role: 'user', content: 'Summarize todays AI news.' }], + messages: [{ role: 'user', content: "Summarize today's AI news." }], tools: [ webSearchTool({ name: 'web_search', diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts index 241ce8c6d..a8510c167 100644 --- a/packages/typescript/ai-anthropic/src/adapters/text.ts +++ b/packages/typescript/ai-anthropic/src/adapters/text.ts @@ -107,7 +107,7 @@ type ResolveToolCapabilities = */ export class AnthropicTextAdapter< TModel extends (typeof ANTHROPIC_MODELS)[number], - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, TToolCapabilities extends ReadonlyArray = @@ -130,7 +130,7 @@ export class AnthropicTextAdapter< } async *chatStream( - options: TextOptions, + options: TextOptions, ): AsyncIterable { try { const requestParams = this.mapCommonOptionsToAnthropic(options) @@ -169,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/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai-anthropic/tests/tools-per-model-type-safety.test.ts index c97557690..40aaef54a 100644 --- 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 @@ -4,7 +4,9 @@ * Positive cases: each supported (model, tool) pair compiles cleanly. * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. */ -import { describe, it, beforeAll } from 'vitest' +import { beforeAll, describe, it } from 'vitest' +import { z } from 'zod' +import { toolDefinition } from '@tanstack/ai' import { anthropicText } from '../src' import { bashTool, @@ -17,8 +19,6 @@ import { webSearchTool, } from '../src/tools' import type { TextActivityOptions } from '@tanstack/ai/adapters' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). function typedTools>( diff --git a/packages/typescript/ai-gemini/src/adapters/text.ts b/packages/typescript/ai-gemini/src/adapters/text.ts index 58309dcaa..6b265122f 100644 --- a/packages/typescript/ai-gemini/src/adapters/text.ts +++ b/packages/typescript/ai-gemini/src/adapters/text.ts @@ -93,7 +93,7 @@ type ResolveToolCapabilities = */ export class GeminiTextAdapter< TModel extends (typeof GEMINI_MODELS)[number], - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, TToolCapabilities extends ReadonlyArray = 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 index 917b0d289..08df3ee9f 100644 --- 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 @@ -4,8 +4,10 @@ * Positive cases: each supported (model, tool) pair compiles cleanly. * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. */ -import { describe, it, beforeAll } from 'vitest' +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, @@ -17,8 +19,6 @@ import { urlContextTool, } from '../src/tools' import type { TextActivityOptions } from '@tanstack/ai/adapters' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). function typedTools>( diff --git a/packages/typescript/ai-grok/src/adapters/text.ts b/packages/typescript/ai-grok/src/adapters/text.ts index 4648842db..6d354fe10 100644 --- a/packages/typescript/ai-grok/src/adapters/text.ts +++ b/packages/typescript/ai-grok/src/adapters/text.ts @@ -64,7 +64,7 @@ export type { ExternalTextProviderOptions as GrokTextProviderOptions } from '../ */ export class GrokTextAdapter< TModel extends (typeof GROK_CHAT_MODELS)[number], - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, TToolCapabilities extends ReadonlyArray = diff --git a/packages/typescript/ai-groq/src/adapters/text.ts b/packages/typescript/ai-groq/src/adapters/text.ts index db8868389..b14879c96 100644 --- a/packages/typescript/ai-groq/src/adapters/text.ts +++ b/packages/typescript/ai-groq/src/adapters/text.ts @@ -69,7 +69,7 @@ export type { ExternalTextProviderOptions as GroqTextProviderOptions } from '../ */ export class GroqTextAdapter< TModel extends (typeof GROQ_CHAT_MODELS)[number], - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, TToolCapabilities extends ReadonlyArray = diff --git a/packages/typescript/ai-openai/src/adapters/text.ts b/packages/typescript/ai-openai/src/adapters/text.ts index 29e72af69..7ccdbfe7d 100644 --- a/packages/typescript/ai-openai/src/adapters/text.ts +++ b/packages/typescript/ai-openai/src/adapters/text.ts @@ -99,7 +99,7 @@ type ResolveToolCapabilities = */ export class OpenAITextAdapter< TModel extends OpenAIChatModel, - TProviderOptions extends object = ResolveProviderOptions, + TProviderOptions extends Record = ResolveProviderOptions, TInputModalities extends ReadonlyArray = ResolveInputModalities, TToolCapabilities extends ReadonlyArray = @@ -122,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 @@ -175,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/tests/tools-per-model-type-safety.test.ts b/packages/typescript/ai-openai/tests/tools-per-model-type-safety.test.ts index 185a85d40..81fcd5819 100644 --- 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 @@ -4,24 +4,24 @@ * Positive cases: each supported (model, tool) pair compiles cleanly. * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. */ -import { describe, it, beforeAll } from 'vitest' +import { beforeAll, describe, it } from 'vitest' +import { z } from 'zod' +import { toolDefinition } from '@tanstack/ai' import { openaiText } from '../src' import { + applyPatchTool, + codeInterpreterTool, + computerUseTool, customTool, - webSearchTool, - webSearchPreviewTool, fileSearchTool, imageGenerationTool, - codeInterpreterTool, - mcpTool, - computerUseTool, localShellTool, + mcpTool, shellTool, - applyPatchTool, + webSearchPreviewTool, + webSearchTool, } from '../src/tools' import type { TextActivityOptions } from '@tanstack/ai/adapters' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). function typedTools>( diff --git a/packages/typescript/ai-openrouter/src/tools/tool-converter.ts b/packages/typescript/ai-openrouter/src/tools/tool-converter.ts index 66233cbb5..0631330b9 100644 --- a/packages/typescript/ai-openrouter/src/tools/tool-converter.ts +++ b/packages/typescript/ai-openrouter/src/tools/tool-converter.ts @@ -1,5 +1,8 @@ import { convertFunctionToolToAdapterFormat } from './function-tool' -import { convertWebSearchToolToAdapterFormat } from './web-search-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' @@ -10,7 +13,9 @@ export function convertToolsToProviderFormat( tools: Array, ): Array { return tools.map((tool) => { - if (tool.name === 'web_search') { + // 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 e697307fd..690499965 100644 --- a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts @@ -1,5 +1,12 @@ 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: { @@ -14,14 +21,38 @@ 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 standard Tool to OpenRouter WebSearchTool format. + * 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 WebSearchToolConfig - return metadata + const metadata = tool.metadata as + | { + __kind?: unknown + type?: unknown + web_search?: WebSearchToolConfig['web_search'] + } + | undefined + if ( + !metadata || + metadata.__kind !== WEB_SEARCH_TOOL_KIND || + metadata.type !== 'web_search' || + typeof metadata.web_search !== 'object' + ) { + 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 } } /** @@ -41,6 +72,7 @@ export function webSearchTool(options?: { name: 'web_search', description: '', metadata: { + __kind: WEB_SEARCH_TOOL_KIND, type: 'web_search' as const, web_search: { engine: options?.engine, 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 index bac6bfcf9..d7736311b 100644 --- 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 @@ -4,13 +4,13 @@ * Positive cases: each supported (model, tool) pair compiles cleanly. * Negative cases: unsupported (model, tool) pairs produce a `@ts-expect-error`. */ -import { describe, it, beforeAll } from 'vitest' +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' -import { toolDefinition } from '@tanstack/ai' -import { z } from 'zod' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). function typedTools>( 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 index 582cad847..7e7900c18 100644 --- a/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts +++ b/packages/typescript/ai/tests/tools-per-model-type-safety.test.ts @@ -5,12 +5,12 @@ * `@ts-expect-error` to assert compile-time rejections and `expectTypeOf` * for positive inference checks. */ -import { describe, it, expectTypeOf } from 'vitest' +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' -import { toolDefinition } from '../src/index' // ---- Mock adapter wired with a fixed toolCapabilities union ---- From aa33f67b1a8f6cf6c2c0c7d16472b2fe3e7d3aab Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 21 Apr 2026 13:27:41 +0200 Subject: [PATCH 46/49] review: address CodeRabbit feedback (round 2) - ai-openrouter: tighten web_search metadata validation against null/arrays (typeof 'object' alone accepted both) - ai-gemini: include googleSearchRetrievalTool in the "rejects all provider tools" case so no gemini provider-tool kind can regress silently --- .../ai-gemini/tests/tools-per-model-type-safety.test.ts | 2 ++ .../typescript/ai-openrouter/src/tools/web-search-tool.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 index 08df3ee9f..29ff12566 100644 --- 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 @@ -79,6 +79,8 @@ describe('Gemini per-model tool gating', () => { 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-openrouter/src/tools/web-search-tool.ts b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts index 690499965..a978bb495 100644 --- a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts @@ -46,7 +46,9 @@ export function convertWebSearchToolToAdapterFormat( !metadata || metadata.__kind !== WEB_SEARCH_TOOL_KIND || metadata.type !== 'web_search' || - typeof metadata.web_search !== 'object' + 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).`, From c2d4beb22264859d623c3bb48aa70fe2a3d40517 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 21 Apr 2026 13:39:47 +0200 Subject: [PATCH 47/49] review: rename generic `A` to `TAdapter` in typedTools helpers Satisfies @typescript-eslint/naming-convention rule (type params must match /^(T|T[A-Z][A-Za-z]+)$/). Applies the fix to all four per-adapter tools-per-model-type-safety test files (anthropic, gemini, openai, openrouter). --- .../ai-anthropic/tests/tools-per-model-type-safety.test.ts | 6 +++--- .../ai-gemini/tests/tools-per-model-type-safety.test.ts | 6 +++--- .../ai-openai/tests/tools-per-model-type-safety.test.ts | 6 +++--- .../ai-openrouter/tests/tools-per-model-type-safety.test.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) 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 index 40aaef54a..c7e01f3af 100644 --- 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 @@ -21,9 +21,9 @@ import { import type { TextActivityOptions } from '@tanstack/ai/adapters' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). -function typedTools>( - adapter: A, - tools: TextActivityOptions['tools'], +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], ) { return { adapter, tools } } 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 index 29ff12566..e32d28c2e 100644 --- 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 @@ -21,9 +21,9 @@ import { import type { TextActivityOptions } from '@tanstack/ai/adapters' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). -function typedTools>( - adapter: A, - tools: TextActivityOptions['tools'], +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], ) { return { adapter, tools } } 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 index 81fcd5819..848a5ad37 100644 --- 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 @@ -24,9 +24,9 @@ import { import type { TextActivityOptions } from '@tanstack/ai/adapters' // Helper — keeps each `it` body to one call (test-hygiene Rule 1). -function typedTools>( - adapter: A, - tools: TextActivityOptions['tools'], +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], ) { return { adapter, tools } } 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 index d7736311b..774326c67 100644 --- 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 @@ -13,9 +13,9 @@ 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: A, - tools: TextActivityOptions['tools'], +function typedTools>( + adapter: TAdapter, + tools: TextActivityOptions['tools'], ) { return { adapter, tools } } From 3d6da70509caab91bdcc47b37089b4544d245528 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 21 Apr 2026 15:05:01 +0200 Subject: [PATCH 48/49] fix(ai-openrouter): type metadata.web_search as unknown for runtime null/array guard Prior typing as `WebSearchToolConfig['web_search']` was non-nullable at the type level, so `metadata.web_search === null` and `Array.isArray(...)` tripped `@typescript-eslint/no-unnecessary-condition` and failed CI lint. Widen to `unknown` so the defensive null/array runtime checks are type-meaningful, then narrow on return. --- .../src/tools/web-search-tool.ts | 7 ++-- terminalOutput | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 terminalOutput 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 a978bb495..59bc27a74 100644 --- a/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts +++ b/packages/typescript/ai-openrouter/src/tools/web-search-tool.ts @@ -39,7 +39,7 @@ export function convertWebSearchToolToAdapterFormat( | { __kind?: unknown type?: unknown - web_search?: WebSearchToolConfig['web_search'] + web_search?: unknown } | undefined if ( @@ -54,7 +54,10 @@ export function convertWebSearchToolToAdapterFormat( `convertWebSearchToolToAdapterFormat: tool "${tool.name}" is not a valid webSearchTool() output (missing branded metadata).`, ) } - return { type: 'web_search', web_search: metadata.web_search } + return { + type: 'web_search', + web_search: metadata.web_search as WebSearchToolConfig['web_search'], + } } /** diff --git a/terminalOutput b/terminalOutput new file mode 100644 index 000000000..8ec4c4303 --- /dev/null +++ b/terminalOutput @@ -0,0 +1,36 @@ + +> @tanstack/ai-solid@0.6.15 build /home/runner/work/ai/ai/packages/typescript/ai-solid +> tsdown + +ℹ tsdown v0.17.3 powered by rolldown v1.0.0-beta.53 +ℹ config file: /home/runner/work/ai/ai/packages/typescript/ai-solid/tsdown.config.ts +ℹ entry: src/index.ts +ℹ tsconfig: tsconfig.json +ℹ Build start +ℹ dist/index.js 0.69 kB │ gzip: 0.24 kB +ℹ dist/use-generate-video.js.map 8.04 kB │ gzip: 2.68 kB +ℹ dist/use-chat.js.map 7.64 kB │ gzip: 2.45 kB +ℹ dist/use-generation.js.map 7.07 kB │ gzip: 2.39 kB +ℹ dist/use-transcription.js.map 4.42 kB │ gzip: 1.62 kB +ℹ dist/use-generate-image.js.map 4.41 kB │ gzip: 1.60 kB +ℹ dist/use-summarize.js.map 4.10 kB │ gzip: 1.51 kB +ℹ dist/use-generate-speech.js.map 4.02 kB │ gzip: 1.53 kB +ℹ dist/use-chat.js 3.28 kB │ gzip: 0.99 kB +ℹ dist/use-generate-video.js 3.03 kB │ gzip: 1.15 kB +ℹ dist/use-generation.js 2.38 kB │ gzip: 0.98 kB +ℹ dist/use-generate-image.js 1.34 kB │ gzip: 0.66 kB +ℹ dist/use-transcription.js 1.32 kB │ gzip: 0.63 kB +ℹ dist/use-generate-speech.js 1.10 kB │ gzip: 0.54 kB +ℹ dist/use-summarize.js 1.07 kB │ gzip: 0.52 kB +ℹ dist/index.d.ts 2.01 kB │ gzip: 0.49 kB +ℹ dist/use-generate-video.d.ts 4.16 kB │ gzip: 1.53 kB +ℹ dist/use-generate-image.d.ts 3.75 kB │ gzip: 1.41 kB +ℹ dist/use-transcription.d.ts 3.74 kB │ gzip: 1.41 kB +ℹ dist/use-generation.d.ts 3.61 kB │ gzip: 1.35 kB +ℹ dist/types.d.ts 3.44 kB │ gzip: 1.31 kB +ℹ dist/use-summarize.d.ts 3.43 kB │ gzip: 1.30 kB +ℹ dist/use-generate-speech.d.ts 3.40 kB │ gzip: 1.32 kB +ℹ dist/use-chat.d.ts 0.34 kB │ gzip: 0.23 kB +ℹ 24 files, total: 81.80 kB +✔ Build complete in 12837ms +✔ [publint] No issues found (2839ms) From a69b70c8a2a7eed9c439d2bbdfcd8cc9fa8b903d Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Tue, 21 Apr 2026 15:05:22 +0200 Subject: [PATCH 49/49] chore: remove accidentally-committed terminalOutput artifact --- terminalOutput | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100644 terminalOutput diff --git a/terminalOutput b/terminalOutput deleted file mode 100644 index 8ec4c4303..000000000 --- a/terminalOutput +++ /dev/null @@ -1,36 +0,0 @@ - -> @tanstack/ai-solid@0.6.15 build /home/runner/work/ai/ai/packages/typescript/ai-solid -> tsdown - -ℹ tsdown v0.17.3 powered by rolldown v1.0.0-beta.53 -ℹ config file: /home/runner/work/ai/ai/packages/typescript/ai-solid/tsdown.config.ts -ℹ entry: src/index.ts -ℹ tsconfig: tsconfig.json -ℹ Build start -ℹ dist/index.js 0.69 kB │ gzip: 0.24 kB -ℹ dist/use-generate-video.js.map 8.04 kB │ gzip: 2.68 kB -ℹ dist/use-chat.js.map 7.64 kB │ gzip: 2.45 kB -ℹ dist/use-generation.js.map 7.07 kB │ gzip: 2.39 kB -ℹ dist/use-transcription.js.map 4.42 kB │ gzip: 1.62 kB -ℹ dist/use-generate-image.js.map 4.41 kB │ gzip: 1.60 kB -ℹ dist/use-summarize.js.map 4.10 kB │ gzip: 1.51 kB -ℹ dist/use-generate-speech.js.map 4.02 kB │ gzip: 1.53 kB -ℹ dist/use-chat.js 3.28 kB │ gzip: 0.99 kB -ℹ dist/use-generate-video.js 3.03 kB │ gzip: 1.15 kB -ℹ dist/use-generation.js 2.38 kB │ gzip: 0.98 kB -ℹ dist/use-generate-image.js 1.34 kB │ gzip: 0.66 kB -ℹ dist/use-transcription.js 1.32 kB │ gzip: 0.63 kB -ℹ dist/use-generate-speech.js 1.10 kB │ gzip: 0.54 kB -ℹ dist/use-summarize.js 1.07 kB │ gzip: 0.52 kB -ℹ dist/index.d.ts 2.01 kB │ gzip: 0.49 kB -ℹ dist/use-generate-video.d.ts 4.16 kB │ gzip: 1.53 kB -ℹ dist/use-generate-image.d.ts 3.75 kB │ gzip: 1.41 kB -ℹ dist/use-transcription.d.ts 3.74 kB │ gzip: 1.41 kB -ℹ dist/use-generation.d.ts 3.61 kB │ gzip: 1.35 kB -ℹ dist/types.d.ts 3.44 kB │ gzip: 1.31 kB -ℹ dist/use-summarize.d.ts 3.43 kB │ gzip: 1.30 kB -ℹ dist/use-generate-speech.d.ts 3.40 kB │ gzip: 1.32 kB -ℹ dist/use-chat.d.ts 0.34 kB │ gzip: 0.23 kB -ℹ 24 files, total: 81.80 kB -✔ Build complete in 12837ms -✔ [publint] No issues found (2839ms)