Skip to content

Commit f4da3b0

Browse files
committed
fix: distinguish custom Anthropic web search tools
1 parent 5fcaf90 commit f4da3b0

5 files changed

Lines changed: 91 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/ai-anthropic': patch
3+
---
4+
5+
Keep ordinary function tools named `web_search` distinct from Anthropic's native web-search tool by checking the provider metadata before conversion.

packages/ai-anthropic/src/tools/tool-converter.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ import { convertWebSearchToolToAdapterFormat } from './web-search-tool'
99
import type { AnthropicTool } from './index'
1010
import type { Tool } from '@tanstack/ai'
1111

12+
function isAnthropicWebSearchTool(tool: Tool): boolean {
13+
const metadata = tool.metadata
14+
if (!metadata) {
15+
return false
16+
}
17+
18+
return (
19+
'type' in metadata &&
20+
metadata.type === 'web_search_20250305' &&
21+
'name' in metadata &&
22+
metadata.name === 'web_search'
23+
)
24+
}
25+
1226
/**
1327
* Converts standard Tool format to Anthropic-specific tool format
1428
*
@@ -53,7 +67,9 @@ export function convertToolsToProviderFormat<TTool extends Tool>(
5367
case 'web_fetch':
5468
return convertWebFetchToolToAdapterFormat(tool)
5569
case 'web_search':
56-
return convertWebSearchToolToAdapterFormat(tool)
70+
return isAnthropicWebSearchTool(tool)
71+
? convertWebSearchToolToAdapterFormat(tool)
72+
: convertCustomToolToAdapterFormat(tool)
5773
default:
5874
return convertCustomToolToAdapterFormat(tool)
5975
}

packages/ai-anthropic/tests/provider-tools-smoke.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,28 @@ describe('convertToolsToProviderFormat — end-to-end shape', () => {
115115
expect(names).toContain('code_execution')
116116
expect(names).toContain('bash')
117117
})
118+
119+
it('keeps an ordinary function named web_search as a custom tool', () => {
120+
const [converted] = convertToolsToProviderFormat([
121+
{
122+
name: 'web_search',
123+
description: 'Search an application index',
124+
inputSchema: {
125+
type: 'object',
126+
properties: { query: { type: 'string' } },
127+
required: ['query'],
128+
},
129+
} satisfies Tool,
130+
])
131+
132+
expect(converted).toMatchObject({
133+
name: 'web_search',
134+
type: 'custom',
135+
description: 'Search an application index',
136+
input_schema: {
137+
properties: { query: { type: 'string' } },
138+
required: ['query'],
139+
},
140+
})
141+
})
118142
})

testing/e2e/src/routes/api.anthropic-skills-wire.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import { createFileRoute } from '@tanstack/react-router'
22
import { chat, createChatOptions } from '@tanstack/ai'
3+
import type { Tool } from '@tanstack/ai'
34
import { createAnthropicChat } from '@tanstack/ai-anthropic'
45
import { codeExecutionTool } from '@tanstack/ai-anthropic/tools'
56

67
const DUMMY_KEY = 'sk-ant-e2e-test-dummy-key'
78

9+
const customWebSearchTool = {
10+
name: 'web_search',
11+
description: 'Search an application index',
12+
inputSchema: {
13+
type: 'object',
14+
properties: { query: { type: 'string' } },
15+
required: ['query'],
16+
},
17+
} satisfies Tool
18+
819
/**
920
* Drives the Anthropic chat adapter with a `codeExecutionTool` carrying a
1021
* hosted skill. A custom `fetch` implementation intercepts the outgoing
@@ -138,6 +149,7 @@ export const Route = createFileRoute('/api/anthropic-skills-wire')({
138149
],
139150
},
140151
),
152+
customWebSearchTool,
141153
],
142154
})) {
143155
// Drain the stream.

testing/e2e/tests/anthropic-skills-wire.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,37 @@ test.describe('anthropic — code_execution skills wire format', () => {
7676
version: 'latest',
7777
})
7878
})
79+
80+
test('an ordinary function named web_search stays a custom tool', async ({
81+
request,
82+
}) => {
83+
const res = await request.post('/api/anthropic-skills-wire')
84+
expect(res.ok()).toBe(true)
85+
const { ok, error, capturedRequest } = (await res.json()) as {
86+
ok: boolean
87+
error?: string
88+
capturedRequest: {
89+
body: Record<string, unknown> | null
90+
} | null
91+
}
92+
93+
if (!ok) {
94+
throw new Error(`Route failed: ${error}`)
95+
}
96+
97+
const tools = capturedRequest?.body?.['tools'] as
98+
| Array<Record<string, unknown>>
99+
| undefined
100+
const customTool = tools?.find((tool) => tool['name'] === 'web_search')
101+
102+
expect(customTool).toMatchObject({
103+
name: 'web_search',
104+
type: 'custom',
105+
description: 'Search an application index',
106+
input_schema: {
107+
properties: { query: { type: 'string' } },
108+
required: ['query'],
109+
},
110+
})
111+
})
79112
})

0 commit comments

Comments
 (0)