diff --git a/open-sse/services/webSearchFallback.ts b/open-sse/services/webSearchFallback.ts index 62f3f06ca5..61fe2355c3 100644 --- a/open-sse/services/webSearchFallback.ts +++ b/open-sse/services/webSearchFallback.ts @@ -130,6 +130,7 @@ function buildFallbackTool(tool: JsonRecord, targetFormat?: string | null): Json } export function supportsNativeWebSearchFallbackBypass({ + sourceFormat, targetFormat, nativeCodexPassthrough, }: { @@ -138,8 +139,16 @@ export function supportsNativeWebSearchFallbackBypass({ targetFormat: string | null | undefined; nativeCodexPassthrough: boolean; }): boolean { + // Native Codex (OpenAI Responses) passthrough: the upstream runs web search itself. if (nativeCodexPassthrough) return true; - return targetFormat === FORMATS.GEMINI; + // Gemini target: the Gemini translator maps built-in web search to googleSearch natively. + if (targetFormat === FORMATS.GEMINI) return true; + // Claude -> Claude passthrough: the Anthropic Messages upstream (e.g. a Claude + // subscription driven by Claude Code) natively runs web_search_20250305. Forward the + // native tool untouched instead of rewriting it to omniroute_web_search. Mirrors the + // Codex/Gemini bypasses so every native-web-search provider is treated symmetrically. + if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) return true; + return false; } export function prepareWebSearchFallbackBody( diff --git a/tests/unit/web-search-fallback-format.test.ts b/tests/unit/web-search-fallback-format.test.ts index 605aa1f7dd..8941eb7a2f 100644 --- a/tests/unit/web-search-fallback-format.test.ts +++ b/tests/unit/web-search-fallback-format.test.ts @@ -1,8 +1,11 @@ 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"); +const { + prepareWebSearchFallbackBody, + supportsNativeWebSearchFallbackBypass, + OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME, +} = await import("../../open-sse/services/webSearchFallback.ts"); // Regression for #2390: when the target is a Responses-API provider, the injected // omniroute_web_search tool must use the FLAT function shape ({ type, name }), not the @@ -72,3 +75,153 @@ test("#2390 tool_choice matches the injected tool shape per target format", () = const cFn = cChoice.function as Record | undefined; assert.equal(cFn?.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); }); + +// ── Native web-search bypass: predicate coverage for every native path ── + +test("bypass predicate: true for native Codex passthrough", () => { + assert.equal( + supportsNativeWebSearchFallbackBypass({ + provider: "openai", + sourceFormat: "openai-responses", + targetFormat: "openai-responses", + nativeCodexPassthrough: true, + }), + true + ); +}); + +test("bypass predicate: true for Gemini target", () => { + assert.equal( + supportsNativeWebSearchFallbackBypass({ + provider: "gemini", + sourceFormat: "openai", + targetFormat: "gemini", + nativeCodexPassthrough: false, + }), + true + ); +}); + +test("bypass predicate: true for Claude -> Claude passthrough", () => { + assert.equal( + supportsNativeWebSearchFallbackBypass({ + provider: "claude", + sourceFormat: "claude", + targetFormat: "claude", + nativeCodexPassthrough: false, + }), + true + ); +}); + +test("bypass predicate: false for standard OpenAI -> OpenAI", () => { + assert.equal( + supportsNativeWebSearchFallbackBypass({ + provider: "openai", + sourceFormat: "openai", + targetFormat: "openai", + nativeCodexPassthrough: false, + }), + false + ); +}); + +test("bypass predicate: false when only the target is Claude (non-native tool must convert)", () => { + // An OpenAI-format client hitting a Claude target sends an OpenAI-shaped web_search + // tool that is NOT native Anthropic format, so it must still be converted. Only the + // Claude -> Claude passthrough (native body) is bypassed. + assert.equal( + supportsNativeWebSearchFallbackBypass({ + provider: "claude", + sourceFormat: "openai", + targetFormat: "claude", + nativeCodexPassthrough: false, + }), + false + ); +}); + +test("bypass predicate: false when only the source is Claude", () => { + assert.equal( + supportsNativeWebSearchFallbackBypass({ + provider: "openai", + sourceFormat: "claude", + targetFormat: "openai", + nativeCodexPassthrough: false, + }), + false + ); +}); + +// ── Native web-search bypass: end-to-end body behavior ── + +test("Claude -> Claude: native web_search_20250305 forwarded untouched", () => { + const inputBody = { + tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }], + }; + const { body, fallback } = prepareWebSearchFallbackBody(inputBody, { + provider: "claude", + sourceFormat: "claude", + targetFormat: "claude", + nativeCodexPassthrough: false, + }); + + assert.equal(fallback.enabled, false); + assert.equal(fallback.toolName, null); + assert.equal(fallback.convertedToolCount, 0); + // Body forwarded verbatim — the native tool reaches the Anthropic upstream as-is. + assert.deepEqual(body, inputBody); +}); + +test("Claude -> Claude: bare web_search type also forwarded untouched", () => { + // Even the bare (unversioned) web_search type is forwarded on the Claude passthrough, + // because the Anthropic upstream owns web search. This is the explicit protection that + // no longer depends on the versioned type being absent from the matcher set. + const inputBody = { tools: [{ type: "web_search" }] }; + const { body, fallback } = prepareWebSearchFallbackBody(inputBody, { + provider: "claude", + sourceFormat: "claude", + targetFormat: "claude", + nativeCodexPassthrough: false, + }); + + assert.equal(fallback.enabled, false); + assert.equal(fallback.toolName, null); + assert.deepEqual(body, inputBody); +}); + +test("native Codex passthrough: built-in web_search_preview forwarded untouched", () => { + // Symmetric end-to-end coverage for the Codex bypass. + const inputBody = { tools: [{ type: "web_search_preview" }] }; + const { body, fallback } = prepareWebSearchFallbackBody(inputBody, { + provider: "openai", + sourceFormat: "openai-responses", + targetFormat: "openai-responses", + nativeCodexPassthrough: true, + }); + + assert.equal(fallback.enabled, false); + assert.equal(fallback.toolName, null); + assert.deepEqual(body, inputBody); +}); + +test("OpenAI -> Claude (non-passthrough): built-in web_search IS still converted", () => { + // Regression guard: the new Claude bypass must NOT swallow the conversion path for a + // non-native (OpenAI-format) client that merely targets a Claude provider. + const inputBody = { + tools: [{ type: "web_search", search_context_size: "low" }], + }; + const { body, fallback } = prepareWebSearchFallbackBody(inputBody, { + provider: "claude", + sourceFormat: "openai", + targetFormat: "claude", + nativeCodexPassthrough: false, + }); + + assert.equal(fallback.enabled, true); + assert.equal(fallback.toolName, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME); + assert.equal(fallback.convertedToolCount, 1); + const tools = (body.tools as Record[]) || []; + const toolNames = tools.map((t) => (t.function ? t.function.name : t.name)); + assert.ok(toolNames.includes(OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME)); +});