From 087d82ffca88b2cd0f9f39a5b68120feaf3305dc Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 18 Aug 2026 16:19:23 -0700 Subject: [PATCH] refactor(ui): move the model info view and pass-through endpoint forms off tremor (#37308) * refactor(ui): move the model info view and pass-through endpoint forms off tremor The pass-through settings form's Save button relied on tremor's implicit submit, so it now carries an explicit type="submit" because the Base UI button defaults to type="button". The include-subpath switch inside the antd Form.Item is wired through onCheckedChange plus form.setFieldsValue, since the Base UI switch does not read the onChange antd injects. Both tab strips keep every panel mounted so the edit forms survive a tab switch, pinned by a new mount-contract test on the model info view. Also prunes the six tremor no-restricted-imports suppressions these files no longer need. * test(ui): pin the model info overview panel to its own dom node across tab switches * fix(ui): keep the line tab strip on the model info and pass-through views Both tab strips were bare tremor TabLists, which defaulted to the line variant, so the straight rename turned them into filled segmented pills. They now use the same full-width line strip the agent, guardrail and prompt info views ship. --- ui/litellm-dashboard/CLAUDE.md | 2 + ui/litellm-dashboard/eslint-suppressions.json | 17 +- .../src/components/add_pass_through.tsx | 44 ++-- .../PassThroughGuardrailsSection.tsx | 12 +- .../PassThroughSecuritySection.tsx | 14 +- .../check_openapi_schema.tsx | 10 +- .../src/components/model_info_view.test.tsx | 22 ++ .../src/components/model_info_view.tsx | 217 +++++++++--------- .../src/components/pass_through_info.tsx | 131 +++++------ ui/litellm-dashboard/tests/setupTests.ts | 9 + 10 files changed, 253 insertions(+), 225 deletions(-) diff --git a/ui/litellm-dashboard/CLAUDE.md b/ui/litellm-dashboard/CLAUDE.md index 9640784c21d..197e6d17fc6 100644 --- a/ui/litellm-dashboard/CLAUDE.md +++ b/ui/litellm-dashboard/CLAUDE.md @@ -19,3 +19,5 @@ Do not trust `eslint --fix` for these two plugins. Fixing the suite in bulk prod A test may reach for a component library's own CSS class only when that library exposes no role, label, title or ARIA state to query instead, and then the line carries a suppression naming the rule and the reason. Check first: antd icons render as `role="img"` with an `aria-label`, and antd `Form.Item` associates its label with the control, so both are reachable accessibly. When a label does not resolve, suspect the control rather than the test, since a custom wrapper that destructures props without spreading them drops the `id` antd injects and leaves the rendered label pointing at nothing Rules beyond the enabled set were measured against the whole suite and left off rather than recorded in a budget file, because a ceiling that permits a violation anywhere is worse than an honest gap. `no-node-access` and `no-container` are the ones worth revisiting first, since they catch the DOM archaeology the rules above only discourage. `prefer-implicit-assert` and `prefer-explicit-assert` contradict each other, so neither is enabled + +Never run the full unit suite (`npx vitest run` with no path). It is 380 files and thousands of tests, it saturates the machine for many minutes, and CI runs it anyway. Run only the test files your change touches, plus any file whose failure your change could plausibly explain, by passing explicit paths diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 4e9dcb810d0..be7da7c2b42 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1981,7 +1981,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 1 } }, "src/components/agent_management/AgentSelector.test.tsx": { @@ -2128,17 +2128,9 @@ }, "src/components/common_components/PassThroughGuardrailsSection.tsx": { "no-restricted-imports": { - "count": 2 - }, - "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/common_components/PassThroughSecuritySection.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "src/components/common_components/RateLimitTypeFormItem.test.tsx": { "no-restricted-imports": { "count": 1 @@ -2167,7 +2159,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 2 } }, "src/components/common_components/fetch_teams.tsx": { @@ -2395,7 +2387,7 @@ "count": 14 }, "no-restricted-imports": { - "count": 2 + "count": 1 }, "prefer-const": { "count": 5 @@ -2493,9 +2485,6 @@ "src/components/pass_through_info.tsx": { "local/filename-pascal-case": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/components/per_user_usage.tsx": { diff --git a/ui/litellm-dashboard/src/components/add_pass_through.tsx b/ui/litellm-dashboard/src/components/add_pass_through.tsx index 6d6fe4b0dc7..9f95e7f27ac 100644 --- a/ui/litellm-dashboard/src/components/add_pass_through.tsx +++ b/ui/litellm-dashboard/src/components/add_pass_through.tsx @@ -1,7 +1,6 @@ "use client"; import React, { useState } from "react"; -import { Card, Title, Subtitle } from "@tremor/react"; import { Modal, Alert } from "antd"; import { CircleHelp, Plug } from "lucide-react"; import { useWatch } from "react-hook-form"; @@ -18,6 +17,8 @@ import PassThroughSecuritySection from "./common_components/PassThroughSecurityS import PassThroughGuardrailsSection from "./common_components/PassThroughGuardrailsSection"; import { FormField } from "@/components/shared/form/FormField"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; @@ -164,11 +165,11 @@ const AddPassThroughEndpoint: React.FC = ({ />
- - Route Configuration - + +

Route Configuration

+

Configure how requests to your domain will be forwarded to the target API - +

= ({ - - Headers - + +

Headers

+

Add headers that will be sent with every request to the target API - +

= ({
- - Default Query Parameters - + +

Default Query Parameters

+

Add query parameters that will be automatically sent with every request to the target API - +

= ({ - - Performance - + +

Performance

+

Configure upstream request timeout for this endpoint - +

= ({
- - Billing - Optional cost tracking for this endpoint + +

Billing

+

Optional cost tracking for this endpoint

= ({ -
diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx index 21d013624fc..2c6856cb8b8 100644 --- a/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughGuardrailsSection.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Card, Title, Subtitle } from "@tremor/react"; import { Alert } from "antd"; import { CircleHelp } from "lucide-react"; @@ -7,6 +6,7 @@ import GuardrailSelector from "../guardrails/GuardrailSelector"; import { TagsInput } from "@/app/(dashboard)/guardrails/_components/content_filter/TagsInput"; import { Field, FieldLabel } from "@/components/shared/form/field"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; type GuardrailFields = { request_fields?: string[]; response_fields?: string[] }; @@ -60,12 +60,12 @@ const PassThroughGuardrailsSection: React.FC return ( - - Guardrails - + +

Guardrails

+

Configure guardrails to enforce policies on requests and responses. Guardrails are opt-in for passthrough endpoints. - +

💡 Tip: Leave empty to check entire payload
{selectedGuardrails.map((guardrailName) => ( - +
{guardrailName}
diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx index 226b5c7e284..e186e9cfa91 100644 --- a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Card, Title, Subtitle, Text } from "@tremor/react"; +import { Card } from "@/components/ui/card"; import { Switch } from "@/components/ui/switch"; export interface PassThroughSecuritySectionProps { @@ -15,11 +15,11 @@ const PassThroughSecuritySection: React.FC = ({ onAuthChange, }) => { return ( - - Security - + +

Security

+

When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key - +

{premiumUser ? ( ) : ( @@ -29,13 +29,13 @@ const PassThroughSecuritySection: React.FC = ({ Authentication (Premium)
- +

Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "} here . - +

)} diff --git a/ui/litellm-dashboard/src/components/common_components/check_openapi_schema.tsx b/ui/litellm-dashboard/src/components/common_components/check_openapi_schema.tsx index 9697ebad788..bbe6f4f46a0 100644 --- a/ui/litellm-dashboard/src/components/common_components/check_openapi_schema.tsx +++ b/ui/litellm-dashboard/src/components/common_components/check_openapi_schema.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Form, Input, InputNumber, Select } from "antd"; -import { TextInput } from "@tremor/react"; +import { Form, Input as AntdInput, InputNumber, Select } from "antd"; +import { Input } from "@/components/ui/input"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Tooltip } from "antd"; import { getOpenAPISchema } from "../networking"; @@ -186,7 +186,7 @@ const SchemaFormFields: React.FC = ({ let inputComponent; if (isJSONField(key, property)) { - inputComponent = ; + inputComponent = ; } else if (property.enum) { inputComponent = ( ; } else { - inputComponent = ; + inputComponent = ; } return ( diff --git a/ui/litellm-dashboard/src/components/model_info_view.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.test.tsx index 1f6b783d1b0..7dfa3ffb9e5 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -376,6 +376,28 @@ describe("ModelInfoView", () => { }); }); + it("keeps the edit form and its touched fields alive across a tab switch", async () => { + const user = userEvent.setup(); + render(, { wrapper }); + + await user.click(await screen.findByRole("button", { name: /edit settings/i })); + const costInput = screen.getByPlaceholderText("Enter input cost") as HTMLInputElement; + await user.clear(costInput); + await user.type(costInput, "5"); + + await user.click(screen.getByRole("tab", { name: /raw json/i })); + await user.click(screen.getByRole("tab", { name: /overview/i })); + + expect(screen.getByPlaceholderText("Enter input cost")).toBe(costInput); + expect(Number(costInput.value)).toBe(5); + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => { + expect(mockModelPatchUpdateCall).toHaveBeenCalled(); + }); + expect(mockModelPatchUpdateCall.mock.calls[0][1].litellm_params.input_cost_per_token).toBeCloseTo(5 / 1_000_000); + }); + it("should display model information in overview tab", async () => { render(, { wrapper }); await waitFor(() => { diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index b5f68677aeb..633c811d6c8 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -3,21 +3,13 @@ import { useModelHub, useModelsInfo } from "@/app/(dashboard)/hooks/models/useMo import { useQueryClient } from "@tanstack/react-query"; import { transformModelData } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { ArrowLeftIcon, KeyIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline"; -import { - Card, - Grid, - Tab, - TabGroup, - TabList, - TabPanel, - TabPanels, - Text, - TextInput, - Title, - Button as TremorButton, -} from "@tremor/react"; -import { Button, DatePicker, Form, Input, Modal, Select, Tooltip } from "antd"; +import { KeyIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { Button as AntdButton, DatePicker, Form, Input as AntdInput, Modal, Select, Tooltip } from "antd"; import { formatPtuUtcDisplay, utcIsoToPickerValue } from "../utils/ptuDatetime"; import { applyPtuModelInfo } from "../utils/ptuModelInfo"; import { usePtuCostAttributionEnabled } from "@/app/(dashboard)/hooks/uiSettings/usePtuCostAttributionEnabled"; @@ -34,7 +26,7 @@ import { PTU_START_FIELD, } from "../utils/ptuValidation"; import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; -import { CheckIcon, CopyIcon } from "lucide-react"; +import { ArrowLeft, CheckIcon, CopyIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils"; import { isMaskedSecret, stripMaskedSecrets } from "../utils/maskedSecretUtils"; @@ -558,10 +550,11 @@ export default function ModelInfoView({ if (isLoadingModel) { return (
- + +

Loading...

); } @@ -570,10 +563,11 @@ export default function ModelInfoView({ if (!modelData) { return (
- + +

Model not found

); } @@ -672,16 +666,18 @@ export default function ModelInfoView({
- + +

Public Model Name: {getDisplayModelName(modelData)}

- {modelData.model_info.id} -
{(!isAnyAutoRouter || isComplexityRouterModel) && ( - + )} {!isAnyAutoRouter && ( <> - + - + )} - +
- - - Overview - Raw JSON - + + + + Overview + + + Raw JSON + + - - +
+ {/* Overview Grid */} - - - Provider +
+ +

Provider

{modelData.provider && } - {modelData.provider || "Not Set"} +

{modelData.provider || "Not Set"}

- - LiteLLM Model + +

LiteLLM Model

@@ -766,14 +766,14 @@ export default function ModelInfoView({
- - Pricing + +

Pricing

- Input: ${modelData.input_cost}/1M tokens - Output: ${modelData.output_cost}/1M tokens +

Input: ${modelData.input_cost}/1M tokens

+

Output: ${modelData.output_cost}/1M tokens

- +
{/* Audit info shown as a subtle banner below the overview */}
@@ -809,20 +809,20 @@ export default function ModelInfoView({
{/* Settings Card */} - +
- Model Settings +

Model Settings

{isAutoRouterModel && canEditModel && !isEditing && ( - setIsAutoRouterModalOpen(true)} className="flex items-center"> + )} {canEditModel ? ( !isEditing && ( - setIsEditing(true)} className="flex items-center"> + ) ) : ( @@ -906,10 +906,10 @@ export default function ModelInfoView({
- Model Name +

Model Name

{isEditing ? ( - + ) : (
{localModelData.model_name}
@@ -917,10 +917,10 @@ export default function ModelInfoView({
- LiteLLM Model Name +

LiteLLM Model Name

{isEditing ? ( - + ) : (
{localModelData.litellm_model_name}
@@ -928,7 +928,7 @@ export default function ModelInfoView({
- Input Cost (per 1M tokens) +

Input Cost (per 1M tokens)

{isEditing ? (
- Output Cost (per 1M tokens) +

Output Cost (per 1M tokens)

{isEditing ? ( - {label} +

{label}

{isEditing ? ( - Cache Read Cost (per 1M tokens) +

Cache Read Cost (per 1M tokens)

{isEditing ? (
- Cache Write Cost (per 1M tokens) +

Cache Write Cost (per 1M tokens)

{isEditing ? (
- API Base +

API Base

{isEditing ? ( - + ) : (
@@ -1076,10 +1076,10 @@ export default function ModelInfoView({
- Custom LLM Provider +

Custom LLM Provider

{isEditing ? ( - + ) : (
@@ -1089,10 +1089,10 @@ export default function ModelInfoView({
- Organization +

Organization

{isEditing ? ( - + ) : (
@@ -1102,7 +1102,7 @@ export default function ModelInfoView({
- TPM (Tokens per Minute) +

TPM (Tokens per Minute)

{isEditing ? ( @@ -1115,7 +1115,7 @@ export default function ModelInfoView({
- RPM (Requests per Minute) +

RPM (Requests per Minute)

{isEditing ? ( @@ -1128,7 +1128,7 @@ export default function ModelInfoView({
- Max Retries +

Max Retries

{isEditing ? ( @@ -1141,7 +1141,7 @@ export default function ModelInfoView({
- Timeout (seconds) +

Timeout (seconds)

{isEditing ? ( @@ -1154,7 +1154,7 @@ export default function ModelInfoView({
- Stream Timeout (seconds) +

Stream Timeout (seconds)

{isEditing ? ( @@ -1167,7 +1167,7 @@ export default function ModelInfoView({
- Model Access Groups +

Model Access Groups

{isEditing ? (
- +

Attached Knowledge Bases (RAG) - +

{isEditing ? (
- Tags +

Tags

{isEditing ? ( - Health Check Model +

Health Check Model

{isEditing ? (