Conversation
Adds GetBlogPosts class that fetches blog posts from GitHub with a 1-hour in-process TTL cache, validates the response, and falls back to the bundled blog_posts_backup.json on any network or validation failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ache duplication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR adds a Blog dropdown to the LiteLLM proxy UI navbar. The feature includes a new backend utility (
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/get_blog_posts.py | New utility for fetching blog posts with TTL cache and fallback. Uses synchronous httpx.get() which blocks the event loop when called from async endpoints. Class-level cache vars are not thread-safe. Env var resolved at import time. |
| litellm/proxy/public_endpoints/public_endpoints.py | Adds new public endpoint GET /public/litellm_blog_posts. Follows existing patterns for public endpoints. Calls synchronous get_blog_posts() in async handler (mirrors GetModelCostMap pattern). |
| litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py | Adds disable_show_blog boolean field to UISettings model and ALLOWED_UI_SETTINGS_FIELDS allowlist. Clean addition following existing patterns. |
| ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.test.tsx | Tests cover success, error, retry, disabled state, and 5-post cap. However, test names don't follow the required "should" prefix convention from AGENTS.md. |
| ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.tsx | New BlogDropdown component using react-query, antd Dropdown, and useDisableShowBlog hook. Well-structured with loading, error, and success states. Enforces 5-post client-side cap. |
| ui/litellm-dashboard/src/components/navbar.tsx | Minimal change: imports BlogDropdown and adds it after the Docs link in the navbar. Clean integration. |
Sequence Diagram
sequenceDiagram
participant UI as BlogDropdown (React)
participant Proxy as FastAPI Endpoint
participant Cache as In-Process Cache
participant GH as GitHub (blog_posts.json)
participant Local as Local Backup JSON
UI->>Proxy: GET /public/litellm_blog_posts
Proxy->>Cache: Check TTL cache
alt Cache hit (< 1 hour)
Cache-->>Proxy: Return cached posts
else Cache miss or expired
Proxy->>GH: httpx.get() (5s timeout)
alt Success
GH-->>Proxy: blog_posts.json
Proxy->>Cache: Store posts + timestamp
else Network failure
Proxy->>Local: Load blog_posts_backup.json
Local-->>Proxy: Fallback posts
end
end
Proxy-->>UI: BlogPostsResponse (max 5 posts)
UI->>UI: Render dropdown with posts
Last reviewed commit: a0965d5
| it("renders the Blog button", async () => { | ||
| global.fetch = vi.fn().mockResolvedValueOnce({ | ||
| ok: true, | ||
| json: async () => SAMPLE_POSTS, | ||
| }); | ||
|
|
||
| render(<BlogDropdown />, { wrapper: createWrapper() }); | ||
| expect(screen.getByText("Blog")).toBeInTheDocument(); | ||
| }); |
There was a problem hiding this comment.
Test names must start with "should"
Per the project's AGENTS.md testing conventions, all it() test names should follow the pattern it("should ..."). The existing tests in the same Navbar/ directory (e.g., CommunityEngagementButtons.test.tsx, UserDropdown.test.tsx) all use this convention. These tests use descriptions like "renders the Blog button", "shows posts on success", etc. instead.
For example:
it("renders the Blog button", ...)→it("should render the Blog button", ...)it("shows posts on success", ...)→it("should show posts on success", ...)it("shows at most 5 posts", ...)→it("should show at most 5 posts", ...)it("shows error message and Retry button on fetch failure", ...)→it("should show error message and Retry button on fetch failure", ...)it("calls refetch when Retry is clicked", ...)→it("should call refetch when Retry is clicked", ...)it("returns null when useDisableShowBlog is true", ...)→it("should return null when useDisableShowBlog is true", ...)
Context Used: Context from dashboard - AGENTS.md (source)
| _cached_posts: Optional[List[Dict[str, str]]] = None | ||
| _last_fetch_time: float = 0.0 |
There was a problem hiding this comment.
Class-level mutable cache is not thread-safe
_cached_posts and _last_fetch_time are class-level mutable variables shared across all threads. In a multi-threaded FastAPI deployment (or when using asyncio.to_thread), concurrent requests could create a race condition in get_blog_posts() where one thread reads _cached_posts while another thread is updating it after the TTL check. While the impact is low for a blog post list (worst case: an extra remote fetch), consider protecting these with a threading.Lock for correctness, particularly since the GetModelCostMap class this is modeled after has the same issue.
import threading
class GetBlogPosts:
_cached_posts: Optional[List[Dict[str, str]]] = None
_last_fetch_time: float = 0.0
_lock: threading.Lock = threading.Lock()| BLOG_POSTS_GITHUB_URL: str = os.getenv( | ||
| "LITELLM_BLOG_POSTS_URL", | ||
| "https://raw.githubusercontent.com/BerriAI/litellm/main/blog_posts.json", | ||
| ) |
There was a problem hiding this comment.
Env var is resolved at import time, not at call time
BLOG_POSTS_GITHUB_URL is computed once at module import via os.getenv(). It then becomes the default parameter value for get_blog_posts() and get_blog_posts(). If a user sets/changes the LITELLM_BLOG_POSTS_URL environment variable after the module has been imported (e.g., through the proxy config), the change won't take effect until the process is restarted. Consider reading the env var inside get_blog_posts() instead:
BLOG_POSTS_GITHUB_URL_DEFAULT = "https://raw.githubusercontent.com/BerriAI/litellm/main/blog_posts.json"
# Then inside get_blog_posts():
url = os.getenv("LITELLM_BLOG_POSTS_URL", BLOG_POSTS_GITHUB_URL_DEFAULT)
Relevant issues
Summary
Adds a Blog dropdown to the top navbar in the LiteLLM proxy UI. The dropdown fetches the latest blog posts from the GitHub-hosted
blog_posts.jsonfile and falls back to a bundled local JSON copy if the network request fails. This follows the same pattern used by the model cost map.Changes
GetBlogPostsutility (litellm/litellm_core_utils/get_blog_posts.py) with a 1-hour in-process TTL cache, GitHub fetch, and local fallback. New public endpointGET /public/litellm_blog_posts(no auth required) that returns up to 5 posts.disable_show_blogboolean toUISettingsandALLOWED_UI_SETTINGS_FIELDS, allowing admins to hide the dropdown.BlogDropdowncomponent using react-query. Shows a loading indicator while fetching, an inline error message with a Retry button on failure, and up to 5 posts (title, date, short description) on success. The component is wired intonavbar.tsxafter the existing Docs/Slack/GitHub community links.blog_posts.jsonadded at the repo root (GitHub source of truth) andlitellm/blog_posts_backup.jsonas the bundled fallback.Testing
GetBlogPosts: TTL cache behavior, network fallback, local env var bypass (tests/test_litellm/test_get_blog_posts.py)/public/litellm_blog_posts: response shape, 5-post cap, fallback on upstream failure (tests/proxy_unit_tests/test_blog_posts_endpoint.py)UISettingstests:disable_show_blogfield default and allowlist presence (tests/proxy_unit_tests/test_proxy_setting_endpoints.py)BlogDropdown: success render, error+retry, disabled state, 5-post cap (ui/litellm-dashboard/src/components/Navbar/BlogDropdown/BlogDropdown.test.tsx)Type
🆕 New Feature
✅ Test
Screenshots