diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 14414e1c5e..9ed2323a7b 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1449,7 +1449,16 @@ class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase): description="The URL to which requests for this path should be forwarded." ) headers: dict = Field( - description="Key-value pairs of headers to be forwarded with the request. You can set any key value pair here and it will be forwarded to your target endpoint" + default={}, + description="Key-value pairs of headers to be forwarded with the request. You can set any key value pair here and it will be forwarded to your target endpoint", + ) + include_subpath: bool = Field( + default=False, + description="If True, requests to subpaths of the path will be forwarded to the target endpoint. For example, if the path is /bria and include_subpath is True, requests to /bria/v1/text-to-image/base/2.3 will be forwarded to the target endpoint.", + ) + input_cost_per_request: float = Field( + default=0.0, + description="The USD cost per request to the target endpoint. This is used to calculate the cost of the request to the target endpoint.", ) diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index bd2303b6f6..53c9c49a47 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -5,7 +5,7 @@ import traceback import uuid from base64 import b64encode from datetime import datetime -from typing import List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union from urllib.parse import urlencode, urlparse import httpx @@ -413,10 +413,10 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): for field_name, field_value in form_data.items(): if isinstance(field_value, (StarletteUploadFile, UploadFile)): - files[ - field_name - ] = await HttpPassThroughEndpointHelpers._build_request_files_from_upload_file( - upload_file=field_value + files[field_name] = ( + await HttpPassThroughEndpointHelpers._build_request_files_from_upload_file( + upload_file=field_value + ) ) else: form_data_dict[field_name] = field_value @@ -473,12 +473,41 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): "passthrough_logging_payload": passthrough_logging_payload, } - logging_obj.model_call_details[ - "passthrough_logging_payload" - ] = passthrough_logging_payload + logging_obj.model_call_details["passthrough_logging_payload"] = ( + passthrough_logging_payload + ) return kwargs + @staticmethod + def construct_target_url_with_subpath( + base_target: str, subpath: str, include_subpath: Optional[bool] + ) -> str: + """ + Helper function to construct the full target URL with subpath handling. + + Args: + base_target: The base target URL + subpath: The captured subpath from the request + include_subpath: Whether to include the subpath in the target URL + + Returns: + The constructed full target URL + """ + if not include_subpath: + return base_target + + if not subpath: + return base_target + + # Ensure base_target ends with / and subpath doesn't start with / + if not base_target.endswith("/"): + base_target = base_target + "/" + if subpath.startswith("/"): + subpath = subpath[1:] + + return base_target + subpath + async def pass_through_request( # noqa: PLR0915 request: Request, @@ -818,6 +847,7 @@ def create_pass_through_route( _forward_headers: Optional[bool] = False, _merge_query_params: Optional[bool] = False, dependencies: Optional[List] = None, + include_subpath: Optional[bool] = False, ): # check if target is an adapter.py or a url import uuid @@ -836,6 +866,7 @@ def create_pass_through_route( request: Request, fastapi_response: Response, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + subpath: str = "", # captures sub-paths when include_subpath=True ): return await chat_completion_pass_through_endpoint( fastapi_response=fastapi_response, @@ -856,10 +887,18 @@ def create_pass_through_route( stream: Optional[ bool ] = None, # if pass-through endpoint is a streaming request + subpath: str = "", # captures sub-paths when include_subpath=True ): + # Construct the full target URL with subpath if needed + full_target = ( + HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target=target, subpath=subpath, include_subpath=include_subpath + ) + ) + return await pass_through_request( # type: ignore request=request, - target=target, + target=full_target, custom_headers=custom_headers or {}, user_api_key_dict=user_api_key_dict, forward_headers=_forward_headers, @@ -879,14 +918,95 @@ def _is_streaming_response(response: httpx.Response) -> bool: return False -async def initialize_pass_through_endpoints(pass_through_endpoints: list): +class InitPassThroughEndpointHelpers: + @staticmethod + def add_exact_path_route( + app, + path: str, + target: str, + custom_headers: Optional[dict], + forward_headers: Optional[bool], + merge_query_params: Optional[bool], + dependencies: Optional[List], + ): + """Add exact path route for pass-through endpoint""" + verbose_proxy_logger.debug( + "adding exact pass through endpoint: %s, dependencies: %s", + path, + dependencies, + ) + + app.add_api_route( # type: ignore + path=path, + endpoint=create_pass_through_route( # type: ignore + path, + target, + custom_headers, + forward_headers, + merge_query_params, + dependencies, + ), + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], + dependencies=dependencies, + ) + + @staticmethod + def add_subpath_route( + app, + path: str, + target: str, + custom_headers: Optional[dict], + forward_headers: Optional[bool], + merge_query_params: Optional[bool], + dependencies: Optional[List], + ): + """Add wildcard route for sub-paths""" + wildcard_path = f"{path}/{{subpath:path}}" + verbose_proxy_logger.debug( + "adding wildcard pass through endpoint: %s, dependencies: %s", + wildcard_path, + dependencies, + ) + + app.add_api_route( # type: ignore + path=wildcard_path, + endpoint=create_pass_through_route( # type: ignore + path, + target, + custom_headers, + forward_headers, + merge_query_params, + dependencies, + include_subpath=True, + ), + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], + dependencies=dependencies, + ) + + +async def initialize_pass_through_endpoints( + pass_through_endpoints: Union[List[Dict], List[PassThroughGenericEndpoint]], +): + """ + Initialize a list of pass-through endpoints by adding them to the FastAPI app routes + + Args: + pass_through_endpoints: List of pass-through endpoints to initialize + + Returns: + None + """ verbose_proxy_logger.debug("initializing pass through endpoints") from litellm.proxy._types import CommonProxyErrors, LiteLLMRoutes from litellm.proxy.proxy_server import app, premium_user for endpoint in pass_through_endpoints: + if isinstance(endpoint, PassThroughGenericEndpoint): + endpoint = endpoint.model_dump() _target = endpoint.get("target", None) - _path = endpoint.get("path", None) + _path: Optional[str] = endpoint.get("path", None) + if _path is None: + raise ValueError("Path is required for pass-through endpoint") _custom_headers = endpoint.get("headers", None) _custom_headers = await set_env_variables_in_header( custom_headers=_custom_headers @@ -908,55 +1028,51 @@ async def initialize_pass_through_endpoints(pass_through_endpoints: list): if _target is None: continue - verbose_proxy_logger.debug( - "adding pass through endpoint: %s, dependencies: %s", _path, _dependencies - ) - app.add_api_route( # type: ignore + # Add exact path route + verbose_proxy_logger.debug("Initializing pass through endpoint: %s", _path) + InitPassThroughEndpointHelpers.add_exact_path_route( + app=app, path=_path, - endpoint=create_pass_through_route( # type: ignore - _path, - _target, - _custom_headers, - _forward_headers, - _merge_query_params, - _dependencies, - ), - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], + target=_target, + custom_headers=_custom_headers, + forward_headers=_forward_headers, + merge_query_params=_merge_query_params, dependencies=_dependencies, ) + # Add wildcard route for sub-paths + if endpoint.get("include_subpath", False) is True: + InitPassThroughEndpointHelpers.add_subpath_route( + app=app, + path=_path, + target=_target, + custom_headers=_custom_headers, + forward_headers=_forward_headers, + merge_query_params=_merge_query_params, + dependencies=_dependencies, + ) + verbose_proxy_logger.debug("Added new pass through endpoint: %s", _path) -@router.get( - "/config/pass_through_endpoint", - dependencies=[Depends(user_api_key_auth)], - response_model=PassThroughEndpointResponse, -) -async def get_pass_through_endpoints( +async def _get_pass_through_endpoints_from_db( endpoint_id: Optional[str] = None, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), -): - """ - GET configured pass through endpoint. - - If no endpoint_id given, return all configured endpoints. - """ +) -> List[PassThroughGenericEndpoint]: from litellm.proxy.proxy_server import get_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: - return PassThroughEndpointResponse(endpoints=[]) + return [] pass_through_endpoint_data: Optional[List] = response.field_value if pass_through_endpoint_data is None: - return PassThroughEndpointResponse(endpoints=[]) + return [] - returned_endpoints = [] + returned_endpoints: List[PassThroughGenericEndpoint] = [] if endpoint_id is None: for endpoint in pass_through_endpoint_data: if isinstance(endpoint, dict): @@ -973,8 +1089,27 @@ async def get_pass_through_endpoints( if _endpoint is not None and _endpoint.path == endpoint_id: returned_endpoints.append(_endpoint) + return returned_endpoints - return PassThroughEndpointResponse(endpoints=returned_endpoints) + +@router.get( + "/config/pass_through_endpoint", + dependencies=[Depends(user_api_key_auth)], + response_model=PassThroughEndpointResponse, +) +async def get_pass_through_endpoints( + endpoint_id: Optional[str] = None, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + GET configured pass through endpoint. + + If no endpoint_id given, return all configured endpoints. + """ ## Get existing pass-through endpoint field value + pass_through_endpoints = await _get_pass_through_endpoints_from_db( + endpoint_id=endpoint_id, user_api_key_dict=user_api_key_dict + ) + return PassThroughEndpointResponse(endpoints=pass_through_endpoints) @router.post( @@ -1107,3 +1242,13 @@ async def delete_pass_through_endpoints( }, ) return PassThroughEndpointResponse(endpoints=[response_obj]) + + +async def initialize_pass_through_endpoints_in_db(): + """ + Gets all pass-through endpoints from db and initializes them in the proxy server. + """ + pass_through_endpoints = await _get_pass_through_endpoints_from_db() + await initialize_pass_through_endpoints( + pass_through_endpoints=pass_through_endpoints + ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0116de9392..f8753d10f9 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2780,6 +2780,7 @@ class ProxyConfig: await self._init_guardrails_in_db(prisma_client=prisma_client) await self._init_vector_stores_in_db(prisma_client=prisma_client) await self._init_mcp_servers_in_db() + await self._init_pass_through_endpoints_in_db() async def _init_guardrails_in_db(self, prisma_client: PrismaClient): from litellm.proxy.guardrails.guardrail_registry import ( @@ -2857,6 +2858,13 @@ class ProxyConfig: ) ) + async def _init_pass_through_endpoints_in_db(self): + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + initialize_pass_through_endpoints_in_db, + ) + + await initialize_pass_through_endpoints_in_db() + def decrypt_credentials(self, credential: Union[dict, BaseModel]) -> CredentialItem: if isinstance(credential, dict): credential_object = CredentialItem(**credential) diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index d5ffcd30f4..d83ae59ac5 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -47,7 +47,7 @@ async def test_build_request_files_from_upload_file(): file_content = b"test content" file = BytesIO(file_content) # Create SpooledTemporaryFile with content type headers - headers = {"content-type": "text/plain"} + headers = Headers({"content-type": "text/plain"}) upload_file = UploadFile(file=file, filename="test.txt", headers=headers) upload_file.read = AsyncMock(return_value=file_content) @@ -82,7 +82,7 @@ async def test_make_multipart_http_request(): file_content = b"test file content" file = BytesIO(file_content) # Create SpooledTemporaryFile with content type headers - headers = {"content-type": "text/plain"} + headers = Headers({"content-type": "text/plain"}) upload_file = UploadFile(file=file, filename="test.txt", headers=headers) upload_file.read = AsyncMock(return_value=file_content) @@ -268,3 +268,227 @@ async def test_langfuse_passthrough_no_logging(): mock_logging_obj.model_call_details["passthrough_logging_payload"] == passthrough_logging_payload ) + + +def test_construct_target_url_with_subpath(): + """ + Test that construct_target_url_with_subpath correctly constructs target URLs + """ + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + HttpPassThroughEndpointHelpers, + ) + + # Test with include_subpath=False + result = HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target="http://example.com", subpath="api/v1", include_subpath=False + ) + assert result == "http://example.com" + + # Test with include_subpath=True and no subpath + result = HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target="http://example.com", subpath="", include_subpath=True + ) + assert result == "http://example.com" + + # Test with include_subpath=True and subpath + result = HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target="http://example.com", subpath="api/v1", include_subpath=True + ) + assert result == "http://example.com/api/v1" + + # Test with base_target already ending with / + result = HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target="http://example.com/", subpath="api/v1", include_subpath=True + ) + assert result == "http://example.com/api/v1" + + # Test with subpath starting with / + result = HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target="http://example.com", subpath="/api/v1", include_subpath=True + ) + assert result == "http://example.com/api/v1" + + # Test with both conditions + result = HttpPassThroughEndpointHelpers.construct_target_url_with_subpath( + base_target="http://example.com/", subpath="/api/v1", include_subpath=True + ) + assert result == "http://example.com/api/v1" + + +def test_add_exact_path_route(): + """ + Test that add_exact_path_route correctly adds exact path routes + """ + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + InitPassThroughEndpointHelpers, + ) + + # Mock FastAPI app + mock_app = MagicMock() + + # Test data + path = "/test/path" + target = "http://example.com" + custom_headers = {"x-custom": "header"} + forward_headers = True + merge_query_params = False + dependencies = [] + + # Call the function + InitPassThroughEndpointHelpers.add_exact_path_route( + app=mock_app, + path=path, + target=target, + custom_headers=custom_headers, + forward_headers=forward_headers, + merge_query_params=merge_query_params, + dependencies=dependencies, + ) + + # Verify add_api_route was called with correct parameters + mock_app.add_api_route.assert_called_once() + call_args = mock_app.add_api_route.call_args[1] + + assert call_args["path"] == path + assert call_args["methods"] == ["GET", "POST", "PUT", "DELETE", "PATCH"] + assert call_args["dependencies"] == dependencies + assert callable(call_args["endpoint"]) + + +def test_add_subpath_route(): + """ + Test that add_subpath_route correctly adds wildcard routes for sub-paths + """ + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + InitPassThroughEndpointHelpers, + ) + + # Mock FastAPI app + mock_app = MagicMock() + + # Test data + path = "/test/path" + target = "http://example.com" + custom_headers = {"x-custom": "header"} + forward_headers = True + merge_query_params = False + dependencies = [] + + # Call the function + InitPassThroughEndpointHelpers.add_subpath_route( + app=mock_app, + path=path, + target=target, + custom_headers=custom_headers, + forward_headers=forward_headers, + merge_query_params=merge_query_params, + dependencies=dependencies, + ) + + # Verify add_api_route was called with correct parameters + mock_app.add_api_route.assert_called_once() + call_args = mock_app.add_api_route.call_args[1] + + # Should have wildcard path + expected_wildcard_path = f"{path}/{{subpath:path}}" + assert call_args["path"] == expected_wildcard_path + assert call_args["methods"] == ["GET", "POST", "PUT", "DELETE", "PATCH"] + assert call_args["dependencies"] == dependencies + assert callable(call_args["endpoint"]) + + +@pytest.mark.asyncio +async def test_initialize_pass_through_endpoints_with_include_subpath(): + """ + Test that initialize_pass_through_endpoints adds wildcard routes when include_subpath is True + """ + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + initialize_pass_through_endpoints, + ) + + # Mock the helper functions directly + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.InitPassThroughEndpointHelpers.add_exact_path_route" + ) as mock_add_exact_route: + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.InitPassThroughEndpointHelpers.add_subpath_route" + ) as mock_add_subpath_route: + with patch( + "litellm.proxy.proxy_server.premium_user", + True, + ): + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.set_env_variables_in_header" + ) as mock_set_env: + mock_set_env.return_value = {} + + # Test endpoint with include_subpath=True + endpoints = [ + { + "path": "/test/endpoint", + "target": "http://example.com", + "include_subpath": True, + } + ] + + await initialize_pass_through_endpoints(endpoints) + + # Should be called once for exact path and once for subpath + mock_add_exact_route.assert_called_once() + mock_add_subpath_route.assert_called_once() + + # Verify exact path route call + exact_call_args = mock_add_exact_route.call_args[1] + assert exact_call_args["path"] == "/test/endpoint" + assert exact_call_args["target"] == "http://example.com" + + # Verify subpath route call + subpath_call_args = mock_add_subpath_route.call_args[1] + assert subpath_call_args["path"] == "/test/endpoint" + assert subpath_call_args["target"] == "http://example.com" + + +@pytest.mark.asyncio +async def test_initialize_pass_through_endpoints_without_include_subpath(): + """ + Test that initialize_pass_through_endpoints only adds exact route when include_subpath is False + """ + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + initialize_pass_through_endpoints, + ) + + # Mock the helper functions directly + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.InitPassThroughEndpointHelpers.add_exact_path_route" + ) as mock_add_exact_route: + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.InitPassThroughEndpointHelpers.add_subpath_route" + ) as mock_add_subpath_route: + with patch( + "litellm.proxy.proxy_server.premium_user", + True, + ): + with patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints.set_env_variables_in_header" + ) as mock_set_env: + mock_set_env.return_value = {} + + # Test endpoint with include_subpath=False (default) + endpoints = [ + { + "path": "/test/endpoint", + "target": "http://example.com", + "include_subpath": False, + } + ] + + await initialize_pass_through_endpoints(endpoints) + + # Should be called only once for exact path + mock_add_exact_route.assert_called_once() + mock_add_subpath_route.assert_not_called() + + # Verify exact path route call + exact_call_args = mock_add_exact_route.call_args[1] + assert exact_call_args["path"] == "/test/endpoint" + assert exact_call_args["target"] == "http://example.com" diff --git a/ui/litellm-dashboard/src/components/add_pass_through.tsx b/ui/litellm-dashboard/src/components/add_pass_through.tsx index addeb2248b..c5060261f5 100644 --- a/ui/litellm-dashboard/src/components/add_pass_through.tsx +++ b/ui/litellm-dashboard/src/components/add_pass_through.tsx @@ -5,7 +5,7 @@ import React, { useState, useEffect, useRef } from "react"; -import { Button, TextInput, Grid, Col } from "@tremor/react"; +import { Button, TextInput, Grid, Col, Switch } from "@tremor/react"; import { Select, SelectItem, MultiSelect, MultiSelectItem, Card, Metric, Text, Title, Subtitle, Accordion, AccordionHeader, AccordionBody, } from "@tremor/react"; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { createPassThroughEndpoint } from "./networking"; @@ -17,7 +17,9 @@ import { InputNumber, Select as Select2, message, + Tooltip, } from "antd"; +import { InfoCircleOutlined, ApiOutlined } from "@ant-design/icons"; import { keyCreateCall, slackBudgetAlertsHealthCheck, modelAvailableCall } from "./networking"; import { list } from "postcss"; import KeyValueInput from "./key_value_input"; @@ -36,110 +38,182 @@ const AddPassThroughEndpoint: React.FC = ({ }) => { const [form] = Form.useForm(); const [isModalVisible, setIsModalVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [selectedModel, setSelectedModel] = useState(""); - const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - }; - const handleCancel = () => { - setIsModalVisible(false); form.resetFields(); + setIsModalVisible(false); }; - const addPassThrough = (formValues: Record) => { - // Print the received value - console.log(formValues); + const addPassThrough = async (formValues: Record) => { + setIsLoading(true); + try { + console.log(`formValues: ${JSON.stringify(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 = { + const newPassThroughItem: passThroughItem = { "headers": formValues["headers"], "path": formValues["path"], - "target": formValues["target"] - } - const updatedPassThroughSettings = [...passThroughItems, newPassThroughItem] - - - try { - createPassThroughEndpoint(accessToken, formValues); - setPassThroughItems(updatedPassThroughSettings) + "target": formValues["target"], + "include_subpath": formValues["include_subpath"] || false, + "input_cost_per_request": formValues["input_cost_per_request"] || 0 + } + + await createPassThroughEndpoint(accessToken, formValues); + + const updatedPassThroughSettings = [...passThroughItems, newPassThroughItem] + setPassThroughItems(updatedPassThroughSettings) + + message.success("Pass-through endpoint created successfully"); + form.resetFields(); + setIsModalVisible(false); } catch (error) { - message.error("Failed to update router settings: " + error, 20); + message.error("Error creating pass-through endpoint: " + error, 20); + } finally { + setIsLoading(false); } - - message.success("Pass through endpoint successfully added"); - - setIsModalVisible(false) - form.resetFields(); }; return (
- + +

