Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4557,10 +4557,10 @@ class DefaultInternalUserParams(LiteLLMPydanticObjectBase):

user_role: (
Literal[
LitellmUserRoles.INTERNAL_USER,
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
LitellmUserRoles.PROXY_ADMIN,
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
LitellmUserRoles.INTERNAL_USER,
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
]
| None
) = Field(
Expand Down
47 changes: 47 additions & 0 deletions litellm/proxy/db/autorouter_session_rollup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,53 @@
CACHE_TTL_5M_SECONDS: Final = 300
CACHE_TTL_1H_SECONDS: Final = 3600

AUTOROUTER_BENCHMARKS_SQL: Final = """
WITH windowed AS (
SELECT * FROM "LiteLLM_AutoRouterSession"
WHERE last_turn_at >= $1::timestamp AND first_turn_at < $2::timestamp
),
tier_maps AS (
SELECT router_name, router_type, jsonb_object_agg(tier, tier_turns) AS tier_turns
FROM (
SELECT router_name, router_type, kv.key AS tier, SUM((kv.value)::int)::int AS tier_turns
FROM windowed, LATERAL jsonb_each_text(tier_turns) AS kv
GROUP BY router_name, router_type, kv.key
) per_tier
GROUP BY router_name, router_type
)
SELECT
agg.*,
COALESCE(tier_maps.tier_turns, '{}'::jsonb) AS tier_turns
FROM (
SELECT
router_name,
router_type,
COUNT(*)::int AS sessions,
COALESCE(SUM(turns), 0)::int AS turns,
COALESCE(SUM(unordered_turns), 0)::int AS unordered_turns,
COALESCE(SUM(covered_turns), 0)::int AS covered_turns,
COALESCE(SUM(cache_hits), 0)::int AS cache_hits,
COALESCE(SUM(same_model_turns), 0)::int AS same_model_turns,
COALESCE(SUM(same_model_hits), 0)::int AS same_model_hits,
COALESCE(SUM(first_visit_turns), 0)::int AS first_visit_turns,
COALESCE(SUM(first_visit_hits), 0)::int AS first_visit_hits,
COALESCE(SUM(return_turns), 0)::int AS return_turns,
COALESCE(SUM(return_hits), 0)::int AS return_hits,
COALESCE(SUM(return_expired_misses), 0)::int AS return_expired_misses,
COALESCE(SUM(return_within_ttl_misses), 0)::int AS return_within_ttl_misses,
COALESCE(SUM(ttl_5m_turns), 0)::int AS ttl_5m_turns,
COALESCE(SUM(ttl_1h_turns), 0)::int AS ttl_1h_turns,
COALESCE(SUM(total_tokens), 0)::bigint AS total_tokens,
COALESCE(SUM(spend), 0)::float8 AS spend,
COALESCE(SUM(saved_spend), 0)::float8 AS saved_spend,
COALESCE(SUM(EXTRACT(EPOCH FROM (last_turn_at - first_turn_at))), 0)::float8 AS session_seconds
FROM windowed
GROUP BY router_name, router_type
) agg
LEFT JOIN tier_maps USING (router_name, router_type)
ORDER BY agg.spend DESC
"""


@dataclass(frozen=True, slots=True)
class AutoRouterTurnTransaction:
Expand Down
50 changes: 2 additions & 48 deletions litellm/proxy/management_endpoints/auto_router_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
can_key_call_resolved_model,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.db.autorouter_session_rollup import AUTOROUTER_BENCHMARKS_SQL
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
from litellm.repositories.team_repository import TeamRepository
from litellm.router_strategy.complexity_router import ComplexityRouter
Expand Down Expand Up @@ -285,53 +286,6 @@ class _SessionAggRow(BaseModel):

_SESSION_AGG_ROWS: Final = TypeAdapter(list[_SessionAggRow])

_BENCHMARKS_SQL: Final = """
WITH windowed AS (
SELECT * FROM "LiteLLM_AutoRouterSession"
WHERE last_turn_at >= $1::timestamp AND first_turn_at < $2::timestamp
),
tier_maps AS (
SELECT router_name, router_type, jsonb_object_agg(tier, tier_turns) AS tier_turns
FROM (
SELECT router_name, router_type, kv.key AS tier, SUM((kv.value)::int)::int AS tier_turns
FROM windowed, LATERAL jsonb_each_text(tier_turns) AS kv
GROUP BY router_name, router_type, kv.key
) per_tier
GROUP BY router_name, router_type
)
SELECT
agg.*,
COALESCE(tier_maps.tier_turns, '{}'::jsonb) AS tier_turns
FROM (
SELECT
router_name,
router_type,
COUNT(*)::int AS sessions,
COALESCE(SUM(turns), 0)::int AS turns,
COALESCE(SUM(unordered_turns), 0)::int AS unordered_turns,
COALESCE(SUM(covered_turns), 0)::int AS covered_turns,
COALESCE(SUM(cache_hits), 0)::int AS cache_hits,
COALESCE(SUM(same_model_turns), 0)::int AS same_model_turns,
COALESCE(SUM(same_model_hits), 0)::int AS same_model_hits,
COALESCE(SUM(first_visit_turns), 0)::int AS first_visit_turns,
COALESCE(SUM(first_visit_hits), 0)::int AS first_visit_hits,
COALESCE(SUM(return_turns), 0)::int AS return_turns,
COALESCE(SUM(return_hits), 0)::int AS return_hits,
COALESCE(SUM(return_expired_misses), 0)::int AS return_expired_misses,
COALESCE(SUM(return_within_ttl_misses), 0)::int AS return_within_ttl_misses,
COALESCE(SUM(ttl_5m_turns), 0)::int AS ttl_5m_turns,
COALESCE(SUM(ttl_1h_turns), 0)::int AS ttl_1h_turns,
COALESCE(SUM(total_tokens), 0)::bigint AS total_tokens,
COALESCE(SUM(spend), 0)::float8 AS spend,
COALESCE(SUM(saved_spend), 0)::float8 AS saved_spend,
COALESCE(SUM(EXTRACT(EPOCH FROM (last_turn_at - first_turn_at))), 0)::float8 AS session_seconds
FROM windowed
GROUP BY router_name, router_type
) agg
LEFT JOIN tier_maps USING (router_name, router_type)
ORDER BY agg.spend DESC
"""


def _parse_benchmark_day(value: str) -> datetime:
try:
Expand Down Expand Up @@ -455,7 +409,7 @@ async def get_auto_router_benchmarks(
raise HTTPException(status_code=400, detail="end_date must not be earlier than start_date")

raw_rows: Final = await prisma_client.db.query_raw(
_BENCHMARKS_SQL,
AUTOROUTER_BENCHMARKS_SQL,
start_day.isoformat(),
(end_day + timedelta(days=1)).isoformat(),
)
Expand Down
2 changes: 1 addition & 1 deletion tests/agent_tests/test_a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self):
name="mock-agent", url="http://mock-agent.local"
)

