Skip to content

fix(ai-service): retry transient model errors within one extraction call - #72

Merged
Fluory merged 7 commits into
mainfrom
claude/fix-ai-model-retry-69
Sep 27, 2026
Merged

Fluory merged 7 commits into
mainfrom
claude/fix-ai-model-retry-69

Conversation

@Fluory

@Fluory Fluory commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Warum

Fixes #69 · gestapelt auf #68 (Showcase-Deployment)

Arbeitsstand

  • Ziel: Ein kurzer Überlast-Moment beim Modellanbieter lässt die Extraktion nicht mehr scheitern – ohne dass ein Aufruf länger läuft als sein Zeitbudget.
  • Nicht-Ziele: Wiederholungen in der Queue (unverändert), Modellwechsel.
  • Erledigt: eigene schmale Wiederholung mit Zeitbudget statt SDK-Retry (Befund des frischen Reviews); 12 Tests; README, CHANGELOG und Runbook im selben PR; AI_MODEL_TIMEOUT_SECONDS=45 im Vercel-Projekt requestflow-ai gesetzt (wirkt ab dem nächsten Deploy).
  • Offen: Produktions-Deploy des KI-Dienstes aus diesem Stand und Live-Nachweis – braucht die Freigabe des Orchestrators; Stand vor dem Review lief bereits live (siehe Nachweis).
  • Annahmen: 503-Antworten treten weitgehend unabhängig voneinander auf; dann sinkt die Fehlerquote von ca. ⅓ auf wenige Prozent (live mit dem Vorgängerstand: 3 von 3 Anfragen im ersten Durchlauf durch).
  • Nächster kleinster Schritt: Nach Freigabe requestflow-ai aus c0f1690 in Produktion deployen und drei Test-Anfragen im Showcase hochladen.

Was ist passiert (Klartext)

Im Showcase blieb eine Anfrage „In Verarbeitung“ hängen. Gemessen war die Ursache eindeutig: Googles Gemini-Free-Tier antwortet bei etwa jedem dritten Aufruf mit „Modell überlastet“. Bisher ließ schon eine einzige solche Antwort die ganze Extraktion scheitern. Jetzt versucht der KI-Dienst es bei genau diesen vorübergehenden Fehlern (Überlast 503, Rate-Limit 429) bis zu zweimal erneut, mit kurzer, wachsender Pause. Dabei gilt ein festes Zeitbudget: Alle Versuche zusammen dauern nie länger als die bisherige Zeitgrenze eines Modellaufrufs. Hängt das Modell (Zeitüberschreitung), gibt es keinen zweiten Versuch im selben Aufruf, denn die Zeit ist dann schon verbraucht. Die Anfrage wird später über die Warteschlange erneut verarbeitet, wie bisher. Andere Fehler, etwa ein falscher Schlüssel oder ein ungültiges Modell, scheitern sofort, damit echte Konfigurationsfehler nicht hinter Wartezeiten verschwinden.

Plan-Pflicht (SYSTEM.md §4)

  • Kein Auslöser – keine Modulgrenze, öffentliche API, Migration, Auth/Rechte, kein Zahlungs-/Daten-/Infrapfad, höchstens zwei Module, keine Architekturvarianten, umkehrbar
  • Auslöser zutreffend – Impact Manifest ausgefüllt (Plan vor Code)

Geändert

  • services/ai/src/requestflow_ai/extraction/model_client.py: RetryPolicy und Wiederholungsschleife im GeminiModelClient – nur APIError 429/503, höchstens AI_MODEL_RETRY_ATTEMPTS Versuche, Pause ab 1 s verdoppelt (max. 8 s, bis 25 % Jitter); jeder Versuch bekommt nur das Restbudget als Timeout; kein Retry mit weniger als 10 s Rest; Timeouts und Netzwerkfehler nie. Der SDK-Retry (HttpRetryOptions) ist entfernt, weil er Timeouts unabhängig von den Statuscodes wiederholt (google/genai/_api_client.py:578–581).
  • services/ai/src/requestflow_ai/config.py: AI_MODEL_RETRY_ATTEMPTS (Standard 3, 1–5), AI_MODEL_RETRY_INITIAL_DELAY_SECONDS (Standard 1, ≤ 5); AI_MODEL_TIMEOUT_SECONDS ist jetzt das Budget inklusive Wiederholungen
  • services/ai/tests/test_model_retry.py: 503 → Erfolg (2 Aufrufe), 429 → Erfolg, Dauer-503 → Fehler nach 3 Aufrufen, 400/401/403/404/500 → genau 1 Aufruf, Timeout → genau 1 Aufruf, zu wenig Restbudget → 1 Aufruf, zweiter Versuch mit kleinerem Timeout als der erste, Gemini-API-Weg wiederholt ebenfalls
  • services/ai/README.md: Budget-Semantik und die beiden neuen Variablen
  • docs/technical/deployment-vercel.md: AI_MODEL_TIMEOUT_SECONDS=45 für den Showcase (unter dem 60-s-Timeout der Web-App abzüglich Parsing)
  • CHANGELOG.md: Fixed-Eintrag für fix(ai-service): retry transient model errors within one extraction call #69 (vorher fälschlich in fix(requests): same processing state in list and detail, retries picked up on page view #73)

