From f4da3b05f953c9898add1e9d5c440edb24868b2b Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 12:13:54 +0200 Subject: [PATCH 1/7] fix: distinguish custom Anthropic web search tools --- .changeset/tidy-searches-coexist.md | 5 +++ .../ai-anthropic/src/tools/tool-converter.ts | 18 +++++++++- .../tests/provider-tools-smoke.test.ts | 24 ++++++++++++++ .../src/routes/api.anthropic-skills-wire.ts | 12 +++++++ .../e2e/tests/anthropic-skills-wire.spec.ts | 33 +++++++++++++++++++ 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 .changeset/tidy-searches-coexist.md diff --git a/.changeset/tidy-searches-coexist.md b/.changeset/tidy-searches-coexist.md new file mode 100644 index 000000000..0b690b7a3 --- /dev/null +++ b/.changeset/tidy-searches-coexist.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-anthropic': patch +--- + +Keep ordinary function tools named `web_search` distinct from Anthropic's native web-search tool by checking the provider metadata before conversion. diff --git a/packages/ai-anthropic/src/tools/tool-converter.ts b/packages/ai-anthropic/src/tools/tool-converter.ts index 8ab68c244..4f61486af 100644 --- a/packages/ai-anthropic/src/tools/tool-converter.ts +++ b/packages/ai-anthropic/src/tools/tool-converter.ts @@ -9,6 +9,20 @@ import { convertWebSearchToolToAdapterFormat } from './web-search-tool' import type { AnthropicTool } from './index' import type { Tool } from '@tanstack/ai' +function isAnthropicWebSearchTool(tool: Tool): boolean { + const metadata = tool.metadata + if (!metadata) { + return false + } + + return ( + 'type' in metadata && + metadata.type === 'web_search_20250305' && + 'name' in metadata && + metadata.name === 'web_search' + ) +} + /** * Converts standard Tool format to Anthropic-specific tool format * @@ -53,7 +67,9 @@ export function convertToolsToProviderFormat( case 'web_fetch': return convertWebFetchToolToAdapterFormat(tool) case 'web_search': - return convertWebSearchToolToAdapterFormat(tool) + return isAnthropicWebSearchTool(tool) + ? convertWebSearchToolToAdapterFormat(tool) + : convertCustomToolToAdapterFormat(tool) default: return convertCustomToolToAdapterFormat(tool) } diff --git a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts index 89c93f389..77535eed4 100644 --- a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts +++ b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts @@ -115,4 +115,28 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => { expect(names).toContain('code_execution') expect(names).toContain('bash') }) + + it('keeps an ordinary function named web_search as a custom tool', () => { + const [converted] = convertToolsToProviderFormat([ + { + name: 'web_search', + description: 'Search an application index', + inputSchema: { + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }, + } satisfies Tool, + ]) + + expect(converted).toMatchObject({ + name: 'web_search', + type: 'custom', + description: 'Search an application index', + input_schema: { + properties: { query: { type: 'string' } }, + required: ['query'], + }, + }) + }) }) diff --git a/testing/e2e/src/routes/api.anthropic-skills-wire.ts b/testing/e2e/src/routes/api.anthropic-skills-wire.ts index 14ac8eec1..960213bac 100644 --- a/testing/e2e/src/routes/api.anthropic-skills-wire.ts +++ b/testing/e2e/src/routes/api.anthropic-skills-wire.ts @@ -1,10 +1,21 @@ import { createFileRoute } from '@tanstack/react-router' import { chat, createChatOptions } from '@tanstack/ai' +import type { Tool } from '@tanstack/ai' import { createAnthropicChat } from '@tanstack/ai-anthropic' import { codeExecutionTool } from '@tanstack/ai-anthropic/tools' const DUMMY_KEY = 'sk-ant-e2e-test-dummy-key' +const customWebSearchTool = { + name: 'web_search', + description: 'Search an application index', + inputSchema: { + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }, +} satisfies Tool + /** * Drives the Anthropic chat adapter with a `codeExecutionTool` carrying a * hosted skill. A custom `fetch` implementation intercepts the outgoing @@ -138,6 +149,7 @@ export const Route = createFileRoute('/api/anthropic-skills-wire')({ ], }, ), + customWebSearchTool, ], })) { // Drain the stream. diff --git a/testing/e2e/tests/anthropic-skills-wire.spec.ts b/testing/e2e/tests/anthropic-skills-wire.spec.ts index 8d21f6e40..9f43ddaa7 100644 --- a/testing/e2e/tests/anthropic-skills-wire.spec.ts +++ b/testing/e2e/tests/anthropic-skills-wire.spec.ts @@ -76,4 +76,37 @@ test.describe('anthropic — code_execution skills wire format', () => { version: 'latest', }) }) + + test('an ordinary function named web_search stays a custom tool', async ({ + request, + }) => { + const res = await request.post('/api/anthropic-skills-wire') + expect(res.ok()).toBe(true) + const { ok, error, capturedRequest } = (await res.json()) as { + ok: boolean + error?: string + capturedRequest: { + body: Record | null + } | null + } + + if (!ok) { + throw new Error(`Route failed: ${error}`) + } + + const tools = capturedRequest?.body?.['tools'] as + | Array> + | undefined + const customTool = tools?.find((tool) => tool['name'] === 'web_search') + + expect(customTool).toMatchObject({ + name: 'web_search', + type: 'custom', + description: 'Search an application index', + input_schema: { + properties: { query: { type: 'string' } }, + required: ['query'], + }, + }) + }) }) From 2437e19c048f4cd8fa70fe5aff44ecca298a39e9 Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 12:31:54 +0200 Subject: [PATCH 2/7] fix: use stable Anthropic tool discrimination --- .changeset/tidy-searches-coexist.md | 2 +- .../src/tools/anthropic-provider-tool.ts | 56 +++++++++++++++++++ packages/ai-anthropic/src/tools/bash-tool.ts | 15 +++-- .../src/tools/code-execution-tool.ts | 15 +++-- .../src/tools/computer-use-tool.ts | 15 +++-- .../ai-anthropic/src/tools/memory-tool.ts | 15 +++-- .../src/tools/text-editor-tool.ts | 15 +++-- .../ai-anthropic/src/tools/tool-converter.ts | 29 ++-------- .../ai-anthropic/src/tools/web-fetch-tool.ts | 15 +++-- .../ai-anthropic/src/tools/web-search-tool.ts | 15 +++-- .../tests/provider-tools-smoke.test.ts | 30 ++++++---- 11 files changed, 146 insertions(+), 76 deletions(-) create mode 100644 packages/ai-anthropic/src/tools/anthropic-provider-tool.ts diff --git a/.changeset/tidy-searches-coexist.md b/.changeset/tidy-searches-coexist.md index 0b690b7a3..6ff952e77 100644 --- a/.changeset/tidy-searches-coexist.md +++ b/.changeset/tidy-searches-coexist.md @@ -2,4 +2,4 @@ '@tanstack/ai-anthropic': patch --- -Keep ordinary function tools named `web_search` distinct from Anthropic's native web-search tool by checking the provider metadata before conversion. +Classify Anthropic-native tools with a stable runtime discriminator so ordinary functions can use the same public names without depending on versioned provider wire identifiers. diff --git a/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts b/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts new file mode 100644 index 000000000..bd212f741 --- /dev/null +++ b/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts @@ -0,0 +1,56 @@ +import { brandProviderTool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' + +const ANTHROPIC_PROVIDER_TOOL_KIND = Symbol.for( + '@tanstack/ai-anthropic/provider-tool-kind', +) + +const ANTHROPIC_PROVIDER_TOOL_KINDS = { + bash: true, + code_execution: true, + computer_use: true, + memory: true, + text_editor: true, + web_fetch: true, + web_search: true, +} as const + +type AnthropicProviderToolKind = keyof typeof ANTHROPIC_PROVIDER_TOOL_KINDS + +type RuntimeAnthropicProviderTool = Tool & { + [ANTHROPIC_PROVIDER_TOOL_KIND]?: unknown +} + +/** + * Adds a stable runtime discriminator to an Anthropic-native tool. + * + * The core ProviderTool brand is intentionally type-only. Anthropic also needs + * a runtime discriminator because custom functions may use the same public + * names as native tools. A symbol keeps this marker off the provider wire, and + * Symbol.for keeps it stable if multiple copies of this package are loaded. + */ +export function brandAnthropicProviderTool< + T extends ProviderTool<'anthropic', AnthropicProviderToolKind>, +>(tool: Omit, toolKind: T['~toolKind']): T { + const brandedTool = brandProviderTool(tool) + Object.defineProperty(brandedTool, ANTHROPIC_PROVIDER_TOOL_KIND, { + value: toolKind, + enumerable: true, + }) + return brandedTool +} + +export function getAnthropicProviderToolKind( + tool: Tool, +): AnthropicProviderToolKind | undefined { + const toolKind = (tool as RuntimeAnthropicProviderTool)[ + ANTHROPIC_PROVIDER_TOOL_KIND + ] + if ( + typeof toolKind !== 'string' || + !(toolKind in ANTHROPIC_PROVIDER_TOOL_KINDS) + ) { + return undefined + } + return toolKind as AnthropicProviderToolKind +} diff --git a/packages/ai-anthropic/src/tools/bash-tool.ts b/packages/ai-anthropic/src/tools/bash-tool.ts index 995e592bf..660155995 100644 --- a/packages/ai-anthropic/src/tools/bash-tool.ts +++ b/packages/ai-anthropic/src/tools/bash-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { BetaToolBash20241022, BetaToolBash20250124, @@ -18,9 +18,12 @@ export function convertBashToolToAdapterFormat(tool: Tool): BashToolConfig { } export function bashTool(config: BashToolConfig): AnthropicBashTool { - return brandProviderTool({ - name: 'bash', - description: '', - metadata: config, - }) + return brandAnthropicProviderTool( + { + name: 'bash', + description: '', + metadata: config, + }, + 'bash', + ) } diff --git a/packages/ai-anthropic/src/tools/code-execution-tool.ts b/packages/ai-anthropic/src/tools/code-execution-tool.ts index 668ffbca3..cf4832506 100644 --- a/packages/ai-anthropic/src/tools/code-execution-tool.ts +++ b/packages/ai-anthropic/src/tools/code-execution-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { BetaCodeExecutionTool20250522, BetaCodeExecutionTool20250825, @@ -88,9 +88,12 @@ export function codeExecutionTool( config, ...(skills && { skills }), } - return brandProviderTool({ - name: 'code_execution', - description: '', - metadata, - }) + return brandAnthropicProviderTool( + { + name: 'code_execution', + description: '', + metadata, + }, + 'code_execution', + ) } diff --git a/packages/ai-anthropic/src/tools/computer-use-tool.ts b/packages/ai-anthropic/src/tools/computer-use-tool.ts index 0fb7017d4..c2f770479 100644 --- a/packages/ai-anthropic/src/tools/computer-use-tool.ts +++ b/packages/ai-anthropic/src/tools/computer-use-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { BetaToolComputerUse20241022, BetaToolComputerUse20250124, @@ -24,9 +24,12 @@ export function convertComputerUseToolToAdapterFormat( export function computerUseTool( config: ComputerUseToolConfig, ): AnthropicComputerUseTool { - return brandProviderTool({ - name: 'computer', - description: '', - metadata: config, - }) + return brandAnthropicProviderTool( + { + name: 'computer', + description: '', + metadata: config, + }, + 'computer_use', + ) } diff --git a/packages/ai-anthropic/src/tools/memory-tool.ts b/packages/ai-anthropic/src/tools/memory-tool.ts index 5848bca27..816934e2b 100644 --- a/packages/ai-anthropic/src/tools/memory-tool.ts +++ b/packages/ai-anthropic/src/tools/memory-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { BetaMemoryTool20250818 } from '@anthropic-ai/sdk/resources/beta' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -18,9 +18,12 @@ export function convertMemoryToolToAdapterFormat(tool: Tool): MemoryToolConfig { } export function memoryTool(config?: MemoryToolConfig): AnthropicMemoryTool { - return brandProviderTool({ - name: 'memory', - description: '', - metadata: config, - }) + return brandAnthropicProviderTool( + { + name: 'memory', + description: '', + metadata: config, + }, + 'memory', + ) } diff --git a/packages/ai-anthropic/src/tools/text-editor-tool.ts b/packages/ai-anthropic/src/tools/text-editor-tool.ts index bb0761c75..4ffed8f22 100644 --- a/packages/ai-anthropic/src/tools/text-editor-tool.ts +++ b/packages/ai-anthropic/src/tools/text-editor-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { ToolTextEditor20250124, ToolTextEditor20250429, @@ -28,9 +28,12 @@ export function convertTextEditorToolToAdapterFormat( export function textEditorTool( config: T, ): AnthropicTextEditorTool { - return brandProviderTool({ - name: 'str_replace_editor', - description: '', - metadata: config, - }) + return brandAnthropicProviderTool( + { + name: 'str_replace_editor', + description: '', + metadata: config, + }, + 'text_editor', + ) } diff --git a/packages/ai-anthropic/src/tools/tool-converter.ts b/packages/ai-anthropic/src/tools/tool-converter.ts index 4f61486af..7c0dc6a1f 100644 --- a/packages/ai-anthropic/src/tools/tool-converter.ts +++ b/packages/ai-anthropic/src/tools/tool-converter.ts @@ -1,3 +1,4 @@ +import { getAnthropicProviderToolKind } from './anthropic-provider-tool' import { convertBashToolToAdapterFormat } from './bash-tool' import { convertCodeExecutionToolToAdapterFormat } from './code-execution-tool' import { convertComputerUseToolToAdapterFormat } from './computer-use-tool' @@ -9,20 +10,6 @@ import { convertWebSearchToolToAdapterFormat } from './web-search-tool' import type { AnthropicTool } from './index' import type { Tool } from '@tanstack/ai' -function isAnthropicWebSearchTool(tool: Tool): boolean { - const metadata = tool.metadata - if (!metadata) { - return false - } - - return ( - 'type' in metadata && - metadata.type === 'web_search_20250305' && - 'name' in metadata && - metadata.name === 'web_search' - ) -} - /** * Converts standard Tool format to Anthropic-specific tool format * @@ -51,26 +38,22 @@ export function convertToolsToProviderFormat( tools: Array, ): Array { return tools.map((tool) => { - const name = tool.name - - switch (name) { + switch (getAnthropicProviderToolKind(tool)) { case 'bash': return convertBashToolToAdapterFormat(tool) case 'code_execution': return convertCodeExecutionToolToAdapterFormat(tool) - case 'computer': + case 'computer_use': return convertComputerUseToolToAdapterFormat(tool) case 'memory': return convertMemoryToolToAdapterFormat(tool) - case 'str_replace_editor': + case 'text_editor': return convertTextEditorToolToAdapterFormat(tool) case 'web_fetch': return convertWebFetchToolToAdapterFormat(tool) case 'web_search': - return isAnthropicWebSearchTool(tool) - ? convertWebSearchToolToAdapterFormat(tool) - : convertCustomToolToAdapterFormat(tool) - default: + return convertWebSearchToolToAdapterFormat(tool) + case undefined: return convertCustomToolToAdapterFormat(tool) } }) diff --git a/packages/ai-anthropic/src/tools/web-fetch-tool.ts b/packages/ai-anthropic/src/tools/web-fetch-tool.ts index 771c90825..2623f57e2 100644 --- a/packages/ai-anthropic/src/tools/web-fetch-tool.ts +++ b/packages/ai-anthropic/src/tools/web-fetch-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { BetaWebFetchTool20250910 } from '@anthropic-ai/sdk/resources/beta' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -23,9 +23,12 @@ export function convertWebFetchToolToAdapterFormat( export function webFetchTool( config?: Omit, ): AnthropicWebFetchTool { - return brandProviderTool({ - name: 'web_fetch', - description: '', - metadata: config, - }) + return brandAnthropicProviderTool( + { + name: 'web_fetch', + description: '', + metadata: config, + }, + 'web_fetch', + ) } diff --git a/packages/ai-anthropic/src/tools/web-search-tool.ts b/packages/ai-anthropic/src/tools/web-search-tool.ts index bc6aacb98..72f339649 100644 --- a/packages/ai-anthropic/src/tools/web-search-tool.ts +++ b/packages/ai-anthropic/src/tools/web-search-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandAnthropicProviderTool } from './anthropic-provider-tool' import type { WebSearchTool20250305 } from '@anthropic-ai/sdk/resources/messages' import type { CacheControl } from '../text/text-provider-options' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -92,9 +92,12 @@ export function webSearchTool( ): AnthropicWebSearchTool { validateDomains(config) validateUserLocation(config) - return brandProviderTool({ - name: 'web_search', - description: '', - metadata: config, - }) + return brandAnthropicProviderTool( + { + name: 'web_search', + description: '', + metadata: config, + }, + 'web_search', + ) } diff --git a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts index 77535eed4..2a4b27831 100644 --- a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts +++ b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts @@ -88,11 +88,13 @@ describe('Anthropic provider tool factories — runtime shape', () => { 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, + { + ...webSearchTool({ + name: 'web_search', + type: 'web_search_20250305', + max_uses: 2, + }), + } as unknown as Tool, ]) expect(converted).toMatchObject({ name: 'web_search', @@ -116,11 +118,19 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => { expect(names).toContain('bash') }) - it('keeps an ordinary function named web_search as a custom tool', () => { + it.each([ + 'bash', + 'code_execution', + 'computer', + 'memory', + 'str_replace_editor', + 'web_fetch', + 'web_search', + ])('keeps an ordinary function named %s as a custom tool', (name) => { const [converted] = convertToolsToProviderFormat([ { - name: 'web_search', - description: 'Search an application index', + name, + description: 'Run an application function', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, @@ -130,9 +140,9 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => { ]) expect(converted).toMatchObject({ - name: 'web_search', + name, type: 'custom', - description: 'Search an application index', + description: 'Run an application function', input_schema: { properties: { query: { type: 'string' } }, required: ['query'], From 95023eda41a0157cff0ba75c52347fc8424dde0a Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 12:41:32 +0200 Subject: [PATCH 3/7] fix: use plain-data Anthropic tool markers --- .../src/tools/anthropic-provider-tool.ts | 74 +++++++++++-------- packages/ai-anthropic/src/tools/bash-tool.ts | 7 +- .../src/tools/code-execution-tool.ts | 17 ++++- .../src/tools/computer-use-tool.ts | 9 ++- .../ai-anthropic/src/tools/memory-tool.ts | 10 ++- .../src/tools/text-editor-tool.ts | 9 ++- .../ai-anthropic/src/tools/web-fetch-tool.ts | 10 ++- .../ai-anthropic/src/tools/web-search-tool.ts | 7 +- .../tests/provider-tools-smoke.test.ts | 39 +++++++++- 9 files changed, 134 insertions(+), 48 deletions(-) diff --git a/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts b/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts index bd212f741..131d7397f 100644 --- a/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts +++ b/packages/ai-anthropic/src/tools/anthropic-provider-tool.ts @@ -1,56 +1,70 @@ import { brandProviderTool } from '@tanstack/ai' import type { ProviderTool, Tool } from '@tanstack/ai' -const ANTHROPIC_PROVIDER_TOOL_KIND = Symbol.for( - '@tanstack/ai-anthropic/provider-tool-kind', -) - const ANTHROPIC_PROVIDER_TOOL_KINDS = { - bash: true, - code_execution: true, - computer_use: true, - memory: true, - text_editor: true, - web_fetch: true, - web_search: true, + bash: 'anthropic.bash', + code_execution: 'anthropic.code_execution', + computer_use: 'anthropic.computer_use', + memory: 'anthropic.memory', + text_editor: 'anthropic.text_editor', + web_fetch: 'anthropic.web_fetch', + web_search: 'anthropic.web_search', } as const type AnthropicProviderToolKind = keyof typeof ANTHROPIC_PROVIDER_TOOL_KINDS -type RuntimeAnthropicProviderTool = Tool & { - [ANTHROPIC_PROVIDER_TOOL_KIND]?: unknown -} - /** * Adds a stable runtime discriminator to an Anthropic-native tool. * * The core ProviderTool brand is intentionally type-only. Anthropic also needs * a runtime discriminator because custom functions may use the same public - * names as native tools. A symbol keeps this marker off the provider wire, and - * Symbol.for keeps it stable if multiple copies of this package are loaded. + * names as native tools. Adapter metadata is the repository-wide extension + * point for this plain-data marker; converters remove it from the wire shape. */ export function brandAnthropicProviderTool< T extends ProviderTool<'anthropic', AnthropicProviderToolKind>, >(tool: Omit, toolKind: T['~toolKind']): T { - const brandedTool = brandProviderTool(tool) - Object.defineProperty(brandedTool, ANTHROPIC_PROVIDER_TOOL_KIND, { - value: toolKind, - enumerable: true, + return brandProviderTool({ + ...tool, + metadata: { + ...tool.metadata, + __kind: ANTHROPIC_PROVIDER_TOOL_KINDS[toolKind], + }, }) - return brandedTool } export function getAnthropicProviderToolKind( tool: Tool, ): AnthropicProviderToolKind | undefined { - const toolKind = (tool as RuntimeAnthropicProviderTool)[ - ANTHROPIC_PROVIDER_TOOL_KIND - ] - if ( - typeof toolKind !== 'string' || - !(toolKind in ANTHROPIC_PROVIDER_TOOL_KINDS) - ) { + const toolKind = tool.metadata?.['__kind'] + switch (toolKind) { + case 'anthropic.bash': + return 'bash' + case 'anthropic.code_execution': + return 'code_execution' + case 'anthropic.computer_use': + return 'computer_use' + case 'anthropic.memory': + return 'memory' + case 'anthropic.text_editor': + return 'text_editor' + case 'anthropic.web_fetch': + return 'web_fetch' + case 'anthropic.web_search': + return 'web_search' + default: + return undefined + } +} + +/** Returns adapter metadata without the internal runtime discriminator. */ +export function getAnthropicProviderToolMetadata( + tool: Tool, +): Tool['metadata'] { + if (!tool.metadata) { return undefined } - return toolKind as AnthropicProviderToolKind + const { __kind: _kind, ...metadata } = tool.metadata + void _kind + return metadata } diff --git a/packages/ai-anthropic/src/tools/bash-tool.ts b/packages/ai-anthropic/src/tools/bash-tool.ts index 660155995..61ed9fb1c 100644 --- a/packages/ai-anthropic/src/tools/bash-tool.ts +++ b/packages/ai-anthropic/src/tools/bash-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { BetaToolBash20241022, BetaToolBash20250124, @@ -13,7 +16,7 @@ export type BashTool = BashToolConfig export type AnthropicBashTool = ProviderTool<'anthropic', 'bash'> export function convertBashToolToAdapterFormat(tool: Tool): BashToolConfig { - const metadata = tool.metadata as BashToolConfig + const metadata = getAnthropicProviderToolMetadata(tool) as BashToolConfig return metadata } diff --git a/packages/ai-anthropic/src/tools/code-execution-tool.ts b/packages/ai-anthropic/src/tools/code-execution-tool.ts index cf4832506..32986c429 100644 --- a/packages/ai-anthropic/src/tools/code-execution-tool.ts +++ b/packages/ai-anthropic/src/tools/code-execution-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { BetaCodeExecutionTool20250522, BetaCodeExecutionTool20250825, @@ -56,7 +59,11 @@ export function convertCodeExecutionToolToAdapterFormat( export function readCodeExecutionConfig( tool: Tool, ): CodeExecutionToolConfig | undefined { - return (tool.metadata as CodeExecutionToolMetadata | undefined)?.config + return ( + getAnthropicProviderToolMetadata(tool) as + | CodeExecutionToolMetadata + | undefined + )?.config } /** @@ -66,7 +73,11 @@ export function readCodeExecutionConfig( export function readCodeExecutionSkills( tool: Tool, ): Array | undefined { - return (tool.metadata as CodeExecutionToolMetadata | undefined)?.skills + return ( + getAnthropicProviderToolMetadata(tool) as + | CodeExecutionToolMetadata + | undefined + )?.skills } export function codeExecutionTool( diff --git a/packages/ai-anthropic/src/tools/computer-use-tool.ts b/packages/ai-anthropic/src/tools/computer-use-tool.ts index c2f770479..d37f5ce07 100644 --- a/packages/ai-anthropic/src/tools/computer-use-tool.ts +++ b/packages/ai-anthropic/src/tools/computer-use-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { BetaToolComputerUse20241022, BetaToolComputerUse20250124, @@ -17,7 +20,9 @@ export type AnthropicComputerUseTool = ProviderTool<'anthropic', 'computer_use'> export function convertComputerUseToolToAdapterFormat( tool: Tool, ): ComputerUseToolConfig { - const metadata = tool.metadata as ComputerUseToolConfig + const metadata = getAnthropicProviderToolMetadata( + tool, + ) as ComputerUseToolConfig return metadata } diff --git a/packages/ai-anthropic/src/tools/memory-tool.ts b/packages/ai-anthropic/src/tools/memory-tool.ts index 816934e2b..fd80e5c6f 100644 --- a/packages/ai-anthropic/src/tools/memory-tool.ts +++ b/packages/ai-anthropic/src/tools/memory-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { BetaMemoryTool20250818 } from '@anthropic-ai/sdk/resources/beta' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -10,7 +13,10 @@ export type MemoryTool = MemoryToolConfig export type AnthropicMemoryTool = ProviderTool<'anthropic', 'memory'> export function convertMemoryToolToAdapterFormat(tool: Tool): MemoryToolConfig { - const metadata = tool.metadata as Omit + const metadata = getAnthropicProviderToolMetadata(tool) as Omit< + MemoryToolConfig, + 'type' + > return { type: 'memory_20250818', ...metadata, diff --git a/packages/ai-anthropic/src/tools/text-editor-tool.ts b/packages/ai-anthropic/src/tools/text-editor-tool.ts index 4ffed8f22..ccfbf53e1 100644 --- a/packages/ai-anthropic/src/tools/text-editor-tool.ts +++ b/packages/ai-anthropic/src/tools/text-editor-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { ToolTextEditor20250124, ToolTextEditor20250429, @@ -19,7 +22,9 @@ export type AnthropicTextEditorTool = ProviderTool<'anthropic', 'text_editor'> export function convertTextEditorToolToAdapterFormat( tool: Tool, ): TextEditorToolConfig { - const metadata = tool.metadata as TextEditorToolConfig + const metadata = getAnthropicProviderToolMetadata( + tool, + ) as TextEditorToolConfig return { ...metadata, } diff --git a/packages/ai-anthropic/src/tools/web-fetch-tool.ts b/packages/ai-anthropic/src/tools/web-fetch-tool.ts index 2623f57e2..d77065491 100644 --- a/packages/ai-anthropic/src/tools/web-fetch-tool.ts +++ b/packages/ai-anthropic/src/tools/web-fetch-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { BetaWebFetchTool20250910 } from '@anthropic-ai/sdk/resources/beta' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -12,7 +15,10 @@ export type AnthropicWebFetchTool = ProviderTool<'anthropic', 'web_fetch'> export function convertWebFetchToolToAdapterFormat( tool: Tool, ): WebFetchToolConfig { - const metadata = tool.metadata as Omit + const metadata = getAnthropicProviderToolMetadata(tool) as Omit< + WebFetchToolConfig, + 'type' | 'name' + > return { name: 'web_fetch', type: 'web_fetch_20250910', diff --git a/packages/ai-anthropic/src/tools/web-search-tool.ts b/packages/ai-anthropic/src/tools/web-search-tool.ts index 72f339649..539b1f0f4 100644 --- a/packages/ai-anthropic/src/tools/web-search-tool.ts +++ b/packages/ai-anthropic/src/tools/web-search-tool.ts @@ -1,4 +1,7 @@ -import { brandAnthropicProviderTool } from './anthropic-provider-tool' +import { + brandAnthropicProviderTool, + getAnthropicProviderToolMetadata, +} from './anthropic-provider-tool' import type { WebSearchTool20250305 } from '@anthropic-ai/sdk/resources/messages' import type { CacheControl } from '../text/text-provider-options' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -54,7 +57,7 @@ const validateUserLocation = (tool: WebSearchToolConfig) => { export function convertWebSearchToolToAdapterFormat( tool: Tool, ): WebSearchToolConfig { - const metadata = tool.metadata as { + const metadata = getAnthropicProviderToolMetadata(tool) as { allowedDomains?: Array | null blockedDomains?: Array | null maxUses?: number | null diff --git a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts index 2a4b27831..25375a49a 100644 --- a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts +++ b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts @@ -28,7 +28,7 @@ describe('Anthropic provider tool factories — runtime shape', () => { }) expect(tool.name).toBe('web_search') expect(tool).toHaveProperty('description') - expect(tool).toHaveProperty('metadata') + expect(tool.metadata).toMatchObject({ __kind: 'anthropic.web_search' }) }) it('codeExecutionTool produces a Tool-shaped object', () => { @@ -96,22 +96,55 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => { }), } as unknown as Tool, ]) - expect(converted).toMatchObject({ + expect(converted).toEqual({ name: 'web_search', type: 'web_search_20250305', + cache_control: null, }) }) + it('keeps provider identity through a plain-data round trip', () => { + const tool = JSON.parse( + JSON.stringify( + webSearchTool({ + name: 'web_search', + type: 'web_search_20250305', + }), + ), + ) as Tool + + expect(convertToolsToProviderFormat([tool])).toEqual([ + { + name: 'web_search', + type: 'web_search_20250305', + cache_control: null, + }, + ]) + }) + it('converts multiple provider tools in one call', () => { const converted = convertToolsToProviderFormat([ webSearchTool({ name: 'web_search', type: 'web_search_20250305' }), + webFetchTool(), codeExecutionTool({ name: 'code_execution', type: 'code_execution_20250825', }), bashTool({ name: 'bash', type: 'bash_20250124' }), + computerUseTool({ + type: 'computer_20250124', + name: 'computer', + display_width_px: 1024, + display_height_px: 768, + }), + memoryTool(), + textEditorTool({ + type: 'text_editor_20250124', + name: 'str_replace_editor', + }), ] as unknown as Tool[]) - expect(converted).toHaveLength(3) + expect(converted).toHaveLength(7) + expect(JSON.stringify(converted)).not.toContain('__kind') const names = converted.map((t) => ('name' in t ? t.name : undefined)) expect(names).toContain('web_search') expect(names).toContain('code_execution') From 84d4df91110933bd8b1a9f65719f9aab8afd249c Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 12:57:33 +0200 Subject: [PATCH 4/7] fix: distinguish OpenAI provider tools at runtime --- .../ai-openai/src/tools/computer-use-tool.ts | 4 +- .../openai-base/src/tools/apply-patch-tool.ts | 14 ++- .../src/tools/code-interpreter-tool.ts | 25 +++-- .../src/tools/computer-use-tool.ts | 21 ++-- packages/openai-base/src/tools/custom-tool.ts | 21 ++-- .../openai-base/src/tools/file-search-tool.ts | 21 ++-- .../src/tools/image-generation-tool.ts | 30 +++--- .../openai-base/src/tools/local-shell-tool.ts | 14 ++- packages/openai-base/src/tools/mcp-tool.ts | 22 +++-- .../src/tools/openai-provider-tool.ts | 72 ++++++++++++++ packages/openai-base/src/tools/shell-tool.ts | 26 +++-- .../openai-base/src/tools/tool-converter.ts | 69 +++++-------- .../src/tools/web-search-preview-tool.ts | 25 +++-- .../openai-base/src/tools/web-search-tool.ts | 28 ++++-- .../tests/provider-tool-dispatch.test.ts | 98 +++++++++++++++++++ 15 files changed, 362 insertions(+), 128 deletions(-) create mode 100644 packages/openai-base/src/tools/openai-provider-tool.ts create mode 100644 packages/openai-base/tests/provider-tool-dispatch.test.ts diff --git a/packages/ai-openai/src/tools/computer-use-tool.ts b/packages/ai-openai/src/tools/computer-use-tool.ts index 8226c7acd..3f65dddb7 100644 --- a/packages/ai-openai/src/tools/computer-use-tool.ts +++ b/packages/ai-openai/src/tools/computer-use-tool.ts @@ -12,8 +12,8 @@ export { // runtime tool name (`computer_use_preview`). The brand matches the model-meta // tool-capability union (`tools: ['computer_use', ...]`) used to gate which // models can construct this tool at compile time, while the runtime name -// matches the OpenAI SDK's literal `'computer_use_preview'` that the -// special-tool dispatcher in `convertToolsToProviderFormat` switches on. +// matches the OpenAI SDK's literal `'computer_use_preview'`. The base factory +// records the canonical `computer_use` kind separately in adapter metadata. export type OpenAIComputerUseTool = ProviderTool<'openai', 'computer_use'> /** diff --git a/packages/openai-base/src/tools/apply-patch-tool.ts b/packages/openai-base/src/tools/apply-patch-tool.ts index d97594d6a..0d3eda32f 100644 --- a/packages/openai-base/src/tools/apply-patch-tool.ts +++ b/packages/openai-base/src/tools/apply-patch-tool.ts @@ -1,3 +1,4 @@ +import { openAIProviderTool } from './openai-provider-tool' import type { ApplyPatchTool as ApplyPatchToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -24,9 +25,12 @@ export function convertApplyPatchToolToAdapterFormat( * re-wrap this in their own package. */ export function applyPatchTool(): Tool { - return { - name: 'apply_patch', - description: 'Apply a patch to modify files', - metadata: {}, - } + return openAIProviderTool( + { + name: 'apply_patch', + description: 'Apply a patch to modify files', + metadata: {}, + }, + 'apply_patch', + ) } diff --git a/packages/openai-base/src/tools/code-interpreter-tool.ts b/packages/openai-base/src/tools/code-interpreter-tool.ts index 448680da7..1a75534b2 100644 --- a/packages/openai-base/src/tools/code-interpreter-tool.ts +++ b/packages/openai-base/src/tools/code-interpreter-tool.ts @@ -14,7 +14,9 @@ export type CodeInterpreterTool = CodeInterpreterToolConfig export function convertCodeInterpreterToolToAdapterFormat( tool: Tool, ): CodeInterpreterToolConfig { - const metadata = tool.metadata as CodeInterpreterToolConfig + const metadata = getOpenAIProviderToolMetadata( + tool, + ) as CodeInterpreterToolConfig return { type: 'code_interpreter', container: metadata.container, @@ -30,12 +32,19 @@ export function convertCodeInterpreterToolToAdapterFormat( export function codeInterpreterTool( container: CodeInterpreterToolConfig, ): Tool { - return { - name: 'code_interpreter', - description: 'Execute code in a sandboxed environment', - metadata: { - type: 'code_interpreter', - container, + return openAIProviderTool( + { + name: 'code_interpreter', + description: 'Execute code in a sandboxed environment', + metadata: { + type: 'code_interpreter', + container, + }, }, - } + 'code_interpreter', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/computer-use-tool.ts b/packages/openai-base/src/tools/computer-use-tool.ts index 4ccb1d108..b69be548d 100644 --- a/packages/openai-base/src/tools/computer-use-tool.ts +++ b/packages/openai-base/src/tools/computer-use-tool.ts @@ -12,7 +12,7 @@ export type ComputerUseTool = ComputerUseToolConfig export function convertComputerUseToolToAdapterFormat( tool: Tool, ): ComputerUseToolConfig { - const metadata = tool.metadata as ComputerUseToolConfig + const metadata = getOpenAIProviderToolMetadata(tool) as ComputerUseToolConfig return { type: 'computer_use_preview', display_height: metadata.display_height, @@ -28,11 +28,18 @@ export function convertComputerUseToolToAdapterFormat( * re-wrap this in their own package. */ export function computerUseTool(toolData: ComputerUseToolConfig): Tool { - return { - name: 'computer_use_preview', - description: 'Control a virtual computer', - metadata: { - ...toolData, + return openAIProviderTool( + { + name: 'computer_use_preview', + description: 'Control a virtual computer', + metadata: { + ...toolData, + }, }, - } + 'computer_use', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/custom-tool.ts b/packages/openai-base/src/tools/custom-tool.ts index 9258270df..57a10b85e 100644 --- a/packages/openai-base/src/tools/custom-tool.ts +++ b/packages/openai-base/src/tools/custom-tool.ts @@ -10,7 +10,7 @@ export type CustomTool = CustomToolConfig * Converts a standard Tool to OpenAI CustomTool format */ export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { - const metadata = tool.metadata as CustomToolConfig + const metadata = getOpenAIProviderToolMetadata(tool) as CustomToolConfig // Conditional spread: the SDK's `CustomToolConfig` declares optional // fields as `description?: string` (no `| undefined`) under // exactOptionalPropertyTypes, so we omit absent fields rather than @@ -29,11 +29,18 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig { * Creates a standard Tool from CustomTool parameters. */ export function customTool(toolData: CustomToolConfig): Tool { - return { - name: 'custom', - description: toolData.description || 'A custom tool', - metadata: { - ...toolData, + return openAIProviderTool( + { + name: 'custom', + description: toolData.description || 'A custom tool', + metadata: { + ...toolData, + }, }, - } + 'custom', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/file-search-tool.ts b/packages/openai-base/src/tools/file-search-tool.ts index 86c9eb02d..6ca6520dd 100644 --- a/packages/openai-base/src/tools/file-search-tool.ts +++ b/packages/openai-base/src/tools/file-search-tool.ts @@ -21,7 +21,7 @@ export type FileSearchTool = FileSearchToolConfig export function convertFileSearchToolToAdapterFormat( tool: Tool, ): FileSearchToolConfig { - const metadata = tool.metadata as FileSearchToolConfig + const metadata = getOpenAIProviderToolMetadata(tool) as FileSearchToolConfig // Conditional spread: SDK's `FileSearchToolConfig` declares the // optional fields without `| undefined`, so we omit absent values // rather than passing them through as explicit `undefined`. @@ -46,13 +46,20 @@ export function convertFileSearchToolToAdapterFormat( */ export function fileSearchTool(toolData: FileSearchToolConfig): Tool { validateMaxNumResults(toolData.max_num_results) - return { - name: 'file_search', - description: 'Search files in vector stores', - metadata: { - ...toolData, + return openAIProviderTool( + { + name: 'file_search', + description: 'Search files in vector stores', + metadata: { + ...toolData, + }, }, - } + 'file_search', + ) } export { validateMaxNumResults } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/image-generation-tool.ts b/packages/openai-base/src/tools/image-generation-tool.ts index b5150e25d..2bd75122e 100644 --- a/packages/openai-base/src/tools/image-generation-tool.ts +++ b/packages/openai-base/src/tools/image-generation-tool.ts @@ -16,15 +16,16 @@ const validatePartialImages = (value: number | undefined) => { /** * Converts a standard Tool to OpenAI ImageGenerationTool format. Spread - * `metadata` first, then force `type: 'image_generation'` last — otherwise a - * `metadata.type` snuck in by a hand-built tool would shadow the literal and - * the dispatcher (which routed by `tool.name`) would emit a tool whose - * runtime `type` doesn't match `image_generation`. + * `metadata` first, then force `type: 'image_generation'` last so a stray + * `metadata.type` cannot shadow the wire discriminator. */ export function convertImageGenerationToolToAdapterFormat( tool: Tool, ): ImageGenerationToolConfig { - const metadata = tool.metadata as Omit + const metadata = getOpenAIProviderToolMetadata(tool) as Omit< + ImageGenerationToolConfig, + 'type' + > return { ...metadata, type: 'image_generation', @@ -41,13 +42,20 @@ export function imageGenerationTool( toolData: Omit, ): Tool { validatePartialImages(toolData.partial_images) - return { - name: 'image_generation', - description: 'Generate images based on text descriptions', - metadata: { - ...toolData, + return openAIProviderTool( + { + name: 'image_generation', + description: 'Generate images based on text descriptions', + metadata: { + ...toolData, + }, }, - } + 'image_generation', + ) } export { validatePartialImages } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/local-shell-tool.ts b/packages/openai-base/src/tools/local-shell-tool.ts index 359c9611f..cd8d14648 100644 --- a/packages/openai-base/src/tools/local-shell-tool.ts +++ b/packages/openai-base/src/tools/local-shell-tool.ts @@ -1,3 +1,4 @@ +import { openAIProviderTool } from './openai-provider-tool' import type { Tool as SDKTool } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -26,9 +27,12 @@ export function convertLocalShellToolToAdapterFormat( * re-wrap this in their own package. */ export function localShellTool(): Tool { - return { - name: 'local_shell', - description: 'Execute local shell commands', - metadata: {}, - } + return openAIProviderTool( + { + name: 'local_shell', + description: 'Execute local shell commands', + metadata: {}, + }, + 'local_shell', + ) } diff --git a/packages/openai-base/src/tools/mcp-tool.ts b/packages/openai-base/src/tools/mcp-tool.ts index 26d9a59f7..e1b6ea620 100644 --- a/packages/openai-base/src/tools/mcp-tool.ts +++ b/packages/openai-base/src/tools/mcp-tool.ts @@ -21,7 +21,10 @@ export function validateMCPtool(tool: MCPToolConfig) { * Converts a standard Tool to OpenAI MCPTool format */ export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig { - const metadata = tool.metadata as Omit + const metadata = getOpenAIProviderToolMetadata(tool) as Omit< + MCPToolConfig, + 'type' + > const mcpTool: MCPToolConfig = { ...metadata, @@ -41,9 +44,16 @@ export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig { export function mcpTool(toolData: Omit): Tool { validateMCPtool({ ...toolData, type: 'mcp' }) - return { - name: 'mcp', - description: toolData.server_description || '', - metadata: toolData, - } + return openAIProviderTool( + { + name: 'mcp', + description: toolData.server_description || '', + metadata: toolData, + }, + 'mcp', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/openai-provider-tool.ts b/packages/openai-base/src/tools/openai-provider-tool.ts new file mode 100644 index 000000000..ce1f103aa --- /dev/null +++ b/packages/openai-base/src/tools/openai-provider-tool.ts @@ -0,0 +1,72 @@ +import type { Tool } from '@tanstack/ai' + +const OPENAI_PROVIDER_TOOL_KINDS = { + apply_patch: 'openai.apply_patch', + code_interpreter: 'openai.code_interpreter', + computer_use: 'openai.computer_use', + custom: 'openai.custom', + file_search: 'openai.file_search', + image_generation: 'openai.image_generation', + local_shell: 'openai.local_shell', + mcp: 'openai.mcp', + shell: 'openai.shell', + web_search: 'openai.web_search', + web_search_preview: 'openai.web_search_preview', +} as const + +export type OpenAIProviderToolKind = keyof typeof OPENAI_PROVIDER_TOOL_KINDS + +export function openAIProviderTool( + tool: Tool, + toolKind: OpenAIProviderToolKind, +): Tool { + return { + ...tool, + metadata: { + ...tool.metadata, + __kind: OPENAI_PROVIDER_TOOL_KINDS[toolKind], + }, + } +} + +export function getOpenAIProviderToolKind( + tool: Tool, +): OpenAIProviderToolKind | undefined { + const toolKind = tool.metadata?.['__kind'] + switch (toolKind) { + case 'openai.apply_patch': + return 'apply_patch' + case 'openai.code_interpreter': + return 'code_interpreter' + case 'openai.computer_use': + return 'computer_use' + case 'openai.custom': + return 'custom' + case 'openai.file_search': + return 'file_search' + case 'openai.image_generation': + return 'image_generation' + case 'openai.local_shell': + return 'local_shell' + case 'openai.mcp': + return 'mcp' + case 'openai.shell': + return 'shell' + case 'openai.web_search': + return 'web_search' + case 'openai.web_search_preview': + return 'web_search_preview' + default: + return undefined + } +} + +/** Returns adapter metadata without the internal runtime discriminator. */ +export function getOpenAIProviderToolMetadata(tool: Tool): Tool['metadata'] { + if (!tool.metadata) { + return undefined + } + const { __kind: _kind, ...metadata } = tool.metadata + void _kind + return metadata +} diff --git a/packages/openai-base/src/tools/shell-tool.ts b/packages/openai-base/src/tools/shell-tool.ts index 66df1a946..4e893cd4f 100644 --- a/packages/openai-base/src/tools/shell-tool.ts +++ b/packages/openai-base/src/tools/shell-tool.ts @@ -21,7 +21,8 @@ export interface ShellToolFactoryConfig { * `environment` (container config + skills) stored in metadata. */ export function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig { - const metadata = (tool.metadata ?? {}) as ShellToolFactoryConfig + const metadata = (getOpenAIProviderToolMetadata(tool) ?? + {}) as ShellToolFactoryConfig return { type: 'shell', ...(metadata.environment !== undefined && { @@ -37,13 +38,20 @@ export function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig { * re-wrap this in their own package. */ export function shellTool(config: ShellToolFactoryConfig = {}): Tool { - return { - name: 'shell', - description: 'Execute shell commands', - metadata: { - ...(config.environment !== undefined && { - environment: config.environment, - }), + return openAIProviderTool( + { + name: 'shell', + description: 'Execute shell commands', + metadata: { + ...(config.environment !== undefined && { + environment: config.environment, + }), + }, }, - } + 'shell', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/tool-converter.ts b/packages/openai-base/src/tools/tool-converter.ts index 2855cd3f0..ea126f0c0 100644 --- a/packages/openai-base/src/tools/tool-converter.ts +++ b/packages/openai-base/src/tools/tool-converter.ts @@ -7,26 +7,13 @@ import { convertFunctionToolToAdapterFormat } from './function-tool' import { convertImageGenerationToolToAdapterFormat } from './image-generation-tool' import { convertLocalShellToolToAdapterFormat } from './local-shell-tool' import { convertMCPToolToAdapterFormat } from './mcp-tool' +import { getOpenAIProviderToolKind } from './openai-provider-tool' import { convertShellToolToAdapterFormat } from './shell-tool' import { convertWebSearchPreviewToolToAdapterFormat } from './web-search-preview-tool' import { convertWebSearchToolToAdapterFormat } from './web-search-tool' import type { OpenAITool } from './index' import type { Tool } from '@tanstack/ai' -const SPECIAL_TOOL_NAMES = new Set([ - 'apply_patch', - 'code_interpreter', - 'computer_use_preview', - 'file_search', - 'image_generation', - 'local_shell', - 'mcp', - 'shell', - 'web_search_preview', - 'web_search', - 'custom', -]) - /** * Converts an array of standard Tools to OpenAI-specific format */ @@ -34,35 +21,31 @@ export function convertToolsToProviderFormat( tools: Array, ): Array { return tools.map((tool) => { - const toolName = tool.name - - if (SPECIAL_TOOL_NAMES.has(toolName)) { - switch (toolName) { - case 'apply_patch': - return convertApplyPatchToolToAdapterFormat(tool) - case 'code_interpreter': - return convertCodeInterpreterToolToAdapterFormat(tool) - case 'computer_use_preview': - return convertComputerUseToolToAdapterFormat(tool) - case 'file_search': - return convertFileSearchToolToAdapterFormat(tool) - case 'image_generation': - return convertImageGenerationToolToAdapterFormat(tool) - case 'local_shell': - return convertLocalShellToolToAdapterFormat(tool) - case 'mcp': - return convertMCPToolToAdapterFormat(tool) - case 'shell': - return convertShellToolToAdapterFormat(tool) - case 'web_search_preview': - return convertWebSearchPreviewToolToAdapterFormat(tool) - case 'web_search': - return convertWebSearchToolToAdapterFormat(tool) - case 'custom': - return convertCustomToolToAdapterFormat(tool) - } + switch (getOpenAIProviderToolKind(tool)) { + case 'apply_patch': + return convertApplyPatchToolToAdapterFormat(tool) + case 'code_interpreter': + return convertCodeInterpreterToolToAdapterFormat(tool) + case 'computer_use': + return convertComputerUseToolToAdapterFormat(tool) + case 'custom': + return convertCustomToolToAdapterFormat(tool) + case 'file_search': + return convertFileSearchToolToAdapterFormat(tool) + case 'image_generation': + return convertImageGenerationToolToAdapterFormat(tool) + case 'local_shell': + return convertLocalShellToolToAdapterFormat(tool) + case 'mcp': + return convertMCPToolToAdapterFormat(tool) + case 'shell': + return convertShellToolToAdapterFormat(tool) + case 'web_search_preview': + return convertWebSearchPreviewToolToAdapterFormat(tool) + case 'web_search': + return convertWebSearchToolToAdapterFormat(tool) + case undefined: + return convertFunctionToolToAdapterFormat(tool) } - - return convertFunctionToolToAdapterFormat(tool) }) } diff --git a/packages/openai-base/src/tools/web-search-preview-tool.ts b/packages/openai-base/src/tools/web-search-preview-tool.ts index 18c71525f..853eb8d5b 100644 --- a/packages/openai-base/src/tools/web-search-preview-tool.ts +++ b/packages/openai-base/src/tools/web-search-preview-tool.ts @@ -9,13 +9,15 @@ export type WebSearchPreviewTool = WebSearchPreviewToolConfig /** * Converts a standard Tool to OpenAI WebSearchPreviewTool format. Force the * literal `type: 'web_search_preview'` instead of trusting `metadata.type`, - * since a hand-authored tool with a missing or wrong `type` would emit a - * malformed payload while the dispatcher already routed by `tool.name`. + * so a missing or wrong metadata type cannot produce a malformed payload. */ export function convertWebSearchPreviewToolToAdapterFormat( tool: Tool, ): WebSearchPreviewToolConfig { - const metadata = tool.metadata as Omit + const metadata = getOpenAIProviderToolMetadata(tool) as Omit< + WebSearchPreviewToolConfig, + 'type' + > return { ...metadata, type: 'web_search_preview', @@ -31,9 +33,16 @@ export function convertWebSearchPreviewToolToAdapterFormat( export function webSearchPreviewTool( toolData: WebSearchPreviewToolConfig, ): Tool { - return { - name: 'web_search_preview', - description: 'Search the web (preview version)', - metadata: toolData, - } + return openAIProviderTool( + { + name: 'web_search_preview', + description: 'Search the web (preview version)', + metadata: toolData, + }, + 'web_search_preview', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/web-search-tool.ts b/packages/openai-base/src/tools/web-search-tool.ts index eb84f31d1..eb6d377c7 100644 --- a/packages/openai-base/src/tools/web-search-tool.ts +++ b/packages/openai-base/src/tools/web-search-tool.ts @@ -8,15 +8,16 @@ export type WebSearchTool = WebSearchToolConfig /** * Converts a standard Tool to OpenAI WebSearchTool format. Spread `metadata` - * first, then force `type: 'web_search'` last to keep the runtime `type` - * matching the discriminator the dispatcher routed by — otherwise a tool - * authored by hand with a different `metadata.type` would emit a malformed - * payload. + * first, then force `type: 'web_search'` last so a different metadata type + * cannot produce a malformed payload. */ export function convertWebSearchToolToAdapterFormat( tool: Tool, ): WebSearchToolConfig { - const metadata = tool.metadata as Omit + const metadata = getOpenAIProviderToolMetadata(tool) as Omit< + WebSearchToolConfig, + 'type' + > return { ...metadata, type: 'web_search', @@ -30,9 +31,16 @@ export function convertWebSearchToolToAdapterFormat( * re-wrap this in their own package. */ export function webSearchTool(toolData: WebSearchToolConfig): Tool { - return { - name: 'web_search', - description: 'Search the web', - metadata: toolData, - } + return openAIProviderTool( + { + name: 'web_search', + description: 'Search the web', + metadata: toolData, + }, + 'web_search', + ) } +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' diff --git a/packages/openai-base/tests/provider-tool-dispatch.test.ts b/packages/openai-base/tests/provider-tool-dispatch.test.ts new file mode 100644 index 000000000..0805b5ea1 --- /dev/null +++ b/packages/openai-base/tests/provider-tool-dispatch.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest' +import { + applyPatchTool, + codeInterpreterTool, + computerUseTool, + customTool, + fileSearchTool, + imageGenerationTool, + localShellTool, + mcpTool, + shellTool, + webSearchPreviewTool, + webSearchTool, +} from '../src/tools' +import { convertToolsToProviderFormat } from '../src/tools/tool-converter' +import type { Tool } from '@tanstack/ai' + +const PROVIDER_TOOL_NAMES = [ + 'apply_patch', + 'code_interpreter', + 'computer_use_preview', + 'custom', + 'file_search', + 'image_generation', + 'local_shell', + 'mcp', + 'shell', + 'web_search_preview', + 'web_search', +] as const + +describe('OpenAI provider tool dispatch', () => { + it.each(PROVIDER_TOOL_NAMES)( + 'keeps an ordinary function named %s as a function tool', + (name) => { + const [converted] = convertToolsToProviderFormat([ + { + name, + description: 'Run an application function', + inputSchema: { + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }, + } satisfies Tool, + ]) + + expect(converted).toMatchObject({ type: 'function', name }) + }, + ) + + it('converts every factory tool without leaking its runtime marker', () => { + const converted = convertToolsToProviderFormat([ + applyPatchTool(), + codeInterpreterTool({ + type: 'code_interpreter', + container: { type: 'auto' }, + }), + computerUseTool({ + type: 'computer_use_preview', + display_height: 768, + display_width: 1024, + environment: 'linux', + }), + customTool({ + type: 'custom', + name: 'lookup_order', + description: 'Look up an order', + }), + fileSearchTool({ + type: 'file_search', + vector_store_ids: ['vs_123'], + }), + imageGenerationTool({}), + localShellTool(), + mcpTool({ + server_label: 'my-server', + server_url: 'https://example.com/mcp', + }), + shellTool(), + webSearchPreviewTool({ type: 'web_search_preview' }), + webSearchTool({ type: 'web_search' }), + ]) + + expect(converted).toHaveLength(PROVIDER_TOOL_NAMES.length) + expect(JSON.stringify(converted)).not.toContain('__kind') + }) + + it('keeps provider identity through a plain-data round trip', () => { + const tool = JSON.parse( + JSON.stringify(webSearchTool({ type: 'web_search' })), + ) as Tool + + expect(convertToolsToProviderFormat([tool])).toEqual([ + { type: 'web_search' }, + ]) + }) +}) From a2c4050df7e558d93746f5a9a4519fb9e179ed89 Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 12:57:34 +0200 Subject: [PATCH 5/7] fix: distinguish Gemini provider tools at runtime --- .../experimental/text-interactions/adapter.ts | 34 ++++--- .../src/tools/code-execution-tool.ts | 15 +-- .../ai-gemini/src/tools/computer-use-tool.ts | 32 ++++--- .../ai-gemini/src/tools/file-search-tool.ts | 20 ++-- .../src/tools/gemini-provider-tool.ts | 60 ++++++++++++ .../ai-gemini/src/tools/google-maps-tool.ts | 20 ++-- .../src/tools/google-search-retriveal-tool.ts | 22 +++-- .../ai-gemini/src/tools/google-search-tool.ts | 20 ++-- .../ai-gemini/src/tools/tool-converter.ts | 7 +- .../ai-gemini/src/tools/url-context-tool.ts | 15 +-- .../tests/provider-tool-dispatch.test.ts | 66 +++++++++++++ .../tests/text-interactions-adapter.test.ts | 93 +++++++++++++------ 12 files changed, 307 insertions(+), 97 deletions(-) create mode 100644 packages/ai-gemini/src/tools/gemini-provider-tool.ts create mode 100644 packages/ai-gemini/tests/provider-tool-dispatch.test.ts diff --git a/packages/ai-gemini/src/experimental/text-interactions/adapter.ts b/packages/ai-gemini/src/experimental/text-interactions/adapter.ts index 9d1de8ffb..5c0cf91e4 100644 --- a/packages/ai-gemini/src/experimental/text-interactions/adapter.ts +++ b/packages/ai-gemini/src/experimental/text-interactions/adapter.ts @@ -6,6 +6,10 @@ import { generateId, getGeminiApiKeyFromEnv, } from '../../utils/client' +import { + getGeminiProviderToolKind, + getGeminiProviderToolMetadata, +} from '../../tools/gemini-provider-tool' import type { InternalLogger } from '@tanstack/ai/adapter-internals' import type { GeminiChatModelToolCapabilitiesByName, @@ -785,16 +789,24 @@ function convertToolsToInteractionsFormat( const result: Array = [] for (const tool of tools) { - switch (tool.name) { + switch (getGeminiProviderToolKind(tool)) { case 'google_search': { - const metadata = (tool.metadata ?? {}) as { - search_types?: Array<'web_search' | 'image_search'> + const metadata = (getGeminiProviderToolMetadata(tool) ?? {}) as { + searchTypes?: { + webSearch?: unknown + imageSearch?: unknown + } + } + const searchTypes: Array<'web_search' | 'image_search'> = [] + if (metadata.searchTypes?.webSearch !== undefined) { + searchTypes.push('web_search') + } + if (metadata.searchTypes?.imageSearch !== undefined) { + searchTypes.push('image_search') } result.push({ type: 'google_search', - ...(metadata.search_types - ? { search_types: metadata.search_types } - : {}), + ...(searchTypes.length > 0 ? { search_types: searchTypes } : {}), }) break } @@ -807,7 +819,7 @@ function convertToolsToInteractionsFormat( break } case 'file_search': { - const metadata = (tool.metadata ?? {}) as { + const metadata = (getGeminiProviderToolMetadata(tool) ?? {}) as { fileSearchStoreNames?: Array topK?: number metadataFilter?: string @@ -825,7 +837,7 @@ function convertToolsToInteractionsFormat( break } case 'computer_use': { - const metadata = (tool.metadata ?? {}) as { + const metadata = (getGeminiProviderToolMetadata(tool) ?? {}) as { environment?: string excludedPredefinedFunctions?: Array } @@ -856,11 +868,7 @@ function convertToolsToInteractionsFormat( throw new Error( '`google_maps` is not yet supported on the Gemini Interactions API. Use `geminiText()` for Google Maps grounding.', ) - case 'mcp_server': - throw new Error( - '`mcp_server` is not yet supported on the `geminiTextInteractions()` adapter.', - ) - default: { + case undefined: { if (!tool.description) { throw new Error( `Tool ${tool.name} requires a description for the Gemini Interactions adapter`, diff --git a/packages/ai-gemini/src/tools/code-execution-tool.ts b/packages/ai-gemini/src/tools/code-execution-tool.ts index dfe5bba2d..bd2b1f3ec 100644 --- a/packages/ai-gemini/src/tools/code-execution-tool.ts +++ b/packages/ai-gemini/src/tools/code-execution-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandGeminiProviderTool } from './gemini-provider-tool' import type { ProviderTool, Tool } from '@tanstack/ai' export interface CodeExecutionToolConfig {} @@ -15,9 +15,12 @@ export function convertCodeExecutionToolToAdapterFormat(_tool: Tool) { } export function codeExecutionTool(): GeminiCodeExecutionTool { - return brandProviderTool({ - name: 'code_execution', - description: '', - metadata: {}, - }) + return brandGeminiProviderTool( + { + name: 'code_execution', + description: '', + metadata: {}, + }, + 'code_execution', + ) } diff --git a/packages/ai-gemini/src/tools/computer-use-tool.ts b/packages/ai-gemini/src/tools/computer-use-tool.ts index 3dcb66346..48b2dc0e2 100644 --- a/packages/ai-gemini/src/tools/computer-use-tool.ts +++ b/packages/ai-gemini/src/tools/computer-use-tool.ts @@ -1,4 +1,7 @@ -import { brandProviderTool } from '@tanstack/ai' +import { + brandGeminiProviderTool, + getGeminiProviderToolMetadata, +} from './gemini-provider-tool' import type { ComputerUse } from '@google/genai' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -10,7 +13,7 @@ export type ComputerUseTool = ComputerUseToolConfig export type GeminiComputerUseTool = ProviderTool<'gemini', 'computer_use'> export function convertComputerUseToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as ComputerUseToolConfig + const metadata = getGeminiProviderToolMetadata(tool) as ComputerUseToolConfig // Vendor `ComputerUse` fields are `field?: T` (no `| undefined`) under EOPT, // so spread each field conditionally rather than emitting explicit // `undefined`s on the wire payload. @@ -29,16 +32,19 @@ export function convertComputerUseToolToAdapterFormat(tool: Tool) { export function computerUseTool( config: ComputerUseToolConfig, ): GeminiComputerUseTool { - return brandProviderTool({ - name: 'computer_use', - description: '', - metadata: { - ...(config.environment !== undefined && { - environment: config.environment, - }), - ...(config.excludedPredefinedFunctions !== undefined && { - excludedPredefinedFunctions: config.excludedPredefinedFunctions, - }), + return brandGeminiProviderTool( + { + name: 'computer_use', + description: '', + metadata: { + ...(config.environment !== undefined && { + environment: config.environment, + }), + ...(config.excludedPredefinedFunctions !== undefined && { + excludedPredefinedFunctions: config.excludedPredefinedFunctions, + }), + }, }, - }) + 'computer_use', + ) } diff --git a/packages/ai-gemini/src/tools/file-search-tool.ts b/packages/ai-gemini/src/tools/file-search-tool.ts index 39695e90e..0d38144ae 100644 --- a/packages/ai-gemini/src/tools/file-search-tool.ts +++ b/packages/ai-gemini/src/tools/file-search-tool.ts @@ -1,4 +1,7 @@ -import { brandProviderTool } from '@tanstack/ai' +import { + brandGeminiProviderTool, + getGeminiProviderToolMetadata, +} from './gemini-provider-tool' import type { FileSearch } from '@google/genai' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -10,7 +13,7 @@ export type FileSearchTool = FileSearchToolConfig export type GeminiFileSearchTool = ProviderTool<'gemini', 'file_search'> export function convertFileSearchToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as FileSearchToolConfig + const metadata = getGeminiProviderToolMetadata(tool) as FileSearchToolConfig return { fileSearch: metadata, } @@ -19,9 +22,12 @@ export function convertFileSearchToolToAdapterFormat(tool: Tool) { export function fileSearchTool( config: FileSearchToolConfig, ): GeminiFileSearchTool { - return brandProviderTool({ - name: 'file_search', - description: '', - metadata: config, - }) + return brandGeminiProviderTool( + { + name: 'file_search', + description: '', + metadata: config, + }, + 'file_search', + ) } diff --git a/packages/ai-gemini/src/tools/gemini-provider-tool.ts b/packages/ai-gemini/src/tools/gemini-provider-tool.ts new file mode 100644 index 000000000..2ebfd6dc7 --- /dev/null +++ b/packages/ai-gemini/src/tools/gemini-provider-tool.ts @@ -0,0 +1,60 @@ +import { brandProviderTool } from '@tanstack/ai' +import type { ProviderTool, Tool } from '@tanstack/ai' + +const GEMINI_PROVIDER_TOOL_KINDS = { + code_execution: 'gemini.code_execution', + computer_use: 'gemini.computer_use', + file_search: 'gemini.file_search', + google_maps: 'gemini.google_maps', + google_search: 'gemini.google_search', + google_search_retrieval: 'gemini.google_search_retrieval', + url_context: 'gemini.url_context', +} as const + +type GeminiProviderToolKind = keyof typeof GEMINI_PROVIDER_TOOL_KINDS + +export function brandGeminiProviderTool< + T extends ProviderTool<'gemini', GeminiProviderToolKind>, +>(tool: Omit, toolKind: T['~toolKind']): T { + return brandProviderTool({ + ...tool, + metadata: { + ...tool.metadata, + __kind: GEMINI_PROVIDER_TOOL_KINDS[toolKind], + }, + }) +} + +export function getGeminiProviderToolKind( + tool: Tool, +): GeminiProviderToolKind | undefined { + const toolKind = tool.metadata?.['__kind'] + switch (toolKind) { + case 'gemini.code_execution': + return 'code_execution' + case 'gemini.computer_use': + return 'computer_use' + case 'gemini.file_search': + return 'file_search' + case 'gemini.google_maps': + return 'google_maps' + case 'gemini.google_search': + return 'google_search' + case 'gemini.google_search_retrieval': + return 'google_search_retrieval' + case 'gemini.url_context': + return 'url_context' + default: + return undefined + } +} + +/** Returns adapter metadata without the internal runtime discriminator. */ +export function getGeminiProviderToolMetadata(tool: Tool): Tool['metadata'] { + if (!tool.metadata) { + return undefined + } + const { __kind: _kind, ...metadata } = tool.metadata + void _kind + return metadata +} diff --git a/packages/ai-gemini/src/tools/google-maps-tool.ts b/packages/ai-gemini/src/tools/google-maps-tool.ts index 4507dca6a..287adc357 100644 --- a/packages/ai-gemini/src/tools/google-maps-tool.ts +++ b/packages/ai-gemini/src/tools/google-maps-tool.ts @@ -1,4 +1,7 @@ -import { brandProviderTool } from '@tanstack/ai' +import { + brandGeminiProviderTool, + getGeminiProviderToolMetadata, +} from './gemini-provider-tool' import type { GoogleMaps } from '@google/genai' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -10,7 +13,7 @@ export type GoogleMapsTool = GoogleMapsToolConfig export type GeminiGoogleMapsTool = ProviderTool<'gemini', 'google_maps'> export function convertGoogleMapsToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as GoogleMapsToolConfig + const metadata = getGeminiProviderToolMetadata(tool) as GoogleMapsToolConfig return { googleMaps: metadata, } @@ -19,9 +22,12 @@ export function convertGoogleMapsToolToAdapterFormat(tool: Tool) { export function googleMapsTool( config?: GoogleMapsToolConfig, ): GeminiGoogleMapsTool { - return brandProviderTool({ - name: 'google_maps', - description: '', - metadata: config, - }) + return brandGeminiProviderTool( + { + name: 'google_maps', + description: '', + metadata: config, + }, + 'google_maps', + ) } diff --git a/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts b/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts index e391009b1..f87e5327e 100644 --- a/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts +++ b/packages/ai-gemini/src/tools/google-search-retriveal-tool.ts @@ -1,4 +1,7 @@ -import { brandProviderTool } from '@tanstack/ai' +import { + brandGeminiProviderTool, + getGeminiProviderToolMetadata, +} from './gemini-provider-tool' import type { GoogleSearchRetrieval } from '@google/genai' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -13,7 +16,9 @@ export type GeminiGoogleSearchRetrievalTool = ProviderTool< > export function convertGoogleSearchRetrievalToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as GoogleSearchRetrievalToolConfig + const metadata = getGeminiProviderToolMetadata( + tool, + ) as GoogleSearchRetrievalToolConfig return { googleSearchRetrieval: metadata, } @@ -22,9 +27,12 @@ export function convertGoogleSearchRetrievalToolToAdapterFormat(tool: Tool) { export function googleSearchRetrievalTool( config?: GoogleSearchRetrievalToolConfig, ): GeminiGoogleSearchRetrievalTool { - return brandProviderTool({ - name: 'google_search_retrieval', - description: '', - metadata: config, - }) + return brandGeminiProviderTool( + { + name: 'google_search_retrieval', + description: '', + metadata: config, + }, + 'google_search_retrieval', + ) } diff --git a/packages/ai-gemini/src/tools/google-search-tool.ts b/packages/ai-gemini/src/tools/google-search-tool.ts index 24dc70e35..0c645831a 100644 --- a/packages/ai-gemini/src/tools/google-search-tool.ts +++ b/packages/ai-gemini/src/tools/google-search-tool.ts @@ -1,4 +1,7 @@ -import { brandProviderTool } from '@tanstack/ai' +import { + brandGeminiProviderTool, + getGeminiProviderToolMetadata, +} from './gemini-provider-tool' import type { GoogleSearch } from '@google/genai' import type { ProviderTool, Tool } from '@tanstack/ai' @@ -10,7 +13,7 @@ export type GoogleSearchTool = GoogleSearchToolConfig export type GeminiGoogleSearchTool = ProviderTool<'gemini', 'google_search'> export function convertGoogleSearchToolToAdapterFormat(tool: Tool) { - const metadata = tool.metadata as GoogleSearchToolConfig + const metadata = getGeminiProviderToolMetadata(tool) as GoogleSearchToolConfig return { googleSearch: metadata, } @@ -19,9 +22,12 @@ export function convertGoogleSearchToolToAdapterFormat(tool: Tool) { export function googleSearchTool( config?: GoogleSearchToolConfig, ): GeminiGoogleSearchTool { - return brandProviderTool({ - name: 'google_search', - description: '', - metadata: config, - }) + return brandGeminiProviderTool( + { + name: 'google_search', + description: '', + metadata: config, + }, + 'google_search', + ) } diff --git a/packages/ai-gemini/src/tools/tool-converter.ts b/packages/ai-gemini/src/tools/tool-converter.ts index aa2e2cef5..d77b424c4 100644 --- a/packages/ai-gemini/src/tools/tool-converter.ts +++ b/packages/ai-gemini/src/tools/tool-converter.ts @@ -1,6 +1,7 @@ import { convertCodeExecutionToolToAdapterFormat } from './code-execution-tool' import { convertComputerUseToolToAdapterFormat } from './computer-use-tool' import { convertFileSearchToolToAdapterFormat } from './file-search-tool' +import { getGeminiProviderToolKind } from './gemini-provider-tool' import { convertGoogleMapsToolToAdapterFormat } from './google-maps-tool' import { convertGoogleSearchRetrievalToolToAdapterFormat } from './google-search-retriveal-tool' import { convertGoogleSearchToolToAdapterFormat } from './google-search-tool' @@ -42,9 +43,7 @@ export function convertToolsToProviderFormat( // Process each tool and group function declarations together for (const tool of tools) { - const name = tool.name - - switch (name) { + switch (getGeminiProviderToolKind(tool)) { case 'code_execution': result.push(convertCodeExecutionToolToAdapterFormat(tool)) break @@ -66,7 +65,7 @@ export function convertToolsToProviderFormat( case 'url_context': result.push(convertUrlContextToolToAdapterFormat(tool)) break - default: + case undefined: // Collect function declarations to group together // Description is required for Gemini function declarations if (!tool.description) { diff --git a/packages/ai-gemini/src/tools/url-context-tool.ts b/packages/ai-gemini/src/tools/url-context-tool.ts index d1d776d57..e162e49ce 100644 --- a/packages/ai-gemini/src/tools/url-context-tool.ts +++ b/packages/ai-gemini/src/tools/url-context-tool.ts @@ -1,4 +1,4 @@ -import { brandProviderTool } from '@tanstack/ai' +import { brandGeminiProviderTool } from './gemini-provider-tool' import type { ProviderTool, Tool } from '@tanstack/ai' export interface UrlContextToolConfig {} @@ -15,9 +15,12 @@ export function convertUrlContextToolToAdapterFormat(_tool: Tool) { } export function urlContextTool(): GeminiUrlContextTool { - return brandProviderTool({ - name: 'url_context', - description: '', - metadata: {}, - }) + return brandGeminiProviderTool( + { + name: 'url_context', + description: '', + metadata: {}, + }, + 'url_context', + ) } diff --git a/packages/ai-gemini/tests/provider-tool-dispatch.test.ts b/packages/ai-gemini/tests/provider-tool-dispatch.test.ts new file mode 100644 index 000000000..116c879b9 --- /dev/null +++ b/packages/ai-gemini/tests/provider-tool-dispatch.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { + codeExecutionTool, + computerUseTool, + fileSearchTool, + googleMapsTool, + googleSearchRetrievalTool, + googleSearchTool, + urlContextTool, +} from '../src/tools' +import { convertToolsToProviderFormat } from '../src/tools/tool-converter' +import type { Tool } from '@tanstack/ai' + +const PROVIDER_TOOL_NAMES = [ + 'code_execution', + 'computer_use', + 'file_search', + 'google_maps', + 'google_search', + 'google_search_retrieval', + 'url_context', +] as const + +describe('Gemini provider tool dispatch', () => { + it.each(PROVIDER_TOOL_NAMES)( + 'keeps an ordinary function named %s as a function declaration', + (name) => { + const [converted] = convertToolsToProviderFormat([ + { + name, + description: 'Run an application function', + inputSchema: { + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }, + } satisfies Tool, + ]) + + expect(converted).toMatchObject({ + functionDeclarations: [{ name }], + }) + }, + ) + + it('converts every factory tool without leaking its runtime marker', () => { + const converted = convertToolsToProviderFormat([ + codeExecutionTool(), + computerUseTool({}), + fileSearchTool({ fileSearchStoreNames: [] }), + googleMapsTool(), + googleSearchTool(), + googleSearchRetrievalTool(), + urlContextTool(), + ]) + + expect(converted).toHaveLength(PROVIDER_TOOL_NAMES.length) + expect(JSON.stringify(converted)).not.toContain('__kind') + }) + + it('keeps provider identity through a plain-data round trip', () => { + const tool = JSON.parse(JSON.stringify(googleSearchTool())) as Tool + + expect(convertToolsToProviderFormat([tool])).toEqual([{ googleSearch: {} }]) + }) +}) diff --git a/packages/ai-gemini/tests/text-interactions-adapter.test.ts b/packages/ai-gemini/tests/text-interactions-adapter.test.ts index c043efb63..d192aec75 100644 --- a/packages/ai-gemini/tests/text-interactions-adapter.test.ts +++ b/packages/ai-gemini/tests/text-interactions-adapter.test.ts @@ -3,6 +3,14 @@ import { z } from 'zod' import { chat } from '@tanstack/ai' import type { StreamChunk, Tool } from '@tanstack/ai' import { GeminiTextInteractionsAdapter } from '../src/experimental/text-interactions/adapter' +import { + codeExecutionTool, + fileSearchTool, + googleMapsTool, + googleSearchRetrievalTool, + googleSearchTool, + urlContextTool, +} from '../src/tools' import type { GeminiTextInteractionsProviderOptions } from '../src/experimental/text-interactions/adapter' import type { GeminiInteractionsCustomEvent, @@ -809,14 +817,10 @@ describe('GeminiTextInteractionsAdapter', () => { adapter, messages: [{ role: 'user', content: 'What happened yesterday?' }], tools: [ - { - name: 'google_search', - description: '', - metadata: { search_types: ['web_search'] }, - }, - { name: 'code_execution', description: '', metadata: {} }, - { name: 'url_context', description: '', metadata: {} }, - ] as Array, + googleSearchTool({ searchTypes: { webSearch: {} } }), + codeExecutionTool(), + urlContextTool(), + ], }), ) @@ -828,6 +832,51 @@ describe('GeminiTextInteractionsAdapter', () => { ]) }) + it('keeps provider-reserved names as ordinary function tools without a factory marker', async () => { + mocks.interactionsCreateSpy.mockResolvedValue( + mkStream([ + { + event_type: 'interaction.created', + interaction: { id: 'int_custom_names', status: 'in_progress' }, + }, + { + event_type: 'interaction.completed', + interaction: { id: 'int_custom_names', status: 'completed' }, + }, + ]), + ) + + const tools = [ + 'code_execution', + 'computer_use', + 'file_search', + 'google_maps', + 'google_search', + 'google_search_retrieval', + 'mcp_server', + 'url_context', + ].map((name) => ({ + name, + description: 'Run an application function', + })) satisfies Array + + await collectChunks( + chat({ + adapter: createAdapter(), + messages: [{ role: 'user', content: 'Use an application function.' }], + tools, + }), + ) + + const [payload] = mocks.interactionsCreateSpy.mock.calls[0]! + expect(payload.tools).toHaveLength(tools.length) + expect(payload.tools).toEqual( + tools.map((tool) => + expect.objectContaining({ type: 'function', name: tool.name }), + ), + ) + }) + it('translates file_search metadata fields into snake_case', async () => { mocks.interactionsCreateSpy.mockResolvedValue( mkStream([ @@ -848,16 +897,12 @@ describe('GeminiTextInteractionsAdapter', () => { adapter, messages: [{ role: 'user', content: 'Find it.' }], tools: [ - { - name: 'file_search', - description: '', - metadata: { - fileSearchStoreNames: ['fileSearchStores/my-store'], - topK: 5, - metadataFilter: 'kind="faq"', - }, - }, - ] as Array, + fileSearchTool({ + fileSearchStoreNames: ['fileSearchStores/my-store'], + topK: 5, + metadataFilter: 'kind="faq"', + }), + ], }), ) @@ -990,9 +1035,7 @@ describe('GeminiTextInteractionsAdapter', () => { chat({ adapter, messages: [{ role: 'user', content: 'Weather in Madrid?' }], - tools: [ - { name: 'google_search', description: '', metadata: {} }, - ] as Array, + tools: [googleSearchTool()], }), ) @@ -1023,9 +1066,7 @@ describe('GeminiTextInteractionsAdapter', () => { chat({ adapter, messages: [{ role: 'user', content: 'Search' }], - tools: [ - { name: 'google_search_retrieval', description: '', metadata: {} }, - ] as Array, + tools: [googleSearchRetrievalTool()], }), ) @@ -1044,9 +1085,7 @@ describe('GeminiTextInteractionsAdapter', () => { chat({ adapter, messages: [{ role: 'user', content: 'Directions' }], - tools: [ - { name: 'google_maps', description: '', metadata: {} }, - ] as Array, + tools: [googleMapsTool()], }), ) From ee9e7a0851bb79e89e4740aae686234b6d7be5fd Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 12:57:34 +0200 Subject: [PATCH 6/7] test: cover provider tool name collisions across adapters --- .changeset/tidy-searches-coexist.md | 5 +- packages/ai-anthropic/src/adapters/text.ts | 13 ++-- .../ai-anthropic/tests/text.skills.test.ts | 17 +++++ .../provider-tool-dispatch/basic.json | 12 ++++ .../routes/api.provider-tool-dispatch-wire.ts | 72 +++++++++++++++++++ .../tests/provider-tool-dispatch-wire.spec.ts | 72 +++++++++++++++++++ 6 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 testing/e2e/fixtures/provider-tool-dispatch/basic.json create mode 100644 testing/e2e/src/routes/api.provider-tool-dispatch-wire.ts create mode 100644 testing/e2e/tests/provider-tool-dispatch-wire.spec.ts diff --git a/.changeset/tidy-searches-coexist.md b/.changeset/tidy-searches-coexist.md index 6ff952e77..7782d5289 100644 --- a/.changeset/tidy-searches-coexist.md +++ b/.changeset/tidy-searches-coexist.md @@ -1,5 +1,8 @@ --- '@tanstack/ai-anthropic': patch +'@tanstack/ai-gemini': patch +'@tanstack/ai-openai': patch +'@tanstack/openai-base': patch --- -Classify Anthropic-native tools with a stable runtime discriminator so ordinary functions can use the same public names without depending on versioned provider wire identifiers. +Classify Anthropic, Gemini, and OpenAI native tools with stable runtime discriminators so ordinary functions can use the same public names without selecting provider-native behavior. diff --git a/packages/ai-anthropic/src/adapters/text.ts b/packages/ai-anthropic/src/adapters/text.ts index d431f5486..0172dc90a 100644 --- a/packages/ai-anthropic/src/adapters/text.ts +++ b/packages/ai-anthropic/src/adapters/text.ts @@ -2,6 +2,7 @@ import { EventType, normalizeSystemPrompts } from '@tanstack/ai' import { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals' import { BaseTextAdapter } from '@tanstack/ai/adapters' import { convertToolsToProviderFormat } from '../tools/tool-converter' +import { getAnthropicProviderToolKind } from '../tools/anthropic-provider-tool' import { readCodeExecutionConfig, readCodeExecutionSkills, @@ -171,7 +172,9 @@ export function computeAnthropicBetas( // Code-execution beta is version-aware: select from the FIRST code_execution // tool's config type. - const codeExecTool = tools?.find((t) => t.name === 'code_execution') + const codeExecTool = tools?.find( + (tool) => getAnthropicProviderToolKind(tool) === 'code_execution', + ) if (codeExecTool) { const cfgType = readCodeExecutionConfig(codeExecTool)?.type // Each code_execution tool version pairs with a specific beta. Known @@ -188,9 +191,9 @@ export function computeAnthropicBetas( // container-lift, which lifts skills from any code_execution tool that // carries them (not just the first). const hasSkills = tools?.some( - (t) => - t.name === 'code_execution' && - (readCodeExecutionSkills(t)?.length ?? 0) > 0, + (tool) => + getAnthropicProviderToolKind(tool) === 'code_execution' && + (readCodeExecutionSkills(tool)?.length ?? 0) > 0, ) if (hasSkills) betas.add('skills-2025-10-02') @@ -574,7 +577,7 @@ export class AnthropicTextAdapter< // canonical path for skills; `modelOptions.container.skills` is deprecated. const toolSkills = options.tools ?.map((tool) => - tool.name === 'code_execution' + getAnthropicProviderToolKind(tool) === 'code_execution' ? readCodeExecutionSkills(tool) : undefined, ) diff --git a/packages/ai-anthropic/tests/text.skills.test.ts b/packages/ai-anthropic/tests/text.skills.test.ts index 7d9ea4134..6d258c742 100644 --- a/packages/ai-anthropic/tests/text.skills.test.ts +++ b/packages/ai-anthropic/tests/text.skills.test.ts @@ -145,4 +145,21 @@ describe('computeAnthropicBetas', () => { ) expect(req.container).toBeUndefined() }) + + it('does not enable code execution for an ordinary function with that name', () => { + const fnTool = { + name: 'code_execution', + description: 'Run an application function', + metadata: { + skills: [{ type: 'anthropic', skill_id: 'must-not-be-lifted' }], + }, + } as any + + expect(computeAnthropicBetas([fnTool], undefined)).toBeUndefined() + + const req = makeAdapter().mapCommonOptionsToAnthropic( + baseOptions({ tools: [fnTool] }), + ) + expect(req.container).toBeUndefined() + }) }) diff --git a/testing/e2e/fixtures/provider-tool-dispatch/basic.json b/testing/e2e/fixtures/provider-tool-dispatch/basic.json new file mode 100644 index 000000000..74628492c --- /dev/null +++ b/testing/e2e/fixtures/provider-tool-dispatch/basic.json @@ -0,0 +1,12 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "[provider-tool-dispatch] preserve custom tool" + }, + "response": { + "content": "ok" + } + } + ] +} diff --git a/testing/e2e/src/routes/api.provider-tool-dispatch-wire.ts b/testing/e2e/src/routes/api.provider-tool-dispatch-wire.ts new file mode 100644 index 000000000..2e8a32380 --- /dev/null +++ b/testing/e2e/src/routes/api.provider-tool-dispatch-wire.ts @@ -0,0 +1,72 @@ +import { createFileRoute } from '@tanstack/react-router' +import { chat } from '@tanstack/ai' +import type { Tool } from '@tanstack/ai' +import type { Provider } from '@/lib/types' +import { createTextAdapter } from '@/lib/providers' + +type CollisionProvider = Extract + +const customTools = { + gemini: createCustomTool('google_search'), + openai: createCustomTool('web_search'), +} satisfies Record + +export const Route = createFileRoute('/api/provider-tool-dispatch-wire')({ + server: { + handlers: { + POST: async ({ request }) => { + await import('@/lib/llmock-server').then((module) => + module.ensureLLMock(), + ) + const url = new URL(request.url) + const provider = readProvider(url.searchParams.get('provider')) + const testId = url.searchParams.get('testId') ?? undefined + + if (!provider) { + return new Response('Unsupported provider', { status: 400 }) + } + + try { + for await (const _ of chat({ + ...createTextAdapter(provider, undefined, undefined, testId), + messages: [ + { + role: 'user', + content: '[provider-tool-dispatch] preserve custom tool', + }, + ], + tools: [customTools[provider]], + })) { + // Drain the stream. + } + } catch (error) { + return Response.json({ + ok: false, + error: error instanceof Error ? error.message : String(error), + }) + } + + return Response.json({ ok: true }) + }, + }, + }, +}) + +function createCustomTool(name: string): Tool { + return { + name, + description: 'Run an application function', + inputSchema: { + type: 'object', + properties: { query: { type: 'string' } }, + required: ['query'], + }, + } +} + +function readProvider(value: string | null): CollisionProvider | undefined { + if (value === 'gemini' || value === 'openai') { + return value + } + return undefined +} diff --git a/testing/e2e/tests/provider-tool-dispatch-wire.spec.ts b/testing/e2e/tests/provider-tool-dispatch-wire.spec.ts new file mode 100644 index 000000000..ea27c2087 --- /dev/null +++ b/testing/e2e/tests/provider-tool-dispatch-wire.spec.ts @@ -0,0 +1,72 @@ +import { test, expect } from './fixtures' +import type { APIRequestContext } from '@playwright/test' + +test.describe('provider tools — custom-name wire dispatch', () => { + test.beforeEach(async ({ request, aimockPort }) => { + await request.delete(`http://127.0.0.1:${aimockPort}/v1/_requests`) + }) + + test('OpenAI preserves web_search as a function tool', async ({ + request, + aimockPort, + testId, + }) => { + await runWireRoute(request, 'openai', testId) + const body = await readCapturedBody(request, aimockPort) + const tools = body['tools'] as Array> | undefined + + expect(tools).toContainEqual( + expect.objectContaining({ + type: 'function', + name: 'web_search', + }), + ) + }) + + test('Gemini preserves google_search as a function declaration', async ({ + request, + aimockPort, + testId, + }) => { + await runWireRoute(request, 'gemini', testId) + const body = await readCapturedBody(request, aimockPort) + const tools = body['tools'] as Array> | undefined + + expect(tools).toContainEqual( + expect.objectContaining({ + functionDeclarations: [ + expect.objectContaining({ name: 'google_search' }), + ], + }), + ) + expect(tools).not.toContainEqual( + expect.objectContaining({ googleSearch: expect.anything() }), + ) + }) +}) + +async function runWireRoute( + request: APIRequestContext, + provider: 'gemini' | 'openai', + testId: string, +) { + const response = await request.post( + `/api/provider-tool-dispatch-wire?provider=${provider}&testId=${encodeURIComponent(testId)}`, + ) + expect(response.ok()).toBe(true) + const result = (await response.json()) as { ok: boolean; error?: string } + expect(result, result.error).toMatchObject({ ok: true }) +} + +async function readCapturedBody( + request: APIRequestContext, + aimockPort: number, +): Promise> { + const response = await request.get( + `http://127.0.0.1:${aimockPort}/v1/_requests`, + ) + const entries = (await response.json()) as Array<{ + body: Record | null + }> + return entries[0]?.body ?? {} +} From 9399c21a75051889368b3cede88bfcf72d27b077 Mon Sep 17 00:00:00 2001 From: jan-kubica Date: Tue, 14 Jul 2026 14:23:35 +0200 Subject: [PATCH 7/7] chore: clean up provider tool imports --- .../tests/provider-tools-smoke.test.ts | 6 +++--- .../openai-base/src/tools/code-interpreter-tool.ts | 8 ++++---- .../openai-base/src/tools/computer-use-tool.ts | 8 ++++---- packages/openai-base/src/tools/custom-tool.ts | 8 ++++---- packages/openai-base/src/tools/file-search-tool.ts | 8 ++++---- .../openai-base/src/tools/image-generation-tool.ts | 8 ++++---- packages/openai-base/src/tools/mcp-tool.ts | 14 +++++++------- packages/openai-base/src/tools/shell-tool.ts | 8 ++++---- .../src/tools/web-search-preview-tool.ts | 8 ++++---- packages/openai-base/src/tools/web-search-tool.ts | 8 ++++---- 10 files changed, 42 insertions(+), 42 deletions(-) diff --git a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts index 25375a49a..287a43046 100644 --- a/packages/ai-anthropic/tests/provider-tools-smoke.test.ts +++ b/packages/ai-anthropic/tests/provider-tools-smoke.test.ts @@ -7,7 +7,7 @@ * 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 { describe, expect, it } from 'vitest' import { bashTool, codeExecutionTool, @@ -94,7 +94,7 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => { type: 'web_search_20250305', max_uses: 2, }), - } as unknown as Tool, + }, ]) expect(converted).toEqual({ name: 'web_search', @@ -142,7 +142,7 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => { type: 'text_editor_20250124', name: 'str_replace_editor', }), - ] as unknown as Tool[]) + ]) expect(converted).toHaveLength(7) expect(JSON.stringify(converted)).not.toContain('__kind') const names = converted.map((t) => ('name' in t ? t.name : undefined)) diff --git a/packages/openai-base/src/tools/code-interpreter-tool.ts b/packages/openai-base/src/tools/code-interpreter-tool.ts index 1a75534b2..af6bbb350 100644 --- a/packages/openai-base/src/tools/code-interpreter-tool.ts +++ b/packages/openai-base/src/tools/code-interpreter-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { Tool as SDKTool } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -44,7 +48,3 @@ export function codeInterpreterTool( 'code_interpreter', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/computer-use-tool.ts b/packages/openai-base/src/tools/computer-use-tool.ts index b69be548d..26bec9b45 100644 --- a/packages/openai-base/src/tools/computer-use-tool.ts +++ b/packages/openai-base/src/tools/computer-use-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { ComputerUsePreviewTool as ComputerUseToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -39,7 +43,3 @@ export function computerUseTool(toolData: ComputerUseToolConfig): Tool { 'computer_use', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/custom-tool.ts b/packages/openai-base/src/tools/custom-tool.ts index 57a10b85e..c0c20c270 100644 --- a/packages/openai-base/src/tools/custom-tool.ts +++ b/packages/openai-base/src/tools/custom-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { CustomTool as CustomToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -40,7 +44,3 @@ export function customTool(toolData: CustomToolConfig): Tool { 'custom', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/file-search-tool.ts b/packages/openai-base/src/tools/file-search-tool.ts index 6ca6520dd..8bd1c33e7 100644 --- a/packages/openai-base/src/tools/file-search-tool.ts +++ b/packages/openai-base/src/tools/file-search-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { FileSearchTool as FileSearchToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -59,7 +63,3 @@ export function fileSearchTool(toolData: FileSearchToolConfig): Tool { } export { validateMaxNumResults } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/image-generation-tool.ts b/packages/openai-base/src/tools/image-generation-tool.ts index 2bd75122e..f5eaac440 100644 --- a/packages/openai-base/src/tools/image-generation-tool.ts +++ b/packages/openai-base/src/tools/image-generation-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { Tool as SDKTool } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -55,7 +59,3 @@ export function imageGenerationTool( } export { validatePartialImages } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/mcp-tool.ts b/packages/openai-base/src/tools/mcp-tool.ts index e1b6ea620..97e495776 100644 --- a/packages/openai-base/src/tools/mcp-tool.ts +++ b/packages/openai-base/src/tools/mcp-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { Tool as SDKTool } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -26,13 +30,13 @@ export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig { 'type' > - const mcpTool: MCPToolConfig = { + const convertedTool: MCPToolConfig = { ...metadata, type: 'mcp', } - validateMCPtool(mcpTool) - return mcpTool + validateMCPtool(convertedTool) + return convertedTool } /** @@ -53,7 +57,3 @@ export function mcpTool(toolData: Omit): Tool { 'mcp', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/shell-tool.ts b/packages/openai-base/src/tools/shell-tool.ts index 4e893cd4f..f739d8267 100644 --- a/packages/openai-base/src/tools/shell-tool.ts +++ b/packages/openai-base/src/tools/shell-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { FunctionShellTool as ShellToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -51,7 +55,3 @@ export function shellTool(config: ShellToolFactoryConfig = {}): Tool { 'shell', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/web-search-preview-tool.ts b/packages/openai-base/src/tools/web-search-preview-tool.ts index 853eb8d5b..5ea7e1931 100644 --- a/packages/openai-base/src/tools/web-search-preview-tool.ts +++ b/packages/openai-base/src/tools/web-search-preview-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { WebSearchPreviewTool as WebSearchPreviewToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -42,7 +46,3 @@ export function webSearchPreviewTool( 'web_search_preview', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool' diff --git a/packages/openai-base/src/tools/web-search-tool.ts b/packages/openai-base/src/tools/web-search-tool.ts index eb6d377c7..eb126bbaa 100644 --- a/packages/openai-base/src/tools/web-search-tool.ts +++ b/packages/openai-base/src/tools/web-search-tool.ts @@ -1,3 +1,7 @@ +import { + getOpenAIProviderToolMetadata, + openAIProviderTool, +} from './openai-provider-tool' import type { WebSearchTool as WebSearchToolConfig } from 'openai/resources/responses/responses' import type { Tool } from '@tanstack/ai' @@ -40,7 +44,3 @@ export function webSearchTool(toolData: WebSearchToolConfig): Tool { 'web_search', ) } -import { - getOpenAIProviderToolMetadata, - openAIProviderTool, -} from './openai-provider-tool'