Skip to content

fix: populate team member emails missing from the roster snapshot - #37759

Merged
ryan-crabbe-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_team_info_member_email
Aug 21, 2026
Merged

fix: populate team member emails missing from the roster snapshot#37759
ryan-crabbe-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_team_info_member_email

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Title

fix: populate team member emails missing from the roster snapshot

Relevant issues

The Admin UI's team member table renders - under User Email for a member whose user row plainly has an email.

Root cause

members_with_roles is a denormalized JSON snapshot on LiteLLM_TeamTable, written once when the member is added. /team/info returns that blob verbatim — there is no join to LiteLLM_UserTable anywhere in the read path — so once an entry is stored with user_email: null, nothing ever repairs it.

Entries land with a null email legitimately: a member added by user_id whose user row had no email yet (_validate_and_populate_member_user_info correctly has nothing to copy), or a roster row written before that populate step existed. The moment that user gets an email, the roster is stale and stays stale forever.

Reproduced live against the dev proxy:

# user created with no email, then added to the team by user_id
curl -X POST :4077/team/member_add -d '{"team_id":"...","member":{"role":"admin","user_id":"ccd211ed-..."}}'
# the user later gets an email
curl -X POST :4077/user/update  -d '{"user_id":"ccd211ed-...","user_email":"ryan@berri.ai"}'

curl :4077/user/info?user_id=ccd211ed-...   # -> user_email: "ryan@berri.ai"
curl :4077/team/info?team_id=...            # -> user_email: null      <-- stale

Before / after

Same team, same database rows, same UI — only the proxy code differs.

Beforeccd211ed-… shows -, even though that user's row holds ryan@berri.ai:

Team members table before the fix: the third member's User Email column shows a dash

After — the email resolves, with no change to the stored roster row:

Team members table after the fix: the third member's User Email column shows ryan@berri.ai

Default Proxy Admin still shows - in both — that user row genuinely has no email. The fix resolves what exists; it does not invent values.

And the same thing at the API layer:

// GET /team/info -> team_info.members_with_roles, with the fix
[
  { "user_id": "default_user_id",    "user_email": null,            "role": "admin" },  // no email on the user row
  { "user_id": "1c1cf1b8-...",       "user_email": "dana@berri.ai", "role": "admin" },  // stored, passed through
  { "user_id": "ccd211ed-...",       "user_email": "ryan@berri.ai", "role": "admin" }   // hydrated
]

The fix

  • Read path (the fix)/team/info fills blank emails from LiteLLM_UserTable before responding, so the rows already sitting in the database display correctly with no migration.
  • Write path (symmetry)_update_team_members_list backfilled user_id from user_email but never the reverse. _resolve_member_identity now resolves both directions off the user rows the add just touched, so the helper is no longer one-way for any caller that reaches it without going through _validate_and_populate_member_user_info. The add/dedupe logic was also duplicated across the single-member and bulk branches; both now share _member_already_in_team.

Is this a behavior change to /team/info?

Only in the sense that a null becomes the correct value. Explicitly not a contract change:

  • A member that already carries a stored email is passed through untouched — the snapshot stays the source of truth wherever it has a value, so an email that deliberately differs from the user row is never silently rewritten.
  • A user row with no email leaves the member as null rather than inventing one.

Cost

One extra WHERE user_id IN (...) on the primary-key index, and only for the members actually missing an email — a roster that is already complete pays for no query at all. /team/info already issues unbounded queries for all team keys and all team memberships, so this is noise next to what the endpoint does today.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Testing

Live proxy verification is the before/after above. Unit tests added in tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py:

  • test_hydrate_member_emails_fills_in_emails_the_roster_snapshot_never_captured
  • test_hydrate_member_emails_never_overwrites_a_stored_email
  • test_hydrate_member_emails_leaves_members_alone_when_the_user_row_has_no_email
  • test_hydrate_member_emails_skips_the_query_when_every_member_has_one
  • test_team_info_hydrates_member_emails_from_the_user_table (endpoint-level; also asserts only the blank member is looked up)
  • test_update_team_members_list_stamps_email_for_a_member_added_by_user_id
  • test_update_team_members_list_stamps_email_for_each_member_in_a_bulk_add
pytest tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py \
       tests/test_litellm/proxy/management_helpers/
479 passed

basedpyright error counts on team_endpoints.py are identical before and after, and ruff check / ruff format --check are clean on the changed source file.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR hydrates missing team-roster emails from user records while preserving stored snapshot values. It also resolves member identities without mutating submitted models and consolidates roster deduplication

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/team_endpoints.py Adds read-time email hydration and immutable bidirectional identity resolution for team roster entries
tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py Adds focused coverage for hydration, stored-email preservation, query skipping, and single or bulk identity resolution

Reviews (2): Last reviewed commit: "fix: populate team member emails missing..." | Re-trigger Greptile

Comment on lines +4091 to +4093

user_rows: Final[Sequence[LiteLLM_UserTable]] = await _user_db(prisma_client).find_many(
where={"user_id": {"in": sorted(missing_user_ids)}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Direct request-path database query

For rosters containing a member with a user ID but no stored email, _hydrate_member_emails calls find_many directly from /team/info, bypassing the repository's required user-lookup helpers and adding an independently managed database round trip to the request path.

Rule Used: What: In critical path of request, there should be... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +2639 to +2640
member.user_id = user.user_id
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Identity resolution mutates inputs

_resolve_member_identity assigns resolved fields directly onto the supplied Member; in the bulk branch these are the request's original objects, so later consumers observe values not present in the submitted model and identity resolution becomes coupled to mutable shared state.

Context Used: CLAUDE.md (source)

@ryan-crabbe-berri
ryan-crabbe-berri changed the base branch from main to litellm_internal_staging August 21, 2026 02:26
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ryan-crabbe-berri
ryan-crabbe-berri force-pushed the litellm_team_info_member_email branch from d769564 to 28b9175 Compare August 21, 2026 02:35
`members_with_roles` is a denormalized JSON snapshot written at add-time.
`_update_team_members_list` backfilled `user_id` from `user_email` but never
the reverse, so a member added by `user_id` alone was stored with
`user_email=None` permanently - and `/team/info` returns that blob verbatim
with no join to `LiteLLM_UserTable`, so the Admin UI's member table renders
"-" for a user that plainly has an email.

Fix both ends:

- write path: `_resolve_member_identity` resolves identity both ways off the
  user rows the add just touched, so new roster entries stop being born blank.
- read path: `/team/info` fills blank emails from `LiteLLM_UserTable` in one
  indexed `user_id IN (...)` query, repairing rows already in the database.
  Members that already carry an email are passed through untouched and cost
  no query, so this only ever turns a null into the right value.
@ryan-crabbe-berri
ryan-crabbe-berri force-pushed the litellm_team_info_member_email branch from 28b9175 to 16cd080 Compare August 21, 2026 02:45
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@tin-berri tin-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix for a real display bug — members_with_roles is an add-time snapshot, so a member added by user_id alone carries user_email=None forever even after the user row gets one. Two solid pieces: _resolve_member_identity makes write-time identity resolution bidirectional (previously only user_id was backfilled from email; now email is backfilled from user_id too), and _hydrate_member_emails fills blanks at read time in /team/info via one batched find_many query, never touching a stored value. No auth/security surface — this is read-only enrichment of display data, no new write paths. Test coverage is thorough: fill-blanks-only, never-overwrite, no-op when the user row also has no email, skip-the-query-when-nothing's-missing, plus both the single-member and bulk-add write paths and an end-to-end /team/info test. CI green. Approved.

@codspeed-hq

codspeed-hq Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_team_info_member_email (16cd080) with litellm_internal_staging (65b4ac0)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (a112ba5) during the generation of this report, so eb6296e was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@ryan-crabbe-berri
ryan-crabbe-berri merged commit b64f180 into litellm_internal_staging Aug 21, 2026
71 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_team_info_member_email branch August 21, 2026 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants