router-for-me / router-for-me/CLIProxyAPI

[Feature/Fix] Sanitize Claude tool names and provide fallback input_schema for parameterless/custom tools

Open
#5,000 2 comments 0 reactions 1 assignee View on GitHub

@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:

  1. 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_time or server/action), which triggers an upstream 400 invalid_request_error: tools.X.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$'.
  2. Missing input_schema on Parameterless / Custom Tools:

    • Under Antigravity / Claude VALIDATED mode, function declarations missing an explicit parameters or input_schema object are rejected with 400 invalid_request_error: tools.X.custom.input_schema: Field required.
    • Some OpenAI-compatible clients emit tool definitions without explicit parameters or wrapped in custom structures.

Proposed Implementation

We prepared a complete fix with unit tests in PR #4999 (as per AGENTS.md and pr-path-guard policy):

  1. internal/util/util.go: Add SanitizeClaudeFunctionName matching Anthropic's ^[a-zA-Z0-9_-]{1,128}$ pattern without breaking Gemini's SanitizeFunctionName (which preserves dots/colons).
  2. internal/translator/claude/openai/chat-completions/claude_openai_request.go: Apply SanitizeClaudeFunctionName to tool names and ensure a default empty schema {"type":"object","properties":{}} when parameters is absent.
  3. internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go: Broaden tool parsing to support custom tool descriptors and inject fallback empty parametersJsonSchema.
  4. internal/util/sanitize_test.go: Added TestSanitizeClaudeFunctionName.

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-6 and gemini-3.7-flash endpoints with complex MCP tools (mcp.server.special:get_time).
  • All unit tests in internal/util/... and internal/translator/... pass cleanly.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.