From a7ebf0c2a0b8e4446dd0367a113217860d2fda9a Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 21 Jun 2026 08:12:07 -0300 Subject: [PATCH] fix(sse): don't bypass web-search fallback for Anthropic-compatible providers without server tools (#4481) (#4490) --- CHANGELOG.md | 1 + open-sse/services/webSearchFallback.ts | 15 +++++++++- tests/unit/web-search-fallback-format.test.ts | 30 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0881e6d2ab..dd05e25f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - **fix(translator): inject a placeholder message when the Responses API `input[]` is empty** — a `POST /v1/responses` with `input: []` translated to `messages: []`, which every upstream Chat-Completions provider rejects (surfaced as a confusing 406); a single placeholder user message is now injected, mirroring the existing empty-string handling. ([#4393](https://github.com/diegosouzapw/OmniRoute/pull/4393) — thanks @diegosouzapw) - **fix(providers): serve the api.airforce live `/models` catalog instead of the stale seed** — the api.airforce provider listed a stale hard-coded seed; it now serves the upstream live `/models` catalog. ([#4395](https://github.com/diegosouzapw/OmniRoute/pull/4395) — thanks @diegosouzapw) - **fix(cli): non-interactive-safe prompts + `context` alias** — the CLI's `confirm()`/prompt helpers no longer hang in non-interactive (piped/CI) contexts, and a singular `context` alias is accepted alongside `contexts`; the contexts workflow is documented. ([#4439](https://github.com/diegosouzapw/OmniRoute/pull/4439), [#4397](https://github.com/diegosouzapw/OmniRoute/pull/4397) — thanks @diegosouzapw) +- **fix(sse): `web_search_20250305` no longer 400s on MiniMax's Anthropic-compatible endpoint** — PR #2960 added a Claude→Claude bypass that forwards Anthropic's typed server tool `web_search_20250305` untouched, assuming the Claude-format upstream implements Anthropic server tools. MiniMax's `/anthropic` endpoint does not, so `claude → minimax` requests carrying that tool got `HTTP 400 "invalid params, function name or parameters is empty (2013)"`. `supportsNativeWebSearchFallbackBypass` now consults the (already-plumbed) `provider` and excludes providers known not to implement server tools (currently `minimax`) from the bypass, so the built-in web-search tool is converted to the `omniroute_web_search` function fallback — which MiniMax accepts as a normal function tool. ([#4481](https://github.com/diegosouzapw/OmniRoute/issues/4481) — thanks @shafqatevo) ### 🔒 Security diff --git a/open-sse/services/webSearchFallback.ts b/open-sse/services/webSearchFallback.ts index 61fe2355c3..f82b565630 100644 --- a/open-sse/services/webSearchFallback.ts +++ b/open-sse/services/webSearchFallback.ts @@ -129,7 +129,16 @@ function buildFallbackTool(tool: JsonRecord, targetFormat?: string | null): Json }; } +// Providers whose endpoint advertises Claude/Anthropic format but does NOT implement +// Anthropic's typed server tools (web_search_20250305, …). For these the Claude -> Claude +// bypass below must NOT apply: forwarding the native server tool makes the upstream 400 +// (MiniMax returns `invalid params, function name or parameters is empty (2013)`), so the +// built-in web-search tool has to be converted to the omniroute_web_search function +// fallback — which these models accept as a normal function tool (#4481). +const CLAUDE_FORMAT_PROVIDERS_WITHOUT_SERVER_TOOLS = new Set(["minimax"]); + export function supportsNativeWebSearchFallbackBypass({ + provider, sourceFormat, targetFormat, nativeCodexPassthrough, @@ -147,7 +156,11 @@ export function supportsNativeWebSearchFallbackBypass({ // 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; + if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) { + // …except Anthropic-compatible providers that don't actually implement server tools. + if (provider && CLAUDE_FORMAT_PROVIDERS_WITHOUT_SERVER_TOOLS.has(provider)) return false; + return true; + } return false; } diff --git a/tests/unit/web-search-fallback-format.test.ts b/tests/unit/web-search-fallback-format.test.ts index 8941eb7a2f..a7e038c855 100644 --- a/tests/unit/web-search-fallback-format.test.ts +++ b/tests/unit/web-search-fallback-format.test.ts @@ -114,6 +114,36 @@ test("bypass predicate: true for Claude -> Claude passthrough", () => { ); }); +// #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({