Skip to content

Comments

[Feature] UI - Blog Dropdown in Navbar#21859

Open
yuneng-jiang wants to merge 13 commits intomainfrom
litellm_blog_dropdown
Open

[Feature] UI - Blog Dropdown in Navbar#21859
yuneng-jiang wants to merge 13 commits intomainfrom
litellm_blog_dropdown

Conversation

@yuneng-jiang
Copy link
Collaborator

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.json file 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

  • Backend: New GetBlogPosts utility (litellm/litellm_core_utils/get_blog_posts.py) with a 1-hour in-process TTL cache, GitHub fetch, and local fallback. New public endpoint GET /public/litellm_blog_posts (no auth required) that returns up to 5 posts.
  • Config: Added disable_show_blog boolean to UISettings and ALLOWED_UI_SETTINGS_FIELDS, allowing admins to hide the dropdown.
  • Frontend: New BlogDropdown component 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 into navbar.tsx after the existing Docs/Slack/GitHub community links.
  • Data: blog_posts.json added at the repo root (GitHub source of truth) and litellm/blog_posts_backup.json as the bundled fallback.

Testing

  • Unit tests for GetBlogPosts: TTL cache behavior, network fallback, local env var bypass (tests/test_litellm/test_get_blog_posts.py)
  • Endpoint tests for /public/litellm_blog_posts: response shape, 5-post cap, fallback on upstream failure (tests/proxy_unit_tests/test_blog_posts_endpoint.py)
  • UISettings tests: disable_show_blog field default and allowlist presence (tests/proxy_unit_tests/test_proxy_setting_endpoints.py)
  • Component tests for 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

yuneng-jiang and others added 13 commits February 21, 2026 17:34
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>
@vercel
Copy link

vercel bot commented Feb 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 22, 2026 1:37am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 22, 2026

Greptile Summary

This PR adds a Blog dropdown to the LiteLLM proxy UI navbar. The feature includes a new backend utility (GetBlogPosts) that fetches blog posts from GitHub with a 1-hour in-process TTL cache and falls back to a bundled local JSON backup on failure, a new public endpoint GET /public/litellm_blog_posts (no auth required, returns up to 5 posts), a new BlogDropdown React component using react-query, and admin-configurable disable_show_blog flag in UISettings.

  • Backend: New GetBlogPosts class in litellm/litellm_core_utils/get_blog_posts.py mirrors the existing GetModelCostMap pattern — synchronous httpx.get() with 5-second timeout, validation, TTL cache, and local fallback. The class-level cache is not thread-safe, and the env var for the URL is resolved at import time rather than at call time.
  • Frontend: New BlogDropdown component with loading, error (with retry), and success states. Integrated into navbar.tsx after the Docs link. A useDisableShowBlog hook reads the admin setting to conditionally hide the dropdown.
  • Config: disable_show_blog added to UISettings model and ALLOWED_UI_SETTINGS_FIELDS allowlist.
  • Tests: Comprehensive coverage across backend (unit + endpoint) and frontend (component) tests. Frontend test names do not follow the project convention of starting with "should".

Confidence Score: 4/5

  • This PR is safe to merge — it adds an isolated, non-critical UI feature with comprehensive fallback behavior and no impact on the request-serving critical path.
  • Score of 4 reflects a well-structured feature addition that follows existing codebase patterns (mirrors GetModelCostMap). The issues found are minor style/convention concerns rather than functional bugs: frontend test names don't follow the "should" prefix convention, class-level cache lacks thread-safety, and the env var is read at import time. The synchronous httpx.get() in an async endpoint is a known pattern already present in the codebase, mitigated by 5-second timeout and 1-hour cache.
  • litellm/litellm_core_utils/get_blog_posts.py (thread-safety of cache, env var resolution) and BlogDropdown.test.tsx (test naming convention)

Important Files Changed

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
Loading

Last reviewed commit: a0965d5

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +48 to +56
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();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +52 to +53
_cached_posts: Optional[List[Dict[str, str]]] = None
_last_fetch_time: float = 0.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Comment on lines +22 to +25
BLOG_POSTS_GITHUB_URL: str = os.getenv(
"LITELLM_BLOG_POSTS_URL",
"https://raw.githubusercontent.com/BerriAI/litellm/main/blog_posts.json",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant