feat: add native Anthropic web search via before_provider_request hook

Inject the web_search_20250305 server-side tool into Anthropic API
requests, eliminating the BRAVE_API_KEY requirement for Anthropic models.
When Anthropic + no Brave key, custom search tools are disabled to avoid
confusing the LLM with broken tools. fetch_page (Jina) is unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Facu_Viñas 2026-03-11 15:45:54 -03:00
parent 2a5c270bb0
commit 2a89b3f56c
2 changed files with 114 additions and 25 deletions

View file

@ -1,5 +1,5 @@
/**
* Web Search Extension v3
* Web Search Extension v4
*
* Provides three tools for grounding the agent in real-world web content:
*
@ -15,6 +15,15 @@
* Returns pre-extracted, relevance-scored page content.
* Best when you need content, not just links.
*
* v4: Native Anthropic web search
* - When using an Anthropic provider, injects the native `web_search_20250305`
* server-side tool via `before_provider_request`. This eliminates the need for
* a BRAVE_API_KEY when using Anthropic models search is billed through the
* existing Anthropic API key ($0.01/search).
* - Custom Brave-based tools (search-the-web, search_and_read) are disabled when
* Anthropic + no BRAVE_API_KEY to avoid confusing the LLM with broken tools.
* - fetch_page (Jina) remains available it works without a key at lower rate limits.
*
* v3 improvements over v2:
* - search_and_read: New tool Brave LLM Context API (search + read in one call)
* - Structured error taxonomy: auth_error, rate_limited, network_error, etc.
@ -30,7 +39,8 @@
* - Cache timer cleanup: purge timers use unref() to not block process exit
*
* Environment variables:
* BRAVE_API_KEY Required for search. Get one at brave.com/search/api
* BRAVE_API_KEY Optional with Anthropic models (built-in search available).
* Required for non-Anthropic providers. Get one at brave.com/search/api
* JINA_API_KEY Optional. Higher rate limits for page extraction.
*/
@ -39,36 +49,17 @@ import { registerSearchTool } from "./tool-search";
import { registerFetchPageTool } from "./tool-fetch-page";
import { registerLLMContextTool } from "./tool-llm-context";
import { registerSearchProviderCommand } from "./command-search-provider.ts";
import { registerNativeSearchHooks } from "./native-search";
export default function (pi: ExtensionAPI) {
// Register all tools
registerSearchTool(pi);
registerFetchPageTool(pi);
registerLLMContextTool(pi);
// Register slash commands
registerSearchProviderCommand(pi);
// Startup diagnostics
pi.on("session_start", async (_event, ctx) => {
const hasBrave = !!process.env.BRAVE_API_KEY;
const hasTavily = !!process.env.TAVILY_API_KEY;
const hasJina = !!process.env.JINA_API_KEY;
const hasAnswers = !!process.env.BRAVE_ANSWERS_KEY;
if (!hasBrave && !hasTavily) {
ctx.ui.notify(
"Web search: Set BRAVE_API_KEY or TAVILY_API_KEY for web search capability",
"warning"
);
}
const parts: string[] = ["Web search v3"];
if (hasTavily) parts.push("Tavily ✓");
if (hasBrave) parts.push("Search ✓");
if (hasAnswers) parts.push("Answers ✓");
if (hasJina) parts.push("Jina ✓");
ctx.ui.notify(parts.join(" · "), "info");
});
// Register native Anthropic web search hooks
registerNativeSearchHooks(pi);
}

View file

@ -0,0 +1,98 @@
/**
* Native Anthropic web search hook logic.
*
* Extracted from index.ts so it can be unit-tested without importing
* the heavy tool-registration modules.
*/
/** Tool names for the Brave-backed custom search tools */
export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
/** Minimal interface matching the subset of ExtensionAPI we use */
export interface NativeSearchPI {
on(event: string, handler: (...args: any[]) => any): void;
getActiveTools(): string[];
setActiveTools(tools: string[]): void;
}
/**
* Register model_select, before_provider_request, and session_start hooks
* for native Anthropic web search injection.
*
* Returns the isAnthropicProvider getter for testing.
*/
export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic: () => boolean } {
let isAnthropicProvider = false;
// Track provider changes via model selection
pi.on("model_select", async (event: any, _ctx: any) => {
const wasAnthropic = isAnthropicProvider;
isAnthropicProvider = event.model.provider === "anthropic";
const hasBrave = !!process.env.BRAVE_API_KEY;
// When Anthropic + no Brave key: disable custom search tools (they'd fail)
if (isAnthropicProvider && !hasBrave) {
const active = pi.getActiveTools();
pi.setActiveTools(
active.filter((t: string) => !BRAVE_TOOL_NAMES.includes(t))
);
} else if (!isAnthropicProvider && wasAnthropic && !hasBrave) {
// Switching away from Anthropic without Brave — re-enable so the user
// sees the "missing key" error rather than tools silently vanishing
const active = pi.getActiveTools();
pi.setActiveTools([...active, ...BRAVE_TOOL_NAMES]);
}
});
// Inject native web search into Anthropic API requests
pi.on("before_provider_request", (event: any) => {
const payload = event.payload as Record<string, unknown>;
if (!payload) return;
// Detect Anthropic by model name prefix (works even before model_select fires)
const model = payload.model as string | undefined;
if (!model || !model.startsWith("claude")) return;
// Keep provider tracking in sync
isAnthropicProvider = true;
if (!Array.isArray(payload.tools)) payload.tools = [];
// Don't double-inject if already present
const tools = payload.tools as Array<Record<string, unknown>>;
if (tools.some((t) => t.type === "web_search_20250305")) return;
tools.push({
type: "web_search_20250305",
name: "web_search",
});
return payload;
});
// Startup diagnostics
pi.on("session_start", async (_event: any, ctx: any) => {
const hasBrave = !!process.env.BRAVE_API_KEY;
const hasJina = !!process.env.JINA_API_KEY;
const hasAnswers = !!process.env.BRAVE_ANSWERS_KEY;
const parts: string[] = ["Web search v4 loaded"];
if (isAnthropicProvider) parts.push("Native search ✓");
if (hasBrave) parts.push("Brave ✓");
if (hasAnswers) parts.push("Answers ✓");
if (hasJina) parts.push("Jina ✓");
if (!isAnthropicProvider && !hasBrave) {
ctx.ui.notify(
"Web search: Set BRAVE_API_KEY or use an Anthropic model for built-in search",
"warning"
);
}
ctx.ui.notify(parts.join(" · "), "info");
});
return { getIsAnthropic: () => isAnthropicProvider };
}