fix(translator): filter nameless hosted tools when converting Responses API to Chat format (#222)

Codex CLI sends "hosted" tools (e.g. `request_user_input`) via the OpenAI
Responses API. These tools have no explicit `name` field. The previous
`body.tools.map()` pass propagated `name: undefined` into the resulting
Chat Completions function declarations, which then became anonymous
`functionDeclarations` after the OpenAI→Gemini translation step.

Gemini strictly requires every function declaration to have a valid name
and rejects the entire request with:

  GenerateContentRequest.tools[0].function_declarations[4].name:
  Invalid function name. Must start with a letter or an underscore.

Fix: filter out any Responses API tool that lacks a non-empty `name`
string before converting to `{ type: "function", function: { name, ... } }`.
Named function tools are unaffected; only unnamed hosted tools are skipped.

Fixes: Gemini 400 error when Codex CLI is routed through 9router.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Владимир Акимов
2026-03-01 11:48:43 +03:00
committed by GitHub
parent f763d4ffed
commit 7076108550

View File

@@ -112,20 +112,31 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
}
}
// Convert tools format
// Convert tools format.
// Responses API supports "hosted" tools (e.g. { type: "request_user_input" }) that carry no
// explicit `name` field and cannot be represented as Chat Completions function declarations.
// Filter them out to avoid sending nameless functionDeclarations to downstream providers
// such as Gemini, which strictly validates function names.
if (body.tools && Array.isArray(body.tools)) {
result.tools = body.tools.map(tool => {
if (tool.function) return tool;
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
strict: tool.strict
}
};
});
result.tools = body.tools
.map(tool => {
// Already in Chat Completions format: { type: "function", function: { name, ... } }
if (tool.function) return tool;
// Responses API function tool: { type: "function", name, description, parameters }
// Only convert when a non-empty name is present; skip hosted tools without one.
const name = tool.name;
if (!name || typeof name !== "string" || name.trim() === "") return null;
return {
type: "function",
function: {
name,
description: tool.description,
parameters: tool.parameters,
strict: tool.strict
}
};
})
.filter(Boolean);
}
// Cleanup Responses API specific fields