[Feat] UI - Allow Adding LiteLLM Auto Router on UI (#12960)

* add router.json

* test_router_auto_router

* async_pre_routing_hook

* fixes for auto router

* add async_pre_routing_hook

* add LiteLLMRouterEncoder

* update test auto_router_embedding_model

* add auto_router_embedding_model

* add AutoRouter

* fix async_pre_routing_hook

* update async_pre_routing_hook

* fix auto router

* fix router.json

* working router init

* working embedding encoder

* working auto router

* test_router_auto_router

* test auto router

* add semantic-router as optional for litellm

* add extras

* semantic_router==0.1.10

* ruff fix

* use aiohttp==3.10.11

* python-dotenv==1.0.1

* test auto router

* test_router_auto_router

* semantic_router

* test_is_auto_router_deployment

* fix check

* fix docker build step

* add semantic_router

* UI  - Add auto router on litellm

* working utterances config

* fix route config builder

* kind of working add automodel router

* move loc of add deployment

* fixes for AutoRouter

* add auto_router_config in types.py

* fixes for init_auto_router_deployment

* fix adding auto router models

* working auto-router with dB

* Revert "add semantic_router"

This reverts commit 537b67288798731a119d811f643b682086377ee9.

* TestAutoRouter

* fix linting

* add semantic router to docker

* test fix

* fix router config builder

* remove export button
This commit is contained in:
Ishaan Jaff 2025-07-24 19:58:49 -07:00 committed by GitHub
parent e63b163578
commit 106a298f0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 825 additions and 72 deletions

View File

@ -65,6 +65,9 @@ COPY --from=builder /wheels/ /wheels/
# Install the built wheel using pip; again using a wildcard if it's the only file
RUN pip install *.whl /wheels/* --no-index --find-links=/wheels/ && rm -f *.whl && rm -rf /wheels
# Install semantic_router without dependencies
RUN pip install semantic_router --no-deps
# Generate prisma client
RUN prisma generate
RUN chmod +x docker/entrypoint.sh

View File

@ -57,6 +57,9 @@ COPY --from=builder /wheels/ /wheels/
# Install the built wheel using pip; again using a wildcard if it's the only file
RUN pip install *.whl /wheels/* --no-index --find-links=/wheels/ && rm -f *.whl && rm -rf /wheels
# Install semantic_router without dependencies
RUN pip install semantic_router --no-deps
# ensure pyjwt is used, not jwt
RUN pip uninstall jwt -y
RUN pip uninstall PyJWT -y

View File

@ -46,6 +46,9 @@ RUN pip install *.whl /wheels/* --no-index --find-links=/wheels/ \
&& rm -f *.whl \
&& rm -rf /wheels
# Install semantic_router without dependencies
RUN pip install semantic_router --no-deps
# Ensure correct JWT library is used (pyjwt not jwt)
RUN pip uninstall jwt -y && \
pip uninstall PyJWT -y && \

View File

@ -4703,11 +4703,6 @@ class Router:
)
#########################################################
# Check if this is an auto-router deployment
#########################################################
if self._is_auto_router_deployment(litellm_params=litellm_params):
self.init_auto_router_deployment(deployment=deployment)
## OLD MODEL REGISTRATION ## Kept to prevent breaking changes
_model_name = deployment.litellm_params.model
@ -4764,9 +4759,10 @@ class Router:
This will initialize the auto-router and add it to the auto-routers dictionary.
"""
from litellm.router_strategy.auto_router.auto_router import AutoRouter
router_config_path: Optional[str] = deployment.litellm_params.auto_router_config_path
if router_config_path is None:
raise ValueError("auto_router_config_path is required for auto-router deployments. Please set it in the litellm_params")
auto_router_config_path: Optional[str] = deployment.litellm_params.auto_router_config_path
auto_router_config: Optional[str] = deployment.litellm_params.auto_router_config
if auto_router_config_path is None and auto_router_config is None:
raise ValueError("auto_router_config_path or auto_router_config is required for auto-router deployments. Please set it in the litellm_params")
default_model: Optional[str] = deployment.litellm_params.auto_router_default_model
if default_model is None:
@ -4778,7 +4774,8 @@ class Router:
autor_router: AutoRouter = AutoRouter(
model_name=deployment.model_name,
router_config_path=router_config_path,
auto_router_config_path=auto_router_config_path,
auto_router_config=auto_router_config,
default_model=default_model,
embedding_model=embedding_model,
litellm_router_instance=self,
@ -4950,6 +4947,12 @@ class Router:
model=deployment.litellm_params.model,
)
#########################################################
# Check if this is an auto-router deployment
#########################################################
if self._is_auto_router_deployment(litellm_params=deployment.litellm_params):
self.init_auto_router_deployment(deployment=deployment)
return deployment
def _initialize_deployment_for_pass_through(

View File

@ -7,11 +7,14 @@ from litellm._logging import verbose_router_logger
from litellm.integrations.custom_logger import CustomLogger
if TYPE_CHECKING:
from semantic_router.routers.base import Route
from litellm.router import Router
from litellm.types.router import PreRoutingHookResponse
else:
Router = Any
PreRoutingHookResponse = Any
Route = Any
class AutoRouter(CustomLogger):
@ -19,30 +22,63 @@ class AutoRouter(CustomLogger):
def __init__(
self,
model_name: str,
router_config_path: str,
default_model: str,
embedding_model: str,
litellm_router_instance: "Router",
auto_router_config_path: Optional[str] = None,
auto_router_config: Optional[str] = None,
):
"""
Auto-Router class that uses a semantic router to route requests to the appropriate model.
Args:
model_name: The name of the model to use for the auto-router. eg. if model = "auto-router1" then us this router.
router_config_path: The path to the router config file.
auto_router_config_path: The path to the router config file.
auto_router_config: The config to use for the auto-router. You can either use this or auto_router_config_path, not both.
default_model: The default model to use if no route is found.
embedding_model: The embedding model to use for the auto-router.
litellm_router_instance: The instance of the LiteLLM Router.
"""
from semantic_router.routers import SemanticRouter
self.router_config_path = router_config_path
self.auto_router_config_path: Optional[str] = auto_router_config_path
self.auto_router_config: Optional[str] = auto_router_config
self.auto_sync_value = self.DEFAULT_AUTO_SYNC_VALUE
self.loaded_router: SemanticRouter = SemanticRouter.from_json(self.router_config_path)
self.loaded_routes: List[Route] = self._load_semantic_routing_routes()
self.routelayer: Optional[SemanticRouter] = None
self.default_model = default_model
self.embedding_model: str = embedding_model
self.litellm_router_instance: "Router" = litellm_router_instance
def _load_semantic_routing_routes(self) -> List[Route]:
from semantic_router.routers import SemanticRouter
if self.auto_router_config_path:
return SemanticRouter.from_json(self.auto_router_config_path).routes
elif self.auto_router_config:
return self._load_auto_router_routes_from_config_json()
else:
raise ValueError("No router config provided")
def _load_auto_router_routes_from_config_json(self) -> List[Route]:
import json
from semantic_router.routers.base import Route
if self.auto_router_config is None:
raise ValueError("No auto router config provided")
auto_router_routes: List[Route] = []
loaded_config = json.loads(self.auto_router_config)
for route in loaded_config.get("routes", []):
auto_router_routes.append(
Route(
name=route.get("name"),
description=route.get("description"),
utterances=route.get("utterances", []),
score_threshold=route.get("score_threshold")
)
)
return auto_router_routes
async def async_pre_routing_hook(
@ -74,7 +110,7 @@ class AutoRouter(CustomLogger):
# Create the route layer
#######################
self.routelayer = SemanticRouter(
routes=self.loaded_router.routes,
routes=self.loaded_routes,
encoder=LiteLLMRouterEncoder(
litellm_router_instance=self.litellm_router_instance,
model_name=self.embedding_model,

View File

@ -212,6 +212,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
# auto-router params
auto_router_config_path: Optional[str] = None
auto_router_config: Optional[str] = None
auto_router_default_model: Optional[str] = None
auto_router_embedding_model: Optional[str] = None
@ -261,6 +262,7 @@ class GenericLiteLLMParams(CredentialLiteLLMParams, CustomPricingLiteLLMParams):
mock_response: Optional[Union[str, ModelResponse, Exception, Any]] = None,
# auto-router params
auto_router_config_path: Optional[str] = None,
auto_router_config: Optional[str] = None,
auto_router_default_model: Optional[str] = None,
auto_router_embedding_model: Optional[str] = None,
**params,

View File

@ -1263,55 +1263,6 @@ def test_is_auto_router_deployment(model_list):
assert router._is_auto_router_deployment(litellm_params_contains) is False
def test_init_auto_router_deployment_missing_params(model_list):
"""Test if the 'init_auto_router_deployment' function raises ValueError when required parameters are missing"""
router = Router(model_list=model_list)
# Test case 1: Missing auto_router_config_path
litellm_params = LiteLLM_Params(
model="auto_router/test",
auto_router_default_model="gpt-3.5-turbo",
auto_router_embedding_model="text-embedding-ada-002"
)
deployment = Deployment(
model_name="test-auto-router",
litellm_params=litellm_params,
model_info={"id": "test-id"}
)
with pytest.raises(ValueError, match="auto_router_config_path is required"):
router.init_auto_router_deployment(deployment)
# Test case 2: Missing auto_router_default_model
litellm_params = LiteLLM_Params(
model="auto_router/test",
auto_router_config_path="/path/to/config",
auto_router_embedding_model="text-embedding-ada-002"
)
deployment = Deployment(
model_name="test-auto-router",
litellm_params=litellm_params,
model_info={"id": "test-id"}
)
with pytest.raises(ValueError, match="auto_router_default_model is required"):
router.init_auto_router_deployment(deployment)
# Test case 3: Missing auto_router_embedding_model
litellm_params = LiteLLM_Params(
model="auto_router/test",
auto_router_config_path="/path/to/config",
auto_router_default_model="gpt-3.5-turbo"
)
deployment = Deployment(
model_name="test-auto-router",
litellm_params=litellm_params,
model_info={"id": "test-id"}
)
with pytest.raises(ValueError, match="auto_router_embedding_model is required"):
router.init_auto_router_deployment(deployment)
@patch('litellm.router_strategy.auto_router.auto_router.AutoRouter')
def test_init_auto_router_deployment_success(mock_auto_router, model_list):

View File

@ -56,14 +56,14 @@ class TestAutoRouter:
# Act
auto_router = AutoRouter(
model_name=model_name,
router_config_path=router_config_path,
auto_router_config_path=router_config_path,
default_model=default_model,
embedding_model=embedding_model,
litellm_router_instance=mock_router_instance,
)
# Assert
assert auto_router.router_config_path == router_config_path
assert auto_router.auto_router_config_path == router_config_path
assert auto_router.auto_sync_value == AutoRouter.DEFAULT_AUTO_SYNC_VALUE
assert auto_router.default_model == default_model
assert auto_router.embedding_model == embedding_model
@ -93,7 +93,7 @@ class TestAutoRouter:
auto_router = AutoRouter(
model_name="test-auto-router",
router_config_path="test/path/router.json",
auto_router_config_path="test/path/router.json",
default_model="gpt-4o-mini",
embedding_model="text-embedding-model",
litellm_router_instance=mock_router_instance,
@ -136,7 +136,7 @@ class TestAutoRouter:
auto_router = AutoRouter(
model_name="test-auto-router",
router_config_path="test/path/router.json",
auto_router_config_path="test/path/router.json",
default_model="gpt-4o-mini",
embedding_model="text-embedding-model",
litellm_router_instance=mock_router_instance,
@ -163,7 +163,7 @@ class TestAutoRouter:
with patch('semantic_router.routers.SemanticRouter'):
auto_router = AutoRouter(
model_name="test-auto-router",
router_config_path="test/path/router.json",
auto_router_config_path="test/path/router.json",
default_model="gpt-4o-mini",
embedding_model="text-embedding-model",
litellm_router_instance=mock_router_instance,

View File

@ -0,0 +1,367 @@
import React, { useEffect, useState } from "react";
import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal, Upload, message } from "antd";
import type { FormInstance } from "antd";
import type { UploadProps } from "antd/es/upload";
import { UploadOutlined } from "@ant-design/icons";
import { Text, TextInput } from "@tremor/react";
import { Row, Col } from "antd";
import { CredentialItem, modelAvailableCall } from "../networking";
import ConnectionErrorDisplay from "./model_connection_test";
import { all_admin_roles } from "@/utils/roles";
import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
import { fetchAvailableModels, ModelGroup } from "../chat_ui/llm_calls/fetch_models";
import RouterConfigBuilder from "./router_config_builder";
interface AddAutoRouterTabProps {
form: FormInstance;
handleOk: () => void;
accessToken: string;
userRole: string;
}
const { Title, Link } = Typography;
const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
form,
handleOk,
accessToken,
userRole,
}) => {
// State for connection testing
const [isResultModalVisible, setIsResultModalVisible] = useState<boolean>(false);
const [isTestingConnection, setIsTestingConnection] = useState<boolean>(false);
const [connectionTestId, setConnectionTestId] = useState<string>("");
const [modelAccessGroups, setModelAccessGroups] = useState<string[]>([]);
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
const [showCustomDefaultModel, setShowCustomDefaultModel] = useState<boolean>(false);
const [showCustomEmbeddingModel, setShowCustomEmbeddingModel] = useState<boolean>(false);
const [routerConfig, setRouterConfig] = useState<any>(null);
useEffect(() => {
const fetchModelAccessGroups = async () => {
const response = await modelAvailableCall(accessToken, "", "", false, null, true, true);
setModelAccessGroups(response["data"].map((model: any) => model["id"]));
};
fetchModelAccessGroups();
}, [accessToken]);
useEffect(() => {
const loadModels = async () => {
try {
const uniqueModels = await fetchAvailableModels(accessToken);
console.log("Fetched models for auto router:", uniqueModels);
setModelInfo(uniqueModels);
} catch (error) {
console.error("Error fetching model info for auto router:", error);
}
};
loadModels();
}, [accessToken]);
const isAdmin = all_admin_roles.includes(userRole);
// Test connection when button is clicked
const handleTestConnection = async () => {
setIsTestingConnection(true);
setConnectionTestId(`test-${Date.now()}`);
setIsResultModalVisible(true);
};
// Auto router specific form submit handler
const handleAutoRouterSubmit = () => {
console.log("Auto router submit triggered!");
console.log("Router config:", routerConfig);
const currentFormValues = form.getFieldsValue();
console.log("Form values:", currentFormValues);
// Check basic required fields first
if (!currentFormValues.auto_router_name) {
message.error("Please enter an Auto Router Name");
return;
}
if (!currentFormValues.auto_router_default_model) {
message.error("Please select a Default Model");
return;
}
// Set auto router specific form values that are required by the regular model form
form.setFieldsValue({
custom_llm_provider: 'auto_router',
model: currentFormValues.auto_router_name,
// api_key is not needed for auto router, but form expects it
api_key: 'not_required_for_auto_router'
});
// Custom validation for router config
if (!routerConfig || !routerConfig.routes || routerConfig.routes.length === 0) {
message.error("Please configure at least one route for the auto router");
return;
}
// Check if all routes have required fields
const invalidRoutes = routerConfig.routes.filter((route: any) =>
!route.name || !route.description || route.utterances.length === 0
);
if (invalidRoutes.length > 0) {
message.error("Please ensure all routes have a target model, description, and at least one utterance");
return;
}
form
.validateFields()
.then((values) => {
console.log("Form validation passed, submitting with values:", values);
// Add the router config to form values
const submitValues = {
...values,
auto_router_config: routerConfig,
};
console.log("Final submit values:", submitValues);
handleAddAutoRouterSubmit(submitValues, accessToken, form, handleOk);
})
.catch((error) => {
console.error("Validation failed:", error);
// Extract specific field errors
const fieldErrors = error.errorFields || [];
if (fieldErrors.length > 0) {
const missingFields = fieldErrors.map((field: any) => {
const fieldName = field.name[0];
const friendlyNames: { [key: string]: string } = {
'auto_router_name': 'Auto Router Name',
'auto_router_default_model': 'Default Model',
'auto_router_embedding_model': 'Embedding Model'
};
return friendlyNames[fieldName] || fieldName;
});
message.error(`Please fill in the following required fields: ${missingFields.join(', ')}`);
} else {
message.error("Please fill in all required fields");
}
});
};
return (
<>
<Title level={2}>Add Auto Router</Title>
<Text className="text-gray-600 mb-6">
Create an auto router with intelligent routing logic that automatically selects the best model based on user input patterns and semantic matching.
</Text>
<Card>
<Form
form={form}
onFinish={handleAutoRouterSubmit}
labelCol={{ span: 10 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
{/* Auto Router Name */}
<Form.Item
rules={[{ required: true, message: "Auto router name is required" }]}
label="Auto Router Name"
name="auto_router_name"
tooltip="Unique name for this auto router configuration"
labelCol={{ span: 10 }}
labelAlign="left"
>
<TextInput placeholder="e.g., auto_router_1, smart_routing" />
</Form.Item>
{/* Router Configuration Builder */}
<div className="w-full mb-4">
<RouterConfigBuilder
modelInfo={modelInfo}
value={routerConfig}
onChange={(config) => {
setRouterConfig(config);
form.setFieldValue('auto_router_config', config);
}}
/>
</div>
{/* Auto Router Default Model */}
<Form.Item
rules={[{ required: true, message: "Default model is required" }]}
label="Default Model"
name="auto_router_default_model"
tooltip="Fallback model to use when auto routing logic cannot determine the best model"
labelCol={{ span: 10 }}
labelAlign="left"
>
<AntdSelect
placeholder="Select a default model"
onChange={(value) => {
setShowCustomDefaultModel(value === 'custom');
}}
options={[
...Array.from(new Set(modelInfo.map(option => option.model_group)))
.map((model_group) => ({
value: model_group,
label: model_group,
})),
{ value: 'custom', label: 'Enter custom model name' }
]}
style={{ width: "100%" }}
showSearch={true}
/>
</Form.Item>
{showCustomDefaultModel && (
<Form.Item
label="Custom Default Model"
name="custom_default_model"
labelCol={{ span: 10 }}
labelAlign="left"
>
<TextInput
placeholder="Enter custom model name"
onChange={(e) => {
form.setFieldValue('auto_router_default_model', e.target.value);
}}
/>
</Form.Item>
)}
{/* Auto Router Embedding Model */}
<Form.Item
label="Embedding Model"
name="auto_router_embedding_model"
tooltip="Optional: Embedding model to use for semantic routing decisions"
labelCol={{ span: 10 }}
labelAlign="left"
>
<AntdSelect
value={form.getFieldValue('auto_router_embedding_model')}
placeholder="Select an embedding model (optional)"
onChange={(value) => {
setShowCustomEmbeddingModel(value === 'custom');
form.setFieldValue('auto_router_embedding_model', value);
}}
options={[
...Array.from(new Set(modelInfo.map(option => option.model_group)))
.map((model_group) => ({
value: model_group,
label: model_group,
})),
{ value: 'custom', label: 'Enter custom model name' }
]}
style={{ width: "100%" }}
showSearch={true}
allowClear
/>
</Form.Item>
{showCustomEmbeddingModel && (
<Form.Item
label="Custom Embedding Model"
name="custom_embedding_model"
labelCol={{ span: 10 }}
labelAlign="left"
>
<TextInput
placeholder="Enter custom embedding model name"
onChange={(e) => {
form.setFieldValue('auto_router_embedding_model', e.target.value);
}}
/>
</Form.Item>
)}
<div className="flex items-center my-4">
<div className="flex-grow border-t border-gray-200"></div>
<span className="px-4 text-gray-500 text-sm">Additional Settings</span>
<div className="flex-grow border-t border-gray-200"></div>
</div>
{/* Model Access Groups - Admin only */}
{isAdmin && (
<Form.Item
label="Model Access Group"
name="model_access_group"
className="mb-4"
tooltip="Use model access groups to control who can access this auto router"
>
<AntdSelect
mode="tags"
showSearch
placeholder="Select existing groups or type to create new ones"
optionFilterProp="children"
tokenSeparators={[',']}
options={modelAccessGroups.map((group) => ({
value: group,
label: group
}))}
maxTagCount="responsive"
allowClear
/>
</Form.Item>
)}
<div className="flex justify-between items-center mb-4">
<Tooltip title="Get help on our github">
<Typography.Link href="https://github.com/BerriAI/litellm/issues">
Need Help?
</Typography.Link>
</Tooltip>
<div className="space-x-2">
<Button onClick={handleTestConnection} loading={isTestingConnection}>Test Connect</Button>
<Button
onClick={() => {
console.log("Add Auto Router button clicked!");
console.log("Current router config:", routerConfig);
console.log("Current form values:", form.getFieldsValue());
handleAutoRouterSubmit();
}}
>
Add Auto Router
</Button>
</div>
</div>
</Form>
</Card>
{/* Test Connection Results Modal */}
<Modal
title="Connection Test Results"
open={isResultModalVisible}
onCancel={() => {
setIsResultModalVisible(false);
setIsTestingConnection(false);
}}
footer={[
<Button key="close" onClick={() => {
setIsResultModalVisible(false);
setIsTestingConnection(false);
}}>
Close
</Button>
]}
width={700}
>
{/* Only render the ConnectionErrorDisplay when modal is visible and we have a test ID */}
{isResultModalVisible && (
<ConnectionErrorDisplay
key={connectionTestId}
formValues={form.getFieldsValue()}
accessToken={accessToken}
testMode="chat"
modelName={form.getFieldValue('auto_router_name')}
onClose={() => {
setIsResultModalVisible(false);
setIsTestingConnection(false);
}}
onTestComplete={() => setIsTestingConnection(false)}
/>
)}
</Modal>
</>
);
};
export default AddAutoRouterTab;

