From e0e1ac617acedab7c1fb35a6372210a9e933efde Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 12:49:07 -0700 Subject: [PATCH 01/13] [Test] UI: Add unit tests for 10 previously untested components Add Vitest + RTL tests for HelpLink, DebugWarningBanner, ExportFormatSelector, ExportTypeSelector, ExportSummary, MetricCard, PolicySelect, ComplexityRouterConfig, RateLimitTypeFormItem, and AgentCardGrid (67 tests total). Co-Authored-By: Claude Opus 4.6 --- .../components/DebugWarningBanner.test.tsx | 41 +++++++++ .../ExportFormatSelector.test.tsx | 20 +++++ .../EntityUsageExport/ExportSummary.test.tsx | 59 ++++++++++++ .../ExportTypeSelector.test.tsx | 46 ++++++++++ .../GuardrailsMonitor/MetricCard.test.tsx | 49 ++++++++++ .../src/components/HelpLink.test.tsx | 24 +++++ .../ToolPolicies/PolicySelect.test.tsx | 84 +++++++++++++++++ .../add_model/ComplexityRouterConfig.test.tsx | 82 +++++++++++++++++ .../agents/agent_card_grid.test.tsx | 90 +++++++++++++++++++ .../RateLimitTypeFormItem.test.tsx | 61 +++++++++++++ 10 files changed, 556 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx create mode 100644 ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx create mode 100644 ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx create mode 100644 ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx create mode 100644 ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx create mode 100644 ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx diff --git a/ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx b/ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx new file mode 100644 index 0000000000..2412a84da3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx @@ -0,0 +1,41 @@ +import { renderWithProviders, screen } from "../../tests/test-utils"; +import { vi } from "vitest"; +import { DebugWarningBanner } from "./DebugWarningBanner"; + +vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness", () => ({ + useHealthReadiness: vi.fn(), +})); + +import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness"; + +describe("DebugWarningBanner", () => { + it("should render", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any); + renderWithProviders(); + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + it("should show warning when detailed debug mode is active", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any); + renderWithProviders(); + expect(screen.getByText(/Performance Warning: Detailed Debug Mode Active/i)).toBeInTheDocument(); + }); + + it("should mention LITELLM_LOG=DEBUG in the description", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any); + renderWithProviders(); + expect(screen.getByText("LITELLM_LOG=DEBUG")).toBeInTheDocument(); + }); + + it("should render nothing when is_detailed_debug is false", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: false } } as any); + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render nothing when health data is undefined", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: undefined } as any); + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx new file mode 100644 index 0000000000..8447df4a6b --- /dev/null +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx @@ -0,0 +1,20 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import ExportFormatSelector from "./ExportFormatSelector"; + +describe("ExportFormatSelector", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Format")).toBeInTheDocument(); + }); + + it("should display the current value", () => { + renderWithProviders(); + expect(screen.getByText("CSV (Excel, Google Sheets)")).toBeInTheDocument(); + }); + + it("should display JSON label when json is selected", () => { + renderWithProviders(); + expect(screen.getByText("JSON (includes metadata)")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx new file mode 100644 index 0000000000..8ebeb33b1e --- /dev/null +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx @@ -0,0 +1,59 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import ExportSummary from "./ExportSummary"; + +describe("ExportSummary", () => { + it("should render", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + const { container } = renderWithProviders( + + ); + expect(container).not.toBeEmptyDOMElement(); + }); + + it("should display the date range", () => { + const from = new Date(2024, 0, 1); + const to = new Date(2024, 0, 31); + const dateRange = { from, to }; + renderWithProviders( + + ); + expect(screen.getByText(new RegExp(from.toLocaleDateString()))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(to.toLocaleDateString()))).toBeInTheDocument(); + }); + + it("should show filter count when filters are selected", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + renderWithProviders( + + ); + expect(screen.getByText(/3 filters/)).toBeInTheDocument(); + }); + + it("should show singular 'filter' for one filter", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + renderWithProviders( + + ); + expect(screen.getByText(/1 filter$/)).toBeInTheDocument(); + }); + + it("should not show filter count when no filters selected", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + renderWithProviders( + + ); + expect(screen.queryByText(/filter/)).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx new file mode 100644 index 0000000000..36d2e12b71 --- /dev/null +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx @@ -0,0 +1,46 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi } from "vitest"; +import ExportTypeSelector from "./ExportTypeSelector"; + +describe("ExportTypeSelector", () => { + it("should render", () => { + renderWithProviders( + + ); + expect(screen.getByText("Export type")).toBeInTheDocument(); + }); + + it("should display entity type in radio labels", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Day-by-day breakdown by team$/)).toBeInTheDocument(); + expect(screen.getByText(/Day-by-day breakdown by team and key/)).toBeInTheDocument(); + expect(screen.getByText(/Day-by-day by team and model/)).toBeInTheDocument(); + }); + + it("should display the correct entity type for different entities", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Day-by-day breakdown by organization$/)).toBeInTheDocument(); + }); + + it("should call onChange when a radio option is selected", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + renderWithProviders( + + ); + await user.click(screen.getByRole("radio", { name: /Day-by-day breakdown by team and key/i })); + expect(onChange).toHaveBeenCalledWith("daily_with_keys"); + }); + + it("should have the correct radio checked", () => { + renderWithProviders( + + ); + expect(screen.getByRole("radio", { name: /Day-by-day by team and model/i })).toBeChecked(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx new file mode 100644 index 0000000000..d30df1a006 --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx @@ -0,0 +1,49 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import React from "react"; +import { MetricCard } from "./MetricCard"; + +describe("MetricCard", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Total Requests")).toBeInTheDocument(); + }); + + it("should display the label and value", () => { + renderWithProviders(); + expect(screen.getByText("Success Rate")).toBeInTheDocument(); + expect(screen.getByText("98.5%")).toBeInTheDocument(); + }); + + it("should display numeric values", () => { + renderWithProviders(); + expect(screen.getByText("42")).toBeInTheDocument(); + }); + + it("should render icon when provided", () => { + renderWithProviders( + icon} + /> + ); + expect(screen.getByTestId("test-icon")).toBeInTheDocument(); + }); + + it("should not render icon container when no icon provided", () => { + renderWithProviders(); + expect(screen.queryByTestId("test-icon")).not.toBeInTheDocument(); + }); + + it("should render subtitle when provided", () => { + renderWithProviders( + + ); + expect(screen.getByText("Last 24 hours")).toBeInTheDocument(); + }); + + it("should not render subtitle when not provided", () => { + renderWithProviders(); + expect(screen.queryByText("Last 24 hours")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/HelpLink.test.tsx b/ui/litellm-dashboard/src/components/HelpLink.test.tsx index 72033bd93a..76c42b8159 100644 --- a/ui/litellm-dashboard/src/components/HelpLink.test.tsx +++ b/ui/litellm-dashboard/src/components/HelpLink.test.tsx @@ -23,6 +23,11 @@ describe("HelpLink", () => { expect(screen.getByText("Custom docs link")).toBeInTheDocument(); }); + it("should have the correct href", () => { + renderWithProviders(); + expect(screen.getByRole("link")).toHaveAttribute("href", "https://docs.example.com/test"); + }); + it("should include a screen-reader-only label for accessibility", () => { renderWithProviders(); @@ -46,7 +51,21 @@ describe("HelpIcon", () => { expect(screen.getByText("Tooltip help text")).toBeInTheDocument(); }); + it("should hide tooltip content when not hovered", () => { + renderWithProviders(); + expect(screen.queryByText("Hidden tooltip")).not.toBeInTheDocument(); + }); + it("should show learn more link when learnMoreHref is provided", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + await user.hover(screen.getByRole("button", { name: /help information/i })); + expect(screen.getByText("Learn more")).toBeInTheDocument(); + }); + + it("should use custom learn more text when provided", async () => { const user = userEvent.setup(); renderWithProviders( { expect(screen.getByRole("button", { name: /docs/i })).toBeInTheDocument(); }); + it("should hide menu items initially", () => { + renderWithProviders(); + expect(screen.queryByText("Custom pricing")).not.toBeInTheDocument(); + }); + it("should show menu items when button is clicked", async () => { const user = userEvent.setup(); renderWithProviders(); diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx new file mode 100644 index 0000000000..cde59fe9c6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx @@ -0,0 +1,84 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import { + PolicySelect, + policyStyle, + INPUT_POLICY_OPTIONS, + OUTPUT_POLICY_OPTIONS, +} from "./PolicySelect"; + +describe("policyStyle", () => { + it("should return the matching option for a known policy", () => { + expect(policyStyle("trusted")).toEqual(INPUT_POLICY_OPTIONS[1]); + }); + + it("should return the matching option for blocked", () => { + expect(policyStyle("blocked")).toEqual(INPUT_POLICY_OPTIONS[2]); + }); + + it("should return the first option as fallback for unknown policy", () => { + expect(policyStyle("unknown")).toEqual(INPUT_POLICY_OPTIONS[0]); + }); +}); + +describe("PolicySelect", () => { + it("should render", () => { + renderWithProviders( + + ); + expect(screen.getByText("untrusted")).toBeInTheDocument(); + }); + + it("should show the current policy value", () => { + renderWithProviders( + + ); + expect(screen.getByText("trusted")).toBeInTheDocument(); + }); + + it("should be disabled when saving is true", () => { + renderWithProviders( + + ); + expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "false"); + expect(screen.getByRole("combobox").closest(".ant-select")).toHaveClass("ant-select-disabled"); + }); + + it("should not be disabled when saving is false", () => { + renderWithProviders( + + ); + expect(screen.getByRole("combobox").closest(".ant-select")).not.toHaveClass("ant-select-disabled"); + }); +}); + +describe("Policy option constants", () => { + it("should have 3 input policy options", () => { + expect(INPUT_POLICY_OPTIONS).toHaveLength(3); + }); + + it("should have 2 output policy options (no blocked)", () => { + expect(OUTPUT_POLICY_OPTIONS).toHaveLength(2); + expect(OUTPUT_POLICY_OPTIONS.map((o) => o.value)).toEqual(["untrusted", "trusted"]); + }); +}); diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx new file mode 100644 index 0000000000..59d8721b14 --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx @@ -0,0 +1,82 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import ComplexityRouterConfig from "./ComplexityRouterConfig"; + +const mockModelInfo = [ + { model_group: "gpt-4" }, + { model_group: "gpt-3.5-turbo" }, + { model_group: "claude-3-opus" }, +] as any[]; + +const defaultTiers = { + SIMPLE: "gpt-3.5-turbo", + MEDIUM: "gpt-3.5-turbo", + COMPLEX: "gpt-4", + REASONING: "claude-3-opus", +}; + +describe("ComplexityRouterConfig", () => { + it("should render", () => { + renderWithProviders( + + ); + expect(screen.getByText("Complexity Tier Configuration")).toBeInTheDocument(); + }); + + it("should display all four tier labels", () => { + renderWithProviders( + + ); + expect(screen.getByText("Simple Tier")).toBeInTheDocument(); + expect(screen.getByText("Medium Tier")).toBeInTheDocument(); + expect(screen.getByText("Complex Tier")).toBeInTheDocument(); + expect(screen.getByText("Reasoning Tier")).toBeInTheDocument(); + }); + + it("should show example queries for each tier", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Hello!/)).toBeInTheDocument(); + expect(screen.getByText(/Explain how REST APIs work/)).toBeInTheDocument(); + expect(screen.getByText(/Design a microservices architecture/)).toBeInTheDocument(); + expect(screen.getByText(/Think step by step/)).toBeInTheDocument(); + }); + + it("should display the how classification works section", () => { + renderWithProviders( + + ); + expect(screen.getByText("How Classification Works")).toBeInTheDocument(); + }); + + it("should show score thresholds in the classification section", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Score < 0.15/)).toBeInTheDocument(); + expect(screen.getByText(/Score 0.15 - 0.35/)).toBeInTheDocument(); + expect(screen.getByText(/Score 0.35 - 0.60/)).toBeInTheDocument(); + expect(screen.getByText(/Score > 0.60/)).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx b/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx new file mode 100644 index 0000000000..c8a21d1b6e --- /dev/null +++ b/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx @@ -0,0 +1,90 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import AgentCardGrid from "./agent_card_grid"; +import type { Agent, AgentKeyInfo } from "./types"; + +vi.mock("./agent_card", () => ({ + default: ({ agent, onAgentClick }: any) => ( +
onAgentClick(agent.agent_id)}> + {agent.agent_name} +
+ ), +})); + +const mockAgents: Agent[] = [ + { + agent_id: "agent-1", + agent_name: "Test Agent 1", + litellm_params: { model: "gpt-4" }, + agent_card_params: { description: "First agent" }, + }, + { + agent_id: "agent-2", + agent_name: "Test Agent 2", + litellm_params: { model: "claude-3" }, + agent_card_params: { description: "Second agent" }, + }, +]; + +const mockKeyInfoMap: Record = { + "agent-1": { has_key: true, key_alias: "key-1" }, + "agent-2": { has_key: false }, +}; + +const defaultProps = { + agentsList: mockAgents, + keyInfoMap: mockKeyInfoMap, + isLoading: false, + onDeleteClick: vi.fn(), + accessToken: "test-token", + onAgentUpdated: vi.fn(), + isAdmin: true, + onAgentClick: vi.fn(), +}; + +describe("AgentCardGrid", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Test Agent 1")).toBeInTheDocument(); + }); + + it("should render all agent cards", () => { + renderWithProviders(); + expect(screen.getByText("Test Agent 1")).toBeInTheDocument(); + expect(screen.getByText("Test Agent 2")).toBeInTheDocument(); + }); + + it("should show loading skeletons when isLoading is true", () => { + renderWithProviders(); + expect(screen.queryByText("Test Agent 1")).not.toBeInTheDocument(); + }); + + it("should show admin empty state message when no agents and isAdmin", () => { + renderWithProviders( + + ); + expect( + screen.getByText("No agents found. Create one to get started.") + ).toBeInTheDocument(); + }); + + it("should show non-admin empty state message when no agents and not admin", () => { + renderWithProviders( + + ); + expect( + screen.getByText("No agents found. Contact an admin to create agents.") + ).toBeInTheDocument(); + }); + + it("should call onAgentClick when a card is clicked", async () => { + const onAgentClick = vi.fn(); + renderWithProviders( + + ); + const { default: userEvent } = await import("@testing-library/user-event"); + const user = userEvent.setup(); + await user.click(screen.getByTestId("agent-card-agent-1")); + expect(onAgentClick).toHaveBeenCalledWith("agent-1"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx b/ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx new file mode 100644 index 0000000000..49b8028c51 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx @@ -0,0 +1,61 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi } from "vitest"; +import { Form } from "antd"; +import React from "react"; +import { RateLimitTypeFormItem } from "./RateLimitTypeFormItem"; + +const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
{children}
+); + +describe("RateLimitTypeFormItem", () => { + it("should render", () => { + renderWithProviders( + + + + ); + expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument(); + }); + + it("should display TPM label for tpm type", () => { + renderWithProviders( + + + + ); + expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument(); + }); + + it("should display RPM label for rpm type", () => { + renderWithProviders( + + + + ); + expect(screen.getByText(/RPM Rate Limit Type/)).toBeInTheDocument(); + }); + + it("should show the select placeholder by default", () => { + renderWithProviders( + + + + ); + expect(screen.getByText("Select rate limit type")).toBeInTheDocument(); + }); + + it("should call onChange when provided", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + renderWithProviders( + + + + ); + await user.click(screen.getByRole("combobox")); + await user.click(screen.getByText("Guaranteed throughput")); + expect(onChange).toHaveBeenCalledWith("guaranteed_throughput"); + }); +}); From bd2502eeaf99111d17442929f68542639417719c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 20:34:15 -0700 Subject: [PATCH 02/13] [Feature] /v2/team/list: Add org admin access control, members_count, and indexes Add org admin support to /v2/team/list so org admins can list teams within their organizations instead of getting 401. Also enrich the response with members_count and add missing indexes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../litellm_proxy_extras/schema.prisma | 4 + .../management_endpoints/team_endpoints.py | 132 +++++++++++++++--- litellm/proxy/schema.prisma | 4 + .../management_endpoints/team_endpoints.py | 9 +- schema.prisma | 4 + 5 files changed, 136 insertions(+), 17 deletions(-) diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index ce79c2b3d5..a2c8329540 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -146,6 +146,10 @@ model LiteLLM_TeamTable { litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) projects LiteLLM_ProjectTable[] + + @@index([organization_id]) + @@index([team_alias]) + @@index([created_at]) } // Projects sit between teams and keys for use-case management diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index d83ceb1b09..a7ee340282 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -101,6 +101,7 @@ from litellm.types.proxy.management_endpoints.team_endpoints import ( BulkTeamMemberAddRequest, BulkTeamMemberAddResponse, GetTeamMemberPermissionsResponse, + TeamListItem, TeamListResponse, TeamMemberAddResult, UpdateTeamMemberPermissionsRequest, @@ -3206,6 +3207,39 @@ async def list_available_teams( return available_teams_correct_type +async def _get_org_admin_org_ids( + user_id: str, + prisma_client: Any, + user_api_key_cache: Any, + proxy_logging_obj: Any, +) -> Optional[List[str]]: + """ + Return the list of organization IDs where the user is an org admin. + Returns None if the user is not an org admin of any organization or if + the user cannot be found. + """ + try: + caller_user = await get_user_object( + user_id=user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + user_id_upsert=False, + proxy_logging_obj=proxy_logging_obj, + ) + except Exception: + return None + + if caller_user is None: + return None + + org_ids = [ + m.organization_id + for m in (caller_user.organization_memberships or []) + if m.user_role == LitellmUserRoles.ORG_ADMIN.value + ] + return org_ids if org_ids else None + + async def _build_team_list_where_conditions( prisma_client: PrismaClient, team_id: Optional[str], @@ -3213,6 +3247,7 @@ async def _build_team_list_where_conditions( organization_id: Optional[str], user_id: Optional[str], use_deleted_table: bool, + org_admin_org_ids: Optional[List[str]] = None, ) -> Dict[str, Any]: """Build where conditions for team list query.""" where_conditions: Dict[str, Any] = {} @@ -3227,7 +3262,14 @@ async def _build_team_list_where_conditions( } if organization_id: - where_conditions["organization_id"] = organization_id + # If a list is passed (org admin viewing multiple orgs), use IN + if isinstance(organization_id, list): + where_conditions["organization_id"] = {"in": organization_id} + else: + where_conditions["organization_id"] = organization_id + elif org_admin_org_ids is not None: + # Org admin without explicit org filter: scope to their orgs + where_conditions["organization_id"] = {"in": org_admin_org_ids} if user_id: try: @@ -3347,7 +3389,11 @@ async def list_team_v2( status: Optional[str] Filter by status. Currently supports "deleted" to query deleted teams. """ - from litellm.proxy.proxy_server import prisma_client + from litellm.proxy.proxy_server import ( + prisma_client, + proxy_logging_obj, + user_api_key_cache, + ) if prisma_client is None: raise HTTPException( @@ -3355,20 +3401,58 @@ async def list_team_v2( detail={"error": f"No db connected. prisma client={prisma_client}"}, ) - if not allowed_route_check_inside_route( - user_api_key_dict=user_api_key_dict, requested_user_id=user_id - ): - raise HTTPException( - status_code=401, - detail={ - "error": "Only admin users can query all teams/other teams. Your user role={}".format( - user_api_key_dict.user_role - ) - }, + # --- Access control --- + # Proxy admins and admin viewers can query any teams. + # Org admins can query teams within their organizations. + # Regular users can only query their own teams. + is_proxy_admin = _user_has_admin_view(user_api_key_dict) + org_admin_org_ids: Optional[List[str]] = None + + if not is_proxy_admin: + # First check if the standard route check passes (user querying own data) + passes_route_check = allowed_route_check_inside_route( + user_api_key_dict=user_api_key_dict, requested_user_id=user_id ) - if user_id is None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: - user_id = user_api_key_dict.user_id + if not passes_route_check: + # Standard check failed — see if the caller is an org admin + if user_api_key_dict.user_id: + org_admin_org_ids = await _get_org_admin_org_ids( + user_id=user_api_key_dict.user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) + + if org_admin_org_ids is None: + # Not an org admin either — reject + raise HTTPException( + status_code=401, + detail={ + "error": "Only admin users can query all teams/other teams. Your user role={}".format( + user_api_key_dict.user_role + ) + }, + ) + + if org_admin_org_ids is not None: + # Org admin: validate org_id filter if provided + if organization_id and organization_id not in org_admin_org_ids: + raise HTTPException( + status_code=403, + detail={ + "error": "You can only view teams within your organizations." + }, + ) + verbose_proxy_logger.debug( + "list_team_v2: org admin access for user=%s, org_ids=%s", + user_api_key_dict.user_id, + org_admin_org_ids, + ) + elif not is_proxy_admin: + # Regular user — auto-inject caller's user_id + if user_id is None: + user_id = user_api_key_dict.user_id if status is not None and status != "deleted": raise HTTPException( @@ -3391,6 +3475,7 @@ async def list_team_v2( organization_id=organization_id, user_id=user_id, use_deleted_table=use_deleted_table, + org_admin_org_ids=org_admin_org_ids, ) # Build order_by conditions @@ -3428,8 +3513,23 @@ async def list_team_v2( # Calculate total pages total_pages = -(-total_count // page_size) # Ceiling division - # Convert Prisma models to Pydantic models, preserving deleted fields when applicable - team_list = _convert_teams_to_response(teams, use_deleted_table) + # Convert Prisma models to response models with members_count + team_list: List[Union[TeamListItem, LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]] = [] + for team in teams: + try: + team_dict = team.model_dump() + except Exception: + team_dict = team.dict() + + if use_deleted_table: + team_list.append(LiteLLM_DeletedTeamTable(**team_dict)) + else: + members_with_roles = team_dict.get("members_with_roles") + if not isinstance(members_with_roles, list): + members_with_roles = [] + team_dict["members_with_roles"] = members_with_roles + members_count = len(members_with_roles) + team_list.append(TeamListItem(**team_dict, members_count=members_count)) return { "teams": team_list, diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index b68872e2ed..46be6b31e1 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -146,6 +146,10 @@ model LiteLLM_TeamTable { litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) projects LiteLLM_ProjectTable[] + + @@index([organization_id]) + @@index([team_alias]) + @@index([created_at]) } // Projects sit between teams and keys for use-case management diff --git a/litellm/types/proxy/management_endpoints/team_endpoints.py b/litellm/types/proxy/management_endpoints/team_endpoints.py index 77816fa78c..a69d69c623 100644 --- a/litellm/types/proxy/management_endpoints/team_endpoints.py +++ b/litellm/types/proxy/management_endpoints/team_endpoints.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -43,10 +44,16 @@ class UpdateTeamMemberPermissionsRequest(BaseModel): team_member_permissions: List[str] +class TeamListItem(LiteLLM_TeamTable): + """A team item in the paginated list response, enriched with computed fields.""" + + members_count: int = 0 + + class TeamListResponse(BaseModel): """Response to get the list of teams""" - teams: List[Union[LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]] + teams: List[Union[TeamListItem, LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]] total: int page: int page_size: int diff --git a/schema.prisma b/schema.prisma index 939f1eb0f4..fde9a466a2 100644 --- a/schema.prisma +++ b/schema.prisma @@ -146,6 +146,10 @@ model LiteLLM_TeamTable { litellm_model_table LiteLLM_ModelTable? @relation(fields: [model_id], references: [id]) object_permission LiteLLM_ObjectPermissionTable? @relation(fields: [object_permission_id], references: [object_permission_id]) projects LiteLLM_ProjectTable[] + + @@index([organization_id]) + @@index([team_alias]) + @@index([created_at]) } // Projects sit between teams and keys for use-case management From 5e2fb72f42ef10e12e8d29e9120398db9b395fd2 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 20:50:58 -0700 Subject: [PATCH 03/13] fix: address review feedback on v2/team/list - Fix org admin own-query regression: always check org admin status before the standard route check so own-queries see all org teams - Clear user_id when org admin is detected so org scope replaces user-membership scope - Remove dead isinstance(organization_id, list) branch - Remove unused datetime import - Remove orphaned _convert_teams_to_response helper Co-Authored-By: Claude Opus 4.6 (1M context) --- .../management_endpoints/team_endpoints.py | 79 +++++++------------ .../management_endpoints/team_endpoints.py | 1 - 2 files changed, 27 insertions(+), 53 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index a7ee340282..35122259c3 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -3262,11 +3262,7 @@ async def _build_team_list_where_conditions( } if organization_id: - # If a list is passed (org admin viewing multiple orgs), use IN - if isinstance(organization_id, list): - where_conditions["organization_id"] = {"in": organization_id} - else: - where_conditions["organization_id"] = organization_id + where_conditions["organization_id"] = organization_id elif org_admin_org_ids is not None: # Org admin without explicit org filter: scope to their orgs where_conditions["organization_id"] = {"in": org_admin_org_ids} @@ -3304,27 +3300,6 @@ async def _build_team_list_where_conditions( return where_conditions -def _convert_teams_to_response( - teams: List[Any], use_deleted_table: bool -) -> List[Union[LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]]: - """Convert Prisma models to Pydantic models.""" - team_list: List[Union[LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]] = [] - if teams: - for team in teams: - # Convert Prisma model to dict (supports both Pydantic v1 and v2) - try: - team_dict = team.model_dump() - except Exception: - # Fallback for Pydantic v1 compatibility - team_dict = team.dict() - if use_deleted_table: - # Use deleted team type to preserve deleted_at, deleted_by, etc. - team_list.append(LiteLLM_DeletedTeamTable(**team_dict)) - else: - team_list.append(LiteLLM_TeamTable(**team_dict)) - return team_list - - @router.get( "/v2/team/list", tags=["team management"], @@ -3409,31 +3384,15 @@ async def list_team_v2( org_admin_org_ids: Optional[List[str]] = None if not is_proxy_admin: - # First check if the standard route check passes (user querying own data) - passes_route_check = allowed_route_check_inside_route( - user_api_key_dict=user_api_key_dict, requested_user_id=user_id - ) - - if not passes_route_check: - # Standard check failed — see if the caller is an org admin - if user_api_key_dict.user_id: - org_admin_org_ids = await _get_org_admin_org_ids( - user_id=user_api_key_dict.user_id, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, - ) - - if org_admin_org_ids is None: - # Not an org admin either — reject - raise HTTPException( - status_code=401, - detail={ - "error": "Only admin users can query all teams/other teams. Your user role={}".format( - user_api_key_dict.user_role - ) - }, - ) + # Always check org admin status so that even own-queries see + # the full set of organisation teams, not just direct memberships. + if user_api_key_dict.user_id: + org_admin_org_ids = await _get_org_admin_org_ids( + user_id=user_api_key_dict.user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) if org_admin_org_ids is not None: # Org admin: validate org_id filter if provided @@ -3444,12 +3403,28 @@ async def list_team_v2( "error": "You can only view teams within your organizations." }, ) + # Org admin scope replaces user_id scope — the org filter + # already returns all teams in their orgs, so we don't also + # restrict by the user's direct team memberships. + user_id = None verbose_proxy_logger.debug( "list_team_v2: org admin access for user=%s, org_ids=%s", user_api_key_dict.user_id, org_admin_org_ids, ) - elif not is_proxy_admin: + else: + # Not an org admin — fall back to standard route check + if not allowed_route_check_inside_route( + user_api_key_dict=user_api_key_dict, requested_user_id=user_id + ): + raise HTTPException( + status_code=401, + detail={ + "error": "Only admin users can query all teams/other teams. Your user role={}".format( + user_api_key_dict.user_role + ) + }, + ) # Regular user — auto-inject caller's user_id if user_id is None: user_id = user_api_key_dict.user_id diff --git a/litellm/types/proxy/management_endpoints/team_endpoints.py b/litellm/types/proxy/management_endpoints/team_endpoints.py index a69d69c623..5055a65783 100644 --- a/litellm/types/proxy/management_endpoints/team_endpoints.py +++ b/litellm/types/proxy/management_endpoints/team_endpoints.py @@ -1,4 +1,3 @@ -from datetime import datetime from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel From 1998571d94b366c9525f84ac96622242f4a16353 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 21:03:35 -0700 Subject: [PATCH 04/13] fix: address second review round on v2/team/list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _get_org_admin_org_ids: catch only ValueError (user not found) instead of bare Exception — DB errors now propagate as 500s instead of silently demoting org admins to regular users - _build_team_list_where_conditions: return None (not a sentinel string) when user has no team memberships; list_team_v2 short-circuits to empty response without hitting the DB - Org admin + team_id + user_id: use exact team_id match with org scope instead of OR expansion that effectively ignored the team_id filter - Org admin + user_id (no team_id): OR(org teams, direct memberships) now matches legacy _authorize_and_filter_teams behaviour Co-Authored-By: Claude Opus 4.6 (1M context) --- .../management_endpoints/team_endpoints.py | 82 ++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 35122259c3..b32206a454 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -3226,7 +3226,8 @@ async def _get_org_admin_org_ids( user_id_upsert=False, proxy_logging_obj=proxy_logging_obj, ) - except Exception: + except ValueError: + # get_user_object raises ValueError when the user doesn't exist return None if caller_user is None: @@ -3248,8 +3249,13 @@ async def _build_team_list_where_conditions( user_id: Optional[str], use_deleted_table: bool, org_admin_org_ids: Optional[List[str]] = None, -) -> Dict[str, Any]: - """Build where conditions for team list query.""" +) -> Optional[Dict[str, Any]]: + """ + Build where conditions for team list query. + + Returns None when the query is guaranteed to yield no results (e.g. user + has no team memberships), allowing the caller to skip the DB round-trip. + """ where_conditions: Dict[str, Any] = {} if team_id: @@ -3263,39 +3269,50 @@ async def _build_team_list_where_conditions( if organization_id: where_conditions["organization_id"] = organization_id - elif org_admin_org_ids is not None: - # Org admin without explicit org filter: scope to their orgs + elif org_admin_org_ids is not None and not user_id: + # Org admin without explicit org or user filter: scope to their orgs where_conditions["organization_id"] = {"in": org_admin_org_ids} if user_id: - try: - user_object = await prisma_client.db.litellm_usertable.find_unique( - where={"user_id": user_id} - ) - except Exception: - raise HTTPException( - status_code=404, - detail={"error": f"User not found, passed user_id={user_id}"}, - ) + user_object = await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": user_id} + ) if user_object is None: raise HTTPException( status_code=404, detail={"error": f"User not found, passed user_id={user_id}"}, ) user_object_correct_type = LiteLLM_UserTable(**user_object.model_dump()) + user_team_ids = user_object_correct_type.teams or [] if use_deleted_table: where_conditions["members"] = {"has": user_id} - else: - if team_id is None: - where_conditions["team_id"] = {"in": user_object_correct_type.teams} - elif team_id in user_object_correct_type.teams: - where_conditions["team_id"] = team_id + elif org_admin_org_ids is not None: + # Org admin with user_id filter: show teams in their orgs + # OR teams the user is a direct member of (matches legacy + # _authorize_and_filter_teams behaviour). + # When team_id is also provided, the exact match is already in + # where_conditions and the org scope just needs to be added — + # no OR expansion needed. + if team_id is not None: + where_conditions["organization_id"] = {"in": org_admin_org_ids} + elif user_team_ids: + org_condition: Dict[str, Any] = {"organization_id": {"in": org_admin_org_ids}} + where_conditions["OR"] = [org_condition, {"team_id": {"in": user_team_ids}}] else: - raise HTTPException( - status_code=404, - detail={"error": f"User is not a member of team_id={team_id}"}, - ) + where_conditions["organization_id"] = {"in": org_admin_org_ids} + else: + if not user_team_ids: + return None # no memberships — skip the DB query + elif team_id is not None: + # team_id exact-match already in where_conditions; verify membership + if team_id not in user_team_ids: + raise HTTPException( + status_code=404, + detail={"error": f"User is not a member of team_id={team_id}"}, + ) + else: + where_conditions["team_id"] = {"in": user_team_ids} return where_conditions @@ -3403,14 +3420,11 @@ async def list_team_v2( "error": "You can only view teams within your organizations." }, ) - # Org admin scope replaces user_id scope — the org filter - # already returns all teams in their orgs, so we don't also - # restrict by the user's direct team memberships. - user_id = None verbose_proxy_logger.debug( - "list_team_v2: org admin access for user=%s, org_ids=%s", + "list_team_v2: org admin access for user=%s, org_ids=%s, user_id_filter=%s", user_api_key_dict.user_id, org_admin_org_ids, + user_id, ) else: # Not an org admin — fall back to standard route check @@ -3442,7 +3456,8 @@ async def list_team_v2( # Calculate skip and take for pagination skip = (page - 1) * page_size - # Build where conditions based on provided parameters + # Build where conditions based on provided parameters. + # Returns None when the query is guaranteed to yield no results. where_conditions = await _build_team_list_where_conditions( prisma_client=prisma_client, team_id=team_id, @@ -3453,6 +3468,15 @@ async def list_team_v2( org_admin_org_ids=org_admin_org_ids, ) + if where_conditions is None: + return { + "teams": [], + "total": 0, + "page": page, + "page_size": page_size, + "total_pages": 0, + } + # Build order_by conditions valid_sort_columns = ["team_id", "team_alias", "created_at"] order_by = None From 0485a1859a94520ff55148aeab313a102d2b50be Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 21:19:42 -0700 Subject: [PATCH 05/13] fix: use get_user_object helper, preserve caller org_id filter - Replace raw find_unique with get_user_object in _build_team_list_where_conditions for cache/metrics consistency - Remove over-complex OR clause for org admin + user_id: when user_id is provided, filter by that user's direct team memberships (same as regular users) since the access control gate already verified the org admin's authority - Preserve caller-supplied organization_id instead of overwriting with org_admin_org_ids - Update test mock to match get_user_object call path Co-Authored-By: Claude Opus 4.6 (1M context) --- .../management_endpoints/team_endpoints.py | 41 +++++++------- .../test_team_endpoints.py | 55 +++++++++++-------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index b32206a454..531cd67589 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -3249,6 +3249,8 @@ async def _build_team_list_where_conditions( user_id: Optional[str], use_deleted_table: bool, org_admin_org_ids: Optional[List[str]] = None, + user_api_key_cache: Optional[Any] = None, + proxy_logging_obj: Optional[Any] = None, ) -> Optional[Dict[str, Any]]: """ Build where conditions for team list query. @@ -3274,34 +3276,33 @@ async def _build_team_list_where_conditions( where_conditions["organization_id"] = {"in": org_admin_org_ids} if user_id: - user_object = await prisma_client.db.litellm_usertable.find_unique( - where={"user_id": user_id} - ) - if user_object is None: + try: + user_object_correct_type = await get_user_object( + user_id=user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + user_id_upsert=False, + proxy_logging_obj=proxy_logging_obj, + ) + except ValueError: + raise HTTPException( + status_code=404, + detail={"error": f"User not found, passed user_id={user_id}"}, + ) + if user_object_correct_type is None: raise HTTPException( status_code=404, detail={"error": f"User not found, passed user_id={user_id}"}, ) - user_object_correct_type = LiteLLM_UserTable(**user_object.model_dump()) user_team_ids = user_object_correct_type.teams or [] if use_deleted_table: where_conditions["members"] = {"has": user_id} - elif org_admin_org_ids is not None: - # Org admin with user_id filter: show teams in their orgs - # OR teams the user is a direct member of (matches legacy - # _authorize_and_filter_teams behaviour). - # When team_id is also provided, the exact match is already in - # where_conditions and the org scope just needs to be added — - # no OR expansion needed. - if team_id is not None: - where_conditions["organization_id"] = {"in": org_admin_org_ids} - elif user_team_ids: - org_condition: Dict[str, Any] = {"organization_id": {"in": org_admin_org_ids}} - where_conditions["OR"] = [org_condition, {"team_id": {"in": user_team_ids}}] - else: - where_conditions["organization_id"] = {"in": org_admin_org_ids} else: + # When user_id is provided, filter by that user's direct team + # memberships. For org admins the access control gate in + # list_team_v2 already verified the caller's authority — the + # filter logic is the same as for regular users. if not user_team_ids: return None # no memberships — skip the DB query elif team_id is not None: @@ -3466,6 +3467,8 @@ async def list_team_v2( user_id=user_id, use_deleted_table=use_deleted_table, org_admin_org_ids=org_admin_org_ids, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, ) if where_conditions is None: 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 41a724c271..122ac749f2 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -2142,19 +2142,21 @@ async def test_list_team_v2_security_check_non_admin_user_own_teams(): user_id="non_admin_user_123", ) - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client: + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client, \ + patch("litellm.proxy.proxy_server.user_api_key_cache"), \ + patch("litellm.proxy.proxy_server.proxy_logging_obj"): # Mock prisma client and database operations mock_db = Mock() mock_prisma_client.db = mock_db - - # Mock user lookup - mock_user_object = Mock() - mock_user_object.model_dump.return_value = { - "user_id": "non_admin_user_123", - "teams": ["team_1", "team_2"], - } - mock_db.litellm_usertable.find_unique = AsyncMock(return_value=mock_user_object) - + + # Mock get_user_object to return a user with teams + from litellm.proxy._types import LiteLLM_UserTable + + mock_user = LiteLLM_UserTable( + user_id="non_admin_user_123", + teams=["team_1", "team_2"], + ) + # Mock team lookup mock_teams = [ Mock(model_dump=lambda: {"team_id": "team_1", "team_alias": "Team 1"}), @@ -2163,21 +2165,26 @@ async def test_list_team_v2_security_check_non_admin_user_own_teams(): mock_db.litellm_teamtable.find_many = AsyncMock(return_value=mock_teams) mock_db.litellm_teamtable.count = AsyncMock(return_value=2) - # Should NOT raise an exception - result = await list_team_v2( - http_request=mock_request, - user_id="non_admin_user_123", # Non-admin querying their own teams - user_api_key_dict=mock_user_api_key_dict_non_admin, - team_id=None, - page=1, - page_size=10, - status=None, - ) + with patch( + "litellm.proxy.management_endpoints.team_endpoints.get_user_object", + new_callable=AsyncMock, + return_value=mock_user, + ): + # Should NOT raise an exception + result = await list_team_v2( + http_request=mock_request, + user_id="non_admin_user_123", # Non-admin querying their own teams + user_api_key_dict=mock_user_api_key_dict_non_admin, + team_id=None, + page=1, + page_size=10, + status=None, + ) - # Should return results without error - assert "teams" in result - assert "total" in result - assert result["total"] == 2 + # Should return results without error + assert "teams" in result + assert "total" in result + assert result["total"] == 2 @pytest.mark.asyncio From 41a7747e8cc99b133dd72ac6de6f55aaa9708e0d Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 21:56:57 -0700 Subject: [PATCH 06/13] fix: document org scope behavior, fix test mocks, add org admin tests - Document intentional legacy-matching behavior: when user_id is provided to an org admin, no org filter is applied (returns all of that user's teams across all orgs, same as legacy endpoint) - Fix two existing security tests to properly patch user_api_key_cache, proxy_logging_obj, and get_user_object instead of relying on incidental error handling - Add three new org admin test cases: - Org admin sees org-scoped teams (200 with correct where clause) - Org admin rejected when filtering by other org (403) - Org admin with user_id filter returns target user's teams Co-Authored-By: Claude Opus 4.6 (1M context) --- .../management_endpoints/team_endpoints.py | 7 +- .../test_team_endpoints.py | 281 +++++++++++++++++- 2 files changed, 280 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 531cd67589..e7cd6c1748 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -3272,7 +3272,12 @@ async def _build_team_list_where_conditions( if organization_id: where_conditions["organization_id"] = organization_id elif org_admin_org_ids is not None and not user_id: - # Org admin without explicit org or user filter: scope to their orgs + # Org admin without explicit org or user filter: scope to their orgs. + # NOTE: when user_id is provided, no org filter is applied — the + # query returns all teams the target user belongs to across all + # organisations. This matches the legacy /team/list behaviour in + # _authorize_and_filter_teams which fetches direct-membership teams + # without an org constraint. where_conditions["organization_id"] = {"in": org_admin_org_ids} if user_id: 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 122ac749f2..c325b0b6fc 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -2062,7 +2062,14 @@ async def test_list_team_v2_security_check_non_admin_user(): user_id="non_admin_user_123", ) - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client: + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client, \ + patch("litellm.proxy.proxy_server.user_api_key_cache"), \ + patch("litellm.proxy.proxy_server.proxy_logging_obj"), \ + patch( + "litellm.proxy.management_endpoints.team_endpoints.get_user_object", + new_callable=AsyncMock, + return_value=None, + ): mock_prisma_client.return_value = MagicMock() # Mock non-None prisma client # Should raise HTTPException with 401 status @@ -2103,7 +2110,14 @@ async def test_list_team_v2_security_check_non_admin_user_other_user(): user_id="non_admin_user_123", ) - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client: + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client, \ + patch("litellm.proxy.proxy_server.user_api_key_cache"), \ + patch("litellm.proxy.proxy_server.proxy_logging_obj"), \ + patch( + "litellm.proxy.management_endpoints.team_endpoints.get_user_object", + new_callable=AsyncMock, + return_value=None, + ): mock_prisma_client.return_value = MagicMock() # Mock non-None prisma client # Should raise HTTPException with 401 status @@ -2300,27 +2314,280 @@ async def test_list_team_v2_with_status_deleted(): assert len(result["teams"]) == 2 +@pytest.mark.asyncio +async def test_list_team_v2_org_admin_sees_org_teams(): + """ + Test that an org admin (internal_user role with org_admin membership) + can list teams scoped to their organisations without getting a 401. + """ + from datetime import datetime + from unittest.mock import AsyncMock, Mock, patch + + from fastapi import Request + + from litellm.proxy._types import ( + LiteLLM_OrganizationMembershipTable, + LiteLLM_UserTable, + LitellmUserRoles, + UserAPIKeyAuth, + ) + from litellm.proxy.management_endpoints.team_endpoints import list_team_v2 + + mock_request = Mock(spec=Request) + mock_user_api_key_dict = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org_admin_user", + ) + + mock_user = LiteLLM_UserTable( + user_id="org_admin_user", + teams=[], + organization_memberships=[ + LiteLLM_OrganizationMembershipTable( + user_id="org_admin_user", + organization_id="org_A", + user_role="org_admin", + spend=0.0, + created_at=datetime.now(), + updated_at=datetime.now(), + ), + ], + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, \ + patch("litellm.proxy.proxy_server.user_api_key_cache"), \ + patch("litellm.proxy.proxy_server.proxy_logging_obj"), \ + patch( + "litellm.proxy.management_endpoints.team_endpoints.get_user_object", + new_callable=AsyncMock, + return_value=mock_user, + ): + mock_db = Mock() + mock_prisma.db = mock_db + + mock_team = Mock() + mock_team.model_dump.return_value = { + "team_id": "team_in_org_A", + "team_alias": "Org A Team", + "organization_id": "org_A", + "members_with_roles": [{"user_id": "u1", "role": "user"}], + } + mock_db.litellm_teamtable.find_many = AsyncMock(return_value=[mock_team]) + mock_db.litellm_teamtable.count = AsyncMock(return_value=1) + + result = await list_team_v2( + http_request=mock_request, + user_id=None, + organization_id=None, + team_id=None, + team_alias=None, + user_api_key_dict=mock_user_api_key_dict, + page=1, + page_size=10, + sort_by=None, + sort_order="asc", + status=None, + ) + + assert result["total"] == 1 + assert len(result["teams"]) == 1 + assert result["teams"][0].members_count == 1 + + # Verify org-scoped where clause + where = mock_db.litellm_teamtable.find_many.call_args.kwargs["where"] + assert where["organization_id"] == {"in": ["org_A"]} + + +@pytest.mark.asyncio +async def test_list_team_v2_org_admin_cannot_view_other_orgs(): + """ + Test that an org admin is rejected with 403 when filtering by an + organisation they do not administer. + """ + from datetime import datetime + from unittest.mock import AsyncMock, Mock, patch + + from fastapi import HTTPException, Request + + from litellm.proxy._types import ( + LiteLLM_OrganizationMembershipTable, + LiteLLM_UserTable, + LitellmUserRoles, + UserAPIKeyAuth, + ) + from litellm.proxy.management_endpoints.team_endpoints import list_team_v2 + + mock_request = Mock(spec=Request) + mock_user_api_key_dict = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org_admin_user", + ) + + mock_user = LiteLLM_UserTable( + user_id="org_admin_user", + teams=[], + organization_memberships=[ + LiteLLM_OrganizationMembershipTable( + user_id="org_admin_user", + organization_id="org_A", + user_role="org_admin", + spend=0.0, + created_at=datetime.now(), + updated_at=datetime.now(), + ), + ], + ) + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, \ + patch("litellm.proxy.proxy_server.user_api_key_cache"), \ + patch("litellm.proxy.proxy_server.proxy_logging_obj"), \ + patch( + "litellm.proxy.management_endpoints.team_endpoints.get_user_object", + new_callable=AsyncMock, + return_value=mock_user, + ): + mock_prisma.db = Mock() + + with pytest.raises(HTTPException) as exc_info: + await list_team_v2( + http_request=mock_request, + user_id=None, + organization_id="org_B", # not their org + team_id=None, + team_alias=None, + user_api_key_dict=mock_user_api_key_dict, + page=1, + page_size=10, + sort_by=None, + sort_order="asc", + status=None, + ) + + assert exc_info.value.status_code == 403 + assert "only view teams within your organizations" in str( + exc_info.value.detail + ).lower() + + +@pytest.mark.asyncio +async def test_list_team_v2_org_admin_with_user_id_returns_user_teams(): + """ + Test that an org admin passing user_id gets that user's direct team + memberships (not all org teams). + """ + from datetime import datetime + from unittest.mock import AsyncMock, Mock, patch + + from fastapi import Request + + from litellm.proxy._types import ( + LiteLLM_OrganizationMembershipTable, + LiteLLM_UserTable, + LitellmUserRoles, + UserAPIKeyAuth, + ) + from litellm.proxy.management_endpoints.team_endpoints import list_team_v2 + + mock_request = Mock(spec=Request) + mock_user_api_key_dict = UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="org_admin_user", + ) + + mock_org_admin = LiteLLM_UserTable( + user_id="org_admin_user", + teams=["team_1"], + organization_memberships=[ + LiteLLM_OrganizationMembershipTable( + user_id="org_admin_user", + organization_id="org_A", + user_role="org_admin", + spend=0.0, + created_at=datetime.now(), + updated_at=datetime.now(), + ), + ], + ) + + # The target user whose teams we want to list + mock_target_user = LiteLLM_UserTable( + user_id="target_user", + teams=["team_X", "team_Y"], + ) + + call_count = 0 + + async def mock_get_user_object(**kwargs): + nonlocal call_count + call_count += 1 + # First call: org admin lookup in list_team_v2 + # Second call: target user lookup in _build_team_list_where_conditions + if call_count == 1: + return mock_org_admin + return mock_target_user + + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, \ + patch("litellm.proxy.proxy_server.user_api_key_cache"), \ + patch("litellm.proxy.proxy_server.proxy_logging_obj"), \ + patch( + "litellm.proxy.management_endpoints.team_endpoints.get_user_object", + side_effect=mock_get_user_object, + ): + mock_db = Mock() + mock_prisma.db = mock_db + + mock_team = Mock() + mock_team.model_dump.return_value = { + "team_id": "team_X", + "team_alias": "Target Team", + "members_with_roles": [{"user_id": "target_user", "role": "user"}], + } + mock_db.litellm_teamtable.find_many = AsyncMock(return_value=[mock_team]) + mock_db.litellm_teamtable.count = AsyncMock(return_value=1) + + result = await list_team_v2( + http_request=mock_request, + user_id="target_user", + organization_id=None, + team_id=None, + team_alias=None, + user_api_key_dict=mock_user_api_key_dict, + page=1, + page_size=10, + sort_by=None, + sort_order="asc", + status=None, + ) + + assert result["total"] == 1 + + # Verify the where clause filters by user's teams, not org scope + where = mock_db.litellm_teamtable.find_many.call_args.kwargs["where"] + assert where["team_id"] == {"in": ["team_X", "team_Y"]} + assert "organization_id" not in where + + @pytest.mark.asyncio async def test_list_team_v2_with_invalid_status(): """ Test that invalid status parameter raises HTTPException. """ from unittest.mock import Mock, patch - + from fastapi import HTTPException, Request - + from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.management_endpoints.team_endpoints import list_team_v2 - + # Mock request mock_request = Mock(spec=Request) - + # Mock admin user mock_user_api_key_dict_admin = UserAPIKeyAuth( user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user_123", ) - + mock_prisma_client = Mock() # Mock prisma_client to be non-None From 2c874a7f3cb25a384336cb4061cebdce51457866 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 23:00:05 -0700 Subject: [PATCH 07/13] fix: prevent body 'key' field from overriding Authorization header in auth On /key/block, /key/unblock, and /key/update, the request body 'key' field could contaminate the api_key Security dependency, causing the auth layer to authenticate against the target key instead of the caller's bearer token. This returned 401 for a nonexistent body key even when the Authorization header contained a valid master key. Added a guard in user_api_key_auth that re-reads the Authorization header directly from the request, ensuring the header is always the authoritative source for authentication. Co-Authored-By: Claude Opus 4.6 (1M context) --- litellm/proxy/auth/user_api_key_auth.py | 11 +++ .../test_key_management_endpoints.py | 85 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 376048e7a1..724184282f 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -1535,6 +1535,17 @@ async def user_api_key_auth( Parent function to authenticate user api key / jwt token. """ + # Guard: re-read the Authorization header directly from the request. + # On endpoints whose request body contains a "key" field (e.g. /key/block, + # /key/unblock, /key/update), the body value can contaminate the api_key + # Security dependency, causing the auth layer to authenticate against the + # target key instead of the caller's bearer token. + _raw_auth_header = request.headers.get( + SpecialHeaders.openai_authorization.value + ) + if _raw_auth_header is not None: + api_key = _raw_auth_header + request_data = await _read_request_body(request=request) request_data = populate_request_with_path_params( request_data=request_data, request=request diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index cfc16808af..f1fb973a79 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -7185,3 +7185,88 @@ def test_update_key_request_has_organization_id(): # Also verify it defaults to None data_no_org = UpdateKeyRequest(key="sk-test-key") assert data_no_org.organization_id is None + + +@pytest.mark.asyncio +async def test_body_key_does_not_override_authorization_header(monkeypatch): + """ + Verify that the request body 'key' field (used by /key/block, /key/unblock, + /key/update) does not contaminate the api_key used for authentication. + + Regression test: previously, sending a request like + POST /key/block + Authorization: Bearer + Body: {"key": "sk-does-not-exist"} + would authenticate against "sk-does-not-exist" instead of the master key, + returning a 401 for a non-existent key even though the caller had a valid + master key in the Authorization header. + """ + from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + + master_key = "sk-master-test-key-1234" + + # Mock proxy_server globals + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", master_key) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", MagicMock()) + monkeypatch.setattr( + "litellm.proxy.proxy_server.general_settings", {} + ) + monkeypatch.setattr("litellm.proxy.proxy_server.jwt_handler", MagicMock()) + monkeypatch.setattr( + "litellm.proxy.proxy_server.litellm_proxy_admin_name", + "default_user_id", + ) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_model_list", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None) + monkeypatch.setattr("litellm.proxy.proxy_server.user_custom_auth", None) + monkeypatch.setattr( + "litellm.proxy.proxy_server.open_telemetry_logger", None + ) + monkeypatch.setattr( + "litellm.proxy.proxy_server.model_max_budget_limiter", MagicMock() + ) + mock_user_api_key_cache = MagicMock() + mock_user_api_key_cache.async_get_cache = AsyncMock(return_value=None) + mock_user_api_key_cache.get_cache = MagicMock(return_value=None) + monkeypatch.setattr( + "litellm.proxy.proxy_server.user_api_key_cache", + mock_user_api_key_cache, + ) + monkeypatch.setattr( + "litellm.proxy.proxy_server.proxy_logging_obj", MagicMock() + ) + + # Build a mock request that simulates: + # POST /key/block + # Authorization: Bearer + # Body: {"key": "sk-does-not-exist"} + mock_request = MagicMock() + mock_request.headers = { + "Authorization": f"Bearer {master_key}", + "Content-Type": "application/json", + } + mock_request.url.path = "/key/block" + mock_request.method = "POST" + mock_request.query_params = {} + mock_request.scope = { + "type": "http", + "path": "/key/block", + # Simulate a pre-parsed body with a 'key' field that differs from the master key + "parsed_body": ( + ("key",), + {"key": "sk-does-not-exist"}, + ), + } + mock_request.state = MagicMock() + + # Call user_api_key_auth the way FastAPI does. If the body 'key' field + # contaminates the api_key parameter, pass it explicitly to prove the + # guard works. + result = await user_api_key_auth( + request=mock_request, + api_key="sk-does-not-exist", # Simulates the contaminated value + ) + + # Auth should succeed — the guard re-reads the Authorization header + assert result is not None + assert result.user_role == LitellmUserRoles.PROXY_ADMIN From b0990e2684d8379e740e2f608e8f21a0cb491404 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 17 Mar 2026 23:02:38 -0700 Subject: [PATCH 08/13] Revert "fix: prevent body 'key' field from overriding Authorization header in auth" This reverts commit 2c874a7f3cb25a384336cb4061cebdce51457866. --- litellm/proxy/auth/user_api_key_auth.py | 11 --- .../test_key_management_endpoints.py | 85 ------------------- 2 files changed, 96 deletions(-) diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 724184282f..376048e7a1 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -1535,17 +1535,6 @@ async def user_api_key_auth( Parent function to authenticate user api key / jwt token. """ - # Guard: re-read the Authorization header directly from the request. - # On endpoints whose request body contains a "key" field (e.g. /key/block, - # /key/unblock, /key/update), the body value can contaminate the api_key - # Security dependency, causing the auth layer to authenticate against the - # target key instead of the caller's bearer token. - _raw_auth_header = request.headers.get( - SpecialHeaders.openai_authorization.value - ) - if _raw_auth_header is not None: - api_key = _raw_auth_header - request_data = await _read_request_body(request=request) request_data = populate_request_with_path_params( request_data=request_data, request=request diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index f1fb973a79..cfc16808af 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -7185,88 +7185,3 @@ def test_update_key_request_has_organization_id(): # Also verify it defaults to None data_no_org = UpdateKeyRequest(key="sk-test-key") assert data_no_org.organization_id is None - - -@pytest.mark.asyncio -async def test_body_key_does_not_override_authorization_header(monkeypatch): - """ - Verify that the request body 'key' field (used by /key/block, /key/unblock, - /key/update) does not contaminate the api_key used for authentication. - - Regression test: previously, sending a request like - POST /key/block - Authorization: Bearer - Body: {"key": "sk-does-not-exist"} - would authenticate against "sk-does-not-exist" instead of the master key, - returning a 401 for a non-existent key even though the caller had a valid - master key in the Authorization header. - """ - from litellm.proxy.auth.user_api_key_auth import user_api_key_auth - - master_key = "sk-master-test-key-1234" - - # Mock proxy_server globals - monkeypatch.setattr("litellm.proxy.proxy_server.master_key", master_key) - monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", MagicMock()) - monkeypatch.setattr( - "litellm.proxy.proxy_server.general_settings", {} - ) - monkeypatch.setattr("litellm.proxy.proxy_server.jwt_handler", MagicMock()) - monkeypatch.setattr( - "litellm.proxy.proxy_server.litellm_proxy_admin_name", - "default_user_id", - ) - monkeypatch.setattr("litellm.proxy.proxy_server.llm_model_list", None) - monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None) - monkeypatch.setattr("litellm.proxy.proxy_server.user_custom_auth", None) - monkeypatch.setattr( - "litellm.proxy.proxy_server.open_telemetry_logger", None - ) - monkeypatch.setattr( - "litellm.proxy.proxy_server.model_max_budget_limiter", MagicMock() - ) - mock_user_api_key_cache = MagicMock() - mock_user_api_key_cache.async_get_cache = AsyncMock(return_value=None) - mock_user_api_key_cache.get_cache = MagicMock(return_value=None) - monkeypatch.setattr( - "litellm.proxy.proxy_server.user_api_key_cache", - mock_user_api_key_cache, - ) - monkeypatch.setattr( - "litellm.proxy.proxy_server.proxy_logging_obj", MagicMock() - ) - - # Build a mock request that simulates: - # POST /key/block - # Authorization: Bearer - # Body: {"key": "sk-does-not-exist"} - mock_request = MagicMock() - mock_request.headers = { - "Authorization": f"Bearer {master_key}", - "Content-Type": "application/json", - } - mock_request.url.path = "/key/block" - mock_request.method = "POST" - mock_request.query_params = {} - mock_request.scope = { - "type": "http", - "path": "/key/block", - # Simulate a pre-parsed body with a 'key' field that differs from the master key - "parsed_body": ( - ("key",), - {"key": "sk-does-not-exist"}, - ), - } - mock_request.state = MagicMock() - - # Call user_api_key_auth the way FastAPI does. If the body 'key' field - # contaminates the api_key parameter, pass it explicitly to prove the - # guard works. - result = await user_api_key_auth( - request=mock_request, - api_key="sk-does-not-exist", # Simulates the contaminated value - ) - - # Auth should succeed — the guard re-reads the Authorization header - assert result is not None - assert result.user_role == LitellmUserRoles.PROXY_ADMIN From cd549bf4f5d61d3eaf0195371a5d2a5dcf98df0a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 18 Mar 2026 00:40:00 -0700 Subject: [PATCH 09/13] [Refactor] UI - Playground: Extract FilePreviewCard from ChatUI Extract duplicate file preview JSX blocks (responses and chat image previews) into a reusable FilePreviewCard component, reducing ~50 lines of duplicated markup in ChatUI.tsx. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/playground/chat_ui/ChatUI.tsx | 69 +++--------------- .../chat_ui/FilePreviewCard.test.tsx | 70 +++++++++++++++++++ .../playground/chat_ui/FilePreviewCard.tsx | 45 ++++++++++++ 3 files changed, 126 insertions(+), 58 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx index eba9d37669..bc0d52cf58 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx @@ -60,6 +60,7 @@ import CodeInterpreterOutput from "./CodeInterpreterOutput"; import CodeInterpreterTool from "./CodeInterpreterTool"; import { generateCodeSnippet } from "./CodeSnippets"; import EndpointSelector from "./EndpointSelector"; +import FilePreviewCard from "./FilePreviewCard"; import MCPEventsDisplay from "./MCPEventsDisplay"; import type { MCPEvent } from "../../mcp_tools/types"; import { EndpointType, getEndpointType } from "./mode_endpoint_mapping"; @@ -2231,67 +2232,19 @@ const ChatUI: React.FC = ({ {/* Show file previews above input when files are uploaded */} {endpointType === EndpointType.RESPONSES && responsesUploadedImage && ( -
-
-
- {responsesUploadedImage.name.toLowerCase().endsWith(".pdf") ? ( -
- -
- ) : ( - Upload preview - )} -
-
-
{responsesUploadedImage.name}
-
- {responsesUploadedImage.name.toLowerCase().endsWith(".pdf") ? "PDF" : "Image"} -
-
- -
-
+ )} {endpointType === EndpointType.CHAT && chatUploadedImage && ( -
-
-
- {chatUploadedImage.name.toLowerCase().endsWith(".pdf") ? ( -
- -
- ) : ( - Upload preview - )} -
-
-
{chatUploadedImage.name}
-
- {chatUploadedImage.name.toLowerCase().endsWith(".pdf") ? "PDF" : "Image"} -
-
- -
-
+ )} {/* Code Interpreter indicator and sample prompts when enabled */} diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx new file mode 100644 index 0000000000..1014df5d16 --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import FilePreviewCard from "./FilePreviewCard"; + +function makeFile(name: string): File { + return new File(["dummy"], name, { type: "application/octet-stream" }); +} + +describe("FilePreviewCard", () => { + it("should render", () => { + render( + + ); + expect(screen.getByText("photo.png")).toBeInTheDocument(); + }); + + it("should display the file name", () => { + render( + + ); + expect(screen.getByText("my-screenshot.jpg")).toBeInTheDocument(); + }); + + it("should show 'Image' label for non-PDF files", () => { + render( + + ); + expect(screen.getByText("Image")).toBeInTheDocument(); + }); + + it("should show 'PDF' label for PDF files", () => { + render( + + ); + expect(screen.getByText("PDF")).toBeInTheDocument(); + }); + + it("should render an image preview when the file is not a PDF", () => { + render( + + ); + expect(screen.getByAltText("Upload preview")).toBeInTheDocument(); + }); + + it("should not render an image preview when the file is a PDF", () => { + render( + + ); + expect(screen.queryByAltText("Upload preview")).not.toBeInTheDocument(); + }); + + it("should call onRemove when the remove button is clicked", async () => { + const onRemove = vi.fn(); + const user = userEvent.setup(); + render( + + ); + await user.click(screen.getByRole("button")); + expect(onRemove).toHaveBeenCalledOnce(); + }); + + it("should treat .PDF (uppercase) as a PDF file", () => { + render( + + ); + expect(screen.getByText("PDF")).toBeInTheDocument(); + expect(screen.queryByAltText("Upload preview")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx new file mode 100644 index 0000000000..26bdf07eda --- /dev/null +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx @@ -0,0 +1,45 @@ +import { DeleteOutlined, FilePdfOutlined } from "@ant-design/icons"; + +interface FilePreviewCardProps { + file: File; + previewUrl: string | null; + onRemove: () => void; +} + +function FilePreviewCard({ file, previewUrl, onRemove }: FilePreviewCardProps) { + const isPdf = file.name.toLowerCase().endsWith(".pdf"); + + return ( +
+
+
+ {isPdf ? ( +
+ +
+ ) : ( + Upload preview + )} +
+
+
{file.name}
+
+ {isPdf ? "PDF" : "Image"} +
+
+ +
+
+ ); +} + +export default FilePreviewCard; From dcc0c709365eca246ea16d7ded577728af2c6d52 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 18 Mar 2026 11:59:31 -0700 Subject: [PATCH 10/13] [Refactor] Extract _convert_teams_to_response_models to fix PLR0915 list_team_v2 had 51 statements (limit 50). Extract the team-to-response-model conversion loop into a helper function to satisfy ruff PLR0915. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../management_endpoints/team_endpoints.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index e7cd6c1748..bee4b2517e 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -3323,6 +3323,30 @@ async def _build_team_list_where_conditions( return where_conditions +def _convert_teams_to_response_models( + teams: list, + use_deleted_table: bool, +) -> List[Union[TeamListItem, LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]]: + """Convert raw Prisma team rows to response models.""" + team_list: List[Union[TeamListItem, LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]] = [] + for team in teams: + try: + team_dict = team.model_dump() + except Exception: + team_dict = team.dict() + + if use_deleted_table: + team_list.append(LiteLLM_DeletedTeamTable(**team_dict)) + else: + members_with_roles = team_dict.get("members_with_roles") + if not isinstance(members_with_roles, list): + members_with_roles = [] + team_dict["members_with_roles"] = members_with_roles + members_count = len(members_with_roles) + team_list.append(TeamListItem(**team_dict, members_count=members_count)) + return team_list + + @router.get( "/v2/team/list", tags=["team management"], @@ -3521,22 +3545,7 @@ async def list_team_v2( total_pages = -(-total_count // page_size) # Ceiling division # Convert Prisma models to response models with members_count - team_list: List[Union[TeamListItem, LiteLLM_TeamTable, LiteLLM_DeletedTeamTable]] = [] - for team in teams: - try: - team_dict = team.model_dump() - except Exception: - team_dict = team.dict() - - if use_deleted_table: - team_list.append(LiteLLM_DeletedTeamTable(**team_dict)) - else: - members_with_roles = team_dict.get("members_with_roles") - if not isinstance(members_with_roles, list): - members_with_roles = [] - team_dict["members_with_roles"] = members_with_roles - members_count = len(members_with_roles) - team_list.append(TeamListItem(**team_dict, members_count=members_count)) + team_list = _convert_teams_to_response_models(teams, use_deleted_table) return { "teams": team_list, From f88e51e1b925f82909d805f0b1a97c8734c50de9 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 18 Mar 2026 14:07:22 -0700 Subject: [PATCH 11/13] =?UTF-8?q?bump:=20version=200.4.57=20=E2=86=92=200.?= =?UTF-8?q?4.58?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- litellm-proxy-extras/pyproject.toml | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index 006aad9480..8aa9e70e43 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.4.57" +version = "0.4.58" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.4.57" +version = "0.4.58" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/pyproject.toml b/pyproject.toml index 0e2fb40d93..15fa1e3bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ boto3 = { version = "^1.40.76", optional = true } redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = ">=1.25.0,<2.0.0", optional = true, python = ">=3.10"} a2a-sdk = {version = "^0.3.22", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "^0.4.57", optional = true} +litellm-proxy-extras = {version = "^0.4.58", optional = true} rich = {version = "^13.7.1", optional = true} litellm-enterprise = {version = "^0.1.33", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 827986487f..d420f4ac60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,7 +57,7 @@ grpcio>=1.75.0; python_version >= "3.14" sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.4.57 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.4.58 # for proxy extras - e.g. prisma migrations llm-sandbox==0.3.31 # for skill execution in sandbox ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env From 7aa673a1375f9054a9df3c4ceb4675c5ecc72b54 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 18 Mar 2026 14:07:43 -0700 Subject: [PATCH 12/13] adding build --- ...litellm_proxy_extras-0.4.58-py3-none-any.whl | Bin 0 -> 76589 bytes .../dist/litellm_proxy_extras-0.4.58.tar.gz | Bin 0 -> 32038 bytes .../migration.sql | 9 +++++++++ 3 files changed, 9 insertions(+) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58.tar.gz create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260318140652_add_index_to_team_table/migration.sql diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..756080f85444bd262788ecc555780fd4d4264a11 GIT binary patch literal 76589 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 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8a52b0584056ce8886164d679617f3d60f298c83 GIT binary patch literal 32038 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 Date: Wed, 18 Mar 2026 14:09:46 -0700 Subject: [PATCH 13/13] updating poetry lock --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 591d0c270e..2c8a125a6e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3219,15 +3219,15 @@ files = [ [[package]] name = "litellm-proxy-extras" -version = "0.4.56" +version = "0.4.58" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." optional = true python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" groups = ["main"] markers = "extra == \"proxy\"" files = [ - {file = "litellm_proxy_extras-0.4.56-py3-none-any.whl", hash = "sha256:52dbe3b5358c790e77e12f1ec5ef8e7508b383c2aaf41299750b6fb400908ee7"}, - {file = "litellm_proxy_extras-0.4.56.tar.gz", hash = "sha256:63ad59baa0defccc5c929cfd933ee7e32a6614b0fc5fa0fc45a12d7608e33f08"}, + {file = "litellm_proxy_extras-0.4.58-py3-none-any.whl", hash = "sha256:8863e70de833c0e35119a1cbbf583619bdebe52222efd5654586519175ba403b"}, + {file = "litellm_proxy_extras-0.4.58.tar.gz", hash = "sha256:84a67483329eced8be4fc61c4e43f117287aa4e3deeb8ddf8fe8cdc9a8508836"}, ] [[package]] @@ -7994,4 +7994,4 @@ utils = ["numpydoc"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "1f3bbf967451633fb6290ba88980bdf4fbf83420024b14e862d1da717d903684" +content-hash = "eda34dfd8b35474beffee18893d6782c7b3d0d3d2c610f66237eb97176f43527"