Repository navigation
feat(ui): add upgrade banner with latest release changelog stats #40429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
litellm-stampbot
merged 10 commits into
main
from
litellm_upgrade_banner_changelog_stats
Sep 21, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
1a35831
feat(ui): add upgrade banner with latest release changelog stats
7edc795
fix(ui): fix CI failures on upgrade banner PR
6d532f2
Merge remote-tracking branch 'origin/main' into litellm_upgrade_banne…
litellm-stampbot 4036b76
fix(proxy): count unprefixed release bullets and coalesce concurrent …
litellm-stampbot 26bcc53
fix(proxy): allow latest release info and reset banner dismissal
litellm-stampbot a36a62a
Merge remote-tracking branch 'origin/main' into litellm_upgrade_banne…
litellm-stampbot ea04267
fix(proxy): return an immutable bucket mapping from count_release_bul…
litellm-stampbot 3777b0b
test(proxy): document why the release info route check asserts by not…
litellm-stampbot 4595d18
Merge remote-tracking branch 'origin/main' into pr40429
litellm-stampbot 9611af7
Merge remote-tracking branch 'origin/main' into litellm_upgrade_banne…
litellm-stampbot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
litellm/proxy/ui_crud_endpoints/latest_release_endpoints.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import asyncio | ||
| import re | ||
| from collections import Counter | ||
| from collections.abc import Awaitable, Mapping | ||
| from dataclasses import dataclass | ||
| from types import MappingProxyType | ||
| from typing import Annotated, Final, Literal, Protocol, TypeAlias | ||
|
|
||
| import httpx | ||
| from fastapi import APIRouter, Depends | ||
| from pydantic import BaseModel, ValidationError | ||
|
|
||
| from litellm._logging import verbose_proxy_logger | ||
| from litellm.caching.in_memory_cache import InMemoryCache | ||
| from litellm.proxy.auth.user_api_key_auth import user_api_key_auth | ||
|
|
||
| router: Final = APIRouter() | ||
|
|
||
| LATEST_RELEASE_URL: Final = "https://api.github.com/repos/BerriAI/litellm/releases/latest" | ||
| LATEST_RELEASE_FETCH_TIMEOUT_SECONDS: Final = 5 | ||
| LATEST_RELEASE_CACHE_TTL_SECONDS: Final = 60 * 60 | ||
| LATEST_RELEASE_UNAVAILABLE_CACHE_TTL_SECONDS: Final = 5 * 60 | ||
| LATEST_RELEASE_CACHE_KEY: Final = "latest_release_info" | ||
|
|
||
| _RELEASE_BULLET_PATTERN: Final = re.compile(r"^\*\s+(?:([A-Za-z]+)(?:\([^)]*\))?!?:\s)?\S") | ||
| _NEW_CONTRIBUTOR_PATTERN: Final = re.compile(r"^\*\s+@\S+ made their first contribution\b") | ||
|
|
||
| _Bucket: TypeAlias = Literal["new_features", "bug_fixes", "other_updates"] | ||
| _PREFIX_BUCKETS: Final[Mapping[str, _Bucket]] = MappingProxyType({"feat": "new_features", "fix": "bug_fixes"}) | ||
|
|
||
|
|
||
| class LatestReleaseInfo(BaseModel): | ||
| version: str | ||
| new_features: int | ||
| bug_fixes: int | ||
| other_updates: int | ||
| release_url: str | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class LatestReleaseUnavailable: | ||
| reason: str | ||
|
|
||
|
|
||
| class _GitHubRelease(BaseModel): | ||
| tag_name: str | ||
| html_url: str | ||
| body: str | ||
|
|
||
|
|
||
| class _AsyncGetClient(Protocol): | ||
| def get(self, url: str, *, timeout: float | None = None) -> Awaitable[httpx.Response]: ... | ||
|
|
||
|
|
||
| _latest_release_cache: Final = InMemoryCache(max_size_in_memory=1, default_ttl=LATEST_RELEASE_CACHE_TTL_SECONDS) | ||
| _latest_release_fetch_lock: Final = asyncio.Lock() | ||
|
|
||
|
|
||
| def _default_client() -> _AsyncGetClient: | ||
| from litellm.llms.custom_httpx.http_handler import get_async_httpx_client | ||
| from litellm.types.llms.custom_http import httpxSpecialProvider | ||
|
|
||
| return get_async_httpx_client(llm_provider=httpxSpecialProvider.UI) | ||
|
|
||
|
|
||
| def _default_cache() -> InMemoryCache: | ||
| return _latest_release_cache | ||
|
|
||
|
|
||
| def _default_fetch_lock() -> asyncio.Lock: | ||
| return _latest_release_fetch_lock | ||
|
|
||
|
|
||
| def _bucket_for(line: str) -> _Bucket | None: | ||
| if _NEW_CONTRIBUTOR_PATTERN.match(line) is not None: | ||
| return None | ||
| match: Final = _RELEASE_BULLET_PATTERN.match(line) | ||
| if match is None: | ||
| return None | ||
| prefix: Final = match.group(1) | ||
| return "other_updates" if prefix is None else _PREFIX_BUCKETS.get(prefix.lower(), "other_updates") | ||
|
|
||
|
|
||
| def count_release_bullets(body: str) -> Mapping[_Bucket, int]: | ||
| """Bucket release-note bullets by conventional-commit type or ``other_updates``.""" | ||
| return MappingProxyType(Counter(bucket for line in body.splitlines() if (bucket := _bucket_for(line)) is not None)) | ||
|
|
||
|
|
||
| def parse_latest_release(response: httpx.Response) -> LatestReleaseInfo | LatestReleaseUnavailable: | ||
| if response.status_code != 200: | ||
| return LatestReleaseUnavailable(reason=f"GitHub responded with status {response.status_code}") | ||
| try: | ||
| release: Final = _GitHubRelease.model_validate_json(response.content) | ||
| except ValidationError as e: | ||
| return LatestReleaseUnavailable(reason=f"GitHub release payload was not the expected shape: {e}") | ||
| counts: Final = count_release_bullets(release.body) | ||
| return LatestReleaseInfo( | ||
| version=release.tag_name.removeprefix("v"), | ||
| new_features=counts.get("new_features", 0), | ||
| bug_fixes=counts.get("bug_fixes", 0), | ||
| other_updates=counts.get("other_updates", 0), | ||
| release_url=release.html_url, | ||
| ) | ||
|
|
||
|
|
||
| async def fetch_latest_release(client: _AsyncGetClient) -> LatestReleaseInfo | LatestReleaseUnavailable: | ||
| try: | ||
| response: Final = await client.get(LATEST_RELEASE_URL, timeout=LATEST_RELEASE_FETCH_TIMEOUT_SECONDS) | ||
| except httpx.HTTPError as e: | ||
| return LatestReleaseUnavailable(reason=f"{type(e).__name__}: {e}") | ||
| return parse_latest_release(response) | ||
|
|
||
|
|
||
| async def get_latest_release_info( | ||
| client: _AsyncGetClient, cache: InMemoryCache, fetch_lock: asyncio.Lock | ||
| ) -> LatestReleaseInfo | LatestReleaseUnavailable: | ||
| cached: Final = cache.get_cache(LATEST_RELEASE_CACHE_KEY) | ||
| if isinstance(cached, (LatestReleaseInfo, LatestReleaseUnavailable)): | ||
| return cached | ||
| async with fetch_lock: | ||
| cached_after_lock: Final = cache.get_cache(LATEST_RELEASE_CACHE_KEY) | ||
| if isinstance(cached_after_lock, (LatestReleaseInfo, LatestReleaseUnavailable)): | ||
| return cached_after_lock | ||
| result: Final = await fetch_latest_release(client) | ||
| ttl: Final = ( | ||
| LATEST_RELEASE_UNAVAILABLE_CACHE_TTL_SECONDS | ||
| if isinstance(result, LatestReleaseUnavailable) | ||
| else LATEST_RELEASE_CACHE_TTL_SECONDS | ||
| ) | ||
| cache.set_cache(LATEST_RELEASE_CACHE_KEY, result, ttl=ttl) | ||
| return result | ||
|
|
||
|
|
||
| @router.get( | ||
| "/get/latest_release_info", | ||
| tags=["UI Settings"], # mutable-ok: FastAPI's route decorator only accepts a list | ||
| dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI's route decorator only accepts a list | ||
| response_model=LatestReleaseInfo | None, | ||
| ) | ||
| async def latest_release_info( | ||
| client: Annotated[_AsyncGetClient, Depends(_default_client)], | ||
| cache: Annotated[InMemoryCache, Depends(_default_cache)], | ||
| fetch_lock: Annotated[asyncio.Lock, Depends(_default_fetch_lock)], | ||
| ) -> LatestReleaseInfo | None: | ||
| """ | ||
| Latest stable LiteLLM GitHub release with its PR count split into new features, bug fixes and other updates. | ||
| Returns null when GitHub can't be reached so the dashboard upgrade banner simply doesn't render. | ||
| """ | ||
| result: Final = await get_latest_release_info(client=client, cache=cache, fetch_lock=fetch_lock) | ||
| if isinstance(result, LatestReleaseUnavailable): | ||
| verbose_proxy_logger.warning("LiteLLM: latest release info unavailable: %s", result.reason) | ||
| return None | ||
| return result | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.