Merge pull request #19686 from BerriAI/litellm_key_team_create_routing_setting_ui

[Feature] UI - Create Team and Key Router Settings
This commit is contained in:
yuneng-jiang 2026-01-23 17:26:13 -08:00 • committed by GitHub
commit b5dfb57073
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 629 additions and 111 deletions

View file

@ -2,17 +2,6 @@
import PoliciesPanel from "@/components/policies";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import {
getPoliciesList,
createPolicyCall,
updatePolicyCall,
deletePolicyCall,
getPolicyInfo,
getPolicyAttachmentsList,
createPolicyAttachmentCall,
deletePolicyAttachmentCall,
getGuardrailsList,
} from "@/components/networking";
const PoliciesPage = () => {
const { accessToken, userRole } = useAuthorized();
@ -21,15 +10,6 @@ const PoliciesPage = () => {
<PoliciesPanel
accessToken={accessToken}
userRole={userRole}
getPoliciesList={getPoliciesList}
createPolicy={createPolicyCall}
updatePolicy={updatePolicyCall}
deletePolicy={deletePolicyCall}
getPolicy={getPolicyInfo}
getAttachmentsList={getPolicyAttachmentsList}
createAttachment={createPolicyAttachmentCall}
deleteAttachment={deletePolicyAttachmentCall}
getGuardrailsList={getGuardrailsList}
/>
);
};

View file

