Explicit timeouts on every OpenAI and Gemini request (#153)

No AI client set a timeout: OpenAI's SDK waits 600s per attempt and retries
twice (timeouts included); google-genai waits forever. A hung provider call
had no upper bound, so recorrelation_sweep's 15-minute pipeline hold-back was
a guess against an unbounded tail.

- config: stt_timeout_seconds (120), openai_timeout_seconds (60),
  openai_max_retries (1), gemini_timeout_seconds (60).
- app/internal/ai_clients.py builds every OpenAI/AsyncOpenAI client with them
  (transcription, extraction, embeddings, summarizer, vocabulary learner,
  trip chat). gemini.py's shared client gets HttpOptions(timeout=ms).
- A timeout raises like any other provider error, so existing fallbacks are
  unchanged: rules-only correlation, rules decision on tiebreak, raw
  transcript on correction, ai_health "transient".
- MIN_MINUTES_SINCE_PIPELINE_START is now derived from those timeouts
  (pipeline_worst_case_seconds, 3-scene budget + 2 min non-AI slack), floor
  15: 21 min with the defaults. Loop warns if it reaches the scan window.
- tests/test_ai_timeouts.py: client limits, a real hung-socket request that
  gives up in <5s, timeout fallback per tier, the derived bound, and a guard
  against any new OpenAI( outside ai_clients.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-04 18:00:25 -04:00
co-authored by Claude Opus 5.5
parent d57738a8f6
commit 278edf48dc
11 changed files with 402 additions and 30 deletions
+18
View File
@@ -30,6 +30,24 @@ class Settings(BaseSettings):
openai_api_key: Optional[str] = None
stt_model: str = "whisper-1" # whisper-1 | gpt-4o-mini-transcribe | gpt-4o-transcribe
# AI request timeouts (server-26#153). Without these the OpenAI SDK waits
# up to 600s per attempt and retries twice, and the google-genai client
# waits forever, so a hung provider call had no upper bound at all — and
# recorrelation_sweep.py's hold-back could only guess at one. Every OpenAI
# client is built by app/internal/ai_clients.py and the one Gemini client
# by app/internal/gemini.py; both read these. A timeout raises like any
# other provider error, so every call site's existing fallback applies
# (rules-only correlation, raw transcript, no summary, ...).
# The timeout bounds each phase of one HTTP attempt (connect/write/read);
# these endpoints are non-streaming, so in practice it bounds the attempt.
stt_timeout_seconds: float = 120.0 # Whisper: uploads up to MAX_RECORDING_SECONDS of audio
openai_timeout_seconds: float = 60.0 # chat completions + embeddings
# Retries per OpenAI request (SDK default 2). The SDK retries timeouts too,
# so each one multiplies the worst case by another full timeout.
openai_max_retries: int = 1
# google-genai does not retry unless told to, so this is the whole bound.
gemini_timeout_seconds: float = 60.0
# Google Maps (geocoding)
google_maps_api_key: Optional[str] = None
# 511NY developer key (server-26#183). Optional: the API answered without one