Bedrock-LLM-Workload Cost-Audit — Checkliste
Reusable fuer jeden Bedrock-LLM-Workload (Open-WebUI + LiteLLM + Bedrock, oder generisch jeder LLM-Proxy). Reihenfolge: zuerst die Mess-Schritte (Punkt 1-3), dann die Hebel (Punkt 4-10). Wer alle 10 Punkte gruen hat ist optimiert — bei den meisten Stacks sind 3-5 Punkte offen.
1. Token-Aufschluesselung pro Modell ziehen
# pro Modell: Cache Read / Cache Write / Plain Input / Output
aws ce get-cost-and-usage \
--time-period Start=YYYY-MM-DD,End=YYYY-MM-DD \
--granularity DAILY \
--metrics UnblendedCost UsageQuantity \
--group-by Type=DIMENSION,Key=USAGE_TYPE \
--filter '{"Dimensions":{"Key":"SERVICE","Values":["Claude Sonnet 4.6 (Amazon Bedrock Edition)"]}}' \
--region us-east-1 --profile <mgmt>Erwartete USAGE_TYPE-Splits in eu-central-1:
EUC1_CacheReadInputTokenCount-Units— gecached, ~90% billigerEUC1_CacheWriteInputTokenCount-Units— Cache-Erstellung, ~25% teurer als PlainEUC1_InputTokenCount-Units— Plain Input (uncached)EUC1_OutputTokenCount-Units— Output
Cache-Hit-Rate = Cache Read / (Cache Read + Plain Input). Erwartung bei gutem Caching: 70-90%. <40% = problem.
2. Klassifikator-Routing-Verteilung messen
Wenn der Stack einen LiteLLM-Pre-Call-Hook fuer dynamisches Modell-Routing hat (Sonnet → Haiku/Opus): pruefen ob die Verteilung gesund ist.
# CloudWatch Logs Insights
aws logs start-query --profile <prod> --region <region> \
--log-group-name <log-group> \
--start-time <unix-24h-ago> --end-time <unix-now> \
--query-string 'fields @message | filter @message like /vf_classify_route:/ and @message like /->/ | parse @message /-> (?<routed>\S+) \(cls=(?<cls>\w+)\)/ | stats count() by cls'Erwartung gesunder Stack: ROUTE 20-40% / REASON 40-60% / PLAN 5-15%. Wenn ROUTE <10%: Klassifikator-System-Prompt scharfstellen.
Trap: logger.info darf NICHT konditional auf new_model != data["model"] triggern — sonst sind REASON-Decisions (=Sonnet) silent und die Verteilung ist nicht messbar. Always-Log.
3. Klassifikator-Failure-Rate pruefen
# Bedrock-RateLimit-Errors im Pre-Hook
aws logs start-query --profile <prod> --region <region> \
--log-group-name <log-group> \
--start-time <unix-24h-ago> --end-time <unix-now> \
--query-string 'fields @message | filter @message like /classifier failed/ | stats count()'Jedes Failure = Call faellt auf Default-Modell (typisch Sonnet) zurueck. Bei >50 Failures/Tag: Bedrock-Quota erhoehen via AWS-Support oder Klassifikator-Cache (LiteLLM cache mit TTL pro User-Message-Hash).
4. Prompt-Caching aktivieren + korrekt strukturieren (groesster Hebel)
LiteLLM-Config:
litellm_settings:
cache_control_injection_points:
- location: message
role: system
model_list:
- model_name: ...
litellm_params: ...
model_info:
supports_prompt_caching: true
cache_creation_input_token_cost: <wert>
cache_read_input_token_cost: <wert>Aber: Aktivieren reicht nicht — der System-Prompt muss strukturiert sein!
cache_control_injection_points: role: system setzt den Cache-Marker am ENDE der system-Message. Alles davor wird gecached, alles dahinter nicht. Heisst:
✅ GUT: stabiler Block (Identitaet, Stammdaten, Werkzeug-Logik, Output-Stil) am Anfang, dynamische Variables ({{CURRENT_DATE}}, {{USER_NAME}}, etc.) am ABSOLUTE Ende.
❌ CACHE-KILLER: dynamische Variables irgendwo MITTENDRIN. Dann ist der gesamte System-Prompt-Block dirty pro Request.
Anti-Patterns (Cache-Killer)
| Pattern | Warum kaputt |
|---|---|
{{CURRENT_TIME}} mit Sekunden-Praezision | Pro Request neuer Wert → Cache 0% Hit |
<kontext> mit Date+User in Zeile 50 von 400 | Stabiler Suffix-Block (350 Zeilen) wird durch dynamic Prefix invalidiert |
| User-Email/Rolle vor Werkzeug-Doku | Pro User-Login andere Cache-Bloecke noetig, viel cold-miss |
Datum als Markdown-Heading ## Heute: 18.05.2026 | Selbe Problematik wie Zeitstempel |
Pattern (Cache-Optimized)
<identitaet> ← stabil, wird gecached
<vf_stammdaten> ← stabil
<werkzeug_prioritaet> ← stabil
<output_stil> ← stabil
<!-- cache_control wird hier oder am Ende automatisch injected -->
<session> ← dynamisch, kleiner Suffix
Heute ist {{CURRENT_DATE}} ({{CURRENT_WEEKDAY}}). Du sprichst mit {{USER_NAME}}.
</session>
Quelle: _index — VF-Audit fand Cache-Hit-Rate 17% mit {{CURRENT_TIME}} mittendrin. Nach Restructure auf <session>-am-Ende erwartet 70-80%.
5. Dynamisches Modell-Routing via Pre-Hook
Sonnet ist 3x teurer als Haiku (1/M Input, 5/M Output). Wenn ein nicht-trivialer Anteil der User-Anfragen mit Haiku-Quality erfuellt wird (ROUTE-Anteil): VFClassifyRouter-Pattern.
LiteLLM-Pre-Call-Hook der User-Anfrage in ROUTE/REASON/PLAN klassifiziert (via Haiku-Klassifikator-Call, max_tokens=5, timeout 5s) und dynamisch das Backend-Modell waehlt. Default fallback: REASON = Sonnet.
Source-Pattern: ~/source/apps/open-webui-vf/infra/lib/open-webui-vf-stack.ts Z. 600-700 (VFClassifyRouter). Wiederverwendbar fuer jeden LiteLLM-Proxy.
6. Auxiliary-Task-Modell auf Haiku
Open-WebUI hat TASK_MODEL + TASK_MODEL_EXTERNAL Env-Vars fuer interne Aufgaben (Chat-Titel-Generation, Tag-Generation, Search-Query-Formulierung). Default ist claude-sonnet-4-6 — sollte auf Haiku zeigen (1-2-Saetze-Output, Quality reicht).
TASK_MODEL: 'vf-haiku-backend'
TASK_MODEL_EXTERNAL: 'vf-haiku-backend'Caveat: Wenn das Task-Modell den Pre-Klassifikator triggern wuerde (TRIGGER_MODEL ist Sonnet), unnoetiger Round-Trip. Backend-Model direkt ansprechen (umgeht Klassifikator).
7. ECS-Task-Rightsizing
Default Open-WebUI-Stack: 1 vCPU / 3 GB. Bei 3 Container-Setup (open-webui + litellm + cloudflared) teilen die sich das. Bei niedriger User-Last (<5 aktive User): 0,5 vCPU / 2 GB reicht oft → 50% Cost-Reduktion auf Fargate.
Pruefen vor Rightsizing: CloudWatch Container-Insights → CPU/Memory-Utilization avg. Wenn <40% idle Avg: rightsizen. Wenn 60-80%: lassen wie ist.
mcp-vf-hosted ist meist Min-Size (256/512) — FastMCP ist mostly async I/O.
8. CloudWatch-Retention konfigurieren
Default ONE_MONTH (30d) bei jeder LogGroup. Pro Stack pruefen:
- App-LogGroup mit Audit-Logs (
AUDIT_LOG_LEVEL: METADATA): 14d Compromise zwischen Compliance + Storage-Cost - Service-LogGroup ohne Audit (Tool-Health, Tunnel-Status): 7d reicht
LITELLM_LOG-Level: DEBUG (oft default fuer Bring-Up) → INFO nach Sign-Off. DEBUG-Logs sind 5-10x mehr Volumen.
9. ECR-Lifecycle-Policies
Pro Repo: Keep last 10 tagged images + Expire untagged after 7 days. Verhindert Image-Tag-Wildwuchs der ECR-Storage-Cost erhoeht.
aws ecr put-lifecycle-policy \
--repository-name <repo> \
--lifecycle-policy-text file://lifecycle.json \
--profile <prod> --region <region>10. Stale Secrets aufraeumen
Secrets Manager kostet $0,40/Secret/Monat. Quartalsweise Audit:
aws secretsmanager list-secrets --profile <prod> --region <region> \
--query 'SecretList[?LastAccessedDate==`null` || LastAccessedDate<`2026-04-01`].{name:Name,lastChanged:LastChangedDate,lastAccessed:LastAccessedDate}' \
--output tableLoeschen mit 7d-Recovery-Window:
aws secretsmanager delete-secret --secret-id <name> --recovery-window-in-days 7 --profile <prod>Sofort-Pruef-Liste (5 Min)
Bei neuem Bedrock-LLM-Workload diese 5 Bedrock-spezifischen Punkte:
- Cache-Hit-Rate >40%? (Sonst Punkt 4 forensisch — System-Prompt-Restructure)
- Klassifikator vorhanden + jede Decision wird geloggt? (Sonst Punkt 5)
- TASK_MODEL auf Haiku oder generell guenstigeres Modell? (Punkt 6)
-
function_calling: 'native'im DEFAULT_MODEL_PARAMS? (Sonst KV-Cache invalidiert pro Tool-Call-Runde) -
thinking.budget_tokensgecappt? (Sonst unsichtbares Thinking-Token-Burning)
Verlauf
- 2026-05-18 Erstausroll aus VF-Audit (_index). Punkt 4 (Cache-Killer Sekunden-Timestamp im System-Prompt) war der ueberraschendste Treffer — Cache-Hit-Rate 17% statt erwartet 70-80%, ~$215/Mo verschenkt.
Related
- _index — Erst-Anwendung dieser Checkliste
- _index — org-weites Cost-Projekt (10-Hebel-Skill, 6-Phasen)
- mcp-best-practices — verwandt fuer MCP-Stacks
- open-webui-fargate-bedrock — Stack-Pattern fuer Open-WebUI auf AWS