router-for-me / router-for-me/CLIProxyAPI
[Feature/Fix] Sanitize Claude tool names and provide fallback input_schema for parameterless/custom tools
Open
@sususu98 is already working on this.
Since Aug 19, 2026.
- Dominant language
- Go
- Stars
- 52.5k
- Forks
- 7.9k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 60
Description
Problem Description
When using OpenAI-compatible clients (such as Model Context Protocol / MCP clients or mobile agents) with Claude and Antigravity models, two critical errors occur during tool calling:
-
Claude Tool Name Pattern Rejection (
^[a-zA-Z0-9_-]{1,128}$):- Anthropic upstream strictly requires tool names matching
^[a-zA-Z0-9_-]{1,128}$. - MCP tools frequently use dotted, colon-separated, or slash-separated namespaces (e.g.
mcp.server.special:get_timeorserver/action), which triggers an upstream400 invalid_request_error: tools.X.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$'.
- Anthropic upstream strictly requires tool names matching
-
Missing
input_schemaon Parameterless / Custom Tools:- Under Antigravity / Claude
VALIDATEDmode, function declarations missing an explicitparametersorinput_schemaobject are rejected with400 invalid_request_error: tools.X.custom.input_schema: Field required. - Some OpenAI-compatible clients emit tool definitions without explicit
parametersor wrapped incustomstructures.
- Under Antigravity / Claude
Proposed Implementation
We prepared a complete fix with unit tests in PR #4999 (as per AGENTS.md and pr-path-guard policy):
internal/util/util.go: AddSanitizeClaudeFunctionNamematching Anthropic's^[a-zA-Z0-9_-]{1,128}$pattern without breaking Gemini'sSanitizeFunctionName(which preserves dots/colons).internal/translator/claude/openai/chat-completions/claude_openai_request.go: ApplySanitizeClaudeFunctionNameto tool names and ensure a default empty schema{"type":"object","properties":{}}whenparametersis absent.internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go: Broaden tool parsing to supportcustomtool descriptors and inject fallback emptyparametersJsonSchema.internal/util/sanitize_test.go: AddedTestSanitizeClaudeFunctionName.
Code Diff / Implementation
1. internal/util/util.go
var claudeFunctionNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
// SanitizeClaudeFunctionName ensures a function name matches Anthropic Claude requirements: ^[a-zA-Z0-9_-]{1,128}$
func SanitizeClaudeFunctionName(name string) string {
if name == "" {
return ""
}
sanitized := claudeFunctionNameSanitizer.ReplaceAllString(name, "_")
if len(sanitized) > 0 {
first := sanitized[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') {
if len(sanitized) >= 64 {
sanitized = sanitized[:63]
}
sanitized = "_" + sanitized
}
} else {
sanitized = "_"
}
if len(sanitized) > 64 {
sanitized = sanitized[:64]
}
return sanitized
}
2. internal/translator/claude/openai/chat-completions/claude_openai_request.go
// Tools mapping: OpenAI tools -> Claude Code tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
var anthropicTools [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if tool.Get("type").String() == "function" || tool.Get("type").String() == "custom" {
function := tool.Get("function")
if !function.Exists() || !function.IsObject() {
if c := tool.Get("custom"); c.Exists() && c.IsObject() {
function = c
} else {
function = tool
}
}
toolName := function.Get("name").String()
if toolName == "" {
toolName = tool.Get("name").String()
}
sanitizedName := util.SanitizeClaudeFunctionName(toolName)
anthropicTool := []byte(`{"name":"","description":"","input_schema":{"type":"object","properties":{}}}`)
anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", sanitizedName)
anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", function.Get("description").String())
// Convert parameters schema for the tool
if parameters := function.Get("parameters"); parameters.Exists() {
anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", util.NormalizeClaudeToolInputSchema([]byte(parameters.Raw)))
} else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() {
anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", util.NormalizeClaudeToolInputSchema([]byte(parameters.Raw)))
}
anthropicTool = common.AttachCacheControl(anthropicTool, tool)
if !gjson.GetBytes(anthropicTool, "cache_control").Exists() {
anthropicTool = common.AttachCacheControl(anthropicTool, function)
}
anthropicTools = append(anthropicTools, anthropicTool)
}
return true
})
Verification
- Tested locally on live
claude-sonnet-4-6andgemini-3.7-flashendpoints with complex MCP tools (mcp.server.special:get_time). - All unit tests in
internal/util/...andinternal/translator/...pass cleanly.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.