rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
b778c1b
·
1 Parent(s): 2524073

fix(single_brain): KI-242 — 2-attempt retry on transient Gemini errors

Browse files

Z1 investigation of intermittent single_brain failures (live HF Space logs
2026-05-15T08:15:07Z..45Z) found 100% of failures were "Gemini HTTP 503
UNAVAILABLE — high demand". Zero timeouts, zero 429s, zero JSON-parse,
zero MAX_ITERATIONS. Pure upstream provider capacity blips.

Previously a 1-second blip caused fall-through to legacy orchestrator. Now
the call retries once with 1.5s backoff on:
- HTTP 429 (rate limit)
- HTTP 500/502/503/504 (server/upstream)
- httpx.TimeoutException
- httpx.HTTPError

Permanent 4xx (400/401/403/404) still raises immediately — bad API key or
malformed body is not retryable on the same payload.

Worst-case added latency: 1.5s, well inside the 45s outer budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. backend/single_brain.py +88 -26
backend/single_brain.py CHANGED
@@ -56,6 +56,16 @@ PER_CALL_TIMEOUT_SEC = 25.0
56
  # where the LLM keeps calling save_profile_field on the same value.
57
  MAX_ITERATIONS = 5
58
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  SYSTEM_PROMPT = """You are an Indian health-insurance advisor speaking with a customer.
61
 
@@ -377,6 +387,13 @@ async def _gemini_call(
377
  ) -> dict:
378
  """Single non-streaming Gemini generateContent call. Returns the raw
379
  JSON payload. Raises SingleBrainError on any 4xx/5xx/transport error.
 
 
 
 
 
 
 
380
  """
381
  url = f"{GEMINI_BASE_URL}/{model}:generateContent?key={api_key}"
382
  body: dict = {
@@ -397,34 +414,79 @@ async def _gemini_call(
397
  pool=2.0,
398
  )
399
 
400
- async with httpx.AsyncClient(timeout=client_timeout) as client:
401
- try:
402
- resp = await client.post(url, headers=headers, json=body)
403
- except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
404
- raise
405
- except httpx.TimeoutException as e:
406
- raise SingleBrainError(
407
- f"Gemini timeout after {timeout_sec:.1f}s (model={model})"
408
- ) from e
409
- except httpx.HTTPError as e:
410
- raise SingleBrainError(
411
- f"Gemini transport error ({type(e).__name__}): {str(e)[:200]}"
412
- ) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
 
414
- if resp.status_code >= 400:
415
- detail = ""
416
  try:
417
- detail = resp.text[:500]
418
- except Exception:
419
- pass
420
- raise SingleBrainError(
421
- f"Gemini HTTP {resp.status_code}: {detail}"
422
- )
423
-
424
- try:
425
- return resp.json()
426
- except Exception as e: # noqa: BLE001
427
- raise SingleBrainError(f"Gemini malformed JSON: {e}") from e
428
 
429
 
430
  def _extract_parts(payload: dict) -> list[dict]:
 
56
  # where the LLM keeps calling save_profile_field on the same value.
57
  MAX_ITERATIONS = 5
58
 
59
+ # Transient-error retry policy (2026-05-15 / KI-singlebrain-503).
60
+ # Live HF Space logs (rohitsar567/InsuranceBot, 2026-05-15 08:15Z) show
61
+ # Gemini intermittently returns HTTP 503 "model is currently experiencing
62
+ # high demand" — sometimes 3 in a row on the same session — which immediately
63
+ # tripped the orchestrator fallback. We retry ONCE on these transient codes
64
+ # with a short backoff before raising SingleBrainError so the legacy
65
+ # orchestrator only takes over on a genuinely sustained outage.
66
+ _TRANSIENT_HTTP_CODES = {429, 500, 502, 503, 504}
67
+ _TRANSIENT_RETRY_BACKOFF_SEC = 1.5
68
+
69
 
70
  SYSTEM_PROMPT = """You are an Indian health-insurance advisor speaking with a customer.
71
 
 
387
  ) -> dict:
