Repository navigation
fix(access_groups): derive attached teams from the team table and reject unknown team ids - #39218
Merged
ryan-crabbe-berri merged 2 commits intoSep 3, 2026
Conversation
…ect unknown team ids
GET /v1/access_group and GET /v1/access_group/{id} (and the /v1/unified_access_group aliases) used to
return the assigned_team_ids column verbatim. That column is a denormalized mirror of
LiteLLM_TeamTable.access_group_ids and can be stale or hold ids of teams that no longer exist, so the
Attached Teams view drifted from reality.
The read path now runs one team find_many per request, unioning teams whose access_group_ids carry any
group in the response with teams listed in the stored columns. Only real team rows come back, so ghost
ids drop out and teams the mirror missed are added. The stored order is kept for ids that survive and
newly discovered teams are appended.
Create and update now resolve the requested assigned_team_ids inside the transaction and answer 400
with the missing ids before anything is written, instead of silently storing ids that point nowhere.
Refs LIT-6593
Claude-Session: https://claude.ai/code/session_01QvQzYztinxj8ZuD5YxbVdL
Team membership deltas on PUT now start from the teams that really carry the group, so a team the mirror column missed can be detached. Read endpoints go through a typed TeamRepository instead of the untyped db handle, and the where clause always carries both OR arms. Claude-Session: https://claude.ai/code/session_01QvQzYztinxj8ZuD5YxbVdL
Contributor
Greptile SummaryThis PR makes the team table authoritative when reading and updating access-group team attachments and rejects references to teams that do not exist.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported issues were withdrawn after the contract change was confirmed intentional and the new database reads were confirmed to be outside the critical LLM request path.
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/access_group_endpoints.py | Reconciles access-group team attachments against team records and validates referenced teams before writes; no eligible blocking issue remains. |
| litellm/repositories/table_repositories.py | Adds the standard repository wrapper for the team table. |
| tests/test_litellm/proxy/management_endpoints/test_access_group_endpoints.py | Expands coverage for derived attachments, ghost rejection, and synchronization of previously unmirrored teams. |
Reviews (2): Last reviewed commit: "fix(access_groups): reconcile update del..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Contributor
Author
|
@greptileai please re-review: both earlier findings were withdrawn, so the confidence score should be refreshed against the tip |
ryan-crabbe-berri
enabled auto-merge
September 3, 2026 22:29
yuneng-berri
approved these changes
Sep 3, 2026
ryan-crabbe-berri
merged commit Sep 3, 2026
a5b3bc8
into
litellm_internal_staging
79 of 80 checks passed
ryan-crabbe-berri
deleted the
litellm_lit_6593_access_group_attached_teams
branch
September 3, 2026 22:30
This was referenced Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
How it solves it:
Unknown team ids: ...for ids that resolve to no teamUser Flow
Before: an admin opens an access group and the Attached Teams card is wrong in both directions
team-that-does-not-exist-8024, count 3, and the third real team is missingGET /v1/access_group/<id>and get the same three ids backPUT /v1/access_group/<id>with the real team plus the ghost id and get 200, so the ghost is saved againAfter: the card shows the teams that really carry the group, and bad ids are rejected
GET /v1/access_group/<id>and get the same three real ids backPUT /v1/access_group/<id>with the real team plus the ghost id and get400 {"detail": "Unknown team ids: team-that-does-not-exist-8024"}, nothing changesRelevant issues
Linear ticket
Resolves LIT-6593
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Setup, worktree proxy on
:4592against the shared dev Postgres, dashboard dev server on:3592. Access group1936eb56-aeb4-4932-af4e-86012582d24fhas three teams attached (lit-6593-team-alpha92851a2d...,lit-6593-team-beta0580ab49...,lit-6593-team-gamma4766b850...). Drift was simulated the way old data looks in the field: the group's stored team column was set to alpha, beta andteam-that-does-not-exist-8024, so gamma carries the group but is missing from the column and the ghost id is presentBefore (97dbd8e)
GET detail
curl -s $P/v1/access_group/$AG -H "$K" | jq '{access_group_name, assigned_team_ids}'{"access_group_name":"lit-6592-platform-tools","assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","0580ab49-3559-46c6-822e-13b20ce4c545","team-that-does-not-exist-8024"]}GET list
curl -s $P/v1/access_group -H "$K" | jq '.[] | select(.access_group_id=="'$AG'") | .assigned_team_ids'["92851a2d-c6db-41ac-ae6e-f8e37786f82f","0580ab49-3559-46c6-822e-13b20ce4c545","team-that-does-not-exist-8024"]POST with a team id that does not exist
curl -s -w 'HTTP %{http_code}\n' -X POST $P/v1/access_group -H "$K" -H 'Content-Type: application/json' -d '{"access_group_name":"lit-6593-ghost-probe","assigned_team_ids":["team-that-does-not-exist-8024"]}'HTTP 201with{"access_group_id":"87be2b8a-6798-4ec7-a319-2242479ed824","assigned_team_ids":["team-that-does-not-exist-8024"]}PUT with a team id that does not exist
curl -s -w 'HTTP %{http_code}\n' -X PUT $P/v1/access_group/$AG -H "$K" -H 'Content-Type: application/json' -d '{"assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","team-that-does-not-exist-8024"]}'HTTP 200with{"access_group_id":"1936eb56-aeb4-4932-af4e-86012582d24f","assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","team-that-does-not-exist-8024"]}PUT that drops gamma, then read gamma back
curl -s -w 'HTTP %{http_code}\n' -X PUT $P/v1/access_group/$AG -H "$K" -H 'Content-Type: application/json' -d '{"assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","0580ab49-3559-46c6-822e-13b20ce4c545"]}'HTTP 200curl -s "$P/team/info?team_id=4766b850-d699-4bb9-bcb4-63349d324b26" -H "$K" | jq -c '.team_info | {team_alias, access_group_ids}'{"team_alias":"lit-6593-team-gamma","access_group_ids":["1936eb56-aeb4-4932-af4e-86012582d24f"]}, gamma still carries the group it was just removed fromAdmin UI
1936eb56-aeb4-4932-af4e-86012582d24fAfter (c14cf9d)
The probe group from Before was deleted and the drifted column re-seeded to the same alpha, beta, ghost state before this run. The Admin UI screenshot on each side was taken before that side's PUT case
GET detail
curl -s $P/v1/access_group/$AG -H "$K" | jq '{access_group_name, assigned_team_ids}'{"access_group_name":"lit-6592-platform-tools","assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","0580ab49-3559-46c6-822e-13b20ce4c545","4766b850-d699-4bb9-bcb4-63349d324b26"]}GET list
curl -s $P/v1/access_group -H "$K" | jq '.[] | select(.access_group_id=="'$AG'") | .assigned_team_ids'["92851a2d-c6db-41ac-ae6e-f8e37786f82f","0580ab49-3559-46c6-822e-13b20ce4c545","4766b850-d699-4bb9-bcb4-63349d324b26"]POST with a team id that does not exist
curl -s -w 'HTTP %{http_code}\n' -X POST $P/v1/access_group -H "$K" -H 'Content-Type: application/json' -d '{"access_group_name":"lit-6593-ghost-probe","assigned_team_ids":["team-that-does-not-exist-8024"]}'HTTP 400with{"detail":"Unknown team ids: team-that-does-not-exist-8024"}PUT with a team id that does not exist
curl -s -w 'HTTP %{http_code}\n' -X PUT $P/v1/access_group/$AG -H "$K" -H 'Content-Type: application/json' -d '{"assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","team-that-does-not-exist-8024"]}'HTTP 400with{"detail":"Unknown team ids: team-that-does-not-exist-8024"}PUT that drops gamma, then read gamma back
curl -s -w 'HTTP %{http_code}\n' -X PUT $P/v1/access_group/$AG -H "$K" -H 'Content-Type: application/json' -d '{"assigned_team_ids":["92851a2d-c6db-41ac-ae6e-f8e37786f82f","0580ab49-3559-46c6-822e-13b20ce4c545"]}'HTTP 200curl -s "$P/team/info?team_id=4766b850-d699-4bb9-bcb4-63349d324b26" -H "$K" | jq -c '.team_info | {team_alias, access_group_ids}'{"team_alias":"lit-6593-team-gamma","access_group_ids":[]}, gamma is detachedAdmin UI
1936eb56-aeb4-4932-af4e-86012582d24fType
🐛 Bug Fix
Caveats (if any)
Severe
/v1/access_group: a team id that resolves to no team used to be stored silently (201/200), it is now a 400. Any automation that seeds groups before the teams exist has to create the teams firstLow
assigned_team_idscolumn is not backfilled; it self-heals on the next PUT that includesassigned_team_ids, and reads no longer depend on itassigned_key_idshas the same mirror shape and was left alone; that is a separate ticket if it bitesFinal Attestation