mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-07 00:24:03 +00:00
chore(proxy): keep public AI hub unauthenticated
This commit is contained in:
@@ -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",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user