From 48fa4a0f06c2ed5f6d31cfae58e8184102142aa3 Mon Sep 17 00:00:00 2001 From: mateo Date: Tue, 11 Aug 2026 02:40:14 +0000 Subject: [PATCH] fix(ui): treat router redis as configured for the no-redis banner Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../skills/testing-admin-ui-banners/SKILL.md | 75 +++++++++++++++++++ .../health_endpoints/_health_endpoints.py | 12 ++- .../health_endpoints/test_health_endpoints.py | 44 +++++++++-- 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 .agents/skills/testing-admin-ui-banners/SKILL.md diff --git a/.agents/skills/testing-admin-ui-banners/SKILL.md b/.agents/skills/testing-admin-ui-banners/SKILL.md new file mode 100644 index 00000000000..615229d4a52 --- /dev/null +++ b/.agents/skills/testing-admin-ui-banners/SKILL.md @@ -0,0 +1,75 @@ +--- +name: testing-admin-ui-banners +description: How to run the LiteLLM Admin UI dev server against a live proxy (including a second BEFORE/base worktree) to test dashboard shell banners and /health/readiness/details driven UI state. +--- + +# Testing Admin UI dashboard banners against a live proxy + +## Bring up AFTER (branch under test) + +``` +sudo service postgresql start +cd && (setsid uv run --no-sync litellm --config litellm/proxy/dev_config.yaml --detailed_debug --port 4000 > /tmp/proxy.log 2>&1 < /dev/null &) +cd ui/litellm-dashboard && (npm run dev > /tmp/ui_dev.log 2>&1 &) # port 3000 +``` + +Proxy startup takes ~45-60s before `/health/readiness/details` answers. Log in at +http://localhost:3000/ (it redirects to the proxy's login page) with `admin` / +the `general_settings.master_key` from `litellm/proxy/dev_config.yaml` (`sk-1234` by default). +In dev (`NODE_ENV=development`) the UI defaults its API base to `http://localhost:4000`, so no +extra env var is needed for the main dev server. + +Launcher gotcha: if an `exec` shell call runs longer than ~10s it gets backgrounded and can take +the freshly spawned proxy with it. Keep the launch command short (`setsid ... & ; sleep 6`) and +poll readiness in a separate call. + +## Bring up BEFORE (base commit) side by side + +``` +git worktree add /home/ubuntu/repos/litellm-base +cp -al /ui/litellm-dashboard/node_modules /home/ubuntu/repos/litellm-base/ui/litellm-dashboard/node_modules +``` + +Do NOT symlink `node_modules` into a worktree: Turbopack panics with +"Symlink [project]/node_modules is invalid, it points out of the filesystem root". A hardlink copy +(`cp -al`) works and is fast. + +Run the base proxy with the main venv but the base source tree, and point the base UI at it: + +``` +cd /home/ubuntu/repos/litellm-base && PYTHONPATH=$PWD /.venv/bin/python -m litellm.proxy.proxy_cli --config /litellm/proxy/dev_config.yaml --detailed_debug --port 4001 +cd /home/ubuntu/repos/litellm-base/ui/litellm-dashboard && NEXT_PUBLIC_BASE_URL=http://localhost:4001 npm run dev -- --port 3001 +``` + +`PYTHONPATH` wins over the editable install, so the base proxy really runs base code (verify with +`python -c "import litellm; print(litellm.__file__)"`). + +## Banner-specific notes + +Dashboard shell banners (`DebugWarningBanner`, `NoRedisWarningBanner`, `LicenseExpiryBanner`) all +read `useHealthReadinessDetails`, which has `staleTime: 5 min` and `retry: false`. After restarting +the proxy with different env, hard-reload the page (ctrl+shift+r) or the cached readiness payload +keeps the old banner state. Running the proxy with `--detailed_debug` always shows the yellow debug +banner, which is a handy control: if it is present but the banner under test is not, the readiness +call succeeded and the banner condition really is false. + +Coordination Redis (`litellm.proxy.proxy_server.redis_usage_cache`, which drives +`show_no_redis_warning`) is NOT populated by `REDIS_HOST`/`REDIS_PORT` alone: the env fallback only +runs inside `_init_cache`, which requires a cache block in the config. To get a real coordination +Redis, run `docker run -d -p 6379:6379 redis:7` and add to the config: + +``` +litellm_settings: + cache: true + cache_params: + type: redis + host: localhost + port: 6379 +``` + +`general_settings.coordination_redis` is the other supported path. + +## Devin Secrets Needed + +None for banner/UI-state testing; the proxy boots with the bundled dev config and a local Postgres. +Provider keys are only needed when a test actually issues LLM requests. diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 29f849ccebb..e814ec42d26 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -1453,17 +1453,23 @@ DISABLE_NO_REDIS_WARNING_ENV_VAR: Final = "LITELLM_DISABLE_NO_REDIS_WARNING" def _show_no_redis_warning() -> bool: """ - Whether the UI should warn that no coordination Redis is configured. + Whether the UI should warn that no Redis is configured. Redis is what makes rate limits, budgets, router state, and cache invalidation consistent across workers, so a proxy running without it is - only safe as a single worker. Operators who know that can silence the + only safe as a single worker. Both places a Redis can land count: the + coordination cache (from a Redis response cache, general_settings. + coordination_redis, or the REDIS_* env fallback) and the router's own + Redis (router_settings.redis_host), which backs cooldowns and usage-based + routing on its own. Operators who know they run one worker can silence the warning with LITELLM_DISABLE_NO_REDIS_WARNING=true. """ - from litellm.proxy.proxy_server import redis_usage_cache + from litellm.proxy.proxy_server import llm_router, redis_usage_cache if redis_usage_cache is not None: return False + if llm_router is not None and llm_router.cache.redis_cache is not None: + return False return get_secret_bool(DISABLE_NO_REDIS_WARNING_ENV_VAR, False) is not True diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index c2a43502c8a..e2705bd5fec 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -2463,25 +2463,58 @@ class TestConfigBaseForHealthCheck: class TestNoRedisWarning: """`show_no_redis_warning` drives the Admin UI's default-on "no Redis" banner.""" - def test_warns_when_no_coordination_redis_is_configured(self, monkeypatch): + @staticmethod + def _router(redis_cache): + return SimpleNamespace(cache=SimpleNamespace(redis_cache=redis_cache)) + + def test_warns_when_no_redis_is_configured(self, monkeypatch): monkeypatch.delenv("LITELLM_DISABLE_NO_REDIS_WARNING", raising=False) - with patch("litellm.proxy.proxy_server.redis_usage_cache", None): + with ( + patch("litellm.proxy.proxy_server.redis_usage_cache", None), + patch("litellm.proxy.proxy_server.llm_router", self._router(None)), + ): + assert _show_no_redis_warning() is True + + def test_warns_when_there_is_no_router_at_all(self, monkeypatch): + monkeypatch.delenv("LITELLM_DISABLE_NO_REDIS_WARNING", raising=False) + with ( + patch("litellm.proxy.proxy_server.redis_usage_cache", None), + patch("litellm.proxy.proxy_server.llm_router", None), + ): assert _show_no_redis_warning() is True def test_stays_quiet_when_a_coordination_redis_is_configured(self, monkeypatch): monkeypatch.delenv("LITELLM_DISABLE_NO_REDIS_WARNING", raising=False) - with patch("litellm.proxy.proxy_server.redis_usage_cache", MagicMock()): + with ( + patch("litellm.proxy.proxy_server.redis_usage_cache", MagicMock()), + patch("litellm.proxy.proxy_server.llm_router", self._router(None)), + ): + assert _show_no_redis_warning() is False + + def test_stays_quiet_when_only_the_router_has_redis(self, monkeypatch): + """router_settings.redis_host alone backs cooldowns and usage-based routing.""" + monkeypatch.delenv("LITELLM_DISABLE_NO_REDIS_WARNING", raising=False) + with ( + patch("litellm.proxy.proxy_server.redis_usage_cache", None), + patch("litellm.proxy.proxy_server.llm_router", self._router(MagicMock())), + ): assert _show_no_redis_warning() is False @pytest.mark.parametrize("value", ["true", "True"]) def test_env_var_suppresses_the_warning(self, monkeypatch, value): monkeypatch.setenv("LITELLM_DISABLE_NO_REDIS_WARNING", value) - with patch("litellm.proxy.proxy_server.redis_usage_cache", None): + with ( + patch("litellm.proxy.proxy_server.redis_usage_cache", None), + patch("litellm.proxy.proxy_server.llm_router", self._router(None)), + ): assert _show_no_redis_warning() is False def test_env_var_set_false_keeps_the_warning(self, monkeypatch): monkeypatch.setenv("LITELLM_DISABLE_NO_REDIS_WARNING", "false") - with patch("litellm.proxy.proxy_server.redis_usage_cache", None): + with ( + patch("litellm.proxy.proxy_server.redis_usage_cache", None), + patch("litellm.proxy.proxy_server.llm_router", self._router(None)), + ): assert _show_no_redis_warning() is True @pytest.mark.asyncio @@ -2492,6 +2525,7 @@ class TestNoRedisWarning: with ( patch("litellm.proxy.proxy_server.prisma_client", prisma_client), patch("litellm.proxy.proxy_server.redis_usage_cache", None), + patch("litellm.proxy.proxy_server.llm_router", self._router(None)), patch.object( _health_endpoints_module, "_db_health_readiness_check",