Repository navigation
Labels
priority: p2Moderately-important priority. Fix may not be included in next release.Moderately-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.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Description
Environment details
- Programming language: Python
- OS: Ubuntu 26.04
- Language runtime version: Python 3.14.4
- Package version: google-genai-2.34.0
Current behavior:
- I set
HttpRetryOptions(attempts=0)orHttpRetryOptions(attempts=1) - API always retries ONCE, sleeping for the retry-after HTTP header between the retry attempts.
Why is this an issue:
When you run out of free tier API rate-limits, you get an extremely long retry-after such as 8h.
{'http.response.body.text': {'error': {'message': 'Rate limit exceeded for model gemini-3.8-flash (limit: 20 requests per day on Free Tier). Please retry in 7h40m27s or upgrade your tier at https://ai.dev/rate-limit.', 'code': 'too_many_requests'}}}
It is not ideal for the interactions API to block for 8h.
I would like the API to return immediately (zero retries).
Steps to reproduce
- We point
interactions.createto a mock HTTP server. - The mock HTTP server prints that it received a "POST" from the interactions API, and then returns 429 with a 60s retry-after header.
import asyncio
from http.server import BaseHTTPRequestHandler, HTTPServer
import threading
import time
from google import genai
from google.genai import types
RETRY_AFTER=60
# 1. Spin up a minimal local server that returns HTTP 429 with Retry-After
class Mock429Handler(BaseHTTPRequestHandler):
def do_POST(self):
print(f"[{time.strftime('%X')}] Mock server received POST, returning 429 with Retry-After: {RETRY_AFTER}s...")
self.send_response(429)
self.send_header("Content-Type", "application/json")
self.send_header("Retry-After", RETRY_AFTER)
self.end_headers()
self.wfile.write(b'{"error": {"code": "too_many_requests", "message": "Daily quota exceeded"}}')
def log_message(self, format, *args):
pass # Suppress default server logs
server = HTTPServer(("127.0.0.1", 8999), Mock429Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
# 2. Reproduce the bug in google-genai
async def main():
print(f"[{time.strftime('%X')}] Initializing genai.Client with attempts=0...")
client = genai.Client(
api_key="fake-key",
http_options=types.HttpOptions(
base_url="http://127.0.0.1:8999",
retry_options=types.HttpRetryOptions(attempts=0), # Attempting to disable retries. Both 0 and 1 yield the same result.
),
)
print(f"[{time.strftime('%X')}] Calling client.aio.interactions.create()...")
print(
"Expected: Fail immediately.\n"
"Actual: API waits for retry-after and retries once despite me disabling retries."
)
await client.aio.interactions.create(
model="gemini-3.8-flash",
input="Hello",
)
print("End of interactions.create")
asyncio.run(main())python standalone.py
[01:07:32] Initializing genai.Client with attempts=0...
[01:07:32] Calling client.aio.interactions.create()...
Expected: Fail immediately.
Actual: API waits for retry-after and retries once despite me disabling retries.
[01:07:33] Mock server received POST, returning 429 with Retry-After: 60s...
[01:08:33] Mock server received POST, returning 429 with Retry-After: 60s...
Traceback (most recent call last):
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/interactions.py", line 1923, in create
return await _speakeasy_parse_response(http_res)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/interactions.py", line 1837, in _speakeasy_parse_response
raise errors.CreateInteractionClientError(
response_data, http_res, http_res_text
)
google.genai._gaos.errors.createinteraction.CreateInteractionClientError: Daily quota exceeded
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/home/ubuntu/test/standalone.py", line 56, in <module>
asyncio.run(main())
~~~~~~~~~~~^^^^^^^^
File "/usr/lib/python3.14/asyncio/runners.py", line 204, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "/usr/lib/python3.14/asyncio/runners.py", line 127, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/lib/python3.14/asyncio/base_events.py", line 719, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "/home/ubuntu/test/standalone.py", line 49, in main
await client.aio.interactions.create(
...<2 lines>...
)
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/google_genai.py", line 655, in create
response = await send_once()
^^^^^^^^^^^^^^^^^
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/google_genai.py", line 710, in _send_create_once
return await async_wrap_sdk_call(
^^^^^^^^^^^^^^^^^^^^^^^^^^
...<3 lines>...
)
^
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/lib/compat_errors.py", line 379, in async_wrap_sdk_call
return await fn(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/interactions.py", line 1925, in create
await response_helpers.raise_parse_error_async(
...<5 lines>...
)
File "/home/ubuntu/test/venv/lib/python3.14/site-packages/google/genai/_gaos/utils/response_helpers.py", line 112, in raise_parse_error_async
raise parse_error from exc
google.genai._gaos.lib.compat_errors.RateLimitError: Error code: 429 - {'error': {'code': 'too_many_requests', 'message': 'Daily quota exceeded'}}Notice that the mock server received two HTTP POSTs from the interactions API.
[01:07:33] Mock server received POST, returning 429 with Retry-After: 60s...
[01:08:33] Mock server received POST, returning 429 with Retry-After: 60s...
Expectation: Only 1 HTTP POST because I set HttpRetryOptions(attempts=0).
Metadata
Metadata
Assignees
Labels
priority: p2Moderately-important priority. Fix may not be included in next release.Moderately-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.Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Activity
fix(interactions): map HttpRetryOptions.attempts onto the retry count
fix(interactions): map HttpRetryOptions.attempts onto the retry count
fix(interactions): map HttpRetryOptions.attempts onto the retry count
fix(interactions): attempts = retries - 1
Venkaiahbabuneelam commentedon Nov 4, 2026
Hi @FlippFuzz, thanks for the reporting this issue, we have reproduced this bug and confirmed that
interactionsmistakenly treatsattempts=1(andattempts=0, which gets normalised to1) as 1 extra retry instead of 1 total request, and we have filed an internal bug.Thanks
fix(interactions): attempts = retries - 1
fix(interactions): retries = attempts - 1
fix(interactions): retries = attempts - 1
fix(interactions): retries = attempts - 1