From 4dd96800e834d65d52146675b0a146abb2938863 Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 11 Oct 2026 05:43:27 +0000 Subject: [PATCH 1/3] fix(home): serve the bundled whats_new.json by default so a release shows only its own launches Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/__init__.py | 5 +---- .../proxy/public_endpoints/public_endpoints.py | 14 +++++++------- .../public_endpoints/test_public_endpoints.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 342a24208f0..e5001c1f608 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -439,10 +439,7 @@ autorouter_presets_url: str = os.getenv( "LITELLM_AUTOROUTER_PRESETS_URL", "https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/proxy/public_endpoints/autorouter_presets.json", ) -whats_new_url: str = os.getenv( - "LITELLM_WHATS_NEW_URL", - "https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/proxy/public_endpoints/whats_new.json", -) +whats_new_url: Optional[str] = os.getenv("LITELLM_WHATS_NEW_URL") suppress_debug_info: bool = False dynamodb_table_name: Optional[str] = None s3_callback_params: Optional[Dict] = None diff --git a/litellm/proxy/public_endpoints/public_endpoints.py b/litellm/proxy/public_endpoints/public_endpoints.py index 75e8a5af6a5..26bef3b3a61 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -591,8 +591,8 @@ async def _fetch_remote_whats_new(url: str) -> WhatsNewResponse: return WhatsNewResponse.model_validate_json(await _fetch_remote_bytes(url)) -async def _resolve_whats_new(url: str, fetch: Callable[[str], Awaitable[WhatsNewResponse]]) -> WhatsNewResponse: - if os.getenv("LITELLM_LOCAL_WHATS_NEW", "").lower() == "true": +async def _resolve_whats_new(url: str | None, fetch: Callable[[str], Awaitable[WhatsNewResponse]]) -> WhatsNewResponse: + if url is None or os.getenv("LITELLM_LOCAL_WHATS_NEW", "").lower() == "true": return _load_bundled_whats_new() try: return await fetch(url) @@ -611,7 +611,7 @@ class _WhatsNewCache: async def get_whats_new( - url: str, + url: str | None, fetch: Callable[[str], Awaitable[WhatsNewResponse]] = _fetch_remote_whats_new, ) -> WhatsNewResponse: cached: Final = _WhatsNewCache.launches @@ -637,10 +637,10 @@ async def get_public_whats_new() -> WhatsNewResponse: """ Return the launches the dashboard Home page shows under What's new. - Resolved once per process: fetched from ``litellm.whats_new_url`` (override with ``LITELLM_WHATS_NEW_URL``) - on the first request, falling back to the list bundled with the package on any failure, so an airgapped - proxy serves the bundled list without retrying. Set ``LITELLM_LOCAL_WHATS_NEW=True`` to skip the fetch. - A restart picks up a newly published list. + Serves the list bundled with this release. Set ``LITELLM_WHATS_NEW_URL`` to fetch a remote list instead, + resolved once per process on the first request and falling back to the bundled list on any failure, so an + airgapped proxy serves the bundled list without retrying. ``LITELLM_LOCAL_WHATS_NEW=True`` forces the + bundled list even when a URL is set. A restart picks up a newly published list. """ return await get_whats_new(url=litellm.whats_new_url) diff --git a/tests/unit/proxy/public_endpoints/test_public_endpoints.py b/tests/unit/proxy/public_endpoints/test_public_endpoints.py index 0779f692b17..830871c7bfc 100644 --- a/tests/unit/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/unit/proxy/public_endpoints/test_public_endpoints.py @@ -1586,6 +1586,23 @@ def test_get_whats_new_local_mode_serves_bundled_list( assert len(served.launches) > 0 +@pytest.mark.asyncio +async def test_get_whats_new_serves_bundled_list_without_fetching_when_no_url_is_configured( + monkeypatch: pytest.MonkeyPatch, reset_whats_new_cache: None +) -> None: + from litellm.proxy.public_endpoints.public_endpoints import _load_bundled_whats_new, get_whats_new + + monkeypatch.delenv("LITELLM_LOCAL_WHATS_NEW", raising=False) + + async def forbidden_fetch(url: str) -> WhatsNewResponse: + raise AssertionError(f"no URL is configured, so nothing may be fetched, got {url}") + + served = await get_whats_new(url=None, fetch=forbidden_fetch) + + assert served == _load_bundled_whats_new() + assert len(served.launches) > 0 + + @pytest.mark.asyncio async def test_get_whats_new_fetches_once_per_process( monkeypatch: pytest.MonkeyPatch, reset_whats_new_cache: None From d1ae7ea0851fee499dbc539d908ef0b181c217ae Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 11 Oct 2026 05:44:03 +0000 Subject: [PATCH 2/3] style: annotate whats_new_url with str | None Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index e5001c1f608..4be29b65a55 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -439,7 +439,7 @@ autorouter_presets_url: str = os.getenv( "LITELLM_AUTOROUTER_PRESETS_URL", "https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/proxy/public_endpoints/autorouter_presets.json", ) -whats_new_url: Optional[str] = os.getenv("LITELLM_WHATS_NEW_URL") +whats_new_url: str | None = os.getenv("LITELLM_WHATS_NEW_URL") suppress_debug_info: bool = False dynamodb_table_name: Optional[str] = None s3_callback_params: Optional[Dict] = None From 4bfec53865ac4676cb19c81f6dc61b63997ef589 Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 11 Oct 2026 05:57:04 +0000 Subject: [PATCH 3/3] test(home): pin whats_new_url to the env var and regenerate the dashboard API types Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../unit/proxy/public_endpoints/test_public_endpoints.py | 7 +++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/unit/proxy/public_endpoints/test_public_endpoints.py b/tests/unit/proxy/public_endpoints/test_public_endpoints.py index 830871c7bfc..396de55ee1e 100644 --- a/tests/unit/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/unit/proxy/public_endpoints/test_public_endpoints.py @@ -1,4 +1,5 @@ import json +import os import re from datetime import datetime, timezone from importlib.resources import files @@ -8,6 +9,8 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +import litellm + from fastapi import FastAPI from fastapi.testclient import TestClient @@ -1603,6 +1606,10 @@ async def test_get_whats_new_serves_bundled_list_without_fetching_when_no_url_is assert len(served.launches) > 0 +def test_whats_new_url_defaults_to_unset_so_only_the_env_var_opts_into_a_remote_list() -> None: + assert litellm.whats_new_url == os.getenv("LITELLM_WHATS_NEW_URL") + + @pytest.mark.asyncio async def test_get_whats_new_fetches_once_per_process( monkeypatch: pytest.MonkeyPatch, reset_whats_new_cache: None diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 4c69007bb21..d73394eb74c 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -14163,10 +14163,10 @@ export interface paths { * Get Public Whats New * @description Return the launches the dashboard Home page shows under What's new. * - * Resolved once per process: fetched from ``litellm.whats_new_url`` (override with ``LITELLM_WHATS_NEW_URL``) - * on the first request, falling back to the list bundled with the package on any failure, so an airgapped - * proxy serves the bundled list without retrying. Set ``LITELLM_LOCAL_WHATS_NEW=True`` to skip the fetch. - * A restart picks up a newly published list. + * Serves the list bundled with this release. Set ``LITELLM_WHATS_NEW_URL`` to fetch a remote list instead, + * resolved once per process on the first request and falling back to the bundled list on any failure, so an + * airgapped proxy serves the bundled list without retrying. ``LITELLM_LOCAL_WHATS_NEW=True`` forces the + * bundled list even when a URL is set. A restart picks up a newly published list. */ get: operations["get_public_whats_new_public_whats_new_get"]; put?: never;