Unstable Multi-Turn Image Editing ๐ŸŒ with Google GenAI SDK Streaming: the Problem and the Fix

Unstable Multi-Turn Image Editing ๐ŸŒ with Google GenAI SDK Streaming: the Problem and the Fix

Hello!

While implementing multi-turn image editing with Gemini 3 Pro Image (Nano Banana Pro), we ran into a troublesome problem: it worked sometimes and failed other times.

In this article, we share the symptoms, the investigation process, and the solution.


The Symptoms

Environment

Google GenAI SDK library (pip): google-genai 1.56.0

Expected Behavior

  1. User: ใ€Œใ‹ใ‚ใ„ใ„ๅญ็Œซใฎ็”ปๅƒใ‚’็”Ÿๆˆใ—ใฆใ€ ("Generate an image of a cute kitten")
  2. Gemini: generates a kitten image
  3. User: ใ€Œใ“ใฎๅญใซใƒกใ‚ฌใƒใ‚’ใ‹ใ‘ใฆใ€ ("Put glasses on this kitten")
  4. Gemini: the same kitten, now wearing glasses

What Actually Happened

  1. User: ใ€Œใ‹ใ‚ใ„ใ„ๅญ็Œซใฎ็”ปๅƒใ‚’็”Ÿๆˆใ—ใฆใ€ ("Generate an image of a cute kitten")
  2. Gemini: generates an image of a brown kitten
  3. User: ใ€Œใ“ใฎๅญใซใƒกใ‚ฌใƒใ‚’ใ‹ใ‘ใฆใ€ ("Put glasses on this kitten")
  4. Gemini: generates an image of a girl wearing glasses
Huh? We should have gotten a kitten with glasses, but instead we got an image of a girl wearing glasses

In other words, the model was in a state where it did not "remember" the previously generated image.

The Tricky Part: No Reproducibility

What made this problem especially troublesome was that it worked sometimes and failed other times.

  • The same code would succeed or fail depending on timing
  • After a server restart, it would stop working for long stretches
  • It worked in development but not in staging

When identical code suddenly stops working and you reflexively reach for "let's just restart it," you lose your fixed environment, isolating the problem gets harder, and you end up in the "but it was working a minute ago..." situation. Pinning down the cause took a long time.


Investigating the Cause

How thought_signature Works

Multi-turn image editing in Gemini 3 Pro Image relies on a mechanism called thought_signature.

  • When generating an image, the model returns a thought_signature.
  • This is roughly 2MB of data that preserves information about the generated image (composition, colors, content, and so on)
  • Passing it back in the next turn puts the model in a state where it "remembers" the previous image

According to Google's official documentation:

If you use the official Google Gen AI SDKs and use the chat feature, thought signatures are handled automatically.

So, in short, if you use the SDK's chat feature, it should be managed automatically... or so we thought.
With this thought_signature mechanism, you can avoid what text chat normally requires โ€” sending the entire history so far on every turn.

The SDK's Chat Session

In our approach, we created a chat session with the Google GenAI SDK's client.aio.chats.create() and sent messages with chat.send_message_stream().

# Create a chat session
chat = client.aio.chats.create(model="gemini-3-pro-preview", config=config)

# Send a message (streaming)
response_stream = await chat.send_message_stream(content_parts)
async for response in response_stream:
    # Process the response
    ...

According to the documentation, thought_signature should be managed automatically with this. In practice, it did not work.

Discovering GitHub Issue #1791

As our investigation progressed, we found a related issue on GitHub.

[Bug] ChatSession history fragmentation when using send_message_stream with Thinking (Gemini 3 Pro)
https://github.com/googleapis/python-genai/issues/1791

The issue in question

According to this issue:

When using Gemini 3 Pro Preview with thinking_config enabled, the ChatSession history becomes fragmented when using send_message_stream. Instead of appending a single model turn with the complete response, the SDK appends multiple model turns corresponding to the streaming chunks.

In short, a bug had been reported where using send_message_stream() causes the chat history to become fragmented.

Expected

[User, Model]  # 2 entries

Actual

[User, Model, Model, Model, Model, ...]  # multiple Model entries

A history entry gets appended for every streaming chunk, breaking the conversation structure.

Why It "Worked Sometimes and Failed Other Times"

Reading this issue, we could infer why the behavior was intermittent.

  1. With consecutive requests inside the same server instance, it can work because the session is still in memory
  2. After a server restart or in a new session, it fails because it tries to resume from the broken history
  3. The degree of history fragmentation varies with timing and network conditions

This was the cause of the irreproducible behavior.


The Solution

Use the Non-Streaming Version

Following issue #1791, we decided to use send_message_stream() instead of send_message().

# Before (streaming)
response_stream = await chat.send_message_stream(content_parts)
async for response in response_stream:
    # process
    ...

# After (non-streaming)
response = await chat.send_message(content_parts)
# process
...

The Result

After switching to the non-streaming version, multi-turn image editing worked reliably.

  • Generate a kitten โ†’ add glasses to the same kitten
This time, the glasses were correctly added to the original kitten
  • Works the same way no matter how many times we try
  • Works even after a server restart

Summary

Problem

Using the Google GenAI SDK's send_message_stream() fragments the chat history, and thought_signature is not managed correctly.

Impact

Multi-turn image editing with Gemini 3 Pro Image becomes unstable (it works sometimes and fails other times).

Solution

send_message_stream()Use send_message() instead.

Side Effects

  • Real-time streaming display is no longer possible (affects incremental rendering of text + SVG output, etc.)
  • No result is returned until image generation completes
  • However, if you separately implement progress sub-messages ("Processing..." and the like), the UX impact is minimal

Going Forward

  • Wait for the SDK bug fix
  • issue #1791 : monitor its progress
  • Consider returning to the streaming version once it is fixed

References


Closing Thoughts

Bugs where identical code "works sometimes and fails other times" are extremely hard to pin down. In this case, it took us a while before we thought to suspect the SDK's internal behavior.

We hope this helps anyone struggling with the same problem.

See you next time!

Read more