chore(proxy): keep public AI hub unauthenticated

This commit is contained in:
user
2026-05-01 19:07:21 -07:00
parent 0c864880a8
commit bef28aa789
7 changed files with 18 additions and 182 deletions
+5
View File
@@ -618,6 +618,11 @@ class LiteLLMRoutes(enum.Enum):
"/config/yaml",
"/litellm/.well-known/litellm-ui-config",
"/.well-known/litellm-ui-config",
"/public/model_hub",
"/public/model_hub/info",
"/public/agent_hub",
"/public/mcp_hub",
"/public/skill_hub",
"/public/litellm_model_cost_map",
]
)
-15
View File
@@ -86,16 +86,6 @@ except ImportError as e:
user_api_key_service_logger_obj = ServiceLogging() # used for tracking latency on OTEL
_PUBLIC_AI_HUB_ROUTES = frozenset(
{
"/public/model_hub",
"/public/model_hub/info",
"/public/agent_hub",
"/public/mcp_hub",
"/public/skill_hub",
}
)
def _normalize_public_auth_route(route: str) -> str:
if route != "/" and route.endswith("/"):
@@ -110,11 +100,6 @@ def _route_requires_auth_despite_public(
if normalized_route == "/metrics":
return litellm.require_auth_for_metrics_endpoint is not False
if normalized_route in _PUBLIC_AI_HUB_ROUTES:
return (general_settings or {}).get(
"require_auth_for_public_ai_hub", True
) is True
return False
@@ -5,7 +5,7 @@ from importlib.resources import files
from typing import Any, Dict, List, Optional
import litellm
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, HTTPException
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.get_blog_posts import (
@@ -16,11 +16,7 @@ from litellm.litellm_core_utils.get_blog_posts import (
)
from litellm.proxy._types import (
CommonProxyErrors,
LitellmUserRoles,
SpecialHeaders,
UserAPIKeyAuth,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.types.agents import AgentCard
from litellm.types.mcp import MCPPublicServer
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
@@ -37,36 +33,6 @@ from litellm.types.utils import LlmProviders
router = APIRouter()
async def public_ai_hub_auth_dependency(request: Request) -> UserAPIKeyAuth:
from litellm.proxy.proxy_server import general_settings
if (general_settings or {}).get("require_auth_for_public_ai_hub", True) is True:
return await user_api_key_auth(
request=request,
api_key=request.headers.get(SpecialHeaders.openai_authorization.value)
or "",
azure_api_key_header=request.headers.get(
SpecialHeaders.azure_authorization.value
)
or "",
anthropic_api_key_header=request.headers.get(
SpecialHeaders.anthropic_authorization.value
),
google_ai_studio_api_key_header=request.headers.get(
SpecialHeaders.google_ai_studio_authorization.value
),
azure_apim_header=request.headers.get(
SpecialHeaders.azure_apim_authorization.value
)
or "",
custom_litellm_key_header=request.headers.get(
SpecialHeaders.custom_litellm_api_key.value
),
)
return UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY)
# ---------------------------------------------------------------------------
# /public/endpoints — helpers
# ---------------------------------------------------------------------------
@@ -189,7 +155,6 @@ def _load_endpoints() -> List[Dict[str, Any]]:
@router.get(
"/public/model_hub",
tags=["public", "model management"],
dependencies=[Depends(public_ai_hub_auth_dependency)],
response_model=List[ModelGroupInfoProxy],
)
async def public_model_hub():
@@ -244,7 +209,6 @@ async def public_model_hub():
@router.get(
"/public/agent_hub",
tags=["[beta] Agents", "public"],
dependencies=[Depends(public_ai_hub_auth_dependency)],
response_model=List[AgentCard],
)
async def get_agents():
@@ -266,7 +230,6 @@ async def get_agents():
@router.get(
"/public/mcp_hub",
tags=["[beta] MCP", "public"],
dependencies=[Depends(public_ai_hub_auth_dependency)],
response_model=List[MCPPublicServer],
)
async def get_mcp_servers():
@@ -286,7 +249,6 @@ async def get_mcp_servers():
@router.get(
"/public/skill_hub",
tags=["public", "Claude Code Marketplace"],
dependencies=[Depends(public_ai_hub_auth_dependency)],
)
async def public_skill_hub():
"""Return enabled (public) Claude Code skills — no auth required."""
@@ -333,7 +295,6 @@ async def public_skill_hub():
@router.get(
"/public/model_hub/info",
tags=["public", "model management"],
dependencies=[Depends(public_ai_hub_auth_dependency)],
response_model=PublicModelHubInfo,
)
async def public_model_hub_info():
@@ -94,11 +94,6 @@ class UISettings(BaseModel):
description="List of page keys that internal users (non-admins) can see in the UI sidebar. If not set, all pages are visible based on role permissions.",
)
require_auth_for_public_ai_hub: bool = Field(
default=True,
description="If true, requires authentication for accessing the public AI Hub.",
)
allow_public_health_readiness_details: bool = Field(
default=False,
description="If true, returns the legacy detailed payload from the unauthenticated /health/readiness endpoint.",
@@ -173,7 +168,6 @@ ALLOWED_UI_SETTINGS_FIELDS = {
"disable_model_add_for_internal_users",
"disable_team_admin_delete_team_user",
"enabled_ui_pages_internal_users",
"require_auth_for_public_ai_hub",
"allow_public_health_readiness_details",
"forward_client_headers_to_llm_api",
"forward_llm_provider_auth_headers",
@@ -189,7 +183,6 @@ ALLOWED_UI_SETTINGS_FIELDS = {
# Flags that must be synced from the persisted UISettings into
# general_settings at runtime (on both read and write).
_RUNTIME_GENERAL_SETTINGS_FLAGS = [
"require_auth_for_public_ai_hub",
"allow_public_health_readiness_details",
"forward_client_headers_to_llm_api",
"forward_llm_provider_auth_headers",
@@ -15,6 +15,7 @@ import litellm
import litellm.proxy.proxy_server
from litellm.caching.dual_cache import DualCache
from litellm.proxy._types import (
LiteLLMRoutes,
LiteLLM_JWTAuth,
LiteLLM_UserTable,
LitellmUserRoles,
@@ -62,25 +63,16 @@ def test_route_requires_auth_despite_public_for_metrics(monkeypatch):
assert _route_requires_auth_despite_public("/metrics", {}) is False
def test_route_requires_auth_despite_public_for_public_ai_hub():
settings = {"require_auth_for_public_ai_hub": True}
assert _route_requires_auth_despite_public("/public/model_hub", {}) is True
assert _route_requires_auth_despite_public("/public/model_hub", settings) is True
assert _route_requires_auth_despite_public("/public/model_hub/", settings) is True
assert (
_route_requires_auth_despite_public("/public/model_hub/info", settings) is True
)
assert _route_requires_auth_despite_public("/public/agent_hub", settings) is True
assert _route_requires_auth_despite_public("/public/mcp_hub", settings) is True
assert _route_requires_auth_despite_public("/public/skill_hub", settings) is True
assert (
_route_requires_auth_despite_public(
"/public/model_hub", {"require_auth_for_public_ai_hub": False}
)
is False
)
def test_public_ai_hub_routes_remain_public():
for route in (
"/public/model_hub",
"/public/model_hub/info",
"/public/agent_hub",
"/public/mcp_hub",
"/public/skill_hub",
):
assert route in LiteLLMRoutes.public_routes.value
assert _route_requires_auth_despite_public(route, {}) is False
@pytest.mark.asyncio
@@ -9,15 +9,9 @@ sys.path.insert(0, os.path.abspath("../../.."))
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.datastructures import URL
from starlette.requests import Request
from litellm.proxy._types import LitellmUserRoles, ProxyException
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.public_endpoints import router
from litellm.proxy.public_endpoints.public_endpoints import (
public_ai_hub_auth_dependency,
)
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
ModelGroupInfoProxy,
)
@@ -97,7 +91,7 @@ def test_get_litellm_model_cost_map_returns_cost_map():
)
def test_public_ai_hub_info_requires_auth_by_default(monkeypatch):
def test_public_ai_hub_info_is_public_by_default(monkeypatch):
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@@ -105,68 +99,11 @@ def test_public_ai_hub_info_requires_auth_by_default(monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
with pytest.raises(ProxyException):
client.get("/public/model_hub/info")
def test_public_ai_hub_info_can_be_explicitly_public(monkeypatch):
app = FastAPI()
app.include_router(router)
client = TestClient(app)
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{"require_auth_for_public_ai_hub": False},
)
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
response = client.get("/public/model_hub/info")
assert response.status_code == 200, response.text
@pytest.mark.asyncio
async def test_public_ai_hub_info_requires_auth_by_default_dependency(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{},
)
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master")
request = Request(
scope={
"type": "http",
"method": "GET",
"path": "/public/model_hub/info",
"headers": [],
}
)
request._url = URL(url="/public/model_hub/info")
with pytest.raises(ProxyException):
await public_ai_hub_auth_dependency(request)
@pytest.mark.asyncio
async def test_public_ai_hub_info_skips_auth_when_explicitly_disabled(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings",
{"require_auth_for_public_ai_hub": False},
)
request = Request(
scope={
"type": "http",
"method": "GET",
"path": "/public/model_hub/info",
"headers": [],
}
)
request._url = URL(url="/public/model_hub/info")
auth = await public_ai_hub_auth_dependency(request)
assert auth.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
def test_watsonx_provider_fields():
"""Test that Watsonx provider has all required credential fields including multiple auth options."""
app = FastAPI()
@@ -1147,43 +1147,6 @@ class TestProxySettingEndpoints:
assert response.status_code == 200
assert general_settings.get("forward_llm_provider_auth_headers") is True
def test_update_ui_settings_syncs_public_ai_hub_auth_to_general_settings(
self, mock_auth, monkeypatch
):
"""Public AI Hub auth flag must be synced so public-route auth checks see it."""
from unittest.mock import AsyncMock, MagicMock
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
mock_user_auth = UserAPIKeyAuth(
user_id="test-user-123",
user_role=LitellmUserRoles.PROXY_ADMIN,
)
app.dependency_overrides[user_api_key_auth] = lambda: mock_user_auth
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
general_settings: dict = {}
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings", general_settings
)
mock_prisma = MagicMock()
mock_prisma.db.litellm_uisettings.upsert = AsyncMock()
mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
payload = {"require_auth_for_public_ai_hub": True}
try:
response = client.patch("/update/ui_settings", json=payload)
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert general_settings.get("require_auth_for_public_ai_hub") is True
def test_update_ui_settings_syncs_public_health_readiness_details_to_general_settings(
self, mock_auth, monkeypatch
):