fix(translator): pass-through tool_search built-in tool type (#2766) (#2811)

Integrated into release/v3.8.6.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-05-29 01:00:54 -03:00
committed by GitHub
parent fee1c17d51
commit c216085e92
3 changed files with 85 additions and 20 deletions

View File

@@ -27,6 +27,7 @@
- **gemini:** translate signature-less Gemini thinking model tool calls to text parts to prevent `400 "missing thought_signature"` errors (#2801 — thanks @herjarsa)
- **translator:** strip `safety_identifier` from `/v1/responses` body before forwarding to Chat Completions upstream; fixes LobeHub-originated `400` errors (#2770)
- **warning-cleanup:** relax node engine constraint to `>=22.0.0` and clean dependencies (keeping `marked-terminal` to prevent TUI REPL crash) (#2792 — thanks @oyi77)
- **translator:** silently drop `tool_search` built-in tool type instead of returning 400 — newer Codex clients send `tool_search` as a Responses API built-in with no Chat Completions equivalent (#2766)
- **usage:** un-invert GitHub Copilot Free / limited plan quota — `limited_user_quotas` is the *remaining* count, not used, so the dashboard now shows 100% when the quota is untouched and 0% when fully exhausted (#2876 — thanks @androw)
- **fix(cli):** register openclaw in the CLI tool-detector so it appears in `omniroute status` alongside its existing API and config support ([#2833](https://github.com/diegosouzapw/OmniRoute/issues/2833))

View File

@@ -15,6 +15,9 @@ const COPILOT_REASONING_SUMMARY_MARKER = "_omnirouteCopilotReasoningSummary";
// Forward-compatible regex: matches web_search, web_search_20250305, and any future versioned names.
const WEB_SEARCH_TOOL_TYPES = /^web_search/;
// tool_search is a Responses API built-in sent by newer Codex clients; it has no Chat Completions
// equivalent and must be silently dropped (not rejected with 400).
const TOOL_SEARCH_TOOL_TYPES = /^tool_search/;
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
@@ -72,6 +75,8 @@ export function openaiResponsesToOpenAIRequest(
// Allow: function tools, tools already in Chat format (have .function property), CLI subagent tools,
// namespace tools (MCP tool groups used by Codex/OpenAI Responses API), and web_search server tools
// (Anthropic versioned: web_search_20250305, web_search_20250101, etc. — or plain web_search).
// tool_search is a Responses API built-in sent by newer Codex clients; silently skip it here
// (it will be filtered out during tools conversion below).
if (
toolType &&
toolType !== "function" &&
@@ -79,6 +84,7 @@ export function openaiResponsesToOpenAIRequest(
toolType !== "command" &&
toolType !== "namespace" &&
!WEB_SEARCH_TOOL_TYPES.test(toolType) &&
!TOOL_SEARCH_TOOL_TYPES.test(toolType) &&
!tool.function
) {
throw unsupportedFeature(
@@ -257,26 +263,33 @@ export function openaiResponsesToOpenAIRequest(
// Convert tools format
if (Array.isArray(root.tools)) {
result.tools = root.tools.map((toolValue) => {
const tool = toRecord(toolValue);
if (tool.function) return toolValue;
const toolType = toString(tool.type);
// Pass web_search server tools through with their original type (versioned or plain).
// These have no Chat Completions equivalent; preserve as-is so upstreams that understand
// Anthropic-style web_search_YYYYMMDD naming receive the exact name they expect.
if (WEB_SEARCH_TOOL_TYPES.test(toolType)) {
return toolValue;
}
return {
type: "function",
function: {
name: toString(tool.name),
description: toString(tool.description),
parameters: tool.parameters,
strict: tool.strict,
},
};
});
result.tools = root.tools
.filter((toolValue) => {
const tool = toRecord(toolValue);
const toolType = toString(tool.type);
// tool_search has no Chat Completions equivalent; drop it silently (issue #2766).
return !TOOL_SEARCH_TOOL_TYPES.test(toolType);
})
.map((toolValue) => {
const tool = toRecord(toolValue);
if (tool.function) return toolValue;
const toolType = toString(tool.type);
// Pass web_search server tools through with their original type (versioned or plain).
// These have no Chat Completions equivalent; preserve as-is so upstreams that understand
// Anthropic-style web_search_YYYYMMDD naming receive the exact name they expect.
if (WEB_SEARCH_TOOL_TYPES.test(toolType)) {
return toolValue;
}
return {
type: "function",
function: {
name: toString(tool.name),
description: toString(tool.description),
parameters: tool.parameters,
strict: tool.strict,
},
};
});
}
// Filter orphaned tool results (no matching tool_call in assistant messages)

View File

@@ -696,3 +696,54 @@ test("Responses -> Chat: unknown tool type still throws unsupported_feature (no
(error: any) => error.statusCode === 400 && error.errorType === "unsupported_feature"
);
});
// --- Issue #2766: tool_search built-in should be silently dropped ---
test("Responses -> Chat: tool_search does not throw (issue #2766)", () => {
// Codex newer clients send tool_search as a Responses API built-in.
// OmniRoute must not return 400 — it should silently drop the tool_search entry.
assert.doesNotThrow(() =>
openaiResponsesToOpenAIRequest(
"gpt-4o",
{
input: [{ role: "user", content: [{ type: "input_text", text: "search" }] }],
tools: [{ type: "tool_search", name: "search" }],
},
false,
null
)
);
});
test("Responses -> Chat: tool_search is stripped from output tools array (issue #2766)", () => {
// Codex clients send tool_search alongside function tools. tool_search has no
// Chat Completions equivalent and must be dropped; function tools must remain.
const result = openaiResponsesToOpenAIRequest(
"gpt-4o",
{
input: [{ role: "user", content: [{ type: "input_text", text: "hello" }] }],
tools: [
{ type: "tool_search", name: "search" },
{
type: "function",
name: "foo",
description: "A function",
parameters: { type: "object" },
},
],
},
false,
null
) as Record<string, unknown>;
const tools = result.tools as any[];
assert.ok(Array.isArray(tools), "tools array must be present");
assert.equal(
tools.some((t) => t.type === "tool_search"),
false,
"tool_search must be stripped from output"
);
assert.equal(tools.length, 1, "only the function tool must remain");
assert.equal(tools[0].type, "function");
assert.equal(tools[0].function.name, "foo");
});