From 98a1b3c546919cdf70f7146b37f64d9eb90d4a65 Mon Sep 17 00:00:00 2001 From: Kagamine Rin Date: Thu, 4 Jun 2026 15:47:29 -0400 Subject: [PATCH] =?UTF-8?q?LiteLLM=20Dashboard=20Tool=20v1.0.0=20=E2=80=94?= =?UTF-8?q?=20two-function=20OWI=20Tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - litellm_health: proxy health + auth check (strips litellm_metadata) - litellm_cost: cost breakdown by date range (per-model, per-day, cache hit rate) - Valves: litellm_url, litellm_master_key (password field, no hardcoded keys) - Tested against mon-rin1:4000 Gemini proxy — all green - Error handling: auth failure, connection failure, timeout - Architecture doc: Memory Palace KB #596 --- README.md | 44 ++++ __pycache__/litellm_dashboard.cpython-314.pyc | Bin 0 -> 14789 bytes litellm_dashboard.py | 235 ++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/litellm_dashboard.cpython-314.pyc create mode 100644 litellm_dashboard.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c10176 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# LiteLLM Dashboard — Open WebUI Tool + +A simple, two-function Open WebUI Tool for checking LiteLLM proxy health and cost breakdowns. + +## Functions + +### `litellm_health` +Check LiteLLM proxy health and authentication status. Returns which models are healthy/unhealthy, their rate limits, and any auth errors. + +### `litellm_cost` +Get LiteLLM cost breakdown for a date range. Returns per-model and per-day spend, token counts, cache hit rate, and request stats. + +## Valves (Configuration) + +| Valve | Default | Purpose | +|-------|---------|---------| +| `litellm_url` | `http://localhost:4000` | LiteLLM proxy base URL | +| `litellm_master_key` | (empty) | LiteLLM master API key (password field) | + +Both valves are passed at runtime — no hardcoded keys. The tool is portable to any LiteLLM proxy instance. + +## Installation + +1. Copy `litellm_dashboard.py` to your Open WebUI Workspace or import via Admin → Tools +2. Configure the two valves with your LiteLLM proxy URL and master key +3. Enable the tool on a per-model or per-chat basis + +## Requirements + +- `requests` (standard pip package) +- Open WebUI 0.4.0+ +- A running LiteLLM proxy instance + +## Design Principles + +- **Two functions, that's it.** No scope creep. +- **Portable.** No hardcoded URLs or keys. +- **Token-efficient.** Strips `litellm_metadata` (~2KB/model) from health responses. Formats cost data as readable text, not raw JSON dumps. +- **General-purpose.** Works with any LiteLLM proxy instance. +- **Read-only.** GET endpoints only. No mutations, no risk. + +## License + +MIT \ No newline at end of file diff --git a/__pycache__/litellm_dashboard.cpython-314.pyc b/__pycache__/litellm_dashboard.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bb6c2b8cccce4dce26ad35ed5efd7ffdad08ef3 GIT binary patch literal 14789 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

Ar|?_`Q42T)2-1>M$cy_hEZr0tk+ zU~gxyBO}~E845hI9+bU{GB3(zAcK>@JhQ;Desr(r;8Vi*Sq8nG#U&6U02{D#@ZfC1 zA&TsD&CVk_G-I={6erjK%5I@7hB8>J66}{D6Vo$Gdg$4(D?-dfHWhmh5v7Z>){v{E zhowoX2Ga`+UK}q%^NCm>-Q&rF@r&t6NI8XwCW$2Cm03m$nMF{1$u@hk|iupgs3ss+&^RZB-*7I-#-IEGO6mymI2+%`AA7+b+k zoz0|kyrNb9bfOU`pM_PHp-&}O1PdG(gS(}Ktz^$p&(G{Pz9@2JV=qF(QQ^R`J;9e$ zac$hZetyF!6b&rtQ7Gn`A{29JliIdV<_gAK-Wz-Q%zkZe!T%^2vvxc_lPq>ZEXH28 z+Pl)bc79!*w6`u9e^pqTa$*OZ% z3Zd!5#>HgWX&@=7UL9K*6Y7s7-bwT+maAdPS zSw6N@0HW0%h6B_oOHEw0ZK;pnN_72gC{@uYH1|HP=-n864DqDVR8>=aYR$E7U%$Gc zTA$cx6i!VF7b)T7lrZHL%4a?t{i3{PX%v{1S1wsUw^uF=rpl`x815V5mUSvw-U%!m zwW~K)Zp3@x+0nT)oI2e3owiTfzSFT~YhE_5oljLaKbW{bv3@);ldL`t@e_MZ{Q9=N z>x=Tbcv1X(LY3&SJ5WdfASx zD{VZ_Ep1*Va*hGCvDA311W3ijXDTrLPbYHm9)Es;_k$ejiMhpe&|4^W z0in2KeQsB$E;{x*jke%`Bwh(oR~QOGk#p%x>cr5_ ziOH=K;3-#kPF#C*;@a}9ozj-A(w6m##F_2VV}keg(z%qiY{y!=WvxY$UtXJ99*-Bt z&&2P3`$9r1G#*JzK`?Il);CYZ8-(J6@o5Ob&HsJ>hDtcxx6y)L&3*I48ZQ(##m^!6 z+GY8w7}ajc-aw~fUDJoCcM zfDniXj`y~WY|2u;*0pV^mg90&ZR6AMn$a?~4Rb>})Ou~(STEnR zYlq^EYc()F3<*vA%Zb4a^TQtD*vO`4b3~ZFvUyr?0Qq&9Tv=7g^6IvclxfiY3!%Y5 z&HpMicnv~>R~}ifqCns^2n5!`r{Txuic7u3r@iiX3N<@z)sUx58k8SUK@Mv}bd-I< zS-_&t5_=G3%_u{V7>l517A-iXpOc`?xU`<&(A@^g4xT!V5qGq~8F6{@`anYLiO)w3JZUeoh?x52EV1{;EbrJ|$3&S^PKF(j`U! literal 0 HcmV?d00001 diff --git a/litellm_dashboard.py b/litellm_dashboard.py new file mode 100644 index 0000000..64dc3f6 --- /dev/null +++ b/litellm_dashboard.py @@ -0,0 +1,235 @@ +""" +title: LiteLLM Dashboard +author: Kagamine Rin +author_url: https://rin.captcouch.xyz +description: Check LiteLLM proxy health + auth status and get cost breakdowns with configurable timerange. Two functions, that's it. +required_open_webui_version: 0.4.0 +requirements: requests +version: 1.0.0 +licence: MIT +""" + +import requests +from pydantic import BaseModel, Field +from typing import Optional + + +class Tools: + def __init__(self): + self.valves = self.Valves() + + class Valves(BaseModel): + """LiteLLM proxy connection settings.""" + litellm_url: str = Field( + default="http://localhost:4000", + description="LiteLLM proxy base URL (e.g. http://localhost:4000)", + ) + litellm_master_key: str = Field( + default="", + description="LiteLLM master API key for authentication", + json_schema_extra={"input": {"type": "password"}}, + ) + + def _headers(self) -> dict: + """Build auth headers from valves.""" + if not self.valves.litellm_master_key: + return {} + return {"Authorization": f"Bearer {self.valves.litellm_master_key}"} + + def _base_url(self) -> str: + """Get base URL, strip trailing slash.""" + return self.valves.litellm_url.rstrip("/") + + def litellm_health(self) -> str: + """ + Check LiteLLM proxy health and authentication status. + Returns which models are healthy/unhealthy, their rate limits, and any auth errors. + Use this to verify your LiteLLM proxy is up and your master key works. + """ + url = f"{self._base_url()}/health" + headers = self._headers() + + try: + resp = requests.get(url, headers=headers, timeout=15) + except requests.exceptions.ConnectionError: + return f"❌ Connection failed — LiteLLM proxy not reachable at {self._base_url()}" + except requests.exceptions.Timeout: + return f"❌ Connection timed out after 15s — proxy at {self._base_url()} may be overloaded" + + if resp.status_code == 401: + return "❌ Authentication failed — check your litellm_master_key valve. The proxy rejected the key." + if resp.status_code == 403: + return "❌ Forbidden — your key doesn't have permission to access /health" + if resp.status_code != 200: + return f"❌ Unexpected status {resp.status_code}: {resp.text[:500]}" + + data = resp.json() + healthy = data.get("healthy_endpoints", []) + unhealthy = data.get("unhealthy_endpoints", []) + healthy_count = data.get("healthy_count", len(healthy)) + unhealthy_count = data.get("unhealthy_count", len(unhealthy)) + + # Strip litellm_metadata — it's ~2KB per model of auth context we don't need + # Also strip other internal fields that bloat the response + STRIP_KEYS = { + "litellm_metadata", "safety_settings", "cache_control_injection_points", + "use_in_pass_through", "use_litellm_proxy", "merge_reasoning_content_in_choices", + } + + lines = [] + lines.append(f"LiteLLM Health Check — {self._base_url()}") + lines.append("=" * 50) + + if not self.valves.litellm_master_key: + lines.append("⚠️ No master key configured — showing public health only") + else: + lines.append("✅ Authenticated with master key") + + lines.append(f"\nHealthy: {healthy_count} | Unhealthy: {unhealthy_count}") + + if healthy: + lines.append(f"\n--- Healthy Models ({healthy_count}) ---") + for ep in healthy: + model = ep.get("model", "unknown") + # Strip gemini/ prefix for readability + display = model.replace("gemini/", "") if model.startswith("gemini/") else model + rpm = ep.get("rpm", "?") + timeout = ep.get("timeout", "?") + extras = [] + if ep.get("reasoning_effort") and ep["reasoning_effort"] != "none": + extras.append(f"reasoning: {ep['reasoning_effort']}") + if ep.get("service_tier"): + extras.append(f"tier: {ep['service_tier']}") + extra_str = f" ({', '.join(extras)})" if extras else "" + lines.append(f" ✅ {display} — rpm: {rpm}, timeout: {timeout}s{extra_str}") + + if unhealthy: + lines.append(f"\n--- Unhealthy Models ({unhealthy_count}) ---") + for ep in unhealthy: + model = ep.get("model", "unknown") + display = model.replace("gemini/", "") if model.startswith("gemini/") else model + lines.append(f" ❌ {display}") + + if not healthy and not unhealthy: + lines.append("\n(no endpoint data returned)") + + return "\n".join(lines) + + def litellm_cost( + self, + start_date: str, + end_date: str, + ) -> str: + """ + Get LiteLLM cost breakdown for a date range. + :param start_date: Start date in YYYY-MM-DD format (e.g. "2026-06-01") + :param end_date: End date in YYYY-MM-DD format (e.g. "2026-06-04") + Returns formatted cost report with per-model and per-day breakdowns, token counts, cache hit rate, and request stats. + """ + if not self.valves.litellm_master_key: + return "❌ Master key required for cost data. Set the litellm_master_key valve." + + url = f"{self._base_url()}/user/daily/activity" + headers = self._headers() + params = {"start_date": start_date, "end_date": end_date} + + try: + resp = requests.get(url, headers=headers, params=params, timeout=30) + except requests.exceptions.ConnectionError: + return f"❌ Connection failed — LiteLLM proxy not reachable at {self._base_url()}" + except requests.exceptions.Timeout: + return f"❌ Connection timed out after 30s — large date ranges may be slow" + + if resp.status_code == 401: + return "❌ Authentication failed — check your litellm_master_key valve." + if resp.status_code == 403: + return "❌ Forbidden — your key doesn't have permission to access cost data" + if resp.status_code != 200: + return f"❌ Unexpected status {resp.status_code}: {resp.text[:500]}" + + data = resp.json() + results = data.get("results", []) + metadata = data.get("metadata", {}) + + if not results: + return f"No cost data found for {start_date} to {end_date}." + + # Aggregate totals from metadata + total_spend = metadata.get("total_spend", 0) + total_prompt = metadata.get("total_prompt_tokens", 0) + total_completion = metadata.get("total_completion_tokens", 0) + total_tokens = metadata.get("total_tokens", 0) + total_successful = metadata.get("total_successful_requests", 0) + total_failed = metadata.get("total_failed_requests", 0) + total_api = metadata.get("total_api_requests", 0) + cache_read = metadata.get("total_cache_read_input_tokens", 0) + cache_created = metadata.get("total_cache_creation_input_tokens", 0) + + # Cache hit rate + total_input_tokens = total_prompt # cache_read is a subset of prompt + if total_input_tokens > 0 and cache_read > 0: + cache_pct = (cache_read / total_input_tokens) * 100 + cache_str = f"{cache_pct:.1f}% ({cache_read:,} / {total_input_tokens:,} input tokens)" + else: + cache_str = "N/A" + + # Build per-model breakdown across all dates + model_totals = {} + for day in results: + breakdown = day.get("breakdown", {}) + models = breakdown.get("models", {}) + for model_name, model_data in models.items(): + m_metrics = model_data.get("metrics", {}) + if model_name not in model_totals: + model_totals[model_name] = { + "spend": 0, "requests": 0, "prompt": 0, + "completion": 0, "total": 0, "failed": 0, + } + model_totals[model_name]["spend"] += m_metrics.get("spend", 0) + model_totals[model_name]["requests"] += m_metrics.get("successful_requests", 0) + model_totals[model_name]["failed"] += m_metrics.get("failed_requests", 0) + model_totals[model_name]["prompt"] += m_metrics.get("prompt_tokens", 0) + model_totals[model_name]["completion"] += m_metrics.get("completion_tokens", 0) + model_totals[model_name]["total"] += m_metrics.get("total_tokens", 0) + + # Build per-date breakdown + date_lines = [] + for day in results: + date = day.get("date", "?") + m = day.get("metrics", {}) + date_lines.append( + f" {date}: ${m.get('spend', 0):.2f} | " + f"{m.get('successful_requests', 0)} ok, {m.get('failed_requests', 0)} fail | " + f"{m.get('total_tokens', 0):,} tokens" + ) + + # Build model lines sorted by spend + model_lines = [] + for model_name in sorted(model_totals, key=lambda k: model_totals[k]["spend"], reverse=True): + mt = model_totals[model_name] + display = model_name.replace("gemini/", "") if model_name.startswith("gemini/") else model_name + model_lines.append( + f" {display:30s} ${mt['spend']:8.2f} | " + f"{mt['requests']:3d} ok, {mt['failed']:d} fail | " + f"{mt['total']:>12,} tokens" + ) + + # Assemble report + lines = [] + lines.append(f"LiteLLM Cost Report: {start_date} to {end_date}") + lines.append("=" * 60) + lines.append(f"Total Spend: ${total_spend:.2f}") + lines.append(f"Total Tokens: {total_tokens:,} ({total_prompt:,} prompt + {total_completion:,} completion)") + lines.append(f"Cache Hit Rate: {cache_str}") + lines.append(f"Requests: {total_successful} successful, {total_failed} failed ({total_api} total)") + if cache_created > 0: + lines.append(f"Cache Created: {cache_created:,} tokens") + lines.append("") + lines.append("By Model:") + lines.append(f' {"Model":30s} {"Spend":>9s} | {"Requests":>14s} | {"Tokens":>12s}') + lines.extend(model_lines) + lines.append("") + lines.append("By Date:") + lines.extend(date_lines) + + return "\n".join(lines) \ No newline at end of file