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 <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-13 10:22:52 -06:00 committed by GitHub
parent c87f15868a
commit c344b0af54
2 changed files with 72 additions and 6 deletions

View file

@ -99,12 +99,11 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): { getIsAnthropic:
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;
// 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.

View file

@ -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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
model: "claude-sonnet-4-6-20250514",
tools: [