Skip to content

Commit 4f54f25

Browse files
feat(anthropic): support webfetch tool
1 parent b8f3094 commit 4f54f25

File tree

4 files changed

+297
-0
lines changed

4 files changed

+297
-0
lines changed
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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
clientOptions: {
17+
defaultHeaders: {
18+
"anthropic-beta": "web-fetch-2025-09-10",
19+
},
20+
},
21+
});
22+
23+
describe("Anthropic Web Fetch Tool Integration Tests", () => {
24+
it("web fetch tool can be bound to ChatAnthropic and triggers server tool use", async () => {
25+
const llm = createModel();
26+
const llmWithWebFetch = llm.bindTools([
27+
webFetch_20250910({ maxUses: 1, citations: { enabled: true } }),
28+
]);
29+
30+
const response = await llmWithWebFetch.invoke([
31+
new HumanMessage(
32+
"Please fetch and summarize the content at https://example.com"
33+
),
34+
]);
35+
36+
expect(response).toBeInstanceOf(AIMessage);
37+
expect(Array.isArray(response.content)).toBe(true);
38+
39+
const contentBlocks = response.content as Array<{ type: string }>;
40+
const hasServerToolUse = contentBlocks.some(
41+
(block) => block.type === "server_tool_use"
42+
);
43+
const hasWebFetchResult = contentBlocks.some(
44+
(block) => block.type === "web_fetch_tool_result"
45+
);
46+
47+
expect(hasServerToolUse).toBe(true);
48+
expect(hasWebFetchResult).toBe(true);
49+
expect(
50+
(response.content as Array<{ text: string }>).find(
51+
(block) =>
52+
block.text ===
53+
"This domain is for use in documentation examples without needing permission."
54+
)
55+
);
56+
}, 30000);
57+
58+
it("web fetch tool streaming works correctly", async () => {
59+
const llm = createModel();
60+
const llmWithWebFetch = llm.bindTools([
61+
webFetch_20250910({ maxUses: 1, citations: { enabled: true } }),
62+
]);
63+
64+
const stream = await llmWithWebFetch.stream([
65+
new HumanMessage(
66+
"Please fetch and describe the content at https://example.com"
67+
),
68+
]);
69+
70+
let finalChunk: AIMessageChunk | undefined;
71+
for await (const chunk of stream) {
72+
if (!finalChunk) {
73+
finalChunk = chunk;
74+
} else {
75+
finalChunk = concat(finalChunk, chunk);
76+
}
77+
}
78+
79+
expect(finalChunk).toBeDefined();
80+
expect(finalChunk).toBeInstanceOf(AIMessageChunk);
81+
expect(Array.isArray(finalChunk?.content)).toBe(true);
82+
const contentBlocks = finalChunk?.content as Array<{ type: string }>;
83+
const hasServerToolUse = contentBlocks.some(
84+
(block) => block.type === "server_tool_use"
85+
);
86+
87+
expect(hasServerToolUse).toBe(true);
88+
expect(
89+
(finalChunk?.content as Array<{ text: string }>).find(
90+
(block) =>
91+
block.text ===
92+
"This domain is for use in documentation examples without needing permission."
93+
)
94+
);
95+
}, 30000);
96+
});
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: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
* @note This tool requires the beta header `web-fetch-2025-09-10` in API requests.
49+
*
50+
* @see {@link https://docs.anthropic.com/en/docs/build-with-claude/tool-use/web-fetch-tool | Anthropic Web Fetch Documentation}
51+
* @param options - Configuration options for the web fetch tool
52+
* @returns A web fetch tool definition to be passed to the Anthropic API
53+
*
54+
* @example
55+
* ```typescript
56+
* import { ChatAnthropic, tools } from "@langchain/anthropic";
57+
*
58+
* const model = new ChatAnthropic({
59+
* model: "claude-sonnet-4-5-20250929",
60+
* });
61+
*
62+
* // Basic usage - fetch content from a URL
63+
* const response = await model.invoke(
64+
* "Please analyze the content at https://example.com/article",
65+
* { tools: [tools.webFetch_20250910()] }
66+
* );
67+
*
68+
* // With options
69+
* const responseWithOptions = await model.invoke(
70+
* "Summarize this research paper: https://arxiv.org/abs/2024.12345",
71+
* {
72+
* tools: [tools.webFetch_20250910({
73+
* maxUses: 5,
74+
* allowedDomains: ["arxiv.org", "example.com"],
75+
* citations: { enabled: true },
76+
* maxContentTokens: 50000,
77+
* })],
78+
* }
79+
* );
80+
*
81+
* // Combined with web search for comprehensive information gathering
82+
* const combinedResponse = await model.invoke(
83+
* "Find recent articles about quantum computing and analyze the most relevant one",
84+
* {
85+
* tools: [
86+
* tools.webSearch_20250305({ maxUses: 3 }),
87+
* tools.webFetch_20250910({ maxUses: 5, citations: { enabled: true } }),
88+
* ],
89+
* }
90+
* );
91+
* ```
92+
*/
93+
export function webFetch_20250910(
94+
options?: WebFetch20250910Options
95+
): Anthropic.Beta.BetaWebFetchTool20250910 {
96+
return {
97+
type: "web_fetch_20250910",
98+
name: "web_fetch",
99+
max_uses: options?.maxUses,
100+
allowed_domains: options?.allowedDomains,
101+
blocked_domains: options?.blockedDomains,
102+
cache_control: options?.cacheControl,
103+
citations: options?.citations,
104+
max_content_tokens: options?.maxContentTokens,
105+
};
106+
}

0 commit comments

Comments
 (0)