Skip to content

interactions.get() always sends stream=false query param with no way to omit it (intermittent 400 "Unknown name stream" during backend rollout) #2661

Description

@pullely-samuel

Summary

client.interactions.get(id) always serializes a stream query parameter on
the GET /interactions/{id} request, and the public client wrapper gives the
caller no way to omit it. The lower-level (_gaos) request model treats
stream as optional and drops it from the query when it is None, but the
public wrapper coerces it to a concrete bool, so the param is always sent.

This is benign as long as every backend accepts the parameter. It is not
benign during a backend rollout: I have been seeing intermittent

400 INVALID_ARGUMENT: Invalid JSON payload received. Unknown name "stream":
Cannot bind query parameter. Field 'stream' could not be found in request message.

from interactions.get(...) today. Because requests are load-balanced, the
failure is intermittent — the same code/SDK version succeeds on most calls and
fails on a fraction, and a failing backend instance is "sticky" for ~30–45s, so
retrying the identical request often doesn't recover. (As I write this the
backend appears to accept stream=false again, but for a multi-hour window it
was rejecting it on a large fraction of calls, and since it's a rollout it could
flip back.)

The request: please let callers omit stream on the non-streaming get() path
(or don't send it by default), so applications aren't forced to send a parameter
some backends reject and have a workaround independent of the backend rollout.

Environment

  • google-genai 2.10.0
  • Python 3.13.11
  • macOS 26.5 (arm64)
  • Gemini API (generativelanguage.googleapis.com), Interactions API

Root cause (code references)

In google/genai/_gaos/google_genai.py, the public interactions.get wrapper
(sync ~L288 and async ~L440):

def get(self, id, *, ..., stream: Any = False, ...):
    stream_bool = bool(_optional_bool(stream, default=False))   # always a real bool
    response = wrap_sdk_call(
        super().get,
        id=id,
        ...
        include_input=_optional_bool(include_input),  # None -> omitted (good)
        stream=stream_bool,                           # always sent (the problem)
        ...
    )

with

def _optional_bool(value, default=None):
    if isinstance(value, bool):
        return value
    return default

So include_input=None is correctly omitted from the query, but stream is
forced to True/False and therefore always serialized. Passing stream=None
does not help — bool(_optional_bool(None, default=False)) collapses it to
False.

The underlying _gaos/interactions.py get(..., stream: Optional[bool] = False)
plus its request-model query serialization do omit the param when it is
None (verified):

  • stream=None → query params {'include_input': ['false']}
  • stream=False → query params {'stream': ['false'], 'include_input': ['false']}

The public wrapper defeats that, so there is no public-API way to omit stream.

Reproduction

from google import genai

client = genai.Client()

# A stored interaction (any model/tools).
created = client.interactions.create(
    model="gemini-3.5-flash",
    input="hello",
    store=True,
)

# Re-fetch it. This always sends ?stream=false; during the backend rollout a
# fraction of these calls 400 with 'Unknown name "stream"'. There is no
# public-API argument that makes get() stop sending stream.
for _ in range(20):
    client.interactions.get(created.id)

The non-streaming get() is the documented way to read a finalized interaction
after a streamed turn — the streaming interaction.completed event carries
steps=None, so reading citations / file_search results requires a follow-up
get(id). That is exactly the path this affects.

Impact

Any code that streams an interaction and then calls interactions.get(id) to
read the finalized steps (citations, file_search results, etc.) is exposed
during a backend rollout and cannot work around it without dropping to the
private _gaos layer.

Suggested fixes (any one unblocks callers)

  1. In the public get() wrapper, forward stream the same way include_input
    is already handled — stream=_optional_bool(stream) (no forced default) — so
    stream=None omits the param. Smallest change; restores a workaround.
  2. Don't send stream at all on the non-streaming get() path (it is only
    meaningful when streaming a GET).
  3. Backend: accept and ignore the documented stream query parameter on
    GET /interactions/{id} across all backends (addresses the root 400).

Metadata

Metadata

Labels

priority: p2Moderately-important priority. Fix may not be included in next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions