diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index dd50b6be77a..9655420485c 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -5,6 +5,36 @@ export const formatDate = (date: Date) => { const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; }; + +export const getCallbackConfigsCall = async (accessToken: string) => { + /** + * Get callback configuration metadata (logos, params, etc.) + */ + try { + let url = proxyBaseUrl ? `${proxyBaseUrl}/callbacks/configs` : `/callbacks/configs`; + + const response = await fetch(url, { + method: "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(); + return data; + } catch (error) { + console.error("Failed to get callbacks:", error); + throw error; + } +}; /** * Helper file for calls being made to proxy */ diff --git a/ui/litellm-dashboard/src/components/settings.test.tsx b/ui/litellm-dashboard/src/components/settings.test.tsx index 20f4988c168..0cc684b156a 100644 --- a/ui/litellm-dashboard/src/components/settings.test.tsx +++ b/ui/litellm-dashboard/src/components/settings.test.tsx @@ -1,10 +1,11 @@ import { render, waitFor } from "@testing-library/react"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { alertingSettingsCall, getCallbacksCall } from "./networking"; +import { alertingSettingsCall, getCallbackConfigsCall, getCallbacksCall } from "./networking"; import Settings from "./settings"; vi.mock("./networking", () => ({ getCallbacksCall: vi.fn(), + getCallbackConfigsCall: vi.fn(), setCallbacksCall: vi.fn(), serviceHealthCheck: vi.fn(), deleteCallback: vi.fn(), @@ -65,6 +66,7 @@ describe("Settings", () => { premiumUser: false, }; const mockGetCallbacksCall = vi.mocked(getCallbacksCall); + const mockGetCallbackConfigsCall = vi.mocked(getCallbackConfigsCall); const mockAlertingSettingsCall = vi.mocked(alertingSettingsCall); beforeEach(() => { @@ -74,6 +76,7 @@ describe("Settings", () => { available_callbacks: [], alerts: [], }); + mockGetCallbackConfigsCall.mockResolvedValue([]); mockAlertingSettingsCall.mockResolvedValue([]); }); @@ -94,4 +97,12 @@ describe("Settings", () => { expect(getByText("Email Alerts")).toBeInTheDocument(); }); }); + + it("should load callback configs from the backend when access token is provided", async () => { + render(); + + await waitFor(() => { + expect(mockGetCallbackConfigsCall).toHaveBeenCalledWith(defaultProps.accessToken); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 907ef412c5a..3f06f08de6f 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -28,8 +28,13 @@ const { Title, Paragraph } = Typography; import FormItem from "antd/es/form/FormItem"; import AlertingSettings from "./alerting/alerting_settings"; -import { CALLBACK_CONFIGS, getCallbackById } from "./callback_info_helpers"; -import { deleteCallback, getCallbacksCall, serviceHealthCheck, setCallbacksCall } from "./networking"; +import { + deleteCallback, + getCallbackConfigsCall, + getCallbacksCall, + serviceHealthCheck, + setCallbacksCall, +} from "./networking"; import { LoggingCallbacksTable } from "./Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable"; import { AlertingObject } from "./Settings/LoggingAndAlerts/LoggingCallbacks/types"; import { parseErrorMessage } from "./shared/errorUtils"; @@ -46,6 +51,8 @@ interface genericCallbackParams { litellm_callback_params: string[] | null; // known required params for this callback } +const assetsLogoFolder = "../ui/assets/logos/"; + const Settings: React.FC = ({ accessToken, userRole, userID, premiumUser }) => { const [callbacks, setCallbacks] = useState([]); const [alerts, setAlerts] = useState([]); @@ -58,6 +65,7 @@ const Settings: React.FC = ({ accessToken, userRole, userID, const [activeAlerts, setActiveAlerts] = useState([]); const [showAddCallbacksModal, setShowAddCallbacksModal] = useState(false); + const [callbackConfigs, setCallbackConfigs] = useState([]); const [allCallbacks, setAllCallbacks] = useState< Record< string, @@ -76,6 +84,19 @@ const Settings: React.FC = ({ accessToken, userRole, userID, const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [callbackToDelete, setCallbackToDelete] = useState(null); + useEffect(() => { + if (!accessToken) { + return; + } + getCallbackConfigsCall(accessToken) + .then((data) => { + setCallbackConfigs(data || []); + }) + .catch((error) => { + NotificationsManager.fromBackend("Failed to load callback configs: " + parseErrorMessage(error)); + }); + }, [accessToken]); + useEffect(() => { if (showEditCallback && selectedEditCallback) { const normalized = Object.fromEntries( @@ -209,13 +230,10 @@ const Settings: React.FC = ({ accessToken, userRole, userID, const handleSelectedCallbackChange = (callbackName: string) => { setSelectedCallback(callbackName); - // Get the callback configuration using the new clean structure - const callbackConfig = getCallbackById(callbackName); + const callbackConfig = callbackConfigs.find((config) => config.id === callbackName); - // Get the parameters from the callback configuration if (callbackConfig?.dynamic_params) { - const params = Object.keys(callbackConfig.dynamic_params); - setSelectedCallbackParams(params); + setSelectedCallbackParams(Object.keys(callbackConfig.dynamic_params)); } else { setSelectedCallbackParams([]); } @@ -559,53 +577,61 @@ const Settings: React.FC = ({ accessToken, userRole, userID, handleSelectedCallbackChange(value); }} > - {CALLBACK_CONFIGS.map((callbackConfig) => ( - -
-
- {/* eslint-disable-next-line @next/next/no-img-element */} - {`${callbackConfig.displayName} { - e.currentTarget.style.display = "none"; - }} - /> + {callbackConfigs.map((callbackConfig) => { + const logo = callbackConfig.logo; + const logoSrc = + logo && (logo.includes("/") || logo.startsWith("data:") || logo.startsWith("http")) + ? logo + : `${assetsLogoFolder}${logo}`; + + return ( + +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`${callbackConfig.displayName} { + e.currentTarget.style.display = "none"; + }} + /> +
+ {callbackConfig.displayName}
- {callbackConfig.displayName} -
- - ))} + + ); + })} {selectedCallbackParams && selectedCallbackParams.length > 0 && (
{selectedCallbackParams.map((param) => { - // Get the callback configuration to look up parameter types - const callbackConfig = getCallbackById(selectedCallback || ""); - const paramType = callbackConfig?.dynamic_params[param] || "text"; - - const fieldLabel = param.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + const callbackConfig = callbackConfigs.find((config) => config.id === selectedCallback); + const paramConfig = callbackConfig?.dynamic_params?.[param] || {}; + const paramType = paramConfig.type || "text"; + const fieldLabel = + paramConfig.ui_name || param.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + const isRequired = paramConfig.required || false; return ( - {fieldLabel} - * - - } + label={{fieldLabel} } name={param} key={param} className="mb-4" - rules={[ - { - required: true, - message: `Please enter the ${fieldLabel.toLowerCase()}`, - }, - ]} + rules={ + isRequired + ? [ + { + required: true, + message: `Please enter the ${fieldLabel.toLowerCase()}`, + }, + ] + : undefined + } > {paramType === "password" ? (