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:
parent
2a5c270bb0
commit
2a89b3f56c
2 changed files with 114 additions and 25 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
98
src/resources/extensions/search-the-web/native-search.ts
Normal file
98
src/resources/extensions/search-the-web/native-search.ts
Normal 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 };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue