mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(sse): bypass web-search fallback on Claude -> Claude passthrough (#2960)
Integrated into release/v3.8.8
This commit is contained in:
@@ -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<T extends JsonRecord>(
|
||||
|
||||
@@ -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<string, unknown> | 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<string, any>[]) || [];
|
||||
const toolNames = tools.map((t) => (t.function ? t.function.name : t.name));
|
||||
assert.ok(toolNames.includes(OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user