From 9bc5ab73ac3500c13b68cc5ea5b161eebed23fbd Mon Sep 17 00:00:00 2001 From: Mrinal Chanshetty Date: Sat, 24 Oct 2026 20:20:49 -0500 Subject: [PATCH] fix(proxy): honor exclude_team_ids on the team tags page The tags page took its team from team_ids alone, so a team that was both named and excluded still returned its tag spend, while the aggregated team report returned nothing for the same request. It now returns an empty page after the permission check. Also drops a comment that restated the code. --- .../management_endpoints/daily_activity_routes.py | 10 +++++----- litellm/repositories/daily_activity_sql.py | 1 - .../test_daily_activity_routes.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/management_endpoints/daily_activity_routes.py b/litellm/proxy/management_endpoints/daily_activity_routes.py index e0f22757e93..ca3798e092c 100644 --- a/litellm/proxy/management_endpoints/daily_activity_routes.py +++ b/litellm/proxy/management_endpoints/daily_activity_routes.py @@ -64,6 +64,7 @@ from litellm.types.repositories.daily_activity import ( ExportType, KeyMetadataRow, KeySpendRow, + TagPage, UserMetadataRow, UserSpendRow, ) @@ -406,11 +407,10 @@ def _register_tag_page_route(router: APIRouter, resolver: EntityScopeResolver, p prisma_client, user_aggregated=False, ) - page: Final = await repository.tag_page( - resolved.scope, - team_id=team_ids[0], - offset=offset, - limit=limit, + page: Final = ( + TagPage(rows=(), total_tags=0) + if team_ids[0] in resolved.scope.exclude_entity_ids + else await repository.tag_page(resolved.scope, team_id=team_ids[0], offset=offset, limit=limit) ) return DailyActivityTagPageResponse( tags=list(page.rows), diff --git a/litellm/repositories/daily_activity_sql.py b/litellm/repositories/daily_activity_sql.py index 2ebf708c530..31e0f3d949a 100644 --- a/litellm/repositories/daily_activity_sql.py +++ b/litellm/repositories/daily_activity_sql.py @@ -117,7 +117,6 @@ def build_where_clause(scope: DailyActivityScope, *, start_index: int = 1) -> tu return " AND ".join(conditions), params -# Same rule as common_daily_activity._is_user_agent_tag _NOT_USER_AGENT_TAG: Final = "NOT (LOWER(BTRIM(tag)) LIKE 'user-agent:%' OR LOWER(BTRIM(tag)) LIKE 'user agent:%')" diff --git a/tests/unit/proxy/management_endpoints/test_daily_activity_routes.py b/tests/unit/proxy/management_endpoints/test_daily_activity_routes.py index fa637a5601e..3fe075cc6de 100644 --- a/tests/unit/proxy/management_endpoints/test_daily_activity_routes.py +++ b/tests/unit/proxy/management_endpoints/test_daily_activity_routes.py @@ -1378,6 +1378,20 @@ def test_team_tag_page_returns_404_for_a_non_member( repository.tag_page.assert_not_awaited() +def test_team_tag_page_is_empty_for_a_team_that_is_also_excluded( + daily_activity_client: tuple[TestClient, _FakeRepository], +) -> None: + client, repository = daily_activity_client + response: Final = client.get( + "/team/daily/activity/aggregated/tags", + params={**_entity_params("team_ids", "team-a"), "exclude_team_ids": "team-a", "offset": 0, "limit": 5}, + ) + + assert response.status_code == 200, response.text + assert response.json() == {"tags": [], "total_tags": 0, "offset": 0, "limit": 5} + repository.tag_page.assert_not_awaited() + + def test_team_aggregated_ignores_group_by_and_reads_the_team_table( daily_activity_client: tuple[TestClient, _FakeRepository], ) -> None: