diff --git a/litellm/proxy/config_management_endpoints/pass_through_endpoints.py b/litellm/proxy/config_management_endpoints/pass_through_endpoints.py new file mode 100644 index 000000000..237f1b74b --- /dev/null +++ b/litellm/proxy/config_management_endpoints/pass_through_endpoints.py @@ -0,0 +1,47 @@ +""" +What is this? + +CRUD endpoints for managing pass-through endpoints +""" + +import asyncio +import traceback +from datetime import datetime, timedelta, timezone +from typing import List, Optional + +import fastapi +import httpx +from fastapi import ( + APIRouter, + Depends, + File, + Form, + Header, + HTTPException, + Request, + Response, + UploadFile, + status, +) + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.batches.main import FileObject +from litellm.proxy._types import * +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + +router = APIRouter() + + +@router.get( + "/config/pass_through_endpoints/settings", + dependencies=[Depends(user_api_key_auth)], + tags=["pass-through-endpoints"], + summary="Create pass-through endpoints for provider specific endpoints - https://docs.litellm.ai/docs/proxy/pass_through", +) +async def create_fine_tuning_job( + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + pass diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a331e150e..4d141955b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -9473,11 +9473,10 @@ async def get_config_list( typed_dict_type = allowed_args[field_name]["type"] if typed_dict_type == "PydanticModel": - pydantic_class_list: Optional[Any] = _resolve_pydantic_type( - field_info.annotation - ) - if pydantic_class_list is None: - continue + if field_name == "pass_through_endpoints": + pydantic_class_list = [PassThroughGenericEndpoint] + else: + pydantic_class_list = [] for pydantic_class in pydantic_class_list: # Get type hints from the TypedDict to create FieldDetail objects diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 9b7a09cbf..02ef8ebe0 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -9,6 +9,7 @@ import Teams from "@/components/teams"; import AdminPanel from "@/components/admins"; import Settings from "@/components/settings"; import GeneralSettings from "@/components/general_settings"; +import PassThroughSettings from "@/components/pass_through_settings"; import BudgetPanel from "@/components/budgets/budget_panel"; import ModelHub from "@/components/model_hub"; import APIRef from "@/components/api_ref"; @@ -263,6 +264,13 @@ const CreateKeyPage = () => { accessToken={accessToken} premiumUser={premiumUser} /> + ) : page == "pass-through-settings" ? ( + ) : ( >; +} + +const AddPassThroughEndpoint: React.FC = ({ + accessToken, setPassThroughItems, passThroughItems +}) => { + const [form] = Form.useForm(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedModel, setSelectedModel] = useState(""); + const handleOk = () => { + setIsModalVisible(false); + form.resetFields(); + }; + + const handleCancel = () => { + setIsModalVisible(false); + form.resetFields(); + }; + + const addPassThrough = (formValues: Record) => { + // Print the received value + console.log(formValues); + + // // Extract model_name and models from formValues + // const { model_name, models } = formValues; + + // // Create new fallback + // const newFallback = { [model_name]: models }; + + // // Get current fallbacks, or an empty array if it's null + // const currentFallbacks = routerSettings.fallbacks || []; + + // // Add new fallback to the current fallbacks + // const updatedFallbacks = [...currentFallbacks, newFallback]; + + // // Create a new routerSettings object with updated fallbacks + // const updatedRouterSettings = { ...routerSettings, fallbacks: updatedFallbacks }; + + const newPassThroughItem: passThroughItem = { + "headers": formValues["headers"], + "path": formValues["path"], + "target": formValues["target"] + } + const updatedPassThroughSettings = [...passThroughItems, newPassThroughItem] + + + try { + createPassThroughEndpoint(accessToken, formValues); + setPassThroughItems(updatedPassThroughSettings) + } catch (error) { + message.error("Failed to update router settings: " + error, 20); + } + + message.success("Pass through endpoint successfully added"); + + setIsModalVisible(false) + form.resetFields(); + }; + + + return ( +
+ + +
+ <> + + + + + + + + + + + + +
+ Add Pass-Through Endpoint +
+
+
+ +
+ ); +}; + +export default AddPassThroughEndpoint; diff --git a/ui/litellm-dashboard/src/components/general_settings.tsx b/ui/litellm-dashboard/src/components/general_settings.tsx index f80ca203b..87750b607 100644 --- a/ui/litellm-dashboard/src/components/general_settings.tsx +++ b/ui/litellm-dashboard/src/components/general_settings.tsx @@ -597,7 +597,7 @@ const GeneralSettings: React.FC = ({ - {generalSettings.map((value, index) => ( + {generalSettings.filter((value) => value.field_type !== "TypedDictionary").map((value, index) => ( {value.field_name} diff --git a/ui/litellm-dashboard/src/components/key_value_input.tsx b/ui/litellm-dashboard/src/components/key_value_input.tsx new file mode 100644 index 000000000..90f58eede --- /dev/null +++ b/ui/litellm-dashboard/src/components/key_value_input.tsx @@ -0,0 +1,56 @@ +import React, { useState } from 'react'; +import { Form, Input, Button, Space } from 'antd'; +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import { TextInput, Grid, Col } from "@tremor/react"; +import { TrashIcon } from "@heroicons/react/outline"; + +interface KeyValueInputProps { + value?: Record; + onChange?: (value: Record) => void; +} + +const KeyValueInput: React.FC = ({ value = {}, onChange }) => { + const [pairs, setPairs] = useState<[string, string][]>(Object.entries(value)); + + const handleAdd = () => { + setPairs([...pairs, ['', '']]); + }; + + const handleRemove = (index: number) => { + const newPairs = pairs.filter((_, i) => i !== index); + setPairs(newPairs); + onChange?.(Object.fromEntries(newPairs)); + }; + + const handleChange = (index: number, key: string, val: string) => { + const newPairs = [...pairs]; + newPairs[index] = [key, val]; + setPairs(newPairs); + onChange?.(Object.fromEntries(newPairs)); + }; + + return ( +
+ {pairs.map(([key, val], index) => ( + + handleChange(index, e.target.value, val)} + /> + handleChange(index, key, e.target.value)} + /> + handleRemove(index)} /> + + ))} + +
+ ); +}; + +export default KeyValueInput; diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index b33dda982..c8f5745ed 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -102,15 +102,21 @@ const Sidebar: React.FC = ({ Router Settings ) : null} + {userRole == "Admin" ? ( - setPage("admin-panel")}> + setPage("pass-through-settings")}> + Pass-Through + + ) : null} + {userRole == "Admin" ? ( + setPage("admin-panel")}> Admin Settings ) : null} - setPage("api_ref")}> + setPage("api_ref")}> API Reference - setPage("model-hub")}> + setPage("model-hub")}> Model Hub diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 263616c91..f55076478 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2388,6 +2388,38 @@ export const getGeneralSettingsCall = async (accessToken: String) => { } }; + +export const getPassThroughEndpointsCall = async (accessToken: String) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/config/pass_through_endpoint` + : `/config/pass_through_endpoint`; + + //message.info("Requesting model data"); + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + //message.info("Received model data"); + return data; + // Handle success - you might want to update some state or UI based on the created key + } catch (error) { + console.error("Failed to get callbacks:", error); + throw error; + } +}; + export const getConfigFieldSetting = async ( accessToken: String, fieldName: string @@ -2420,6 +2452,85 @@ export const getConfigFieldSetting = async ( } }; +export const updatePassThroughFieldSetting = async ( + accessToken: String, + fieldName: string, + fieldValue: any +) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/config/pass_through_endpoint` + : `/config/pass_through_endpoint`; + + let formData = { + field_name: fieldName, + field_value: fieldValue, + }; + //message.info("Requesting model data"); + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + //message.info("Received model data"); + message.success("Successfully updated value!"); + return data; + // Handle success - you might want to update some state or UI based on the created key + } catch (error) { + console.error("Failed to set callbacks:", error); + throw error; + } +}; + +export const createPassThroughEndpoint = async ( + accessToken: String, + formValues: Record +) => { + /** + * Set callbacks on proxy + */ + try { + let url = proxyBaseUrl ? `${proxyBaseUrl}/config/pass_through_endpoint` : `/config/pass_through_endpoint`; + + //message.info("Requesting model data"); + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...formValues, // Include formValues in the request body + }), + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + //message.info("Received model data"); + return data; + // Handle success - you might want to update some state or UI based on the created key + } catch (error) { + console.error("Failed to set callbacks:", error); + throw error; + } +}; + export const updateConfigFieldSetting = async ( accessToken: String, fieldName: string, @@ -2500,6 +2611,38 @@ export const deleteConfigFieldSetting = async ( throw error; } }; + +export const deletePassThroughEndpointsCall = async (accessToken: String, endpointId: string) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/config/pass_through_endpoint?endpoint_id=${endpointId}` + : `/config/pass_through_endpoint${endpointId}`; + + //message.info("Requesting model data"); + const response = await fetch(url, { + method: "DELETE", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + //message.info("Received model data"); + return data; + // Handle success - you might want to update some state or UI based on the created key + } catch (error) { + console.error("Failed to get callbacks:", error); + throw error; + } +}; + export const setCallbacksCall = async ( accessToken: String, formValues: Record diff --git a/ui/litellm-dashboard/src/components/pass_through_settings.tsx b/ui/litellm-dashboard/src/components/pass_through_settings.tsx new file mode 100644 index 000000000..c979076a2 --- /dev/null +++ b/ui/litellm-dashboard/src/components/pass_through_settings.tsx @@ -0,0 +1,196 @@ +import React, { useState, useEffect } from "react"; +import { + Card, + Title, + Subtitle, + Table, + TableHead, + TableRow, + Badge, + TableHeaderCell, + TableCell, + TableBody, + Metric, + Text, + Grid, + Button, + TextInput, + Select as Select2, + SelectItem, + Col, + Accordion, + AccordionBody, + AccordionHeader, + AccordionList, +} from "@tremor/react"; +import { + TabPanel, + TabPanels, + TabGroup, + TabList, + Tab, + Icon, +} from "@tremor/react"; +import { + getCallbacksCall, + setCallbacksCall, + getGeneralSettingsCall, + deletePassThroughEndpointsCall, + getPassThroughEndpointsCall, + serviceHealthCheck, + updateConfigFieldSetting, + deleteConfigFieldSetting, +} from "./networking"; +import { + Modal, + Form, + Input, + Select, + Button as Button2, + message, + InputNumber, +} from "antd"; +import { + InformationCircleIcon, + PencilAltIcon, + PencilIcon, + StatusOnlineIcon, + TrashIcon, + RefreshIcon, + CheckCircleIcon, + XCircleIcon, + QuestionMarkCircleIcon, +} from "@heroicons/react/outline"; +import StaticGenerationSearchParamsBailoutProvider from "next/dist/client/components/static-generation-searchparams-bailout-provider"; +import AddFallbacks from "./add_fallbacks"; +import AddPassThroughEndpoint from "./add_pass_through"; +import openai from "openai"; +import Paragraph from "antd/es/skeleton/Paragraph"; +interface GeneralSettingsPageProps { + accessToken: string | null; + userRole: string | null; + userID: string | null; + modelData: any; +} + + +interface routingStrategyArgs { + ttl?: number; + lowest_latency_buffer?: number; +} + +interface nestedFieldItem { + field_name: string; + field_type: string; + field_value: any; + field_description: string; + stored_in_db: boolean | null; +} + +export interface passThroughItem { + path: string + target: string + headers: object +} + + + + +const PassThroughSettings: React.FC = ({ + accessToken, + userRole, + userID, + modelData, +}) => { + const [generalSettings, setGeneralSettings] = useState( + [] + ); + useEffect(() => { + if (!accessToken || !userRole || !userID) { + return; + } + getPassThroughEndpointsCall(accessToken).then((data) => { + let general_settings = data["endpoints"]; + setGeneralSettings(general_settings); + }); + }, [accessToken, userRole, userID]); + + + const handleResetField = (fieldName: string, idx: number) => { + if (!accessToken) { + return; + } + + try { + deletePassThroughEndpointsCall(accessToken, fieldName); + // update value in state + + const updatedSettings = generalSettings.filter((setting) => setting.path !== fieldName); + setGeneralSettings(updatedSettings); + + message.success("Endpoint deleted successfully."); + + } catch (error) { + // do something + } + }; + + + if (!accessToken) { + return null; + } + + + + return ( +
+ + + + + + Path + Target + Headers + Action + + + + {generalSettings.map((value, index) => ( + + + {value.path} + + + { + value.target + } + + + { + JSON.stringify(value.headers) + } + + + + handleResetField(value.path, index) + } + > + Reset + + + + ))} + +
+ +
+
+
+ ); +}; + +export default PassThroughSettings;