mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 12:52:25 +03:00
Merged. The repro in the description is exactly right — rewriting a native `web_search` tool into `omniroute_web_search` for a client that never declared it makes streaming Responses clients disconnect. Mirroring the existing Gemini bypass keeps the change to the one target that needs it, and the suite goes from 20 pass / 3 fail on base to 23 / 0 with the fix. Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thank you.
379 lines
13 KiB
TypeScript
379 lines
13 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
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
|
|
// nested Chat Completions shape ({ type, function: { name } }). On the Responses→Responses
|
|
// passthrough path nothing flattens it, so a nested tool reaches the upstream as
|
|
// tools[0].function.name and is rejected with "Missing required parameter: 'tools[0].name'".
|
|
|
|
function makeBody() {
|
|
return {
|
|
model: "gpt-5.5",
|
|
messages: [{ role: "user", content: "search the web" }],
|
|
tools: [{ type: "web_search" }],
|
|
};
|
|
}
|
|
|
|
test("#2390 web_search fallback is FLAT for Responses API target", () => {
|
|
const { body, fallback } = prepareWebSearchFallbackBody(makeBody(), {
|
|
targetFormat: "openai-responses",
|
|
nativeCodexPassthrough: false,
|
|
});
|
|
|
|
assert.equal(fallback.enabled, true);
|
|
const injected = body.tools[0] as Record<string, unknown>;
|
|
assert.equal(injected.type, "function");
|
|
assert.equal(injected.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
|
|
assert.equal(
|
|
injected.function,
|
|
undefined,
|
|
"Responses API tool must not be nested under .function"
|
|
);
|
|
assert.ok(injected.parameters, "flat tool keeps top-level parameters");
|
|
});
|
|
|
|
test("#2390 web_search fallback stays NESTED for Chat Completions target", () => {
|
|
const { body, fallback } = prepareWebSearchFallbackBody(makeBody(), {
|
|
targetFormat: "openai",
|
|
nativeCodexPassthrough: false,
|
|
});
|
|
|
|
assert.equal(fallback.enabled, true);
|
|
const injected = body.tools[0] as Record<string, unknown>;
|
|
assert.equal(injected.type, "function");
|
|
const fn = injected.function as Record<string, unknown> | undefined;
|
|
assert.ok(fn, "Chat Completions tool must be nested under .function");
|
|
assert.equal(fn?.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
|
|
assert.equal(
|
|
injected.name,
|
|
undefined,
|
|
"Chat Completions tool must not expose a flat top-level name"
|
|
);
|
|
});
|
|
|
|
test("#2390 tool_choice matches the injected tool shape per target format", () => {
|
|
const responses = prepareWebSearchFallbackBody(
|
|
{ ...makeBody(), tool_choice: { type: "web_search" } },
|
|
{ targetFormat: "openai-responses", nativeCodexPassthrough: false }
|
|
);
|
|
const rChoice = responses.body.tool_choice as Record<string, unknown>;
|
|
assert.equal(rChoice.name, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
|
|
assert.equal(rChoice.function, undefined);
|
|
|
|
const chat = prepareWebSearchFallbackBody(
|
|
{ ...makeBody(), tool_choice: { type: "web_search" } },
|
|
{ targetFormat: "openai", nativeCodexPassthrough: false }
|
|
);
|
|
const cChoice = chat.body.tool_choice as Record<string, unknown>;
|
|
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 when native Responses passthrough flag is set (#8964 xAI)", () => {
|
|
// Callers OR codex|xai into nativeCodexPassthrough (existing flag = "any native lane").
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "xai-oauth",
|
|
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
|
|
);
|
|
});
|
|
|
|
// #13447: Antigravity executes through the Gemini lane — its executor sanitizes
|
|
// request.tools via buildGeminiTools(), which maps native `web_search` to
|
|
// `{ googleSearch: {} }`. Rewriting web_search to omniroute_web_search breaks
|
|
// streaming Responses clients (they reject the undeclared tool), so Antigravity
|
|
// must take the same native bypass as Gemini.
|
|
test("bypass predicate: true for Antigravity target (#13447)", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "antigravity",
|
|
sourceFormat: "openai-responses",
|
|
targetFormat: "antigravity",
|
|
nativeCodexPassthrough: false,
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("bypass predicate: true for Antigravity target even without provider name (#13447)", () => {
|
|
// The targetFormat check alone must suffice: some call paths resolve the
|
|
// format without carrying the provider string.
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: null,
|
|
sourceFormat: "openai-responses",
|
|
targetFormat: "antigravity",
|
|
nativeCodexPassthrough: false,
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("bypass predicate: true for Claude -> Claude passthrough", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "claude",
|
|
sourceFormat: "claude",
|
|
targetFormat: "claude",
|
|
nativeCodexPassthrough: false,
|
|
}),
|
|
true
|
|
);
|
|
});
|
|
|
|
// #4481: MiniMax's Anthropic-compatible endpoint claims Claude format but does NOT
|
|
// implement Anthropic's typed server tools, so forwarding web_search_20250305 untouched
|
|
// (the Claude->Claude bypass) makes api.minimax.io return HTTP 400 "invalid params,
|
|
// function name or parameters is empty (2013)". For such providers we must NOT bypass —
|
|
// the tool has to be converted to the omniroute_web_search function fallback (which the
|
|
// model accepts as a normal function tool).
|
|
test("bypass predicate: false for Claude -> Claude when provider lacks Anthropic server tools (minimax, #4481)", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "minimax",
|
|
sourceFormat: "claude",
|
|
targetFormat: "claude",
|
|
nativeCodexPassthrough: false,
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("bypass predicate: still true for Claude -> Claude on a real Claude provider (regression guard, #4481)", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "anthropic",
|
|
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("Antigravity target: native web_search forwarded untouched (#13447)", () => {
|
|
// End-to-end proof of the issue: on the Responses → Antigravity path the
|
|
// client's declared `web_search` tool must reach the translator verbatim so
|
|
// it can be grounded natively — not rewritten to omniroute_web_search, which
|
|
// streaming Responses clients reject as an undeclared tool.
|
|
const inputBody = { tools: [{ type: "web_search" }] };
|
|
const { body, fallback } = prepareWebSearchFallbackBody(inputBody, {
|
|
provider: "antigravity",
|
|
sourceFormat: "openai-responses",
|
|
targetFormat: "antigravity",
|
|
nativeCodexPassthrough: false,
|
|
});
|
|
|
|
assert.equal(fallback.enabled, false);
|
|
assert.equal(fallback.toolName, null);
|
|
assert.equal(fallback.convertedToolCount, 0);
|
|
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));
|
|
});
|
|
|
|
// ── #3384: per-model interceptSearch override wins over every native-bypass default ──
|
|
|
|
test("#3384 interceptSearchOverride=true forces interception even on the Claude->Claude bypass path", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "claude",
|
|
sourceFormat: "claude",
|
|
targetFormat: "claude",
|
|
nativeCodexPassthrough: false,
|
|
interceptSearchOverride: true,
|
|
}),
|
|
false,
|
|
"explicit interceptSearch:true must NOT bypass, overriding the native Claude passthrough"
|
|
);
|
|
});
|
|
|
|
test("#3384 interceptSearchOverride=false forces native passthrough even for a standard provider", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "openai",
|
|
sourceFormat: "openai",
|
|
targetFormat: "openai",
|
|
nativeCodexPassthrough: false,
|
|
interceptSearchOverride: false,
|
|
}),
|
|
true,
|
|
"explicit interceptSearch:false must bypass even though OpenAI->OpenAI has no native default bypass"
|
|
);
|
|
});
|
|
|
|
test("#3384 interceptSearchOverride=undefined falls through to the existing native-bypass defaults", () => {
|
|
assert.equal(
|
|
supportsNativeWebSearchFallbackBypass({
|
|
provider: "claude",
|
|
sourceFormat: "claude",
|
|
targetFormat: "claude",
|
|
nativeCodexPassthrough: false,
|
|
interceptSearchOverride: undefined,
|
|
}),
|
|
true,
|
|
"no override configured — default Claude->Claude bypass still applies"
|
|
);
|
|
});
|
|
|
|
test("#3384 end-to-end: interceptSearchOverride=true converts the tool on the Claude->Claude bypass path", () => {
|
|
const inputBody = { tools: [{ type: "web_search" }] };
|
|
const { fallback } = prepareWebSearchFallbackBody(inputBody, {
|
|
provider: "claude",
|
|
sourceFormat: "claude",
|
|
targetFormat: "claude",
|
|
nativeCodexPassthrough: false,
|
|
interceptSearchOverride: true,
|
|
});
|
|
|
|
assert.equal(fallback.enabled, true);
|
|
assert.equal(fallback.toolName, OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME);
|
|
});
|