Skip to content

Commit 922593b

Browse files
github-actions[bot]claudeenricoros
committed
feat: Add Anthropic web search and web fetch tools support
- Add web search capability to Claude 3.7, 4.5 Sonnet and 4.5 Haiku models - Implement simplified llmVndAntWebTools parameter with options: off, search, fetch, search+fetch - Add web_search_20250305 and web_fetch_20250910 tool definitions to wire types - Implement dynamic beta header injection for web-fetch-2025-09-10 - Update message adapter to inject tools based on configuration - Add UI parameter support for web tools control Implements requested feature for Anthropic's May 2025 web search API and September 2025 web fetch tool, providing similar UX to GPT-5 and o4-deep research functionality. Fixes: #842 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Enrico Ros <enricoros@users.noreply.github.com>
1 parent 5d7b00f commit 922593b

8 files changed

Lines changed: 101 additions & 20 deletions

File tree

src/common/stores/llms/llms.parameters.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ export const DModelParameterRegistry = {
8888
} as const,
8989
} as const,
9090

91+
llmVndAntWebTools: {
92+
label: 'Web Tools',
93+
type: 'enum' as const,
94+
description: 'Enable web search and fetch capabilities',
95+
values: ['off', 'search', 'fetch', 'search+fetch'] as const,
96+
// No initialValue - defaults to undefined (off)
97+
} as const,
98+
9199
llmVndGeminiAspectRatio: {
92100
label: 'Aspect Ratio',
93101
type: 'enum' as const,

src/modules/aix/server/api/aix.wiretypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ export namespace AixWire_API {
426426
topP: z.number().min(0).max(1).optional(),
427427
forceNoStream: z.boolean().optional(),
428428
vndAntThinkingBudget: z.number().nullable().optional(),
429+
vndAntWebTools: z.enum(['off', 'search', 'fetch', 'search+fetch']).optional(),
429430
vndGeminiAspectRatio: z.enum(['1:1', '2:3', '3:2', '3:4', '4:3', '9:16', '16:9', '21:9']).optional(),
430431
vndGeminiGoogleSearch: z.enum(['unfiltered', '1d', '1w', '1m', '6m', '1y']).optional(),
431432
vndGeminiShowThoughts: z.boolean().optional(),

src/modules/aix/server/dispatch/chatGenerate/adapters/anthropic.messageCreate.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,37 @@ export function aixToAnthropicMessageCreate(model: AixAPI_Model, _chatGenerate:
141141
// --- Tools ---
142142

143143
// Allow/deny auto-adding hosted tools when custom tools are present
144-
// const hasCustomTools = chatGenerate.tools?.some(t => t.type === 'function_call');
145-
// const hasRestrictivePolicy = chatGenerate.toolsPolicy?.type === 'any' || chatGenerate.toolsPolicy?.type === 'function_call';
146-
// const skipHostedToolsDueToCustomTools = hasCustomTools && hasRestrictivePolicy;
144+
const hasCustomTools = chatGenerate.tools?.some(t => t.type === 'function_call');
145+
const hasRestrictivePolicy = chatGenerate.toolsPolicy?.type === 'any' || chatGenerate.toolsPolicy?.type === 'function_call';
146+
const skipHostedToolsDueToCustomTools = hasCustomTools && hasRestrictivePolicy;
147+
148+
// Hosted tools: Web Search and Web Fetch
149+
if (model.vndAntWebTools && !skipHostedToolsDueToCustomTools) {
150+
const tools = payload.tools || [];
151+
152+
// Add web search tool
153+
if (model.vndAntWebTools === 'search' || model.vndAntWebTools === 'search+fetch') {
154+
tools.push({
155+
type: 'web_search_20250305',
156+
name: 'web_search',
157+
max_uses: 5, // reasonable default
158+
// Could add more configuration based on future parameters
159+
} as any);
160+
}
161+
162+
// Add web fetch tool
163+
if (model.vndAntWebTools === 'fetch' || model.vndAntWebTools === 'search+fetch') {
164+
tools.push({
165+
type: 'web_fetch_20250910',
166+
name: 'web_fetch',
167+
max_uses: 5, // reasonable default
168+
citations: { enabled: true }, // enable citations by default
169+
// Could add more configuration based on future parameters
170+
} as any);
171+
}
147172

148-
// Hosted tools
149-
// ...
173+
payload.tools = tools.length > 0 ? tools : undefined;
174+
}
150175

151176

152177
// Preemptive error detection with server-side payload validation before sending it upstream

src/modules/aix/server/dispatch/chatGenerate/chatGenerate.dispatch.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,22 @@ export function createChatGenerateDispatch(access: AixAPI_Access, model: AixAPI_
3636
} {
3737

3838
switch (access.dialect) {
39-
case 'anthropic':
39+
case 'anthropic': {
40+
// Add web-fetch beta header if web fetch is enabled
41+
const additionalBetaFeatures: string[] = [];
42+
if (model.vndAntWebTools === 'fetch' || model.vndAntWebTools === 'search+fetch') {
43+
additionalBetaFeatures.push('web-fetch-2025-09-10');
44+
}
45+
4046
return {
4147
request: {
42-
...anthropicAccess(access, model.id, '/v1/messages'),
48+
...anthropicAccess(access, model.id, '/v1/messages', additionalBetaFeatures),
4349
body: aixToAnthropicMessageCreate(model, chatGenerate, streaming),
4450
},
4551
demuxerFormat: streaming ? 'fast-sse' : null,
4652
chatGenerateParse: streaming ? createAnthropicMessageParser() : createAnthropicMessageParserNS(),
4753
};
54+
}
4855

4956
case 'gemini':
5057
/**

src/modules/aix/server/dispatch/wiretypes/anthropic.wiretypes.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,44 @@ export namespace AnthropicWire_Tools {
223223
name: z.literal('str_replace_editor'),
224224
});
225225

226+
const _WebSearch_20250305_schema = _ToolDefinitionBase_schema.extend({
227+
type: z.enum(['web_search_20250305']),
228+
name: z.literal('web_search'),
229+
230+
// tool configuration
231+
max_uses: z.number().int().optional(),
232+
allowed_domains: z.array(z.string()).optional(),
233+
blocked_domains: z.array(z.string()).optional(),
234+
user_location: z.object({
235+
type: z.literal('approximate'),
236+
city: z.string().optional(),
237+
region: z.string().optional(),
238+
country: z.string().optional(),
239+
timezone: z.string().optional(),
240+
}).optional(),
241+
});
242+
243+
const _WebFetch_20250910_schema = _ToolDefinitionBase_schema.extend({
244+
type: z.enum(['web_fetch_20250910']),
245+
name: z.literal('web_fetch'),
246+
247+
// tool configuration
248+
max_uses: z.number().int().optional(),
249+
allowed_domains: z.array(z.string()).optional(),
250+
blocked_domains: z.array(z.string()).optional(),
251+
citations: z.object({
252+
enabled: z.boolean(),
253+
}).optional(),
254+
max_content_tokens: z.number().int().optional(),
255+
});
256+
226257
export const ToolDefinition_schema = z.discriminatedUnion('type', [
227258
_CustomToolDefinition_schema,
228259
_ComputerUseTool_20241022_schema,
229260
_BashTool_20241022_schema,
230261
_TextEditor_20241022_schema,
262+
_WebSearch_20250305_schema,
263+
_WebFetch_20250910_schema,
231264
]);
232265

233266
}

src/modules/llms/server/anthropic/anthropic.models.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Reasoning, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
1+
import { LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Reasoning, LLM_IF_OAI_Vision, LLM_IF_Tools_WebSearch } from '~/common/stores/llms/llms.types';
22

33
import type { ModelDescriptionSchema } from '../llm.server.types';
44

@@ -10,19 +10,19 @@ export const hardcodedAnthropicVariants: { [modelId: string]: Partial<ModelDescr
1010
idVariant: 'thinking',
1111
label: 'Claude Sonnet 4.5 (Thinking)',
1212
description: 'Claude Sonnet 4.5 with extended thinking mode enabled for complex reasoning',
13-
parameterSpecs: [{ paramId: 'llmVndAntThinkingBudget', required: true, hidden: false }],
13+
parameterSpecs: [{ paramId: 'llmVndAntThinkingBudget', required: true, hidden: false }, { paramId: 'llmVndAntWebTools' }],
1414
maxCompletionTokens: 64000,
15-
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Reasoning],
15+
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_Tools_WebSearch],
1616
benchmark: { cbaElo: 1451 + 1 }, // FALLBACK-UNTIL-AVAILABLE: claude-opus-4-1-20250805-thinking-16k + 1
1717
},
1818

1919
'claude-haiku-4-5-20251001': {
2020
idVariant: 'thinking',
2121
label: 'Claude Haiku 4.5 (Thinking)',
2222
description: 'Claude Haiku 4.5 with extended thinking mode - first Haiku model with reasoning capabilities',
23-
parameterSpecs: [{ paramId: 'llmVndAntThinkingBudget', required: true, hidden: false }],
23+
parameterSpecs: [{ paramId: 'llmVndAntThinkingBudget', required: true, hidden: false }, { paramId: 'llmVndAntWebTools' }],
2424
maxCompletionTokens: 64000,
25-
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Reasoning],
25+
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_Tools_WebSearch],
2626
},
2727

2828
// Claude 4.1 models with thinking variants
@@ -63,9 +63,9 @@ export const hardcodedAnthropicVariants: { [modelId: string]: Partial<ModelDescr
6363
idVariant: 'thinking',
6464
label: 'Claude Sonnet 3.7 (Thinking)',
6565
description: 'Claude 3.7 with extended thinking mode enabled for complex reasoning',
66-
parameterSpecs: [{ paramId: 'llmVndAntThinkingBudget', required: true, hidden: false }],
66+
parameterSpecs: [{ paramId: 'llmVndAntThinkingBudget', required: true, hidden: false }, { paramId: 'llmVndAntWebTools' }],
6767
maxCompletionTokens: 64000,
68-
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Reasoning],
68+
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_Tools_WebSearch],
6969
benchmark: { cbaElo: 1385 }, // claude-3-7-sonnet-20250219-thinking-32k
7070
},
7171

@@ -82,7 +82,8 @@ export const hardcodedAnthropicModels: (ModelDescriptionSchema & { isLegacy?: bo
8282
contextWindow: 200000,
8383
maxCompletionTokens: 64000,
8484
trainingDataCutoff: 'Jul 2025',
85-
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching],
85+
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_Tools_WebSearch],
86+
parameterSpecs: [{ paramId: 'llmVndAntWebTools' }],
8687
// Note: Tiered pricing - ≤200K: $3/$15, >200K: $6/$22.50. Using lower tier as base.
8788
chatPrice: { input: 3, output: 15, cache: { cType: 'ant-bp', read: 0.30, write: 3.75, duration: 300 } },
8889
benchmark: { cbaElo: 1438 + 1 }, // FALLBACK-UNTIL-AVAILABLE: claude-opus-4-1-20250805 + 1
@@ -94,7 +95,8 @@ export const hardcodedAnthropicModels: (ModelDescriptionSchema & { isLegacy?: bo
9495
contextWindow: 200000,
9596
maxCompletionTokens: 64000,
9697
trainingDataCutoff: 'Jul 2025',
97-
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching],
98+
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_Tools_WebSearch],
99+
parameterSpecs: [{ paramId: 'llmVndAntWebTools' }],
98100
chatPrice: { input: 1, output: 5, cache: { cType: 'ant-bp', read: 0.10, write: 1.25, duration: 300 } },
99101
},
100102

@@ -144,7 +146,8 @@ export const hardcodedAnthropicModels: (ModelDescriptionSchema & { isLegacy?: bo
144146
contextWindow: 200000,
145147
maxCompletionTokens: 64000,
146148
trainingDataCutoff: 'Nov 2024',
147-
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching],
149+
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Fn, LLM_IF_ANT_PromptCaching, LLM_IF_Tools_WebSearch],
150+
parameterSpecs: [{ paramId: 'llmVndAntWebTools' }],
148151
chatPrice: { input: 3, output: 15, cache: { cType: 'ant-bp', read: 0.30, write: 3.75, duration: 300 } },
149152
benchmark: { cbaElo: 1369 }, // claude-3-7-sonnet-20250219
150153
},

src/modules/llms/server/anthropic/anthropic.router.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const PER_MODEL_BETA_FEATURES: { [modelId: string]: string[] } = {
7878
] as const,
7979
} as const;
8080

81-
function _anthropicHeaders(modelId?: string): HeadersInit {
81+
function _anthropicHeaders(modelId?: string, additionalBetaFeatures?: string[]): HeadersInit {
8282

8383
// accumulate the beta features
8484
const betaFeatures = [...DEFAULT_ANTHROPIC_BETA_FEATURES];
@@ -88,6 +88,9 @@ function _anthropicHeaders(modelId?: string): HeadersInit {
8888
if (key.includes(modelId))
8989
betaFeatures.push(...value);
9090
}
91+
if (additionalBetaFeatures) {
92+
betaFeatures.push(...additionalBetaFeatures);
93+
}
9194

9295
return {
9396
...DEFAULT_ANTHROPIC_HEADERS,
@@ -108,7 +111,7 @@ async function anthropicGETOrThrow<TOut extends object>(access: AnthropicAccessS
108111
// return await fetchJsonOrTRPCThrow<TOut, TPostBody>({ url, method: 'POST', headers, body, name: 'Anthropic' });
109112
// }
110113

111-
export function anthropicAccess(access: AnthropicAccessSchema, antModelIdForBetaFeatures: undefined | string, apiPath: string): { headers: HeadersInit, url: string } {
114+
export function anthropicAccess(access: AnthropicAccessSchema, antModelIdForBetaFeatures: undefined | string, apiPath: string, additionalBetaFeatures?: string[]): { headers: HeadersInit, url: string } {
112115
// API key
113116
const anthropicKey = access.anthropicKey || env.ANTHROPIC_API_KEY || '';
114117

@@ -135,7 +138,7 @@ export function anthropicAccess(access: AnthropicAccessSchema, antModelIdForBeta
135138
headers: {
136139
'Accept': 'application/json',
137140
'Content-Type': 'application/json',
138-
..._anthropicHeaders(antModelIdForBetaFeatures),
141+
..._anthropicHeaders(antModelIdForBetaFeatures, additionalBetaFeatures),
139142
'X-API-Key': anthropicKey,
140143
...(heliKey && { 'Helicone-Auth': `Bearer ${heliKey}` }),
141144
},

src/modules/llms/server/llm.server.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const ModelParameterSpec_schema = z.object({
7878
'llmTopP',
7979
'llmForceNoStream',
8080
'llmVndAntThinkingBudget',
81+
'llmVndAntWebTools',
8182
'llmVndGeminiAspectRatio',
8283
'llmVndGeminiGoogleSearch',
8384
'llmVndGeminiShowThoughts',

0 commit comments

Comments
 (0)