From e22a2f762253b70e938e51e068c3ea3e8a59db77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Facu=5FVi=C3=B1as?= Date: Wed, 11 Mar 2026 17:54:09 -0300 Subject: [PATCH] fix: remove Brave search tools from API payload when no BRAVE_API_KEY The model_select event doesn't reliably fire on startup, so Brave tools remained visible to Claude even without a key. Now before_provider_request filters search-the-web and search_and_read from the payload directly, ensuring Claude only sees the native web_search tool. Co-Authored-By: Claude Opus 4.6 --- .../search-the-web/native-search.ts | 14 +++- src/tests/native-search.test.ts | 70 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/search-the-web/native-search.ts b/src/resources/extensions/search-the-web/native-search.ts index 3b6cd23d6..6c237fd37 100644 --- a/src/resources/extensions/search-the-web/native-search.ts +++ b/src/resources/extensions/search-the-web/native-search.ts @@ -111,10 +111,22 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic: if (!Array.isArray(payload.tools)) payload.tools = []; + let tools = payload.tools as Array>; + // Don't double-inject if already present - const tools = payload.tools as Array>; if (tools.some((t) => t.type === "web_search_20250305")) return; + // When no Brave key, remove Brave-based search tool definitions from the + // payload so Claude doesn't see (and try to call) broken tools. + // This is more reliable than setActiveTools since model_select may not fire. + const hasBrave = !!process.env.BRAVE_API_KEY; + if (!hasBrave) { + tools = tools.filter( + (t) => !BRAVE_TOOL_NAMES.includes(t.name as string) + ); + payload.tools = tools; + } + tools.push({ type: "web_search_20250305", name: "web_search", diff --git a/src/tests/native-search.test.ts b/src/tests/native-search.test.ts index b84db2aea..e466720e0 100644 --- a/src/tests/native-search.test.ts +++ b/src/tests/native-search.test.ts @@ -331,6 +331,76 @@ test("BRAVE_TOOL_NAMES contains expected tool names", () => { assert.deepEqual(BRAVE_TOOL_NAMES, ["search-the-web", "search_and_read"]); }); +test("before_provider_request removes Brave tools from payload when no BRAVE_API_KEY", async () => { + const originalKey = process.env.BRAVE_API_KEY; + delete process.env.BRAVE_API_KEY; + + try { + const pi = createMockPI(); + registerNativeSearchHooks(pi); + + const payload: Record = { + model: "claude-sonnet-4-6-20250514", + tools: [ + { name: "bash", type: "function" }, + { name: "search-the-web", type: "function" }, + { name: "search_and_read", type: "function" }, + { name: "fetch_page", type: "function" }, + ], + }; + + const result = await pi.fire("before_provider_request", { + type: "before_provider_request", + payload, + }); + + const tools = ((result as any)?.tools ?? payload.tools) as any[]; + const names = tools.map((t: any) => t.name); + + assert.ok(!names.includes("search-the-web"), "search-the-web should be removed from payload"); + assert.ok(!names.includes("search_and_read"), "search_and_read should be removed from payload"); + assert.ok(names.includes("bash"), "bash should remain"); + assert.ok(names.includes("fetch_page"), "fetch_page should remain"); + assert.ok(names.includes("web_search"), "native web_search should be injected"); + } finally { + if (originalKey) process.env.BRAVE_API_KEY = originalKey; + else delete process.env.BRAVE_API_KEY; + } +}); + +test("before_provider_request keeps Brave tools in payload when BRAVE_API_KEY set", async () => { + const originalKey = process.env.BRAVE_API_KEY; + process.env.BRAVE_API_KEY = "test-key"; + + try { + const pi = createMockPI(); + registerNativeSearchHooks(pi); + + const payload: Record = { + model: "claude-sonnet-4-6-20250514", + tools: [ + { name: "search-the-web", type: "function" }, + { name: "search_and_read", type: "function" }, + ], + }; + + const result = await pi.fire("before_provider_request", { + type: "before_provider_request", + payload, + }); + + const tools = ((result as any)?.tools ?? payload.tools) as any[]; + const names = tools.map((t: any) => t.name); + + assert.ok(names.includes("search-the-web"), "search-the-web should remain with Brave key"); + assert.ok(names.includes("search_and_read"), "search_and_read should remain with Brave key"); + assert.ok(names.includes("web_search"), "native web_search should also be injected"); + } finally { + if (originalKey) process.env.BRAVE_API_KEY = originalKey; + else delete process.env.BRAVE_API_KEY; + } +}); + // ─── stripThinkingFromHistory tests ───────────────────────────────────────── test("stripThinkingFromHistory removes thinking from earlier assistant messages", () => {