mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-07 18:23:28 +00:00
[Feat] UI + Management - Allow setting all routing strategies, tag filtering on UI (#16139)
* add ROUTER_SETTINGS_FIELDS * feat: add router_settings_router on proxy * add RouterSettingsField * update RouterSettingsResponse * add ROUTER_SETTINGS_FIELDS * add getRouterSettingsCall * add UI friendly name * fix router settings * fix get descriptions * fix toggles /refator * fix refactor * fix refactor * fix ROUTING_STRATEGY_DESCRIPTIONS * fix fields
This commit is contained in:
@@ -7073,7 +7073,8 @@
|
||||
"output_cost_per_token": 6e-07,
|
||||
"litellm_provider": "deepinfra",
|
||||
"mode": "chat",
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"deepinfra/Qwen/Qwen3-14B": {
|
||||
"max_tokens": 40960,
|
||||
@@ -18620,7 +18621,8 @@
|
||||
"max_tokens": 8192,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 6.3e-07,
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"openrouter/qwen/qwen3-coder": {
|
||||
"input_cost_per_token": 1e-06,
|
||||
@@ -21143,16 +21145,6 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.4e-07
|
||||
},
|
||||
"vercel_ai_gateway/glm-4.6": {
|
||||
"litellm_provider": "vercel_ai_gateway",
|
||||
"cache_read_input_token_cost": 1.1e-07,
|
||||
"input_cost_per_token": 6e-07,
|
||||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 200000,
|
||||
"max_tokens": 200000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.2e-06
|
||||
},
|
||||
"vercel_ai_gateway/alibaba/qwen-3-235b": {
|
||||
"input_cost_per_token": 2e-07,
|
||||
"litellm_provider": "vercel_ai_gateway",
|
||||
@@ -21986,6 +21978,20 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.1e-06
|
||||
},
|
||||
"vercel_ai_gateway/zai/glm-4.6": {
|
||||
"litellm_provider": "vercel_ai_gateway",
|
||||
"cache_read_input_token_cost": 1.1e-07,
|
||||
"input_cost_per_token": 4.5e-07,
|
||||
"max_input_tokens": 200000,
|
||||
"max_output_tokens": 200000,
|
||||
"max_tokens": 200000,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.8e-06,
|
||||
"source": "https://vercel.com/ai-gateway/models/glm-4.6",
|
||||
"supports_function_calling": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_tool_choice": true
|
||||
},
|
||||
"vertex_ai/claude-3-5-haiku": {
|
||||
"input_cost_per_token": 1e-06,
|
||||
"litellm_provider": "vertex_ai-anthropic_models",
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
ROUTER SETTINGS MANAGEMENT
|
||||
|
||||
Endpoints for accessing router configuration and metadata
|
||||
|
||||
GET /router/settings - Get router configuration including available routing strategies
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from typing import Any, Dict, List, get_args
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.router import Router
|
||||
from litellm.types.management_endpoints import (
|
||||
ROUTER_SETTINGS_FIELDS,
|
||||
ROUTING_STRATEGY_DESCRIPTIONS,
|
||||
RouterSettingsField,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class RouterSettingsResponse(BaseModel):
|
||||
fields: List[RouterSettingsField] = Field(
|
||||
description="List of all configurable router settings with metadata"
|
||||
)
|
||||
current_values: Dict[str, Any] = Field(
|
||||
description="Current values of router settings"
|
||||
)
|
||||
routing_strategy_descriptions: Dict[str, str] = Field(
|
||||
description="Descriptions for each routing strategy option"
|
||||
)
|
||||
|
||||
|
||||
def _get_routing_strategies_from_router_class() -> List[str]:
|
||||
"""
|
||||
Dynamically extract routing strategies from the Router class __init__ method.
|
||||
"""
|
||||
# Get the __init__ signature
|
||||
sig = inspect.signature(Router.__init__)
|
||||
|
||||
# Get the routing_strategy parameter
|
||||
routing_strategy_param = sig.parameters.get("routing_strategy")
|
||||
|
||||
if routing_strategy_param and routing_strategy_param.annotation:
|
||||
# Extract Literal values using get_args
|
||||
literal_values = get_args(routing_strategy_param.annotation)
|
||||
if literal_values:
|
||||
return list(literal_values)
|
||||
|
||||
raise ValueError("Unable to extract routing strategies from Router class")
|
||||
|
||||
|
||||
@router.get(
|
||||
"/router/settings",
|
||||
tags=["Router Settings"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=RouterSettingsResponse,
|
||||
)
|
||||
async def get_router_settings(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Get router configuration and available settings.
|
||||
|
||||
Returns:
|
||||
- fields: List of all configurable router settings with their metadata (type, description, default, options)
|
||||
The routing_strategy field includes available options extracted from the Router class
|
||||
- current_values: Current values of router settings from config
|
||||
"""
|
||||
from litellm.proxy.proxy_server import llm_router, proxy_config
|
||||
|
||||
try:
|
||||
# Get available routing strategies dynamically from Router class
|
||||
available_routing_strategies = _get_routing_strategies_from_router_class()
|
||||
|
||||
# Get router settings fields from types file
|
||||
router_fields = [field.model_copy(deep=True) for field in ROUTER_SETTINGS_FIELDS]
|
||||
|
||||
# Populate routing_strategy field with available options and descriptions
|
||||
for field in router_fields:
|
||||
if field.field_name == "routing_strategy":
|
||||
field.options = available_routing_strategies
|
||||
break
|
||||
|
||||
# Try to get router settings from config
|
||||
config = await proxy_config.get_config()
|
||||
router_settings_from_config = config.get("router_settings", {})
|
||||
|
||||
# Get current values from llm_router if initialized
|
||||
current_values = {}
|
||||
if llm_router is not None:
|
||||
# Check all field names from the fields list
|
||||
for field in router_fields:
|
||||
if hasattr(llm_router, field.field_name):
|
||||
value = getattr(llm_router, field.field_name)
|
||||
current_values[field.field_name] = value
|
||||
|
||||
# Merge with config values (config takes precedence)
|
||||
current_values.update(router_settings_from_config)
|
||||
|
||||
# Update field values with current values
|
||||
for field in router_fields:
|
||||
if field.field_name in current_values:
|
||||
field.field_value = current_values[field.field_name]
|
||||
|
||||
return RouterSettingsResponse(
|
||||
fields=router_fields,
|
||||
current_values=current_values,
|
||||
routing_strategy_descriptions=ROUTING_STRATEGY_DESCRIPTIONS,
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(
|
||||
f"Error fetching router settings: {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
@@ -263,6 +263,9 @@ from litellm.proxy.management_endpoints.cost_tracking_settings import (
|
||||
from litellm.proxy.management_endpoints.customer_endpoints import (
|
||||
router as customer_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.router_settings_endpoints import (
|
||||
router as router_settings_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||
router as internal_user_router,
|
||||
)
|
||||
@@ -10040,6 +10043,7 @@ app.include_router(budget_management_router)
|
||||
app.include_router(model_management_router)
|
||||
app.include_router(tag_management_router)
|
||||
app.include_router(cost_tracking_settings_router)
|
||||
app.include_router(router_settings_router)
|
||||
app.include_router(user_agent_analytics_router)
|
||||
app.include_router(enterprise_router)
|
||||
app.include_router(ui_discovery_endpoints_router)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Types for management endpoints
|
||||
"""
|
||||
|
||||
from .router_settings_endpoints import (
|
||||
ROUTER_SETTINGS_FIELDS,
|
||||
ROUTING_STRATEGY_DESCRIPTIONS,
|
||||
RouterSettingsField,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ROUTER_SETTINGS_FIELDS",
|
||||
"ROUTING_STRATEGY_DESCRIPTIONS",
|
||||
"RouterSettingsField",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Types and field definitions for router settings management endpoints
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RouterSettingsField(BaseModel):
|
||||
field_name: str
|
||||
field_type: str
|
||||
field_value: Any
|
||||
field_description: str
|
||||
field_default: Any = None
|
||||
options: Optional[List[str]] = None # For fields with predefined options/enum values
|
||||
ui_field_name: str # User-friendly display name
|
||||
link: Optional[str] = None # Documentation link for the field
|
||||
|
||||
|
||||
# Routing strategy descriptions
|
||||
ROUTING_STRATEGY_DESCRIPTIONS: Dict[str, str] = {
|
||||
"simple-shuffle": "Randomly picks a deployment from the list. Simple and fast.",
|
||||
"least-busy": "Routes to the deployment with the lowest number of ongoing requests.",
|
||||
"latency-based-routing": "Routes to the deployment with the lowest latency over a sliding window.",
|
||||
"cost-based-routing": "Routes to the deployment with the lowest cost per token.",
|
||||
"usage-based-routing": "Routes to the deployment with the lowest TPM (Tokens Per Minute) usage. (deprecated)",
|
||||
"usage-based-routing-v2": "Improved version of usage-based routing with better tracking.",
|
||||
}
|
||||
|
||||
|
||||
# Define all available router settings fields
|
||||
ROUTER_SETTINGS_FIELDS: List[RouterSettingsField] = [
|
||||
RouterSettingsField(
|
||||
field_name="routing_strategy",
|
||||
field_type="String",
|
||||
field_value=None,
|
||||
field_description="Routing strategy to use for load balancing across deployments",
|
||||
field_default="simple-shuffle",
|
||||
options=[], # Will be populated dynamically from Router class
|
||||
ui_field_name="Routing Strategy",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="routing_strategy_args",
|
||||
field_type="Dictionary",
|
||||
field_value=None,
|
||||
field_description="Arguments to pass to the routing strategy (e.g., ttl, lowest_latency_buffer for latency-based-routing)",
|
||||
field_default={},
|
||||
ui_field_name="Routing Strategy Args",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="num_retries",
|
||||
field_type="Integer",
|
||||
field_value=None,
|
||||
field_description="Number of retries for failed requests",
|
||||
field_default=0,
|
||||
ui_field_name="Number of Retries",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="timeout",
|
||||
field_type="Float",
|
||||
field_value=None,
|
||||
field_description="Timeout for requests in seconds",
|
||||
field_default=None,
|
||||
ui_field_name="Timeout",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="stream_timeout",
|
||||
field_type="Float",
|
||||
field_value=None,
|
||||
field_description="Timeout for streaming requests in seconds",
|
||||
field_default=None,
|
||||
ui_field_name="Stream Timeout",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="max_fallbacks",
|
||||
field_type="Integer",
|
||||
field_value=None,
|
||||
field_description="Maximum number of fallbacks to try before exiting the call",
|
||||
field_default=5,
|
||||
ui_field_name="Max Fallbacks",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="fallbacks",
|
||||
field_type="List",
|
||||
field_value=None,
|
||||
field_description="List of fallback model mappings",
|
||||
field_default=[],
|
||||
ui_field_name="Fallbacks",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="context_window_fallbacks",
|
||||
field_type="List",
|
||||
field_value=None,
|
||||
field_description="List of fallback models for context window errors",
|
||||
field_default=[],
|
||||
ui_field_name="Context Window Fallbacks",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="content_policy_fallbacks",
|
||||
field_type="List",
|
||||
field_value=None,
|
||||
field_description="List of fallback models for content policy errors",
|
||||
field_default=[],
|
||||
ui_field_name="Content Policy Fallbacks",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="allowed_fails",
|
||||
field_type="Integer",
|
||||
field_value=None,
|
||||
field_description="Number of times a deployment can fail before being added to cooldown",
|
||||
field_default=None,
|
||||
ui_field_name="Allowed Fails",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="cooldown_time",
|
||||
field_type="Float",
|
||||
field_value=None,
|
||||
field_description="Time in seconds to cooldown a deployment after failure",
|
||||
field_default=None,
|
||||
ui_field_name="Cooldown Time",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="retry_after",
|
||||
field_type="Integer",
|
||||
field_value=None,
|
||||
field_description="Minimum time to wait before retrying a failed request in seconds",
|
||||
field_default=0,
|
||||
ui_field_name="Retry After",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="retry_policy",
|
||||
field_type="Dictionary",
|
||||
field_value=None,
|
||||
field_description="Custom retry policy for different exception types",
|
||||
field_default=None,
|
||||
ui_field_name="Retry Policy",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="model_group_alias",
|
||||
field_type="Dictionary",
|
||||
field_value=None,
|
||||
field_description="Aliases for model groups",
|
||||
field_default={},
|
||||
ui_field_name="Model Group Alias",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="enable_pre_call_checks",
|
||||
field_type="Boolean",
|
||||
field_value=None,
|
||||
field_description="Enable pre-call checks before routing requests",
|
||||
field_default=False,
|
||||
ui_field_name="Enable Pre-call Checks",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="default_litellm_params",
|
||||
field_type="Dictionary",
|
||||
field_value=None,
|
||||
field_description="Default parameters for Router.chat.completion.create",
|
||||
field_default=None,
|
||||
ui_field_name="Default LiteLLM Params",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="set_verbose",
|
||||
field_type="Boolean",
|
||||
field_value=None,
|
||||
field_description="Enable verbose logging for router",
|
||||
field_default=False,
|
||||
ui_field_name="Verbose Logging",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="default_max_parallel_requests",
|
||||
field_type="Integer",
|
||||
field_value=None,
|
||||
field_description="Default maximum parallel requests across all deployments",
|
||||
field_default=None,
|
||||
ui_field_name="Max Parallel Requests",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="enable_tag_filtering",
|
||||
field_type="Boolean",
|
||||
field_value=None,
|
||||
field_description="Enable tag-based routing to route requests based on tags",
|
||||
field_default=False,
|
||||
ui_field_name="Enable Tag Filtering",
|
||||
link="https://docs.litellm.ai/docs/proxy/tag_routing",
|
||||
),
|
||||
RouterSettingsField(
|
||||
field_name="disable_cooldowns",
|
||||
field_type="Boolean",
|
||||
field_value=None,
|
||||
field_description="Disable cooldown mechanism for failed deployments",
|
||||
field_default=None,
|
||||
ui_field_name="Disable Cooldowns",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Button,
|
||||
Icon,
|
||||
} from "@tremor/react";
|
||||
import {
|
||||
getCallbacksCall,
|
||||
setCallbacksCall,
|
||||
} from "./networking";
|
||||
import { TrashIcon } from "@heroicons/react/outline";
|
||||
import AddFallbacks from "./add_fallbacks";
|
||||
import openai from "openai";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
interface FallbacksProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
modelData: any;
|
||||
}
|
||||
|
||||
async function testFallbackModelResponse(selectedModel: string, accessToken: string) {
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal != true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : window.location.origin;
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken,
|
||||
baseURL: proxyBaseUrl,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await client.chat.completions.create({
|
||||
model: selectedModel,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hi, this is a test message",
|
||||
},
|
||||
],
|
||||
// @ts-ignore
|
||||
mock_testing_fallbacks: true,
|
||||
});
|
||||
|
||||
NotificationsManager.success(
|
||||
<span>
|
||||
Test model=<strong>{selectedModel}</strong>, received model=
|
||||
<strong>{response.model}</strong>. See{" "}
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => window.open("https://docs.litellm.ai/docs/proxy/reliability", "_blank")}
|
||||
style={{ textDecoration: "underline", color: "blue" }}
|
||||
>
|
||||
curl
|
||||
</a>
|
||||
</span>,
|
||||
);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(
|
||||
`Error occurred while generating model response. Please try again. Error: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID, modelData }) => {
|
||||
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || !userRole || !userID) {
|
||||
return;
|
||||
}
|
||||
getCallbacksCall(accessToken, userID, userRole).then((data) => {
|
||||
console.log("callbacks", data);
|
||||
let router_settings = data.router_settings;
|
||||
if ("model_group_retry_policy" in router_settings) {
|
||||
delete router_settings["model_group_retry_policy"];
|
||||
}
|
||||
setRouterSettings(router_settings);
|
||||
});
|
||||
}, [accessToken, userRole, userID]);
|
||||
|
||||
const deleteFallbacks = async (key: string) => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`received key: ${key}`);
|
||||
console.log(`routerSettings['fallbacks']: ${routerSettings["fallbacks"]}`);
|
||||
|
||||
const updatedFallbacks = routerSettings["fallbacks"]
|
||||
.map((dict: { [key: string]: any }) => {
|
||||
if (key in dict) {
|
||||
delete dict[key];
|
||||
}
|
||||
return dict;
|
||||
})
|
||||
.filter((dict: { [key: string]: any }) => Object.keys(dict).length > 0);
|
||||
|
||||
const updatedSettings = {
|
||||
...routerSettings,
|
||||
fallbacks: updatedFallbacks,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedSettings,
|
||||
};
|
||||
|
||||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
setRouterSettings(updatedSettings);
|
||||
NotificationsManager.success("Router settings updated successfully");
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Model Name</TableHeaderCell>
|
||||
<TableHeaderCell>Fallbacks</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{routerSettings["fallbacks"] &&
|
||||
routerSettings["fallbacks"].map((item: object, index: number) =>
|
||||
Object.entries(item).map(([key, value]) => (
|
||||
<TableRow key={index.toString() + key}>
|
||||
<TableCell>{key}</TableCell>
|
||||
<TableCell>{Array.isArray(value) ? value.join(", ") : value}</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => testFallbackModelResponse(key, accessToken)}>Test Fallback</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={TrashIcon} size="sm" onClick={() => deleteFallbacks(key)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<AddFallbacks
|
||||
models={modelData?.data ? modelData.data.map((data: any) => data.model_name) : []}
|
||||
accessToken={accessToken}
|
||||
routerSettings={routerSettings}
|
||||
setRouterSettings={setRouterSettings}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Fallbacks;
|
||||
|
||||
@@ -10,20 +10,11 @@ import {
|
||||
TableCell,
|
||||
TableBody,
|
||||
Text,
|
||||
Grid,
|
||||
Button,
|
||||
TextInput,
|
||||
Select as Select2,
|
||||
SelectItem,
|
||||
Col,
|
||||
Accordion,
|
||||
AccordionBody,
|
||||
AccordionHeader,
|
||||
Icon,
|
||||
} from "@tremor/react";
|
||||
import { TabPanel, TabPanels, TabGroup, TabList, Tab, Icon } from "@tremor/react";
|
||||
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
|
||||
import {
|
||||
getCallbacksCall,
|
||||
setCallbacksCall,
|
||||
getGeneralSettingsCall,
|
||||
updateConfigFieldSetting,
|
||||
deleteConfigFieldSetting,
|
||||
@@ -31,9 +22,8 @@ import {
|
||||
import { Form, InputNumber } from "antd";
|
||||
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
|
||||
|
||||
import AddFallbacks from "./add_fallbacks";
|
||||
import openai from "openai";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import RouterSettings from "./router_settings";
|
||||
import Fallbacks from "./fallbacks";
|
||||
interface GeneralSettingsPageProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
@@ -41,64 +31,6 @@ interface GeneralSettingsPageProps {
|
||||
modelData: any;
|
||||
}
|
||||
|
||||
async function testFallbackModelResponse(selectedModel: string, accessToken: string) {
|
||||
// base url should be the current base_url
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
if (isLocal != true) {
|
||||
console.log = function () {};
|
||||
}
|
||||
console.log("isLocal:", isLocal);
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : window.location.origin;
|
||||
const client = new openai.OpenAI({
|
||||
apiKey: accessToken, // Replace with your OpenAI API key
|
||||
baseURL: proxyBaseUrl, // Replace with your OpenAI API base URL
|
||||
dangerouslyAllowBrowser: true, // using a temporary litellm proxy key
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await client.chat.completions.create({
|
||||
model: selectedModel,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Hi, this is a test message",
|
||||
},
|
||||
],
|
||||
// @ts-ignore
|
||||
mock_testing_fallbacks: true,
|
||||
});
|
||||
|
||||
NotificationsManager.success(
|
||||
<span>
|
||||
Test model=<strong>{selectedModel}</strong>, received model=
|
||||
<strong>{response.model}</strong>. See{" "}
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => window.open("https://docs.litellm.ai/docs/proxy/reliability", "_blank")}
|
||||
style={{ textDecoration: "underline", color: "blue" }}
|
||||
>
|
||||
curl
|
||||
</a>
|
||||
</span>,
|
||||
);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(
|
||||
`Error occurred while generating model response. Please try again. Error: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface AccordionHeroProps {
|
||||
selectedStrategy: string | null;
|
||||
strategyArgs: routingStrategyArgs;
|
||||
paramExplanation: { [key: string]: string };
|
||||
}
|
||||
|
||||
interface routingStrategyArgs {
|
||||
ttl?: number;
|
||||
lowest_latency_buffer?: number;
|
||||
}
|
||||
|
||||
interface generalSettingsItem {
|
||||
field_name: string;
|
||||
field_type: string;
|
||||
@@ -107,152 +39,18 @@ interface generalSettingsItem {
|
||||
stored_in_db: boolean | null;
|
||||
}
|
||||
|
||||
const defaultLowestLatencyArgs: routingStrategyArgs = {
|
||||
ttl: 3600,
|
||||
lowest_latency_buffer: 0,
|
||||
};
|
||||
|
||||
export const AccordionHero: React.FC<AccordionHeroProps> = ({ selectedStrategy, strategyArgs, paramExplanation }) => (
|
||||
<Accordion>
|
||||
<AccordionHeader className="text-sm font-medium text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
Routing Strategy Specific Args
|
||||
</AccordionHeader>
|
||||
<AccordionBody>
|
||||
{selectedStrategy == "latency-based-routing" ? (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Setting</TableHeaderCell>
|
||||
<TableHeaderCell>Value</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(strategyArgs).map(([param, value]) => (
|
||||
<TableRow key={param}>
|
||||
<TableCell>
|
||||
<Text>{param}</Text>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.65rem",
|
||||
color: "#808080",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
className="mt-1"
|
||||
>
|
||||
{paramExplanation[param]}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInput
|
||||
name={param}
|
||||
defaultValue={typeof value === "object" ? JSON.stringify(value, null, 2) : value.toString()}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
) : (
|
||||
<Text>No specific settings</Text>
|
||||
)}
|
||||
</AccordionBody>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, userRole, userID, modelData }) => {
|
||||
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
|
||||
const [generalSettingsDict, setGeneralSettingsDict] = useState<{
|
||||
[key: string]: any;
|
||||
}>({});
|
||||
const [generalSettings, setGeneralSettings] = useState<generalSettingsItem[]>([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [selectedCallback, setSelectedCallback] = useState<string | null>(null);
|
||||
const [selectedStrategy, setSelectedStrategy] = useState<string | null>(null);
|
||||
const [strategySettings, setStrategySettings] = useState<routingStrategyArgs | null>(null);
|
||||
|
||||
let paramExplanation: { [key: string]: string } = {
|
||||
routing_strategy_args: "(dict) Arguments to pass to the routing strategy",
|
||||
routing_strategy: "(string) Routing strategy to use",
|
||||
allowed_fails: "(int) Number of times a deployment can fail before being added to cooldown",
|
||||
cooldown_time: "(int) time in seconds to cooldown a deployment after failure",
|
||||
num_retries: "(int) Number of retries for failed requests. Defaults to 0.",
|
||||
timeout: "(float) Timeout for requests. Defaults to None.",
|
||||
retry_after: "(int) Minimum time to wait before retrying a failed request",
|
||||
ttl: "(int) Sliding window to look back over when calculating the average latency of a deployment. Default - 1 hour (in seconds).",
|
||||
lowest_latency_buffer:
|
||||
"(float) Shuffle between deployments within this % of the lowest latency. Default - 0 (i.e. always pick lowest latency).",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || !userRole || !userID) {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
getCallbacksCall(accessToken, userID, userRole).then((data) => {
|
||||
console.log("callbacks", data);
|
||||
let router_settings = data.router_settings;
|
||||
// remove "model_group_retry_policy" from general_settings if exists
|
||||
if ("model_group_retry_policy" in router_settings) {
|
||||
delete router_settings["model_group_retry_policy"];
|
||||
}
|
||||
setRouterSettings(router_settings);
|
||||
});
|
||||
getGeneralSettingsCall(accessToken).then((data) => {
|
||||
let general_settings = data;
|
||||
setGeneralSettings(general_settings);
|
||||
});
|
||||
}, [accessToken, userRole, userID]);
|
||||
|
||||
const handleAddCallback = () => {
|
||||
console.log("Add callback clicked");
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
setSelectedCallback(null);
|
||||
};
|
||||
|
||||
const deleteFallbacks = async (key: string) => {
|
||||
/**
|
||||
* pop the key from the Object, if it exists
|
||||
*/
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`received key: ${key}`);
|
||||
console.log(`routerSettings['fallbacks']: ${routerSettings["fallbacks"]}`);
|
||||
|
||||
const updatedFallbacks = routerSettings["fallbacks"]
|
||||
.map((dict: { [key: string]: any }) => {
|
||||
if (key in dict) {
|
||||
delete dict[key];
|
||||
}
|
||||
return dict;
|
||||
})
|
||||
.filter((dict: { [key: string]: any }) => Object.keys(dict).length > 0);
|
||||
|
||||
const updatedSettings = {
|
||||
...routerSettings,
|
||||
fallbacks: updatedFallbacks,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedSettings,
|
||||
};
|
||||
|
||||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
setRouterSettings(updatedSettings);
|
||||
NotificationsManager.success("Router settings updated successfully");
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
};
|
||||
}, [accessToken]);
|
||||
|
||||
const handleInputChange = (fieldName: string, newValue: any) => {
|
||||
// Update the value in the state
|
||||
@@ -303,212 +101,33 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveChanges = (router_settings: any) => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("router_settings", router_settings);
|
||||
|
||||
const numberKeys = new Set(["allowed_fails", "cooldown_time", "num_retries", "timeout", "retry_after"]);
|
||||
const jsonKeys = new Set(["model_group_alias", "retry_policy"]);
|
||||
|
||||
const parseInputValue = (key: string, raw: string | undefined, fallback: unknown) => {
|
||||
if (raw === undefined) return fallback;
|
||||
|
||||
const v = raw.trim();
|
||||
|
||||
if (v.toLowerCase() === "null") return null;
|
||||
|
||||
if (numberKeys.has(key)) {
|
||||
const n = Number(v);
|
||||
return Number.isNaN(n) ? fallback : n;
|
||||
}
|
||||
|
||||
if (jsonKeys.has(key)) {
|
||||
if (v === "") return null;
|
||||
try {
|
||||
return JSON.parse(v);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.toLowerCase() === "true") return true;
|
||||
if (v.toLowerCase() === "false") return false;
|
||||
|
||||
return v;
|
||||
};
|
||||
|
||||
const updatedVariables = Object.fromEntries(
|
||||
Object.entries(router_settings)
|
||||
.map(([key, value]) => {
|
||||
if (key !== "routing_strategy_args" && key !== "routing_strategy") {
|
||||
const inputEl = document.querySelector(`input[name="${key}"]`) as HTMLInputElement | null;
|
||||
const parsed = parseInputValue(key, inputEl?.value, value);
|
||||
return [key, parsed];
|
||||
} else if (key === "routing_strategy") {
|
||||
return [key, selectedStrategy];
|
||||
} else if (key === "routing_strategy_args" && selectedStrategy === "latency-based-routing") {
|
||||
let setRoutingStrategyArgs: routingStrategyArgs = {};
|
||||
|
||||
const lowestLatencyBufferElement = document.querySelector(
|
||||
`input[name="lowest_latency_buffer"]`,
|
||||
) as HTMLInputElement;
|
||||
const ttlElement = document.querySelector(`input[name="ttl"]`) as HTMLInputElement;
|
||||
|
||||
if (lowestLatencyBufferElement?.value) {
|
||||
setRoutingStrategyArgs["lowest_latency_buffer"] = Number(lowestLatencyBufferElement.value);
|
||||
}
|
||||
|
||||
if (ttlElement?.value) {
|
||||
setRoutingStrategyArgs["ttl"] = Number(ttlElement.value);
|
||||
}
|
||||
|
||||
console.log(`setRoutingStrategyArgs: ${setRoutingStrategyArgs}`);
|
||||
return ["routing_strategy_args", setRoutingStrategyArgs];
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((entry) => entry !== null && entry !== undefined) as Iterable<[string, unknown]>,
|
||||
);
|
||||
console.log("updatedVariables", updatedVariables);
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedVariables,
|
||||
};
|
||||
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
|
||||
NotificationsManager.success("router settings updated successfully");
|
||||
};
|
||||
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mx-4">
|
||||
<TabGroup className="gap-2 p-8 h-[75vh] w-full mt-2">
|
||||
<TabList variant="line" defaultValue="1">
|
||||
<div className="w-full">
|
||||
<TabGroup className="h-[75vh] w-full">
|
||||
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
|
||||
<Tab value="1">Loadbalancing</Tab>
|
||||
<Tab value="2">Fallbacks</Tab>
|
||||
<Tab value="3">General</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanels className="px-8 py-6">
|
||||
<TabPanel>
|
||||
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
|
||||
<Title>Router Settings</Title>
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Setting</TableHeaderCell>
|
||||
<TableHeaderCell>Value</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(routerSettings)
|
||||
.filter(
|
||||
([param, value]) =>
|
||||
param != "fallbacks" &&
|
||||
param != "context_window_fallbacks" &&
|
||||
param != "routing_strategy_args",
|
||||
)
|
||||
.map(([param, value]) => (
|
||||
<TableRow key={param}>
|
||||
<TableCell>
|
||||
<Text>{param}</Text>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "0.65rem",
|
||||
color: "#808080",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
className="mt-1"
|
||||
>
|
||||
{paramExplanation[param]}
|
||||
</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{param == "routing_strategy" ? (
|
||||
<Select2
|
||||
defaultValue={value}
|
||||
className="w-full max-w-md"
|
||||
onValueChange={setSelectedStrategy}
|
||||
>
|
||||
<SelectItem value="usage-based-routing">usage-based-routing</SelectItem>
|
||||
<SelectItem value="latency-based-routing">latency-based-routing</SelectItem>
|
||||
<SelectItem value="simple-shuffle">simple-shuffle</SelectItem>
|
||||
</Select2>
|
||||
) : (
|
||||
<TextInput
|
||||
name={param}
|
||||
defaultValue={
|
||||
typeof value === "object" ? JSON.stringify(value, null, 2) : value.toString()
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<AccordionHero
|
||||
selectedStrategy={selectedStrategy}
|
||||
strategyArgs={
|
||||
routerSettings &&
|
||||
routerSettings["routing_strategy_args"] &&
|
||||
Object.keys(routerSettings["routing_strategy_args"]).length > 0
|
||||
? routerSettings["routing_strategy_args"]
|
||||
: defaultLowestLatencyArgs // default value when keys length is 0
|
||||
}
|
||||
paramExplanation={paramExplanation}
|
||||
/>
|
||||
</Card>
|
||||
<Col>
|
||||
<Button className="mt-2" onClick={() => handleSaveChanges(routerSettings)}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Col>
|
||||
</Grid>
|
||||
<RouterSettings
|
||||
accessToken={accessToken}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
modelData={modelData}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Model Name</TableHeaderCell>
|
||||
<TableHeaderCell>Fallbacks</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{routerSettings["fallbacks"] &&
|
||||
routerSettings["fallbacks"].map((item: object, index: number) =>
|
||||
Object.entries(item).map(([key, value]) => (
|
||||
<TableRow key={index.toString() + key}>
|
||||
<TableCell>{key}</TableCell>
|
||||
<TableCell>{Array.isArray(value) ? value.join(", ") : value}</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => testFallbackModelResponse(key, accessToken)}>Test Fallback</Button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={TrashIcon} size="sm" onClick={() => deleteFallbacks(key)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<AddFallbacks
|
||||
models={modelData?.data ? modelData.data.map((data: any) => data.model_name) : []}
|
||||
<Fallbacks
|
||||
accessToken={accessToken}
|
||||
routerSettings={routerSettings}
|
||||
setRouterSettings={setRouterSettings}
|
||||
userRole={userRole}
|
||||
userID={userID}
|
||||
modelData={modelData}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
|
||||
@@ -4442,6 +4442,35 @@ export const getGeneralSettingsCall = async (accessToken: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getRouterSettingsCall = async (accessToken: string) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/router/settings`
|
||||
: `/router/settings`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to get router settings:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPassThroughEndpointsCall = async (accessToken: string, teamId?: string | null) => {
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/config/pass_through_endpoint` : `/config/pass_through_endpoint`;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import { TextInput } from "@tremor/react";
|
||||
|
||||
interface routingStrategyArgs {
|
||||
ttl?: number;
|
||||
lowest_latency_buffer?: number;
|
||||
}
|
||||
|
||||
const defaultLowestLatencyArgs: routingStrategyArgs = {
|
||||
ttl: 3600,
|
||||
lowest_latency_buffer: 0,
|
||||
};
|
||||
|
||||
interface LatencyBasedConfigurationProps {
|
||||
routingStrategyArgs: { [key: string]: any };
|
||||
}
|
||||
|
||||
const LatencyBasedConfiguration: React.FC<LatencyBasedConfigurationProps> = ({
|
||||
routingStrategyArgs,
|
||||
}) => {
|
||||
const paramExplanation: { [key: string]: string } = {
|
||||
ttl: "Sliding window to look back over when calculating the average latency of a deployment. Default - 1 hour (in seconds).",
|
||||
lowest_latency_buffer:
|
||||
"Shuffle between deployments within this % of the lowest latency. Default - 0 (i.e. always pick lowest latency).",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="max-w-3xl">
|
||||
<h3 className="text-sm font-medium text-gray-900">Latency-Based Configuration</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">Fine-tune latency-based routing behavior</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(routingStrategyArgs || defaultLowestLatencyArgs).map(([param, value]) => (
|
||||
<div key={param} className="space-y-2">
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
{param.replace(/_/g, " ")}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-0.5 mb-2">
|
||||
{paramExplanation[param] || ""}
|
||||
</p>
|
||||
<TextInput
|
||||
name={param}
|
||||
defaultValue={typeof value === "object" ? JSON.stringify(value, null, 2) : value?.toString()}
|
||||
className="font-mono text-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LatencyBasedConfiguration;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { TextInput } from "@tremor/react";
|
||||
|
||||
interface ReliabilityRetriesSectionProps {
|
||||
routerSettings: { [key: string]: any };
|
||||
routerFieldsMetadata: { [key: string]: any };
|
||||
}
|
||||
|
||||
const ReliabilityRetriesSection: React.FC<ReliabilityRetriesSectionProps> = ({
|
||||
routerSettings,
|
||||
routerFieldsMetadata,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="max-w-3xl">
|
||||
<h3 className="text-sm font-medium text-gray-900">Reliability & Retries</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">Configure retry logic and failure handling</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{Object.entries(routerSettings)
|
||||
.filter(
|
||||
([param, value]) =>
|
||||
param != "fallbacks" &&
|
||||
param != "context_window_fallbacks" &&
|
||||
param != "routing_strategy_args" &&
|
||||
param != "routing_strategy" &&
|
||||
param != "enable_tag_filtering",
|
||||
)
|
||||
.map(([param, value]) => (
|
||||
<div key={param} className="space-y-2">
|
||||
<label className="block">
|
||||
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
{routerFieldsMetadata[param]?.ui_field_name || param}
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-0.5 mb-2">
|
||||
{routerFieldsMetadata[param]?.field_description || ""}
|
||||
</p>
|
||||
<TextInput
|
||||
name={param}
|
||||
defaultValue={
|
||||
typeof value === "object" ? JSON.stringify(value, null, 2) : value?.toString() || ""
|
||||
}
|
||||
placeholder="—"
|
||||
className="font-mono text-sm w-full"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReliabilityRetriesSection;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { Select } from "antd";
|
||||
|
||||
interface RoutingStrategySelectorProps {
|
||||
selectedStrategy: string | null;
|
||||
availableStrategies: string[];
|
||||
routingStrategyDescriptions: { [key: string]: string };
|
||||
routerFieldsMetadata: { [key: string]: any };
|
||||
onStrategyChange: (strategy: string) => void;
|
||||
}
|
||||
|
||||
const RoutingStrategySelector: React.FC<RoutingStrategySelectorProps> = ({
|
||||
selectedStrategy,
|
||||
availableStrategies,
|
||||
routingStrategyDescriptions,
|
||||
routerFieldsMetadata,
|
||||
onStrategyChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-2 max-w-3xl">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
{routerFieldsMetadata["routing_strategy"]?.ui_field_name || "Routing Strategy"}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-0.5 mb-2">
|
||||
{routerFieldsMetadata["routing_strategy"]?.field_description || ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="routing-strategy-select max-w-3xl">
|
||||
<Select
|
||||
value={selectedStrategy}
|
||||
onChange={onStrategyChange}
|
||||
style={{ width: "100%" }}
|
||||
size="large"
|
||||
>
|
||||
{availableStrategies.map((strategy) => (
|
||||
<Select.Option key={strategy} value={strategy} label={strategy}>
|
||||
<div className="flex flex-col gap-0.5 py-1">
|
||||
<span className="font-mono text-sm font-medium">{strategy}</span>
|
||||
{routingStrategyDescriptions[strategy] && (
|
||||
<span className="text-xs text-gray-500 font-normal">
|
||||
{routingStrategyDescriptions[strategy]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoutingStrategySelector;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { Switch } from "@tremor/react";
|
||||
|
||||
interface TagFilteringToggleProps {
|
||||
enabled: boolean;
|
||||
routerFieldsMetadata: { [key: string]: any };
|
||||
onToggle: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const TagFilteringToggle: React.FC<TagFilteringToggleProps> = ({
|
||||
enabled,
|
||||
routerFieldsMetadata,
|
||||
onToggle,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-3 max-w-3xl">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<label className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
{routerFieldsMetadata["enable_tag_filtering"]?.ui_field_name || "Enable Tag Filtering"}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{routerFieldsMetadata["enable_tag_filtering"]?.field_description || ""}
|
||||
{routerFieldsMetadata["enable_tag_filtering"]?.link && (
|
||||
<>
|
||||
{" "}
|
||||
<a
|
||||
href={routerFieldsMetadata["enable_tag_filtering"].link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={onToggle}
|
||||
className="ml-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagFilteringToggle;
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Button,
|
||||
TextInput,
|
||||
} from "@tremor/react";
|
||||
import {
|
||||
getCallbacksCall,
|
||||
setCallbacksCall,
|
||||
getRouterSettingsCall,
|
||||
} from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import RoutingStrategySelector from "./RoutingStrategySelector";
|
||||
import TagFilteringToggle from "./TagFilteringToggle";
|
||||
import LatencyBasedConfiguration from "./LatencyBasedConfiguration";
|
||||
import ReliabilityRetriesSection from "./ReliabilityRetriesSection";
|
||||
|
||||
interface RouterSettingsProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
modelData: any;
|
||||
}
|
||||
|
||||
interface routingStrategyArgs {
|
||||
ttl?: number;
|
||||
lowest_latency_buffer?: number;
|
||||
}
|
||||
|
||||
const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole, userID, modelData }) => {
|
||||
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
|
||||
const [selectedStrategy, setSelectedStrategy] = useState<string | null>(null);
|
||||
const [availableRoutingStrategies, setAvailableRoutingStrategies] = useState<string[]>([]);
|
||||
const [routerFieldsMetadata, setRouterFieldsMetadata] = useState<{ [key: string]: any }>({});
|
||||
const [routingStrategyDescriptions, setRoutingStrategyDescriptions] = useState<{ [key: string]: string }>({});
|
||||
const [enableTagFiltering, setEnableTagFiltering] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || !userRole || !userID) {
|
||||
return;
|
||||
}
|
||||
getCallbacksCall(accessToken, userID, userRole).then((data) => {
|
||||
console.log("callbacks", data);
|
||||
let router_settings = data.router_settings;
|
||||
if ("model_group_retry_policy" in router_settings) {
|
||||
delete router_settings["model_group_retry_policy"];
|
||||
}
|
||||
setRouterSettings(router_settings);
|
||||
// Set initial selected strategy
|
||||
if (router_settings.routing_strategy) {
|
||||
setSelectedStrategy(router_settings.routing_strategy);
|
||||
}
|
||||
});
|
||||
getRouterSettingsCall(accessToken).then((data) => {
|
||||
console.log("router settings from API", data);
|
||||
if (data.fields) {
|
||||
// Build metadata map for easy lookup
|
||||
const fieldsMap: { [key: string]: any } = {};
|
||||
data.fields.forEach((field: any) => {
|
||||
fieldsMap[field.field_name] = {
|
||||
ui_field_name: field.ui_field_name,
|
||||
field_description: field.field_description,
|
||||
options: field.options,
|
||||
link: field.link,
|
||||
};
|
||||
});
|
||||
setRouterFieldsMetadata(fieldsMap);
|
||||
|
||||
// Extract routing strategies from the routing_strategy field's options
|
||||
const routingStrategyField = data.fields.find(
|
||||
(field: any) => field.field_name === "routing_strategy"
|
||||
);
|
||||
if (routingStrategyField?.options) {
|
||||
setAvailableRoutingStrategies(routingStrategyField.options);
|
||||
}
|
||||
|
||||
// Store routing strategy descriptions
|
||||
if (data.routing_strategy_descriptions) {
|
||||
setRoutingStrategyDescriptions(data.routing_strategy_descriptions);
|
||||
}
|
||||
|
||||
// Set enable_tag_filtering value
|
||||
const tagFilteringField = data.fields.find(
|
||||
(field: any) => field.field_name === "enable_tag_filtering"
|
||||
);
|
||||
if (tagFilteringField?.field_value !== null && tagFilteringField?.field_value !== undefined) {
|
||||
setEnableTagFiltering(tagFilteringField.field_value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [accessToken, userRole, userID]);
|
||||
|
||||
const handleSaveChanges = (router_settings: any) => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("router_settings", router_settings);
|
||||
|
||||
const numberKeys = new Set(["allowed_fails", "cooldown_time", "num_retries", "timeout", "retry_after"]);
|
||||
const jsonKeys = new Set(["model_group_alias", "retry_policy"]);
|
||||
|
||||
const parseInputValue = (key: string, raw: string | undefined, fallback: unknown) => {
|
||||
if (raw === undefined) return fallback;
|
||||
|
||||
const v = raw.trim();
|
||||
|
||||
if (v.toLowerCase() === "null") return null;
|
||||
|
||||
if (numberKeys.has(key)) {
|
||||
const n = Number(v);
|
||||
return Number.isNaN(n) ? fallback : n;
|
||||
}
|
||||
|
||||
if (jsonKeys.has(key)) {
|
||||
if (v === "") return null;
|
||||
try {
|
||||
return JSON.parse(v);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.toLowerCase() === "true") return true;
|
||||
if (v.toLowerCase() === "false") return false;
|
||||
|
||||
return v;
|
||||
};
|
||||
|
||||
// Add enable_tag_filtering to router_settings before processing
|
||||
const settingsToUpdate = {
|
||||
...router_settings,
|
||||
enable_tag_filtering: enableTagFiltering,
|
||||
};
|
||||
|
||||
const updatedVariables = Object.fromEntries(
|
||||
Object.entries(settingsToUpdate)
|
||||
.map(([key, value]) => {
|
||||
if (key !== "routing_strategy_args" && key !== "routing_strategy" && key !== "enable_tag_filtering") {
|
||||
const inputEl = document.querySelector(`input[name="${key}"]`) as HTMLInputElement | null;
|
||||
const parsed = parseInputValue(key, inputEl?.value, value);
|
||||
return [key, parsed];
|
||||
} else if (key === "routing_strategy") {
|
||||
return [key, selectedStrategy];
|
||||
} else if (key === "enable_tag_filtering") {
|
||||
return [key, enableTagFiltering];
|
||||
} else if (key === "routing_strategy_args" && selectedStrategy === "latency-based-routing") {
|
||||
let setRoutingStrategyArgs: routingStrategyArgs = {};
|
||||
|
||||
const lowestLatencyBufferElement = document.querySelector(
|
||||
`input[name="lowest_latency_buffer"]`,
|
||||
) as HTMLInputElement;
|
||||
const ttlElement = document.querySelector(`input[name="ttl"]`) as HTMLInputElement;
|
||||
|
||||
if (lowestLatencyBufferElement?.value) {
|
||||
setRoutingStrategyArgs["lowest_latency_buffer"] = Number(lowestLatencyBufferElement.value);
|
||||
}
|
||||
|
||||
if (ttlElement?.value) {
|
||||
setRoutingStrategyArgs["ttl"] = Number(ttlElement.value);
|
||||
}
|
||||
|
||||
console.log(`setRoutingStrategyArgs: ${setRoutingStrategyArgs}`);
|
||||
return ["routing_strategy_args", setRoutingStrategyArgs];
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((entry) => entry !== null && entry !== undefined) as Iterable<[string, unknown]>,
|
||||
);
|
||||
console.log("updatedVariables", updatedVariables);
|
||||
|
||||
const payload = {
|
||||
router_settings: updatedVariables,
|
||||
};
|
||||
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update router settings: " + error);
|
||||
}
|
||||
|
||||
NotificationsManager.success("router settings updated successfully");
|
||||
};
|
||||
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-8 py-2">
|
||||
{/* Routing Settings Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="max-w-3xl">
|
||||
<h3 className="text-sm font-medium text-gray-900">Routing Settings</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">Configure how requests are routed to deployments</p>
|
||||
</div>
|
||||
|
||||
{/* Routing Strategy */}
|
||||
{routerSettings.routing_strategy && (
|
||||
<RoutingStrategySelector
|
||||
selectedStrategy={selectedStrategy || routerSettings.routing_strategy}
|
||||
availableStrategies={availableRoutingStrategies}
|
||||
routingStrategyDescriptions={routingStrategyDescriptions}
|
||||
routerFieldsMetadata={routerFieldsMetadata}
|
||||
onStrategyChange={setSelectedStrategy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tag Filtering */}
|
||||
<TagFilteringToggle
|
||||
enabled={enableTagFiltering}
|
||||
routerFieldsMetadata={routerFieldsMetadata}
|
||||
onToggle={setEnableTagFiltering}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-gray-200" />
|
||||
|
||||
{/* Strategy-Specific Args - Show immediately after strategy if latency-based */}
|
||||
{selectedStrategy === "latency-based-routing" && (
|
||||
<LatencyBasedConfiguration
|
||||
routingStrategyArgs={routerSettings["routing_strategy_args"]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Other Settings */}
|
||||
<ReliabilityRetriesSection
|
||||
routerSettings={routerSettings}
|
||||
routerFieldsMetadata={routerFieldsMetadata}
|
||||
/>
|
||||
|
||||
{/* Actions - Sticky at bottom */}
|
||||
<div className="border-t border-gray-200 pt-6 flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => window.location.reload()}
|
||||
className="text-sm"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleSaveChanges(routerSettings)}
|
||||
className="text-sm font-medium"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouterSettings;
|
||||
|
||||
Reference in New Issue
Block a user