Skip to content

fix(core): keep the synthetic binary ack so injected audio/video stays in its own turn - #30230

Open
dongs0104 wants to merge 1 commit into
google-gemini:mainfrom
dongs0104:fix/binary-ack-plain-text
Open

dongs0104 wants to merge 1 commit into
google-gemini:mainfrom
dongs0104:fix/binary-ack-plain-text

Conversation

@dongs0104

Copy link
Copy Markdown

Summary

With Gemini 3 models, read_file on audio or video fails with 400 "Requests ending with a model turn are not supported" when using Code Assist (Login with Google). The binary injection from #26352 sends the media in its own user turn behind a synthetic model ack. Because that ack is a thought-only part, thought stripping drops it. The media then coalesces into the functionResponse turn, which is the shape the API rejects. This PR keeps the ack as a plain-text model turn when history is sent.

Details

  • In stripThoughts (both copies), scrubHistory and scrubContents, a synthetic binary ack turn becomes model[text: "Binary content received. Proceeding with analysis."] instead of being dropped. The !isSyntheticBinaryAckTurn exclusions are removed because those branches can no longer be reached for the ack.
  • The recorded history keeps the thought ack, so nothing changes in the UI. Sessions recorded before this change are also fixed on resume, because the conversion happens when the request is built.
  • The historyHardening test that expected the merged [functionResponse, inlineData] turn now expects the three-turn shape. A new GeminiChat test checks the request a modern model receives. Both new assertions fail on main and pass with this change.
  • Images and PDFs are unaffected, since they stay nested in functionResponse.parts.

Related Issues

Fixes #30229

How to Validate

  1. Unit tests:

    npm test -w @google/gemini-cli-core -- src/utils/historyHardening.test.ts src/core/geminiChat.test.ts
  2. Live check on Linux with Login with Google, -m gemini-3.8-flash, and a small .mp4 and .wav in the workspace. I recorded the request shapes with a local proxy via CODE_ASSIST_ENDPOINT:

    Build Request tail Result
    main e5d88fb2c user[functionResponse(read_file), inlineData(video/mp4)] 400
    this PR user[functionResponse], model[text], user[inlineData(video/mp4)] 200, the model read the text in the video correctly
    main e5d88fb2c user[functionResponse(read_file), inlineData(audio/wav)] 400
    this PR user[functionResponse], model[text], user[inlineData(audio/wav)] 200

Note: on main, an audio or video file larger than about 48 KB also fails with "Base64 decoding failed". Since #30180, truncateFunctionResponsePart truncates the injected base64 at 64 KB. That is a separate issue, so the live check used small files.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

https://claude.ai/code/session_01Sw78yR9Us3cqRudv2DDgdA

@dongs0104
dongs0104 requested a review from a team as a code owner November 11, 2026 13:01
@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 addresses an issue where binary data injection for audio and video files failed for Gemini 3 models. By ensuring that the synthetic binary acknowledgment is treated as a plain-text model turn rather than a thought-only part, the media content is correctly separated into its own user turn. This change maintains compatibility with the Code Assist API requirements and prevents invalid request shapes.

Highlights

  • Binary Injection Fix: Modified the handling of synthetic binary acknowledgment turns to be preserved as plain-text model turns instead of being dropped during thought stripping.
  • API Compatibility: Ensured that injected audio/video media remains in its own user turn, preventing it from coalescing into the functionResponse turn, which avoids 400 errors with the Code Assist API.
  • Test Coverage: Updated existing history hardening tests and added a new GeminiChat test case to verify the correct request structure for modern models.
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 the 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 counterproductive. 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. ↩

@github-actions github-actions Bot added the size/m A medium sized PR label Nov 11, 2026
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 238
  • Additions: +199
  • Deletions: -39
  • Files changed: 4

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

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.

Code Review

This pull request ensures that synthetic binary acknowledgment turns are preserved as plain text model turns instead of being stripped or coalesced during history hardening. This prevents injected binary data from coalescing into a functionResponse turn, which avoids 400 errors from the Code Assist API. Relevant tests have been added and updated to verify this behavior. I have no feedback to provide as there are no review comments.

@dongs0104
dongs0104 force-pushed the fix/binary-ack-plain-text branch from dbac79c to f0db098 Compare November 11, 2026 13:07
@gemini-cli gemini-cli Bot added area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality priority/p1 Important and should be addressed in the near term. labels Nov 11, 2026
…s in its own turn

Audio and video read by read_file are injected as
user[functionResponse] -> model[ack] -> user[inlineData]. The ack is a
thought-only part, so stripThoughts, scrubHistory and scrubContents drop
it and the two user turns coalesce into
user[functionResponse, inlineData]. The Code Assist API rejects that
shape with 400 "Requests ending with a model turn are not supported".

Send the ack as a plain-text model turn instead. The recorded history is
unchanged, so sessions recorded with the thought ack are fixed on resume.

Fixes google-gemini#30229

Claude-Session: https://claude.ai/code/session_01Sw78yR9Us3cqRudv2DDgdA
@dongs0104
dongs0104 force-pushed the fix/binary-ack-plain-text branch from f0db098 to 47f1eea Compare November 12, 2026 15:58
@dongs0104

Copy link
Copy Markdown
Author

/gemini review

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

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.

Code Review

This pull request updates the history hardening and thought stripping logic to preserve synthetic binary acknowledgment turns as plain text model turns, preventing injected media from coalescing into function response turns. One review comment was provided, suggesting a defensive check for undefined array elements and explicit handling of optional properties in geminiChat.ts to prevent potential runtime errors.

Note: Security Review did not run due to the size of the PR.

Comment on lines +2150 to 2165
if (stripped.at(-1)?.role === 'user') {
for (let i = stripped.length - 1; i >= 0; i--) {
const prev = stripped[i];
if (prev.role === 'user') {
if (prev.parts?.some((part) => !!part.functionResponse)) {
hasToolResponseUserTurn = true;
break;
}
continue;
}
if (isBinaryAckTurn(prev)) {
continue;
}
break;
}
}

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

The loop accesses prev.role and prev.parts directly without checking if prev is defined. While stripped is populated sequentially, adding a defensive check for prev prevents potential runtime crashes or compilation errors under strict TypeScript configurations (such as noUncheckedIndexedAccess). Additionally, since parts is an optional property, we should handle the undefined case explicitly using the nullish coalescing operator (??) to align with our repository guidelines.

      if (stripped.at(-1)?.role === 'user') {
        for (let i = stripped.length - 1; i >= 0; i--) {
          const prev = stripped[i];
          if (!prev) {
            break;
          }
          if (prev.role === 'user') {
            if (prev.parts?.some((part) => !!part.functionResponse) ?? false) {
              hasToolResponseUserTurn = true;
              break;
            }
            continue;
          }
          if (isBinaryAckTurn(prev)) {
            continue;
          }
          break;
        }
      }
References
  1. When consuming an object, if a property is optional in its type definition (interface), callers must handle the undefined case (e.g., by providing a default with ??).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality priority/p1 Important and should be addressed in the near term. size/m A medium sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

read_file on audio/video returns 400 "Requests ending with a model turn are not supported" on Gemini 3: synthetic binary ack turn is stripped

1 participant