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)
- 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.
- Don't send
stream at all on the non-streaming get() path (it is only
meaningful when streaming a GET).
- Backend: accept and ignore the documented
stream query parameter on
GET /interactions/{id} across all backends (addresses the root 400).
Summary
client.interactions.get(id)always serializes astreamquery parameter onthe
GET /interactions/{id}request, and the public client wrapper gives thecaller no way to omit it. The lower-level (
_gaos) request model treatsstreamas optional and drops it from the query when it isNone, but thepublic 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
from
interactions.get(...)today. Because requests are load-balanced, thefailure 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=falseagain, but for a multi-hour window itwas rejecting it on a large fraction of calls, and since it's a rollout it could
flip back.)
The request: please let callers omit
streamon the non-streamingget()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-genai2.10.0generativelanguage.googleapis.com), Interactions APIRoot cause (code references)
In
google/genai/_gaos/google_genai.py, the publicinteractions.getwrapper(sync ~L288 and async ~L440):
with
So
include_input=Noneis correctly omitted from the query, butstreamisforced to
True/Falseand therefore always serialized. Passingstream=Nonedoes not help —
bool(_optional_bool(None, default=False))collapses it toFalse.The underlying
_gaos/interactions.pyget(..., 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
The non-streaming
get()is the documented way to read a finalized interactionafter a streamed turn — the streaming
interaction.completedevent carriessteps=None, so reading citations /file_searchresults requires a follow-upget(id). That is exactly the path this affects.Impact
Any code that streams an interaction and then calls
interactions.get(id)toread the finalized
steps(citations,file_searchresults, etc.) is exposedduring a backend rollout and cannot work around it without dropping to the
private
_gaoslayer.Suggested fixes (any one unblocks callers)
get()wrapper, forwardstreamthe same wayinclude_inputis already handled —
stream=_optional_bool(stream)(no forced default) — sostream=Noneomits the param. Smallest change; restores a workaround.streamat all on the non-streamingget()path (it is onlymeaningful when streaming a GET).
streamquery parameter onGET /interactions/{id}across all backends (addresses the root 400).