Skip to content

fix(translator): sanitize tool names for Gemini function_declarations compatibility#2268

Merged
luispater merged 3 commits intorouter-for-me:devfrom
sususu98:fix/sanitize-tool-names
Mar 23, 2026
Merged

fix(translator): sanitize tool names for Gemini function_declarations compatibility#2268
luispater merged 3 commits intorouter-for-me:devfrom
sususu98:fix/sanitize-tool-names

Conversation

@sususu98
Copy link
Copy Markdown
Collaborator

Summary

Claude Code and MCP clients may send tool definitions with names containing characters invalid for Gemini's function_declarations format (e.g. /, @, #, spaces). The proxy currently passes these names through to upstream without sanitization, causing 400 INVALID_ARGUMENT errors from Gemini APIs.

This PR sanitizes tool names on request via the existing SanitizeFunctionName utility and restores original names on response, applied to both antigravity/claude and gemini-cli/claude translators.

Error Logs

Gemini rejects the request with:

{
    "error": {
        "code": 400,
        "message": "The GenerateContentRequest proto is invalid:\n  * tools[0].function_declarations[0].name: [FIELD_INVALID] Invalid function name. Must start with a letter or an underscore. Must be a-z, A-Z, 0-9, or contain underscores (_), dots (.), colons (:), or dashes (-), with a maximum length of 64.",
        "status": "INVALID_ARGUMENT"
    }
}

Claude models via Antigravity also reject names with /:

{
    "error": {
        "code": 400,
        "message": "{\"type\":\"error\",\"error\":{\"type\":\"invalid_request_error\",\"message\":\"tools.0.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$'\"},\"request_id\":\"req_vrtx_...\"}",
        "status": "INVALID_ARGUMENT"
    }
}

Common trigger: Claude Code with MCP tools sending names like mcp/server/read_file, tool@v2, etc.

Changes

File Change
internal/util/translator.go Add SanitizedToolNameMap() and RestoreSanitizedToolName() — build sanitized→original reverse map and lookup
internal/translator/antigravity/claude/antigravity_claude_request.go Apply SanitizeFunctionName() at 4 points: tool declarations, functionCall.name, functionResponse.name, allowedFunctionNames
internal/translator/antigravity/claude/antigravity_claude_response.go Add ToolNameMap to Params; restore original names in streaming and non-streaming paths
internal/translator/gemini-cli/claude/gemini-cli_claude_request.go Same 4-point sanitization as antigravity
internal/translator/gemini-cli/claude/gemini-cli_claude_response.go Same reverse mapping as antigravity
internal/util/sanitize_test.go Add tests for SanitizedToolNameMap and RestoreSanitizedToolName

Data Flow

Client → [original name] → Request Translator → [SanitizeFunctionName] → Upstream API
Upstream API → [sanitized name] → Response Translator → [RestoreSanitizedToolName] → Client [original name]

Design Notes

  • SanitizeFunctionName is a no-op for already-valid names (e.g. Read, Bash, get_weather) — zero overhead for common cases
  • SanitizedToolNameMap returns nil when no sanitization is needed — response path skips map lookup entirely
  • The existing SanitizeFunctionName in internal/util/util.go was already defined but never used in translators

Testing

  • A/B tested against live Gemini and Claude endpoints
  • All existing translator tests pass
  • New unit tests for SanitizedToolNameMap and RestoreSanitizedToolName

… compatibility

Claude Code and MCP clients may send tool names containing characters
invalid for Gemini's function_declarations (e.g. '/', '@', spaces).
Sanitize on request via SanitizeFunctionName and restore original names
on response for both antigravity/claude and gemini-cli/claude translators.
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a mechanism to sanitize tool names in requests to ensure compatibility with Gemini's function_declarations format. It prevents INVALID_ARGUMENT errors caused by invalid characters in tool names, while preserving the original tool names for Claude Code and MCP clients.

Highlights

  • Tool Name Sanitization: This PR addresses an issue where Claude Code and MCP clients send tool definitions with names containing invalid characters for Gemini's function_declarations format, causing errors.
  • Sanitization Implementation: The solution involves sanitizing tool names on the request side using the existing SanitizeFunctionName utility and restoring the original names on the response side.
  • Affected Translators: The changes are applied to both antigravity/claude and gemini-cli/claude translators to ensure compatibility across different clients.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses a critical compatibility issue by sanitizing tool names for Gemini API requests and restoring them in responses. The implementation is well-structured and consistently applied across both the antigravity and gemini-cli translators. I've identified a potential issue with how sanitized name collisions are handled, an opportunity to improve test coverage for this edge case, and an inefficiency in the streaming response handler for antigravity/claude. My detailed comments provide suggestions to address these points.