async def send_message(self, request):
async def send_message(self, request, *, context=None):
from a2a.compat.v0_3.conversions import pb2_v10

for text in ("hel", "hello"):
Expand Down
24 changes: 18 additions & 6 deletions tests/litellm_utils_tests/test_proxy_budget_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,12 @@ async def test_service_logger_keys_success():
logger success hook is called with the correct event metadata and no exception is logged.
"""
keys = [
{"id": "key1", "spend": 10.0, "budget_duration": 60, "token": "key1"},
{"id": "key2", "spend": 15.0, "budget_duration": 60, "token": "key2"},
_attrify(
{"id": "key1", "spend": 10.0, "budget_duration": 60, "token": "key1"}
),
_attrify(
{"id": "key2", "spend": 15.0, "budget_duration": 60, "token": "key2"}
),
]
prisma_client = MagicMock()
prisma_client.get_data = AsyncMock(return_value=keys)
Expand Down Expand Up @@ -740,8 +744,12 @@ async def test_service_logger_users_success():
the correct metadata and no exception is logged.
"""
users = [
{"id": "user1", "spend": 20.0, "budget_duration": 120, "user_id": "user1"},
{"id": "user2", "spend": 25.0, "budget_duration": 120, "user_id": "user2"},
_attrify(
{"id": "user1", "spend": 20.0, "budget_duration": 120, "user_id": "user1"}
),
_attrify(
{"id": "user2", "spend": 25.0, "budget_duration": 120, "user_id": "user2"}
),
]
prisma_client = MagicMock()
prisma_client.get_data = AsyncMock(return_value=users)
Expand Down Expand Up @@ -853,8 +861,12 @@ async def test_service_logger_teams_success():
the proper metadata and nothing is logged as an exception.
"""
teams = [
{"id": "team1", "spend": 30.0, "budget_duration": 180, "team_id": "team1"},
{"id": "team2", "spend": 35.0, "budget_duration": 180, "team_id": "team2"},
_attrify(
{"id": "team1", "spend": 30.0, "budget_duration": 180, "team_id": "team1"}
),
_attrify(
{"id": "team2", "spend": 35.0, "budget_duration": 180, "team_id": "team2"}
),
]
prisma_client = MagicMock()
prisma_client.get_data = AsyncMock(return_value=teams)
Expand Down
4 changes: 2 additions & 2 deletions tests/llm_responses_api_testing/base_responses_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ async def test_basic_openai_responses_get_endpoint(self, sync_mode):
)
assert result is not None
assert result.id == response.id
assert result.output == response.output
assert result.output_text == response.output_text
else:
raise ValueError("response is not a ResponsesAPIResponse")
else:
Expand All @@ -352,7 +352,7 @@ async def test_basic_openai_responses_get_endpoint(self, sync_mode):
)
assert result is not None
assert result.id == response.id
assert result.output == response.output
assert result.output_text == response.output_text
else:
raise ValueError("response is not a ResponsesAPIResponse")

Expand Down
16 changes: 9 additions & 7 deletions tests/proxy_behavior/spend/test_autorouter_session_rollup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

import pytest

from litellm.proxy.db.autorouter_session_rollup import UPSERT_AUTOROUTER_SESSION_SQL
from litellm.proxy.management_endpoints.auto_router_endpoints import _BENCHMARKS_SQL
from litellm.proxy.db.autorouter_session_rollup import (
AUTOROUTER_BENCHMARKS_SQL,
UPSERT_AUTOROUTER_SESSION_SQL,
)

pytestmark = pytest.mark.asyncio(loop_scope="session")

Expand Down Expand Up @@ -164,7 +166,7 @@ async def test_the_benchmarks_aggregate_reads_only_overlapping_sessions(db):
await _turn(db, key, "A", T0 - timedelta(days=40), session_id=out_of_window, router=router)

rows = await db.query_raw(
_BENCHMARKS_SQL,
AUTOROUTER_BENCHMARKS_SQL,
(T0 - timedelta(days=1)).isoformat(),
(T0 + timedelta(days=1)).isoformat(),
)
Expand All @@ -186,7 +188,7 @@ async def test_a_reconfigured_alias_reports_each_router_type_as_its_own_group(db
await _turn(db, key, "A", T0 + timedelta(seconds=10), session_id=f"s-{uuid.uuid4()}", router=router, router_type="quality")

rows = await db.query_raw(
_BENCHMARKS_SQL,
AUTOROUTER_BENCHMARKS_SQL,
(T0 - timedelta(days=1)).isoformat(),
(T0 + timedelta(days=1)).isoformat(),
)
Expand Down Expand Up @@ -248,7 +250,7 @@ async def test_the_benchmarks_aggregate_sums_tier_turns_across_sessions(db):
await _turn(db, key, "C", T0 + timedelta(seconds=30), session_id=f"s-{uuid.uuid4()}", router=router, tier=None)

rows = await db.query_raw(
_BENCHMARKS_SQL,
AUTOROUTER_BENCHMARKS_SQL,
(T0 - timedelta(days=1)).isoformat(),
(T0 + timedelta(days=1)).isoformat(),
)
Expand All @@ -275,7 +277,7 @@ async def test_tier_maps_stay_separate_per_router_type_on_a_reconfigured_alias(d
)

rows = await db.query_raw(
_BENCHMARKS_SQL,
AUTOROUTER_BENCHMARKS_SQL,
(T0 - timedelta(days=1)).isoformat(),
(T0 + timedelta(days=1)).isoformat(),
)
Expand All @@ -289,7 +291,7 @@ async def test_a_window_with_no_tiered_turns_aggregates_to_an_empty_map(db):
await _turn(db, key, "A", T0, session_id=f"s-{uuid.uuid4()}", router=router, tier=None)

rows = await db.query_raw(
_BENCHMARKS_SQL,
AUTOROUTER_BENCHMARKS_SQL,
(T0 - timedelta(days=1)).isoformat(),
(T0 + timedelta(days=1)).isoformat(),
)
Expand Down
23 changes: 1 addition & 22 deletions ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
}));

vi.mock("@/app/(dashboard)/hooks/keys/useKeys", () => ({
useKeys: vi.fn().mockReturnValue({

Check warning on line 122 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 4 properties passed inline as an argument; assign it to a named variable first
data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 },
isPending: false,
isFetching: false,
Expand Down Expand Up @@ -206,26 +206,26 @@
mockUseAllProxyModels.mockReturnValue({
data: { data: [] },
isLoading: false,
} as any);

Check warning on line 209 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
mockUseTeam.mockReturnValue({
data: undefined,
isLoading: false,
} as any);

Check warning on line 213 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
mockUseOrganization.mockReturnValue({
data: undefined,
isLoading: false,
} as any);

Check warning on line 217 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
mockUseCurrentUser.mockReturnValue({
data: { models: [] },
isLoading: false,
} as any);

Check warning on line 221 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
mockUseKeys.mockReturnValue({
data: { keys: [], total_count: 0, current_page: 1, total_pages: 1 },
isPending: false,
isFetching: false,
refetch: vi.fn(),
} as any);

Check warning on line 227 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
vi.mocked(useTeamMetadataSchema).mockReturnValue({ data: [], isLoading: false } as any);

Check warning on line 228 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

vi.mocked(networking.getGuardrailsList).mockResolvedValue({ guardrails: [] });
vi.mocked(networking.getPoliciesList).mockResolvedValue({ policies: [] });
Expand Down Expand Up @@ -261,9 +261,9 @@
});

it("should display error message when team is not found", async () => {
vi.mocked(networking.teamInfoCall).mockResolvedValue({

Check warning on line 264 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 4 properties passed inline as an argument; assign it to a named variable first
team_id: "123",
team_info: null as any,

Check warning on line 266 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
keys: [],
team_memberships: [],
});
Expand Down Expand Up @@ -563,7 +563,7 @@
isPending: false,
isFetching: false,
refetch: vi.fn(),
} as any);

Check warning on line 566 in ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

renderWithProviders(<TeamInfoView {...defaultProps} />);

Expand Down Expand Up @@ -919,7 +919,7 @@
});
};

it("prefills pairs from team metadata, hides UI-managed keys, and round-trips typed values on save", async () => {
it("should preserve metadata types and hide managed keys", async () => {
const user = userEvent.setup({ delay: null });
vi.mocked(networking.teamInfoCall).mockResolvedValue(
createMockTeamData({
Expand Down Expand Up @@ -964,27 +964,6 @@
expect(updateArg.model_tpm_limit).toEqual({ "gpt-4": 100 });
Comment on lines 919 to 964

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 Preserve added-metadata integration coverage

Deleting this test removes the only TeamInfo-level check that a newly added metadata row passes through form conversion into teamUpdateCall. The focused component test stops at raw form submission, so a regression that drops new metadata from the update payload would pass the dashboard test suite.

Rule Used: What: Flag any modifications to existing tests and... (source)

Knowledge Base Used: Admin dashboard (ui/litellm-dashboard)

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!

});

it("includes a newly added pair in the team update", async () => {
const user = userEvent.setup({ delay: null });
vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData({ models: ["gpt-4"] }));
vi.mocked(networking.teamUpdateCall).mockResolvedValue({ data: {}, team_id: "123" } as any);

renderWithProviders(<TeamInfoView {...defaultProps} />);
await openSettingsEditor(user);

await user.click(screen.getByRole("button", { name: /add key-value pair/i }));
await user.type(screen.getByPlaceholderText("Key"), "cost_center");
await user.type(screen.getByPlaceholderText("Value"), "eng-1");

await user.click(screen.getByRole("button", { name: /save changes/i }));

await waitFor(() => {
expect(networking.teamUpdateCall).toHaveBeenCalled();
});

expect(vi.mocked(networking.teamUpdateCall).mock.calls[0][1].metadata).toMatchObject({ cost_center: "eng-1" });
});

it("should keep declared keys as ordinary prefilled rows and submit the edited value", async () => {
const user = userEvent.setup({ delay: null });
vi.mocked(useTeamMetadataSchema).mockReturnValue({
Expand Down
Loading