diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx index 1a8c6632a03..bac1d571666 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx @@ -27,6 +27,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { organizationKeys } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions"; import SearchToolSelector from "@/components/SearchTools/SearchToolSelector"; +import FetchToolSelector from "@/components/FetchTools/FetchToolSelector"; interface ModelAliases { [key: string]: string; @@ -229,6 +230,10 @@ const CreateTeamModal = ({ Array.isArray(formValues.object_permission_search_tools) && formValues.object_permission_search_tools.length > 0; + const hasFetchTools = + Array.isArray(formValues.object_permission_fetch_tools) && + formValues.object_permission_fetch_tools.length > 0; + if ( (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) || (formValues.allowed_mcp_servers_and_groups && @@ -236,7 +241,8 @@ const CreateTeamModal = ({ formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0 || formValues.allowed_mcp_servers_and_groups.toolPermissions)) || hasAgents || - hasSearchTools + hasSearchTools || + hasFetchTools ) { if (!formValues.object_permission) { formValues.object_permission = {}; @@ -278,6 +284,11 @@ const CreateTeamModal = ({ formValues.object_permission.search_tools = formValues.object_permission_search_tools; delete formValues.object_permission_search_tools; } + + if (hasFetchTools) { + formValues.object_permission.fetch_tools = formValues.object_permission_fetch_tools; + delete formValues.object_permission_fetch_tools; + } } // Transform allowed_mcp_access_groups into object_permission @@ -776,6 +787,34 @@ const CreateTeamModal = ({ + + + Fetch Tool Settings + + + + Allowed Fetch Tools{" "} + + + + + } + name="object_permission_fetch_tools" + className="mt-4" + help="Restrict which configured fetch tools keys on this team may call." + > + form.setFieldValue("object_permission_fetch_tools", vals)} + value={form.getFieldValue("object_permission_fetch_tools")} + accessToken={accessToken || ""} + placeholder="Select fetch tools (optional, empty = all allowed)" + /> + + + + Logging Settings diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index a8553d5405b..25422101968 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -19,7 +19,13 @@ import { Team } from "@/components/key_team_helpers/key_list"; import { MCPServers } from "@/components/mcp_tools"; import ModelHubTable from "@/components/AIHub/ModelHubTable"; import Navbar from "@/components/navbar"; -import { getUiConfig, Organization, proxyBaseUrl, setGlobalLitellmHeaderName, getInProductNudgesCall } from "@/components/networking"; +import { + getUiConfig, + Organization, + proxyBaseUrl, + setGlobalLitellmHeaderName, + getInProductNudgesCall, +} from "@/components/networking"; import NewUsagePage from "@/components/UsagePage/components/UsagePageView"; import OldTeams from "@/components/OldTeams"; import { fetchUserModels, CreateKeyPrefillData } from "@/components/organisms/create_key_button"; @@ -28,6 +34,7 @@ import PassThroughSettings from "@/components/pass_through_settings"; import PromptsPanel from "@/components/prompts"; import PublicModelHub from "@/components/public_model_hub"; import { SearchTools } from "@/components/SearchTools"; +import { FetchTools } from "@/components/FetchTools"; import Settings from "@/components/settings"; import { SurveyPrompt, SurveyModal, ClaudeCodePrompt, ClaudeCodeModal } from "@/components/survey"; import TagManagement from "@/components/tag_management"; @@ -46,7 +53,13 @@ import ViewUserDashboard from "@/components/view_users"; import { ThemeProvider } from "@/contexts/ThemeContext"; import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; import { isJwtExpired } from "@/utils/jwtUtils"; -import { buildLoginUrlWithReturn, consumeReturnUrl, isValidReturnUrl, normalizeUrlForCompare, storeReturnUrl } from "@/utils/returnUrlUtils"; +import { + buildLoginUrlWithReturn, + consumeReturnUrl, + isValidReturnUrl, + normalizeUrlForCompare, + storeReturnUrl, +} from "@/utils/returnUrlUtils"; import { formatUserRole, isAdminRole } from "@/utils/roles"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { jwtDecode } from "jwt-decode"; @@ -137,15 +150,13 @@ function CreateKeyPageContent() { // Validate owned_by against allowed values const validOwnedByValues = ["you", "service_account", "another_user"]; - const validatedOwnedBy = ownedBy && validOwnedByValues.includes(ownedBy) - ? (ownedBy as CreateKeyPrefillData["owned_by"]) - : undefined; + const validatedOwnedBy = + ownedBy && validOwnedByValues.includes(ownedBy) ? (ownedBy as CreateKeyPrefillData["owned_by"]) : undefined; // Validate key_type against allowed values const validKeyTypes = ["default", "llm_api", "management"]; - const validatedKeyType = keyType && validKeyTypes.includes(keyType) - ? (keyType as CreateKeyPrefillData["key_type"]) - : undefined; + const validatedKeyType = + keyType && validKeyTypes.includes(keyType) ? (keyType as CreateKeyPrefillData["key_type"]) : undefined; // Sanitize key_alias (limit length, trim whitespace) const sanitizedKeyAlias = keyAlias @@ -157,8 +168,8 @@ function CreateKeyPageContent() { ? modelsParam .split(",") .slice(0, 100) // Limit number of models to prevent DoS - .map(m => m.trim().slice(0, 256)) // Limit individual model name length - .filter(m => m.length > 0) // Remove empty strings + .map((m) => m.trim().slice(0, 256)) // Limit individual model name length + .filter((m) => m.length > 0) // Remove empty strings : undefined; return { @@ -359,7 +370,9 @@ function CreateKeyPageContent() { if (accessToken && userID && userRole) { v2TeamListCall(accessToken, 1, 100, { userID: userRole !== "Admin" && userRole !== "Admin Viewer" ? userID : null, - }).then((response) => setTeams(response.teams ?? [])).catch(console.error); + }) + .then((response) => setTeams(response.teams ?? [])) + .catch(console.error); } if (accessToken) { fetchOrganizations(accessToken, setOrganizations); @@ -453,239 +466,237 @@ function CreateKeyPageContent() { return ( }> - - - {invitation_id ? ( - + + {invitation_id ? ( + + ) : ( +
+ - ) : ( -
- -
-
+
+
- {page == "api-keys" ? ( - + ) : page == "models" ? ( + + ) : page == "llm-playground" ? ( + + ) : page == "users" ? ( + + ) : page == "teams" ? ( + + ) : page == "organizations" ? ( + + ) : page == "admin-panel" ? ( + + ) : page == "logging-and-alerts" ? ( + + ) : page == "budgets" ? ( + + ) : page == "guardrails" ? ( + + ) : page == "policies" ? ( + + ) : page == "agents" ? ( + + ) : page == "prompts" ? ( + + ) : page == "transform-request" ? ( + + ) : page == "router-settings" ? ( + + ) : page == "ui-theme" ? ( + + ) : page == "cost-tracking" ? ( + + ) : page == "model-hub-table" ? ( + isAdminRole(userRole) ? ( + - ) : page == "models" ? ( - - ) : page == "llm-playground" ? ( - - ) : page == "users" ? ( - - ) : page == "teams" ? ( - - ) : page == "organizations" ? ( - - ) : page == "admin-panel" ? ( - - ) : page == "logging-and-alerts" ? ( - - ) : page == "budgets" ? ( - - ) : page == "guardrails" ? ( - - ) : page == "policies" ? ( - - ) : page == "agents" ? ( - - ) : page == "prompts" ? ( - - ) : page == "transform-request" ? ( - - ) : page == "router-settings" ? ( - - ) : page == "ui-theme" ? ( - - ) : page == "cost-tracking" ? ( - - ) : page == "model-hub-table" ? ( - isAdminRole(userRole) ? ( - - ) : ( - - ) - ) : page == "caching" ? ( - - ) : page == "pass-through-settings" ? ( - - ) : page == "logs" ? ( - - ) : page == "mcp-servers" ? ( - - ) : page == "search-tools" ? ( - - ) : page == "tag-management" ? ( - - ) : page == "skills" || page == "claude-code-plugins" ? ( - - ) : page == "access-groups" ? ( - - ) : page == "projects" ? ( - - ) : page == "vector-stores" ? ( - - ) : page == "tool-policies" ? ( - - ) : page == "workflows" ? ( - - ) : page == "memory" ? ( - - ) : page == "guardrails-monitor" ? ( - - ) : page == "new_usage" ? ( - ) : ( - - )} -
- - {/* Survey Components */} - - - - {/* Claude Code Components */} - - + + ) + ) : page == "caching" ? ( + + ) : page == "pass-through-settings" ? ( + + ) : page == "logs" ? ( + + ) : page == "mcp-servers" ? ( + + ) : page == "search-tools" ? ( + + ) : page == "fetch-tools" ? ( + + ) : page == "tag-management" ? ( + + ) : page == "skills" || page == "claude-code-plugins" ? ( + + ) : page == "access-groups" ? ( + + ) : page == "projects" ? ( + + ) : page == "vector-stores" ? ( + + ) : page == "tool-policies" ? ( + + ) : page == "workflows" ? ( + + ) : page == "memory" ? ( + + ) : page == "guardrails-monitor" ? ( + + ) : page == "new_usage" ? ( + + ) : ( + + )}
- )} - - + + {/* Survey Components */} + + + + {/* Claude Code Components */} + + +
+ )} + + ); } diff --git a/ui/litellm-dashboard/src/components/FetchTools/CreateFetchTool.tsx b/ui/litellm-dashboard/src/components/FetchTools/CreateFetchTool.tsx new file mode 100644 index 00000000000..51e0432dd96 --- /dev/null +++ b/ui/litellm-dashboard/src/components/FetchTools/CreateFetchTool.tsx @@ -0,0 +1,116 @@ +import { LoadingOutlined } from "@ant-design/icons"; +import { Form, Input, Modal, Select, Spin } from "antd"; +import React, { useState } from "react"; +import NotificationsManager from "../molecules/notifications_manager"; +import { createFetchTool } from "../networking"; +import { AvailableFetchProvider, FetchTool } from "./types"; + +interface CreateFetchToolProps { + accessToken: string | null; + availableProviders: AvailableFetchProvider[]; + onSuccess: (newFetchTool: FetchTool) => void; + onCancel: () => void; +} + +const CreateFetchTool: React.FC = ({ accessToken, availableProviders, onSuccess, onCancel }) => { + const [form] = Form.useForm(); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + if (!accessToken) return; + + try { + const values = await form.validateFields(); + setIsLoading(true); + + const fetchToolData = { + fetch_tool_name: values.fetch_tool_name, + litellm_params: { + fetch_provider: values.fetch_provider, + api_key: values.api_key, + api_base: values.api_base, + timeout: values.timeout ? parseFloat(values.timeout) : undefined, + max_retries: values.max_retries ? parseInt(values.max_retries) : undefined, + }, + fetch_tool_info: values.description + ? { + description: values.description, + } + : undefined, + }; + + const result = await createFetchTool(accessToken, fetchToolData); + NotificationsManager.success("Fetch tool created successfully"); + form.resetFields(); + onSuccess(result); + } catch (error) { + console.error("Failed to create fetch tool:", error); + NotificationsManager.error("Failed to create fetch tool"); + } finally { + setIsLoading(false); + } + }; + + return ( + } size="large"> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ ); +}; + +export default CreateFetchTool; diff --git a/ui/litellm-dashboard/src/components/FetchTools/FetchToolColumn.tsx b/ui/litellm-dashboard/src/components/FetchTools/FetchToolColumn.tsx new file mode 100644 index 00000000000..0503b0c7be4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/FetchTools/FetchToolColumn.tsx @@ -0,0 +1,110 @@ +import { Tag } from "antd"; +import { ColumnsType } from "antd/es/table"; +import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton"; +import { FetchTool } from "./types"; + +export const fetchToolColumns = ( + onView: (fetchToolId: string) => void, + onEdit: (fetchToolId: string) => void, + onDelete: (fetchToolId: string) => void, + availableProviders: Array<{ provider_name: string; ui_friendly_name: string }>, +): ColumnsType => [ + { + title: "Fetch Tool ID", + dataIndex: "fetch_tool_id", + key: "fetch_tool_id", + render: (_, tool) => { + const isFromConfig = tool.is_from_config; + + if (isFromConfig) { + return -; + } + + return ( + + ); + }, + }, + { + title: "Name", + dataIndex: "fetch_tool_name", + key: "fetch_tool_name", + render: (name: string) => {name}, + }, + { + title: "Provider", + key: "provider", + render: (_, tool) => { + const provider = tool.litellm_params.fetch_provider; + const providerInfo = availableProviders.find((p) => p.provider_name === provider); + const displayName = providerInfo?.ui_friendly_name || provider; + + return {displayName}; + }, + }, + { + title: "Created At", + dataIndex: "created_at", + key: "created_at", + render: (_, tool) => { + return {tool.created_at ? new Date(tool.created_at).toLocaleDateString() : "-"}; + }, + }, + { + title: "Updated At", + dataIndex: "updated_at", + key: "updated_at", + render: (_, tool) => { + return {tool.updated_at ? new Date(tool.updated_at).toLocaleDateString() : "-"}; + }, + }, + { + title: "Source", + key: "source", + render: (_, tool) => { + const isFromConfig = tool.is_from_config ?? false; + + return {isFromConfig ? "Config" : "DB"}; + }, + }, + { + title: "Actions", + key: "actions", + render: (_, tool) => { + const toolId = tool.fetch_tool_id; + const isFromConfig = tool.is_from_config ?? false; + + return ( +
+ { + if (toolId && !isFromConfig) { + onEdit(toolId); + } + }} + /> + { + if (toolId && !isFromConfig) { + onDelete(toolId); + } + }} + /> +
+ ); + }, + }, +]; diff --git a/ui/litellm-dashboard/src/components/FetchTools/FetchToolSelector.tsx b/ui/litellm-dashboard/src/components/FetchTools/FetchToolSelector.tsx new file mode 100644 index 00000000000..931f3b66856 --- /dev/null +++ b/ui/litellm-dashboard/src/components/FetchTools/FetchToolSelector.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from "react"; +import { Select } from "antd"; +import { fetchFetchTools } from "../networking"; + +export interface FetchToolSelectorProps { + onChange: (selected: string[]) => void; + value?: string[]; + className?: string; + accessToken: string; + placeholder?: string; + disabled?: boolean; +} + +const FetchToolSelector: React.FC = ({ + onChange, + value, + className, + accessToken, + placeholder = "Select fetch tools (optional)", + disabled = false, +}) => { + const [options, setOptions] = useState<{ label: string; value: string }[]>([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const load = async () => { + if (!accessToken) return; + setLoading(true); + try { + const data = await fetchFetchTools(accessToken); + const tools = Array.isArray(data?.fetch_tools) ? data.fetch_tools : Array.isArray(data?.data) ? data.data : []; + setOptions( + tools + .map((tool: { fetch_tool_name?: string }) => tool?.fetch_tool_name) + .filter((name: unknown): name is string => typeof name === "string" && name.length > 0) + .map((name: string) => ({ label: name, value: name })), + ); + } catch (e) { + console.error("Failed to load fetch tools:", e); + } finally { + setLoading(false); + } + }; + load(); + }, [accessToken]); + + return ( + + + + + + + + + + + + + + + + + + + + ); + + if (!accessToken || !userRole || !userID) { + console.log("Missing required authentication parameters", { accessToken, userRole, userID }); + return
Missing required authentication parameters.
; + } + + const ToolsTab = () => + selectedToolId ? ( + tool.fetch_tool_id === selectedToolId) || { + fetch_tool_id: "", + fetch_tool_name: "", + litellm_params: { + fetch_provider: "", + }, + } + } + onBack={() => { + setEditTool(false); + setSelectedToolId(null); + refetch(); + }} + isEditing={editTool} + accessToken={accessToken} + availableProviders={availableProviders} + /> + ) : ( +
+ } size="large"> + record.fetch_tool_id || record.fetch_tool_name} + pagination={false} + locale={{ + emptyText: "No fetch tools configured", + }} + size="small" + /> + + + ); + + return ( +
+ + +
+
+
+ Fetch Tools + Manage your fetch tools for web content extraction +
+ +
+ +
+ +
+
+ + {/* Create Modal */} + setCreateModalVisible(false)} + footer={null} + width={800} + > + setCreateModalVisible(false)} + /> + + + {/* Edit Modal */} + { + setEditModalVisible(false); + form.resetFields(); + setSelectedToolId(null); + }} + onOk={handleEditSubmit} + okText="Save Changes" + width={800} + > + {renderEditForm()} + +
+ ); +}; + +export default FetchTools; diff --git a/ui/litellm-dashboard/src/components/FetchTools/index.tsx b/ui/litellm-dashboard/src/components/FetchTools/index.tsx new file mode 100644 index 00000000000..8c665fabc9c --- /dev/null +++ b/ui/litellm-dashboard/src/components/FetchTools/index.tsx @@ -0,0 +1 @@ +export { default as FetchTools } from "./FetchTools"; diff --git a/ui/litellm-dashboard/src/components/FetchTools/types.tsx b/ui/litellm-dashboard/src/components/FetchTools/types.tsx new file mode 100644 index 00000000000..23f91a334d8 --- /dev/null +++ b/ui/litellm-dashboard/src/components/FetchTools/types.tsx @@ -0,0 +1,36 @@ +export interface FetchToolLiteLLMParams { + fetch_provider: string; + api_key?: string; + api_base?: string; + timeout?: number; + max_retries?: number; + [key: string]: any; +} + +export interface FetchToolInfo { + description?: string; + [key: string]: any; +} + +export interface FetchTool { + fetch_tool_id?: string; + fetch_tool_name: string; + litellm_params: FetchToolLiteLLMParams; + fetch_tool_info?: FetchToolInfo; + created_at?: string; + updated_at?: string; + is_from_config?: boolean; +} + +export interface FetchToolsResponse { + fetch_tools: FetchTool[]; +} + +export interface AvailableFetchProvider { + provider_name: string; + ui_friendly_name: string; +} + +export interface AvailableFetchProvidersResponse { + providers: AvailableFetchProvider[]; +} diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx index abc26c4cd44..be3a4a9875b 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.tsx @@ -516,6 +516,10 @@ const Teams: React.FC = ({ Array.isArray(formValues.object_permission_search_tools) && formValues.object_permission_search_tools.length > 0; + const hasFetchTools = + Array.isArray(formValues.object_permission_fetch_tools) && + formValues.object_permission_fetch_tools.length > 0; + if ( (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) || (formValues.allowed_mcp_servers_and_groups && @@ -579,6 +583,14 @@ const Teams: React.FC = ({ delete formValues.object_permission_search_tools; } + if (hasFetchTools) { + if (!formValues.object_permission) { + formValues.object_permission = {}; + } + formValues.object_permission.fetch_tools = formValues.object_permission_fetch_tools; + delete formValues.object_permission_fetch_tools; + } + // Add model_aliases if any are defined if (Object.keys(modelAliases).length > 0) { formValues.model_aliases = modelAliases; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index cecc99739b1..1cfc24c839e 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -13,6 +13,7 @@ import { BookOutlined, CreditCardOutlined, DatabaseOutlined, + DownloadOutlined, ExperimentOutlined, ExportOutlined, FileTextOutlined, @@ -32,7 +33,13 @@ import { import type { MenuProps } from "antd"; import { ConfigProvider, Layout, Menu } from "antd"; import { useMemo } from "react"; -import { all_admin_roles, internalUserRoles, isAdminRole, isUserTeamAdminForAnyTeam, rolesWithWriteAccess } from "../utils/roles"; +import { + all_admin_roles, + internalUserRoles, + isAdminRole, + isUserTeamAdminForAnyTeam, + rolesWithWriteAccess, +} from "../utils/roles"; import NewBadge from "./common_components/NewBadge"; import type { Organization } from "./networking"; import UsageIndicator from "./UsageIndicator"; @@ -169,11 +176,7 @@ const menuGroups: MenuGroup[] = [ { key: "policies", page: "policies", - label: ( - - Policies - - ), + label: Policies, icon: , roles: all_admin_roles, }, @@ -189,6 +192,12 @@ const menuGroups: MenuGroup[] = [ label: "Search Tools", icon: , }, + { + key: "fetch-tools", + page: "fetch-tools", + label: "Fetch Tools", + icon: , + }, { key: "vector-stores", page: "vector-stores", @@ -342,7 +351,7 @@ const menuGroups: MenuGroup[] = [ page: "usage", label: "Old Usage", icon: , - } + }, ], }, ], @@ -381,7 +390,10 @@ const menuGroups: MenuGroup[] = [ page: "admin-panel", label: ( - Admin Settings + Admin Settings{" "} + + + ), icon: , @@ -407,7 +419,17 @@ const menuGroups: MenuGroup[] = [ }, ]; -const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers, enableProjectsUI, disableAgentsForInternalUsers, allowAgentsForTeamAdmins, disableVectorStoresForInternalUsers, allowVectorStoresForTeamAdmins }) => { +const Sidebar: React.FC = ({ + setPage, + defaultSelectedKey, + collapsed = false, + enabledPagesInternalUsers, + enableProjectsUI, + disableAgentsForInternalUsers, + allowAgentsForTeamAdmins, + disableVectorStoresForInternalUsers, + allowVectorStoresForTeamAdmins, +}) => { const { userId, accessToken, userRole } = useAuthorized(); const { data: organizations } = useOrganizations(); const { data: teams } = useTeams(); @@ -438,11 +460,7 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse // Wrap label in so every nav item supports right-click → "Open in new tab" // and Ctrl/Cmd+click to open in a new tab, while preserving SPA navigation for normal clicks. - const renderNavLink = ( - label: React.ReactNode, - page: string, - externalUrl?: string, - ): React.ReactNode => { + const renderNavLink = (label: React.ReactNode, page: string, externalUrl?: string): React.ReactNode => { if (externalUrl) { return ( = ({ setPage, defaultSelectedKey, collapse const migratedRoute = MIGRATED_PAGES[page]; const href = migratedRoute ? migratedHref(migratedRoute) - : (() => { const params = new URLSearchParams(window.location.search); params.set("page", page); return `?${params.toString()}`; })(); + : (() => { + const params = new URLSearchParams(window.location.search); + params.set("page", page); + return `?${params.toString()}`; + })(); return ( = ({ setPage, defaultSelectedKey, collapse // Hide agents and vector-stores pages for non-admin users when disabled, // unless allow_*_for_team_admins is on and the user is a team admin. - if (!isAdmin && item.key === "agents" && disableAgentsForInternalUsers && !(allowAgentsForTeamAdmins && isTeamAdmin)) return false; - if (!isAdmin && item.key === "vector-stores" && disableVectorStoresForInternalUsers && !(allowVectorStoresForTeamAdmins && isTeamAdmin)) return false; + if ( + !isAdmin && + item.key === "agents" && + disableAgentsForInternalUsers && + !(allowAgentsForTeamAdmins && isTeamAdmin) + ) + return false; + if ( + !isAdmin && + item.key === "vector-stores" && + disableVectorStoresForInternalUsers && + !(allowVectorStoresForTeamAdmins && isTeamAdmin) + ) + return false; // Existing role check if (item.roles && !item.roles.includes(userRole)) return false; @@ -526,9 +560,7 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) { // If item has children, check if any children are visible if (item.children && item.children.length > 0) { - const hasVisibleChildren = item.children.some((child) => - enabledPagesInternalUsers.includes(child.page) - ); + const hasVisibleChildren = item.children.some((child) => enabledPagesInternalUsers.includes(child.page)); if (hasVisibleChildren) { console.log(`[LeftNav] Parent "${item.page}" (${item.key}): VISIBLE (has visible children)`); return true; @@ -593,12 +625,12 @@ const Sidebar: React.FC = ({ setPage, defaultSelectedKey, collapse })), onClick: !item.children ? () => { - if (item.external_url) { - window.open(item.external_url, "_blank"); - } else { - navigateToPage(item.page); + if (item.external_url) { + window.open(item.external_url, "_blank"); + } else { + navigateToPage(item.page); + } } - } : undefined, })), }); diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 44208904a70..d4165c4f81a 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -7067,6 +7067,160 @@ export const testSearchToolConnection = async (accessToken: string, litellmParam } }; +// Fetch Tools API calls +export const fetchFetchTools = async (accessToken: string) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/fetch_tools/list` : `/fetch_tools/list`; + console.log("Fetching fetch tools from:", url); + + const response = await fetch(url, { + method: HTTP_REQUEST.GET, + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log("Fetched fetch tools:", data); + return data; + } catch (error) { + console.error("Failed to fetch fetch tools:", error); + throw error; + } +}; + +export const createFetchTool = async (accessToken: string, formValues: Record) => { + try { + console.log("Creating fetch tool with values:", formValues); + const url = proxyBaseUrl ? `${proxyBaseUrl}/fetch_tools` : `/fetch_tools`; + + const response = await fetch(url, { + method: HTTP_REQUEST.POST, + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fetch_tool: formValues, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log("Created fetch tool:", data); + return data; + } catch (error) { + console.error("Failed to create fetch tool:", error); + throw error; + } +}; + +export const updateFetchTool = async (accessToken: string, fetchToolId: string, formValues: Record) => { + try { + console.log("Updating fetch tool with ID:", fetchToolId, "values:", formValues); + const url = proxyBaseUrl ? `${proxyBaseUrl}/fetch_tools/${fetchToolId}` : `/fetch_tools/${fetchToolId}`; + + const response = await fetch(url, { + method: HTTP_REQUEST.PUT, + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fetch_tool: formValues, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log("Updated fetch tool:", data); + return data; + } catch (error) { + console.error("Failed to update fetch tool:", error); + throw error; + } +}; + +export const deleteFetchTool = async (accessToken: string, fetchToolId: string) => { + try { + const url = (proxyBaseUrl ? `${proxyBaseUrl}` : "") + `/fetch_tools/${fetchToolId}`; + console.log("Deleting fetch tool:", fetchToolId); + + const response = await fetch(url, { + method: HTTP_REQUEST.DELETE, + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log("Deleted fetch tool:", data); + return data; + } catch (error) { + console.error("Failed to delete fetch tool:", error); + throw error; + } +}; + +export const fetchAvailableFetchProviders = async (accessToken: string) => { + try { + const url = proxyBaseUrl + ? `${proxyBaseUrl}/fetch_tools/ui/available_providers` + : `/fetch_tools/ui/available_providers`; + console.log("Fetching available fetch providers from:", url); + + const response = await fetch(url, { + method: HTTP_REQUEST.GET, + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + console.log("Fetched available fetch providers:", data); + return data; + } catch (error) { + console.error("Failed to fetch available fetch providers:", error); + throw error; + } +}; + export const listMCPTools = async ( accessToken: string, serverId: string, diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index d2c00e5326b..858f8e60782 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -18,7 +18,14 @@ import { useGuardrails } from "@/app/(dashboard)/hooks/guardrails/useGuardrails" import { formatNumberWithCommas } from "@/utils/dataUtils"; import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils"; import { isProxyAdminRole } from "@/utils/roles"; -import { EditOutlined, GlobalOutlined, InfoCircleOutlined, MinusCircleOutlined, PlusOutlined, SaveOutlined } from "@ant-design/icons"; +import { + EditOutlined, + GlobalOutlined, + InfoCircleOutlined, + MinusCircleOutlined, + PlusOutlined, + SaveOutlined, +} from "@ant-design/icons"; import { ArrowLeftIcon } from "@heroicons/react/outline"; import { Accordion, AccordionBody, AccordionHeader, Badge, Card, Grid, Text, TextInput, Title } from "@tremor/react"; import { Button, Form, Input, InputNumber, Select, Space, Switch, Tabs, Tag, Tooltip } from "antd"; @@ -43,6 +50,7 @@ import ObjectPermissionsView from "../object_permissions_view"; import NumericalInput from "../shared/numerical_input"; import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"; import SearchToolSelector from "../SearchTools/SearchToolSelector"; +import FetchToolSelector from "../FetchTools/FetchToolSelector"; import EditLoggingSettings from "./EditLoggingSettings"; import RouterSettingsAccordion, { RouterSettingsAccordionRef } from "../common_components/RouterSettingsAccordion"; import MemberModal from "./EditMembership"; @@ -120,6 +128,7 @@ export interface TeamData { agents?: string[]; agent_access_groups?: string[]; search_tools?: string[]; + fetch_tools?: string[]; }; team_member_budget_table: { max_budget: number; @@ -227,10 +236,7 @@ const TeamInfoView: React.FC = ({ const canEditTeam = is_team_admin || is_proxy_admin || is_org_admin || isOrgAdminForTeam; const visibleTabs = useMemo(() => getTeamInfoVisibleTabs(canEditTeam), [canEditTeam]); - const defaultTabKey = useMemo( - () => getTeamInfoDefaultTab(editTeam, canEditTeam), - [editTeam, canEditTeam] - ); + const defaultTabKey = useMemo(() => getTeamInfoDefaultTab(editTeam, canEditTeam), [editTeam, canEditTeam]); const fetchTeamInfo = async () => { try { @@ -322,7 +328,7 @@ const TeamInfoView: React.FC = ({ console.error(`Failed to fetch guardrails for policy ${policyName}:`, error); guardrailsMap[policyName] = []; } - }) + }), ); setPolicyGuardrails(guardrailsMap); } catch (error) { @@ -498,9 +504,7 @@ const TeamInfoView: React.FC = ({ const killSwitchOnAtSave = values.disable_global_guardrails === true; const optedOutGlobalGuardrails = killSwitchOnAtSave ? Array.from(globalGuardrailNames) - : Array.from(globalGuardrailNames).filter( - (n) => !(values.guardrails || []).includes(n), - ); + : Array.from(globalGuardrailNames).filter((n) => !(values.guardrails || []).includes(n)); const updateData: any = { team_id: teamId, @@ -522,16 +526,14 @@ const TeamInfoView: React.FC = ({ soft_budget_alerting_emails: typeof values.soft_budget_alerting_emails === "string" ? values.soft_budget_alerting_emails - .split(",") - .map((email: string) => email.trim()) - .filter((email: string) => email.length > 0) + .split(",") + .map((email: string) => email.trim()) + .filter((email: string) => email.length > 0) : values.soft_budget_alerting_emails || [], ...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}), }, ...(values.policies?.length > 0 ? { policies: values.policies } : {}), - ...(values.organization_id !== info.organization_id - ? { organization_id: values.organization_id ?? null } - : {}), + ...(values.organization_id !== info.organization_id ? { organization_id: values.organization_id ?? null } : {}), }; updateData.max_budget = mapEmptyStringToNull(updateData.max_budget); @@ -599,6 +601,10 @@ const TeamInfoView: React.FC = ({ updateData.object_permission.search_tools = values.object_permission_search_tools; } + if (Array.isArray(values.object_permission_fetch_tools)) { + updateData.object_permission.fetch_tools = values.object_permission_fetch_tools; + } + // Pass access_group_ids to the update request if (values.access_group_ids !== undefined) { updateData.access_group_ids = values.access_group_ids; @@ -620,8 +626,7 @@ const TeamInfoView: React.FC = ({ !(Array.isArray(value) && value.length === 0); const hasNewValues = Object.values(currentRouterSettings.router_settings).some(isMeaningfulValue); - const hadExistingSettings = info.router_settings && - Object.values(info.router_settings).some(isMeaningfulValue); + const hadExistingSettings = info.router_settings && Object.values(info.router_settings).some(isMeaningfulValue); // Send if there are new values OR if the user is clearing existing ones if (hasNewValues || hadExistingSettings) { @@ -659,10 +664,7 @@ const TeamInfoView: React.FC = ({ ); const effectiveGuardrails: string[] = initialKillSwitchOn ? nonGlobalOptIns - : [ - ...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)), - ...nonGlobalOptIns, - ]; + : [...Array.from(globalGuardrailNames).filter((n) => !optedOutGlobals.has(n)), ...nonGlobalOptIns]; const preventTagMouseDown = (e: React.MouseEvent) => { e.preventDefault(); @@ -699,12 +701,7 @@ const TeamInfoView: React.FC = ({
- {info.team_alias} @@ -715,10 +712,11 @@ const TeamInfoView: React.FC = ({ size="small" icon={copiedStates["team-id"] ? : } onClick={() => copyToClipboard(info.team_id, "team-id")} - className={`left-2 z-10 transition-all duration-200 ${copiedStates["team-id"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} + className={`left-2 z-10 transition-all duration-200 ${ + copiedStates["team-id"] + ? "text-green-600 bg-green-50 border-green-200" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" + }`} />
@@ -868,13 +866,7 @@ const TeamInfoView: React.FC = ({ { key: TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS, label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.VIRTUAL_KEYS], - children: ( - - ), + children: , }, { key: TEAM_INFO_TAB_KEYS.MEMBERS, @@ -893,9 +885,7 @@ const TeamInfoView: React.FC = ({ { key: TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS, label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS], - children: ( - - ), + children: , }, { key: TEAM_INFO_TAB_KEYS.SETTINGS, @@ -905,7 +895,9 @@ const TeamInfoView: React.FC = ({
Team Settings {canEditTeam && !isEditing && ( - + )}
@@ -933,6 +925,7 @@ const TeamInfoView: React.FC = ({ tpm_limit: info.tpm_limit, rpm_limit: info.rpm_limit, object_permission_search_tools: info.object_permission?.search_tools || [], + object_permission_fetch_tools: info.object_permission?.fetch_tools || [], modelLimits: Array.from( new Set([ ...Object.keys(info.metadata?.model_tpm_limit ?? {}), @@ -953,16 +946,22 @@ const TeamInfoView: React.FC = ({ guardrails: effectiveGuardrails, policies: info.policies || [], disable_global_guardrails: info.metadata?.disable_global_guardrails || false, - soft_budget_alerting_emails: - Array.isArray(info.metadata?.soft_budget_alerting_emails) - ? info.metadata.soft_budget_alerting_emails.join(", ") - : "", + soft_budget_alerting_emails: Array.isArray(info.metadata?.soft_budget_alerting_emails) + ? info.metadata.soft_budget_alerting_emails.join(", ") + : "", metadata: info.metadata ? JSON.stringify( - (({ logging, secret_manager_settings, soft_budget_alerting_emails, model_tpm_limit, model_rpm_limit, ...rest }) => rest)(info.metadata), - null, - 2, - ) + (({ + logging, + secret_manager_settings, + soft_budget_alerting_emails, + model_tpm_limit, + model_rpm_limit, + ...rest + }) => rest)(info.metadata), + null, + 2, + ) : "", logging_settings: info.metadata?.logging || [], secret_manager_settings: info.metadata?.secret_manager_settings @@ -1008,7 +1007,8 @@ const TeamInfoView: React.FC = ({ options={{ includeSpecialOptions: true, includeUserModels: !teamData?.team_info?.organization_id, - showAllProxyModelsOverride: isProxyAdminRole(userRole) && !teamData?.team_info?.organization_id, + showAllProxyModelsOverride: + isProxyAdminRole(userRole) && !teamData?.team_info?.organization_id, }} context="team" dataTestId="models-select" @@ -1037,7 +1037,8 @@ const TeamInfoView: React.FC = ({ - Optional defaults applied when members join this team. All fields can be overridden per member. + Optional defaults applied when members join this team. All fields can be overridden per + member. = ({ {(fields, { add, remove }) => ( <> {fields.map(({ key, name, ...restField }) => ( - + = ({ validator: (_, value) => { if (!value) return Promise.resolve(); const all = form.getFieldValue("modelLimits") ?? []; - const dupes = all.filter( - (entry: { model?: string }) => entry?.model === value, - ); + const dupes = all.filter((entry: { model?: string }) => entry?.model === value); if (dupes.length > 1) { return Promise.reject(new Error("Duplicate model")); } @@ -1182,19 +1177,11 @@ const TeamInfoView: React.FC = ({ - remove(name)} - style={{ color: "#ef4444" }} - /> + remove(name)} style={{ color: "#ef4444" }} /> ))} - @@ -1260,11 +1247,7 @@ const TeamInfoView: React.FC = ({ {(guardrailsData?.guardrails ?? []) .filter((g) => !g.litellm_params?.default_on) .map((g) => ( - + {g.guardrail_name} ))} @@ -1406,6 +1389,26 @@ const TeamInfoView: React.FC = ({ + + + Fetch Tool Settings + + + + form.setFieldValue("object_permission_fetch_tools", vals)} + value={form.getFieldValue("object_permission_fetch_tools")} + accessToken={accessToken || ""} + placeholder="Select fetch tools (optional, empty = all allowed)" + /> + + + +