From cd55488ce69988bba52b29b71e72676f98905b28 Mon Sep 17 00:00:00 2001 From: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> Date: Sat, 26 Sep 2026 14:52:00 -0700 Subject: [PATCH] fix(agents): hydrate identities on registry cache misses --- litellm/proxy/agent_endpoints/endpoints.py | 2 +- .../common_utils/registry_read_through.py | 5 ++- .../proxy/agent_endpoints/test_endpoints.py | 28 +++++++++++++++ .../test_registry_read_through.py | 34 +++++++++++++++++-- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/agent_endpoints/endpoints.py b/litellm/proxy/agent_endpoints/endpoints.py index 763d9cfa640..ee8a48e029c 100644 --- a/litellm/proxy/agent_endpoints/endpoints.py +++ b/litellm/proxy/agent_endpoints/endpoints.py @@ -592,7 +592,7 @@ async def get_agent_by_id( if agent is None: agent_row: Final = await agents_table(prisma_client).find_unique( where={"agent_id": agent_id}, - include={"object_permission": True}, + include={"object_permission": True, "identity": True}, ) if agent_row is not None: agent_dict: Final = agent_row.model_dump() diff --git a/litellm/proxy/common_utils/registry_read_through.py b/litellm/proxy/common_utils/registry_read_through.py index 63da5d15207..8a82e253c5c 100644 --- a/litellm/proxy/common_utils/registry_read_through.py +++ b/litellm/proxy/common_utils/registry_read_through.py @@ -174,7 +174,10 @@ async def _resync_agents(agent_id_or_name: str) -> bool: table: Final = agents_table(prisma_client) id_filter: Final[LiteLLM_AgentsTableWhereUniqueInput] = {"agent_id": agent_id_or_name} name_filter: Final[LiteLLM_AgentsTableWhereUniqueInput] = {"agent_name": agent_id_or_name} - include_permission: Final[LiteLLM_AgentsTableInclude] = {"object_permission": True} + include_permission: Final[LiteLLM_AgentsTableInclude] = { + "object_permission": True, + "identity": True, + } async with AGENT_RECONCILE_LOCK: if _agent_from_registry(agent_id_or_name) is not None: return True diff --git a/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py b/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py index fc1eab89899..7f316f9ca5b 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_endpoints.py @@ -1389,3 +1389,31 @@ def test_agent_identity_configuration_is_only_returned_to_admins(role, path, mon payload = response.json()[0] if path == "/v1/agents" else response.json() assert payload["identity"] == (binding.model_dump(mode="json") if role == LitellmUserRoles.PROXY_ADMIN else None) assert agent.identity == binding + + +@pytest.mark.parametrize("role", [LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.INTERNAL_USER]) +def test_agent_detail_cache_miss_preserves_admin_identity_visibility(role, monkeypatch): + binding = AgentIdentityBinding( + agent_id="agent-123", provider="microsoft_entra", tenant_id="tenant", client_id="client", + issuer="https://login.microsoftonline.com/tenant/v2.0", revision="revision", + ) + agent = _sample_agent_response() + registry = MagicMock() + registry.get_agent_by_id.return_value = None + registry.ids_for_agent.return_value = frozenset({agent.agent_id}) + monkeypatch.setattr(agent_endpoints, "AGENT_REGISTRY", registry) + monkeypatch.setattr( + "litellm.proxy.agent_endpoints.auth.agent_permission_handler.AgentRequestHandler.is_agent_allowed", + AsyncMock(return_value=True), + ) + + async def load_row(*, where, include): + assert where == {"agent_id": agent.agent_id} + return agent.model_copy(update={"identity": binding if include.get("identity") else None}) + + with patch("litellm.proxy.proxy_server.prisma_client") as prisma: + prisma.db.litellm_agentstable.find_unique = AsyncMock(side_effect=load_row) + prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + response = _make_app_with_role(role).get("/v1/agents/agent-123") + assert response.status_code == 200 + assert response.json()["identity"] == (binding.model_dump(mode="json") if role == LitellmUserRoles.PROXY_ADMIN else None) diff --git a/tests/test_litellm/proxy/common_utils/test_registry_read_through.py b/tests/test_litellm/proxy/common_utils/test_registry_read_through.py index ca2ff8bcce1..9e20386bf3d 100644 --- a/tests/test_litellm/proxy/common_utils/test_registry_read_through.py +++ b/tests/test_litellm/proxy/common_utils/test_registry_read_through.py @@ -177,7 +177,7 @@ async def test_get_agent_with_read_through_recovers_agent_created_on_sibling_rep assert agent.agent_id == agent_id prisma_client.db.litellm_agentstable.find_unique.assert_awaited_once_with( where={"agent_id": agent_id}, - include={"object_permission": True}, + include={"object_permission": True, "identity": True}, ) @@ -202,7 +202,7 @@ async def test_get_agent_with_read_through_recovers_agent_by_name(clean_agent_re assert agent.agent_name == agent_name prisma_client.db.litellm_agentstable.find_unique.assert_awaited_with( where={"agent_name": agent_name}, - include={"object_permission": True}, + include={"object_permission": True, "identity": True}, ) @@ -521,3 +521,33 @@ async def test_resync_agents_waits_for_agent_reload_and_skips_duplicate_registra assert await resync_task is True assert len(clean_agent_registry.agent_list) == 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("lookup", ["agent-id", "Agent name"]) +async def test_agent_read_through_hydrates_identity_binding(lookup, clean_agent_registry, fresh_agent_read_through, monkeypatch): + from types import SimpleNamespace + from unittest.mock import AsyncMock, MagicMock + + from litellm.proxy.common_utils.registry_read_through import get_agent_with_read_through + + binding = { + "agent_id": "agent-id", "provider": "microsoft_entra", "tenant_id": "tenant", "client_id": "client", + "issuer": "https://login.microsoftonline.com/tenant/v2.0", "revision": "revision", + } + + async def load_row(*, where, include): + if where == {"agent_id": "Agent name"}: + return None + row = FakeAgentRow("agent-id", "Agent name").model_dump() + return SimpleNamespace(model_dump=lambda: {**row, "identity": binding if include.get("identity") else None}) + + prisma = MagicMock() + prisma.db.litellm_agentstable.find_unique = AsyncMock(side_effect=load_row) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", prisma) + monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True) + agent = await get_agent_with_read_through(lookup) + assert agent is not None + assert agent.identity is not None + assert agent.identity.model_dump(include=set(binding)) == binding + assert clean_agent_registry.get_agent_by_id(agent_id="agent-id").identity == agent.identity