Block B: ClaudeCliError + differenzierte HTTP-Status + Rate-Limit-Retry

- Neue Exception-Klasse ClaudeCliError(error_type, message) in claude_client.py
  mit Kategorien rate_limit / auth_error / timeout / cli_error.
- _classify_cli_error() als geteilter Klassifikator (Keywords fuer Rate-Limit
  und Auth-Fehler wie "does not have access", "login again").
- call_claude() erkennt jetzt auch is_error=true im JSON bei returncode=0
  (Hauptursache des Ausfalls vom 22.04.: CLI liefert "Your organization
  does not have access" mit is_error=true statt Exit-Code).
- Orchestrator: ClaudeCliError mit rate_limit/timeout als transient behandelt
  (3 Retries mit Backoff 0s/120s/300s). auth_error/cli_error brechen sofort
  ab ohne Retry. Behebt den bestehenden Bug, dass Rate-Limit-Fehler gar nicht
  retried wurden.
- routers/incidents.py Enhance-Endpoint: ClaudeCliError wird auf
  503 (auth_error) / 429 (rate_limit) gemappt, TimeoutError auf 504.
- routers/chat.py _call_claude_chat(): wirft jetzt ClaudeCliError statt
  generischem RuntimeError. Chat-Endpoint mappt auth_error auf 503.
- Frontend: neue ApiError-Klasse in api.js mit status+detail.
  generateDescription() in app.js zeigt differenzierte Toasts nach
  HTTP-Status (503/429/504/403).
- dashboard.html: Cache-Bust api.js + app.js auf v=20260423a
Dieser Commit ist enthalten in:
claude-dev
2026-04-23 17:54:13 +00:00
Ursprung e8ac0d0c50
Commit 5d5ec7c924
7 geänderte Dateien mit 151 neuen und 51 gelöschten Zeilen

Datei anzeigen

@@ -245,7 +245,7 @@ async def enhance_description(
db: aiosqlite.Connection = Depends(db_dependency),
):
"""Generiert eine strukturierte Beschreibung per KI aus dem Titel."""
from agents.claude_client import call_claude
from agents.claude_client import call_claude, ClaudeCliError
from config import CLAUDE_MODEL_FAST
from services.license_service import charge_usage_to_tenant
@@ -255,17 +255,30 @@ async def enhance_description(
try:
result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST, raw_text=True)
_enhance_logger.info(
f"Beschreibung generiert fuer \"{data.title[:50]}\": "
f"{usage.input_tokens}in/{usage.output_tokens}out"
)
await charge_usage_to_tenant(db, current_user.get("tenant_id"), usage, source="enhance")
await db.commit()
return {"description": result.strip()}
except ClaudeCliError as e:
_enhance_logger.error(f"Beschreibung generieren: ClaudeCliError [{e.error_type}]: {e.message}")
if e.error_type == "auth_error":
raise HTTPException(status_code=503, detail="KI-Zugang aktuell nicht verfuegbar. Bitte Administrator kontaktieren.")
if e.error_type == "rate_limit":
raise HTTPException(status_code=429, detail="KI ist gerade ausgelastet. Bitte in einer Minute erneut versuchen.")
raise HTTPException(status_code=500, detail="Beschreibung konnte nicht generiert werden")
except TimeoutError:
_enhance_logger.error("Beschreibung generieren: Timeout")
raise HTTPException(status_code=504, detail="Die KI antwortet gerade nicht. Bitte erneut versuchen.")
except HTTPException:
raise
except Exception as e:
_enhance_logger.error(f"Beschreibung generieren fehlgeschlagen: {e}")
raise HTTPException(status_code=500, detail="Beschreibung konnte nicht generiert werden")
_enhance_logger.info(
f"Beschreibung generiert fuer \"{data.title[:50]}\": "
f"{usage.input_tokens}in/{usage.output_tokens}out"
)
await charge_usage_to_tenant(db, current_user.get("tenant_id"), usage, source="enhance")
await db.commit()
return {"description": result.strip()}
@router.get("/{incident_id}", response_model=IncidentResponse)
async def get_incident(