googleapis / googleapis/python-genai
interactions.get() always sends stream=false query param with no way to omit it (intermittent 400 "Unknown name stream" during backend rollout)
- Dominant language
- Python
- Stars
- 4k
- Forks
- 1k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 40
Description
## 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):
```python
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
```python
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
```python
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).
Contributor guide
Assessment
This issue has not been assessed yet.