feat: add GET /public/litellm_blog_posts endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-02-21 16:51:55 -08:00
parent 7e5c63c134
commit 021a9097c4
2 changed files with 106 additions and 0 deletions

View file

@ -4,6 +4,12 @@ from typing import List
from fastapi import APIRouter, Depends, HTTPException
from litellm.litellm_core_utils.get_blog_posts import (
BlogPost,
BlogPostsResponse,
GetBlogPosts,
get_blog_posts,
)
from litellm.proxy._types import CommonProxyErrors
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.types.agents import AgentCard
@ -193,6 +199,27 @@ async def get_litellm_model_cost_map():
)
@router.get(
"/public/litellm_blog_posts",
tags=["public"],
response_model=BlogPostsResponse,
)
async def get_litellm_blog_posts():
"""
Public endpoint to get the latest LiteLLM blog posts.
Fetches from GitHub with a 1-hour in-process cache.
Falls back to the bundled local backup on any failure.
"""
try:
posts_data = get_blog_posts()
except Exception:
posts_data = GetBlogPosts.load_local_blog_posts()
posts = [BlogPost(**p) for p in posts_data[:5]]
return BlogPostsResponse(posts=posts)
@router.get(
"/public/agents/fields",
tags=["public", "[beta] Agents"],

View file

@ -0,0 +1,79 @@
"""Tests for the /public/litellm_blog_posts endpoint."""
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
SAMPLE_POSTS = [
{
"title": "Test Post",
"description": "A test post.",
"date": "2026-01-01",
"url": "https://www.litellm.ai/blog/test",
}
]
@pytest.fixture
def client():
"""Create a TestClient with just the public_endpoints router."""
from fastapi import FastAPI
from litellm.proxy.public_endpoints.public_endpoints import router
app = FastAPI()
app.include_router(router)
return TestClient(app)
def test_get_blog_posts_returns_response_shape(client):
with patch(
"litellm.proxy.public_endpoints.public_endpoints.get_blog_posts",
return_value=SAMPLE_POSTS,
):
response = client.get("/public/litellm_blog_posts")
assert response.status_code == 200
data = response.json()
assert "posts" in data
assert len(data["posts"]) == 1
post = data["posts"][0]
assert post["title"] == "Test Post"
assert post["description"] == "A test post."
assert post["date"] == "2026-01-01"
assert post["url"] == "https://www.litellm.ai/blog/test"
def test_get_blog_posts_limits_to_five(client):
"""Endpoint returns at most 5 posts."""
many_posts = [
{
"title": f"Post {i}",
"description": "desc",
"date": "2026-01-01",
"url": f"https://www.litellm.ai/blog/{i}",
}
for i in range(10)
]
with patch(
"litellm.proxy.public_endpoints.public_endpoints.get_blog_posts",
return_value=many_posts,
):
response = client.get("/public/litellm_blog_posts")
assert response.status_code == 200
assert len(response.json()["posts"]) == 5
def test_get_blog_posts_returns_local_backup_on_failure(client):
"""Endpoint returns local backup (non-empty list) when fetcher fails."""
with patch(
"litellm.proxy.public_endpoints.public_endpoints.get_blog_posts",
side_effect=Exception("fetch failed"),
):
response = client.get("/public/litellm_blog_posts")
# Should not 500 — returns local backup
assert response.status_code == 200
assert "posts" in response.json()