From 9d6d6c4fe02f01d955bed5775283c31efe3dd024 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:13:09 -0700 Subject: [PATCH] fix(agents): allow optional securityScheme fields so /public/agent_hub does not 500 (#33897) Co-authored-by: shivam Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/types/agents.py | 30 +++++------ .../public_endpoints/test_public_endpoints.py | 50 +++++++++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 4 +- 3 files changed, 67 insertions(+), 17 deletions(-) diff --git a/litellm/types/agents.py b/litellm/types/agents.py index 254ed5c6c7b..b9f2d7073d2 100644 --- a/litellm/types/agents.py +++ b/litellm/types/agents.py @@ -45,26 +45,26 @@ class SecuritySchemeBase(TypedDict, total=False): description: Optional[str] -class APIKeySecurityScheme(SecuritySchemeBase): +class APIKeySecurityScheme(SecuritySchemeBase, total=False): """Defines a security scheme using an API key.""" - type: Literal["apiKey"] - in_: Literal["query", "header", "cookie"] # using in_ to avoid Python keyword - name: str + type: Required[Literal["apiKey"]] + in_: Required[Literal["query", "header", "cookie"]] # using in_ to avoid Python keyword + name: Required[str] -class HTTPAuthSecurityScheme(SecuritySchemeBase): +class HTTPAuthSecurityScheme(SecuritySchemeBase, total=False): """Defines a security scheme using HTTP authentication.""" - type: Literal["http"] - scheme: str + type: Required[Literal["http"]] + scheme: Required[str] bearerFormat: Optional[str] -class MutualTLSSecurityScheme(SecuritySchemeBase): +class MutualTLSSecurityScheme(SecuritySchemeBase, total=False): """Defines a security scheme using mTLS authentication.""" - type: Literal["mutualTLS"] + type: Required[Literal["mutualTLS"]] class OAuthFlows(TypedDict, total=False): @@ -76,19 +76,19 @@ class OAuthFlows(TypedDict, total=False): password: Optional[Dict[str, Any]] -class OAuth2SecurityScheme(SecuritySchemeBase): +class OAuth2SecurityScheme(SecuritySchemeBase, total=False): """Defines a security scheme using OAuth 2.0.""" - type: Literal["oauth2"] - flows: OAuthFlows + type: Required[Literal["oauth2"]] + flows: Required[OAuthFlows] oauth2MetadataUrl: Optional[str] -class OpenIdConnectSecurityScheme(SecuritySchemeBase): +class OpenIdConnectSecurityScheme(SecuritySchemeBase, total=False): """Defines a security scheme using OpenID Connect.""" - type: Literal["openIdConnect"] - openIdConnectUrl: str + type: Required[Literal["openIdConnect"]] + openIdConnectUrl: Required[str] # Union of all security schemes 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 d920488c352..8b8b7871cce 100644 --- a/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/test_litellm/proxy/public_endpoints/test_public_endpoints.py @@ -600,6 +600,56 @@ def test_public_agent_hub_rewrites_upstream_url_to_proxy(): assert card["url"].endswith("/a2a/agent-123") +def test_public_agent_hub_serializes_http_security_scheme_without_bearer_format(): + """Regression: agents created through the UI carry an auto-generated + ``securitySchemes.LiteLLMKey`` of ``{"type": "http", "scheme": "bearer"}`` + with no ``bearerFormat``. The endpoint response_model must accept this + optional-field-omitted scheme; otherwise response validation raises and + /public/agent_hub returns 500, which the frontend swallows into an empty + list and hides the Agent Hub tab.""" + from litellm.types.agents import AgentResponse + + agent = AgentResponse( + agent_id="agent-123", + agent_name="public-agent", + agent_card_params={ + "name": "public-agent", + "url": "https://upstream.internal.example.com/a2a", + "securitySchemes": { + "LiteLLMKey": { + "type": "http", + "scheme": "bearer", + "description": "LiteLLM virtual key", + } + }, + }, + ) + + app = FastAPI() + app.include_router(router) + client = TestClient(app) + + mock_registry = MagicMock() + mock_registry.get_public_agent_list.return_value = [agent] + + with ( + patch("litellm.public_agent_groups", ["agent-123"]), + patch( + "litellm.proxy.agent_endpoints.agent_registry.global_agent_registry", + mock_registry, + ), + ): + response = client.get("/public/agent_hub") + + assert response.status_code == 200, response.text + payload = response.json() + assert len(payload) == 1 + scheme = payload[0]["securitySchemes"]["LiteLLMKey"] + assert scheme["type"] == "http" + assert scheme["scheme"] == "bearer" + assert "bearerFormat" not in scheme + + def test_public_agent_hub_returns_empty_when_no_public_groups(): app = FastAPI() app.include_router(router) diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 2ceb75a06a9..f173ce7b9d6 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -24341,7 +24341,7 @@ export interface components { */ HTTPAuthSecurityScheme: { /** Bearerformat */ - bearerFormat: string | null; + bearerFormat?: string | null; /** Description */ description?: string | null; /** Scheme */ @@ -28529,7 +28529,7 @@ export interface components { description?: string | null; flows: components["schemas"]["OAuthFlows"]; /** Oauth2Metadataurl */ - oauth2MetadataUrl: string | null; + oauth2MetadataUrl?: string | null; /** * Type * @constant