From c344b0af5414d5fcef32799a445687988bde6689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Fri, 13 Mar 2026 10:22:52 -0600 Subject: [PATCH] fix: use provider field instead of model name prefix for Anthropic detection (#142) (#196) The before_provider_request hook used model.startsWith("claude") to gate native web search injection. This matched claude-* models served by any provider (GitHub Copilot, AWS Bedrock, etc.), incorrectly injecting Anthropic-only web_search_20250305 tool definitions into non-Anthropic API requests. The fix checks the isAnthropicProvider flag (set by model_select via the provider field) instead of sniffing the model name. Co-authored-by: Claude Opus 4.6 --- .../search-the-web/native-search.ts | 11 ++- src/tests/native-search.test.ts | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/resources/extensions/search-the-web/native-search.ts b/src/resources/extensions/search-the-web/native-search.ts index 30fda9e46..7aac943a7 100644 --- a/src/resources/extensions/search-the-web/native-search.ts +++ b/src/resources/extensions/search-the-web/native-search.ts @@ -99,12 +99,11 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic: const payload = event.payload as Record; 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; + // Only inject native web search for confirmed Anthropic provider. + // model_select sets isAnthropicProvider via the provider field. + // Model name prefix is NOT sufficient — other providers (GitHub Copilot, + // AWS Bedrock, etc.) serve claude-* models through non-Anthropic APIs. + if (!isAnthropicProvider) return; // Strip thinking blocks from history to avoid signature validation errors // caused by the SDK dropping server_tool_use/web_search_tool_result blocks. diff --git a/src/tests/native-search.test.ts b/src/tests/native-search.test.ts index 02f566764..2f2474b23 100644 --- a/src/tests/native-search.test.ts +++ b/src/tests/native-search.test.ts @@ -72,6 +72,14 @@ test("before_provider_request injects web_search for claude models", async () => const pi = createMockPI(); registerNativeSearchHooks(pi); + // Confirm Anthropic provider via model_select before request + await pi.fire("model_select", { + type: "model_select", + model: { provider: "anthropic", name: "claude-sonnet-4-6" }, + previousModel: undefined, + source: "set", + }); + const payload: Record = { model: "claude-sonnet-4-6-20250514", tools: [{ name: "bash", type: "custom" }], @@ -109,10 +117,48 @@ test("before_provider_request does NOT inject for non-claude models", async () = assert.equal(tools.length, 1, "Should not add tools to non-claude payload"); }); +test("before_provider_request does NOT inject for claude model on non-Anthropic provider", async () => { + const pi = createMockPI(); + registerNativeSearchHooks(pi); + + // GitHub Copilot (or Bedrock, etc.) serving a claude model + await pi.fire("model_select", { + type: "model_select", + model: { provider: "copilot", name: "claude-sonnet-4-6" }, + previousModel: undefined, + source: "set", + }); + + const payload: Record = { + model: "claude-sonnet-4-6-20250514", + tools: [{ name: "bash", type: "custom" }], + }; + + const result = await pi.fire("before_provider_request", { + type: "before_provider_request", + payload, + }); + + assert.equal(result, undefined, "Should not modify payload for non-Anthropic provider"); + const tools = payload.tools as any[]; + assert.equal(tools.length, 1, "Should not inject web_search for non-Anthropic provider"); + assert.ok( + !tools.some((t: any) => t.type === "web_search_20250305"), + "web_search_20250305 must NOT be present for non-Anthropic providers" + ); +}); + test("before_provider_request does not double-inject", async () => { const pi = createMockPI(); registerNativeSearchHooks(pi); + await pi.fire("model_select", { + type: "model_select", + model: { provider: "anthropic", name: "claude-opus-4-6" }, + previousModel: undefined, + source: "set", + }); + const payload: Record = { model: "claude-opus-4-6-20250514", tools: [{ type: "web_search_20250305", name: "web_search" }], @@ -132,6 +178,13 @@ test("before_provider_request creates tools array if missing", async () => { const pi = createMockPI(); registerNativeSearchHooks(pi); + await pi.fire("model_select", { + type: "model_select", + model: { provider: "anthropic", name: "claude-haiku-4-5" }, + previousModel: undefined, + source: "set", + }); + const payload: Record = { model: "claude-haiku-4-5-20251001", }; @@ -342,6 +395,13 @@ test("before_provider_request removes Brave tools from payload when no BRAVE_API const pi = createMockPI(); registerNativeSearchHooks(pi); + await pi.fire("model_select", { + type: "model_select", + model: { provider: "anthropic", name: "claude-sonnet-4-6" }, + previousModel: undefined, + source: "set", + }); + const payload: Record = { model: "claude-sonnet-4-6-20250514", tools: [ @@ -379,6 +439,13 @@ test("before_provider_request keeps Brave tools in payload when BRAVE_API_KEY se const pi = createMockPI(); registerNativeSearchHooks(pi); + await pi.fire("model_select", { + type: "model_select", + model: { provider: "anthropic", name: "claude-sonnet-4-6" }, + previousModel: undefined, + source: "set", + }); + const payload: Record = { model: "claude-sonnet-4-6-20250514", tools: [