Nachweis (SYSTEM.md §11)

  • verify:changed: grün – pytest tests/test_model_retry.py 12/12; zuerst rot: Timeout-Test mit 3 statt 1 Aufruf, Budget-Test mit 2 statt 1 Aufruf, Timeout pro Versuch unverändert 30,0 s
  • verify: grün lokal – AI-Service ruff check, ruff format --check, pyright ohne Befund, pytest 452 bestanden, 2 übersprungen (Modell-Tests, feat(evals): run the scanned-PDF eval cases with OCR in CI #49); AI-Eval-Gate (Replay, 15 Fälle) bestanden; TS-Seite unverändert; CI am PR
  • verify:full / E2E-Spec: nicht betroffen
  • Manueller Prüfnachweis: Vorgängerstand (38e9ff3, SDK-Retry) lief live: 3 von 3 Test-Uploads erreichten Prüfung im ersten Durchlauf (2026-09-27). Der korrigierte Stand ist noch nicht in Produktion (Freigabe offen).
  • Frischer Review (P1 vor Ready-for-review; Architektur/API/DB immer): fix(ai-service): retry transient model errors within one extraction call #72 (comment) – changes requested; Blocker (Timeouts außerhalb des Budgets), README, CHANGELOG und Budget-Grenze behoben; der rote Vercel-Check war ein von mir abgebrochener Preview-Build, der neue Preview-Build läuft durch

Doku-Entscheidung (genau eine)

  • Keine langlebige Doku betroffen – Begründung: –
  • Doku betroffen und im selben PR aktualisiert:

Entferntes oder Umbenanntes: docs/ + README gegrept, Treffer bereinigt: „The SDK does not retry“ in services/ai/README.md ersetzt

Dateigrößen und neue Bausteine (SYSTEM.md §7)

Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):

  • keine
  • bewusst belassen – Begründung: –
  • im selben PR nach fachlicher Verantwortung geteilt
  • Folge-Issue –

Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen

Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:

  • nein
  • ja – gesucht nach: –

Subagent-Einsätze

  • Frischer Review durch einen unabhängigen Reviewer-Agenten (nur lesend, ohne Umsetzungskontext); Ergebnis unverändert als Kommentar gepostet.

Risiken / offene Punkte

  • Worst Case eines Modellaufrufs ist jetzt das Budget selbst (AI_MODEL_TIMEOUT_SECONDS, Showcase 45 s, lokal 60 s bei 120 s Worker-Timeout) statt bis zu 3 × Timeout.
  • Ein Modell, das hängt statt 503 zu antworten, wird in diesem Aufruf nicht wiederholt; das übernimmt wie bisher die Queue.

🤖 Generated with Claude Code

Fluory and others added 2 commits September 27, 2026 02:11
Symptom: a showcase request stayed "In Verarbeitung" with ai.unavailable.
Cause: the Gemini API free tier answers about one call in three with
503 UNAVAILABLE (high demand), measured 2026-09-27; one 503 failed the
whole extraction and left the job to the queue backoff.
Fix: the SDK retries 429/503 up to 3 attempts in total with exponential
backoff and jitter; every other status still fails at once, and the
contract after the last attempt is unchanged (502 model_error).

Fixes #69

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Refs #69

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

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
requestflow Ready Ready Preview Sep 27, 2026 10:39am UTC
requestflow-ai Ready Ready Preview Sep 27, 2026 10:39am UTC

@Fluory

Fluory commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Frischer Review (/review-pr) – unabhängiger Reviewer-Agent ohne Umsetzungskontext, 2026-09-27

Review PR #72 (fix(ai-service): retry transient model errors), Kopf 38e9ff3