388
  """Single non-streaming Gemini generateContent call. Returns the raw
389
  JSON payload. Raises SingleBrainError on any 4xx/5xx/transport error.
390
+
391
+ Internal retry: on transient failures (HTTP 429/5xx, httpx
392
+ TimeoutException, httpx.HTTPError) we retry ONCE after a short
393
+ backoff before raising. This soaks up the brief Gemini "high demand"
394
+ 503 bursts observed live (2026-05-15) so we don't fall through to
395
+ the legacy orchestrator mid-session for what is usually a sub-second
396
+ blip on the provider side.
397
  """
398
  url = f"{GEMINI_BASE_URL}/{model}:generateContent?key={api_key}"
399
  body: dict = {
 
414
  pool=2.0,
415
  )
416
 
417
+ last_err: Optional[str] = None
418
+ last_status: Optional[int] = None
419
+ # 2 attempts total: initial + 1 retry on transient failure.
420
+ for attempt in range(2):
421
+ async with httpx.AsyncClient(timeout=client_timeout) as client:
422
+ try:
423
+ resp = await client.post(url, headers=headers, json=body)
424
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
425
+ raise
426
+ except httpx.TimeoutException as e:
427
+ last_err = (
428
+ f"Gemini timeout after {timeout_sec:.1f}s (model={model})"
429
+ )
430
+ last_status = None
431
+ if attempt == 0:
432
+ _log.warning(
433
+ "single_brain transient timeout (attempt=1); "
434
+ "retrying once after %.1fs backoff",
435
+ _TRANSIENT_RETRY_BACKOFF_SEC,
436
+ )
437
+ await asyncio.sleep(_TRANSIENT_RETRY_BACKOFF_SEC)
438
+ continue
439
+ raise SingleBrainError(last_err) from e
440
+ except httpx.HTTPError as e:
441
+ last_err = (
442
+ f"Gemini transport error "
443
+ f"({type(e).__name__}): {str(e)[:200]}"
444
+ )
445
+ last_status = None
446
+ if attempt == 0:
447
+ _log.warning(
448
+ "single_brain transient transport error "
449
+ "(attempt=1, %s); retrying once after %.1fs backoff",
450
+ type(e).__name__, _TRANSIENT_RETRY_BACKOFF_SEC,
451
+ )
452
+ await asyncio.sleep(_TRANSIENT_RETRY_BACKOFF_SEC)
453
+ continue
454
+ raise SingleBrainError(last_err) from e
455
+
456
+ if resp.status_code >= 400:
457
+ detail = ""
458
+ try:
459
+ detail = resp.text[:500]
460
+ except Exception:
461
+ pass
462
+ last_status = resp.status_code
463
+ last_err = f"Gemini HTTP {resp.status_code}: {detail}"
464
+ # Transient → retry once. Permanent (4xx like 400/401/403/404) →
465
+ # raise immediately; retrying won't help.
466
+ if (
467
+ attempt == 0
468
+ and resp.status_code in _TRANSIENT_HTTP_CODES
469
+ ):
470
+ _log.warning(
471
+ "single_brain transient HTTP %d (attempt=1); "
472
+ "retrying once after %.1fs backoff",
473
+ resp.status_code, _TRANSIENT_RETRY_BACKOFF_SEC,
474
+ )
475
+ await asyncio.sleep(_TRANSIENT_RETRY_BACKOFF_SEC)
476
+ continue
477
+ raise SingleBrainError(last_err)
478
 
 
 
479
  try:
480
+ return resp.json()
481
+ except Exception as e: # noqa: BLE001
482
+ raise SingleBrainError(f"Gemini malformed JSON: {e}") from e
483
+
484
+ # Defensive — loop fell through without returning or raising. Should
485
+ # be unreachable, but raise so we never silently return None.
486
+ raise SingleBrainError(
487
+ last_err
488
+ or f"Gemini exhausted retries (last_status={last_status})"
489
+ )
 
490
 
491
 
492
  def _extract_parts(payload: dict) -> list[dict]: