Skip to content

fix(gemini-cli): sanitize tool schemas and filter empty parts#2108

Open
sususu98 wants to merge 1 commit intorouter-for-me:mainfrom
sususu98:fix/gemini-cli-tool-schema-and-empty-parts
Open

fix(gemini-cli): sanitize tool schemas and filter empty parts#2108
sususu98 wants to merge 1 commit intorouter-for-me:mainfrom
sususu98:fix/gemini-cli-tool-schema-and-empty-parts

Conversation

@sususu98
Copy link
Contributor

Summary

The gemini-cli translator is missing several protections that the antigravity translator already has, causing 400 INVALID_ARGUMENT errors from the Gemini API in two scenarios:

  1. Claude Code tool declarations contain unsupported fields — Fields like eager_input_streaming and JSON Schema keywords ($schema, anyOf, const, format, additionalProperties, etc.) are passed through to the Gemini API, which rejects them.

  2. Gemini-native clients send empty parts arrays — Clients like CherryStudio include {"role":"model","parts":[]} in conversation history, which Gemini API rejects with "required oneof field 'data' must have one initialized field".

Changes

Claude translator (gemini-cli_claude_request.go)

  • Add util.CleanJSONSchemaForGemini() call to sanitize tool input schemas before forwarding (removes $schema, anyOf, const, format, additionalProperties, propertyNames, x-*, etc.)
  • Add eager_input_streaming to the blacklist of deleted tool-level fields
  • Remove fragile bytes.Replace for "format":"uri" — now fully covered by CleanJSONSchemaForGemini()

Gemini native translator (gemini-cli_gemini_request.go)

  • Filter out content entries with empty or missing parts arrays before sending upstream

Both fixes align the gemini-cli translator with protections already present in the antigravity translator (see PR #1294 for the antigravity empty-parts fix).

Actual error logs

Error 1eager_input_streaming:

Unknown name "eager_input_streaming" at 'request.tools[0].function_declarations[0]': Cannot find field.

(repeated for all 13 tool declarations)

Error 2 — empty parts:

Request contains an invalid argument. (INVALID_ARGUMENT)

Test plan

  • go build ./... — compiles cleanly
  • go test ./... — all existing tests pass
  • Verified with actual Claude Code request that previously returned 400
  • Verified with CherryStudio request containing empty model parts

1. Claude translator: add CleanJSONSchemaForGemini() to sanitize tool
   input schemas (removes $schema, anyOf, const, format, etc.) and
   delete eager_input_streaming from tool declarations. Remove fragile
   bytes.Replace for format:"uri" now covered by schema cleaner.

2. Gemini native translator: filter out content entries with empty or
   missing parts arrays to prevent Gemini API 400 error "required
   oneof field 'data' must have one initialized field".

Both fixes align gemini-cli with protections already present in the
antigravity translator.
@gemini-code-assist
Copy link
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 enhances the gemini-cli translator by introducing robust error prevention mechanisms. It ensures that requests forwarded to the Gemini API conform to its strict requirements, specifically by cleaning up tool schemas from Claude requests and filtering out malformed content parts from Gemini-native requests. These changes align the gemini-cli translator with existing protections in other translators, improving overall API compatibility and reliability.

Highlights

  • Claude Tool Schema Sanitization: Implemented sanitization for Claude Code tool declarations, removing unsupported fields like eager_input_streaming and various JSON Schema keywords ($schema, anyOf, const, format, additionalProperties, propertyNames, x-*) to prevent 400 INVALID_ARGUMENT errors from the Gemini API.
  • Empty Parts Array Filtering: Added logic to filter out content entries with empty or missing parts arrays from Gemini-native client requests, addressing a specific Gemini API error related to uninitialized fields.

🧠 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.

Changelog
  • internal/translator/gemini-cli/claude/gemini-cli_claude_request.go
    • Removed the bytes package import.
    • Added the github.com/router-for-me/CLIProxyAPI/v6/internal/util package import.
    • Eliminated a bytes.Replace call previously used for format:uri sanitation, now handled by a new utility function.
    • Integrated util.CleanJSONSchemaForGemini() to sanitize tool input schemas.
    • Added deletion of the eager_input_streaming field from tool declarations.
  • internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go
    • Introduced new logic to iterate through request.contents and filter out any entries where the parts array is empty or missing, preventing API errors.
Activity
  • No human activity has been recorded on this pull request yet.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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
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 introduces two important fixes to the gemini-cli translator to prevent INVALID_ARGUMENT errors from the Gemini API. It sanitizes tool schemas by removing unsupported fields and filters out content with empty parts arrays. The changes are logical and address the described issues. I've provided a couple of suggestions to improve maintainability and performance in the implementation.

Comment on lines 154 to +159
tool, _ = sjson.Delete(tool, "strict")
tool, _ = sjson.Delete(tool, "input_examples")
tool, _ = sjson.Delete(tool, "type")
tool, _ = sjson.Delete(tool, "cache_control")
tool, _ = sjson.Delete(tool, "defer_loading")
tool, _ = sjson.Delete(tool, "eager_input_streaming")
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For better maintainability, you can refactor these repeated sjson.Delete calls into a loop over a slice of fields to be deleted. This makes it easier to add or remove fields from the blacklist in the future.

                fieldsToDelete := []string{
					"strict",
					"input_examples",
					"type",
					"cache_control",
					"defer_loading",
					"eager_input_streaming",
				}
				for _, field := range fieldsToDelete {
					tool, _ = sjson.Delete(tool, field)
				}

Comment on lines +116 to +129
filteredContents := "[]"
hasFiltered := false
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
parts := content.Get("parts")
if !parts.IsArray() || len(parts.Array()) == 0 {
hasFiltered = true
return true
}
filteredContents, _ = sjson.SetRaw(filteredContents, "-1", content.Raw)
return true
})
if hasFiltered {
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContents))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation of filtering contents with empty parts can be inefficient. Calling sjson.SetRaw inside the ForEach loop causes the filteredContents JSON string to be parsed on every iteration. A more performant approach would be to collect the raw content strings into a slice and then build the final JSON array string once at the end using strings.Join.

Suggested change
filteredContents := "[]"
hasFiltered := false
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
parts := content.Get("parts")
if !parts.IsArray() || len(parts.Array()) == 0 {
hasFiltered = true
return true
}
filteredContents, _ = sjson.SetRaw(filteredContents, "-1", content.Raw)
return true
})
if hasFiltered {
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContents))
}
var filteredContentsRaws []string
hasFiltered := false
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
parts := content.Get("parts")
if !parts.IsArray() || len(parts.Array()) == 0 {
hasFiltered = true
return true
}
filteredContentsRaws = append(filteredContentsRaws, content.Raw)
return true
})
if hasFiltered {
filteredContentsJSON := "[" + strings.Join(filteredContentsRaws, ",") + "]"
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContentsJSON))
}

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.

1 participant