From 00fe75e83d3f14130fea44afd8160ae4e4abc670 Mon Sep 17 00:00:00 2001 From: moe-berri Date: Tue, 1 Sep 2026 15:31:35 -0700 Subject: [PATCH] feat(ui): configure capability routers --- .../app/(dashboard)/hooks/models/useModels.ts | 1 + .../AutoRouters/autoRouterRows.test.ts | 26 ++ .../components/AutoRouters/autoRouterRows.ts | 12 +- .../add_model/AutoRouterRoutingTest.tsx | 7 +- .../add_model/CapabilityRouterConfig.tsx | 151 ++++++++ .../add_model/add_auto_router_tab.tsx | 358 +++++++++++------- ...capability_router_tab.integration.test.tsx | 90 +++++ .../add_model/auto_router_strategies.ts | 16 +- .../build_auto_router_routing_test_request.ts | 9 +- .../capability_router_config.test.ts | 52 +++ .../add_model/capability_router_config.ts | 63 +++ .../handle_add_auto_router_submit.tsx | 25 +- .../edit_auto_router_modal.tsx | 228 +++++++---- .../src/components/networking.tsx | 13 +- .../LogDetailsDrawer/RoutingDecisionCard.tsx | 35 ++ 15 files changed, 861 insertions(+), 225 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/add_model/CapabilityRouterConfig.tsx create mode 100644 ui/litellm-dashboard/src/components/add_model/add_capability_router_tab.integration.test.tsx create mode 100644 ui/litellm-dashboard/src/components/add_model/capability_router_config.test.ts create mode 100644 ui/litellm-dashboard/src/components/add_model/capability_router_config.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts index a9f7c54698a..6a9627e1938 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts @@ -92,6 +92,7 @@ export interface AutoRouterDeployment extends AutoRouterCandidateDeployment { model?: string | null; complexity_router_config?: unknown; complexity_router_default_model?: string | null; + capability_router_config?: unknown; auto_router_config?: unknown; auto_router_default_model?: string | null; auto_router_embedding_model?: string | null; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts index 23585f6c110..f5400fb51c7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts @@ -43,6 +43,32 @@ const semanticDeployment = { }; describe("autoRouterRows", () => { + it("classifies a capability router and lists its candidate models", () => { + const row = toAutoRouterRow( + { + model_name: "cost-router", + litellm_params: { + model: "auto_router/capability_router", + capability_router_config: { + candidates: [ + { model: "small", description: "bounded tasks" }, + { model: "frontier", description: "hard tasks" }, + ], + }, + }, + model_info: { id: "cap-1", db_model: true }, + }, + 0, + ADMIN, + null, + ); + + expect(row.kind).toBe("capability"); + expect(row.typeLabel).toBe("Capability"); + expect(row.targets).toEqual(["small", "frontier"]); + expect(row.canEdit).toBe(true); + }); + it("classifies a complexity router and unions its tier models as targets", () => { const row = toAutoRouterRow(complexityDeployment, 0, ADMIN, null); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts index c4d7f45b7cc..fb587d6b5b5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts @@ -79,6 +79,14 @@ const configManaged = (label: string, config: Record): Presenta /** How each strategy renders itself, given its own config object. */ const PRESENTERS: Record) => Presentation> = { + capability: (config) => ({ + typeLabel: "Capability", + targets: dedupe( + (Array.isArray(config.candidates) ? config.candidates : []) + .map((candidate) => asRecord(candidate).model) + .filter((model): model is string => typeof model === "string" && model.length > 0), + ), + }), complexity: (config) => ({ typeLabel: complexityTypeLabel(config), targets: dedupe(Object.values(asRecord(config.tiers)).flatMap(normalizeTierModels)), @@ -116,7 +124,9 @@ export const toAutoRouterRow = ( canDelete: canDelete && mayActOnRow, editBlockedReason, createdAt: info.created_at ?? undefined, - defaultModel: (params[strategy.defaultModelKey] as string | null | undefined) ?? null, + defaultModel: strategy.defaultModelKey + ? (params[strategy.defaultModelKey] as string | null | undefined) ?? null + : null, deployment, ...PRESENTERS[strategy.kind](asRecord(params[strategy.configKey])), }; diff --git a/ui/litellm-dashboard/src/components/add_model/AutoRouterRoutingTest.tsx b/ui/litellm-dashboard/src/components/add_model/AutoRouterRoutingTest.tsx index 00b2e75dfd1..37852ae0229 100644 --- a/ui/litellm-dashboard/src/components/add_model/AutoRouterRoutingTest.tsx +++ b/ui/litellm-dashboard/src/components/add_model/AutoRouterRoutingTest.tsx @@ -7,10 +7,12 @@ import RoutingDecisionCard from "@/components/view_logs/LogDetailsDrawer/Routing import { AutoRouterRoutingTestResult, testAutoRouterRouting } from "../networking"; import { ComplexityRouterConfigPayload } from "./build_complexity_router_config"; import { buildAutoRouterRoutingTestRequest } from "./build_auto_router_routing_test_request"; +import { CapabilityRouterConfigValue } from "./capability_router_config"; interface AutoRouterRoutingTestProps { accessToken: string; - config: ComplexityRouterConfigPayload; + config?: ComplexityRouterConfigPayload; + capabilityConfig?: CapabilityRouterConfigValue; defaultModel: string | undefined; routerName: string | undefined; teamId: string | undefined; @@ -25,6 +27,7 @@ type TestState = const AutoRouterRoutingTest: React.FC = ({ accessToken, config, + capabilityConfig, defaultModel, routerName, teamId, @@ -34,7 +37,7 @@ const AutoRouterRoutingTest: React.FC = ({ const send = async () => { setState({ status: "running" }); - const params = { prompt, config, defaultModel, routerName, teamId }; + const params = { prompt, config, capabilityConfig, defaultModel, routerName, teamId }; const request = buildAutoRouterRoutingTestRequest(params); const response = await testAutoRouterRouting(accessToken, request); setState( diff --git a/ui/litellm-dashboard/src/components/add_model/CapabilityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/CapabilityRouterConfig.tsx new file mode 100644 index 00000000000..564077e239c --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/CapabilityRouterConfig.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import { Plus, Trash2 } from "lucide-react"; + +import { type ModelGroup } from "@/components/llm_calls/fetch_models"; +import { SearchSelect } from "@/components/shared/SearchSelect"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { type CapabilityRouterConfigValue } from "./capability_router_config"; + +interface Props { + modelInfo: ModelGroup[]; + value: CapabilityRouterConfigValue; + onChange: (value: CapabilityRouterConfigValue) => void; +} + +const CapabilityRouterConfig: React.FC = ({ modelInfo, value, onChange }) => { + const options = React.useMemo( + () => + Array.from(new Set(modelInfo.filter((model) => model.mode !== "embedding").map((model) => model.model_group))) + .filter((model) => !model.startsWith("auto_router/")) + .map((model) => ({ value: model, label: model })), + [modelInfo], + ); + + const updateCandidate = (index: number, patch: Partial) => { + const previousModel = value.candidates[index]?.model; + const candidates = value.candidates.map((candidate, row) => + row === index ? { ...candidate, ...patch } : candidate, + ); + const fallback_model = + previousModel === value.fallback_model && patch.model !== undefined ? patch.model : value.fallback_model; + onChange({ ...value, candidates, fallback_model }); + }; + + return ( +
+
+

Candidate models

+

+ Describe the tasks each model can reliably complete. Price is calculated separately. +

+
+ {value.candidates.map((candidate, index) => ( +
+
+ + +
+ + option.value === candidate.model || + !value.candidates.some((item, row) => row !== index && item.model === option.value), + )} + value={candidate.model} + onValueChange={(model) => updateCandidate(index, { model })} + placeholder="Select a model group" + emptyText="No models found" + /> +