Skip to content

Commit 56c67d5

Browse files
MehediHfelixarntz
andauthored
feat(anthropic): add web_fetch_20260209 and web_search_20260209 (#12668)
<!-- Welcome to contributing to AI SDK! We're excited to see your changes. We suggest you read the following contributing guide we've created before submitting: https://github.com/vercel/ai/blob/main/CONTRIBUTING.md --> ## Background Anthropic introduced new web tool versions (`web_search_20260209` and `web_fetch_20260209`) that include dynamic filtering capabilities, but those versions were not yet available in AI SDK. ## Summary This PR adds support for `web_search_20260209` and `web_fetch_20260209` in the Anthropic provider. ## Checklist - [x] Tests have been added / updated (for bug fixes / features) - [ ] Documentation has been added / updated (for bug fixes / features) - [x] A _patch_ changeset for relevant packages has been added (for bug fixes / features - run `pnpm changeset` in the project root) - [x] I have reviewed this pull request (self-review) --------- Co-authored-by: Felix Arntz <felix.arntz@vercel.com>
1 parent dddb43f commit 56c67d5

28 files changed

+2273
-3
lines changed

.changeset/wise-walls-press.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/anthropic': patch
3+
---
4+
5+
feat(provider/anthropic): add support for Anthropic web tools `web_fetch_20260209` and `web_search_20260209`
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { anthropic } from '@ai-sdk/anthropic';
2+
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
3+
4+
export const anthropicWebFetch20260209Agent = new ToolLoopAgent({
5+
model: anthropic('claude-sonnet-4-6'),
6+
tools: {
7+
web_fetch: anthropic.tools.webFetch_20260209(),
8+
},
9+
});
10+
11+
export type AnthropicWebFetch20260209Message = InferAgentUIMessage<
12+
typeof anthropicWebFetch20260209Agent
13+
>;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { anthropic } from '@ai-sdk/anthropic';
2+
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
3+
4+
export const anthropicWebSearch20260209Agent = new ToolLoopAgent({
5+
model: anthropic('claude-sonnet-4-6'),
6+
tools: {
7+
web_search: anthropic.tools.webSearch_20260209({
8+
maxUses: 3,
9+
userLocation: {
10+
type: 'approximate',
11+
city: 'New York',
12+
country: 'US',
13+
timezone: 'America/New_York',
14+
},
15+
}),
16+
},
17+
});
18+
19+
export type AnthropicWebSearch20260209Message = InferAgentUIMessage<
20+
typeof anthropicWebSearch20260209Agent
21+
>;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { anthropicWebFetch20260209Agent } from '@/agent/anthropic/web-fetch-20260209-agent';
2+
import { createAgentUIStreamResponse } from 'ai';
3+
4+
export async function POST(request: Request) {
5+
const body = await request.json();
6+
7+
return createAgentUIStreamResponse({
8+
agent: anthropicWebFetch20260209Agent,
9+
uiMessages: body.messages,
10+
sendSources: true,
11+
});
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { anthropicWebSearch20260209Agent } from '@/agent/anthropic/web-search-20260209-agent';
2+
import { createAgentUIStreamResponse } from 'ai';
3+
4+
export async function POST(request: Request) {
5+
const body = await request.json();
6+
7+
return createAgentUIStreamResponse({
8+
agent: anthropicWebSearch20260209Agent,
9+
uiMessages: body.messages,
10+
sendSources: true,
11+
});
12+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use client';
2+
3+
import { AnthropicWebFetch20260209Message } from '@/agent/anthropic/web-fetch-20260209-agent';
4+
import { Response } from '@/components/ai-elements/response';
5+
import ChatInput from '@/components/chat-input';
6+
import SourcesView from '@/components/sources-view';
7+
import AnthropicCodeExecution20260120View from '@/components/tool/anthropic-code-execution-20260120-view';
8+
import AnthropicWebFetch20260209View from '@/components/tool/anthropic-web-fetch-20260209-view';
9+
import DynamicToolView from '@/components/tool/dynamic-tool-view';
10+
import { useChat } from '@ai-sdk/react';
11+
import { DefaultChatTransport } from 'ai';
12+
13+
export default function TestAnthropicWebFetch20260209() {
14+
const { error, status, sendMessage, messages, regenerate } =
15+
useChat<AnthropicWebFetch20260209Message>({
16+
transport: new DefaultChatTransport({
17+
api: '/api/chat/anthropic-web-fetch-20260209',
18+
}),
19+
});
20+
21+
return (
22+
<div className="flex flex-col py-24 mx-auto w-full max-w-md stretch">
23+
<h1 className="mb-4 text-xl font-bold">Anthropic Web Fetch (20260209)</h1>
24+
25+
{messages.map(message => (
26+
<div key={message.id}>
27+
{message.role === 'user' ? 'User: ' : 'AI: '}
28+
{message.parts.map((part, index) => {
29+
switch (part.type) {
30+
case 'text': {
31+
return <Response key={index}>{part.text}</Response>;
32+
}
33+
case 'tool-web_fetch': {
34+
return (
35+
<AnthropicWebFetch20260209View
36+
invocation={part}
37+
key={index}
38+
/>
39+
);
40+
}
41+
case 'dynamic-tool': {
42+
if (part.toolName === 'code_execution') {
43+
return (
44+
<AnthropicCodeExecution20260120View
45+
invocation={
46+
{
47+
...part,
48+
input: {
49+
type: 'programmatic-tool-call',
50+
code:
51+
typeof part.input === 'object' &&
52+
part.input !== null &&
53+
'code' in part.input
54+
? String(part.input.code)
55+
: '',
56+
},
57+
} as any
58+
}
59+
key={index}
60+
/>
61+
);
62+
}
63+
return <DynamicToolView invocation={part} key={index} />;
64+
}
65+
}
66+
})}
67+
68+
<SourcesView
69+
sources={message.parts.filter(part => part.type === 'source-url')}
70+
/>
71+
</div>
72+
))}
73+
74+
{error && (
75+
<div className="mt-4">
76+
<div className="text-red-500">An error occurred.</div>
77+
<button
78+
type="button"
79+
className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500"
80+
onClick={() => regenerate()}
81+
>
82+
Retry
83+
</button>
84+
</div>
85+
)}
86+
87+
<ChatInput status={status} onSubmit={text => sendMessage({ text })} />
88+
</div>
89+
);
90+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
'use client';
2+
3+
import { AnthropicWebSearch20260209Message } from '@/agent/anthropic/web-search-20260209-agent';
4+
import { Response } from '@/components/ai-elements/response';
5+
import ChatInput from '@/components/chat-input';
6+
import SourcesView from '@/components/sources-view';
7+
import AnthropicCodeExecution20260120View from '@/components/tool/anthropic-code-execution-20260120-view';
8+
import AnthropicWebSearch20260209View from '@/components/tool/anthropic-web-search-20260209-view';
9+
import DynamicToolView from '@/components/tool/dynamic-tool-view';
10+
import { useChat } from '@ai-sdk/react';
11+
import { DefaultChatTransport } from 'ai';
12+
13+
export default function TestAnthropicWebSearch20260209() {
14+
const { error, status, sendMessage, messages, regenerate } =
15+
useChat<AnthropicWebSearch20260209Message>({
16+
transport: new DefaultChatTransport({
17+
api: '/api/chat/anthropic-web-search-20260209',
18+
}),
19+
});
20+
21+
return (
22+
<div className="flex flex-col py-24 mx-auto w-full max-w-md stretch">
23+
<h1 className="mb-4 text-xl font-bold">
24+
Anthropic Web Search (20260209)
25+
</h1>
26+
27+
{messages.map(message => (
28+
<div key={message.id}>
29+
{message.role === 'user' ? 'User: ' : 'AI: '}
30+
{message.parts.map((part, index) => {
31+
switch (part.type) {
32+
case 'text': {
33+
return <Response key={index}>{part.text}</Response>;
34+
}
35+
case 'tool-web_search': {
36+
return (
37+
<AnthropicWebSearch20260209View
38+
invocation={part}
39+
key={index}
40+
/>
41+
);
42+
}
43+
case 'dynamic-tool': {
44+
if (part.toolName === 'code_execution') {
45+
return (
46+
<AnthropicCodeExecution20260120View
47+
invocation={
48+
{
49+
...part,
50+
input: {
51+
type: 'programmatic-tool-call',
52+
code:
53+
typeof part.input === 'object' &&
54+
part.input !== null &&
55+
'code' in part.input
56+
? String(part.input.code)
57+
: '',
58+
},
59+
} as any
60+
}
61+
key={index}
62+
/>
63+
);
64+
}
65+
return <DynamicToolView invocation={part} key={index} />;
66+
}
67+
}
68+
})}
69+
70+
<SourcesView
71+
sources={message.parts.filter(part => part.type === 'source-url')}
72+
/>
73+
</div>
74+
))}
75+
76+
{error && (
77+
<div className="mt-4">
78+
<div className="text-red-500">An error occurred.</div>
79+
<button
80+
type="button"
81+
className="px-4 py-2 mt-4 text-blue-500 rounded-md border border-blue-500"
82+
onClick={() => regenerate()}
83+
>
84+
Retry
85+
</button>
86+
</div>
87+
)}
88+
89+
<ChatInput status={status} onSubmit={text => sendMessage({ text })} />
90+
</div>
91+
);
92+
}

0 commit comments

Comments
 (0)