diff --git a/litellm/proxy/public_endpoints/public_endpoints.py b/litellm/proxy/public_endpoints/public_endpoints.py index 29c9cb571ca..2288d02c35f 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -1,5 +1,6 @@ import json import os +import re from typing import List import litellm @@ -23,11 +24,16 @@ from litellm.types.proxy.public_endpoints.public_endpoints import ( AgentCreateInfo, ProviderCreateInfo, PublicModelHubInfo, + SupportedEndpointInfo, + SupportedEndpointsResponse, + SupportedProviderInfo, ) from litellm.types.utils import LlmProviders router = APIRouter() +_supported_endpoints_cache: SupportedEndpointsResponse | None = None + @router.get( "/public/model_hub", @@ -225,6 +231,68 @@ async def get_litellm_blog_posts(): return BlogPostsResponse(posts=posts) +@router.get( + "/public/supported_endpoints", + tags=["public", "providers"], + response_model=SupportedEndpointsResponse, +) +async def get_provider_supported_endpoints() -> SupportedEndpointsResponse: + """ + Return all supported endpoints and which providers support them. + + Reads from provider_endpoints_support.json at the repo root. + Result is cached for the lifetime of the process. + """ + global _supported_endpoints_cache + if _supported_endpoints_cache is not None: + return _supported_endpoints_cache + + provider_endpoints_support_path = os.path.join( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + ), + "provider_endpoints_support.json", + ) + + with open(provider_endpoints_support_path, "r") as f: + data = json.load(f) + + schema_endpoints = data["_schema"]["provider_slug"]["endpoints"] + + endpoints = [] + for key, description in schema_endpoints.items(): + path_match = re.search(r"(/[\w/{}.()*-]+)", description) + endpoint_path = path_match.group(1) if path_match else f"/{key}" + display_name = key.replace("_", " ").title() + endpoints.append( + SupportedEndpointInfo( + key=key, + display_name=display_name, + endpoint=endpoint_path, + ) + ) + + providers = [] + for slug, provider_data in data["providers"].items(): + supported = [ + endpoint_key + for endpoint_key, supported in provider_data["endpoints"].items() + if supported + ] + providers.append( + SupportedProviderInfo( + slug=slug, + display_name=provider_data["display_name"], + supported=supported, + ) + ) + + _supported_endpoints_cache = SupportedEndpointsResponse( + endpoints=endpoints, providers=providers + ) + return _supported_endpoints_cache + + @router.get( "/public/agents/fields", tags=["public", "[beta] Agents"], diff --git a/litellm/types/proxy/public_endpoints/public_endpoints.py b/litellm/types/proxy/public_endpoints/public_endpoints.py index 57d68771c7f..a167eeec6a2 100644 --- a/litellm/types/proxy/public_endpoints/public_endpoints.py +++ b/litellm/types/proxy/public_endpoints/public_endpoints.py @@ -52,3 +52,20 @@ class AgentCreateInfo(BaseModel): credential_fields: List[AgentCredentialField] litellm_params_template: Optional[Dict[str, str]] = None model_template: Optional[str] = None + + +class SupportedEndpointInfo(BaseModel): + key: str + display_name: str + endpoint: str + + +class SupportedProviderInfo(BaseModel): + slug: str + display_name: str + supported: List[str] + + +class SupportedEndpointsResponse(BaseModel): + endpoints: List[SupportedEndpointInfo] + providers: List[SupportedProviderInfo] diff --git a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py index 5f5e2cf1ff8..0b21bc2636c 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -87,6 +87,54 @@ def test_get_litellm_model_cost_map_returns_cost_map(): assert "input_cost_per_token" in sample_model_data or "output_cost_per_token" in sample_model_data +def test_get_provider_supported_endpoints(): + """Test /public/supported_endpoints returns correct structure with endpoints and providers.""" + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + response = client.get("/public/supported_endpoints") + + assert response.status_code == 200 + data = response.json() + + # Check top-level structure + assert "endpoints" in data + assert "providers" in data + assert isinstance(data["endpoints"], list) + assert isinstance(data["providers"], list) + + # Verify endpoints structure + assert len(data["endpoints"]) > 0 + for endpoint in data["endpoints"]: + assert "key" in endpoint + assert "display_name" in endpoint + assert "endpoint" in endpoint + assert isinstance(endpoint["key"], str) + assert isinstance(endpoint["display_name"], str) + assert endpoint["endpoint"].startswith("/") + + # Verify providers structure + assert len(data["providers"]) > 0 + for provider in data["providers"]: + assert "slug" in provider + assert "display_name" in provider + assert "supported" in provider + assert isinstance(provider["slug"], str) + assert isinstance(provider["display_name"], str) + assert isinstance(provider["supported"], list) + + # Verify some expected endpoints exist + endpoint_keys = {e["key"] for e in data["endpoints"]} + assert "chat_completions" in endpoint_keys + assert "embeddings" in endpoint_keys + assert "responses" in endpoint_keys + + # Verify some expected providers exist + provider_slugs = {p["slug"] for p in data["providers"]} + assert "openai" in provider_slugs + + def test_watsonx_provider_fields(): """Test that Watsonx provider has all required credential fields including multiple auth options.""" app = FastAPI()