[Blocker]   – services/ai/src/requestflow_ai/extraction/model_client.py:127-134 (SDK: .venv/.../google/genai/_api_client.py:578-581) – Das SDK wiederholt httpx.TimeoutException/ConnectError unabhängig von http_status_codes. Per Probe verifiziert: ReadTimeout → 3 Requests. Bei AI_MODEL_TIMEOUT_SECONDS=60 (config.py:27) dauert der Worst Case ≈ 3×60 s + 3–5 s Backoff ≈ 185 s. Das AC „within the existing model timeout" ist damit nicht erfüllt. Die Risikoangabe „ca. 3 s … innerhalb 60 s Worker-Timeout" ist falsch, ebenso der Klartext-Satz „alle anderen Fehler scheitern sofort". – Der Worker bricht nach 60 s ab (Showcase; Default 120 s), der KI-Dienst ruft weiter das Modell auf, und der pg-boss-Retry startet parallel neu: Modellaufrufe und Quota/Kosten doppeln sich, Concurrency-Slots bleiben belegt (Slot-Effekt nicht verifiziert). – Ein Gesamtbudget erzwingen: entweder Timeout pro Versuch = Budget/Versuche, oder eine eigene schmale Retry-Schleife nur für APIError 429/503 mit Deadline. Dazu einen Test für ReadTimeout ergänzen.
[Important] – services/ai/README.md:48 – Dort steht weiterhin „The SDK does not retry; retries belong to the worker (pg-boss)". AI_MODEL_RETRY_ATTEMPTS und AI_MODEL_RETRY_INITIAL_DELAY_SECONDS fehlen in der Env-Tabelle. Die Doku-Entscheidung „Keine langlebige Doku betroffen" ist damit falsch. – Die Betriebsdoku widerspricht dem Code, und die neuen Einstellungen sind nicht auffindbar. – In diesem PR die Zeile 48 korrigieren, zwei Zeilen ergänzen und die Doku-Entscheidung umstellen.
[Important] – CHANGELOG.md – Der #69-Eintrag steht in Commit 9163a41 in PR #73 (gestapelt auf #72), nicht in diesem PR. Die Regel verlangt: im selben PR, nie später. – Wird #72 ohne #73 gemergt, ist sichtbares Verhalten undokumentiert. Die Autorin/der Autor hält den Eintrag selbst für nötig, der PR sagt das Gegenteil. – Die #69-Zeile nach #72 verschieben.
[Note]      – config.py:30-31 / PR-Abschnitt „Risiken" – Der Backoff liegt bei 3 Versuchen bei 1+U(0,1) + 2+U(0,1) = 3–5 s. 5 Versuche sind erlaubt, die Anfangsverzögerung hat keine Obergrenze, und es gibt keinen Abgleich mit dem Worker-Timeout. – Fehlkonfiguration fällt still aus dem Budget. – Die Budgetformel dokumentieren oder begrenzen.
[Note]      – services/ai/tests/test_model_retry.py – Solide: echtes SDK, HTTP-Grenze per MockTransport, beide Pfade, 400/401/403/404/500 jeweils genau 1 Request. Lokal 9/9 grün. CI „check" ist auf 38e9ff3 grün, inkl. „AI eval gate (replay)" (D8 erfüllt). Es fehlen der Timeout-Fall (siehe Blocker) und die 502-model_error-Abbildung auf API-Ebene. Das Mapping ist unverändert, das ist vertretbar. – Keine Maßnahme außer dem Timeout-Test.
[Note]      – gh pr checks – „Vercel – requestflow" ist fail, weil die Web-App-Preview im Dashboard abgebrochen wurde. Das hat nichts mit dem reinen AI-Diff zu tun. Der Live-Nachweis im Showcase ist noch offen. – Vor ready-for-review nachholen und im PR belegen.

Übrige Punkte ohne Befund:

  • Plan-Pflicht „kein Auslöser" ist ehrlich: ein Modul, umkehrbar.
  • „Geändert" passt zum Diff (3 Dateien).
  • Keine neue Dependency: tenacity kommt transitiv über google-genai 2.25.0.
  • Keine Secrets: der Test-Key ist synthetisch.
  • Die Nicht-Ziele sind eingehalten: Queue-Retries und Modell bleiben unverändert.
  • ACs 2–4 sind erfüllt und getestet.
  • Der Klartext ist verständlich, bis auf den oben genannten falschen Satz.

Urteil: changes requested. Wiederholte Timeouts sprengen das Zeitbudget des AC und den Worker-Timeout. Außerdem sind README und CHANGELOG nicht in diesem PR nachgezogen.

Fluory and others added 4 commits September 27, 2026 11:54
The fresh review of #72 showed that the SDK's HttpRetryOptions also
retry timeouts and connection errors, whatever status codes are set: a
hanging model could take three full timeouts (~185 s) while the worker
gave up after 60 s and pg-boss started the job again - duplicate model
calls and cost. A small retry loop now repeats only 429/503, never a
timeout, gives each attempt only the budget that is left and skips a
retry when less than 10 s remain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
README still said the SDK does not retry and lacked the two new
settings; the CHANGELOG entry for #69 lived in the stacked #73 instead
of the PR that changes the behaviour. The showcase runbook now sets the
model budget below the web app's AI timeout so the worker never gives up
while a model call is still running. The initial delay gets an upper
bound so a misconfiguration cannot silently exceed the budget.

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

Fluory commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Merge auf ausdrückliche Anweisung des Orchestrators (2026-09-27: „Merge und deploy“).

@Fluory
Fluory merged commit dd827f8 into main Sep 27, 2026
5 checks passed

This branch was successfully deployed

3 active (1 outdated) deployments
Preview – requestflow-ai — 8110ffca Deployed Sep 27, 2026 by vercel[bot]
Preview – requestflow — 8110ffca Deployed Sep 27, 2026 by vercel[bot]
Production – requestflow-ai — 38e9ff36 Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ai-service): retry transient model errors within one extraction call

1 participant