Skip to content

Commit 4836f6c

Browse files
feat(anthropic): support webfetch tool (#9538)
Co-authored-by: Hunter Lovell <[email protected]>
1 parent e6d798b commit 4836f6c

File tree

7 files changed

+358
-0
lines changed

7 files changed

+358
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/anthropic": patch
3+
---
4+
5+
add named webfetch tool

libs/providers/langchain-anthropic/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,68 @@ const response = await llm.invoke("Latest news about AI?", {
178178

179179
For more information, see [Anthropic's Web Search Tool documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-search-tool).
180180

181+
### Web Fetch Tool
182+
183+
The web fetch tool (`webFetch_20250910`) allows Claude to retrieve full content from specified web pages and PDF documents. Claude can only fetch URLs that have been explicitly provided by the user or that come from previous web search or web fetch results.
184+
185+
> **⚠️ Security Warning:** Enabling the web fetch tool in environments where Claude processes untrusted input alongside sensitive data poses data exfiltration risks. We recommend only using this tool in trusted environments or when handling non-sensitive data.
186+
187+
```typescript
188+
import { ChatAnthropic, tools } from "@langchain/anthropic";
189+
190+
const llm = new ChatAnthropic({
191+
model: "claude-sonnet-4-5-20250929",
192+
});
193+
194+
// Basic usage - fetch content from a URL
195+
const response = await llm.invoke(
196+
"Please analyze the content at https://example.com/article",
197+
{ tools: [tools.webFetch_20250910()] }
198+
);
199+
```
200+
201+
The web fetch tool supports several configuration options:
202+
203+
```typescript
204+
const response = await llm.invoke(
205+
"Summarize this research paper: https://arxiv.org/abs/2024.12345",
206+
{
207+
tools: [
208+
tools.webFetch_20250910({
209+
// Maximum number of times the tool can be used in the API request
210+
maxUses: 5,
211+
// Only fetch from these domains
212+
allowedDomains: ["arxiv.org", "example.com"],
213+
// Or block specific domains (cannot be used with allowedDomains)
214+
// blockedDomains: ["example.com"],
215+
// Enable citations for fetched content (optional, unlike web search)
216+
citations: { enabled: true },
217+
// Maximum content length in tokens (helps control token usage)
218+
maxContentTokens: 50000,
219+
}),
220+
],
221+
}
222+
);
223+
```
224+
225+
You can combine web fetch with web search for comprehensive information gathering:
226+
227+
```typescript
228+
import { tools } from "@langchain/anthropic";
229+
230+
const response = await llm.invoke(
231+
"Find recent articles about quantum computing and analyze the most relevant one",
232+
{
233+
tools: [
234+
tools.webSearch_20250305({ maxUses: 3 }),
235+
tools.webFetch_20250910({ maxUses: 5, citations: { enabled: true } }),
236+
],
237+
}
238+
);
239+
```
240+
241+
For more information, see [Anthropic's Web Fetch Tool documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-fetch-tool).
242+
181243
## Development
182244

183245
To develop the Anthropic package, you'll need to follow these instructions:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { memory_20250818 } from "./memory.js";
22
import { webSearch_20250305 } from "./webSearch.js";
3+
import { webFetch_20250910 } from "./webFetch.js";
34

45
export const tools = {
56
memory_20250818,
67
webSearch_20250305,
8+
webFetch_20250910,
79
};
810

911
export type * from "./types.js";
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { expect, it, describe } from "vitest";
2+
import {
3+
AIMessage,
4+
AIMessageChunk,
5+
HumanMessage,
6+
} from "@langchain/core/messages";
7+
import { concat } from "@langchain/core/utils/stream";
8+
9+
import { ChatAnthropic } from "../../chat_models.js";
10+
import { webFetch_20250910 } from "../webFetch.js";
11+
12+
const createModel = () =>
13+
new ChatAnthropic({
14+
model: "claude-sonnet-4-5",
15+
temperature: 0,
16+
});
17+
18+
describe("Anthropic Web Fetch Tool Integration Tests", () => {
19+
it("web fetch tool can be bound to ChatAnthropic and triggers server tool use", async () => {
20+
const llm = createModel();
21+
const llmWithWebFetch = llm.bindTools([
22+
webFetch_20250910({ maxUses: 1, citations: { enabled: true } }),
23+
]);
24+
25+
const response = await llmWithWebFetch.invoke([
26+
new HumanMessage(
27+
"Please fetch and summarize the content at https://example.com"
28+
),
29+
]);
30+
31+
expect(response).toBeInstanceOf(AIMessage);
32+
expect(Array.isArray(response.content)).toBe(true);
33+
34+
const contentBlocks = response.content as Array<{ type: string }>;
35+
const hasServerToolUse = contentBlocks.some(
36+
(block) => block.type === "server_tool_use"
37+
);
38+
const hasWebFetchResult = contentBlocks.some(
39+
(block) => block.type === "web_fetch_tool_result"
40+
);
41+
42+
expect(hasServerToolUse).toBe(true);
43+
expect(hasWebFetchResult).toBe(true);
44+
expect(
45+
(response.content as Array<{ text: string }>).find(
46+
(block) =>
47+
block.text ===
48+
"This domain is for use in documentation examples without needing permission."
49+
)
50+
);
51+
}, 30000);
52+
53+
it("web fetch tool streaming works correctly", async () => {
54+
const llm = createModel();
55+
const llmWithWebFetch = llm.bindTools([
56+
webFetch_20250910({ maxUses: 1, citations: { enabled: true } }),
57+
]);
58+
59+
const stream = await llmWithWebFetch.stream([
60+
new HumanMessage(
61+
"Please fetch and describe the content at https://example.com"
62+
),
63+
]);
64+
65+
let finalChunk: AIMessageChunk | undefined;
66+
for await (const chunk of stream) {
67+
if (!finalChunk) {
68+
finalChunk = chunk;
69+
} else {
70+
finalChunk = concat(finalChunk, chunk);
71+
}
72+
}
73+
74+
expect(finalChunk).toBeDefined();
75+
expect(finalChunk).toBeInstanceOf(AIMessageChunk);
76+
expect(Array.isArray(finalChunk?.content)).toBe(true);
77+
const contentBlocks = finalChunk?.content as Array<{ type: string }>;
78+
const hasServerToolUse = contentBlocks.some(
79+
(block) => block.type === "server_tool_use"
80+
);
81+
82+
expect(hasServerToolUse).toBe(true);
83+
expect(
84+
(finalChunk?.content as Array<{ text: string }>).find(
85+
(block) =>
86+
block.text ===
87+
"This domain is for use in documentation examples without needing permission."
88+
)
89+
);
90+
}, 30000);
91+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect, it, describe } from "vitest";
2+
import { webFetch_20250910 } from "../webFetch.js";
3+
4+
describe("Anthropic Web Fetch Tool Unit Tests", () => {
5+
it("webFetch_20250910 creates a valid tool definition with no options", () => {
6+
expect(webFetch_20250910()).toMatchInlineSnapshot(`
7+
{
8+
"allowed_domains": undefined,
9+
"blocked_domains": undefined,
10+
"cache_control": undefined,
11+
"citations": undefined,
12+
"max_content_tokens": undefined,
13+
"max_uses": undefined,
14+
"name": "web_fetch",
15+
"type": "web_fetch_20250910",
16+
}
17+
`);
18+
});
19+
20+
it("webFetch_20250910 creates a valid tool definition with all options", () => {
21+
expect(
22+
webFetch_20250910({
23+
maxUses: 5,
24+
allowedDomains: ["example.com", "docs.example.com"],
25+
cacheControl: { type: "ephemeral" },
26+
citations: { enabled: true },
27+
maxContentTokens: 50000,
28+
})
29+
).toMatchInlineSnapshot(`
30+
{
31+
"allowed_domains": [
32+
"example.com",
33+
"docs.example.com",
34+
],
35+
"blocked_domains": undefined,
36+
"cache_control": {
37+
"type": "ephemeral",
38+
},
39+
"citations": {
40+
"enabled": true,
41+
},
42+
"max_content_tokens": 50000,
43+
"max_uses": 5,
44+
"name": "web_fetch",
45+
"type": "web_fetch_20250910",
46+
}
47+
`);
48+
});
49+
50+
it("webFetch_20250910 creates a valid tool definition with blocked domains", () => {
51+
expect(
52+
webFetch_20250910({
53+
maxUses: 10,
54+
blockedDomains: ["private.example.com", "internal.example.com"],
55+
})
56+
).toMatchInlineSnapshot(`
57+
{
58+
"allowed_domains": undefined,
59+
"blocked_domains": [
60+
"private.example.com",
61+
"internal.example.com",
62+
],
63+
"cache_control": undefined,
64+
"citations": undefined,
65+
"max_content_tokens": undefined,
66+
"max_uses": 10,
67+
"name": "web_fetch",
68+
"type": "web_fetch_20250910",
69+
}
70+
`);
71+
});
72+
73+
it("webFetch_20250910 creates a valid tool definition with citations disabled", () => {
74+
expect(
75+
webFetch_20250910({
76+
citations: { enabled: false },
77+
})
78+
).toMatchInlineSnapshot(`
79+
{
80+
"allowed_domains": undefined,
81+
"blocked_domains": undefined,
82+
"cache_control": undefined,
83+
"citations": {
84+
"enabled": false,
85+
},
86+
"max_content_tokens": undefined,
87+
"max_uses": undefined,
88+
"name": "web_fetch",
89+
"type": "web_fetch_20250910",
90+
}
91+
`);
92+
});
93+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Anthropic from "@anthropic-ai/sdk";
2+
3+
/**
4+
* Options for the web fetch tool.
5+
*/
6+
interface WebFetch20250910Options {
7+
/**
8+
* Maximum number of times the tool can be used in the API request.
9+
*/
10+
maxUses?: number;
11+
/**
12+
* If provided, only these domains will be fetched. Cannot be used
13+
* alongside `blockedDomains`.
14+
*/
15+
allowedDomains?: string[];
16+
/**
17+
* If provided, these domains will never be fetched. Cannot be used
18+
* alongside `allowedDomains`.
19+
*/
20+
blockedDomains?: string[];
21+
/**
22+
* Create a cache control breakpoint at this content block.
23+
*/
24+
cacheControl?: Anthropic.Beta.BetaCacheControlEphemeral;
25+
/**
26+
* Enable citations for fetched content. Unlike web search where citations
27+
* are always enabled, citations are optional for web fetch.
28+
*/
29+
citations?: {
30+
enabled: boolean;
31+
};
32+
/**
33+
* Maximum content length in tokens. If the fetched content exceeds this limit,
34+
* it will be truncated. This helps control token usage when fetching large documents.
35+
*/
36+
maxContentTokens?: number;
37+
}
38+
39+
/**
40+
* Creates a web fetch tool that allows Claude to retrieve full content from specified
41+
* web pages and PDF documents. Claude can only fetch URLs that have been explicitly
42+
* provided by the user or that come from previous web search or web fetch results.
43+
*
44+
* @warning Enabling the web fetch tool in environments where Claude processes untrusted
45+
* input alongside sensitive data poses data exfiltration risks. We recommend only using
46+
* this tool in trusted environments or when handling non-sensitive data.
47+
*
48+
* @see {@link https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-fetch-tool | Anthropic Web Fetch Documentation}
49+
* @param options - Configuration options for the web fetch tool
50+
* @returns A web fetch tool definition to be passed to the Anthropic API
51+
*
52+
* @example
53+
* ```typescript
54+
* import { ChatAnthropic, tools } from "@langchain/anthropic";
55+
*
56+
* const model = new ChatAnthropic({
57+
* model: "claude-sonnet-4-5-20250929",
58+
* });
59+
*
60+
* // Basic usage - fetch content from a URL
61+
* const response = await model.invoke(
62+
* "Please analyze the content at https://example.com/article",
63+
* { tools: [tools.webFetch_20250910()] }
64+
* );
65+
*
66+
* // With options
67+
* const responseWithOptions = await model.invoke(
68+
* "Summarize this research paper: https://arxiv.org/abs/2024.12345",
69+
* {
70+
* tools: [tools.webFetch_20250910({
71+
* maxUses: 5,
72+
* allowedDomains: ["arxiv.org", "example.com"],
73+
* citations: { enabled: true },
74+
* maxContentTokens: 50000,
75+
* })],
76+
* }
77+
* );
78+
*
79+
* // Combined with web search for comprehensive information gathering
80+
* const combinedResponse = await model.invoke(
81+
* "Find recent articles about quantum computing and analyze the most relevant one",
82+
* {
83+
* tools: [
84+
* tools.webSearch_20250305({ maxUses: 3 }),
85+
* tools.webFetch_20250910({ maxUses: 5, citations: { enabled: true } }),
86+
* ],
87+
* }
88+
* );
89+
* ```
90+
*/
91+
export function webFetch_20250910(
92+
options?: WebFetch20250910Options
93+
): Anthropic.Beta.BetaWebFetchTool20250910 {
94+
return {
95+
type: "web_fetch_20250910",
96+
name: "web_fetch",
97+
max_uses: options?.maxUses,
98+
allowed_domains: options?.allowedDomains,
99+
blocked_domains: options?.blockedDomains,
100+
cache_control: options?.cacheControl,
101+
citations: options?.citations,
102+
max_content_tokens: options?.maxContentTokens,
103+
};
104+
}

libs/providers/langchain-anthropic/src/utils/tools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ export const ANTHROPIC_TOOL_BETAS: Record<string, string> = {
5454
tool_search_tool_regex_20251119: "advanced-tool-use-2025-11-20",
5555
tool_search_tool_bm25_20251119: "advanced-tool-use-2025-11-20",
5656
memory_20250818: "context-management-2025-06-27",
57+
web_fetch_20250910: "web-fetch-2025-09-10",
5758
};

0 commit comments

Comments
 (0)