Welle 1 Tag 4 — Routing-Hook-Handoff

Stand zum 2026-05-17 abends

  • Branch: feat/welle1-tag4-backend-routing in ~/source/apps/open-webui-vf/, noch nicht committed, working tree hat den ganzen Tag-4-Code drin.
  • Production: 3 mal deployed (Revisions 17 failed, 18 silent, 19 live mit DEBUG-Logs). Service HEALTHY.
  • Drei Bedrock-Modelle exposed in LiteLLM (claude-sonnet-4-6, vf-haiku-backend, vf-opus-backend). MODEL_FILTER_LIST versteckt die Backend-Modelle vor User.
  • CloudWatch-Alarm scaffolding (LogMetricFilter vf-classifier-failed + SNS-Alarm an av-ops-alerts) angelegt — feuert aber nie weil Hook nicht laeuft.
  • AVV + ZDR-Mail erweitert um Haiku 4.5 + Opus 4.7 (siehe avv-2026-openwebui-vf + 2026-05-17-zdr-addendum-anfrage).

Problem: Der Pre-Klassifikator-Hook VFClassifyRouter wird sauber geladen (taucht in litellm.callbacks auf), aber async_pre_call_hook wird bei OWUI-/chat/completions-Anfragen nie aufgerufen. Sonnet wird direkt angesprochen, kein Klassifikator-Routing, keine Routing-Entscheidung in Logs.

Was wir verifiziert haben (= nicht das Problem)

  1. Hook-Code laedt sauber. Modul importiert, Klasse instantiiert, proxy_handler_instance exposed. Validiert via lokalem importlib.util.spec_from_file_location Trockenlauf gegen das synthetisierte CloudFormation-Template.
  2. LiteLLM-Imports stimmen (litellm.integrations.custom_logger.CustomLogger, litellm.proxy._types.UserAPIKeyAuth, litellm.caching.caching.DualCache). Lokal in pip install litellm venv getestet, alle Pfade existieren.
  3. Config-File wird geparst und Hook landed in litellm.callbacks. Lokal mit identischem YAML + initialize_callbacks_on_proxy() reproduziert — Output: [..., <vf_classify_route.VFClassifyRouter object at 0x...>]. Im Production-DEBUG-Log sichtbar als Initialized Callbacks - [<vf_classify_route.VFClassifyRouter ...>].
  4. Alle 4 Gate-Bedingungen aus utils.py:1432-1438 passen lokal:
    • isinstance(_callback, CustomLogger) → True
    • "async_pre_call_hook" in vars(_callback.__class__) → True (vars(VFClassifyRouter) == ['async_pre_call_hook'])
    • _callback.__class__.async_pre_call_hook != CustomLogger.async_pre_call_hook → True
    • call_type ist nicht call_mcp_tool → True

Was wir NICHT verstanden haben (= das eigentliche Problem)

  • proxy_logging_obj.pre_call_hook wird im Container offenbar NIE aufgerufen fuer /chat/completions von OWUI.
  • Indiz: KEIN PROXY_PRE_CALL-Log-Eintrag, KEIN service_logging_obj.async_service_success_hook Eintrag (das wuerde utils.py:1461 machen sobald irgendein Hook in der Schleife durchlaeuft, > 10ms).
  • Auch die anderen built-in pre_call_hooks (_PROXY_MaxBudgetLimiter, _PROXY_MaxParallelRequestsHandler_v3, _PROXY_MaxIterationsHandler) zeigen keine Hook-Activity in den Logs — nur ihren callback_controls.py:38 “is disabled” Check. Das ist eine viel oberflaechlichere Schicht als der Pre-Call-Hook-Loop.
  • Code-Pfad sollte sein: chat_completion() (proxy_server.py:8106) → ProxyBaseLLMRequestProcessing.base_process_llm_request() (common_request_processing.py:1001) → proxy_logging_obj.pre_call_hook() (Zeile 943).

