Edit add callbacks route to use data from backend (#16699)

This commit is contained in:
yuneng-jiang 2025-11-15 18:00:36 -08:00 committed by GitHub
parent 84df0634f1
commit 6375959afe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 42 deletions

View file

@ -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
*/

View file

@ -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(<Settings {...defaultProps} />);
await waitFor(() => {
expect(mockGetCallbackConfigsCall).toHaveBeenCalledWith(defaultProps.accessToken);
});
});
});

View file

@ -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<SettingsPageProps> = ({ accessToken, userRole, userID, premiumUser }) => {
const [callbacks, setCallbacks] = useState<AlertingObject[]>([]);
const [alerts, setAlerts] = useState<any[]>([]);
@ -58,6 +65,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
const [activeAlerts, setActiveAlerts] = useState<string[]>([]);
const [showAddCallbacksModal, setShowAddCallbacksModal] = useState(false);
const [callbackConfigs, setCallbackConfigs] = useState<any[]>([]);
const [allCallbacks, setAllCallbacks] = useState<
Record<
string,
@ -76,6 +84,19 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
const [callbackToDelete, setCallbackToDelete] = useState<string | null>(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<SettingsPageProps> = ({ 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<SettingsPageProps> = ({ accessToken, userRole, userID,
handleSelectedCallbackChange(value);
}}
>
{CALLBACK_CONFIGS.map((callbackConfig) => (
<SelectItem key={callbackConfig.id} value={callbackConfig.id}>
<div className="flex items-center space-x-3 py-1">
<div className="w-6 h-6 flex items-center justify-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={callbackConfig.logo}
alt={`${callbackConfig.displayName} logo`}
className="w-6 h-6 rounded object-contain"
onError={(e) => {
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 (
<SelectItem key={callbackConfig.id} value={callbackConfig.id}>
<div className="flex items-center space-x-3 py-1">
<div className="w-6 h-6 flex items-center justify-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={logoSrc}
alt={`${callbackConfig.displayName} logo`}
className="w-6 h-6 rounded object-contain"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
</div>
<span className="font-medium text-gray-900">{callbackConfig.displayName}</span>
</div>
<span className="font-medium text-gray-900">{callbackConfig.displayName}</span>
</div>
</SelectItem>
))}
</SelectItem>
);
})}
</Select>
</FormItem>
{selectedCallbackParams && selectedCallbackParams.length > 0 && (
<div className="space-y-4 mt-6 p-4 bg-gray-50 rounded-lg border">
{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 (
<FormItem
label={
<span className="text-sm font-medium text-gray-700">
{fieldLabel}
<span className="text-red-500 ml-1">*</span>
</span>
}
label={<span className="text-sm font-medium text-gray-700">{fieldLabel} </span>}
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" ? (
<Input.Password