diff --git a/litellm/__init__.py b/litellm/__init__.py index 34f08da3c89..a33a78fb325 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -440,10 +440,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: 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 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..4f1cf2d8c28 100644 --- a/tests/unit/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/unit/proxy/public_endpoints/test_public_endpoints.py @@ -1,18 +1,18 @@ import json +import os import re +from collections.abc import Iterator from datetime import datetime, timezone from importlib.resources import files -from collections.abc import Iterator from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest - - from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import TypeAdapter +import litellm from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.public_endpoints import router from litellm.router_strategy.complexity_router.fuse_presets import get_fuse_presets @@ -1168,8 +1168,8 @@ def test_public_mcp_hub_derives_publication_metadata_without_mutating_registry( stored_public: bool | None, ) -> None: from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager - from litellm.types.mcp_server.mcp_server_manager import MCPServer from litellm.proxy._types import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer app: Final = FastAPI() app.include_router(router) @@ -1253,8 +1253,8 @@ def test_public_mcp_hub_returns_empty_when_whitelist_unset(): def test_public_mcp_hub_does_not_expose_upstream_url(): """Regression: /public/mcp_hub is unauthenticated, so the gateway-internal upstream url must never appear in its response even when the server has one.""" - from litellm.types.mcp_server.mcp_server_manager import MCPServer from litellm.proxy._types import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer app = FastAPI() app.include_router(router) @@ -1586,6 +1586,47 @@ 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 + + +def test_whats_new_url_default_mirrors_the_env_var_so_no_remote_url_is_baked_in() -> None: + assert litellm.whats_new_url == os.environ.get("LITELLM_WHATS_NEW_URL") + + +@pytest.mark.asyncio +async def test_public_whats_new_route_serves_the_bundled_list_without_fetching_when_no_url_is_configured( + monkeypatch: pytest.MonkeyPatch, reset_whats_new_cache: None +) -> None: + from litellm.proxy.public_endpoints import public_endpoints + from litellm.proxy.public_endpoints.public_endpoints import _load_bundled_whats_new, get_public_whats_new + + monkeypatch.delenv("LITELLM_LOCAL_WHATS_NEW", raising=False) + + async def forbidden_fetch(url: str) -> bytes: + raise AssertionError(f"no URL is configured, so the route may not fetch, got {url}") + + monkeypatch.setattr(public_endpoints, "_fetch_remote_bytes", forbidden_fetch) + + served = await get_public_whats_new() + + 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 diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 241aceb1894..ada0bb2b8a2 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -13669,10 +13669,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;