diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index f09302fbdde..2169dac38da 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -372,6 +372,10 @@ class LiteLLMRoutes(enum.Enum): "/v1/search", "/search/{search_tool_name}", "/v1/search/{search_tool_name}", + "/search_tools/list", + "/search_tools/ui/available_providers", + "/search/tools", + "/v1/search/tools", # OCR "/ocr", "/v1/ocr", diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 3a4f234d800..a747349c2e6 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -1001,7 +1001,7 @@ async def new_team( # noqa: PLR0915 team_row: LiteLLM_TeamTable = await prisma_client.db.litellm_teamtable.create( data=complete_team_data_dict, - include={"litellm_model_table": True}, # type: ignore + include={"litellm_model_table": True, "object_permission": True}, # type: ignore ) ## ADD TEAM ID TO USER TABLE ## @@ -1550,7 +1550,7 @@ async def update_team( # noqa: PLR0915 ] = await prisma_client.db.litellm_teamtable.update( where={"team_id": data.team_id}, data=updated_kv, - include={"litellm_model_table": True}, # type: ignore + include={"litellm_model_table": True, "object_permission": True}, # type: ignore ) if team_row is None or team_row.team_id is None: @@ -3590,6 +3590,7 @@ async def list_team_v2( skip=skip, take=page_size, order=order_by if order_by else {"created_at": "desc"}, # Default sort + include={"object_permission": True}, ) # Get total count for pagination total_count = await prisma_client.db.litellm_teamtable.count( @@ -3668,7 +3669,7 @@ async def _authorize_and_filter_teams( # Org admin: query DB for teams in their orgs org_teams = await prisma_client.db.litellm_teamtable.find_many( where={"organization_id": {"in": allowed_org_ids}}, - include={"litellm_model_table": True}, + include={"litellm_model_table": True, "object_permission": True}, ) if not user_id: return list(org_teams) @@ -3678,7 +3679,7 @@ async def _authorize_and_filter_teams( # Prisma doesn't support filtering JSON array fields, so we fetch by membership separately member_teams = await prisma_client.db.litellm_teamtable.find_many( where={"team_id": {"not_in": list(seen_team_ids)}} if seen_team_ids else {}, - include={"litellm_model_table": True}, + include={"litellm_model_table": True, "object_permission": True}, ) for team in member_teams: if team.members_with_roles and any( @@ -3689,7 +3690,7 @@ async def _authorize_and_filter_teams( elif user_id: # Regular user: fetch all and filter by membership (Prisma can't filter JSON arrays) response = await prisma_client.db.litellm_teamtable.find_many( - include={"litellm_model_table": True} + include={"litellm_model_table": True, "object_permission": True} ) return [ team @@ -3701,7 +3702,7 @@ async def _authorize_and_filter_teams( # Proxy admin: all teams return list( await prisma_client.db.litellm_teamtable.find_many( - include={"litellm_model_table": True} + include={"litellm_model_table": True, "object_permission": True} ) ) diff --git a/litellm/proxy/search_endpoints/search_tool_management.py b/litellm/proxy/search_endpoints/search_tool_management.py index c46bbfddcac..fac12ebf256 100644 --- a/litellm/proxy/search_endpoints/search_tool_management.py +++ b/litellm/proxy/search_endpoints/search_tool_management.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from litellm._logging import verbose_proxy_logger -from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth from litellm.proxy.search_endpoints.search_tool_registry import SearchToolRegistry from litellm.types.search import ( ListSearchToolsResponse, @@ -46,7 +46,9 @@ def _convert_datetime_to_str(value: Union[datetime, str, None]) -> Union[str, No dependencies=[Depends(user_api_key_auth)], response_model=ListSearchToolsResponse, ) -async def list_search_tools(): +async def list_search_tools( + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): """ List all search tools that are available in the database and config file. @@ -161,6 +163,19 @@ async def list_search_tools(): ) ) + # Filter based on caller's key/team permissions + from litellm.proxy.search_endpoints.endpoints import ( + _get_allowed_search_tool_names, + ) + + allowed_names = await _get_allowed_search_tool_names(user_api_key_dict) + if allowed_names is not None: + search_tool_configs = [ + tool + for tool in search_tool_configs + if tool.get("search_tool_name") in allowed_names + ] + return ListSearchToolsResponse(search_tools=search_tool_configs) except Exception as e: verbose_proxy_logger.exception(f"Error getting search tools: {e}") diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts index a86b5cd51f6..f74a71e901e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts @@ -1,4 +1,4 @@ -import { keepPreviousData, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query"; +import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query"; import { Team } from "@/components/key_team_helpers/key_list"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { fetchTeams } from "@/app/(dashboard)/networking"; @@ -124,6 +124,43 @@ export const useTeam = (teamId?: string) => { }); }; +const infiniteTeamKeys = createQueryKeys("infiniteTeams"); + +export const useInfiniteTeams = ( + pageSize: number = 50, + search?: string, + organizationId?: string | null, +) => { + const { accessToken, userId, userRole } = useAuthorized(); + const isAdmin = userRole === "Admin" || userRole === "Admin Viewer"; + + return useInfiniteQuery({ + queryKey: infiniteTeamKeys.list({ + filters: { + pageSize, + ...(search && { search }), + ...(organizationId && { organizationId }), + ...(userId && { userId }), + }, + }), + queryFn: async ({ pageParam }) => { + return await teamListCall(accessToken!, pageParam as number, pageSize, { + team_alias: search || undefined, + organizationID: organizationId, + userID: !isAdmin ? userId : undefined, + }); + }, + initialPageParam: 1, + getNextPageParam: (lastPage) => { + if (lastPage.page < lastPage.total_pages) { + return lastPage.page + 1; + } + return undefined; + }, + enabled: Boolean(accessToken), + }); +}; + const deletedTeamListCall = async ( accessToken: string, page: number, diff --git a/ui/litellm-dashboard/src/components/CreateUserButton.tsx b/ui/litellm-dashboard/src/components/CreateUserButton.tsx index fbfcb402766..fc29887a5da 100644 --- a/ui/litellm-dashboard/src/components/CreateUserButton.tsx +++ b/ui/litellm-dashboard/src/components/CreateUserButton.tsx @@ -210,9 +210,7 @@ export const CreateUserButton: React.FC = ({ - + @@ -294,7 +292,7 @@ export const CreateUserButton: React.FC = ({ name="team_id" help="If selected, user will be added as a 'user' role to the team." > - + = ({ delete formValues.allowed_agents_and_groups; } + // Always send search_tools to ensure the permission record is created. + // Empty array = no access (least privilege for new teams). + if (!formValues.object_permission) { + formValues.object_permission = {}; + } + formValues.object_permission.search_tools = formValues.allowed_search_tool_ids || []; + delete formValues.allowed_search_tool_ids; + // Add model_aliases if any are defined if (Object.keys(modelAliases).length > 0) { formValues.model_aliases = modelAliases; @@ -579,14 +589,12 @@ const Teams: React.FC = ({ } } - const response: any = await teamCreateCall(accessToken, formValues); - if (teams !== null) { - setTeams([...teams, response]); - } else { - setTeams([response]); - } - console.log(`response for team create call: ${response}`); + await teamCreateCall(accessToken, formValues); NotificationsManager.success("Team created"); + await fetchTeamsV2({ + page: currentPage, + size: pageSize, + }); form.resetFields(); setLoggingSettings([]); setModelAliases({}); @@ -1516,6 +1524,40 @@ const Teams: React.FC = ({ + + + Search Tool Settings + + + + + Allowed Search Tools{" "} + + + + + } + name="allowed_search_tool_ids" + className="mt-4" + > + form.setFieldValue("allowed_search_tool_ids", values)} + value={form.getFieldValue("allowed_search_tool_ids")} + accessToken={accessToken || ""} + placeholder="Select search tools (defaults to no access)" + /> + + + + Logging Settings diff --git a/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx b/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx index 49b57c2a884..c5763773502 100644 --- a/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx +++ b/ui/litellm-dashboard/src/components/SearchTools/CreateSearchTools.tsx @@ -1,50 +1,14 @@ import { isAdminRole } from "@/utils/roles"; import { InfoCircleOutlined } from "@ant-design/icons"; import { useQuery } from "@tanstack/react-query"; -import { Button, TextInput } from "@tremor/react"; -import { Form, Input, Modal, Select, Tooltip, Typography } from "antd"; -import Image from "next/image"; +import { Button, Form, Input, Modal, Select, Tooltip, Typography } from "antd"; import React, { useState } from "react"; +import { ProviderLogo } from "../molecules/models/ProviderLogo"; import NotificationsManager from "../molecules/notifications_manager"; import { createSearchTool, fetchAvailableSearchProviders } from "../networking"; import SearchConnectionTest from "./SearchConnectionTest"; import { AvailableSearchProvider, SearchTool } from "./types"; -const { TextArea } = Input; - -// Search provider logos folder path (matches existing provider logo pattern) -const searchProviderLogosFolder = "../ui/assets/logos/"; - -// Helper function to get logo path for a search provider -const getSearchProviderLogo = (providerName: string): string => { - return `${searchProviderLogosFolder}${providerName}.png`; -}; - -// Component to display search provider logo and name -interface SearchProviderLabelProps { - providerName: string; - displayName: string; -} - -const SearchProviderLabel: React.FC = ({ providerName, displayName }) => ( -
- { - e.currentTarget.style.display = "none"; - }} - /> - {displayName} -
-); - interface CreateSearchToolProps { userRole: string; accessToken: string | null; @@ -85,7 +49,6 @@ const CreateSearchTool: React.FC = ({ const handleCreate = async (formValues: Record) => { setIsLoading(true); try { - // Prepare the payload const payload = { search_tool_name: formValues.search_tool_name, litellm_params: { @@ -102,8 +65,6 @@ const CreateSearchTool: React.FC = ({ : undefined, }; - console.log(`Creating search tool with payload:`, payload); - if (accessToken != null) { const response = await createSearchTool(accessToken, payload); @@ -128,13 +89,10 @@ const CreateSearchTool: React.FC = ({ const handleTestConnection = async () => { try { - // Validate required fields for testing await form.validateFields(["search_provider", "api_key"]); setIsTestingConnection(true); - // Generate a new test ID (using timestamp for uniqueness) setConnectionTestId(`test-${Date.now()}`); - // Show the modal with the fresh test setIsTestModalVisible(true); } catch (error) { NotificationsManager.error("Please fill in Search Provider and API Key before testing"); @@ -154,144 +112,112 @@ const CreateSearchTool: React.FC = ({ return ( - 🔍 -

Add New Search Tool

+ title="Add New Search Tool" + open={isModalVisible} + width={600} + onCancel={handleCancel} + footer={ +
+ + Need Help? + +
+ + +
} - open={isModalVisible} - width={800} - onCancel={handleCancel} - footer={null} - className="top-8" - styles={{ - body: { padding: "24px" }, - header: { padding: "24px 24px 0 24px", border: "none" }, - }} > -
-
setFormValues(allValues)} - layout="vertical" - className="space-y-6" + setFormValues(allValues)} + layout="vertical" + className="mt-4" + > + + Search Tool Name{" "} + + + + + } + name="search_tool_name" + rules={[ + { required: true, message: "Please enter a search tool name" }, + { + pattern: /^[a-zA-Z0-9_-]+$/, + message: "Name can only contain letters, numbers, hyphens, and underscores", + }, + ]} > -
- - Search Tool Name - - - - - } - name="search_tool_name" - rules={[ - { required: true, message: "Please enter a search tool name" }, - { - pattern: /^[a-zA-Z0-9_-]+$/, - message: "Name can only contain letters, numbers, hyphens, and underscores", - }, - ]} - > - - + + - - Search Provider - - - - - } - name="search_provider" - rules={[{ required: true, message: "Please select a search provider" }]} - > - + {availableProviders.map((provider) => ( + - {availableProviders.map((provider) => ( - - } - > - - - ))} - - +
+ + {provider.ui_friendly_name} +
+ + ))} + + - - API Key - - - - - } - name="api_key" - rules={[{ required: false, message: "Please enter an API key" }]} - > - - + + API Key{" "} + + + + + } + name="api_key" + > + + - Description (Optional)} - name="description" - > -