[Feat] UI QA: Pass through endpoints (#11939)

* use ID for pass through management

* use id for pass through

* fix columns

* fix PassThroughInfoView

* cleanup

* working edit and delete pass through

* fix rendering id for pt row

* fixes for pt info view

* working delete pass through

* fix use NumericalInput

* fix alignment

* qa - creating pt

* show route preview

* fix show just 1 msg

* test_create_pass_through_endpoint

* fix ui linting
This commit is contained in:
Ishaan Jaff
2025-06-20 18:35:38 -07:00
committed by GitHub
parent 99d851544a
commit dc2c1122bf
8 changed files with 681 additions and 150 deletions
+4
View File
@@ -1431,6 +1431,10 @@ class DynamoDBArgs(LiteLLMPydanticObjectBase):
class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase):
id: Optional[str] = Field(
default=None,
description="Optional unique identifier for the pass-through endpoint. If not provided, endpoints will be identified by path for backwards compatibility.",
)
path: str = Field(description="The route to be added to the LiteLLM Proxy Server.")
target: str = Field(
description="The URL to which requests for this path should be forwarded."
@@ -1023,6 +1023,7 @@ async def initialize_pass_through_endpoints(
Returns:
None
"""
import uuid
verbose_proxy_logger.debug("initializing pass through endpoints")
from litellm.proxy._types import CommonProxyErrors, LiteLLMRoutes
from litellm.proxy.proxy_server import app, premium_user
@@ -1030,6 +1031,11 @@ async def initialize_pass_through_endpoints(
for endpoint in pass_through_endpoints:
if isinstance(endpoint, PassThroughGenericEndpoint):
endpoint = endpoint.model_dump()
# Auto-generate ID for backwards compatibility if not present
if endpoint.get("id") is None:
endpoint["id"] = str(uuid.uuid4())
_target = endpoint.get("target", None)
_path: Optional[str] = endpoint.get("path", None)
if _path is None:
@@ -1056,7 +1062,7 @@ async def initialize_pass_through_endpoints(
continue
# Add exact path route
verbose_proxy_logger.debug("Initializing pass through endpoint: %s", _path)
verbose_proxy_logger.debug("Initializing pass through endpoint: %s (ID: %s)", _path, endpoint.get("id"))
InitPassThroughEndpointHelpers.add_exact_path_route(
app=app,
path=_path,
@@ -1081,7 +1087,7 @@ async def initialize_pass_through_endpoints(
cost_per_request=endpoint.get("cost_per_request", None),
)
verbose_proxy_logger.debug("Added new pass through endpoint: %s", _path)
verbose_proxy_logger.debug("Added new pass through endpoint: %s (ID: %s)", _path, endpoint.get("id"))
async def _get_pass_through_endpoints_from_db(
@@ -1103,21 +1109,20 @@ async def _get_pass_through_endpoints_from_db(
returned_endpoints: List[PassThroughGenericEndpoint] = []
if endpoint_id is None:
# Return all endpoints
for endpoint in pass_through_endpoint_data:
if isinstance(endpoint, dict):
returned_endpoints.append(PassThroughGenericEndpoint(**endpoint))
elif isinstance(endpoint, PassThroughGenericEndpoint):
returned_endpoints.append(endpoint)
elif endpoint_id is not None:
for endpoint in 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:
returned_endpoints.append(_endpoint)
else:
# Find specific endpoint by ID
found_endpoint = _find_endpoint_by_id(
pass_through_endpoint_data, endpoint_id
)
if found_endpoint is not None:
returned_endpoints.append(found_endpoint)
return returned_endpoints
@@ -1151,7 +1156,7 @@ async def update_pass_through_endpoints(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Update a pass-through endpoint
Update a pass-through endpoint by ID.
"""
from litellm.proxy.proxy_server import (
get_config_general_settings,
@@ -1176,46 +1181,54 @@ async def update_pass_through_endpoints(
detail={"error": "No pass-through endpoints found"},
)
# Find and update the endpoint
updated_endpoint: Optional[PassThroughGenericEndpoint] = None
endpoint_found = False
# Find the endpoint to update
found_endpoint = _find_endpoint_by_id(
pass_through_endpoint_data, endpoint_id
)
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:
if found_endpoint is None:
raise HTTPException(
status_code=404,
detail={
"error": f"Endpoint with path '{endpoint_id}' not found"
"error": f"Endpoint with ID '{endpoint_id}' not found"
},
)
# Find the index for updating the list
endpoint_index = None
for idx, endpoint in enumerate(pass_through_endpoint_data):
_endpoint = PassThroughGenericEndpoint(**endpoint) if isinstance(endpoint, dict) else endpoint
if _endpoint.id == endpoint_id:
endpoint_index = idx
break
if endpoint_index is None:
raise HTTPException(
status_code=404,
detail={
"error": f"Could not find index for endpoint with ID '{endpoint_id}'"
},
)
# 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 = found_endpoint.model_dump()
# Update with new data (only non-None values)
endpoint_dict.update(update_data)
# Preserve existing ID if not provided in update and endpoint has ID
if "id" not in update_data and found_endpoint.id is not None:
endpoint_dict["id"] = found_endpoint.id
# Create updated endpoint object
updated_endpoint = PassThroughGenericEndpoint(**endpoint_dict)
# Update the list
pass_through_endpoint_data[endpoint_index] = endpoint_dict
## Update db
updated_data = ConfigFieldUpdate(
field_name="pass_through_endpoints",
@@ -1240,6 +1253,8 @@ async def create_pass_through_endpoints(
"""
Create new pass-through endpoint
"""
import uuid
from litellm.proxy.proxy_server import (
get_config_general_settings,
update_config_general_settings,
@@ -1256,8 +1271,11 @@ async def create_pass_through_endpoints(
field_name="pass_through_endpoints", field_value=None
)
## Update field with new endpoint
## Auto-generate ID if not provided
data_dict = data.model_dump()
if data_dict.get("id") is None:
data_dict["id"] = str(uuid.uuid4())
if response.field_value is None:
response.field_value = [data_dict]
elif isinstance(response.field_value, List):
@@ -1273,6 +1291,10 @@ async def create_pass_through_endpoints(
data=updated_data, user_api_key_dict=user_api_key_dict
)
# Return the created endpoint with the generated ID
created_endpoint = PassThroughGenericEndpoint(**data_dict)
return PassThroughEndpointResponse(endpoints=[created_endpoint])
@router.delete(
"/config/pass_through_endpoint",
@@ -1284,7 +1306,7 @@ async def delete_pass_through_endpoints(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Delete a pass-through endpoint
Delete a pass-through endpoint by ID.
Returns - the deleted endpoint
"""
@@ -1306,27 +1328,46 @@ async def delete_pass_through_endpoints(
## Update field by removing endpoint
pass_through_endpoint_data: Optional[List] = response.field_value
response_obj: Optional[PassThroughGenericEndpoint] = None
if response.field_value is None or pass_through_endpoint_data is None:
raise HTTPException(
status_code=400,
detail={"error": "There are no pass-through endpoints setup."},
)
elif isinstance(response.field_value, List):
invalid_idx: Optional[int] = None
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:
invalid_idx = idx
response_obj = _endpoint
# Find the endpoint to delete
found_endpoint = _find_endpoint_by_id(
pass_through_endpoint_data, endpoint_id
)
if found_endpoint is None:
raise HTTPException(
status_code=400,
detail={
"error": "Endpoint with ID '{}' was not found in pass-through endpoint list.".format(
endpoint_id
)
},
)
# Find the index for deleting from the list
endpoint_index = None
for idx, endpoint in enumerate(pass_through_endpoint_data):
_endpoint = PassThroughGenericEndpoint(**endpoint) if isinstance(endpoint, dict) else endpoint
if _endpoint.id == endpoint_id:
endpoint_index = idx
break
if invalid_idx is not None:
pass_through_endpoint_data.pop(invalid_idx)
if endpoint_index is None:
raise HTTPException(
status_code=400,
detail={
"error": f"Could not find index for endpoint with ID '{endpoint_id}'"
},
)
# Remove the endpoint
pass_through_endpoint_data.pop(endpoint_index)
response_obj = found_endpoint
## Update db
updated_data = ConfigFieldUpdate(
@@ -1338,18 +1379,37 @@ async def delete_pass_through_endpoints(
data=updated_data, user_api_key_dict=user_api_key_dict
)
if response_obj is None:
raise HTTPException(
status_code=400,
detail={
"error": "Endpoint={} was not found in pass-through endpoint list.".format(
endpoint_id
)
},
)
return PassThroughEndpointResponse(endpoints=[response_obj])
def _find_endpoint_by_id(
endpoints_data: List,
endpoint_id: str,
) -> Optional[PassThroughGenericEndpoint]:
"""
Find an endpoint by ID.
Args:
endpoints_data: List of endpoint data (dicts or PassThroughGenericEndpoint objects)
endpoint_id: ID to search for
Returns:
Found endpoint or None if not found
"""
for endpoint in endpoints_data:
_endpoint: Optional[PassThroughGenericEndpoint] = None
if isinstance(endpoint, dict):
_endpoint = PassThroughGenericEndpoint(**endpoint)
elif isinstance(endpoint, PassThroughGenericEndpoint):
_endpoint = endpoint
# Only compare IDs to IDs
if _endpoint is not None and _endpoint.id == endpoint_id:
return _endpoint
return None
async def initialize_pass_through_endpoints_in_db():
"""
Gets all pass-through endpoints from db and initializes them in the proxy server.
@@ -852,3 +852,396 @@ async def test_pass_through_request_contains_proxy_server_request_in_kwargs():
assert metadata["user_api_key_alias"] == "test-alias"
assert metadata["user_api_key_user_email"] == "test@example.com"
assert metadata["user_api_key_user_id"] == "test-user-id"
@pytest.mark.asyncio
async def test_create_pass_through_endpoint():
"""
Test creating a new pass-through endpoint
This test verifies that the create_pass_through_endpoints function:
1. Accepts a PassThroughGenericEndpoint object
2. Auto-generates an ID if not provided
3. Adds the endpoint to the database
4. Returns the created endpoint with the generated ID
"""
from litellm.proxy._types import (
ConfigFieldInfo,
ConfigFieldUpdate,
PassThroughEndpointResponse,
PassThroughGenericEndpoint,
UserAPIKeyAuth,
)
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
create_pass_through_endpoints,
)
# Mock the database functions
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
with patch("litellm.proxy.proxy_server.update_config_general_settings") as mock_update_config:
# Mock existing config (empty list)
mock_get_config.return_value = ConfigFieldInfo(
field_name="pass_through_endpoints",
field_value=[]
)
# Create test endpoint data
test_endpoint = PassThroughGenericEndpoint(
path="/test/endpoint",
target="http://example.com/api",
headers={"Authorization": "Bearer test-token"},
include_subpath=True,
cost_per_request=0.50
)
# Mock user API key dict
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
# Call the create function
result = await create_pass_through_endpoints(
data=test_endpoint,
user_api_key_dict=mock_user_api_key_dict
)
# Verify the result
assert isinstance(result, PassThroughEndpointResponse)
assert len(result.endpoints) == 1
created_endpoint = result.endpoints[0]
assert created_endpoint.path == "/test/endpoint"
assert created_endpoint.target == "http://example.com/api"
assert created_endpoint.headers == {"Authorization": "Bearer test-token"}
assert created_endpoint.include_subpath is True
assert created_endpoint.cost_per_request == 0.50
assert created_endpoint.id is not None # Should be auto-generated
# Verify database calls
mock_get_config.assert_called_once_with(
field_name="pass_through_endpoints",
user_api_key_dict=mock_user_api_key_dict
)
mock_update_config.assert_called_once()
update_call_args = mock_update_config.call_args[1]
assert update_call_args["data"].field_name == "pass_through_endpoints"
assert len(update_call_args["data"].field_value) == 1
assert update_call_args["data"].field_value[0]["path"] == "/test/endpoint"
assert update_call_args["data"].field_value[0]["id"] is not None
@pytest.mark.asyncio
async def test_update_pass_through_endpoint():
"""
Test updating an existing pass-through endpoint
This test verifies that the update_pass_through_endpoints function:
1. Finds the existing endpoint by ID
2. Updates only the provided fields (partial updates)
3. Preserves the existing ID
4. Updates the database with the modified endpoint
5. Returns the updated endpoint
"""
from litellm.proxy._types import (
ConfigFieldInfo,
ConfigFieldUpdate,
PassThroughEndpointResponse,
PassThroughGenericEndpoint,
UserAPIKeyAuth,
)
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
update_pass_through_endpoints,
)
# Mock the database functions
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
with patch("litellm.proxy.proxy_server.update_config_general_settings") as mock_update_config:
# Create existing endpoint data
existing_endpoint_id = "test-endpoint-123"
existing_endpoints = [
{
"id": existing_endpoint_id,
"path": "/test/endpoint",
"target": "http://example.com/api",
"headers": {"Authorization": "Bearer old-token"},
"include_subpath": False,
"cost_per_request": 0.25
}
]
# Mock existing config
mock_get_config.return_value = ConfigFieldInfo(
field_name="pass_through_endpoints",
field_value=existing_endpoints
)
# Create update data (partial update)
update_data = PassThroughGenericEndpoint(
path="/test/endpoint", # Keep same path
target="http://newapi.com/v2", # Update target
headers={"Authorization": "Bearer new-token", "X-Custom": "header"}, # Update headers
cost_per_request=0.75 # Update cost
# include_subpath not provided - should preserve existing value
)
# Mock user API key dict
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
# Call the update function
result = await update_pass_through_endpoints(
endpoint_id=existing_endpoint_id,
data=update_data,
user_api_key_dict=mock_user_api_key_dict
)
# Verify the result
assert isinstance(result, PassThroughEndpointResponse)
assert len(result.endpoints) == 1
updated_endpoint = result.endpoints[0]
assert updated_endpoint.id == existing_endpoint_id # ID preserved
assert updated_endpoint.path == "/test/endpoint"
assert updated_endpoint.target == "http://newapi.com/v2" # Updated
assert updated_endpoint.headers == {"Authorization": "Bearer new-token", "X-Custom": "header"} # Updated
assert updated_endpoint.include_subpath is False # Preserved existing value
assert updated_endpoint.cost_per_request == 0.75 # Updated
# Verify database calls
mock_get_config.assert_called_once_with(
field_name="pass_through_endpoints",
user_api_key_dict=mock_user_api_key_dict
)
mock_update_config.assert_called_once()
update_call_args = mock_update_config.call_args[1]
assert update_call_args["data"].field_name == "pass_through_endpoints"
assert len(update_call_args["data"].field_value) == 1
updated_data = update_call_args["data"].field_value[0]
assert updated_data["id"] == existing_endpoint_id
assert updated_data["target"] == "http://newapi.com/v2"
assert updated_data["cost_per_request"] == 0.75
@pytest.mark.asyncio
async def test_update_pass_through_endpoint_not_found():
"""
Test updating a non-existent pass-through endpoint raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy._types import (
ConfigFieldInfo,
PassThroughGenericEndpoint,
UserAPIKeyAuth,
)
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
update_pass_through_endpoints,
)
# Mock the database functions
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
# Mock existing config with different endpoint
existing_endpoints = [
{
"id": "different-endpoint-456",
"path": "/different/endpoint",
"target": "http://different.com/api",
"headers": {},
"include_subpath": False,
"cost_per_request": 0.0
}
]
mock_get_config.return_value = ConfigFieldInfo(
field_name="pass_through_endpoints",
field_value=existing_endpoints
)
# Create update data
update_data = PassThroughGenericEndpoint(
path="/test/endpoint",
target="http://newapi.com/v2"
)
# Mock user API key dict
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
# Call the update function with non-existent ID
with pytest.raises(HTTPException) as exc_info:
await update_pass_through_endpoints(
endpoint_id="non-existent-endpoint-123",
data=update_data,
user_api_key_dict=mock_user_api_key_dict
)
# Verify the exception
assert exc_info.value.status_code == 404
assert "not found" in str(exc_info.value.detail).lower()
@pytest.mark.asyncio
async def test_delete_pass_through_endpoint():
"""
Test deleting an existing pass-through endpoint
This test verifies that the delete_pass_through_endpoints function:
1. Finds the existing endpoint by ID
2. Removes it from the database
3. Returns the deleted endpoint
"""
from litellm.proxy._types import (
ConfigFieldInfo,
ConfigFieldUpdate,
PassThroughEndpointResponse,
UserAPIKeyAuth,
)
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
delete_pass_through_endpoints,
)
# Mock the database functions
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
with patch("litellm.proxy.proxy_server.update_config_general_settings") as mock_update_config:
# Create existing endpoint data
endpoint_to_delete_id = "test-endpoint-123"
other_endpoint_id = "other-endpoint-456"
existing_endpoints = [
{
"id": endpoint_to_delete_id,
"path": "/test/endpoint",
"target": "http://example.com/api",
"headers": {"Authorization": "Bearer test-token"},
"include_subpath": True,
"cost_per_request": 0.50
},
{
"id": other_endpoint_id,
"path": "/other/endpoint",
"target": "http://other.com/api",
"headers": {},
"include_subpath": False,
"cost_per_request": 0.25
}
]
# Mock existing config
mock_get_config.return_value = ConfigFieldInfo(
field_name="pass_through_endpoints",
field_value=existing_endpoints
)
# Mock user API key dict
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
# Call the delete function
result = await delete_pass_through_endpoints(
endpoint_id=endpoint_to_delete_id,
user_api_key_dict=mock_user_api_key_dict
)
# Verify the result
assert isinstance(result, PassThroughEndpointResponse)
assert len(result.endpoints) == 1
deleted_endpoint = result.endpoints[0]
assert deleted_endpoint.id == endpoint_to_delete_id
assert deleted_endpoint.path == "/test/endpoint"
assert deleted_endpoint.target == "http://example.com/api"
assert deleted_endpoint.headers == {"Authorization": "Bearer test-token"}
assert deleted_endpoint.include_subpath is True
assert deleted_endpoint.cost_per_request == 0.50
# Verify database calls
mock_get_config.assert_called_once_with(
field_name="pass_through_endpoints",
user_api_key_dict=mock_user_api_key_dict
)
mock_update_config.assert_called_once()
update_call_args = mock_update_config.call_args[1]
assert update_call_args["data"].field_name == "pass_through_endpoints"
# Should only have the other endpoint remaining
assert len(update_call_args["data"].field_value) == 1
remaining_endpoint = update_call_args["data"].field_value[0]
assert remaining_endpoint["id"] == other_endpoint_id
assert remaining_endpoint["path"] == "/other/endpoint"
@pytest.mark.asyncio
async def test_delete_pass_through_endpoint_not_found():
"""
Test deleting a non-existent pass-through endpoint raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy._types import ConfigFieldInfo, UserAPIKeyAuth
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
delete_pass_through_endpoints,
)
# Mock the database functions
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
# Mock existing config with different endpoint
existing_endpoints = [
{
"id": "different-endpoint-456",
"path": "/different/endpoint",
"target": "http://different.com/api",
"headers": {},
"include_subpath": False,
"cost_per_request": 0.0
}
]
mock_get_config.return_value = ConfigFieldInfo(
field_name="pass_through_endpoints",
field_value=existing_endpoints
)
# Mock user API key dict
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
# Call the delete function with non-existent ID
with pytest.raises(HTTPException) as exc_info:
await delete_pass_through_endpoints(
endpoint_id="non-existent-endpoint-123",
user_api_key_dict=mock_user_api_key_dict
)
# Verify the exception
assert exc_info.value.status_code == 400
assert "not found" in str(exc_info.value.detail).lower()
@pytest.mark.asyncio
async def test_delete_pass_through_endpoint_empty_list():
"""
Test deleting from an empty endpoint list raises HTTPException
"""
from fastapi import HTTPException
from litellm.proxy._types import ConfigFieldInfo, UserAPIKeyAuth
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import (
delete_pass_through_endpoints,
)
# Mock the database functions
with patch("litellm.proxy.proxy_server.get_config_general_settings") as mock_get_config:
# Mock empty config
mock_get_config.return_value = ConfigFieldInfo(
field_name="pass_through_endpoints",
field_value=None
)
# Mock user API key dict
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
# Call the delete function
with pytest.raises(HTTPException) as exc_info:
await delete_pass_through_endpoints(
endpoint_id="any-endpoint-123",
user_api_key_dict=mock_user_api_key_dict
)
# Verify the exception
assert exc_info.value.status_code == 400
assert "no pass-through endpoints setup" in str(exc_info.value.detail).lower()
@@ -21,6 +21,7 @@ import {
Divider,
Collapse,
} from "antd";
import NumericalInput from "./shared/numerical_input";
import { InfoCircleOutlined, ApiOutlined, ExclamationCircleOutlined, CheckCircleOutlined, CopyOutlined } from "@ant-design/icons";
import { keyCreateCall, slackBudgetAlertsHealthCheck, modelAvailableCall } from "./networking";
import { list } from "postcss";
@@ -71,17 +72,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
try {
console.log(`formValues: ${JSON.stringify(formValues)}`);
const newPassThroughItem: passThroughItem = {
"headers": formValues["headers"],
"path": formValues["path"],
"target": formValues["target"],
"include_subpath": formValues["include_subpath"] || false,
"cost_per_request": formValues["cost_per_request"] || 0
}
const response = await createPassThroughEndpoint(accessToken, formValues);
await createPassThroughEndpoint(accessToken, formValues);
// Use the created endpoint from the API response (includes the generated ID)
const createdEndpoint = response.endpoints[0];
const updatedPassThroughSettings = [...passThroughItems, newPassThroughItem]
const updatedPassThroughSettings = [...passThroughItems, createdEndpoint]
setPassThroughItems(updatedPassThroughSettings)
message.success("Pass-through endpoint created successfully");
@@ -284,13 +280,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
</div>
}
>
<InputNumber
<NumericalInput
min={0}
step={0.001}
precision={6}
placeholder="2.000000"
precision={4}
placeholder="2.0000"
size="large"
className="rounded-lg w-full"
/>
</Form.Item>
</Card>
@@ -32,7 +32,7 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
return (
<div>
{pairs.map(([key, val], index) => (
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="start">
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="center">
<TextInput
placeholder="Header Name"
value={key}
@@ -43,7 +43,12 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
value={val}
onChange={(e) => handleChange(index, key, e.target.value)}
/>
<MinusCircleOutlined onClick={() => handleRemove(index)} />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
<MinusCircleOutlined
onClick={() => handleRemove(index)}
style={{ cursor: 'pointer' }}
/>
</div>
</Space>
))}
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}>
@@ -15,14 +15,14 @@ import {
} from "@tremor/react";
import { Button, Form, Input, Switch, message, InputNumber } from "antd";
import {
getPassThroughEndpointInfo,
updatePassThroughEndpoint,
deletePassThroughEndpointsCall
} from "./networking";
import { Eye, EyeOff } from "lucide-react";
import RoutePreview from "./route_preview";
export interface PassThroughInfoProps {
endpointPath: string;
endpointData: PassThroughEndpoint;
onClose: () => void;
accessToken: string | null;
isAdmin: boolean;
@@ -30,6 +30,7 @@ export interface PassThroughInfoProps {
}
interface PassThroughEndpoint {
id?: string;
path: string;
target: string;
headers: Record<string, any>;
@@ -63,38 +64,20 @@ const PasswordField: React.FC<{ value: Record<string, any> }> = ({ value }) => {
};
const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
endpointPath,
endpointData: initialEndpointData,
onClose,
accessToken,
isAdmin,
onEndpointUpdated
}) => {
const [endpointData, setEndpointData] = useState<PassThroughEndpoint | null>(null);
const [loading, setLoading] = useState(true);
const [endpointData, setEndpointData] = useState<PassThroughEndpoint | null>(initialEndpointData);
const [loading, setLoading] = useState(false);
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;
if (!accessToken || !endpointData?.id) return;
// Parse headers if provided as string
let headers = {};
@@ -110,15 +93,21 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
}
const updateData = {
path: endpointData.path,
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();
await updatePassThroughEndpoint(accessToken, endpointData.id, updateData);
// Update local state with the new values
setEndpointData({
...endpointData,
...updateData,
});
setIsEditing(false);
if (onEndpointUpdated) {
onEndpointUpdated();
@@ -131,9 +120,9 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
const handleDeleteEndpoint = async () => {
try {
if (!accessToken) return;
if (!accessToken || !endpointData?.id) return;
await deletePassThroughEndpointsCall(accessToken, endpointPath);
await deletePassThroughEndpointsCall(accessToken, endpointData.id);
message.success("Pass through endpoint deleted successfully");
onClose();
if (onEndpointUpdated) {
@@ -158,8 +147,8 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4"> Back</Button>
<Title>Pass Through Endpoint</Title>
<Text className="text-gray-500 font-mono">{endpointData.path}</Text>
<Title>Pass Through Endpoint: {endpointData.path}</Title>
<Text className="text-gray-500 font-mono">{endpointData.id}</Text>
</div>
</div>
@@ -204,6 +193,15 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</Card>
</Grid>
{/* Route Preview Section */}
<div className="mt-6">
<RoutePreview
pathValue={endpointData.path}
targetValue={endpointData.target}
includeSubpath={endpointData.include_subpath || false}
/>
</div>
{endpointData.headers && Object.keys(endpointData.headers).length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center">
@@ -34,23 +34,14 @@ import {
Button as Button2,
message,
InputNumber,
Tooltip,
} from "antd";
import {
InformationCircleIcon,
PencilAltIcon,
PencilIcon,
StatusOnlineIcon,
TrashIcon,
RefreshIcon,
CheckCircleIcon,
XCircleIcon,
QuestionMarkCircleIcon,
} 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";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, EyeOff } from "lucide-react";
@@ -76,6 +67,7 @@ interface nestedFieldItem {
}
export interface passThroughItem {
id?: string
path: string
target: string
headers: object
@@ -117,7 +109,9 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
const [generalSettings, setGeneralSettings] = useState<passThroughItem[]>(
[]
);
const [selectedEndpointPath, setSelectedEndpointPath] = useState<string | null>(null);
const [selectedEndpointId, setSelectedEndpointId] = useState<string | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [endpointToDelete, setEndpointToDelete] = useState<string | null>(null);
useEffect(() => {
if (!accessToken || !userRole || !userID) {
@@ -139,41 +133,65 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
}
};
const handleResetField = (fieldName: string, idx: number) => {
if (!accessToken) {
const handleDelete = async (endpointId: string) => {
// Set the endpoint to delete and open the confirmation modal
setEndpointToDelete(endpointId);
setIsDeleteModalOpen(true);
};
const confirmDelete = async () => {
if (endpointToDelete == null || !accessToken) {
return;
}
try {
deletePassThroughEndpointsCall(accessToken, fieldName);
await deletePassThroughEndpointsCall(accessToken, endpointToDelete);
const updatedSettings = generalSettings.filter((setting) => setting.path !== fieldName);
const updatedSettings = generalSettings.filter((setting) => setting.id !== endpointToDelete);
setGeneralSettings(updatedSettings);
message.success("Endpoint deleted successfully.");
} catch (error) {
// do something
console.error("Error deleting the endpoint:", error);
message.error("Error deleting the endpoint: " + error);
}
// Close the confirmation modal and reset the endpointToDelete
setIsDeleteModalOpen(false);
setEndpointToDelete(null);
};
const cancelDelete = () => {
// Close the confirmation modal and reset the endpointToDelete
setIsDeleteModalOpen(false);
setEndpointToDelete(null);
};
const handleResetField = (endpointId: string, idx: number) => {
// Use handleDelete instead of direct deletion
handleDelete(endpointId);
};
// Define columns for the DataTable
const columns: ColumnDef<passThroughItem>[] = [
{
header: "Path",
accessorKey: "path",
header: "ID",
accessorKey: "id",
cell: (info: any) => (
<div className="overflow-hidden">
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
onClick={() => setSelectedEndpointPath(info.getValue())}
<Tooltip title={info.row.original.id}>
<div
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
onClick={() => info.row.original.id && setSelectedEndpointId(info.row.original.id)}
>
{info.getValue()}
</Button>
</div>
{info.row.original.id}
</div>
</Tooltip>
),
},
{
header: "Path",
accessorKey: "path"
},
{
header: "Target",
accessorKey: "target",
@@ -196,13 +214,13 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => setSelectedEndpointPath(row.original.path)}
onClick={() => row.original.id && setSelectedEndpointId(row.original.id)}
title="Edit"
/>
<Icon
icon={TrashIcon}
size="sm"
onClick={() => handleResetField(row.original.path, row.index)}
onClick={() => handleResetField(row.original.id!, row.index)}
title="Delete"
/>
</div>
@@ -215,11 +233,20 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
}
// If a specific endpoint is selected, show the info view
if (selectedEndpointPath) {
if (selectedEndpointId) {
// Find the endpoint by ID to get the endpoint data for the info view
console.log("selectedEndpointId", selectedEndpointId);
console.log("generalSettings", generalSettings);
const selectedEndpoint = generalSettings.find(endpoint => endpoint.id === selectedEndpointId);
if (!selectedEndpoint) {
return <div>Endpoint not found</div>;
}
return (
<PassThroughInfoView
endpointPath={selectedEndpointPath}
onClose={() => setSelectedEndpointPath(null)}
endpointData={selectedEndpoint}
onClose={() => setSelectedEndpointId(null)}
accessToken={accessToken}
isAdmin={userRole === "Admin" || userRole === "admin"}
onEndpointUpdated={handleEndpointUpdated}
@@ -250,6 +277,55 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
isLoading={false}
noDataMessage="No pass-through endpoints configured"
/>
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 transition-opacity"
aria-hidden="true"
>
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
{/* Modal Panel */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>
{/* Confirmation Modal Content */}
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete Pass-Through Endpoint
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this pass-through endpoint? This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button
onClick={confirmDelete}
color="red"
className="ml-2"
>
Delete
</Button>
<Button onClick={cancelDelete}>Cancel</Button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
@@ -783,7 +783,7 @@ const Settings: React.FC<SettingsPageProps> = ({
okButtonProps={{ danger: true }}
>
<p>
Are you sure you want to delete the callback "{callbackToDelete}"?
Are you sure you want to delete the callback - {callbackToDelete}?
This action cannot be undone.
</p>
</Modal>