Add Pass-Through Endpoint

+
+ } + open={isModalVisible} + width={1000} onCancel={handleCancel} + footer={null} + className="top-8" + styles={{ + body: { padding: '24px' }, + header: { padding: '24px 24px 0 24px', border: 'none' }, + }} > -
- <> - + +
+ + Path + + + + + } name="path" - rules={[{ required: true, message: 'The route to be added to the LiteLLM Proxy Server.' }]} - help="required" + rules={[{ required: true, message: 'Please enter the endpoint path' }]} > - + - + Target URL + + + + + } name="target" - rules={[{ required: true, message: 'The URL to which requests for this path should be forwarded.' }]} - help="required" + rules={[ + { required: true, message: 'Please enter the target URL' }, + { type: 'url', message: 'Please enter a valid URL' } + ]} > - + - + Headers + + + + + } name="headers" - rules={[{ required: true, message: 'Key-value pairs of headers to be forwarded with the request. You can set any key value pair here and it will be forwarded to your target endpoint' }]} - help="required" + rules={[{ required: true, message: 'Please configure the headers' }]} > - - -
- Add Pass-Through Endpoint -
- + + + Include Subpath + + + + + } + name="include_subpath" + valuePropName="checked" + > + + + + + Cost Per Request (USD) + + + + + } + name="input_cost_per_request" + > + + +
+ +
+ + +
+ + diff --git a/ui/litellm-dashboard/src/components/pass_through_settings.tsx b/ui/litellm-dashboard/src/components/pass_through_settings.tsx index 3772d335df..4de63bd06c 100644 --- a/ui/litellm-dashboard/src/components/pass_through_settings.tsx +++ b/ui/litellm-dashboard/src/components/pass_through_settings.tsx @@ -1,15 +1,6 @@ import React, { useState, useEffect } from "react"; import { - Card, - Title, - Subtitle, - Table, - TableHead, - TableRow, Badge, - TableHeaderCell, - TableCell, - TableBody, Metric, Text, Grid, @@ -22,14 +13,8 @@ import { AccordionBody, AccordionHeader, AccordionList, -} from "@tremor/react"; -import { - TabPanel, - TabPanels, - TabGroup, - TabList, - Tab, Icon, + Title, } from "@tremor/react"; import { getCallbacksCall, @@ -65,6 +50,10 @@ import AddFallbacks from "./add_fallbacks"; import AddPassThroughEndpoint from "./add_pass_through"; import openai from "openai"; import Paragraph from "antd/es/skeleton/Paragraph"; +import { DataTable } from "./view_logs/table"; +import { ColumnDef } from "@tanstack/react-table"; +import { Eye, EyeOff } from "lucide-react"; + interface GeneralSettingsPageProps { accessToken: string | null; userRole: string | null; @@ -72,7 +61,6 @@ interface GeneralSettingsPageProps { modelData: any; } - interface routingStrategyArgs { ttl?: number; lowest_latency_buffer?: number; @@ -90,10 +78,34 @@ export interface passThroughItem { path: string target: string headers: object + include_subpath?: boolean + input_cost_per_request?: number } - - +// Password field component for headers +const PasswordField: React.FC<{ value: object }> = ({ value }) => { + const [showPassword, setShowPassword] = useState(false); + const headerString = JSON.stringify(value); + + return ( +
+ + {showPassword ? headerString : "••••••••"} + + +
+ ); +}; const PassThroughSettings: React.FC = ({ accessToken, @@ -104,6 +116,7 @@ const PassThroughSettings: React.FC = ({ const [generalSettings, setGeneralSettings] = useState( [] ); + useEffect(() => { if (!accessToken || !userRole || !userID) { return; @@ -114,7 +127,6 @@ const PassThroughSettings: React.FC = ({ }); }, [accessToken, userRole, userID]); - const handleResetField = (fieldName: string, idx: number) => { if (!accessToken) { return; @@ -122,72 +134,84 @@ const PassThroughSettings: React.FC = ({ 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 } }; + // Define columns for the DataTable + const columns: ColumnDef[] = [ + { + header: "Path", + accessorKey: "path", + cell: (info: any) => ( + {info.getValue()} + ), + }, + { + header: "Target", + accessorKey: "target", + cell: (info: any) => ( + {info.getValue()} + ), + }, + { + header: "Headers", + accessorKey: "headers", + cell: (info: any) => ( + + ), + }, + { + header: "Action", + id: "actions", + cell: ({ row }) => ( + handleResetField(row.original.path, row.index)} + > + Delete + + ), + }, + ]; 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 - - - - ))} - -
- -
-
+
+
+
+ Pass Through Endpoints + + Configure and manage your pass-through endpoints + +
+
+ + + +
} + getRowCanExpand={() => false} + isLoading={false} + noDataMessage="No pass-through endpoints configured" + />
); };