diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py
index dc4f17c7b31..bad5149104a 100644
--- a/litellm/proxy/_types.py
+++ b/litellm/proxy/_types.py
@@ -265,6 +265,7 @@ class KeyManagementRoutes(str, enum.Enum):
# team usage routes
TEAM_DAILY_ACTIVITY = "/team/daily/activity"
+ TEAM_DAILY_ACTIVITY_AGGREGATED = "/team/daily/activity/aggregated"
# team spend-log viewing
SPEND_LOGS = "/spend/logs"
@@ -589,6 +590,7 @@ class LiteLLMRoutes(enum.Enum):
KeyManagementRoutes.KEY_BULK_UPDATE.value,
KeyManagementRoutes.TEAM_KEY_BULK_UPDATE.value,
KeyManagementRoutes.TEAM_DAILY_ACTIVITY.value,
+ KeyManagementRoutes.TEAM_DAILY_ACTIVITY_AGGREGATED.value,
KeyManagementRoutes.SPEND_LOGS.value,
KeyManagementRoutes.SPEND_LOGS_V2.value,
KeyManagementRoutes.KEY_RESET_SPEND.value,
@@ -623,6 +625,7 @@ class LiteLLMRoutes(enum.Enum):
"/team/permissions_update",
"/team/permissions_bulk_update",
"/team/daily/activity",
+ "/team/daily/activity/aggregated",
# gateway request counts (SGR); deployment-wide, admin-only
"/gateway/daily/activity",
# model
@@ -777,6 +780,7 @@ class LiteLLMRoutes(enum.Enum):
"/team/permissions_list",
"/team/permissions_update",
"/team/daily/activity",
+ "/team/daily/activity/aggregated",
"/team/{team_id}/members/me",
"/model/new",
"/model/update",
@@ -839,6 +843,7 @@ class LiteLLMRoutes(enum.Enum):
"/user/available_roles",
"/user/daily/activity",
"/team/daily/activity",
+ "/team/daily/activity/aggregated",
"/tag/daily/activity",
"/tag/list",
"/audit",
diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py
index d1542b38996..92f6fd1bf60 100644
--- a/litellm/proxy/management_endpoints/common_daily_activity.py
+++ b/litellm/proxy/management_endpoints/common_daily_activity.py
@@ -121,6 +121,7 @@ class _AggregatedSpendData(TypedDict):
class _GroupingSetsRow(SimpleNamespace):
date: str
+ entity_id: str | None = None
api_key: str | None
model: str | None
model_group: str | None
@@ -563,9 +564,10 @@ def _build_aggregated_sql_query(
start_date: str,
end_date: str,
model: str | None,
- api_key: str | None,
+ api_key: str | list[str] | None, # mutable-ok: filter union shared with the paginated path
exclude_entity_ids: list[str] | None = None,
timezone_offset_minutes: int | None = None,
+ entity_breakdown_field: str | None = None,
) -> tuple[str, list[str]]:
"""Build a parameterized SQL GROUP BY query for aggregated daily activity.
@@ -574,6 +576,11 @@ def _build_aggregated_sql_query(
The entity_id column is intentionally omitted from GROUP BY to collapse
rows across entities — this is where the biggest row reduction comes from.
+ When entity_breakdown_field is set (e.g. "team_id"), two extra rollup
+ levels are added — (date, entity) and (date, entity, api_key) — and the
+ entity column joins the GROUPING() call as the most-significant bit, so
+ every pre-existing rollup keeps its bitmask value plus that bit set.
+
Returns:
Tuple of (sql_query, params_list) ready for prisma_client.db.query_raw().
"""
@@ -621,8 +628,16 @@ def _build_aggregated_sql_query(
sql_params.append(model)
p += 1
- # Optional api_key filter
- if api_key:
+ # Optional api_key filter; an empty list must match nothing, not everything
+ if isinstance(api_key, list):
+ if api_key:
+ placeholders = ", ".join(f"${p + i}" for i in range(len(api_key)))
+ sql_conditions.append(f"api_key IN ({placeholders})")
+ sql_params.extend(api_key)
+ p += len(api_key)
+ else:
+ sql_conditions.append("FALSE")
+ elif api_key:
sql_conditions.append(f"api_key = ${p}")
sql_params.append(api_key)
p += 1
@@ -649,8 +664,16 @@ def _build_aggregated_sql_query(
if table_name == "litellm_dailyteamspend"
else "0::float AS ptu_flat_cost"
)
+ entity_select: Final = f'"{entity_breakdown_field}" AS entity_id,' if entity_breakdown_field else ""
+ entity_grouping_arg: Final = f'"{entity_breakdown_field}", ' if entity_breakdown_field else ""
+ entity_grouping_sets: Final = (
+ f'(date, "{entity_breakdown_field}"), (date, "{entity_breakdown_field}", api_key),'
+ if entity_breakdown_field
+ else ""
+ )
sql_query: Final = f"""
SELECT
+ {entity_select}
date,
api_key,
model,
@@ -658,7 +681,7 @@ def _build_aggregated_sql_query(
custom_llm_provider,
mcp_namespaced_tool_name,
endpoint,
- GROUPING(date, api_key, model, COALESCE(NULLIF(model_group, ''), model),
+ GROUPING({entity_grouping_arg}date, api_key, model, COALESCE(NULLIF(model_group, ''), model),
custom_llm_provider, mcp_namespaced_tool_name,
endpoint) AS group_level,
SUM(spend)::float AS spend,
@@ -677,6 +700,7 @@ def _build_aggregated_sql_query(
FROM "{pg_table}"
WHERE {where_clause}
GROUP BY GROUPING SETS (
+ {entity_grouping_sets}
(date),
(date, api_key),
(date, model),
@@ -796,6 +820,13 @@ _GROUP_DATE_MCP_API_KEY: Final = 29 # 0b0011101
_GROUP_DATE_ENDPOINT: Final = 62 # 0b0111110
_GROUP_DATE_ENDPOINT_API_KEY: Final = 30 # 0b0011110
+# When entity breakdown is on, the entity column is prepended to the GROUPING()
+# arguments as the most-significant bit. Every pre-existing rollup then carries
+# this bit (entity rolled up) on top of its value above, while the two new
+# entity rollup sets keep the bit clear: (date, entity) -> _GROUP_DATE and
+# (date, entity, api_key) -> _GROUP_DATE_API_KEY.
+_ENTITY_ROLLED_BIT: Final = 128 # 0b10000000
+
def _record_to_spend_metrics(record: _GroupingSetsRow) -> SpendMetrics:
"""Build a SpendMetrics directly from one already-aggregated rollup row.
@@ -832,6 +863,9 @@ def _aggregate_grouping_sets_records_sync(
*,
records: Sequence[_GroupingSetsRow],
api_key_metadata: Mapping[str, _KeyMetadataDict],
+ entity_breakdown: bool = False,
+ entity_metadata_field: Mapping[str, dict[str, object]]
+ | None = None, # mutable-ok: matches every entity_metadata_field signature in this file
) -> _AggregatedSpendData:
"""Build the response from rollup rows produced by the GROUPING SETS query.
@@ -871,11 +905,38 @@ def _aggregate_grouping_sets_records_sync(
metrics=metrics, metadata=_key_metadata(api_key_metadata, api_key)
)
+ def ensure_entity(entities: dict[str, MetricWithMetadata], entity_id: str) -> MetricWithMetadata:
+ existing: MetricWithMetadata | None = entities.get(entity_id)
+ if existing is not None:
+ return existing
+ created: Final = MetricWithMetadata(
+ metrics=SpendMetrics(),
+ metadata=(entity_metadata_field.get(entity_id, {}) if entity_metadata_field else {}),
+ )
+ entities[entity_id] = created
+ return created
+
+ def assign_entity_rollup(record: _GroupingSetsRow, metrics: SpendMetrics) -> None:
+ entity_id: Final = record.entity_id or "Unassigned"
+ entity_bucket: Final = ensure_entity(ensure_date(record.date)["breakdown"].entities, entity_id)
+ if record.group_level == _GROUP_DATE:
+ entity_bucket.metrics = metrics
+ return
+ if record.api_key and record.api_key != PTU_SENTINEL_API_KEY:
+ entity_bucket.api_key_breakdown[record.api_key] = KeyMetricWithMetadata(
+ metrics=metrics, metadata=_key_metadata(api_key_metadata, record.api_key)
+ )
+
for record in records:
- level = record.group_level
+ level = record.group_level & ~_ENTITY_ROLLED_BIT if entity_breakdown else record.group_level
metrics = _record_to_spend_metrics(record)
is_ptu_sentinel = record.api_key == PTU_SENTINEL_API_KEY
+ if entity_breakdown and not (record.group_level & _ENTITY_ROLLED_BIT):
+ if level in (_GROUP_DATE, _GROUP_DATE_API_KEY):
+ assign_entity_rollup(record, metrics)
+ continue
+
if level == _GROUP_GRAND_TOTAL:
total_metrics = metrics
continue
@@ -958,6 +1019,9 @@ async def _aggregate_grouping_sets_records(
*,
prisma_client: PrismaClient,
records: Sequence[_GroupingSetsRow],
+ entity_breakdown: bool = False,
+ entity_metadata_field: Mapping[str, dict[str, object]]
+ | None = None, # mutable-ok: matches every entity_metadata_field signature in this file
) -> _AggregatedSpendData:
"""Async wrapper: fetch api_key_metadata, then dispatch on a worker thread."""
api_keys: Final[set[str]] = {r.api_key for r in records if r.api_key and r.api_key != PTU_SENTINEL_API_KEY}
@@ -970,6 +1034,8 @@ async def _aggregate_grouping_sets_records(
_aggregate_grouping_sets_records_sync,
records=records,
api_key_metadata=api_key_metadata,
+ entity_breakdown=entity_breakdown,
+ entity_metadata_field=entity_metadata_field,
)
@@ -1106,9 +1172,10 @@ async def get_daily_activity_aggregated(
start_date: str | None,
end_date: str | None,
model: str | None,
- api_key: str | None,
+ api_key: str | list[str] | None, # mutable-ok: filter union shared with the paginated path
exclude_entity_ids: list[str] | None = None,
timezone_offset_minutes: int | None = None,
+ include_entity_breakdown: bool = False,
) -> SpendAnalyticsPaginatedResponse:
"""Aggregated variant that returns the full result set (no pagination).
@@ -1116,6 +1183,11 @@ async def get_daily_activity_aggregated(
all individual rows into Python. This collapses rows across entities
(users/teams/orgs), reducing ~150k rows to ~2-3k grouped rows.
+ include_entity_breakdown adds per-entity rollup levels to the GROUPING SETS
+ so `breakdown.entities` is populated (with entity_metadata_field metadata),
+ as entity-scoped views like Team Usage need — still one SQL pass, without
+ reverting to the per-leaf-row explosion that plain GROUP BY entity causes.
+
Matches the response model of the paginated endpoint so the UI does not need to transform.
"""
if prisma_client is None:
@@ -1141,6 +1213,7 @@ async def get_daily_activity_aggregated(
api_key=api_key,
exclude_entity_ids=exclude_entity_ids,
timezone_offset_minutes=timezone_offset_minutes,
+ entity_breakdown_field=entity_id_field if include_entity_breakdown else None,
)
# Execute GROUPING SETS query — returns one row per rollup level.
@@ -1155,6 +1228,8 @@ async def get_daily_activity_aggregated(
aggregated: Final = await _aggregate_grouping_sets_records(
prisma_client=prisma_client,
records=records,
+ entity_breakdown=include_entity_breakdown,
+ entity_metadata_field=entity_metadata_field,
)
return SpendAnalyticsPaginatedResponse(
diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py
index 60d3d650d00..6e14ecc7212 100644
--- a/litellm/proxy/management_endpoints/team_endpoints.py
+++ b/litellm/proxy/management_endpoints/team_endpoints.py
@@ -15,7 +15,7 @@ import math
import traceback
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
-from typing import Annotated, Final, Protocol, TypedDict, TypeVar, cast
+from typing import Annotated, Final, NamedTuple, Protocol, TypedDict, TypeVar, cast
import fastapi
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
@@ -87,6 +87,9 @@ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.callback_utils import encrypt_callback_vars
from litellm.proxy.common_utils.json_merge_patch import apply_json_merge_patch
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
+from litellm.proxy.management_endpoints.common_daily_activity import (
+ get_daily_activity_aggregated,
+)
from litellm.proxy.management_endpoints.common_utils import (
_check_passthrough_routes_caller_permission,
_is_user_org_admin_for_team,
@@ -5534,49 +5537,28 @@ async def _append_permissions_to_all_teams(prisma_client: PrismaClient, permissi
return teams_updated
-@router.get(
- "/team/daily/activity",
- response_model=SpendAnalyticsPaginatedResponse,
- tags=["team management"],
-)
-async def get_team_daily_activity(
- team_ids: str | None = None,
- start_date: str | None = None,
- end_date: str | None = None,
- model: str | None = None,
- api_key: str | None = None,
- page: int = 1,
- page_size: int = 10,
- exclude_team_ids: str | None = None,
- user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
-):
- """
- Get daily activity for specific teams or all teams.
+class _TeamDailyActivityScope(NamedTuple):
+ team_ids: list[str] | None # mutable-ok: downstream daily-activity signatures take str | list unions
+ exclude_team_ids: list[str] | None # mutable-ok: downstream daily-activity signatures take str | list unions
+ team_alias_metadata: dict[
+ str, dict[str, object]
+ ] # mutable-ok: entity_metadata_field contract is Mapping[str, dict]
+ api_key_filter: str | list[str] | None # mutable-ok: downstream daily-activity signatures take str | list unions
- Args:
- team_ids (Optional[str]): Comma-separated list of team IDs to filter by. If not provided, returns data for all teams.
- start_date (Optional[str]): Start date for the activity period (YYYY-MM-DD).
- end_date (Optional[str]): End date for the activity period (YYYY-MM-DD).
- model (Optional[str]): Filter by model name.
- api_key (Optional[str]): Filter by API key.
- page (int): Page number for pagination.
- page_size (int): Number of items per page.
- exclude_team_ids (Optional[str]): Comma-separated list of team IDs to exclude.
- Returns:
- SpendAnalyticsPaginatedResponse: Paginated response containing daily activity data.
- """
- from litellm.proxy.proxy_server import (
- prisma_client,
- proxy_logging_obj,
- user_api_key_cache,
- )
-
- if prisma_client is None:
- raise HTTPException(
- status_code=500,
- detail={"error": CommonProxyErrors.db_not_connected_error.value},
- )
+async def _resolve_team_daily_activity_scope(
+ *,
+ team_ids: str | None,
+ exclude_team_ids: str | None,
+ api_key: str | None,
+ user_api_key_dict: UserAPIKeyAuth,
+ prisma_client: PrismaClient,
+ user_api_key_cache: UserApiKeyCache,
+ proxy_logging_obj: ProxyLogging,
+) -> _TeamDailyActivityScope:
+ """Resolve which teams the caller may see and whether results must be
+ narrowed to their own API keys. Shared by the paginated and aggregated
+ /team/daily/activity endpoints so both enforce identical permissions."""
# Convert comma-separated tags string to list if provided
team_ids_list = team_ids.split(",") if team_ids else None
exclude_team_ids_list: list[str] | None = None
@@ -5659,17 +5641,149 @@ async def get_team_daily_activity(
if final_api_key_filter is None and user_api_keys is not None:
final_api_key_filter = user_api_keys
+ return _TeamDailyActivityScope(
+ team_ids=team_ids_list,
+ exclude_team_ids=exclude_team_ids_list,
+ team_alias_metadata=team_alias_metadata,
+ api_key_filter=final_api_key_filter,
+ )
+
+
+@router.get(
+ "/team/daily/activity",
+ response_model=SpendAnalyticsPaginatedResponse,
+ tags=["team management"],
+)
+async def get_team_daily_activity(
+ user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
+ team_ids: str | None = None,
+ start_date: str | None = None,
+ end_date: str | None = None,
+ model: str | None = None,
+ api_key: str | None = None,
+ page: int = 1,
+ page_size: int = 10,
+ exclude_team_ids: str | None = None,
+):
+ """
+ Get daily activity for specific teams or all teams.
+
+ Args:
+ team_ids (Optional[str]): Comma-separated list of team IDs to filter by. If not provided, returns data for all teams.
+ start_date (Optional[str]): Start date for the activity period (YYYY-MM-DD).
+ end_date (Optional[str]): End date for the activity period (YYYY-MM-DD).
+ model (Optional[str]): Filter by model name.
+ api_key (Optional[str]): Filter by API key.
+ page (int): Page number for pagination.
+ page_size (int): Number of items per page.
+ exclude_team_ids (Optional[str]): Comma-separated list of team IDs to exclude.
+ Returns:
+ SpendAnalyticsPaginatedResponse: Paginated response containing daily activity data.
+ """
+ from litellm.proxy.proxy_server import (
+ prisma_client,
+ proxy_logging_obj,
+ user_api_key_cache,
+ )
+
+ if prisma_client is None:
+ raise HTTPException(
+ status_code=500,
+ detail={"error": CommonProxyErrors.db_not_connected_error.value},
+ )
+
+ scope: Final = await _resolve_team_daily_activity_scope(
+ team_ids=team_ids,
+ exclude_team_ids=exclude_team_ids,
+ api_key=api_key,
+ user_api_key_dict=user_api_key_dict,
+ prisma_client=prisma_client,
+ user_api_key_cache=user_api_key_cache,
+ proxy_logging_obj=proxy_logging_obj,
+ )
+
return await get_daily_activity(
prisma_client=prisma_client,
table_name="litellm_dailyteamspend",
entity_id_field="team_id",
- entity_id=team_ids_list,
- entity_metadata_field=team_alias_metadata,
- exclude_entity_ids=exclude_team_ids_list,
+ entity_id=scope.team_ids,
+ entity_metadata_field=scope.team_alias_metadata,
+ exclude_entity_ids=scope.exclude_team_ids,
start_date=start_date,
end_date=end_date,
model=model,
- api_key=final_api_key_filter,
+ api_key=scope.api_key_filter,
page=page,
page_size=page_size,
)
+
+
+@router.get(
+ "/team/daily/activity/aggregated",
+ response_model=SpendAnalyticsPaginatedResponse,
+ tags=["team management"],
+)
+async def get_team_daily_activity_aggregated(
+ user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
+ team_ids: str | None = None,
+ start_date: str | None = None,
+ end_date: str | None = None,
+ model: str | None = None,
+ api_key: str | None = None,
+ exclude_team_ids: str | None = None,
+ timezone: int | None = None,
+):
+ """
+ Aggregated daily activity for teams without pagination, including per-team breakdown.
+
+ One SQL GROUPING SETS pass returns every day in the range regardless of row
+ volume, so callers never reassemble pages. Same response shape as the
+ paginated endpoint with page metadata pinned to a single page.
+
+ Args:
+ team_ids (Optional[str]): Comma-separated list of team IDs to filter by. If not provided, returns data for all teams.
+ start_date (Optional[str]): Start date for the activity period (YYYY-MM-DD).
+ end_date (Optional[str]): End date for the activity period (YYYY-MM-DD).
+ model (Optional[str]): Filter by model name.
+ api_key (Optional[str]): Filter by API key.
+ exclude_team_ids (Optional[str]): Comma-separated list of team IDs to exclude.
+ timezone (Optional[int]): Timezone offset in minutes from UTC, matching JavaScript's Date.getTimezoneOffset() convention.
+ Returns:
+ SpendAnalyticsPaginatedResponse: Response containing all daily activity data for the range.
+ """
+ from litellm.proxy.proxy_server import (
+ prisma_client,
+ proxy_logging_obj,
+ user_api_key_cache,
+ )
+
+ if prisma_client is None:
+ raise HTTPException(
+ status_code=500,
+ detail={"error": CommonProxyErrors.db_not_connected_error.value},
+ )
+
+ scope: Final = await _resolve_team_daily_activity_scope(
+ team_ids=team_ids,
+ exclude_team_ids=exclude_team_ids,
+ api_key=api_key,
+ user_api_key_dict=user_api_key_dict,
+ prisma_client=prisma_client,
+ user_api_key_cache=user_api_key_cache,
+ proxy_logging_obj=proxy_logging_obj,
+ )
+
+ return await get_daily_activity_aggregated(
+ prisma_client=prisma_client,
+ table_name="litellm_dailyteamspend",
+ entity_id_field="team_id",
+ entity_id=scope.team_ids,
+ entity_metadata_field=scope.team_alias_metadata,
+ start_date=start_date,
+ end_date=end_date,
+ model=model,
+ api_key=scope.api_key_filter,
+ exclude_entity_ids=scope.exclude_team_ids,
+ timezone_offset_minutes=timezone,
+ include_entity_breakdown=True,
+ )
diff --git a/tests/proxy_behavior/management/test_team_daily_activity.py b/tests/proxy_behavior/management/test_team_daily_activity.py
index 7a1e70b91fc..d84cc4c94af 100644
--- a/tests/proxy_behavior/management/test_team_daily_activity.py
+++ b/tests/proxy_behavior/management/test_team_daily_activity.py
@@ -5,11 +5,12 @@ from .actors import Actor
pytestmark = pytest.mark.asyncio(loop_scope="session")
-# GET /team/daily/activity. A proxy admin (admin view) sees activity for any
-# team. A non-admin is scoped to user_info.teams: a bare query defaults to its
-# own teams (200), and an explicit team_ids filter naming a team it does not
-# belong to is 404 (the VERIA-43 fix). Org admins have no team memberships, so
-# they behave like a non-member for any specific team.
+# GET /team/daily/activity and its /aggregated variant (same shared scope
+# resolver, so the matrix must hold for both). A proxy admin (admin view) sees
+# activity for any team. A non-admin is scoped to user_info.teams: a bare query
+# defaults to its own teams (200), and an explicit team_ids filter naming a
+# team it does not belong to is 404 (the VERIA-43 fix). Org admins have no
+# team memberships, so they behave like a non-member for any specific team.
_MEMBERS = {
"alpha": {
Actor.TEAM_ADMIN,
@@ -40,13 +41,18 @@ _CASES = [
_DATES = "start_date=2024-01-01&end_date=2024-12-31"
+@pytest.mark.parametrize(
+ "endpoint",
+ ("/team/daily/activity", "/team/daily/activity/aggregated"),
+ ids=("paginated", "aggregated"),
+)
@pytest.mark.parametrize(
"actor,team,expected_status",
[(a, t, s) for (_id, a, t, s) in _CASES],
ids=[c[0] for c in _CASES],
)
async def test_team_daily_activity_matrix(
- actor: Actor, team: str, expected_status: int, proxy_client, world
+ actor: Actor, team: str, expected_status: int, endpoint: str, proxy_client, world
):
query = _DATES
if team == "alpha":
@@ -55,7 +61,7 @@ async def test_team_daily_activity_matrix(
query += f"&team_ids={world.team_beta_id}"
resp = await proxy_client.get(
- f"/team/daily/activity?{query}",
+ f"{endpoint}?{query}",
headers={"Authorization": f"Bearer {world.keys[actor].cleartext}"},
)
assert (
diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py
index ab9b4bc3922..eb2e3243ac0 100644
--- a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py
+++ b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py
@@ -1719,3 +1719,133 @@ class TestFlagIsNotReadOnTheHotPath:
reads = self._count_flag_reads([_spend_record(PTU_SENTINEL_API_KEY, spend=0.0, ptu_flat_cost=240.0)])
assert reads > 0
+
+
+def test_build_aggregated_sql_query_entity_breakdown_and_api_key_list():
+ """entity_breakdown_field adds the two entity rollup sets with the entity
+ column as the most-significant GROUPING bit; a list api_key filter becomes
+ a parameterized IN, and an empty list must match nothing (not everything)."""
+ sql, params = _build_aggregated_sql_query(
+ table_name="litellm_dailyteamspend",
+ entity_id_field="team_id",
+ entity_id=None,
+ start_date="2024-01-01",
+ end_date="2024-01-31",
+ model=None,
+ api_key=["key-1", "key-2"],
+ entity_breakdown_field="team_id",
+ )
+ assert '"team_id" AS entity_id' in sql
+ assert 'GROUPING("team_id", date' in sql
+ assert '(date, "team_id"), (date, "team_id", api_key),' in sql
+ assert "api_key IN ($3, $4)" in sql
+ assert params == ["2024-01-01", "2024-01-31", "key-1", "key-2"]
+
+ plain_sql, _ = _build_aggregated_sql_query(
+ table_name="litellm_dailyteamspend",
+ entity_id_field="team_id",
+ entity_id=None,
+ start_date="2024-01-01",
+ end_date="2024-01-31",
+ model=None,
+ api_key=None,
+ )
+ assert "entity_id" not in plain_sql
+ assert "GROUPING(date" in plain_sql
+
+ empty_sql, empty_params = _build_aggregated_sql_query(
+ table_name="litellm_dailyteamspend",
+ entity_id_field="team_id",
+ entity_id=None,
+ start_date="2024-01-01",
+ end_date="2024-01-31",
+ model=None,
+ api_key=[],
+ )
+ assert "FALSE" in empty_sql
+ assert empty_params == ["2024-01-01", "2024-01-31"]
+
+
+@pytest.mark.asyncio
+async def test_get_daily_activity_aggregated_with_entity_breakdown():
+ """include_entity_breakdown must populate breakdown.entities from the
+ (date, entity) / (date, entity, api_key) rollups while every pre-existing
+ rollup still dispatches correctly with the entity bit (128) set."""
+ mock_prisma = MagicMock()
+ mock_prisma.db = MagicMock()
+
+ base = {
+ "entity_id": None,
+ "model": None,
+ "model_group": None,
+ "custom_llm_provider": None,
+ "mcp_namespaced_tool_name": None,
+ "endpoint": None,
+ "api_key": None,
+ "cache_read_input_tokens": 0,
+ "cache_creation_input_tokens": 0,
+ "compression_saved_tokens": 0,
+ "compression_savings_spend": 0.0,
+ "prompt_caching_savings_spend": 0.0,
+ "autorouter_savings_spend": 0.0,
+ "failed_requests": 0,
+ "prompt_tokens": 0,
+ "completion_tokens": 0,
+ "api_requests": 0,
+ "successful_requests": 0,
+ }
+ mock_rows = [
+ # () grand total: 127 + entity bit
+ {**base, "date": None, "group_level": 255, "spend": 18.0},
+ # (date): 63 + entity bit
+ {**base, "date": "2024-01-01", "group_level": 191, "spend": 18.0},
+ # (date, entity): entity bit clear
+ {**base, "date": "2024-01-01", "entity_id": "team-a", "group_level": 63, "spend": 12.0},
+ {**base, "date": "2024-01-01", "entity_id": "team-b", "group_level": 63, "spend": 6.0},
+ # (date, entity, api_key): entity bit clear
+ {**base, "date": "2024-01-01", "entity_id": "team-a", "api_key": "key-1", "group_level": 31, "spend": 12.0},
+ {**base, "date": "2024-01-01", "entity_id": "team-b", "api_key": "key-2", "group_level": 31, "spend": 6.0},
+ # (date, model): 47 + entity bit
+ {**base, "date": "2024-01-01", "model": "gpt-4o", "group_level": 175, "spend": 18.0},
+ # (date, api_key): 31 + entity bit
+ {**base, "date": "2024-01-01", "api_key": "key-1", "group_level": 159, "spend": 12.0},
+ ]
+
+ mock_prisma.db.query_raw = AsyncMock(return_value=mock_rows)
+ mock_prisma.db.litellm_verificationtoken = MagicMock()
+ mock_prisma.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[])
+
+ result = await get_daily_activity_aggregated(
+ prisma_client=mock_prisma,
+ table_name="litellm_dailyteamspend",
+ entity_id_field="team_id",
+ entity_id=None,
+ entity_metadata_field={"team-a": {"team_alias": "Alpha"}},
+ start_date="2024-01-01",
+ end_date="2024-01-01",
+ model=None,
+ api_key=None,
+ include_entity_breakdown=True,
+ )
+
+ sql = mock_prisma.db.query_raw.call_args[0][0]
+ assert '"team_id" AS entity_id' in sql
+ assert '(date, "team_id"), (date, "team_id", api_key),' in sql
+
+ assert result.metadata.total_spend == 18.0
+ assert len(result.results) == 1
+ daily = result.results[0]
+ assert daily.metrics.spend == 18.0
+
+ entities = daily.breakdown.entities
+ assert set(entities) == {"team-a", "team-b"}
+ assert entities["team-a"].metrics.spend == 12.0
+ assert entities["team-a"].metadata == {"team_alias": "Alpha"}
+ assert entities["team-a"].api_key_breakdown["key-1"].metrics.spend == 12.0
+ assert entities["team-b"].metrics.spend == 6.0
+ assert entities["team-b"].metadata == {}
+ assert entities["team-b"].api_key_breakdown["key-2"].metrics.spend == 6.0
+
+ # Rollups with the entity bit set must still land in their usual buckets
+ assert daily.breakdown.models["gpt-4o"].metrics.spend == 18.0
+ assert daily.breakdown.api_keys["key-1"].metrics.spend == 12.0
diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py
index 6abc40eb28e..1d96dc8ef53 100644
--- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py
+++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py
@@ -11272,3 +11272,81 @@ async def test_new_team_output_token_estimate_rejected_for_non_admin():
assert str(exc.value.code) == "403"
assert "on a team" in str(exc.value.message)
+
+
+@pytest.mark.asyncio
+async def test_get_team_daily_activity_aggregated_scopes_and_flags(mock_db_client):
+ """The aggregated endpoint must apply the same non-admin key scoping as the
+ paginated one and request the per-team entity breakdown with the caller's
+ timezone, so the Team Usage UI gets every day in one response."""
+ from litellm.proxy.management_endpoints.team_endpoints import (
+ get_team_daily_activity_aggregated,
+ )
+
+ user_id = "test_user_123"
+ team_id = "test_team_456"
+ user_api_key_dict = UserAPIKeyAuth(
+ user_id=user_id, user_role=LitellmUserRoles.INTERNAL_USER
+ )
+
+ mock_user_info = LiteLLM_UserTable(
+ user_id=user_id,
+ teams=[team_id],
+ max_budget=1000.0,
+ spend=0.0,
+ user_email="test@example.com",
+ user_role="internal_user",
+ )
+
+ mock_team_member = Member(user_id=user_id, role="user")
+ mock_team = MagicMock(spec=LiteLLM_TeamTable)
+ mock_team.team_id = team_id
+ mock_team.team_alias = "Test Team"
+ mock_team.members_with_roles = [mock_team_member]
+ mock_team.model_dump.return_value = {
+ "team_id": team_id,
+ "team_alias": "Test Team",
+ "members_with_roles": [{"user_id": user_id, "role": "user"}],
+ }
+
+ user_api_key_1 = MagicMock()
+ user_api_key_1.token = "user_key_1"
+
+ mock_db_client.db.litellm_teamtable.find_many = AsyncMock(return_value=[mock_team])
+ mock_db_client.db.litellm_verificationtoken.find_many = AsyncMock(
+ return_value=[user_api_key_1]
+ )
+
+ with patch(
+ "litellm.proxy.management_endpoints.team_endpoints.get_user_object",
+ new_callable=AsyncMock,
+ ) as mock_get_user_object:
+ mock_get_user_object.return_value = mock_user_info
+
+ with patch(
+ "litellm.proxy.management_endpoints.team_endpoints.get_daily_activity_aggregated",
+ new_callable=AsyncMock,
+ ) as mock_aggregated:
+ mock_aggregated.return_value = MagicMock()
+
+ await get_team_daily_activity_aggregated(
+ team_ids=team_id,
+ start_date="2024-01-01",
+ end_date="2024-01-31",
+ model=None,
+ api_key=None,
+ exclude_team_ids=None,
+ timezone=480,
+ user_api_key_dict=user_api_key_dict,
+ )
+
+ mock_aggregated.assert_called_once()
+ call_kwargs = mock_aggregated.call_args[1]
+ assert call_kwargs["api_key"] == ["user_key_1"]
+ assert call_kwargs["entity_id"] == [team_id]
+ assert call_kwargs["entity_metadata_field"] == {
+ team_id: {"team_alias": "Test Team"}
+ }
+ assert call_kwargs["include_entity_breakdown"] is True
+ assert call_kwargs["timezone_offset_minutes"] == 480
+ assert call_kwargs["table_name"] == "litellm_dailyteamspend"
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx
index c85a9fb71f6..883e953a24d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx
@@ -17,6 +17,7 @@ beforeAll(() => {
vi.mock("@/components/networking", () => ({
tagDailyActivityCall: vi.fn(),
teamDailyActivityCall: vi.fn(),
+ teamDailyActivityAggregatedCall: vi.fn(),
organizationDailyActivityCall: vi.fn(),
customerDailyActivityCall: vi.fn(),
agentDailyActivityCall: vi.fn(),
@@ -79,6 +80,7 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
describe("EntityUsage", () => {
const mockTagDailyActivityCall = vi.mocked(networking.tagDailyActivityCall);
const mockTeamDailyActivityCall = vi.mocked(networking.teamDailyActivityCall);
+ const mockTeamDailyActivityAggregatedCall = vi.mocked(networking.teamDailyActivityAggregatedCall);
const mockOrganizationDailyActivityCall = vi.mocked(networking.organizationDailyActivityCall);
const mockCustomerDailyActivityCall = vi.mocked(networking.customerDailyActivityCall);
const mockAgentDailyActivityCall = vi.mocked(networking.agentDailyActivityCall);
@@ -370,12 +372,14 @@ describe("EntityUsage", () => {
beforeEach(() => {
mockTagDailyActivityCall.mockClear();
mockTeamDailyActivityCall.mockClear();
+ mockTeamDailyActivityAggregatedCall.mockClear();
mockOrganizationDailyActivityCall.mockClear();
mockCustomerDailyActivityCall.mockClear();
mockAgentDailyActivityCall.mockClear();
mockUserDailyActivityCall.mockClear();
mockTagDailyActivityCall.mockResolvedValue(mockSpendData);
mockTeamDailyActivityCall.mockResolvedValue(mockSpendData);
+ mockTeamDailyActivityAggregatedCall.mockResolvedValue(mockSpendData);
mockOrganizationDailyActivityCall.mockResolvedValue(mockSpendData);
mockCustomerDailyActivityCall.mockResolvedValue(mockSpendData);
mockAgentDailyActivityCall.mockResolvedValue(mockAgentSpendData);
@@ -404,7 +408,7 @@ describe("EntityUsage", () => {
render();
await waitFor(() => {
- expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
});
// Check that it shows team-specific label
@@ -536,7 +540,7 @@ describe("EntityUsage", () => {
const { container } = render();
await waitFor(() => {
- expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
});
act(() => {
@@ -688,7 +692,7 @@ describe("EntityUsage", () => {
render();
await waitFor(() => {
- expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
});
expect(screen.getByText("Agent Activity")).toBeInTheDocument();
@@ -708,7 +712,7 @@ describe("EntityUsage", () => {
render();
await waitFor(() => {
- expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
});
expect(screen.getByText("Top Agents Driving Spend")).toBeInTheDocument();
@@ -752,7 +756,7 @@ describe("EntityUsage", () => {
render();
await waitFor(() => {
- expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
});
const agentActivityTab = screen.getByText("Agent Activity");
@@ -888,7 +892,7 @@ describe("EntityUsage", () => {
render();
await waitFor(() => {
- expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
});
expect(screen.getByText("Team Spend Overview")).toBeInTheDocument();
@@ -945,7 +949,7 @@ describe("EntityUsage", () => {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
};
- mockTeamDailyActivityCall.mockResolvedValue({
+ mockTeamDailyActivityAggregatedCall.mockResolvedValue({
...mockSpendData,
results: [
{
@@ -967,4 +971,31 @@ describe("EntityUsage", () => {
expect(screen.getByText("top-models:gpt-4o=70.25")).toBeInTheDocument();
expect(screen.getByText(/^top-models:Code Review Agent=/)).toBeInTheDocument();
});
+
+ it("uses the aggregated team endpoint and never drains paginated pages for teams", async () => {
+ render();
+
+ await waitFor(() => {
+ expect(mockTeamDailyActivityAggregatedCall).toHaveBeenCalled();
+ });
+ expect(mockTeamDailyActivityCall).not.toHaveBeenCalled();
+
+ await waitFor(() => {
+ expect(screen.getAllByText("$100.50").length).toBeGreaterThan(0);
+ });
+ });
+
+ it("falls back to the paginated team endpoint when the aggregated call fails", async () => {
+ mockTeamDailyActivityAggregatedCall.mockRejectedValue(new Error("aggregated unavailable"));
+
+ render();
+
+ await waitFor(() => {
+ expect(mockTeamDailyActivityCall).toHaveBeenCalled();
+ });
+
+ await waitFor(() => {
+ expect(screen.getAllByText("$100.50").length).toBeGreaterThan(0);
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx
index 956060fc244..3a90f61bb86 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx
@@ -44,6 +44,7 @@ import {
customerDailyActivityCall,
organizationDailyActivityCall,
tagDailyActivityCall,
+ teamDailyActivityAggregatedCall,
teamDailyActivityCall,
userDailyActivityCall,
} from "@/components/networking";
@@ -108,6 +109,12 @@ const ENTITY_FETCH_FNS: Record Promise> = {
user: userDailyActivityCall,
};
+// Single-shot endpoints returning the whole range in one response; entity types
+// without one fall back to page-draining the paginated endpoint.
+const ENTITY_AGGREGATED_FETCH_FNS: Partial Promise>> = {
+ team: teamDailyActivityAggregatedCall,
+};
+
const ENTITY_CAPABILITIES: Partial> = {
organization: "viewOrganizationUsage",
agent: "viewAgentUsage",
@@ -138,6 +145,7 @@ const EntityUsage: React.FC = ({
}, [entityType, selectedTags]);
const fetchFn = ENTITY_FETCH_FNS[entityType];
+ const aggregatedFetchFn = ENTITY_AGGREGATED_FETCH_FNS[entityType];
const entityCapability = ENTITY_CAPABILITIES[entityType];
const canViewEntity = entityCapability === undefined || hasCapability(userRole, entityCapability);
const showAgentBreakdown = entityType === "team" && hasCapability(userRole, "viewAgentUsage");
@@ -154,6 +162,7 @@ const EntityUsage: React.FC = ({
fetchFn,
args: [accessToken, startTime, endTime, entityFilterArg],
enabled,
+ aggregatedFetchFn,
});
const spendData = spendDataRaw as unknown as EntitySpendData;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts
index a8bfee5be2f..453c9fae8e4 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts
@@ -40,6 +40,13 @@ interface UsePaginatedDailyActivityParams {
args: any[];
/** Whether the hook should fetch. Set to false to disable. */
enabled: boolean;
+ /**
+ * Optional single-shot endpoint returning the whole range at once (e.g.
+ * teamDailyActivityAggregatedCall). Called with `args` as-is (no page).
+ * On success pagination is skipped entirely; on failure the hook falls
+ * back to the paginated flow.
+ */
+ aggregatedFetchFn?: (...args: any[]) => Promise;
}
interface UsePaginatedDailyActivityReturn {
@@ -96,6 +103,7 @@ export function usePaginatedDailyActivity({
fetchFn,
args,
enabled,
+ aggregatedFetchFn,
}: UsePaginatedDailyActivityParams): UsePaginatedDailyActivityReturn {
const [data, setData] = useState(EMPTY_DATA);
const [loading, setLoading] = useState(false);
@@ -159,6 +167,20 @@ export function usePaginatedDailyActivity({
setIsFetchingMore(false);
setProgress({ currentPage: 1, totalPages: 1 });
+ if (aggregatedFetchFn) {
+ try {
+ const aggregated = await aggregatedFetchFn(...currentArgs);
+ if (isStale()) return;
+ setData(aggregated);
+ setProgress({ currentPage: 1, totalPages: 1 });
+ setLoading(false);
+ return;
+ } catch (error) {
+ if (isStale()) return;
+ console.error("Aggregated daily activity failed, falling back to pagination:", error);
+ }
+ }
+
try {
// Inject page=1 as the 4th argument.
const argsWithPage = [...currentArgs.slice(0, 3), 1, ...currentArgs.slice(3)];
@@ -239,7 +261,7 @@ export function usePaginatedDailyActivity({
};
// argsKey is a stable JSON string so the effect only re-fires when arg values change.
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [enabled, fetchFn, argsKey]);
+ }, [enabled, fetchFn, aggregatedFetchFn, argsKey]);
return { data, loading, isFetchingMore, progress, cancelled, cancel };
}
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 321dfd6b2a6..594340a58d0 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -1452,6 +1452,32 @@ export const teamDailyActivityCall = async (
});
};
+export const teamDailyActivityAggregatedCall = async (
+ accessToken: string,
+ startTime: Date,
+ endTime: Date,
+ teamIds: string[] | null = null,
+) => {
+ /**
+ * Get aggregated daily team activity with per-team breakdown (no pagination)
+ */
+ try {
+ return await apiClient.get(`/team/daily/activity/aggregated`, {
+ accessToken,
+ query: {
+ start_date: formatDate(startTime),
+ end_date: formatDate(endTime),
+ timezone: new Date().getTimezoneOffset().toString(),
+ team_ids: teamIds && teamIds.length > 0 ? teamIds.join(",") : undefined,
+ exclude_team_ids: "litellm-dashboard",
+ },
+ });
+ } catch (error) {
+ console.error("Failed to fetch aggregated team daily activity:", error);
+ throw error;
+ }
+};
+
export const organizationDailyActivityCall = async (
accessToken: string,
startTime: Date,
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index 8989e098f54..d0f0c0a2f0f 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -13729,6 +13729,41 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/team/daily/activity/aggregated": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * Get Team Daily Activity Aggregated
+ * @description Aggregated daily activity for teams without pagination, including per-team breakdown.
+ *
+ * One SQL GROUPING SETS pass returns every day in the range regardless of row
+ * volume, so callers never reassemble pages. Same response shape as the
+ * paginated endpoint with page metadata pinned to a single page.
+ *
+ * Args:
+ * team_ids (Optional[str]): Comma-separated list of team IDs to filter by. If not provided, returns data for all teams.
+ * start_date (Optional[str]): Start date for the activity period (YYYY-MM-DD).
+ * end_date (Optional[str]): End date for the activity period (YYYY-MM-DD).
+ * model (Optional[str]): Filter by model name.
+ * api_key (Optional[str]): Filter by API key.
+ * exclude_team_ids (Optional[str]): Comma-separated list of team IDs to exclude.
+ * timezone (Optional[int]): Timezone offset in minutes from UTC, matching JavaScript's Date.getTimezoneOffset() convention.
+ * Returns:
+ * SpendAnalyticsPaginatedResponse: Response containing all daily activity data for the range.
+ */
+ get: operations["get_team_daily_activity_aggregated_team_daily_activity_aggregated_get"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/team/delete": {
parameters: {
query?: never;
@@ -25699,7 +25734,7 @@ export interface components {
* @description Enum for key management routes
* @enum {string}
*/
- KeyManagementRoutes: "/key/generate" | "/key/update" | "/key/delete" | "/key/regenerate" | "/key/service-account/generate" | "/key/{key_id}/regenerate" | "/key/block" | "/key/unblock" | "/key/bulk_update" | "/team/key/bulk_update" | "/key/{key_id}/reset_spend" | "/key/access_group_assignment" | "/key/info" | "/key/health" | "/key/list" | "/key/aliases" | "/team/daily/activity" | "/spend/logs" | "/spend/logs/v2";
+ KeyManagementRoutes: "/key/generate" | "/key/update" | "/key/delete" | "/key/regenerate" | "/key/service-account/generate" | "/key/{key_id}/regenerate" | "/key/block" | "/key/unblock" | "/key/bulk_update" | "/team/key/bulk_update" | "/key/{key_id}/reset_spend" | "/key/access_group_assignment" | "/key/info" | "/key/health" | "/key/list" | "/key/aliases" | "/team/daily/activity" | "/team/daily/activity/aggregated" | "/spend/logs" | "/spend/logs/v2";
/**
* KeyManagementSystem
* @enum {string}
@@ -52512,6 +52547,43 @@ export interface operations {
};
};
};
+ get_team_daily_activity_aggregated_team_daily_activity_aggregated_get: {
+ parameters: {
+ query?: {
+ team_ids?: string | null;
+ start_date?: string | null;
+ end_date?: string | null;
+ model?: string | null;
+ api_key?: string | null;
+ exclude_team_ids?: string | null;
+ timezone?: number | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["SpendAnalyticsPaginatedResponse"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
delete_team_team_delete_post: {
parameters: {
query?: never;