View File

@ -9,4 +9,14 @@ export const TEST_MODES = [
{ value: "rerank", label: "Rerank - /rerank" },
{ value: "realtime", label: "Realtime - /realtime"},
{ value: "batch", label: "Batch - /batch"}
];
// Define the available auto router routing strategies
export const AUTO_ROUTER_MODES = [
{ value: "simple-shuffle", label: "Simple Shuffle - Random selection from available models" },
{ value: "least-busy", label: "Least Busy - Route to model with lowest current load" },
{ value: "latency-based", label: "Latency Based - Route to model with best response time" },
{ value: "cost-based", label: "Cost Based - Route to most cost-effective model" },
{ value: "usage-based", label: "Usage Based - Route based on historical usage patterns" },
{ value: "custom", label: "Custom - Use custom routing logic defined in config" }
];

View File

@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd";
import type { FormInstance } from "antd";
import type { UploadProps } from "antd/es/upload";
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
import LiteLLMModelNameField from "./litellm_model_name";
import ConditionalPublicModelName from "./conditional_public_model_name";
import ProviderSpecificFields from "./provider_specific_fields";
@ -15,6 +16,8 @@ import { Row, Col } from "antd";
import { Text, TextInput, Switch } from "@tremor/react";
import TeamDropdown from "../common_components/team_dropdown";
import { all_admin_roles } from "@/utils/roles";
import AddAutoRouterTab from "./add_auto_router_tab";
import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
interface AddModelTabProps {
form: FormInstance;
@ -85,10 +88,28 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
const isAdmin = all_admin_roles.includes(userRole);
const handleAutoRouterOk = () => {
form
.validateFields()
.then((values) => {
handleAddAutoRouterSubmit(values, accessToken, form, handleOk);
})
.catch((error) => {
console.error("Validation failed:", error);
});
};
return (
<>
<Title level={2}>Add new model</Title>
<Card>
<TabGroup className="w-full">
<TabList className="mb-4">
<Tab>Add Model</Tab>
<Tab>Add Auto Router</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Card>
<Form
form={form}
onFinish={handleOk}
@ -328,7 +349,18 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
</div>
</>
</Form>
</Card>
</Card>
</TabPanel>
<TabPanel>
<AddAutoRouterTab
form={form}
handleOk={handleAutoRouterOk}
accessToken={accessToken}
userRole={userRole}
/>
</TabPanel>
</TabPanels>
</TabGroup>
{/* Test Connection Results Modal */}
<Modal

View File

@ -0,0 +1,65 @@
import { message } from "antd";
import { modelCreateCall, Model } from "../networking";
export const handleAddAutoRouterSubmit = async (
values: any,
accessToken: string,
form: any,
callback?: () => void,
) => {
try {
console.log("=== AUTO ROUTER SUBMIT HANDLER CALLED ===");
console.log("handling auto router submit for formValues:", values);
console.log("Access token:", accessToken ? "Present" : "Missing");
console.log("Form:", form ? "Present" : "Missing");
console.log("Callback:", callback ? "Present" : "Missing");
// Create auto router configuration
const autoRouterConfig: any = {
model_name: values.auto_router_name,
litellm_params: {
model: `auto_router/${values.auto_router_name}`,
auto_router_config: JSON.stringify(values.auto_router_config), // Convert JSON object to string as expected by backend
auto_router_default_model: values.auto_router_default_model,
},
model_info: {},
};
// Add optional embedding model if provided
if (values.auto_router_embedding_model && values.auto_router_embedding_model !== 'custom') {
autoRouterConfig.litellm_params.auto_router_embedding_model = values.auto_router_embedding_model;
} else if (values.custom_embedding_model) {
autoRouterConfig.litellm_params.auto_router_embedding_model = values.custom_embedding_model;
}
// Add team information if provided
if (values.team_id) {
autoRouterConfig.model_info.team_id = values.team_id;
}
// Add model access groups if provided
if (values.model_access_group && values.model_access_group.length > 0) {
autoRouterConfig.model_info.access_groups = values.model_access_group;
}
console.log("Auto router configuration to be created:", autoRouterConfig);
console.log("Auto router config (stringified):", autoRouterConfig.litellm_params.auto_router_config);
// Create the auto router using the same model creation endpoint
console.log("Calling modelCreateCall with:", { accessToken: accessToken ? "Present" : "Missing", config: autoRouterConfig });
const response: any = await modelCreateCall(accessToken, autoRouterConfig as Model);
console.log(`response for auto router create call:`, response);
message.success("Auto router added successfully!");
// Call the callback function if provided (usually to refresh the model list)
callback && callback();
// Reset the form
form.resetFields();
} catch (error) {
console.error("Failed to add auto router:", error);
message.error("Failed to add auto router: " + error, 10);
}
};

View File

@ -0,0 +1,274 @@
import React, { useState, useEffect } from "react";
import { Card, Form, Button, Input, InputNumber, Select as AntdSelect, Space, Tooltip, Collapse } from "antd";
import { PlusOutlined, DeleteOutlined, InfoCircleOutlined, DownOutlined } from "@ant-design/icons";
import { Text, TextInput } from "@tremor/react";
import { ModelGroup } from "../chat_ui/llm_calls/fetch_models";
const { TextArea } = Input;
const { Panel } = Collapse;
interface Route {
id: string;
model: string;
utterances: string[];
description: string;
score_threshold: number;
}
interface RouterConfigBuilderProps {
modelInfo: ModelGroup[];
value?: any;
onChange?: (config: any) => void;
}
const RouterConfigBuilder: React.FC<RouterConfigBuilderProps> = ({
modelInfo,
value,
onChange,
}) => {
const [routes, setRoutes] = useState<Route[]>(value?.routes || []);
const [showJsonPreview, setShowJsonPreview] = useState<boolean>(false);
const [expandedRoutes, setExpandedRoutes] = useState<string[]>([]);
// Initialize expanded routes for existing routes on mount
useEffect(() => {
if (value?.routes && value.routes.length > 0 && expandedRoutes.length === 0) {
const existingRouteIds = value.routes.map((route: any) => route.id || `route-${Math.random()}`);
setExpandedRoutes(existingRouteIds);
}
}, [value?.routes, expandedRoutes.length]);
// Handle adding a new route
const addRoute = () => {
const newRouteId = `route-${Date.now()}`;
const newRoute: Route = {
id: newRouteId,
model: "",
utterances: [],
description: "",
score_threshold: 0.5,
};
const updatedRoutes = [...routes, newRoute];
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
// Automatically expand the new route
setExpandedRoutes(prev => [...prev, newRouteId]);
};
// Handle removing a route
const removeRoute = (routeId: string) => {
const updatedRoutes = routes.filter(route => route.id !== routeId);
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
// Remove from expanded routes as well
setExpandedRoutes(prev => prev.filter(id => id !== routeId));
};
// Handle updating a route
const updateRoute = (routeId: string, field: keyof Route, value: any) => {
const updatedRoutes = routes.map(route =>
route.id === routeId ? { ...route, [field]: value } : route
);
setRoutes(updatedRoutes);
updateConfig(updatedRoutes);
};
// Update the overall configuration
const updateConfig = (updatedRoutes: Route[]) => {
const config = {
routes: updatedRoutes.map(route => ({
name: route.model,
utterances: route.utterances,
description: route.description,
score_threshold: route.score_threshold,
})),
};
onChange?.(config);
};
// Handle utterances change (convert textarea string to array)
const handleUtterancesChange = (routeId: string, utterancesText: string) => {
const utterancesArray = utterancesText
.split('\n')
.map(line => line.trim()) // Only trims leading/trailing whitespace, preserves internal spaces
.filter(line => line.length > 0);
updateRoute(routeId, 'utterances', utterancesArray);
};
// Prepare model options for dropdowns
const modelOptions = modelInfo.map(model => ({
value: model.model_group,
label: model.model_group,
}));
const generateConfig = () => {
return {
routes: routes.map(route => ({
name: route.model,
utterances: route.utterances,
description: route.description,
score_threshold: route.score_threshold,
})),
};
};
return (
<div className="w-full max-w-none">
{/* Routes Configuration Header */}
<div className="flex justify-between items-center mb-6 w-full">
<div className="flex items-center gap-2">
<Text className="text-lg font-semibold">Routes Configuration</Text>
<Tooltip title="Configure routing logic to automatically select the best model based on user input patterns">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={addRoute}
className="bg-blue-600 hover:bg-blue-700"
>
Add Route
</Button>
</div>
{/* Routes */}
{routes.length === 0 ? (
<div className="text-center py-12 text-gray-500 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200 mb-6">
<Text>No routes configured. Click &ldquo;Add Route&rdquo; to get started.</Text>
</div>
) : (
<div className="space-y-3 mb-6 w-full">
{routes.map((route, index) => (
<Card
key={route.id}
className="border border-gray-200 shadow-sm w-full"
bodyStyle={{ padding: 0 }}
>
<Collapse
ghost
expandIcon={({ isActive }) => <DownOutlined rotate={isActive ? 180 : 0} />}
activeKey={expandedRoutes}
onChange={(keys) => setExpandedRoutes(Array.isArray(keys) ? keys : [keys].filter(Boolean))}
items={[
{
key: route.id,
label: (
<div className="flex justify-between items-center py-2">
<Text className="font-medium text-base">
Route {index + 1}: {route.model || 'Unnamed'}
</Text>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
removeRoute(route.id);
}}
className="mr-2"
/>
</div>
),
children: (
<div className="px-6 pb-6 w-full">
{/* Model Selection */}
<div className="mb-4 w-full">
<Text className="text-sm font-medium mb-2 block">Model</Text>
<AntdSelect
value={route.model}
onChange={(value) => updateRoute(route.id, 'model', value)}
placeholder="Select model"
showSearch
style={{ width: '100%' }}
options={modelOptions}
/>
</div>
{/* Description */}
<div className="mb-4 w-full">
<Text className="text-sm font-medium mb-2 block">Description</Text>
<TextArea
value={route.description}
onChange={(e) => updateRoute(route.id, 'description', e.target.value)}
placeholder="Describe when this route should be used..."
rows={2}
style={{ width: '100%' }}
/>
</div>
{/* Score Threshold */}
<div className="mb-4 w-full">
<div className="flex items-center gap-2 mb-2">
<Text className="text-sm font-medium">Score Threshold</Text>
<Tooltip title="Minimum similarity score to route to this model (0-1)">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<InputNumber
value={route.score_threshold}
onChange={(value) => updateRoute(route.id, 'score_threshold', value || 0)}
min={0}
max={1}
step={0.1}
style={{ width: '100%' }}
placeholder="0.5"
/>
</div>
{/* Example Utterances */}
<div className="w-full">
<div className="flex items-center gap-2 mb-2">
<Text className="text-sm font-medium">Example Utterances</Text>
<Tooltip title="Training examples for this route. Type an utterance and press Enter to add it.">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Text className="text-xs text-gray-500 mb-2">Type an utterance and press Enter to add it. You can also paste multiple lines.</Text>
<AntdSelect
mode="tags"
value={route.utterances}
onChange={(utterances) => updateRoute(route.id, 'utterances', utterances)}
placeholder="Type an utterance and press Enter..."
style={{ width: '100%' }}
tokenSeparators={['\n']}
maxTagCount="responsive"
allowClear
/>
</div>
</div>
),
},
]}
/>
</Card>
))}
</div>
)}
{/* JSON Preview */}
<div className="border-t pt-6 w-full">
<div className="flex justify-between items-center mb-4 w-full">
<Text className="text-lg font-semibold">JSON Preview</Text>
<Button
type="link"
onClick={() => setShowJsonPreview(!showJsonPreview)}
className="text-blue-600 p-0"
>
{showJsonPreview ? 'Hide' : 'Show'}
</Button>
</div>
{showJsonPreview && (
<Card className="bg-gray-50 w-full">
<pre className="text-sm overflow-auto max-h-64 w-full">
{JSON.stringify(generateConfig(), null, 2)}
</pre>
</Card>
)}
</div>
</div>
);
};
export default RouterConfigBuilder;

