diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 9ab7c2c7e61..24105499792 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -1139,11 +1139,88 @@ async def get_pass_through_endpoints( "/config/pass_through_endpoint/{endpoint_id}", dependencies=[Depends(user_api_key_auth)], ) -async def update_pass_through_endpoints(request: Request, endpoint_id: str): +async def update_pass_through_endpoints( + endpoint_id: str, + data: PassThroughGenericEndpoint, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): """ Update a pass-through endpoint """ - pass + from litellm.proxy.proxy_server import ( + get_config_general_settings, + update_config_general_settings, + ) + + ## Get existing pass-through endpoint field value + try: + response: ConfigFieldInfo = await get_config_general_settings( + field_name="pass_through_endpoints", user_api_key_dict=user_api_key_dict + ) + except Exception: + raise HTTPException( + status_code=404, + detail={"error": "No pass-through endpoints found"}, + ) + + pass_through_endpoint_data: Optional[List] = response.field_value + if pass_through_endpoint_data is None: + raise HTTPException( + status_code=404, + detail={"error": "No pass-through endpoints found"}, + ) + + # Find and update the endpoint + updated_endpoint: Optional[PassThroughGenericEndpoint] = None + endpoint_found = False + + for idx, endpoint in enumerate(pass_through_endpoint_data): + _endpoint: Optional[PassThroughGenericEndpoint] = None + if isinstance(endpoint, dict): + _endpoint = PassThroughGenericEndpoint(**endpoint) + elif isinstance(endpoint, PassThroughGenericEndpoint): + _endpoint = endpoint + + if _endpoint is not None and _endpoint.path == endpoint_id: + endpoint_found = True + # Get the update data as dict, excluding None values for partial updates + update_data = data.model_dump(exclude_none=True) + + # Start with existing endpoint data + endpoint_dict = _endpoint.model_dump() + + # Update with new data (only non-None values) + endpoint_dict.update(update_data) + + # Ensure the path stays the same (can't change the endpoint_id) + endpoint_dict["path"] = endpoint_id + + # Create updated endpoint object + updated_endpoint = PassThroughGenericEndpoint(**endpoint_dict) + + # Update the list + pass_through_endpoint_data[idx] = endpoint_dict + break + + if not endpoint_found: + raise HTTPException( + status_code=404, + detail={ + "error": f"Endpoint with path '{endpoint_id}' not found" + }, + ) + + ## Update db + updated_data = ConfigFieldUpdate( + field_name="pass_through_endpoints", + field_value=pass_through_endpoint_data, + config_type="general_settings", + ) + await update_config_general_settings( + data=updated_data, user_api_key_dict=user_api_key_dict + ) + + return PassThroughEndpointResponse(endpoints=[updated_endpoint] if updated_endpoint else []) @router.post( diff --git a/ui/litellm-dashboard/src/components/add_pass_through.tsx b/ui/litellm-dashboard/src/components/add_pass_through.tsx index efea49e86b0..800c4ac9340 100644 --- a/ui/litellm-dashboard/src/components/add_pass_through.tsx +++ b/ui/litellm-dashboard/src/components/add_pass_through.tsx @@ -77,7 +77,7 @@ const AddPassThroughEndpoint: React.FC = ({ return (
+
+ ); +}; + +const PassThroughInfoView: React.FC = ({ + endpointPath, + onClose, + accessToken, + isAdmin, + onEndpointUpdated +}) => { + const [endpointData, setEndpointData] = useState(null); + const [loading, setLoading] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const [form] = Form.useForm(); + + const fetchEndpointInfo = async () => { + try { + setLoading(true); + if (!accessToken) return; + const response = await getPassThroughEndpointInfo(accessToken, endpointPath); + setEndpointData(response); + } catch (error) { + message.error("Failed to load pass through endpoint information"); + console.error("Error fetching endpoint info:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchEndpointInfo(); + }, [endpointPath, accessToken]); + + const handleEndpointUpdate = async (values: any) => { + try { + if (!accessToken) return; + + // Parse headers if provided as string + let headers = {}; + if (values.headers) { + try { + headers = typeof values.headers === 'string' + ? JSON.parse(values.headers) + : values.headers; + } catch (e) { + message.error("Invalid JSON format for headers"); + return; + } + } + + const updateData = { + target: values.target, + headers: headers, + include_subpath: values.include_subpath, + cost_per_request: values.cost_per_request, + }; + + await updatePassThroughEndpoint(accessToken, endpointPath, updateData); + message.success("Pass through endpoint updated successfully"); + fetchEndpointInfo(); + setIsEditing(false); + if (onEndpointUpdated) { + onEndpointUpdated(); + } + } catch (error) { + console.error("Error updating endpoint:", error); + message.error("Failed to update pass through endpoint"); + } + }; + + const handleDeleteEndpoint = async () => { + try { + if (!accessToken) return; + + await deletePassThroughEndpointsCall(accessToken, endpointPath); + message.success("Pass through endpoint deleted successfully"); + onClose(); + if (onEndpointUpdated) { + onEndpointUpdated(); + } + } catch (error) { + console.error("Error deleting endpoint:", error); + message.error("Failed to delete pass through endpoint"); + } + }; + + if (loading) { + return
Loading...
; + } + + if (!endpointData) { + return
Pass through endpoint not found
; + } + + return ( +
+
+
+ + Pass Through Endpoint + {endpointData.path} +
+
+ + + + Overview + {isAdmin ? Settings : <>} + + + + {/* Overview Panel */} + + + + Path +
+ {endpointData.path} +
+
+ + + Target +
+ {endpointData.target} +
+
+ + + Configuration +
+
+ + {endpointData.include_subpath ? "Include Subpath" : "Exact Path"} + +
+ {endpointData.cost_per_request !== undefined && ( +
+ Cost per request: ${endpointData.cost_per_request} +
+ )} +
+
+
+ + {endpointData.headers && Object.keys(endpointData.headers).length > 0 && ( + +
+ Headers + + {Object.keys(endpointData.headers).length} headers configured + +
+
+ +
+
+ )} +
+ + {/* Settings Panel (only for admins) */} + {isAdmin && ( + + +
+ Pass Through Endpoint Settings +
+ {!isEditing && ( + <> + setIsEditing(true)} + > + Edit Settings + + + Delete Endpoint + + + )} +
+
+ + {isEditing ? ( +
+ + + + + + + + + + + + + + + + +
+ + + Save Changes + +
+
+ ) : ( +
+
+ Path +
{endpointData.path}
+
+
+ Target URL +
{endpointData.target}
+
+
+ Include Subpath + + {endpointData.include_subpath ? "Yes" : "No"} + +
+ {endpointData.cost_per_request !== undefined && ( +
+ Cost per Request +
${endpointData.cost_per_request}
+
+ )} +
+ Headers + {endpointData.headers && Object.keys(endpointData.headers).length > 0 ? ( +
+ +
+ ) : ( +
No headers configured
+ )} +
+
+ )} +
+
+ )} +
+
+
+ ); +}; + +export default PassThroughInfoView; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/pass_through_settings.tsx b/ui/litellm-dashboard/src/components/pass_through_settings.tsx index 00446b70558..9f80060c6b6 100644 --- a/ui/litellm-dashboard/src/components/pass_through_settings.tsx +++ b/ui/litellm-dashboard/src/components/pass_through_settings.tsx @@ -48,6 +48,7 @@ import { } from "@heroicons/react/outline"; import AddFallbacks from "./add_fallbacks"; import AddPassThroughEndpoint from "./add_pass_through"; +import PassThroughInfoView from "./pass_through_info"; import openai from "openai"; import Paragraph from "antd/es/skeleton/Paragraph"; import { DataTable } from "./view_logs/table"; @@ -116,6 +117,7 @@ const PassThroughSettings: React.FC = ({ const [generalSettings, setGeneralSettings] = useState( [] ); + const [selectedEndpointPath, setSelectedEndpointPath] = useState(null); useEffect(() => { if (!accessToken || !userRole || !userID) { @@ -127,6 +129,16 @@ const PassThroughSettings: React.FC = ({ }); }, [accessToken, userRole, userID]); + const handleEndpointUpdated = () => { + // Refresh the endpoints list when an endpoint is updated + if (accessToken) { + getPassThroughEndpointsCall(accessToken).then((data) => { + let general_settings = data["endpoints"]; + setGeneralSettings(general_settings); + }); + } + }; + const handleResetField = (fieldName: string, idx: number) => { if (!accessToken) { return; @@ -150,7 +162,16 @@ const PassThroughSettings: React.FC = ({ header: "Path", accessorKey: "path", cell: (info: any) => ( - {info.getValue()} +
+ +
), }, { @@ -168,17 +189,23 @@ const PassThroughSettings: React.FC = ({ ), }, { - header: "Action", + header: "Actions", id: "actions", cell: ({ row }) => ( - handleResetField(row.original.path, row.index)} - > - Delete - +
+ setSelectedEndpointPath(row.original.path)} + title="Edit" + /> + handleResetField(row.original.path, row.index)} + title="Delete" + /> +
), }, ]; @@ -187,16 +214,27 @@ const PassThroughSettings: React.FC = ({ return null; } + // If a specific endpoint is selected, show the info view + if (selectedEndpointPath) { + return ( + setSelectedEndpointPath(null)} + accessToken={accessToken} + isAdmin={userRole === "Admin" || userRole === "admin"} + onEndpointUpdated={handleEndpointUpdated} + /> + ); + } + return ( -
-
+
Pass Through Endpoints Configure and manage your pass-through endpoints
-