@ -42,6 +42,7 @@ import AgentSelector from "./agent_management/AgentSelector";
import { fetchTeams } from "./common_components/fetch_teams";
import ModelAliasManager from "./common_components/ModelAliasManager";
import PremiumLoggingSettings from "./common_components/PremiumLoggingSettings";
import RouterSettingsAccordion, { RouterSettingsAccordionValue } from "./common_components/RouterSettingsAccordion";
import {
fetchAvailableModelsForTeamOrKey,
getModelDisplayName,
@ -227,6 +228,8 @@ const Teams: React.FC<TeamProps> = ({
const [mcpAccessGroups, setMcpAccessGroups] = useState<string[]>([]);
const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
const [routerSettings, setRouterSettings] = useState<RouterSettingsAccordionValue | null>(null);
const [routerSettingsKey, setRouterSettingsKey] = useState<number>(0);
useEffect(() => {
console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
@ -317,6 +320,8 @@ const Teams: React.FC<TeamProps> = ({
form.resetFields();
setLoggingSettings([]);
setModelAliases({});
setRouterSettings(null);
setRouterSettingsKey((prev) => prev + 1);
};
const handleMemberOk = () => {
@ -330,6 +335,8 @@ const Teams: React.FC<TeamProps> = ({
form.resetFields();
setLoggingSettings([]);
setModelAliases({});
setRouterSettings(null);
setRouterSettingsKey((prev) => prev + 1);
};
const handleMemberCancel = () => {
@ -503,6 +510,17 @@ const Teams: React.FC<TeamProps> = ({
formValues.model_aliases = modelAliases;
}
// Add router_settings if any are defined
if (routerSettings?.router_settings) {
// Only include router_settings if it has at least one non-null value
const hasValues = Object.values(routerSettings.router_settings).some(
(value) => value !== null && value !== undefined && value !== "",
);
if (hasValues) {
formValues.router_settings = routerSettings.router_settings;
}
}
const response: any = await teamCreateCall(accessToken, formValues);
if (teams !== null) {
setTeams([...teams, response]);
@ -514,6 +532,8 @@ const Teams: React.FC<TeamProps> = ({
form.resetFields();
setLoggingSettings([]);
setModelAliases({});
setRouterSettings(null);
setRouterSettingsKey((prev) => prev + 1);
setIsTeamModalVisible(false);
}
} catch (error) {
@ -1428,6 +1448,23 @@ const Teams: React.FC<TeamProps> = ({
</AccordionBody>
</Accordion>
<Accordion key={`router-settings-accordion-${routerSettingsKey}`} className="mt-8 mb-8">
<AccordionHeader>
<b>Router Settings</b>
</AccordionHeader>
<AccordionBody>
<div className="mt-4 w-full">
<RouterSettingsAccordion
key={routerSettingsKey}
accessToken={accessToken || ""}
value={routerSettings || undefined}
onChange={setRouterSettings}
modelData={userModels.length > 0 ? { data: userModels.map((model) => ({ model_name: model })) } : undefined}
/>
</div>
</AccordionBody>
</Accordion>
<Accordion className="mt-8 mb-8">
<AccordionHeader>
<b>Model Aliases</b>

View file

@ -1,7 +1,7 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import Fallbacks from "./fallbacks";
import Fallbacks from "./Fallbacks";
import * as networkingModule from "../../../networking";
import * as fetchModelsModule from "../../../playground/llm_calls/fetch_models";
@ -108,7 +108,7 @@ describe("Fallbacks", () => {
return buttons[buttons.length - 1];
}
const clickableElements = lastCell.querySelectorAll("[class*='cursor-pointer'], button");
return Array.from(clickableElements).find((el) =>
return Array.from(clickableElements).find((el) =>
el.className.includes("red") || el.className.includes("hover:text-red")
) || clickableElements[clickableElements.length - 1];
};
@ -202,7 +202,7 @@ describe("Fallbacks", () => {
await waitFor(() => {
expect(networkingModule.setCallbacksCall).toHaveBeenCalled();
const callArgs = networkingModule.setCallbacksCall.mock.calls[0];
const callArgs = (networkingModule.setCallbacksCall as any).mock.calls[0];
expect(callArgs[0]).toBe(mockAccessToken);
expect(callArgs[1].router_settings.fallbacks).toHaveLength(1);
});

View file

@ -0,0 +1,373 @@
import React, { useEffect, useState, useImperativeHandle, forwardRef, useRef } from "react";
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
import { getRouterSettingsCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "../router_settings/RouterSettingsForm";
import { Fallbacks } from "../Settings/RouterSettings/Fallbacks/AddFallbacks";
import { FallbackSelectionForm } from "../Settings/RouterSettings/Fallbacks/FallbackSelectionForm";
import { FallbackGroup } from "../Settings/RouterSettings/Fallbacks/FallbackGroupConfig";
import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models";
export interface RouterSettingsAccordionValue {
router_settings: {
routing_strategy?: string | null;
allowed_fails?: number | null;
cooldown_time?: number | null;
num_retries?: number | null;
timeout?: number | null;
retry_after?: number | null;
fallbacks?: Fallbacks | null;
context_window_fallbacks?: any | null;
retry_policy?: any | null;
model_group_alias?: { [key: string]: any } | null;
enable_tag_filtering?: boolean;
routing_strategy_args?: { [key: string]: any } | null;
};
}
interface RouterSettingsAccordionProps {
accessToken: string;
value?: RouterSettingsAccordionValue;
onChange?: (value: RouterSettingsAccordionValue) => void;
modelData?: any;
}
export interface RouterSettingsAccordionRef {
getValue: () => RouterSettingsAccordionValue;
}
const RouterSettingsAccordion = forwardRef<RouterSettingsAccordionRef, RouterSettingsAccordionProps>(
({ accessToken, value, onChange, modelData }, ref) => {
const [formValue, setFormValue] = useState<RouterSettingsFormValue>({
routerSettings: {},
selectedStrategy: null,
enableTagFiltering: false,
});
const [fallbacks, setFallbacks] = useState<Fallbacks>([]);
const [fallbackGroups, setFallbackGroups] = useState<FallbackGroup[]>([]);
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
const [availableRoutingStrategies, setAvailableRoutingStrategies] = useState<string[]>([]);
const [routerFieldsMetadata, setRouterFieldsMetadata] = useState<{ [key: string]: any }>({});
const [routingStrategyDescriptions, setRoutingStrategyDescriptions] = useState<{ [key: string]: string }>({});
const isInternalUpdateRef = useRef(false);
const lastInitializedValueRef = useRef<string | null>(null);
// Convert fallbacks format to groups format
const fallbacksToGroups = (fallbacks: Fallbacks): FallbackGroup[] => {
if (!fallbacks || fallbacks.length === 0) {
return [
{
id: "1",
primaryModel: null,
fallbackModels: [],
},
];
}
return fallbacks.map((entry, index) => {
const [primaryModel, fallbackModels] = Object.entries(entry)[0];
return {
id: (index + 1).toString(),
primaryModel: primaryModel || null,
fallbackModels: fallbackModels || [],
};
});
};
// Convert groups format to fallbacks format
const groupsToFallbacks = (groups: FallbackGroup[]): Fallbacks => {
return groups
.filter((g) => g.primaryModel && g.fallbackModels.length > 0)
.map((g) => ({
[g.primaryModel!]: g.fallbackModels,
}));
};
// Initialize from value prop if provided (only when value actually changes externally)
useEffect(() => {
// Create a stable key from the value to detect actual external changes
const valueKey = value?.router_settings
? JSON.stringify({
routing_strategy: value.router_settings.routing_strategy,
fallbacks: value.router_settings.fallbacks,
enable_tag_filtering: value.router_settings.enable_tag_filtering,
})
: null;
// Skip if this is an internal update (from our own onChange) and the value hasn't actually changed
if (isInternalUpdateRef.current && valueKey === lastInitializedValueRef.current) {
isInternalUpdateRef.current = false;
return;
}
// Reset the flag if value actually changed externally
if (isInternalUpdateRef.current && valueKey !== lastInitializedValueRef.current) {
isInternalUpdateRef.current = false;
}
// Only update if the value actually changed from an external source
if (valueKey === lastInitializedValueRef.current) {
return;
}
lastInitializedValueRef.current = valueKey;
if (value?.router_settings) {
const rs = value.router_settings;
const { fallbacks: _, ...routerSettingsWithoutFallbacks } = rs;
setFormValue({
routerSettings: routerSettingsWithoutFallbacks as { [key: string]: any },
selectedStrategy: rs.routing_strategy || null,
enableTagFiltering: rs.enable_tag_filtering ?? false,
});
const initialFallbacks = rs.fallbacks || [];
setFallbacks(initialFallbacks);
setFallbackGroups(fallbacksToGroups(initialFallbacks));
} else {
// Initialize with empty defaults if no value provided
setFormValue({
routerSettings: {},
selectedStrategy: null,
enableTagFiltering: false,
});
setFallbacks([]);
setFallbackGroups([
{
id: "1",
primaryModel: null,
fallbackModels: [],
},
]);
}
}, [value]);
// Fetch router settings metadata
useEffect(() => {
if (!accessToken) {
return;
}
getRouterSettingsCall(accessToken).then((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);
}
}
});
}, [accessToken]);
// Fetch available models for fallbacks
useEffect(() => {
if (!accessToken) {
return;
}
const loadModels = async () => {
try {
const uniqueModels = await fetchAvailableModels(accessToken);
setModelInfo(uniqueModels);
} catch (error) {
console.error("Error fetching model info for fallbacks:", error);
}
};
loadModels();
}, [accessToken]);
// Helper function to build router_settings from current state
const buildRouterSettings = (): RouterSettingsAccordionValue["router_settings"] => {
// Parse input values from DOM (similar to RouterSettings component)
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 || raw === null) return fallback;
const v = String(raw).trim();
if (v === "" || 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;
};
// Build router_settings object
const routerSettings = formValue.routerSettings;
const settingsToUpdate: { [key: string]: any } = {
...routerSettings,
enable_tag_filtering: formValue.enableTagFiltering,
routing_strategy: formValue.selectedStrategy,
fallbacks: fallbacks.length > 0 ? fallbacks : null,
};
// Parse values from DOM inputs for reliability/retry fields
const updatedVariables = Object.fromEntries(
Object.entries(settingsToUpdate)
.map(([key, value]) => {
if (
key !== "routing_strategy_args" &&
key !== "routing_strategy" &&
key !== "enable_tag_filtering" &&
key !== "fallbacks"
) {
const inputEl = document.querySelector(`input[name="${key}"]`) as HTMLInputElement | null;
if (inputEl && inputEl.value !== undefined && inputEl.value !== "") {
const parsed = parseInputValue(key, inputEl.value, value);
return [key, parsed];
}
return [key, value];
} else if (key === "routing_strategy") {
return [key, formValue.selectedStrategy];
} else if (key === "enable_tag_filtering") {
return [key, formValue.enableTagFiltering];
} else if (key === "fallbacks") {
return [key, fallbacks.length > 0 ? fallbacks : null];
} else if (key === "routing_strategy_args" && formValue.selectedStrategy === "latency-based-routing") {
const lowestLatencyBufferElement = document.querySelector(
`input[name="lowest_latency_buffer"]`,
) as HTMLInputElement;
const ttlElement = document.querySelector(`input[name="ttl"]`) as HTMLInputElement;
const routingStrategyArgs: { [key: string]: any } = {};
if (lowestLatencyBufferElement?.value) {
routingStrategyArgs["lowest_latency_buffer"] = Number(lowestLatencyBufferElement.value);
}
if (ttlElement?.value) {
routingStrategyArgs["ttl"] = Number(ttlElement.value);
}
return ["routing_strategy_args", Object.keys(routingStrategyArgs).length > 0 ? routingStrategyArgs : null];
}
return [key, value];
})
.filter((entry) => entry !== null && entry !== undefined) as Iterable<[string, unknown]>,
);
// Ensure all required fields are present with null defaults if not set
// Convert empty objects/undefined to null for proper type safety
const normalizeValue = (val: any, isNumber = false): any => {
if (val === undefined || val === null) return null;
if (typeof val === "object" && !Array.isArray(val) && Object.keys(val).length === 0) return null;
if (isNumber && (typeof val !== "number" || Number.isNaN(val))) return null;
return val;
};
return {
routing_strategy: normalizeValue(updatedVariables.routing_strategy) as string | null | undefined,
allowed_fails: normalizeValue(updatedVariables.allowed_fails, true) as number | null | undefined,
cooldown_time: normalizeValue(updatedVariables.cooldown_time, true) as number | null | undefined,
num_retries: normalizeValue(updatedVariables.num_retries, true) as number | null | undefined,
timeout: normalizeValue(updatedVariables.timeout, true) as number | null | undefined,
retry_after: normalizeValue(updatedVariables.retry_after, true) as number | null | undefined,
fallbacks: fallbacks.length > 0 ? fallbacks : null,
context_window_fallbacks: normalizeValue(updatedVariables.context_window_fallbacks),
retry_policy: normalizeValue(updatedVariables.retry_policy),
model_group_alias: normalizeValue(updatedVariables.model_group_alias),
enable_tag_filtering: formValue.enableTagFiltering,
routing_strategy_args: normalizeValue(updatedVariables.routing_strategy_args),
};
};
// Update parent when form values change (with debounce to avoid infinite loops)
useEffect(() => {
if (!onChange) {
return;
}
const timeoutId = setTimeout(() => {
isInternalUpdateRef.current = true;
const finalRouterSettings = buildRouterSettings();
onChange({
router_settings: finalRouterSettings,
});
}, 100);
return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formValue, fallbacks]);
const handleFallbackGroupsChange = (newGroups: FallbackGroup[]): void => {
setFallbackGroups(newGroups);
const newFallbacks = groupsToFallbacks(newGroups);
setFallbacks(newFallbacks);
};
const availableModels = Array.from(new Set(modelInfo.map((option) => option.model_group))).sort();
// Expose getValue method via ref
useImperativeHandle(ref, () => ({
getValue: () => {
return {
router_settings: buildRouterSettings(),
};
},
}));
if (!accessToken) {
return null;
}
return (
<div className="w-full">
<TabGroup className="w-full">
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
<Tab value="1">Loadbalancing</Tab>
<Tab value="2">Fallbacks</Tab>
</TabList>
<TabPanels className="px-8 py-6">
<TabPanel>
<RouterSettingsForm
value={formValue}
onChange={setFormValue}
routerFieldsMetadata={routerFieldsMetadata}
availableRoutingStrategies={availableRoutingStrategies}
routingStrategyDescriptions={routingStrategyDescriptions}
/>
</TabPanel>
<TabPanel>
<FallbackSelectionForm
groups={fallbackGroups}
onGroupsChange={handleFallbackGroupsChange}
availableModels={availableModels}
maxFallbacks={5}
maxGroups={5}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
);
});
RouterSettingsAccordion.displayName = "RouterSettingsAccordion";
export default RouterSettingsAccordion;

View file

@ -19,6 +19,7 @@ import ModelAliasManager from "../common_components/ModelAliasManager";
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
import PremiumLoggingSettings from "../common_components/PremiumLoggingSettings";
import RateLimitTypeFormItem from "../common_components/RateLimitTypeFormItem";
import RouterSettingsAccordion, { RouterSettingsAccordionValue } from "../common_components/RouterSettingsAccordion";
import TeamDropdown from "../common_components/team_dropdown";
import Createuser from "../create_user_button";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
@ -163,6 +164,8 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(false);
const [rotationInterval, setRotationInterval] = useState<string>("30d");
const [routerSettings, setRouterSettings] = useState<RouterSettingsAccordionValue | null>(null);
const [routerSettingsKey, setRouterSettingsKey] = useState<number>(0);
const handleOk = () => {
setIsModalVisible(false);
form.resetFields();
@ -172,6 +175,8 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
setModelAliases({});
setAutoRotationEnabled(false);
setRotationInterval("30d");
setRouterSettings(null);
setRouterSettingsKey((prev) => prev + 1);
};
const handleCancel = () => {
@ -185,6 +190,8 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
setModelAliases({});
setAutoRotationEnabled(false);
setRotationInterval("30d");
setRouterSettings(null);
setRouterSettingsKey((prev) => prev + 1);
};
useEffect(() => {
@ -383,6 +390,17 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
formValues.aliases = JSON.stringify(modelAliases);
}
// Add router_settings if any are defined
if (routerSettings?.router_settings) {
// Only include router_settings if it has at least one non-null value
const hasValues = Object.values(routerSettings.router_settings).some(
(value) => value !== null && value !== undefined && value !== "",
);
if (hasValues) {
formValues.router_settings = routerSettings.router_settings;
}
}
let response;
if (keyOwner === "service_account") {
response = await keyCreateServiceAccountCall(accessToken, formValues);
@ -1154,6 +1172,23 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
</Tooltip>
)}
<Accordion key={`router-settings-accordion-${routerSettingsKey}`} className="mt-4 mb-4">
<AccordionHeader>
<b>Router Settings</b>
</AccordionHeader>
<AccordionBody>
<div className="mt-4 w-full">
<RouterSettingsAccordion
key={routerSettingsKey}
accessToken={accessToken || ""}
value={routerSettings || undefined}
onChange={setRouterSettings}
modelData={userModels.length > 0 ? { data: userModels.map((model) => ({ model_name: model })) } : undefined}
/>
</div>
</AccordionBody>
</Accordion>
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<b>Model Aliases</b>

View file

@ -34,7 +34,8 @@ describe("CodeSnippets", () => {
selectedTags: [],
selectedVectorStores: [],
selectedGuardrails: [],
selectedMCPTools: [],
selectedPolicies: [],
selectedMCPServers: [],
selectedSdk: "openai" as const,
selectedVoice: "alloy",
};

View file

@ -619,6 +619,7 @@ export default function CompareUI({ accessToken, disabledPersonalKeyCreation }:
guardrails,
undefined,
undefined,
undefined,
(searchResults) => updateSearchResultsForComparison(prepared.id, searchResults),
useAdvancedParams ? prepared.temperature : undefined,
useAdvancedParams ? prepared.maxTokens : undefined,

View file

@ -95,7 +95,8 @@ describe("chat_completion", () => {
undefined, // traceId
undefined, // vector_store_ids
undefined, // guardrails
undefined, // selectedMCPTools
undefined, // policies
undefined, // selectedMCPServers
undefined, // onImageGenerated
undefined, // onSearchResults
0.7, // temperature
@ -157,6 +158,7 @@ describe("chat_completion", () => {
undefined, // traceId
undefined, // vector_store_ids
undefined, // guardrails
undefined, // policies
selectedMCPServers,
undefined, // onImageGenerated
undefined, // onSearchResults

View file

@ -102,21 +102,22 @@ describe("responses_api", () => {
mockUpdateTextUI,
"gpt-4",
"test-token",
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined, // tags
undefined, // signal
undefined, // onReasoningContent
undefined, // onTimingData
undefined, // onUsageData
undefined, // traceId
undefined, // vector_store_ids
undefined, // guardrails
undefined, // policies
selectedMCPServers,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined, // previousResponseId
undefined, // onResponseId
undefined, // onMCPEvent
undefined, // codeInterpreterEnabled
undefined, // onCodeInterpreterResult
undefined, // customBaseUrl
mcpServers,
mcpServerToolRestrictions,
);

View file

@ -2,8 +2,9 @@ import React, { useState, useEffect } from "react";
import { Modal, Form, Select, Radio, Divider, Typography } from "antd";
import { Button } from "@tremor/react";
import { Policy, PolicyAttachmentCreateRequest } from "./types";
import { createPolicyAttachmentCall, teamListCall, keyInfoCall } from "../networking";
import { teamListCall, keyInfoCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const { Text } = Typography;
@ -13,6 +14,7 @@ interface AddAttachmentFormProps {
onSuccess: () => void;
accessToken: string | null;
policies: Policy[];
createAttachment: (accessToken: string, attachmentData: any) => Promise<any>;
}
const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
@ -21,6 +23,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
onSuccess,
accessToken,
policies,
createAttachment,
}) => {
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
@ -29,6 +32,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
const [availableKeys, setAvailableKeys] = useState<string[]>([]);
const [isLoadingTeams, setIsLoadingTeams] = useState(false);
const [isLoadingKeys, setIsLoadingKeys] = useState(false);
const { userId, userRole } = useAuthorized();
useEffect(() => {
if (visible && accessToken) {
@ -43,7 +47,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
// Load teams
setIsLoadingTeams(true);
try {
const teamsResponse = await teamListCall(accessToken);
const teamsResponse = await teamListCall(accessToken, userId, userRole);
if (teamsResponse?.data) {
const teamAliases = teamsResponse.data
.map((t: any) => t.team_alias)
@ -59,7 +63,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
// Load keys
setIsLoadingKeys(true);
try {
const keysResponse = await keyInfoCall(accessToken, null, null);
const keysResponse = await keyInfoCall(accessToken, []);
if (keysResponse?.data) {
const keyAliases = keysResponse.data
.map((k: any) => k.key_alias)
@ -111,7 +115,7 @@ const AddAttachmentForm: React.FC<AddAttachmentFormProps> = ({
}
}
await createPolicyAttachmentCall(accessToken, data);
await createAttachment(accessToken, data);
NotificationsManager.success("Attachment created successfully");
resetForm();

View file

@ -3,8 +3,9 @@ import { Form, Select, Modal, Divider, Typography, Tag, Alert, Radio } from "ant
import { Button, TextInput, Textarea } from "@tremor/react";
import { Policy, PolicyCreateRequest, PolicyUpdateRequest } from "./types";
import { Guardrail } from "../guardrails/types";
import { createPolicyCall, updatePolicyCall, getResolvedGuardrails, modelAvailableCall } from "../networking";
import { getResolvedGuardrails, modelAvailableCall } from "../networking";
import NotificationsManager from "../molecules/notifications_manager";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
const { Text } = Typography;
const { Option } = Select;
@ -17,6 +18,8 @@ interface AddPolicyFormProps {
editingPolicy?: Policy | null;
existingPolicies: Policy[];
availableGuardrails: Guardrail[];
createPolicy: (accessToken: string, policyData: any) => Promise<any>;
updatePolicy: (accessToken: string, policyId: string, policyData: any) => Promise<any>;
}
const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
@ -27,6 +30,8 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
editingPolicy,
existingPolicies,
availableGuardrails,
createPolicy,
updatePolicy,
}) => {
const [form] = Form.useForm();
const [isSubmitting, setIsSubmitting] = useState(false);
@ -34,6 +39,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
const [isLoadingResolved, setIsLoadingResolved] = useState(false);
const [modelConditionType, setModelConditionType] = useState<"model" | "regex">("model");
const [availableModels, setAvailableModels] = useState<string[]>([]);
const { userId, userRole } = useAuthorized();
const isEditing = !!editingPolicy;
@ -43,7 +49,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
// Detect if it's a regex pattern (contains *, ., [, ], etc.)
const isRegex = modelCondition && /[.*+?^${}()|[\]\\]/.test(modelCondition);
setModelConditionType(isRegex ? "regex" : "model");
form.setFieldsValue({
policy_name: editingPolicy.policy_name,
description: editingPolicy.description,
@ -73,9 +79,9 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
const loadAvailableModels = async () => {
if (!accessToken) return;
try {
const response = await modelAvailableCall(accessToken, null, null, null);
const response = await modelAvailableCall(accessToken, userId, userRole);
if (response?.data) {
const models = response.data.map((m: any) => m.id || m.model_name).filter(Boolean);
setAvailableModels(models);
@ -87,7 +93,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
const loadResolvedGuardrails = async (policyId: string) => {
if (!accessToken) return;
setIsLoadingResolved(true);
try {
const data = await getResolvedGuardrails(accessToken, policyId);
@ -128,7 +134,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
const resolveParentGuardrails = (policy: Policy): string[] => {
let resolved = new Set<string>();
// If parent inherits, resolve recursively
if (policy.inherit) {
const grandparent = existingPolicies.find(p => p.policy_name === policy.inherit);
@ -188,10 +194,10 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
};
if (isEditing && editingPolicy) {
await updatePolicyCall(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
await updatePolicy(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest);
NotificationsManager.success("Policy updated successfully");
} else {
await createPolicyCall(accessToken, data as PolicyCreateRequest);
await createPolicy(accessToken, data as PolicyCreateRequest);
NotificationsManager.success("Policy created successfully");
}

View file

@ -14,6 +14,10 @@ import {
getPolicyAttachmentsList,
deletePolicyAttachmentCall,
getGuardrailsList,
getPolicyInfo,
createPolicyCall,
updatePolicyCall,
createPolicyAttachmentCall,
} from "../networking";
import {
Policy,
@ -194,6 +198,7 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
}}
accessToken={accessToken}
isAdmin={isAdmin}
getPolicy={getPolicyInfo}
/>
) : (
<PolicyTable
@ -217,6 +222,8 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
editingPolicy={editingPolicy}
existingPolicies={policiesList}
availableGuardrails={guardrailsList}
createPolicy={createPolicyCall}
updatePolicy={updatePolicyCall}
/>
<DeleteResourceModal
@ -259,6 +266,7 @@ const PoliciesPanel: React.FC<PoliciesPanelProps> = ({
onSuccess={handleAttachmentSuccess}
accessToken={accessToken}
policies={policiesList}
createAttachment={createPolicyAttachmentCall}
/>
</TabPanel>
</TabPanels>

View file

@ -3,7 +3,7 @@ import { Card, Badge, Button } from "@tremor/react";
import { ArrowLeftIcon, PencilIcon } from "@heroicons/react/outline";
import { Descriptions, Tag, Spin, Divider, Typography, Alert } from "antd";
import { Policy } from "./types";
import { getPolicyInfo, getResolvedGuardrails } from "../networking";
import { getResolvedGuardrails } from "../networking";
const { Title, Text } = Typography;
@ -13,6 +13,7 @@ interface PolicyInfoViewProps {
onEdit: (policy: Policy) => void;
accessToken: string | null;
isAdmin: boolean;
getPolicy: (accessToken: string, policyId: string) => Promise<any>;
}
const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
@ -21,6 +22,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
onEdit,
accessToken,
isAdmin,
getPolicy,
}) => {
const [policy, setPolicy] = useState<Policy | null>(null);
const [isLoading, setIsLoading] = useState(true);
@ -32,7 +34,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
setIsLoading(true);
try {
const data = await getPolicyInfo(accessToken, policyId);
const data = await getPolicy(accessToken, policyId);
setPolicy(data);
// Also fetch resolved guardrails
@ -50,7 +52,7 @@ const PolicyInfoView: React.FC<PolicyInfoViewProps> = ({
} finally {
setIsLoading(false);
}
}, [policyId, accessToken]);
}, [policyId, accessToken, getPolicy]);
useEffect(() => {
fetchPolicy();

View file

@ -1470,6 +1470,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
selectedTags: [],
selectedVectorStores: [],
selectedGuardrails: [],
selectedPolicies: [],
selectedMCPServers: [],
endpointType: getEndpointType(selectedModel.mode || "chat"),
selectedModel: selectedModel.model_group,
@ -1491,6 +1492,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
selectedTags: [],
selectedVectorStores: [],
selectedGuardrails: [],
selectedPolicies: [],
selectedMCPServers: [],
endpointType: getEndpointType(selectedModel.mode || "chat"),
selectedModel: selectedModel.model_group,

View file

@ -16,7 +16,7 @@ const ReliabilityRetriesSection: React.FC<ReliabilityRetriesSectionProps> = ({
<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(
@ -39,7 +39,11 @@ const ReliabilityRetriesSection: React.FC<ReliabilityRetriesSectionProps> = ({
<TextInput
name={param}
defaultValue={
typeof value === "object" ? JSON.stringify(value, null, 2) : value?.toString() || ""
value === null || value === undefined || value === "null"
? ""
: typeof value === "object"
? JSON.stringify(value, null, 2)
: value?.toString() || ""
}
placeholder="—"
className="font-mono text-sm w-full"

View file

@ -0,0 +1,84 @@
import React from "react";
import LatencyBasedConfiguration from "./LatencyBasedConfiguration";
import ReliabilityRetriesSection from "./ReliabilityRetriesSection";
import RoutingStrategySelector from "./RoutingStrategySelector";
import TagFilteringToggle from "./TagFilteringToggle";
export interface RouterSettingsFormValue {
routerSettings: { [key: string]: any };
selectedStrategy: string | null;
enableTagFiltering: boolean;
}
interface RouterSettingsFormProps {
value: RouterSettingsFormValue;
onChange: (value: RouterSettingsFormValue) => void;
routerFieldsMetadata: { [key: string]: any };
availableRoutingStrategies: string[];
routingStrategyDescriptions: { [key: string]: string };
}
const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
value,
onChange,
routerFieldsMetadata,
availableRoutingStrategies,
routingStrategyDescriptions,
}) => {
const handleStrategyChange = (strategy: string) => {
onChange({
...value,
selectedStrategy: strategy,
});
};
const handleTagFilteringToggle = (enabled: boolean) => {
onChange({
...value,
enableTagFiltering: enabled,
});
};
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 */}
{availableRoutingStrategies.length > 0 && (
<RoutingStrategySelector
selectedStrategy={value.selectedStrategy || value.routerSettings.routing_strategy || null}
availableStrategies={availableRoutingStrategies}
routingStrategyDescriptions={routingStrategyDescriptions}
routerFieldsMetadata={routerFieldsMetadata}
onStrategyChange={handleStrategyChange}
/>
)}
{/* Tag Filtering */}
<TagFilteringToggle
enabled={value.enableTagFiltering}
routerFieldsMetadata={routerFieldsMetadata}
onToggle={handleTagFilteringToggle}
/>
</div>
{/* Divider */}
<div className="border-t border-gray-200" />
{/* Strategy-Specific Args - Show immediately after strategy if latency-based */}
{value.selectedStrategy === "latency-based-routing" && (
<LatencyBasedConfiguration routingStrategyArgs={value.routerSettings["routing_strategy_args"]} />
)}
{/* Other Settings */}
<ReliabilityRetriesSection routerSettings={value.routerSettings} routerFieldsMetadata={routerFieldsMetadata} />
</div>
);
};
export default RouterSettingsForm;

View file

@ -2,10 +2,7 @@ import { Button } from "@tremor/react";
import React, { useEffect, useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
import LatencyBasedConfiguration from "./LatencyBasedConfiguration";
import ReliabilityRetriesSection from "./ReliabilityRetriesSection";
import RoutingStrategySelector from "./RoutingStrategySelector";
import TagFilteringToggle from "./TagFilteringToggle";
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
interface RouterSettingsProps {
accessToken: string | null;
@ -20,12 +17,14 @@ interface routingStrategyArgs {
}
const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole, userID, modelData }) => {
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
const [selectedStrategy, setSelectedStrategy] = useState<string | null>(null);
const [formValue, setFormValue] = useState<RouterSettingsFormValue>({
routerSettings: {},
selectedStrategy: null,
enableTagFiltering: false,
});
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) {
@ -37,11 +36,13 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
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);
}
const initialStrategy = router_settings.routing_strategy || null;
setFormValue((prev) => ({
...prev,
routerSettings: router_settings,
selectedStrategy: initialStrategy,
}));
});
getRouterSettingsCall(accessToken).then((data) => {
console.log("router settings from API", data);
@ -72,17 +73,21 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
// 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);
setFormValue((prev) => ({
...prev,
enableTagFiltering: tagFilteringField.field_value,
}));
}
}
});
}, [accessToken, userRole, userID]);
const handleSaveChanges = (router_settings: any) => {
const handleSaveChanges = () => {
if (!accessToken) {
return;
}
const router_settings = formValue.routerSettings;
console.log("router_settings", router_settings);
const numberKeys = new Set(["allowed_fails", "cooldown_time", "num_retries", "timeout", "retry_after"]);
@ -118,7 +123,7 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
// Add enable_tag_filtering to router_settings before processing
const settingsToUpdate = {
...router_settings,
enable_tag_filtering: enableTagFiltering,
enable_tag_filtering: formValue.enableTagFiltering,
};
const updatedVariables = Object.fromEntries(
@ -129,10 +134,10 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
const parsed = parseInputValue(key, inputEl?.value, value);
return [key, parsed];
} else if (key === "routing_strategy") {
return [key, selectedStrategy];
return [key, formValue.selectedStrategy];
} else if (key === "enable_tag_filtering") {
return [key, enableTagFiltering];
} else if (key === "routing_strategy_args" && selectedStrategy === "latency-based-routing") {
return [key, formValue.enableTagFiltering];
} else if (key === "routing_strategy_args" && formValue.selectedStrategy === "latency-based-routing") {
let setRoutingStrategyArgs: routingStrategyArgs = {};
const lowestLatencyBufferElement = document.querySelector(
@ -175,50 +180,21 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
}
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} />
<div className="w-full">
<RouterSettingsForm
value={formValue}
onChange={setFormValue}
routerFieldsMetadata={routerFieldsMetadata}
availableRoutingStrategies={availableRoutingStrategies}
routingStrategyDescriptions={routingStrategyDescriptions}
/>
{/* 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">
<Button size="sm" onClick={handleSaveChanges} className="text-sm font-medium">
Save Changes
</Button>
</div>
@ -227,3 +203,5 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
};
export default RouterSettings;
export { RouterSettingsForm };
export type { RouterSettingsFormValue };