fix(providers): use prefix regex for web search fallback detector to catch versioned tool types (#9279)

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-07 18:08:44 -03:00
committed by GitHub
parent 2e71558a0f
commit 3be585ef41
3 changed files with 88 additions and 3 deletions

View File

@@ -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))

View File

@@ -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<string, number> = {
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 {

View File

@@ -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<string, unknown>;
const fn = choice.function as Record<string, unknown> | undefined;
assert.equal(fn?.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
assert.equal(choice.type, "function");
});