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.
This commit is contained in:
ryan-crabbe-berri 2026-08-18 16:19:23 -07:00 committed by GitHub
parent 1a1467b9d6
commit 087d82ffca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 253 additions and 225 deletions

View file

@ -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

View file

@ -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": {

View file

@ -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<AddFallbacksProps> = ({
/>
<form onSubmit={form.handleSubmit(addPassThrough)} className="space-y-6">
<Card className="p-5">
<Title className="mb-2 text-lg font-semibold text-foreground">Route Configuration</Title>
<Subtitle className="mb-5 text-muted-foreground">
<Card className="block p-5">
<h3 className="mb-2 text-lg font-semibold text-foreground">Route Configuration</h3>
<p className="mb-5 text-sm text-muted-foreground">
Configure how requests to your domain will be forwarded to the target API
</Subtitle>
</p>
<div className="space-y-5">
<FormField
@ -252,11 +253,11 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
<RoutePreview pathValue={pathValue} targetValue={targetValue} includeSubpath={includeSubpath} />
<Card className="p-6">
<Title className="mb-2 text-lg font-semibold text-foreground">Headers</Title>
<Subtitle className="mb-6 text-muted-foreground">
<Card className="block p-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Headers</h3>
<p className="mb-6 text-sm text-muted-foreground">
Add headers that will be sent with every request to the target API
</Subtitle>
</p>
<FormField
control={form.control}
@ -278,11 +279,11 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
</FormField>
</Card>
<Card className="p-6">
<Title className="mb-2 text-lg font-semibold text-foreground">Default Query Parameters</Title>
<Subtitle className="mb-6 text-muted-foreground">
<Card className="block p-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Default Query Parameters</h3>
<p className="mb-6 text-sm text-muted-foreground">
Add query parameters that will be automatically sent with every request to the target API
</Subtitle>
</p>
<FormField
control={form.control}
@ -318,11 +319,11 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
<PassThroughGuardrailsSection accessToken={accessToken} value={guardrails} onChange={setGuardrails} />
<Card className="p-6">
<Title className="mb-2 text-lg font-semibold text-foreground">Performance</Title>
<Subtitle className="mb-6 text-muted-foreground">
<Card className="block p-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Performance</h3>
<p className="mb-6 text-sm text-muted-foreground">
Configure upstream request timeout for this endpoint
</Subtitle>
</p>
<FormField
control={form.control}
@ -348,9 +349,9 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
</FormField>
</Card>
<Card className="p-6">
<Title className="mb-2 text-lg font-semibold text-foreground">Billing</Title>
<Subtitle className="mb-6 text-muted-foreground">Optional cost tracking for this endpoint</Subtitle>
<Card className="block p-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Billing</h3>
<p className="mb-6 text-sm text-muted-foreground">Optional cost tracking for this endpoint</p>
<FormField
control={form.control}
@ -377,7 +378,8 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
<Button type="button" variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button type="submit" disabled={isLoading}>
<Button type="submit" disabled={isLoading} aria-busy={isLoading}>
{isLoading && <UiLoadingSpinner className="size-4" />}
{isLoading ? "Creating..." : "Add Pass-Through Endpoint"}
</Button>
</div>

View file

@ -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<PassThroughGuardrailsSectionProps>
return (
<TooltipProvider>
<Card className="p-6">
<Title className="mb-2 text-lg font-semibold text-foreground">Guardrails</Title>
<Subtitle className="mb-6 text-muted-foreground">
<Card className="block p-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Guardrails</h3>
<p className="mb-6 text-sm text-muted-foreground">
Configure guardrails to enforce policies on requests and responses. Guardrails are opt-in for passthrough
endpoints.
</Subtitle>
</p>
<Alert
message={
@ -128,7 +128,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
<div className="text-xs text-muted-foreground">💡 Tip: Leave empty to check entire payload</div>
</div>
{selectedGuardrails.map((guardrailName) => (
<Card key={guardrailName} className="bg-muted/50 p-4">
<Card key={guardrailName} className="block bg-muted/50 p-4">
<div className="mb-3 text-sm font-medium text-foreground">{guardrailName}</div>
<div className="space-y-3">
<Field>

View file

@ -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<PassThroughSecuritySectionProps> = ({
onAuthChange,
}) => {
return (
<Card className="p-6">
<Title className="mb-2 text-lg font-semibold text-foreground">Security</Title>
<Subtitle className="mb-4 text-muted-foreground">
<Card className="block p-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Security</h3>
<p className="mb-4 text-sm text-muted-foreground">
When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key
</Subtitle>
</p>
{premiumUser ? (
<Switch checked={authEnabled} onCheckedChange={onAuthChange} />
) : (
@ -29,13 +29,13 @@ const PassThroughSecuritySection: React.FC<PassThroughSecuritySectionProps> = ({
<span className="ml-2 text-sm text-muted-foreground">Authentication (Premium)</span>
</div>
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-900 dark:bg-yellow-950">
<Text className="text-sm text-yellow-800 dark:text-yellow-300">
<p className="text-sm text-yellow-800 dark:text-yellow-300">
Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
here
</a>
.
</Text>
</p>
</div>
</div>
)}

View file

@ -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<SchemaFormFieldsProps> = ({
let inputComponent;
if (isJSONField(key, property)) {
inputComponent = <Input.TextArea rows={4} placeholder="Enter as JSON" className="font-mono" />;
inputComponent = <AntdInput.TextArea rows={4} placeholder="Enter as JSON" className="font-mono" />;
} else if (property.enum) {
inputComponent = (
<Select>
@ -200,9 +200,9 @@ const SchemaFormFields: React.FC<SchemaFormFieldsProps> = ({
} else if (type === "number" || type === "integer") {
inputComponent = <InputNumber style={{ width: "100%" }} precision={type === "integer" ? 0 : undefined} />;
} else if (key === "duration") {
inputComponent = <TextInput placeholder="eg: 30s, 30h, 30d" />;
inputComponent = <Input placeholder="eg: 30s, 30h, 30d" />;
} else {
inputComponent = <TextInput placeholder={tooltip || ""} />;
inputComponent = <Input placeholder={tooltip || ""} />;
}
return (

View file

@ -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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { wrapper });
await waitFor(() => {

View file

@ -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 (
<div className="p-4">
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
<Button variant="ghost" onClick={onClose} className="mb-4">
<ArrowLeft className="size-4" />
Back to Models
</TremorButton>
<Text>Loading...</Text>
</Button>
<p className="text-sm">Loading...</p>
</div>
);
}
@ -570,10 +563,11 @@ export default function ModelInfoView({
if (!modelData) {
return (
<div className="p-4">
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
<Button variant="ghost" onClick={onClose} className="mb-4">
<ArrowLeft className="size-4" />
Back to Models
</TremorButton>
<Text>Model not found</Text>
</Button>
<p className="text-sm">Model not found</p>
</div>
);
}
@ -672,16 +666,18 @@ export default function ModelInfoView({
<div className="p-4">
<div className="flex justify-between items-center mb-6">
<div>
<TremorButton icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
<Button variant="ghost" onClick={onClose} className="mb-4">
<ArrowLeft className="size-4" />
Back to Models
</TremorButton>
<Title>Public Model Name: {getDisplayModelName(modelData)}</Title>
</Button>
<h2 className="text-xl font-semibold">Public Model Name: {getDisplayModelName(modelData)}</h2>
<div className="flex items-center cursor-pointer">
<Text className="text-gray-500 font-mono">{modelData.model_info.id}</Text>
<Button
<span className="text-sm text-gray-500 font-mono">{modelData.model_info.id}</span>
<AntdButton
type="text"
size="small"
icon={copiedStates["model-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
aria-label="Copy model ID"
onClick={() => copyToClipboard(modelData.model_info.id, "model-id")}
className={`left-2 z-10 transition-all duration-200 ${
copiedStates["model-id"]
@ -693,19 +689,19 @@ export default function ModelInfoView({
</div>
<div className="flex gap-2">
{(!isAnyAutoRouter || isComplexityRouterModel) && (
<Button
<AntdButton
icon={<RefreshIcon className="h-4 w-4" />}
onClick={handleTestConnection}
className="flex items-center gap-2"
data-testid="test-connection-button"
>
Test Connection
</Button>
</AntdButton>
)}
{!isAnyAutoRouter && (
<>
<Button
<AntdButton
icon={<KeyIcon className="h-4 w-4" />}
onClick={() => setIsUpdateCredentialsModalOpen(true)}
className="flex items-center"
@ -713,9 +709,9 @@ export default function ModelInfoView({
data-testid="update-api-key-button"
>
Update API Key
</Button>
</AntdButton>
<Button
<AntdButton
icon={<KeyIcon className="h-4 w-4" />}
onClick={() => setIsCredentialModalOpen(true)}
className="flex items-center"
@ -723,10 +719,10 @@ export default function ModelInfoView({
data-testid="reuse-credentials-button"
>
Re-use Credentials
</Button>
</AntdButton>
</>
)}
<Button
<AntdButton
danger
icon={<TrashIcon className="h-4 w-4" />}
onClick={() => setIsDeleteModalOpen(true)}
@ -735,29 +731,33 @@ export default function ModelInfoView({
data-testid="delete-model-button"
>
{deleteLabel}
</Button>
</AntdButton>
</div>
</div>
<TabGroup>
<TabList className="mb-6">
<Tab>Overview</Tab>
<Tab>Raw JSON</Tab>
</TabList>
<Tabs defaultValue="overview">
<TabsList variant="line" className="mb-6 h-auto w-full justify-start rounded-none border-b p-0">
<TabsTrigger value="overview" className="flex-none rounded-none px-4 py-2">
Overview
</TabsTrigger>
<TabsTrigger value="raw" className="flex-none rounded-none px-4 py-2">
Raw JSON
</TabsTrigger>
</TabsList>
<TabPanels>
<TabPanel>
<div>
<TabsContent value="overview" keepMounted>
{/* Overview Grid */}
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6 mb-6">
<Card>
<Text>Provider</Text>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
<Card className="block p-6">
<p className="text-sm">Provider</p>
<div className="mt-2 flex items-center space-x-2">
{modelData.provider && <Logo provider={modelData.provider} className="w-4 h-4" />}
<Title>{modelData.provider || "Not Set"}</Title>
<h3 className="text-lg font-medium">{modelData.provider || "Not Set"}</h3>
</div>
</Card>
<Card>
<Text>LiteLLM Model</Text>
<Card className="block p-6">
<p className="text-sm">LiteLLM Model</p>
<div className="mt-2 overflow-hidden">
<Tooltip title={modelData.litellm_model_name || "Not Set"}>
<div className="break-all text-sm font-medium leading-relaxed cursor-pointer">
@ -766,14 +766,14 @@ export default function ModelInfoView({
</Tooltip>
</div>
</Card>
<Card>
<Text>Pricing</Text>
<Card className="block p-6">
<p className="text-sm">Pricing</p>
<div className="mt-2">
<Text>Input: ${modelData.input_cost}/1M tokens</Text>
<Text>Output: ${modelData.output_cost}/1M tokens</Text>
<p className="text-sm">Input: ${modelData.input_cost}/1M tokens</p>
<p className="text-sm">Output: ${modelData.output_cost}/1M tokens</p>
</div>
</Card>
</Grid>
</div>
{/* Audit info shown as a subtle banner below the overview */}
<div className="mb-6 text-sm text-gray-500 flex items-center gap-x-6">
@ -809,20 +809,20 @@ export default function ModelInfoView({
</div>
{/* Settings Card */}
<Card>
<Card className="block p-6">
<div className="flex justify-between items-center mb-4">
<Title>Model Settings</Title>
<h3 className="text-lg font-medium">Model Settings</h3>
<div className="flex gap-2">
{isAutoRouterModel && canEditModel && !isEditing && (
<TremorButton onClick={() => setIsAutoRouterModalOpen(true)} className="flex items-center">
<Button onClick={() => setIsAutoRouterModalOpen(true)} className="flex items-center">
Edit Auto Router
</TremorButton>
</Button>
)}
{canEditModel ? (
!isEditing && (
<TremorButton onClick={() => setIsEditing(true)} className="flex items-center">
<Button onClick={() => setIsEditing(true)} className="flex items-center">
Edit Settings
</TremorButton>
</Button>
)
) : (
<Tooltip title="Only DB models can be edited. You must be an admin or the creator of the model to edit it.">
@ -906,10 +906,10 @@ export default function ModelInfoView({
<div className="space-y-4">
<div className="space-y-4">
<div>
<Text className="font-medium">Model Name</Text>
<p className="text-sm font-medium">Model Name</p>
{isEditing ? (
<Form.Item name="model_name" className="mb-0">
<TextInput placeholder="Enter model name" />
<Input placeholder="Enter model name" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded-sm">{localModelData.model_name}</div>
@ -917,10 +917,10 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">LiteLLM Model Name</Text>
<p className="text-sm font-medium">LiteLLM Model Name</p>
{isEditing ? (
<Form.Item name="litellm_model_name" className="mb-0">
<TextInput placeholder="Enter LiteLLM model name" />
<Input placeholder="Enter LiteLLM model name" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded-sm">{localModelData.litellm_model_name}</div>
@ -928,7 +928,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Input Cost (per 1M tokens)</Text>
<p className="text-sm font-medium">Input Cost (per 1M tokens)</p>
{isEditing ? (
<Form.Item
name="input_cost"
@ -950,7 +950,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Output Cost (per 1M tokens)</Text>
<p className="text-sm font-medium">Output Cost (per 1M tokens)</p>
{isEditing ? (
<Form.Item
name="output_cost"
@ -977,7 +977,7 @@ export default function ModelInfoView({
const { windowPeer, bound } = ptuField;
return (
<div key={name}>
<Text className="font-medium">{label}</Text>
<p className="text-sm font-medium">{label}</p>
{isEditing ? (
<Form.Item
name={name}
@ -1013,7 +1013,7 @@ export default function ModelInfoView({
})}
<div>
<Text className="font-medium">Cache Read Cost (per 1M tokens)</Text>
<p className="text-sm font-medium">Cache Read Cost (per 1M tokens)</p>
{isEditing ? (
<Form.Item
name="cache_read_cost"
@ -1038,7 +1038,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Cache Write Cost (per 1M tokens)</Text>
<p className="text-sm font-medium">Cache Write Cost (per 1M tokens)</p>
{isEditing ? (
<Form.Item
name="cache_write_cost"
@ -1063,10 +1063,10 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">API Base</Text>
<p className="text-sm font-medium">API Base</p>
{isEditing ? (
<Form.Item name="api_base" className="mb-0">
<TextInput placeholder="Enter API base" />
<Input placeholder="Enter API base" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded-sm">
@ -1076,10 +1076,10 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Custom LLM Provider</Text>
<p className="text-sm font-medium">Custom LLM Provider</p>
{isEditing ? (
<Form.Item name="custom_llm_provider" className="mb-0">
<TextInput placeholder="Enter custom LLM provider" />
<Input placeholder="Enter custom LLM provider" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded-sm">
@ -1089,10 +1089,10 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Organization</Text>
<p className="text-sm font-medium">Organization</p>
{isEditing ? (
<Form.Item name="organization" className="mb-0">
<TextInput placeholder="Enter organization" />
<Input placeholder="Enter organization" />
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded-sm">
@ -1102,7 +1102,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">TPM (Tokens per Minute)</Text>
<p className="text-sm font-medium">TPM (Tokens per Minute)</p>
{isEditing ? (
<Form.Item name="tpm" className="mb-0">
<NumericalInput placeholder="Enter TPM" />
@ -1115,7 +1115,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">RPM (Requests per Minute)</Text>
<p className="text-sm font-medium">RPM (Requests per Minute)</p>
{isEditing ? (
<Form.Item name="rpm" className="mb-0">
<NumericalInput placeholder="Enter RPM" />
@ -1128,7 +1128,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Max Retries</Text>
<p className="text-sm font-medium">Max Retries</p>
{isEditing ? (
<Form.Item name="max_retries" className="mb-0">
<NumericalInput placeholder="Enter max retries" />
@ -1141,7 +1141,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Timeout (seconds)</Text>
<p className="text-sm font-medium">Timeout (seconds)</p>
{isEditing ? (
<Form.Item name="timeout" className="mb-0">
<NumericalInput placeholder="Enter timeout" />
@ -1154,7 +1154,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Stream Timeout (seconds)</Text>
<p className="text-sm font-medium">Stream Timeout (seconds)</p>
{isEditing ? (
<Form.Item name="stream_timeout" className="mb-0">
<NumericalInput placeholder="Enter stream timeout" />
@ -1167,7 +1167,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Model Access Groups</Text>
<p className="text-sm font-medium">Model Access Groups</p>
{isEditing ? (
<Form.Item name="model_access_group" className="mb-0">
<Select
@ -1214,7 +1214,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">
<p className="text-sm font-medium">
Guardrails
<Tooltip title="Apply safety guardrails to this model to filter content or enforce policies">
<a
@ -1226,7 +1226,7 @@ export default function ModelInfoView({
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</Text>
</p>
{isEditing ? (
<Form.Item name="guardrails" className="mb-0">
<Select
@ -1275,7 +1275,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">
<p className="text-sm font-medium">
Attached Knowledge Bases (RAG)
<Tooltip title="Vector stores used for RAG. Every request to this model will automatically retrieve context from these knowledge bases.">
<a
@ -1287,7 +1287,7 @@ export default function ModelInfoView({
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</Text>
</p>
{isEditing ? (
<Form.Item name="vector_store_ids" className="mb-0">
<VectorStoreSelector
@ -1327,7 +1327,7 @@ export default function ModelInfoView({
</div>
<div>
<Text className="font-medium">Tags</Text>
<p className="text-sm font-medium">Tags</p>
{isEditing ? (
<Form.Item name="tags" className="mb-0">
<Select
@ -1374,7 +1374,7 @@ export default function ModelInfoView({
)}
</div>
<div>
<Text className="font-medium">Existing Credentials</Text>
<p className="text-sm font-medium">Existing Credentials</p>
{isEditing ? (
<Form.Item name="litellm_credential_name" className="mb-0">
<Select
@ -1403,7 +1403,7 @@ export default function ModelInfoView({
{isWildcardModel && (
<div>
<Text className="font-medium">Health Check Model</Text>
<p className="text-sm font-medium">Health Check Model</p>
{isEditing ? (
<Form.Item name="health_check_model" className="mb-0">
<Select
@ -1447,7 +1447,7 @@ export default function ModelInfoView({
/>
) : (
<div>
<Text className="font-medium">Cache Control</Text>
<p className="text-sm font-medium">Cache Control</p>
<div className="mt-1 p-2 bg-gray-50 rounded-sm">
{localModelData.litellm_params?.cache_control_injection_points ? (
<div>
@ -1471,10 +1471,10 @@ export default function ModelInfoView({
)}
<div>
<Text className="font-medium">Model Info</Text>
<p className="text-sm font-medium">Model Info</p>
{isEditing ? (
<Form.Item name="model_info" className="mb-0">
<Input.TextArea
<AntdInput.TextArea
rows={4}
placeholder='{"gpt-4": 100, "claude-v1": 200}'
defaultValue={JSON.stringify(modelData.model_info, null, 2)}
@ -1489,7 +1489,7 @@ export default function ModelInfoView({
)}
</div>
<div>
<Text className="font-medium">
<p className="text-sm font-medium">
LiteLLM Params
<Tooltip title="Optional litellm params used for making a litellm.completion() call. Some params are automatically added by LiteLLM.">
<a
@ -1501,10 +1501,10 @@ export default function ModelInfoView({
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</Text>
</p>
{isEditing ? (
<Form.Item name="litellm_extra_params" rules={[{ validator: formItemValidateJSON }]}>
<Input.TextArea
<AntdInput.TextArea
rows={4}
placeholder='{
"rpm": 100,
@ -1522,7 +1522,7 @@ export default function ModelInfoView({
)}
</div>
<div>
<Text className="font-medium">Team ID</Text>
<p className="text-sm font-medium">Team ID</p>
<div className="mt-1 p-2 bg-gray-50 rounded-sm">
{modelData.model_info.team_id || "Not Set"}
</div>
@ -1531,7 +1531,7 @@ export default function ModelInfoView({
{isEditing && (
<div className="mt-6 flex justify-end gap-2">
<TremorButton
<Button
variant="secondary"
onClick={() => {
form.resetFields();
@ -1541,29 +1541,30 @@ export default function ModelInfoView({
disabled={isSaving}
>
Cancel
</TremorButton>
<TremorButton variant="primary" onClick={() => form.submit()} loading={isSaving}>
</Button>
<Button onClick={() => form.submit()} disabled={isSaving} aria-busy={isSaving}>
{isSaving && <UiLoadingSpinner className="size-4" />}
Save Changes
</TremorButton>
</Button>
</div>
)}
</div>
</Form>
) : (
<Text>Loading...</Text>
<p className="text-sm">Loading...</p>
)}
</Card>
</TabPanel>
</TabsContent>
<TabPanel>
<Card>
<TabsContent value="raw" keepMounted>
<Card className="block p-6">
<pre className="bg-gray-100 p-4 rounded-sm text-xs overflow-auto">
{JSON.stringify(modelData, null, 2)}
</pre>
</Card>
</TabPanel>
</TabPanels>
</TabGroup>
</TabsContent>
</div>
</Tabs>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
@ -1608,7 +1609,7 @@ export default function ModelInfoView({
onCancel={() => setIsCredentialModalOpen(false)}
title="Using Existing Credential"
>
<Text>{modelData.litellm_params.litellm_credential_name}</Text>
<p className="text-sm">{modelData.litellm_params.litellm_credential_name}</p>
</Modal>
)}
@ -1639,9 +1640,9 @@ export default function ModelInfoView({
open={isAutoRouterTestModalOpen}
onCancel={() => setIsAutoRouterTestModalOpen(false)}
footer={[
<Button key="close" onClick={() => setIsAutoRouterTestModalOpen(false)}>
<AntdButton key="close" onClick={() => setIsAutoRouterTestModalOpen(false)}>
Close
</Button>,
</AntdButton>,
]}
width={700}
>

View file

@ -1,17 +1,4 @@
import React, { useState } from "react";
import {
Card,
Title,
Text,
Grid,
Badge,
Button as TremorButton,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
} from "@tremor/react";
import { updatePassThroughEndpoint, deletePassThroughEndpointsCall } from "./networking";
import { Eye, EyeOff } from "lucide-react";
import { useWatch } from "react-hook-form";
@ -21,11 +8,14 @@ import { toast } from "@/lib/toast";
import PassThroughSecuritySection from "./common_components/PassThroughSecuritySection";
import PassThroughGuardrailsSection from "./common_components/PassThroughGuardrailsSection";
import { FormField } from "@/components/shared/form/FormField";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { useZodForm } from "@/lib/forms/useZodForm";
@ -128,7 +118,12 @@ const PasswordField: React.FC<{ value: Record<string, any> }> = ({ value }) => {
<pre className="font-mono text-xs bg-gray-50 p-2 rounded-sm max-w-md overflow-auto">
{showPassword ? headerString : "••••••••"}
</pre>
<button onClick={() => setShowPassword(!showPassword)} className="p-1 hover:bg-gray-100 rounded-sm" type="button">
<button
onClick={() => setShowPassword(!showPassword)}
className="p-1 hover:bg-gray-100 rounded-sm"
type="button"
aria-label={showPassword ? "Hide headers" : "Show headers"}
>
{showPassword ? <EyeOff className="w-4 h-4 text-gray-500" /> : <Eye className="w-4 h-4 text-gray-500" />}
</button>
</div>
@ -243,54 +238,60 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
<Button onClick={onClose} className="mb-4">
Back
</Button>
<Title>Pass Through Endpoint: {endpointData.path}</Title>
<Text className="text-gray-500 font-mono">{endpointData.id}</Text>
<h2 className="text-xl font-semibold">Pass Through Endpoint: {endpointData.path}</h2>
<p className="text-sm text-gray-500 font-mono">{endpointData.id}</p>
</div>
</div>
<TabGroup>
<TabList className="mb-4">
<Tab key="overview">Overview</Tab>
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
</TabList>
<Tabs defaultValue="overview">
<TabsList variant="line" className="mb-4 h-auto w-full justify-start rounded-none border-b p-0">
<TabsTrigger value="overview" className="flex-none rounded-none px-4 py-2">
Overview
</TabsTrigger>
{isAdmin && (
<TabsTrigger value="settings" className="flex-none rounded-none px-4 py-2">
Settings
</TabsTrigger>
)}
</TabsList>
<TabPanels>
<div>
{/* Overview Panel */}
<TabPanel>
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Path</Text>
<TabsContent value="overview" keepMounted>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Card className="block p-6">
<p className="text-sm">Path</p>
<div className="mt-2">
<Title className="font-mono">{endpointData.path}</Title>
<h3 className="text-lg font-medium font-mono">{endpointData.path}</h3>
</div>
</Card>
<Card>
<Text>Target</Text>
<Card className="block p-6">
<p className="text-sm">Target</p>
<div className="mt-2">
<Title>{endpointData.target}</Title>
<h3 className="text-lg font-medium">{endpointData.target}</h3>
</div>
</Card>
<Card>
<Text>Configuration</Text>
<Card className="block p-6">
<p className="text-sm">Configuration</p>
<div className="mt-2 space-y-2">
<div>
<Badge color={endpointData.include_subpath ? "green" : "gray"}>
<Badge variant={endpointData.include_subpath ? "secondary" : "outline"}>
{endpointData.include_subpath ? "Include Subpath" : "Exact Path"}
</Badge>
</div>
<div>
<Badge color={endpointData.auth ? "blue" : "gray"}>
<Badge variant={endpointData.auth ? "secondary" : "outline"}>
{endpointData.auth ? "Auth Required" : "No Auth"}
</Badge>
</div>
{endpointData.methods && endpointData.methods.length > 0 && (
<div>
<Text className="text-xs text-gray-500">HTTP Methods:</Text>
<p className="text-xs text-gray-500">HTTP Methods:</p>
<div className="flex flex-wrap gap-1 mt-1">
{endpointData.methods.map((method) => (
<Badge key={method} color="indigo" size="sm">
<Badge key={method} variant="secondary">
{method}
</Badge>
))}
@ -299,17 +300,17 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
)}
{(!endpointData.methods || endpointData.methods.length === 0) && (
<div>
<Text className="text-xs text-gray-500">All HTTP methods supported</Text>
<p className="text-xs text-gray-500">All HTTP methods supported</p>
</div>
)}
{endpointData.cost_per_request !== undefined && (
<div>
<Text>Cost per request: ${endpointData.cost_per_request}</Text>
<p className="text-sm">Cost per request: ${endpointData.cost_per_request}</p>
</div>
)}
</div>
</Card>
</Grid>
</div>
{/* Route Preview Section */}
<div className="mt-6">
@ -321,10 +322,10 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</div>
{endpointData.headers && Object.keys(endpointData.headers).length > 0 && (
<Card className="mt-6">
<Card className="block mt-6 p-6">
<div className="flex justify-between items-center">
<Text className="font-medium">Headers</Text>
<Badge color="blue">{Object.keys(endpointData.headers).length} headers configured</Badge>
<p className="text-sm font-medium">Headers</p>
<Badge variant="secondary">{Object.keys(endpointData.headers).length} headers configured</Badge>
</div>
<div className="mt-4">
<PasswordField value={endpointData.headers} />
@ -333,10 +334,10 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
)}
{endpointData.guardrails && Object.keys(endpointData.guardrails).length > 0 && (
<Card className="mt-6">
<Card className="block mt-6 p-6">
<div className="flex justify-between items-center">
<Text className="font-medium">Guardrails</Text>
<Badge color="purple">{Object.keys(endpointData.guardrails).length} guardrails configured</Badge>
<p className="text-sm font-medium">Guardrails</p>
<Badge variant="secondary">{Object.keys(endpointData.guardrails).length} guardrails configured</Badge>
</div>
<div className="mt-4 space-y-2">
{Object.entries(endpointData.guardrails).map(([name, settings]) => (
@ -356,21 +357,21 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</div>
</Card>
)}
</TabPanel>
</TabsContent>
{/* Settings Panel (only for admins) */}
{isAdmin && (
<TabPanel>
<Card>
<TabsContent value="settings" keepMounted>
<Card className="block p-6">
<div className="flex justify-between items-center mb-4">
<Title>Pass Through Endpoint Settings</Title>
<h3 className="text-lg font-medium">Pass Through Endpoint Settings</h3>
<div className="space-x-2">
{!isEditing && (
<>
<TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>
<TremorButton onClick={handleDeleteEndpoint} variant="secondary" color="red">
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
<Button onClick={handleDeleteEndpoint} variant="destructive">
Delete Endpoint
</TremorButton>
</Button>
</>
)}
</div>
@ -493,37 +494,39 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
) : (
<div className="space-y-4">
<div>
<Text className="font-medium">Path</Text>
<p className="text-sm font-medium">Path</p>
<div className="font-mono">{endpointData.path}</div>
</div>
<div>
<Text className="font-medium">Target URL</Text>
<p className="text-sm font-medium">Target URL</p>
<div>{endpointData.target}</div>
</div>
<div>
<Text className="font-medium">Include Subpath</Text>
<Badge color={endpointData.include_subpath ? "green" : "gray"}>
<p className="text-sm font-medium">Include Subpath</p>
<Badge variant={endpointData.include_subpath ? "secondary" : "outline"}>
{endpointData.include_subpath ? "Yes" : "No"}
</Badge>
</div>
{endpointData.cost_per_request !== undefined && (
<div>
<Text className="font-medium">Cost per Request</Text>
<p className="text-sm font-medium">Cost per Request</p>
<div>${endpointData.cost_per_request}</div>
</div>
)}
{endpointData.timeout !== undefined && endpointData.timeout !== null && (
<div>
<Text className="font-medium">Request Timeout</Text>
<p className="text-sm font-medium">Request Timeout</p>
<div>{endpointData.timeout}s</div>
</div>
)}
<div>
<Text className="font-medium">Authentication Required</Text>
<Badge color={endpointData.auth ? "green" : "gray"}>{endpointData.auth ? "Yes" : "No"}</Badge>
<p className="text-sm font-medium">Authentication Required</p>
<Badge variant={endpointData.auth ? "secondary" : "outline"}>
{endpointData.auth ? "Yes" : "No"}
</Badge>
</div>
<div>
<Text className="font-medium">Headers</Text>
<p className="text-sm font-medium">Headers</p>
{endpointData.headers && Object.keys(endpointData.headers).length > 0 ? (
<div className="mt-2">
<PasswordField value={endpointData.headers} />
@ -535,10 +538,10 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</div>
)}
</Card>
</TabPanel>
</TabsContent>
)}
</TabPanels>
</TabGroup>
</div>
</Tabs>
</div>
);
};

View file

@ -214,6 +214,15 @@ if (typeof window !== "undefined") {
document.getAnimations = () => [];
}
// Base UI's ScrollAreaViewport calls viewport.getAnimations() from a timer, which jsdom
// does not implement, so the TypeError surfaces as an unhandled error and fails the run.
// BASE_UI_ANIMATIONS_DISABLED keeps useAnimationsFinished on the synchronous path it
// already took while getAnimations was missing, so popup unmount timing is unchanged.
(globalThis as { BASE_UI_ANIMATIONS_DISABLED?: boolean }).BASE_UI_ANIMATIONS_DISABLED = true;
if (!Element.prototype.getAnimations) {
Element.prototype.getAnimations = () => [];
}
// Stub URL.revokeObjectURL so vi.spyOn can intercept it in tests
if (!URL.revokeObjectURL) {
URL.revokeObjectURL = () => {};