Comment on lines +299 to +301
if _, exists := out[sanitized]; !exists {
out[sanitized] = name
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This logic silently ignores sanitized tool name collisions. If two different tool names (e.g., tool/a and tool@a) sanitize to the same value (tool_a), only the first one encountered will be stored in the reverse map. This will lead to incorrect tool name restoration for any subsequent tools that collide. While the upstream API would likely reject the request due to duplicate function names, it would be better to handle this collision explicitly, for example by logging a warning to aid in debugging.

Comment on lines +84 to +86
if params.ToolNameMap == nil {
params.ToolNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This lazy initialization can be inefficient. SanitizedToolNameMap returns nil if no tool names need sanitization. In that case, params.ToolNameMap remains nil, causing SanitizedToolNameMap to be called and the request JSON to be parsed on every single streaming chunk. A better approach is to ensure this initialization logic runs only once. This can be achieved by adding a boolean flag (e.g., toolNameMapInitialized) to the Params struct and updating this logic accordingly.

Suggested change
if params.ToolNameMap == nil {
params.ToolNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
}
if !params.toolNameMapInitialized {
params.ToolNameMap = util.SanitizedToolNameMap(originalRequestRawJSON)
params.toolNameMapInitialized = true
}

}
}

func TestSanitizedToolNameMap(t *testing.T) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It would be beneficial to add a test case to TestSanitizedToolNameMap that covers the scenario of a sanitized tool name collision. For example, providing two different tool names in the input JSON that produce the same sanitized name (e.g., mcp/server/read and mcp-server-read). This would help verify and document the behavior for this edge case, where the current implementation silently keeps the first mapping.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2398ebad55

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +299 to +300
if _, exists := out[sanitized]; !exists {
out[sanitized] = name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle collisions in sanitized tool-name map

In SanitizedToolNameMap, a sanitized key is stored only once (if _, exists := out[sanitized]; !exists), so colliding names are silently merged. I checked both updated response paths (antigravity and gemini-cli), and they now rely on this map via RestoreSanitizedToolName; with tools like read_file and read/file (or any two names that sanitize to the same token), a provider functionCall.name of read_file can be restored to the wrong original tool name, causing the client to execute a different tool than the model intended. This needs explicit collision handling (rejecting or disambiguating) instead of first-write wins.

Useful? React with 👍 / 👎.

@sususu98
Copy link
Copy Markdown
Collaborator Author

Remaining translators that need the same fix

This PR covers antigravity/claude and gemini-cli/claude — the two Claude-format input translators that output to Gemini APIs. There are 5 additional translators that also write functionDeclarations to Gemini-compatible APIs and may encounter the same FIELD_INVALID error:

# Translator Input Format → Output
1 internal/translator/antigravity/openai/chat-completions/ OpenAI → Antigravity
2 internal/translator/gemini-cli/openai/chat-completions/ OpenAI → GeminiCLI
3 internal/translator/gemini/claude/ Claude → Gemini (direct)
4 internal/translator/gemini/openai/chat-completions/ OpenAI → Gemini (direct)
5 internal/translator/gemini/openai/responses/ OpenAI Responses → Gemini

Each needs the same pattern: SanitizeFunctionName() on request + RestoreSanitizedToolName() on response. Happy to follow up with a separate PR if desired.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 755ca75879

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


out := make(map[string]string)
tools.ForEach(func(_, tool gjson.Result) bool {
name := strings.TrimSpace(tool.Get("name").String())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep tool names untrimmed in sanitized-name reverse map

SanitizedToolNameMap trims each tool name before generating the reverse lookup key, but both updated request translators sanitize the raw (untrimmed) name when emitting functionDeclaration.name/functionCall.name. For names with leading or trailing whitespace (e.g. " foo"), upstream sees "_foo" while this map is computed from "foo" and often omits the entry entirely, so RestoreSanitizedToolName cannot restore the original client tool name and the client may fail to dispatch the returned tool call.

Useful? React with 👍 / 👎.

…slators

Apply SanitizeFunctionName on request and RestoreSanitizedToolName on
response for: gemini/claude, gemini/openai/chat-completions,
gemini/openai/responses, antigravity/openai/chat-completions,
gemini-cli/openai/chat-completions.

Also update SanitizedToolNameMap to handle OpenAI format
(tools[].function.name) in addition to Claude format (tools[].name).
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8bb350467

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +295 to +297
name := strings.TrimSpace(tool.Get("name").String())
if name == "" {
return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include OpenAI function names in sanitized reverse map

SanitizedToolNameMap only reads tools[].name and skips entries when that field is empty, but Chat Completions tools carry the name at tools[].function.name (see the request translators iterating t.Get("function") before sanitizing). As a result, the new OpenAI response paths that call RestoreSanitizedToolName (e.g. ConvertGeminiResponseToOpenAI and ConvertAntigravityResponseToOpenAI) get a nil/empty map for OpenAI requests and emit sanitized tool names back to clients; when the original tool was something like mcp/server/read, clients that dispatch by the declared name cannot find a matching tool.

Useful? React with 👍 / 👎.

@luispater luispater merged commit 5d6cdcc into router-for-me:dev Mar 23, 2026
1 of 2 checks passed
Adamcf123 pushed a commit to Adamcf123/CLIProxyAPI that referenced this pull request Mar 24, 2026
…-names

fix(translator): sanitize tool names for Gemini function_declarations compatibility
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants