fix(eu): Kapazitaetsengpaesse bei Bedrock aussitzen statt Schritte verlieren

Am 01.08.2026 lieferte Bedrock waehrend eines Laufs ServiceUnavailableException.
Botocore versuchte es dreimal in fuenf Sekunden und gab auf, die
Web-Source-Selektion fiel ersatzlos aus. Im Bericht war davon nichts zu sehen,
der Lauf galt als vollstaendig. Gemessen sind es zwei betroffene Aufrufe bei
rund 340, also 0,6 Prozent, aber jeder Vorfall kostet einen ganzen
Pipeline-Schritt.

Der Fehler ist auch keiner unserer Quote. ServiceUnavailableException ist ein
503, AWS hat also gerade keine Kapazitaet fuer das Modell. Unsere Meldung
sagte trotzdem "Rate-Limit", weil der Code beide Faelle in einen Topf warf.

Aendert sich damit:

1. Geduldiges Wiederholen. Kapazitaets- (503), Kontingent- (429) und
   Verbindungsfehler werden nach 5, 15 und 30 Sekunden erneut versucht. Diese
   Dellen dauern typischerweise unter einer Minute, unsere bisherigen fuenf
   Sekunden lagen genau im ungeguenstigsten Fenster. Stufen ueber
   BEDROCK_RETRY_WAITS einstellbar.

2. Zwei Bremsen gegen lange Laeufe. Gewartet wird nur, wenn die Wartezeit ins
   Zeitbudget des Aufrufs passt (die Wartezeit zaehlt gegen dasselbe
   asyncio-Budget, und die Haiku-Planungsaufrufe haben nur 120 Sekunden), und
   nur solange das Wartebudget des Refreshs reicht (BEDROCK_RETRY_BUDGET_S,
   Vorgabe 120 Sekunden). Ein Lauf kann sich dadurch um hoechstens zwei
   Minuten verlaengern.

3. Ursachen werden getrennt benannt. Im Log steht jetzt "hat keine freie
   Kapazitaet" oder "meldet ausgeschoepftes Kontingent" statt pauschal
   "Rate-Limit". Nach aussen bleibt beides die Kategorie rate_limit, damit die
   Retry-Steuerung des Orchestrators unveraendert greift.

4. Stoerungen werden sichtbar. Waehrend einer Wartezeit meldet die Oberflaeche
   "KI-Dienst hat keine freie Kapazitaet, neuer Versuch in 15 Sekunden", sonst
   sieht die stehende Anzeige wie ein Haenger aus. Nach dem Lauf steht eine
   Zusammenfassung im Refresh-Protokoll, auch wenn der Lauf sonst glatt
   durchlief.

5. botocore laeuft im Modus 'adaptive' statt 'standard'. Es bremst sich bei
   Drosselung selbst ein und deckt die Sekunden ab, unsere Schleife die
   Zehnersekunden.

Zeitliche Wirkung, gemessen am EU-Lauf der Ceuta-Lage (252 Sekunden, 15
Bedrock-Aufrufe): im stoerungsfreien Fall null, im betroffenen Lauf rund eine
Minute mehr. Bei 0,6 Prozent Fehlerrate je Aufruf trifft das etwa jeden
elften Lauf, im Mittel ueber alle Laeufe rund zwei Prozent.

Gegenprobe mit einem echten Bedrock-Aufruf ueber den geaenderten Pfad: Antwort,
Verbrauch und Kosten unveraendert, keine Stoerung protokolliert.

Neu sind 23 Pruefungen, insgesamt laufen 179 ohne Netzzugriff und ohne Kosten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-08-01 22:58:50 +02:00
Ursprung 3495409377
Commit 0154c0f575
5 geänderte Dateien mit 490 neuen und 25 gelöschten Zeilen

Datei anzeigen

@@ -12,6 +12,7 @@ from urllib.parse import urlparse, urlunparse, quote_plus
import httpx
from agents.claude_client import UsageAccumulator, _cancel_event_var, _ai_backend_var
from agents import bedrock_client
from agents.factchecker import find_matching_claim, deduplicate_new_facts, TWOPHASE_MIN_FACTS
from source_rules import (
_detect_category,
@@ -878,6 +879,24 @@ class AgentOrchestrator:
_ai_backend_var.set(ai_backend)
if ai_backend != "cli":
logger.info("Lage %s läuft über KI-Backend '%s'", incident_id, ai_backend)
# Wartebudget des EU-Wegs zuruecksetzen und die Oberflaeche an die
# Wartemeldungen anschliessen. Ohne diesen Hinweis sieht eine
# Wiederholung nach einem Kapazitaetsengpass wie ein Haenger aus,
# weil die Anzeige waehrend der Wartezeit stillsteht.
bedrock_client.refresh_kontext_starten()
if self._ws_manager:
def _warte_melden(art: str, sekunden: float, _iid=incident_id,
_vis=visibility, _cb=created_by, _tid=tenant_id) -> None:
text = bedrock_client._ART_TEXT.get(art, art)
asyncio.create_task(self._ws_manager.broadcast_for_incident({
"type": "status_update",
"incident_id": _iid,
"data": {
"status": "running",
"detail": f"KI-Dienst {text}, neuer Versuch in {sekunden:.0f} Sekunden...",
},
}, _vis, _cb, _tid))
bedrock_client.wartemelder_setzen(_warte_melden)
output_language_iso = await get_org_language(db, tenant_id) if tenant_id else "de"
output_language = language_display(output_language_iso)
# research_language steuert nur den WebSearch-Prompt ("suche in Sprache X").
@@ -2200,18 +2219,24 @@ class AgentOrchestrator:
if _missing_key not in _started_keys:
await _pipe_skip(_missing_key)
# Refresh-Log abschließen (mit Token-Statistiken)
# Refresh-Log abschließen (mit Token-Statistiken). Störungen des
# KI-Dienstes werden auch bei einem erfolgreichen Lauf vermerkt,
# sonst sieht ein Durchlauf mit Aussetzern aus wie ein glatter.
stoerungshinweis = bedrock_client.stoerungen_zusammenfassen() or None
if stoerungshinweis:
logger.info("Refresh-Protokoll Lage %s: %s", incident_id, stoerungshinweis)
await db.execute(
"""UPDATE refresh_log SET
completed_at = ?, articles_found = ?, status = 'completed',
input_tokens = ?, output_tokens = ?,
cache_creation_tokens = ?, cache_read_tokens = ?,
total_cost_usd = ?, api_calls = ?
total_cost_usd = ?, api_calls = ?, error_message = ?
WHERE id = ?""",
(datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'), new_count,
usage_acc.input_tokens, usage_acc.output_tokens,
usage_acc.cache_creation_tokens, usage_acc.cache_read_tokens,
round(usage_acc.total_cost_usd, 7), usage_acc.call_count, log_id),
round(usage_acc.total_cost_usd, 7), usage_acc.call_count,
stoerungshinweis, log_id),
)
await db.commit()
logger.info(