diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 7fe02c6d8bc..0f7a933e77c 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -782,6 +782,13 @@ }, "ValidationError": { "properties": { + "ctx": { + "title": "Context", + "type": "object" + }, + "input": { + "title": "Input" + }, "loc": { "items": { "anyOf": [ @@ -1202,6 +1209,61 @@ "access_groups" ] }, + "patch": { + "operationId": "update_access_group_v1_access_group__access_group_id__patch", + "parameters": [ + { + "in": "path", + "name": "access_group_id", + "required": true, + "schema": { + "title": "Access Group Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroupUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroupResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Update Access Group", + "tags": [ + "access_groups" + ] + }, "put": { "operationId": "update_access_group_v1_access_group__access_group_id__put", "parameters": [ @@ -1416,6 +1478,61 @@ "access_groups" ] }, + "patch": { + "operationId": "update_access_group_v1_unified_access_group__access_group_id__patch", + "parameters": [ + { + "in": "path", + "name": "access_group_id", + "required": true, + "schema": { + "title": "Access Group Id", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroupUpdateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessGroupResponse" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Update Access Group", + "tags": [ + "access_groups" + ] + }, "put": { "operationId": "update_access_group_v1_unified_access_group__access_group_id__put", "parameters": [ diff --git a/litellm/proxy/management_endpoints/access_group_endpoints.py b/litellm/proxy/management_endpoints/access_group_endpoints.py index 2271501d480..117b5b3f824 100644 --- a/litellm/proxy/management_endpoints/access_group_endpoints.py +++ b/litellm/proxy/management_endpoints/access_group_endpoints.py @@ -416,6 +416,10 @@ async def get_access_group( return _record_to_response(record) +@router.patch( + "/v1/access_group/{access_group_id}", + response_model=AccessGroupResponse, +) @router.put( "/v1/access_group/{access_group_id}", response_model=AccessGroupResponse, @@ -637,6 +641,12 @@ router.add_api_route( methods=["PUT"], response_model=AccessGroupResponse, ) +router.add_api_route( + "/v1/unified_access_group/{access_group_id}", + update_access_group, + methods=["PATCH"], + response_model=AccessGroupResponse, +) router.add_api_route( "/v1/unified_access_group/{access_group_id}", delete_access_group, diff --git a/tests/test_litellm/proxy/management_endpoints/test_access_group_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_access_group_endpoints.py index 016e10859b6..9331555bd62 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_access_group_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_access_group_endpoints.py @@ -410,6 +410,7 @@ def test_get_access_group_forbidden_non_admin(client_and_mocks, user_role): # --------------------------------------------------------------------------- +@pytest.mark.parametrize("method", ["put", "patch"]) @pytest.mark.parametrize("base_path", ACCESS_GROUP_PATHS) @pytest.mark.parametrize( "update_payload", @@ -419,18 +420,36 @@ def test_get_access_group_forbidden_non_admin(client_and_mocks, user_role): {"assigned_team_ids": [], "assigned_key_ids": ["key-1"]}, ], ) -def test_update_access_group_success(client_and_mocks, base_path, update_payload): - """Update access group with various payloads returns 200.""" +def test_update_access_group_success(client_and_mocks, method, base_path, update_payload): + """Update access group with various payloads returns 200 over both PUT and PATCH.""" client, _, mock_table, *_ = client_and_mocks existing = _make_access_group_record(access_group_id="ag-update") mock_table.find_unique = AsyncMock(return_value=existing) - resp = client.put(f"{base_path}/ag-update", json=update_payload) + resp = client.request(method, f"{base_path}/ag-update", json=update_payload) assert resp.status_code == 200 mock_table.update.assert_awaited_once() +@pytest.mark.parametrize("base_path", ACCESS_GROUP_PATHS) +def test_patch_access_group_writes_only_sent_fields(client_and_mocks, base_path): + """PATCH with one field leaves every other column out of the write, so untouched grants survive.""" + client, _, mock_table, *_ = client_and_mocks + + existing = _make_access_group_record( + access_group_id="ag-update", access_model_names=["model-1"], access_agent_ids=["agent-1"] + ) + mock_table.find_unique = AsyncMock(return_value=existing) + + resp = client.patch(f"{base_path}/ag-update", json={"description": "Only this changes"}) + assert resp.status_code == 200 + assert mock_table.update.call_args.kwargs["data"] == { + "updated_by": "admin_user", + "description": "Only this changes", + } + + def test_update_access_group_not_found(client_and_mocks): """Update access group returns 404 when not found.""" client, _, mock_table, *_ = client_and_mocks diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.test.tsx index cf41f623fd6..ba256d713a2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.test.tsx @@ -7,11 +7,11 @@ import { renderWithProviders } from "../../../../../tests/test-utils"; import { AccessGroupDetail } from "./AccessGroupsDetailsPage"; vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails"); -vi.mock("./AccessGroupsModal/AccessGroupEditModal", () => ({ - AccessGroupEditModal: ({ visible, onCancel }: { visible: boolean; onCancel: () => void }) => - visible ? ( +vi.mock("./access-group-edit/AccessGroupEditDialog", () => ({ + AccessGroupEditDialog: ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => + open ? (
- +
) : null, })); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx index 9476a8d98af..6d72489c052 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsDetailsPage.tsx @@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; -import { AccessGroupEditModal } from "./AccessGroupsModal/AccessGroupEditModal"; +import { AccessGroupEditDialog } from "./access-group-edit/AccessGroupEditDialog"; interface AccessGroupDetailProps { accessGroupId: string; @@ -218,11 +218,7 @@ export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailPr - setIsEditModalVisible(false)} - /> + ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupBaseForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupBaseForm.tsx deleted file mode 100644 index 72b89e34301..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsModal/AccessGroupBaseForm.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents"; -import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; -import { ModelSelect } from "@/components/ModelSelect/ModelSelect"; -import type { FormInstance } from "antd"; -import { Form, Input, Select, Space, Tabs } from "antd"; -import { BotIcon, InfoIcon, LayersIcon, ServerIcon } from "lucide-react"; - -const { TextArea } = Input; - -export interface AccessGroupFormValues { - name: string; - description: string; - modelIds: string[]; - mcpServerIds: string[]; - agentIds: string[]; -} - -interface AccessGroupBaseFormProps { - form: FormInstance; - isNameDisabled?: boolean; -} - -export function AccessGroupBaseForm({ form, isNameDisabled = false }: AccessGroupBaseFormProps) { - const { data: agentsData } = useAgents(); - const { data: mcpServersData } = useMCPServers(); - - const agents = agentsData?.agents ?? []; - const mcpServers = mcpServersData ?? []; - const items = [ - { - key: "1", - label: ( - - - General Info - - ), - children: ( -
- - - - -