[Feat] v2 Pass through endpoints - Add support for subroutes for pass through endpoints + Cleaned up UI (#11827)

* fix: add construct_target_url_with_subpath

* add InitPassThroughEndpointHelpers

* added debugging for pass through routes

* add PassThroughGenericEndpoint to include subpath and input_cost_per_request

* polish page

* Add Pass-Through Endpoint

* polish pass through ui

* fixes for initialize_pass_through_endpoints

* PassThroughGenericEndpoint

* test_add_subpath_route

* test_initialize_pass_through_endpoints_with_include_subpath

* fix code QA check
This commit is contained in:
Ishaan Jaff
2025-06-17 20:52:28 -07:00
committed by GitHub
parent 8bcf163b83
commit 0eb8a3de10
6 changed files with 673 additions and 189 deletions
+10 -1
View File
@@ -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.",
)
@@ -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
)
+8
View File
@@ -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)
@@ -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"
@@ -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<AddFallbacksProps> = ({
}) => {
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<string, any>) => {
// Print the received value
console.log(formValues);
const addPassThrough = async (formValues: Record<string, any>) => {
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 (
<div>
<Button className="mx-auto" onClick={() => setIsModalVisible(true)}>
<Button
className="mx-auto mb-4"
onClick={() => setIsModalVisible(true)}
>
+ Add Pass-Through Endpoint
</Button>
<Modal
title="Add Pass-Through Endpoint"
visible={isModalVisible}
width={800}
footer={null}
onOk={handleOk}
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
<ApiOutlined className="text-xl text-blue-500" />
<h2 className="text-xl font-semibold text-gray-900">Add Pass-Through Endpoint</h2>
</div>
}
open={isModalVisible}
width={1000}
onCancel={handleCancel}
footer={null}
className="top-8"
styles={{
body: { padding: '24px' },
header: { padding: '24px 24px 0 24px', border: 'none' },
}}
>
<Form
form={form}
onFinish={addPassThrough}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<>
<Form.Item
label="Path"
<div className="mt-6">
<Form
form={form}
onFinish={addPassThrough}
layout="vertical"
className="space-y-6"
>
<div className="grid grid-cols-1 gap-6">
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Path
<Tooltip title="The route to be added to the LiteLLM Proxy Server (e.g., /my-endpoint)">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
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' }]}
>
<TextInput/>
<TextInput
placeholder="e.g., /my-endpoint"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label="Target"
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Target URL
<Tooltip title="The URL to which requests for this path should be forwarded">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
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' }
]}
>
<TextInput/>
<TextInput
placeholder="https://your-service.com/api"
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
/>
</Form.Item>
<Form.Item
label="Headers"
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Headers
<Tooltip title="Key-value pairs of headers to be forwarded with the request">
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
</Tooltip>
</span>
}
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' }]}
>
<KeyValueInput/>
</Form.Item>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Add Pass-Through Endpoint</Button2>
</div>
</Form>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Include Subpath
<Tooltip title="If enabled, requests to subpaths will also be forwarded to the target endpoint">
<InfoCircleOutlined className="ml-2 text-gray-400 hover:text-gray-600" />
</Tooltip>
</span>
}
name="include_subpath"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={
<span className="text-sm font-medium text-gray-700 flex items-center">
Cost Per Request (USD)
<Tooltip title="The cost in USD per request to the target endpoint">
<InfoCircleOutlined className="ml-2 text-gray-400 hover:text-gray-600" />
</Tooltip>
</span>
}
name="input_cost_per_request"
>
<InputNumber
min={0}
step={0.001}
precision={6}
placeholder="0.000000"
size="large"
className="rounded-lg w-full"
/>
</Form.Item>
</div>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-100">
<Button
variant="secondary"
onClick={handleCancel}
>
Cancel
</Button>
<Button
variant="primary"
loading={isLoading}
>
{isLoading ? 'Creating...' : 'Add Pass-Through Endpoint'}
</Button>
</div>
</Form>
</div>
</Modal>
</div>
@@ -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 (
<div className="flex items-center space-x-2">
<span className="font-mono text-xs">
{showPassword ? headerString : "••••••••"}
</span>
<button
onClick={() => setShowPassword(!showPassword)}
className="p-1 hover:bg-gray-100 rounded"
type="button"
>
{showPassword ? (
<EyeOff className="w-4 h-4 text-gray-500" />
) : (
<Eye className="w-4 h-4 text-gray-500" />
)}
</button>
</div>
);
};
const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
accessToken,
@@ -104,6 +116,7 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
const [generalSettings, setGeneralSettings] = useState<passThroughItem[]>(
[]
);
useEffect(() => {
if (!accessToken || !userRole || !userID) {
return;
@@ -114,7 +127,6 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
});
}, [accessToken, userRole, userID]);
const handleResetField = (fieldName: string, idx: number) => {
if (!accessToken) {
return;
@@ -122,72 +134,84 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
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<passThroughItem>[] = [
{
header: "Path",
accessorKey: "path",
cell: (info: any) => (
<Text className="font-mono">{info.getValue()}</Text>
),
},
{
header: "Target",
accessorKey: "target",
cell: (info: any) => (
<Text>{info.getValue()}</Text>
),
},
{
header: "Headers",
accessorKey: "headers",
cell: (info: any) => (
<PasswordField value={info.getValue() || {}} />
),
},
{
header: "Action",
id: "actions",
cell: ({ row }) => (
<Icon
icon={TrashIcon}
color="red"
className="cursor-pointer"
onClick={() => handleResetField(row.original.path, row.index)}
>
Delete
</Icon>
),
},
];
if (!accessToken) {
return null;
}
return (
<div className="w-full mx-4">
<TabGroup className="gap-2 p-8 h-[75vh] w-full mt-2">
<Card>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Path</TableHeaderCell>
<TableHeaderCell>Target</TableHeaderCell>
<TableHeaderCell>Headers</TableHeaderCell>
<TableHeaderCell>Action</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{generalSettings.map((value, index) => (
<TableRow key={index}>
<TableCell>
<Text>{value.path}</Text>
</TableCell>
<TableCell>
{
value.target
}
</TableCell>
<TableCell>
{
JSON.stringify(value.headers)
}
</TableCell>
<TableCell>
<Icon
icon={TrashIcon}
color="red"
onClick={() =>
handleResetField(value.path, index)
}
>
Reset
</Icon>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<AddPassThroughEndpoint accessToken={accessToken} setPassThroughItems={setGeneralSettings} passThroughItems={generalSettings}/>
</Card>
</TabGroup>
<div className="w-full h-[75vh] p-6">
<div className="mb-2 mt-4">
<div>
<Title>Pass Through Endpoints</Title>
<Text className="text-tremor-content">
Configure and manage your pass-through endpoints
</Text>
</div>
</div>
<AddPassThroughEndpoint
accessToken={accessToken}
setPassThroughItems={setGeneralSettings}
passThroughItems={generalSettings}
/>
<DataTable
data={generalSettings}
columns={columns}
renderSubComponent={() => <div></div>}
getRowCanExpand={() => false}
isLoading={false}
noDataMessage="No pass-through endpoints configured"
/>
</div>
);
};