View File

@ -21,6 +21,7 @@ import {
} from "./networking";
import { handleAddModelSubmit } from "./add_model/handle_add_model_submit";
import CredentialsPanel from "@/components/model_add/credentials";
import { getDisplayModelName } from "./view_model/model_name_display";
import {
@ -67,6 +68,7 @@ import {
} from "./provider_info_helpers";
import ModelInfoView from "./model_info_view";
import AddModelTab from "./add_model/add_model_tab";
import { ModelDataTable } from "./model_dashboard/table";
import { columns } from "./model_dashboard/columns";
import HealthCheckComponent from "./model_dashboard/HealthCheckComponent";
@ -967,6 +969,8 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
});
};
console.log(`selectedProvider: ${selectedProvider}`);
console.log(`providerModels.length: ${providerModels.length}`);

View File

@ -393,7 +393,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
return (
<div style={{ width: "100%" }} className="p-8 relative">
{/* Export Data Button - Positioned in top right corner */}
{all_admin_roles.includes(userRole || "") && (
{/* {all_admin_roles.includes(userRole || "") && (
<div className="absolute top-4 right-4 z-10">
<button
onClick={() => setIsCloudZeroModalOpen(true)}
@ -428,7 +428,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
</svg>
</button>
</div>
)}
)} */}
<TabGroup>
<TabList variant="solid" className="mt-1">