From 3be585ef41a152363c4459db124705f0448a2cf0 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Fri, 7 Aug 2026 18:08:44 -0300 Subject: [PATCH] fix(providers): use prefix regex for web search fallback detector to catch versioned tool types (#9279) Co-authored-by: diegosouzapw --- changelog.d/fixes/9279-fix.plan.md | 1 + open-sse/services/webSearchFallback.ts | 9 ++- tests/unit/web-search-9279-repro.test.ts | 81 ++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 changelog.d/fixes/9279-fix.plan.md create mode 100644 tests/unit/web-search-9279-repro.test.ts diff --git a/changelog.d/fixes/9279-fix.plan.md b/changelog.d/fixes/9279-fix.plan.md new file mode 100644 index 0000000000..5dbc10c5f4 --- /dev/null +++ b/changelog.d/fixes/9279-fix.plan.md @@ -0,0 +1 @@ +- **fix(providers):** the web search fallback detector in `webSearchFallback.ts` used an exact `Set` (`web_search`, `web_search_preview`) that missed Anthropic's date-suffixed server-tool variant `web_search_20250305` (sent by Claude Code 2.1.220+). Changed to prefix regex `/^web_search/`, matching the two other detectors in the codebase, so the fallback intercepts versioned web search tools for OpenAI-compatible upstreams ([#9279](https://github.com/diegosouzapw/OmniRoute/pull/9279)) diff --git a/open-sse/services/webSearchFallback.ts b/open-sse/services/webSearchFallback.ts index 7ae1749803..0cc33fdac7 100644 --- a/open-sse/services/webSearchFallback.ts +++ b/open-sse/services/webSearchFallback.ts @@ -1,7 +1,10 @@ import { FORMATS } from "../translator/formats.ts"; export const OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME = "omniroute_web_search"; -const WEB_SEARCH_TOOL_TYPES = new Set(["web_search", "web_search_preview"]); +// Prefix match — Anthropic sends date-suffixed variants (web_search_20250305, …). +// The other two detectors (openai-responses/helpers.ts, webSearchRouting.ts) already +// use /^web_search/ prefix matching; this aligns the fallback detector with them. +const WEB_SEARCH_TOOL_TYPES = /^web_search/; const SEARCH_CONTEXT_DEFAULTS: Record = { low: 5, medium: 8, @@ -27,13 +30,13 @@ function toRecord(value: unknown): JsonRecord { function isBuiltInWebSearchTool(tool: unknown): tool is JsonRecord { const toolRecord = toRecord(tool); const toolType = typeof toolRecord.type === "string" ? toolRecord.type : ""; - return WEB_SEARCH_TOOL_TYPES.has(toolType) && !toolRecord.function; + return WEB_SEARCH_TOOL_TYPES.test(toolType) && !toolRecord.function; } function isBuiltInWebSearchToolChoice(toolChoice: unknown): boolean { const choice = toRecord(toolChoice); const toolType = typeof choice.type === "string" ? choice.type : ""; - return WEB_SEARCH_TOOL_TYPES.has(toolType); + return WEB_SEARCH_TOOL_TYPES.test(toolType); } function buildFallbackDescription(tool: JsonRecord): string { diff --git a/tests/unit/web-search-9279-repro.test.ts b/tests/unit/web-search-9279-repro.test.ts new file mode 100644 index 0000000000..10ca5645c6 --- /dev/null +++ b/tests/unit/web-search-9279-repro.test.ts @@ -0,0 +1,81 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { prepareWebSearchFallbackBody, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } = + await import("../../open-sse/services/webSearchFallback.ts"); + +// #9279 — Anthropic's date-suffixed server-tool variant web_search_20250305 +// (sent by Claude Code 2.1.220+) is not intercepted by the web search fallback +// detector in webSearchFallback.ts:4, which uses an exact Set. +// Clasue -> OpenAI-compatible provider requests carry the raw Claude tool shape +// { type: "web_search_20250305", name: "web_search", max_uses: 8 }. +// The fallback must detect and intercept these too. + +test("#9279 versioned web_search_20250305 IS intercepted with interceptSearchOverride=true", () => { + const { body, fallback } = prepareWebSearchFallbackBody( + { + tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 8 }], + }, + { + provider: "opencode-go", + sourceFormat: "claude", + targetFormat: "openai", + nativeCodexPassthrough: false, + interceptSearchOverride: true, + } + ); + + assert.equal(fallback.enabled, true); + assert.equal( + fallback.toolName, + OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME + ); + assert.equal(fallback.convertedToolCount, 1); +}); + +test("#9279 versioned web_search_20250305 intercepted even without per-model override (claude->openai is not a native-bypass path)", () => { + // sourceFormat=claude, targetFormat=openai is NOT a native bypass path + // (supportsNativeWebSearchFallbackBypass returns false), so the fallback + // MUST fire without any interceptSearchOverride. + const { body, fallback } = prepareWebSearchFallbackBody( + { + tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 8 }], + }, + { + provider: "opencode-go", + sourceFormat: "claude", + targetFormat: "openai", + nativeCodexPassthrough: false, + // no interceptSearchOverride — must still be detected by tool type matching + } + ); + + assert.equal(fallback.enabled, true); + assert.equal( + fallback.toolName, + OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME + ); + assert.equal(fallback.convertedToolCount, 1); +}); + +test("#9279 tool_choice with web_search_20250305 redirects to omniroute_web_search", () => { + const { body, fallback } = prepareWebSearchFallbackBody( + { + tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 8 }], + tool_choice: { type: "web_search_20250305" }, + }, + { + provider: "opencode-go", + sourceFormat: "claude", + targetFormat: "openai", + nativeCodexPassthrough: false, + interceptSearchOverride: true, + } + ); + + assert.equal(fallback.enabled, true); + const choice = body.tool_choice as Record; + const fn = choice.function as Record | undefined; + assert.equal(fn?.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); + assert.equal(choice.type, "function"); +}); \ No newline at end of file