From b61537cd0f5a4efc4399fe6d3662414eebcae8e9 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 20 Feb 2026 21:14:29 -0800 Subject: [PATCH] feat: expose user_email in organization members response Add user_email field to LiteLLM_OrganizationMembershipTable with a model_validator that populates it from the nested user object, and update the /organization/info Prisma query to select user_email from the related user record. Co-Authored-By: Claude Sonnet 4.6 --- litellm/proxy/_types.py | 10 ++++++++ .../organization_endpoints.py | 8 +++++- .../test_organization_endpoints.py | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index ef471b29e6..10774ffb62 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2437,9 +2437,19 @@ class LiteLLM_OrganizationMembershipTable(LiteLLMPydanticObjectBase): Any ] = None # You might want to replace 'Any' with a more specific type if available litellm_budget_table: Optional[LiteLLM_BudgetTable] = None + user_email: Optional[str] = None model_config = ConfigDict(protected_namespaces=()) + @model_validator(mode="after") + def populate_user_email(self) -> "LiteLLM_OrganizationMembershipTable": + if self.user_email is None and self.user is not None: + if isinstance(self.user, dict): + self.user_email = self.user.get("user_email") + else: + self.user_email = getattr(self.user, "user_email", None) + return self + class LiteLLM_OrganizationTableUpdate(LiteLLM_BudgetTable): """Represents user-controllable params for a LiteLLM_OrganizationTable record""" diff --git a/litellm/proxy/management_endpoints/organization_endpoints.py b/litellm/proxy/management_endpoints/organization_endpoints.py index 94cfa82c0c..43c19cff72 100644 --- a/litellm/proxy/management_endpoints/organization_endpoints.py +++ b/litellm/proxy/management_endpoints/organization_endpoints.py @@ -721,7 +721,13 @@ async def info_organization(organization_id: str): where={"organization_id": organization_id}, include={ "litellm_budget_table": True, - "members": True, + "members": { + "include": { + "user": { + "select": {"user_email": True}, + } + } + }, "teams": True, "object_permission": True, }, diff --git a/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py index 2a2c37d03c..1f72b147ad 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_organization_endpoints.py @@ -535,3 +535,28 @@ async def test_list_organization_filter_by_org_alias(monkeypatch): "members": True, "teams": True, } + + +@pytest.mark.asyncio +async def test_organization_info_includes_user_email(monkeypatch): + """ + Test that GET /organization/info returns user_email in members list. + """ + from litellm.proxy._types import LiteLLM_OrganizationMembershipTable + from datetime import datetime + + # Simulate a membership row with a nested user object that has user_email + raw_membership = { + "user_id": "user_abc", + "organization_id": "org_xyz", + "user_role": "org_admin", + "spend": 0.0, + "budget_id": None, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow(), + "user": {"user_email": "alice@example.com"}, + "litellm_budget_table": None, + } + + membership = LiteLLM_OrganizationMembershipTable(**raw_membership) + assert membership.user_email == "alice@example.com"