diff --git a/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx index 86e063c7358..c1f6ec51d78 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/policies/page.tsx @@ -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 = () => { ); }; diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx index 1202cc91697..4f16c5cdd09 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.tsx @@ -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 = ({ const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({}); + const [routerSettings, setRouterSettings] = useState(null); + const [routerSettingsKey, setRouterSettingsKey] = useState(0); useEffect(() => { console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`); @@ -317,6 +320,8 @@ const Teams: React.FC = ({ form.resetFields(); setLoggingSettings([]); setModelAliases({}); + setRouterSettings(null); + setRouterSettingsKey((prev) => prev + 1); }; const handleMemberOk = () => { @@ -330,6 +335,8 @@ const Teams: React.FC = ({ form.resetFields(); setLoggingSettings([]); setModelAliases({}); + setRouterSettings(null); + setRouterSettingsKey((prev) => prev + 1); }; const handleMemberCancel = () => { @@ -503,6 +510,17 @@ const Teams: React.FC = ({ 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 = ({ form.resetFields(); setLoggingSettings([]); setModelAliases({}); + setRouterSettings(null); + setRouterSettingsKey((prev) => prev + 1); setIsTeamModalVisible(false); } } catch (error) { @@ -1428,6 +1448,23 @@ const Teams: React.FC = ({ + + + Router Settings + + +
+ 0 ? { data: userModels.map((model) => ({ model_name: model })) } : undefined} + /> +
+
+
+ Model Aliases diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx index bc6f6a4856c..8a7119aa4a3 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx @@ -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); }); diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx new file mode 100644 index 00000000000..1f691715d30 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx @@ -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( + ({ accessToken, value, onChange, modelData }, ref) => { + const [formValue, setFormValue] = useState({ + routerSettings: {}, + selectedStrategy: null, + enableTagFiltering: false, + }); + const [fallbacks, setFallbacks] = useState([]); + const [fallbackGroups, setFallbackGroups] = useState([]); + const [modelInfo, setModelInfo] = useState([]); + const [availableRoutingStrategies, setAvailableRoutingStrategies] = useState([]); + const [routerFieldsMetadata, setRouterFieldsMetadata] = useState<{ [key: string]: any }>({}); + const [routingStrategyDescriptions, setRoutingStrategyDescriptions] = useState<{ [key: string]: string }>({}); + const isInternalUpdateRef = useRef(false); + const lastInitializedValueRef = useRef(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 ( +
+ + + Loadbalancing + Fallbacks + + + + + + + + + + +
+ ); + }); + +RouterSettingsAccordion.displayName = "RouterSettingsAccordion"; + +export default RouterSettingsAccordion; diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index 90ed9a62cc0..6bfb3696606 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -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 = ({ team, teams, data, addKey }) => { const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({}); const [autoRotationEnabled, setAutoRotationEnabled] = useState(false); const [rotationInterval, setRotationInterval] = useState("30d"); + const [routerSettings, setRouterSettings] = useState(null); + const [routerSettingsKey, setRouterSettingsKey] = useState(0); const handleOk = () => { setIsModalVisible(false); form.resetFields(); @@ -172,6 +175,8 @@ const CreateKey: React.FC = ({ 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 = ({ team, teams, data, addKey }) => { setModelAliases({}); setAutoRotationEnabled(false); setRotationInterval("30d"); + setRouterSettings(null); + setRouterSettingsKey((prev) => prev + 1); }; useEffect(() => { @@ -383,6 +390,17 @@ const CreateKey: React.FC = ({ 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 = ({ team, teams, data, addKey }) => { )} + + + Router Settings + + +
+ 0 ? { data: userModels.map((model) => ({ model_name: model })) } : undefined} + /> +
+
+
+ Model Aliases diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx index 56c5fd404e3..0113f7b2832 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx +++ b/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx @@ -34,7 +34,8 @@ describe("CodeSnippets", () => { selectedTags: [], selectedVectorStores: [], selectedGuardrails: [], - selectedMCPTools: [], + selectedPolicies: [], + selectedMCPServers: [], selectedSdk: "openai" as const, selectedVoice: "alloy", }; diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx b/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx index 92bcb21c4b5..fa68851d690 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx +++ b/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx @@ -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, diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx index dd23e0591ea..8786e022013 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx @@ -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 diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.test.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.test.tsx index 569c9878243..f14ac52b840 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.test.tsx +++ b/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.test.tsx @@ -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, ); diff --git a/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx b/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx index e00f20fa163..cb4912b7f0b 100644 --- a/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx +++ b/ui/litellm-dashboard/src/components/policies/add_attachment_form.tsx @@ -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; } const AddAttachmentForm: React.FC = ({ @@ -21,6 +23,7 @@ const AddAttachmentForm: React.FC = ({ onSuccess, accessToken, policies, + createAttachment, }) => { const [form] = Form.useForm(); const [isSubmitting, setIsSubmitting] = useState(false); @@ -29,6 +32,7 @@ const AddAttachmentForm: React.FC = ({ const [availableKeys, setAvailableKeys] = useState([]); 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 = ({ // 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 = ({ // 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 = ({ } } - await createPolicyAttachmentCall(accessToken, data); + await createAttachment(accessToken, data); NotificationsManager.success("Attachment created successfully"); resetForm(); diff --git a/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx b/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx index 945c29931f1..0b4f83a1611 100644 --- a/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx +++ b/ui/litellm-dashboard/src/components/policies/add_policy_form.tsx @@ -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; + updatePolicy: (accessToken: string, policyId: string, policyData: any) => Promise; } const AddPolicyForm: React.FC = ({ @@ -27,6 +30,8 @@ const AddPolicyForm: React.FC = ({ editingPolicy, existingPolicies, availableGuardrails, + createPolicy, + updatePolicy, }) => { const [form] = Form.useForm(); const [isSubmitting, setIsSubmitting] = useState(false); @@ -34,6 +39,7 @@ const AddPolicyForm: React.FC = ({ const [isLoadingResolved, setIsLoadingResolved] = useState(false); const [modelConditionType, setModelConditionType] = useState<"model" | "regex">("model"); const [availableModels, setAvailableModels] = useState([]); + const { userId, userRole } = useAuthorized(); const isEditing = !!editingPolicy; @@ -43,7 +49,7 @@ const AddPolicyForm: React.FC = ({ // 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 = ({ 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 = ({ 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 = ({ const resolveParentGuardrails = (policy: Policy): string[] => { let resolved = new Set(); - + // 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 = ({ }; 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"); } diff --git a/ui/litellm-dashboard/src/components/policies/index.tsx b/ui/litellm-dashboard/src/components/policies/index.tsx index 5a137d704ad..4a1c2424890 100644 --- a/ui/litellm-dashboard/src/components/policies/index.tsx +++ b/ui/litellm-dashboard/src/components/policies/index.tsx @@ -14,6 +14,10 @@ import { getPolicyAttachmentsList, deletePolicyAttachmentCall, getGuardrailsList, + getPolicyInfo, + createPolicyCall, + updatePolicyCall, + createPolicyAttachmentCall, } from "../networking"; import { Policy, @@ -194,6 +198,7 @@ const PoliciesPanel: React.FC = ({ }} accessToken={accessToken} isAdmin={isAdmin} + getPolicy={getPolicyInfo} /> ) : ( = ({ editingPolicy={editingPolicy} existingPolicies={policiesList} availableGuardrails={guardrailsList} + createPolicy={createPolicyCall} + updatePolicy={updatePolicyCall} /> = ({ onSuccess={handleAttachmentSuccess} accessToken={accessToken} policies={policiesList} + createAttachment={createPolicyAttachmentCall} /> diff --git a/ui/litellm-dashboard/src/components/policies/policy_info.tsx b/ui/litellm-dashboard/src/components/policies/policy_info.tsx index a7ea4703ee5..1ef8aa0530d 100644 --- a/ui/litellm-dashboard/src/components/policies/policy_info.tsx +++ b/ui/litellm-dashboard/src/components/policies/policy_info.tsx @@ -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; } const PolicyInfoView: React.FC = ({ @@ -21,6 +22,7 @@ const PolicyInfoView: React.FC = ({ onEdit, accessToken, isAdmin, + getPolicy, }) => { const [policy, setPolicy] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -32,7 +34,7 @@ const PolicyInfoView: React.FC = ({ 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 = ({ } finally { setIsLoading(false); } - }, [policyId, accessToken]); + }, [policyId, accessToken, getPolicy]); useEffect(() => { fetchPolicy(); diff --git a/ui/litellm-dashboard/src/components/public_model_hub.tsx b/ui/litellm-dashboard/src/components/public_model_hub.tsx index 6f95904e9f8..e751f4d65b8 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.tsx @@ -1470,6 +1470,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded selectedTags: [], selectedVectorStores: [], selectedGuardrails: [], + selectedPolicies: [], selectedMCPServers: [], endpointType: getEndpointType(selectedModel.mode || "chat"), selectedModel: selectedModel.model_group, @@ -1491,6 +1492,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded selectedTags: [], selectedVectorStores: [], selectedGuardrails: [], + selectedPolicies: [], selectedMCPServers: [], endpointType: getEndpointType(selectedModel.mode || "chat"), selectedModel: selectedModel.model_group, diff --git a/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx b/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx index 6601950fdea..d217a45358b 100644 --- a/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx +++ b/ui/litellm-dashboard/src/components/router_settings/ReliabilityRetriesSection.tsx @@ -16,7 +16,7 @@ const ReliabilityRetriesSection: React.FC = ({

Reliability & Retries

Configure retry logic and failure handling

- +
{Object.entries(routerSettings) .filter( @@ -39,7 +39,11 @@ const ReliabilityRetriesSection: React.FC = ({ void; + routerFieldsMetadata: { [key: string]: any }; + availableRoutingStrategies: string[]; + routingStrategyDescriptions: { [key: string]: string }; +} + +const RouterSettingsForm: React.FC = ({ + value, + onChange, + routerFieldsMetadata, + availableRoutingStrategies, + routingStrategyDescriptions, +}) => { + const handleStrategyChange = (strategy: string) => { + onChange({ + ...value, + selectedStrategy: strategy, + }); + }; + + const handleTagFilteringToggle = (enabled: boolean) => { + onChange({ + ...value, + enableTagFiltering: enabled, + }); + }; + + return ( +
+ {/* Routing Settings Section */} +
+
+

Routing Settings

+

Configure how requests are routed to deployments

+
+ + {/* Routing Strategy */} + {availableRoutingStrategies.length > 0 && ( + + )} + + {/* Tag Filtering */} + +
+ + {/* Divider */} +
+ + {/* Strategy-Specific Args - Show immediately after strategy if latency-based */} + {value.selectedStrategy === "latency-based-routing" && ( + + )} + + {/* Other Settings */} + +
+ ); +}; + +export default RouterSettingsForm; diff --git a/ui/litellm-dashboard/src/components/router_settings/index.tsx b/ui/litellm-dashboard/src/components/router_settings/index.tsx index a94860eacd1..aaad5306832 100644 --- a/ui/litellm-dashboard/src/components/router_settings/index.tsx +++ b/ui/litellm-dashboard/src/components/router_settings/index.tsx @@ -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 = ({ accessToken, userRole, userID, modelData }) => { - const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({}); - const [selectedStrategy, setSelectedStrategy] = useState(null); + const [formValue, setFormValue] = useState({ + routerSettings: {}, + selectedStrategy: null, + enableTagFiltering: false, + }); const [availableRoutingStrategies, setAvailableRoutingStrategies] = useState([]); const [routerFieldsMetadata, setRouterFieldsMetadata] = useState<{ [key: string]: any }>({}); const [routingStrategyDescriptions, setRoutingStrategyDescriptions] = useState<{ [key: string]: string }>({}); - const [enableTagFiltering, setEnableTagFiltering] = useState(false); useEffect(() => { if (!accessToken || !userRole || !userID) { @@ -37,11 +36,13 @@ const RouterSettings: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ accessToken, userRole, } return ( -
- {/* Routing Settings Section */} -
-
-

Routing Settings

-

Configure how requests are routed to deployments

-
- - {/* Routing Strategy */} - {routerSettings.routing_strategy && ( - - )} - - {/* Tag Filtering */} - -
- - {/* Divider */} -
- - {/* Strategy-Specific Args - Show immediately after strategy if latency-based */} - {selectedStrategy === "latency-based-routing" && ( - - )} - - {/* Other Settings */} - +
+ {/* Actions - Sticky at bottom */}
-
@@ -227,3 +203,5 @@ const RouterSettings: React.FC = ({ accessToken, userRole, }; export default RouterSettings; +export { RouterSettingsForm }; +export type { RouterSettingsFormValue };