Skip to content

fix(gemini): clean tool schemas, stream thinking passthrough, and strip trailing unanswered function calls#2474

Merged
luispater merged 3 commits intorouter-for-me:mainfrom
aikins01:fix/gemini-claude-tool-schema
Apr 2, 2026
Merged

fix(gemini): clean tool schemas, stream thinking passthrough, and strip trailing unanswered function calls#2474
luispater merged 3 commits intorouter-for-me:mainfrom
aikins01:fix/gemini-claude-tool-schema

Conversation

@aikins01
Copy link
Copy Markdown
Contributor

@aikins01 aikins01 commented Apr 1, 2026

Summary

  • Delegate tool schema sanitization to util.CleanJSONSchemaForGemini and remove brittle bytes.Replace hack
  • Strip eager_input_streaming from Claude tool declarations before sending to Gemini
  • Let thinking blocks pass through in SSE streaming (aligned with fix(amp): stop suppressing thinking blocks in streaming mode #2472)
  • Strip trailing model turns with unanswered function calls to prevent Gemini empty responses
  • Update tests to expect thinking blocks to pass through during streaming

Problem

  1. Tool schema validation errors: The gemini translator sent Claude tool schemas directly to Gemini without cleaning unsupported keys ($schema, format, patternProperties, eager_input_streaming), causing 400 Bad Request errors. Mirrors fix from PR fix(gemini-cli): sanitize tool schemas and filter empty parts #2108 applied to gemini-cli translator.

  2. Broken SSE stream: The ResponseRewriter was suppressing thinking blocks during streaming, which corrupted the SSE event sequence (orphaned event: lines, missing content_block_start for indices Amp expected). Thinking blocks now pass through intact in streaming, matching ampcode.com behavior (ref Amp TUI crashes with 'undefined is not an object' when using local provider for Claude models with thinking #2471).

  3. Empty Gemini responses: When conversation history ended with a model turn containing an unanswered functionCall, Gemini returned empty responses with finishReason: STOP. The translator now strips these trailing turns.

aikins01 added 3 commits April 1, 2026 19:49
delegate schema sanitization to util.CleanJSONSchemaForGemini and drop the top-level eager_input_streaming key to prevent validation errors when sending claude tools to the gemini api
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 refactors the ResponseRewriter to simplify thinking block suppression by replacing map-based tracking with a boolean flag, which now allows thinking blocks to pass through in streaming responses. Additionally, the Gemini-Claude request translator is updated to handle trailing model turns with unanswered function calls, clean JSON schemas for Gemini compatibility, and remove unsupported tool fields. Feedback includes removing a redundant nil check in the streaming logic and a suggestion to refine the model turn stripping to preserve text content while removing function calls.

Comment on lines 227 to 230
if rewritten == nil {
// Event suppressed (e.g. thinking block), skip event+data pair
i = dataIdx + 1
continue
}
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

The check if rewritten == nil is now redundant because rewriteStreamEvent (line 274) always returns a non-nil slice. Previously, this check was used to skip events when thinking blocks were suppressed in the stream. Since thinking blocks are now allowed to pass through in streaming, this logic can be simplified.

Suggested change
if rewritten == nil {
// Event suppressed (e.g. thinking block), skip event+data pair
i = dataIdx + 1
continue
}
rewritten := rw.rewriteStreamEvent(jsonData)
// Emit event line
out = append(out, line)
// Emit blank lines between event and data
for k := i + 1; k < dataIdx; k++ {
out = append(out, lines[k])
}
// Emit rewritten data
out = append(out, append([]byte("data: "), rewritten...))
i = dataIdx + 1
continue

Comment on lines +150 to +169
contents := gjson.GetBytes(out, "contents")
if contents.Exists() && contents.IsArray() {
arr := contents.Array()
if len(arr) > 0 {
last := arr[len(arr)-1]
if last.Get("role").String() == "model" {
hasFC := false
last.Get("parts").ForEach(func(_, part gjson.Result) bool {
if part.Get("functionCall").Exists() {
hasFC = true
return false
}
return true
})
if hasFC {
out, _ = sjson.DeleteBytes(out, fmt.Sprintf("contents.%d", len(arr)-1))
}
}
}
}
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

The logic to strip trailing model turns with unanswered function calls is effective for preventing Gemini empty responses. However, deleting the entire turn might lose valuable text content if the model turn contains both text and a function call. While this is a valid trade-off to ensure a successful response from Gemini, consider if only the functionCall parts should be removed while preserving text parts, provided the resulting turn is not empty.

Copy link
Copy Markdown
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

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

Summary
This is a solid set of fixes for AMP streaming behavior and Gemini tool/schema handling. I can’t approve yet because CI blocks translator edits.

Blocking

  • translator-path-guard / ensure-no-translator-changes is failing because this PR modifies internal/translator/**. The workflow currently forbids translator changes in PRs. Please split the AMP-only changes into this PR and route translator changes via the maintenance process (or land a separate PR that updates the guard policy).

Non-blocking

  • Please run gofmt on internal/api/modules/amp/response_rewriter.go (alignment/blank lines).
  • Commit 3 subject is labeled test(amp) but it changes translator code; might want to fix when squashing.

Test plan

  • go build -o test-output ./cmd/server (OK)

Follow-ups

  • Consider a unit test for the “strip trailing model functionCall turn” logic (especially if the last model message contains both text and functionCall parts).

@luispater luispater merged commit ab9ebea into router-for-me:main Apr 2, 2026
1 of 2 checks passed
@aikins01
Copy link
Copy Markdown
Contributor Author

aikins01 commented Apr 2, 2026

i was actually just working on addressing the blockers and fixing a regression we caught in the streaming logic but i see this got merged

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