Drei Hypothesen die getestet werden muessen:

  1. Pre-Call-Hooks brauchen einen anderen Registrierungs-Mechanismus als litellm_settings.callbacks. Vielleicht muss man general_settings.proxy_hook_modules setzen oder den Hook ueber litellm.proxy.proxy_server.proxy_logging_obj.callbacks.append() explizit registrieren statt nur in litellm.callbacks.
  2. LITELLM_MODE=PRODUCTION skippt Pre-Call-Hooks aus Performance-Gruenden. Aktuelle Container-env hat das gesetzt (open-webui-vf-stack.ts LiteLLM-Container environment.LITELLM_MODE). Wenn man das auf DEVELOPMENT oder weglaesst, koennte der Hook greifen.
  3. OWUI nutzt einen anderen Code-Pfad (z.B. /v1/messages Anthropic-Format oder eine direkte Router-Bypass-Route), der base_process_llm_request umgeht. Lass uns die exakte Request-URL pruefen die OWUI zu LiteLLM schickt — das war in den Logs nicht explizit sichtbar.

Auftrag fuer naechste Session

Phase 1 — Diagnose (max 90 Min, dann stoppen):

  1. Lokal in open-webui-vf-stack.ts das LiteLLM-Container-Command rausholen, identisches Setup in einem Test-Container starten. Eine Test-Anfrage gegen /v1/chat/completions schicken und mit breakpoint() ODER mit LITELLM_LOG=DEBUG + verbose_logger.debug(...) Patches in litellm.proxy.utils.pre_call_hook rausfinden ob die Methode aufgerufen wird.
  2. Pruefen welche konkrete URL OWUI an LiteLLM schickt fuer Chat-Completions (sollte POST /v1/chat/completions sein, aber Verifikation). CloudWatch-Logs nach `“POST /v1/” filter pattern haben das gezeigt — aber ich war nicht 100% sicher.
  3. Pruefen ob LITELLM_MODE=PRODUCTION einen Side-Effect auf pre_call_hooks hat: GitHub-Issue-Search auf BerriAI/litellm mit Stichworten pre_call_hook PRODUCTION mode silent + callbacks not triggering custom_logger.
  4. Falls Diagnose abschliessend zeigt dass pre_call_hook einfach nicht aufgerufen wird (Bug oder Design): direkt zu Phase 3.

Phase 2 — Alternative-Mechanismen evaluieren (parallel, max 60 Min):

Folgende Alternativen mit Aufwand + Risiko + Stack-Fit bewerten:

AlternativeWieAufwandStack-FitRisiko
A. Guardrails-API statt callbacksHook als litellm_settings.guardrails: [{guardrail_name: vf-classify, callbacks: [vf_classify_route.proxy_handler_instance], mode: pre_call}] registrieren. Das geht durch _maybe_execute_pipelines was im Code VOR dem callback-Loop kommt.30 Mingut — same codemittel — Guardrails-API tendenziell stabil aber semantisch fuer Moderation, evtl Edge-Cases
B. LiteLLM Router-RulesLiteLLM hat eingebauten Router mit routing_strategy und Model-Aliases. Statt Hook: definiere vf-sonnet als Router-Group mit 3 Modellen + Routing-Rule “wenn user_message matched X dann Y”.1-2 Stdsehr gut — native Featureniedrig — etabliertes Pattern
C. Pre-Service vor LiteLLMFastAPI-Container ZWISCHEN OWUI und LiteLLM. Macht den Klassifikator-Call, modifiziert data["model"], forwarded zu LiteLLM. Stack-Sidecar wie Cloudflared.1-2 Tagesehr gut — voll kontrolliert, kein LiteLLM-Magicniedrig
D. Custom Model in OWUI mit LogicOWUI’s Custom-Model vf-sonnet koennte ein “Filter”-Plugin haben das pre-completion eine Routing-Entscheidung trifft. OWUI hat Plugin-System fuer Filters.2-3 Stdmittel — OWUI-Plugin-Wartungmittel — OWUI-Updates koennten Plugin-API brechen
E. Routing-Logic in System-PromptKEIN externer Klassifikator. Stattdessen: System-Prompt enthaelt Anweisung “wenn die Anfrage trivial ist, antworte kurz; wenn komplex, denke laenger nach”. Sonnet macht beides selber.0 Mintrivialhoch — keine echte Cost-Optimierung, nur UX
F. Aufgeben fuer Welle 1Tag 4 = nur Modelle exposed + Alarm-Infra. Routing zur Welle 2 vertagen.5 Minn/an/a

Phase 3 — Entscheidung + Umsetzung:

Marvin praesentieren: gefundenes Root-Cause + die 2-3 sinnvollsten Optionen mit konkretem Effort. Empfehlung mit Begruendung. Erst nach Marvins OK umsetzen.

Wichtige Files / Pfade

DateiZweck
~/source/apps/open-webui-vf/infra/lib/open-webui-vf-stack.tsCDK-Stack mit dem LiteLLM-Container-Command und dem inline-Heredoc Hook-Code
/private/tmp/litellm-test/venv mit litellm==1.85.0 fuer lokale Repro (gleiche Version wie Container)
/Users/marvinkuehlmann/source/apps/open-webui-vf/infra/cdk.out/OpenWebUiVf.template.jsonSynthesized template, hier kann man command[0] extrahieren um den Heredoc-Inhalt 1:1 zu rekonstruieren
CloudWatch LogGroup /aws/ecs/default/open-webui-vfLive-Logs. Filter vf_classify_route zeigt Hook-Init. Filter _PROXY_MaxBudgetLimiter als built-in-Vergleich.
ECS Cluster default, Service open-webui-vf, TaskDef revision 19Aktuell laufende Version mit Tag-4-Code + DEBUG-Logs
AWS Profile av-prod (= av-production)Account 425924867359, eu-central-1
welle-1-perfektion (Tag 4, Zeilen 530-668)Original-Plan, beschreibt das gewuenschte Verhalten

Was NICHT angefasst werden sollte

  • Die bereits-deployten Aenderungen (Modelle, MODEL_FILTER, Alarm, AVV/ZDR-Doku-Updates) sind alle korrekt und sollen drinbleiben. Nur der Hook ist defekt.
  • DEBUG-Log-Level (LITELLM_LOG=DEBUG im Container) kann nach Closure auf INFO zurueck, aber waehrend der Investigation bitte drin lassen.
  • AVV + ZDR-Mail-Drafts sind so an Marvin freigegeben — bitte nicht selbst veraendern.

Done-Kriterien fuer naechste Session

  • Entweder: Pre-Klassifikator triggert nachweislich (CloudWatch zeigt vf_classify_route: claude-sonnet-4-6 -> vf-haiku-backend (cls=ROUTE) bei einer trivialen Test-Anfrage) UND alle 5 Routing-Eval-Cases (021-025) zeigen Treffer-Rate > 80 %.
  • Oder: Marvin hat entschieden welche Alternative (B/C/D/E/F) gewaehlt wird und der entsprechende Branch/Commit ist sauber.
  • Plus: Welle-1-Run-Akte in intern/runs/2026-05-XX-welle-1-completion/ mit Lessons-Learned.

Lessons-Learned die JETZT schon sicher sind

  1. LiteLLM-Pre-Call-Hooks sind in v1.85 nicht-trivial zu verdrahten. Doku zeigt scalar callbacks: my.module.instance, aber das fuehrt nicht zwingend dazu dass der Hook auch aufgerufen wird. Naechstes Mal: vor Code-Schreiben einen Smoke-Test mit der minimalsten Hook-Klasse (logger.info im Hook) machen.
  2. Pre-Check-Pattern mit importlib.util.spec_from_file_location ist gut — fing v1 (falscher Pfad) und v2 (falsche Imports) frueh ab. Beibehalten.
  3. set -eu im Container-Command rettete uns mehrfach vor stillen Halb-Crashes. Pattern fuer alle zukuenftigen Inline-Heredoc-Container.
  4. Cross-Reviewer-Agreement aus ce:review ist wertvoll — adv-005 (set -eu) hat genau dieses Problem vorhergesagt das im v1-Deploy auftrat.
  5. MODEL_FILTER_LIST ist robuster als API-Hiding — deklarativ, kein Race-Window, kein Admin-Key-Bedarf.