feat(ui): give each Models + Endpoints tab its own path (#34327)

* feat(ui): give each Models + Endpoints tab its own path

* refactor(ui): decompose Models + Endpoints into per-tab pages with URL-driven detail

Dissolve the 488-line ModelsAndEndpointsView monolith into one page per tab
under the models-and-endpoints route, with a persistent layout owning the
header, cost banner, tab bar and refresh. Each tab page owns only its own
state; shared lists come from a small useModelDashboardData hook.

Replace the stateful model/team drill-in (setSelectedModelId/setSelectedTeamId
full-page takeover) with real URL navigation: ?model=<id> and ?team=<id> render
ModelInfoView/TeamInfoView from the layout, so a model or team detail view is
now shareable, bookmarkable and back-button friendly. Removes the empty
placeholder pages from the first commit.

Swap the tab bar off phased-out tremor onto antd Tabs.

* fix(ui): render model tab panels standalone instead of Tremor TabPanel

AllModelsTab, ModelRetrySettingsTab and PriceDataManagementTab rooted their
render in a Tremor <TabPanel>, which only renders inside a Tremor <TabGroup>.
After the decomposition these panels live under antd Tabs / as route pages with
no such ancestor, so All Models (and the other two) rendered blank. Root them in
a plain container instead.

The existing component tests mocked @tremor/react (stubbing TabPanel to render
children), which hid this; add a regression test that renders with real Tremor
and asserts the content is visible standalone.

Also type visibleSlugs/TAB_LABELS with the canonical ModelTabSlug so a tab added
without a matching label is a compile error.

* fix(ui): make model/team drill-in navigation work under the /ui static mount

The drill-in close (Back to Models) and open were no-ops: the dashboard is a
static export served under /ui, a prefix the Next router (basePath "") does not
know, so a router.push to the current pathname with only the query changed is
deduped and never re-renders. Drive the ?model=/?team= overlay via real browser
navigation (window.location) so open and close reliably work; verified live.

Also address review feedback: gate the tab-permission redirect on teams/uiSettings
having loaded so a team admin hard-loading /add is not bounced to the base before
their membership resolves, and memoize getProviderFromModel on modelCostMapData so
the health tab's provider labels refresh when the cost map loads.

* fix(ui): use window.location.replace for the tab-permission redirect

router.replace is unreliable under the /ui static mount (same class of Next-router
issue that broke the drill-in back button), so the forbidden-tab redirect could
fail to fire. Use window.location.replace, which keeps the no-history semantics of
a permission redirect and is deterministic. Redirect stays gated on teams/uiSettings
having loaded.

* fix(ui): drive model/team drill-in with history.pushState for client-side nav

Switch the ?model=/?team= overlay navigation from window.location.assign to
window.history.pushState, which Next's App Router observes. This keeps navigation
client-side (no full page reload, React Query cache preserved) while still working
for the same-path query-only change that router.push cannot do under the /ui static
mount. Open, close (Back to Models) and browser Back are all verified in the built
UI. Adds unit coverage for the open/close/read behavior.
This commit is contained in:
ryan-crabbe-berri 2026-07-22 21:39:33 -07:00 • committed by GitHub
parent ba86889f11
commit 0b0d59d62e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 912 additions and 715 deletions

View file

@ -1,212 +0,0 @@
/* @vitest-environment jsdom */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, fireEvent, render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ModelsAndEndpointsView from "./ModelsAndEndpointsView";
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value;
},
removeItem: (key: string) => {
delete store[key];
},
clear: () => {
store = {};
},
};
})();
Object.defineProperty(window, "localStorage", { value: localStorageMock });
// Minimal stubs to avoid Next.js router and network usage during render
vi.mock("@/components/networking", () => ({
credentialListCall: vi.fn().mockResolvedValue({ credentials: [] }),
modelInfoCall: vi.fn().mockResolvedValue({ data: [] }),
modelCostMap: vi.fn().mockResolvedValue({}),
getPassThroughEndpointsCall: vi.fn().mockResolvedValue({ endpoints: {} }),
getCallbacksCall: vi.fn().mockResolvedValue({ router_settings: {} }),
setCallbacksCall: vi.fn().mockResolvedValue(undefined),
getUiSettings: vi.fn().mockResolvedValue({ values: {} }),
latestHealthChecksCall: vi.fn().mockResolvedValue({ latest_health_checks: {} }),
getModelCostMapReloadStatus: vi.fn().mockResolvedValue({}),
}));
vi.mock("@/app/(dashboard)/models-and-endpoints/components/ModelAnalyticsTab/ModelAnalyticsTab", () => ({
default: () => null,
}));
vi.mock("@/components/add_model/add_auto_router_tab", () => ({
default: () => null,
}));
vi.mock("@/components/add_model/AddModelForm", () => ({
default: () => null,
}));
const mockHealthCheckComponent = vi.fn((_props: { all_models_on_proxy?: string[] }) => null);
vi.mock("@/components/model_dashboard/HealthCheckComponent", () => ({
default: (props: { all_models_on_proxy?: string[] }) => {
mockHealthCheckComponent(props);
return null;
},
}));
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
default: () => ({
teams: [],
setTeams: vi.fn(),
}),
}));
const mockUseModelsInfo = vi.fn();
vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({
useModelsInfo: () => mockUseModelsInfo(),
}));
const mockUseUISettings = vi.fn();
vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: () => mockUseUISettings(),
}));
const mockUseModelCostMap = vi.fn();
vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({
useModelCostMap: () => mockUseModelCostMap(),
}));
const mockUseAuthorized = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => mockUseAuthorized(),
}));
const createQueryClient = () =>
new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
});
describe("ModelsAndEndpointsView", () => {
beforeEach(() => {
mockUseModelsInfo.mockReturnValue({
data: { data: [] },
isLoading: false,
refetch: vi.fn(),
});
mockUseUISettings.mockReturnValue({
data: { values: {} },
});
mockUseModelCostMap.mockReturnValue({
data: {},
isLoading: false,
error: null,
});
mockUseAuthorized.mockReturnValue({
accessToken: "123",
token: "123",
userRole: "Admin",
userId: "123",
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});
it("should render the models and endpoints view", async () => {
const queryClient = createQueryClient();
const { findByText } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
expect(await findByText("Model Management", {}, { timeout: 10000 })).toBeInTheDocument();
});
it("should show Cost Optimization feedback banner by default", async () => {
localStorageMock.clear();
const queryClient = createQueryClient();
const { findByText } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
expect(await findByText("Help shape cost optimization", {}, { timeout: 10000 })).toBeInTheDocument();
});
it("should hide Cost Optimization feedback banner when dismiss button is clicked and persist to localStorage", async () => {
localStorageMock.clear();
const queryClient = createQueryClient();
const { findByText, queryByText, container } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
// Wait for banner to appear
expect(await findByText("Help shape cost optimization", {}, { timeout: 10000 })).toBeInTheDocument();
// Find and click dismiss button (X button)
const dismissButton = container.querySelector('button[aria-label="Dismiss banner"]');
expect(dismissButton).not.toBeNull();
fireEvent.click(dismissButton!);
// Banner should be hidden
expect(queryByText("Help shape cost optimization")).not.toBeInTheDocument();
// LocalStorage should be updated
expect(localStorageMock.getItem("hideCostOptimizationFeedbackBanner")).toBe("true");
});
it("should keep Cost Optimization feedback banner hidden across remounts once dismissed", async () => {
// Set localStorage to hide banner
localStorageMock.setItem("hideCostOptimizationFeedbackBanner", "true");
const queryClient = createQueryClient();
const { findByText, queryByText } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
// Wait for component to render
await findByText("Model Management", {}, { timeout: 10000 });
// Banner should not be visible
expect(queryByText("Help shape cost optimization")).not.toBeInTheDocument();
});
it("should pass model IDs (not model names) to HealthCheckComponent as all_models_on_proxy", async () => {
mockHealthCheckComponent.mockClear();
const modelDataWithIds = {
data: [
{ model_name: "gpt-4", model_info: { id: "deployment-id-1" } },
{ model_name: "gpt-4", model_info: { id: "deployment-id-2" } },
],
};
mockUseModelsInfo.mockReturnValue({
data: { data: modelDataWithIds.data },
isLoading: false,
refetch: vi.fn(),
});
const queryClient = createQueryClient();
const { getByRole } = render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsView premiumUser={false} teams={[]} />
</QueryClientProvider>,
);
const healthStatusTab = getByRole("tab", { name: "Health Status" });
await act(async () => {
healthStatusTab.click();
});
expect(mockHealthCheckComponent).toHaveBeenCalled();
const healthCheckProps = mockHealthCheckComponent.mock.calls[0][0];
expect(healthCheckProps.all_models_on_proxy).toEqual(["deployment-id-1", "deployment-id-2"]);
expect(healthCheckProps.all_models_on_proxy).not.toContain("gpt-4");
});
});

View file

@ -1,487 +0,0 @@
import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials";
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { useUpdateRetryPolicy } from "@/app/(dashboard)/hooks/routerSettings/useUpdateRetryPolicy";
import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab";
import CostOptimizationFeedbackBanner from "@/components/molecules/cost_optimization_feedback_banner";
import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab";
import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab";
import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit";
import { Team } from "@/components/key_team_helpers/key_list";
import CredentialsPanel from "@/components/model_add/CredentialsPanel";
import { getCallbacksCall } from "@/components/networking";
import { Providers, getPlaceholder, getProviderModels } from "@/components/provider_info_helpers";
import { getDisplayModelName } from "@/components/view_model/model_name_display";
import { transformModelData } from "./utils/modelDataTransformer";
import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles";
import { RefreshIcon } from "@heroicons/react/outline";
import { useQueryClient } from "@tanstack/react-query";
import type { PaginationState } from "@tanstack/react-table";
import { Col, Grid, Icon, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import type { UploadProps } from "antd";
import { Form } from "antd";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import AddModelTab from "../../../components/add_model/add_model_tab";
import HealthCheckComponent from "../../../components/model_dashboard/HealthCheckComponent";
import ModelGroupAliasSettings from "../../../components/model_group_alias_settings";
import ModelInfoView from "../../../components/model_info_view";
import NotificationsManager from "../../../components/molecules/notifications_manager";
import PassThroughSettings from "../../../components/PassThroughSettings/PassThroughSettings";
import TeamInfoView from "../../../components/team/TeamInfo";
import useAuthorized from "../hooks/useAuthorized";
interface ModelDashboardProps {
premiumUser: boolean;
teams: Team[] | null;
}
interface RetryPolicyObject {
[key: string]: { [retryPolicyKey: string]: number } | undefined;
}
interface GlobalRetryPolicyObject {
[retryPolicyKey: string]: number;
}
interface RouterSettings {
model_group_retry_policy?: RetryPolicyObject | null;
retry_policy?: GlobalRetryPolicyObject | null;
num_retries?: number | null;
model_group_alias?: { [key: string]: string } | null;
}
const HEALTH_PAGE_SIZE = 50;
const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, teams }) => {
const { accessToken, token, userRole, userId: userID } = useAuthorized();
const [addModelForm] = Form.useForm();
const [lastRefreshed, setLastRefreshed] = useState("");
const [providerModels, setProviderModels] = useState<Array<string>>([]);
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.Anthropic);
const [selectedModelGroup, setSelectedModelGroup] = useState<string | null>(null);
const [retryScope, setRetryScope] = useState<string | null>("global");
const [modelGroupRetryPolicy, setModelGroupRetryPolicy] = useState<RetryPolicyObject | null>(null);
const [globalRetryPolicy, setGlobalRetryPolicy] = useState<GlobalRetryPolicyObject | null>(null);
const [defaultRetry, setDefaultRetry] = useState<number>(0);
const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({});
const [showAdvancedSettings, setShowAdvancedSettings] = useState<boolean>(false);
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [healthPagination, setHealthPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: HEALTH_PAGE_SIZE,
});
const queryClient = useQueryClient();
const { data: modelDataResponse, isLoading: isLoadingModels, refetch: refetchModels } = useModelsInfo();
const { data: healthModelDataResponse, isLoading: isLoadingHealthModels } = useModelsInfo(
healthPagination.pageIndex + 1,
healthPagination.pageSize,
);
const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap();
const { data: credentialsResponse, isLoading: isLoadingCredentials } = useCredentials();
const credentialsList = credentialsResponse?.credentials || [];
const { data: uiSettings, isLoading: isLoadingUISettings } = useUISettings();
const updateRetryPolicy = useUpdateRetryPolicy(accessToken);
const availableModelGroups = useMemo(() => {
if (!modelDataResponse?.data) return [];
const allModelGroups = new Set<string>();
for (const model of modelDataResponse.data) {
allModelGroups.add(model.model_name);
}
return Array.from(allModelGroups).sort();
}, [modelDataResponse?.data]);
const availableModelAccessGroups = useMemo(() => {
if (!modelDataResponse?.data) return [];
const allModelAccessGroups = new Set<string>();
for (const model of modelDataResponse.data) {
const modelInfo = model.model_info;
if (modelInfo?.access_groups) {
for (const group of modelInfo.access_groups) {
allModelAccessGroups.add(group);
}
}
}
return Array.from(allModelAccessGroups);
}, [modelDataResponse?.data]);
const allModelsOnProxy = useMemo<string[]>(() => {
if (!modelDataResponse?.data) return [];
return modelDataResponse.data.map((model: any) => model.model_name);
}, [modelDataResponse?.data]);
const healthModelIdsOnProxy = useMemo<string[]>(() => {
if (!healthModelDataResponse?.data) return [];
return healthModelDataResponse.data
.map((model: any) => model.model_info?.id)
.filter((id: string | undefined): id is string => Boolean(id));
}, [healthModelDataResponse?.data]);
const getProviderFromModel = (model: string) => {
if (modelCostMapData !== null && modelCostMapData !== undefined) {
if (typeof modelCostMapData == "object" && model in modelCostMapData) {
return modelCostMapData[model]["litellm_provider"];
}
}
return "openai";
};
const processedModelData = useMemo(() => {
if (!modelDataResponse?.data) return { data: [] };
return transformModelData(modelDataResponse, getProviderFromModel);
}, [modelDataResponse?.data, getProviderFromModel]);
const processedHealthModelData = useMemo(() => {
if (!healthModelDataResponse?.data) return { data: [] };
return transformModelData(healthModelDataResponse, getProviderFromModel);
}, [healthModelDataResponse?.data, getProviderFromModel]);
const healthRowCount = healthModelDataResponse?.total_count ?? 0;
const isProxyAdmin = userRole && isProxyAdminRole(userRole);
const isInternalUser = userRole && internalUserRoles.includes(userRole);
const isUserTeamAdmin = userID && isUserTeamAdminForAnyTeam(teams, userID);
const addModelDisabledForInternalUsers =
isInternalUser && uiSettings?.values?.disable_model_add_for_internal_users === true;
// Hide tab if user is NOT a proxy admin AND (internal user with setting enabled OR not a team admin)
const shouldHideAddModelTab = !isProxyAdmin && (addModelDisabledForInternalUsers || !isUserTeamAdmin);
const setProviderModelsFn = (provider: Providers) => {
const _providerModels = getProviderModels(provider, modelCostMapData);
setProviderModels(_providerModels);
};
const uploadProps: UploadProps = {
name: "file",
accept: ".json",
pastable: false,
beforeUpload: (file) => {
if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (e) => {
if (e.target) {
const jsonStr = e.target.result as string;
addModelForm.setFieldsValue({ vertex_credentials: jsonStr });
}
};
reader.readAsText(file);
}
return false;
},
onChange(info) {
if (info.file.status === "done") {
NotificationsManager.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === "error") {
NotificationsManager.fromBackend(`${info.file.name} file upload failed.`);
}
},
};
const handleRefreshClick = () => {
const currentDate = new Date();
setLastRefreshed(currentDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
setHealthPagination((previous) => ({ ...previous, pageIndex: 0 }));
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
refetchModels();
};
const fetchRouterSettings = useCallback(async (): Promise<RouterSettings | null> => {
if (!accessToken || !userID || !userRole) {
return null;
}
try {
const routerSettingsInfo = await getCallbacksCall(accessToken, userID, userRole);
return routerSettingsInfo.router_settings;
} catch (error) {
console.error("Error fetching model data:", error);
return null;
}
}, [accessToken, userID, userRole]);
const applyRouterSettings = useCallback((routerSettings: RouterSettings) => {
setModelGroupRetryPolicy(routerSettings.model_group_retry_policy ?? null);
setGlobalRetryPolicy(routerSettings.retry_policy ?? null);
setDefaultRetry(routerSettings.num_retries ?? 2);
setModelGroupAlias(routerSettings.model_group_alias || {});
}, []);
const loadRetrySettings = useCallback(async () => {
const routerSettings = await fetchRouterSettings();
if (routerSettings) {
applyRouterSettings(routerSettings);
}
}, [fetchRouterSettings, applyRouterSettings]);
const handleSaveRetrySettings = () => {
updateRetryPolicy.mutate(
{
retry_policy: globalRetryPolicy,
model_group_retry_policy: modelGroupRetryPolicy,
},
{
onSuccess: () => {
NotificationsManager.success("Retry settings saved successfully");
loadRetrySettings();
},
onError: () => {
NotificationsManager.fromBackend("Failed to save retry settings");
},
},
);
};
useEffect(() => {
if (!accessToken || !token || !userRole || !userID || !modelDataResponse) {
return;
}
let active = true;
void (async () => {
const routerSettings = await fetchRouterSettings();
if (active && routerSettings) {
applyRouterSettings(routerSettings);
}
})();
return () => {
active = false;
};
}, [accessToken, token, userRole, userID, modelDataResponse, fetchRouterSettings, applyRouterSettings]);
const isLoading = isLoadingModels || isLoadingModelCostMap || isLoadingCredentials || isLoadingUISettings;
// Admin Viewer can view all models read-only — page render proceeds; the
// individual write-action tabs (Add Model, LLM Credentials, etc.) are
// gated separately below.
const handleOk = async () => {
try {
const values = await addModelForm.validateFields();
await handleAddModelSubmit(values, accessToken, addModelForm, handleRefreshClick);
} catch (error: any) {
const errorMessages =
error.errorFields
?.map((field: any) => {
return `${field.name.join(".")}: ${field.errors.join(", ")}`;
})
.join(" | ") || "Unknown validation error";
NotificationsManager.fromBackend(`Please fill in the following required fields: ${errorMessages}`);
}
};
Object.keys(Providers).find((key) => (Providers as { [index: string]: any })[key] === selectedProvider);
// If a team is selected, render TeamInfoView in full page layout
if (selectedTeamId) {
return (
<div className="w-full h-full">
<TeamInfoView
teamId={selectedTeamId}
onClose={() => setSelectedTeamId(null)}
accessToken={accessToken}
is_team_admin={userRole === "Admin"}
is_proxy_admin={userRole === "Proxy Admin"}
userModels={allModelsOnProxy}
editTeam={false}
onUpdate={handleRefreshClick}
premiumUser={premiumUser}
/>
</div>
);
}
return (
<div className="mx-4 h-[75vh]">
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
<Col numColSpan={1} className="flex flex-col gap-2">
{/* Model Management Header */}
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-lg font-semibold">Model Management</h2>
{!all_admin_roles.includes(userRole) ? (
<p className="text-sm text-gray-600">Add models for teams you are an admin for.</p>
) : (
<p className="text-sm text-gray-600">Add and manage models for the proxy</p>
)}
</div>
</div>
{/* Cost Optimization Feedback Banner */}
<CostOptimizationFeedbackBanner />
{selectedModelId && !isLoading ? (
<ModelInfoView
modelId={selectedModelId}
onClose={() => {
setSelectedModelId(null);
}}
accessToken={accessToken}
userID={userID}
userRole={userRole}
onModelUpdate={(updatedModel) => {
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
handleRefreshClick();
}}
modelAccessGroups={availableModelAccessGroups}
/>
) : (
(() => {
// Build a single source-of-truth list of {tab, panel} pairs.
// Conditionally-hidden tabs (e.g. "Add Model" for non-admin) get
// filtered out as a unit so tab indices and panel indices can
// never drift apart — Tremor's TabList and TabPanels filter
// falsy children inconsistently, which previously caused
// "click LLM Credentials, see nothing" for Admin Viewer.
const isAdmin = all_admin_roles.includes(userRole);
const visibleTabs: Array<{ tab: React.ReactElement; panel: React.ReactElement }> = [
{
tab: <Tab key="all-models">{isAdmin ? "All Models" : "Your Models"}</Tab>,
panel: (
<AllModelsTab
key="all-models"
selectedModelGroup={selectedModelGroup}
setSelectedModelGroup={setSelectedModelGroup}
availableModelGroups={availableModelGroups}
availableModelAccessGroups={availableModelAccessGroups}
setSelectedModelId={setSelectedModelId}
setSelectedTeamId={setSelectedTeamId}
/>
),
},
];
if (!shouldHideAddModelTab) {
visibleTabs.push({
tab: <Tab key="add-model">Add Model</Tab>,
panel: (
<TabPanel key="add-model" className="h-full">
<AddModelTab
form={addModelForm}
handleOk={handleOk}
selectedProvider={selectedProvider}
setSelectedProvider={setSelectedProvider}
providerModels={providerModels}
setProviderModelsFn={setProviderModelsFn}
getPlaceholder={getPlaceholder}
uploadProps={uploadProps}
showAdvancedSettings={showAdvancedSettings}
setShowAdvancedSettings={setShowAdvancedSettings}
teams={teams}
credentials={credentialsList}
accessToken={accessToken}
userRole={userRole}
/>
</TabPanel>
),
});
}
if (isAdmin) {
visibleTabs.push(
{
tab: <Tab key="llm-credentials">LLM Credentials</Tab>,
panel: (
<TabPanel key="llm-credentials">
<CredentialsPanel uploadProps={uploadProps} />
</TabPanel>
),
},
{
tab: <Tab key="pass-through">Pass-Through Endpoints</Tab>,
panel: (
<TabPanel key="pass-through">
<PassThroughSettings
accessToken={accessToken}
userRole={userRole}
userID={userID}
premiumUser={premiumUser}
/>
</TabPanel>
),
},
{
tab: <Tab key="health-status">Health Status</Tab>,
panel: (
<TabPanel key="health-status">
<HealthCheckComponent
accessToken={accessToken}
modelData={processedHealthModelData}
all_models_on_proxy={healthModelIdsOnProxy}
getDisplayModelName={getDisplayModelName}
setSelectedModelId={setSelectedModelId}
teams={teams}
isLoading={isLoadingHealthModels}
pagination={healthPagination}
onPaginationChange={setHealthPagination}
rowCount={healthRowCount}
/>
</TabPanel>
),
},
{
tab: <Tab key="model-retry-settings">Model Retry Settings</Tab>,
panel: (
<ModelRetrySettingsTab
key="model-retry-settings"
selectedModelGroup={retryScope}
setSelectedModelGroup={setRetryScope}
availableModelGroups={availableModelGroups}
globalRetryPolicy={globalRetryPolicy}
setGlobalRetryPolicy={setGlobalRetryPolicy}
defaultRetry={defaultRetry}
modelGroupRetryPolicy={modelGroupRetryPolicy}
setModelGroupRetryPolicy={setModelGroupRetryPolicy}
handleSaveRetrySettings={handleSaveRetrySettings}
isSaving={updateRetryPolicy.isPending}
/>
),
},
{
tab: <Tab key="model-group-alias">Model Group Alias</Tab>,
panel: (
<TabPanel key="model-group-alias">
<ModelGroupAliasSettings
accessToken={accessToken}
initialModelGroupAlias={modelGroupAlias}
onAliasUpdate={setModelGroupAlias}
/>
</TabPanel>
),
},
{
tab: <Tab key="price-data-reload">Price Data Reload</Tab>,
panel: <PriceDataManagementTab key="price-data-reload" />,
},
);
}
return (
<TabGroup
index={selectedTabIndex}
onIndexChange={setSelectedTabIndex}
className="gap-2 h-[75vh] w-full "
>
<TabList className="flex justify-between mt-2 w-full items-center">
<div className="flex">{visibleTabs.map((t) => t.tab)}</div>
<div className="flex items-center space-x-2 self-center">
{lastRefreshed && <span className="text-xs text-gray-500">Last Refreshed: {lastRefreshed}</span>}
<Icon
icon={RefreshIcon}
variant="shadow"
size="xs"
className="cursor-pointer"
onClick={handleRefreshClick}
/>
</div>
</TabList>
<TabPanels>{visibleTabs.map((t) => t.panel)}</TabPanels>
</TabGroup>
);
})()
)}
</Col>
</Grid>
</div>
);
};
export default ModelsAndEndpointsView;

View file

@ -0,0 +1,59 @@
"use client";
import { Form } from "antd";
import { useState } from "react";
import { useQueryClient } from "@tanstack/react-query";
import AddModelTab from "@/components/add_model/add_model_tab";
import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit";
import { Providers, getPlaceholder, getProviderModels } from "@/components/provider_info_helpers";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { vertexCredentialsUploadProps } from "@/app/(dashboard)/models-and-endpoints/vertexCredentialsUpload";
export default function AddModelPage() {
const { accessToken, userRole } = useAuthorized();
const [form] = Form.useForm();
const queryClient = useQueryClient();
const { data: modelCostMapData } = useModelCostMap();
const { data: credentialsResponse } = useCredentials();
const { data: teams } = useTeams();
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.Anthropic);
const [providerModels, setProviderModels] = useState<string[]>([]);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const refresh = () => queryClient.invalidateQueries({ queryKey: ["models", "list"] });
const handleOk = async () => {
try {
const values = await form.validateFields();
await handleAddModelSubmit(values, accessToken, form, refresh);
} catch (error: any) {
const errorMessages =
error.errorFields?.map((field: any) => `${field.name.join(".")}: ${field.errors.join(", ")}`).join(" | ") ||
"Unknown validation error";
NotificationsManager.fromBackend(`Please fill in the following required fields: ${errorMessages}`);
}
};
return (
<AddModelTab
form={form}
handleOk={handleOk}
selectedProvider={selectedProvider}
setSelectedProvider={setSelectedProvider}
providerModels={providerModels}
setProviderModelsFn={(provider) => setProviderModels(getProviderModels(provider, modelCostMapData))}
getPlaceholder={getPlaceholder}
uploadProps={vertexCredentialsUploadProps(form)}
showAdvancedSettings={showAdvancedSettings}
setShowAdvancedSettings={setShowAdvancedSettings}
teams={teams ?? null}
credentials={credentialsResponse?.credentials || []}
accessToken={accessToken}
userRole={userRole}
/>
);
}

View file

@ -11,7 +11,7 @@ import { modelDeleteCall, modelPatchUpdateCall } from "@/components/networking";
import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons";
import { PaginationState, SortingState } from "@tanstack/react-table";
import { useQueryClient } from "@tanstack/react-query";
import { Grid, TabPanel } from "@tremor/react";
import { Grid } from "@tremor/react";
import { Badge, Button, Select, Skeleton, Space, Typography } from "antd";
import ModelSettingsModal from "@/components/model_dashboard/ModelSettingsModal/ModelSettingsModal";
import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer";
@ -232,7 +232,7 @@ const AllModelsTab = ({
};
return (
<TabPanel>
<div className="w-full">
<Grid>
<div className="flex flex-col space-y-4">
<div className="bg-white rounded-lg shadow-sm">
@ -600,7 +600,7 @@ const AllModelsTab = ({
onCancel={() => setIsModelSettingsModalVisible(false)}
onSuccess={() => setIsModelSettingsModalVisible(false)}
/>
</TabPanel>
</div>
);
};

View file

@ -1,4 +1,4 @@
import { Button, Select, SelectItem, TabPanel, Text, Title } from "@tremor/react";
import { Button, Select, SelectItem, Text, Title } from "@tremor/react";
import { InputNumber } from "antd";
import React from "react";
@ -64,7 +64,7 @@ const ModelRetrySettingsTab = ({
};
return (
<TabPanel>
<div>
<div className="flex items-center gap-4 mb-6">
<div className="flex items-center">
<Text>Retry Policy Scope:</Text>
@ -132,7 +132,7 @@ const ModelRetrySettingsTab = ({
<Button className="mt-6 mr-8" onClick={handleSaveRetrySettings} loading={isSaving} disabled={isSaving}>
Save
</Button>
</TabPanel>
</div>
);
};

View file

@ -0,0 +1,22 @@
/* @vitest-environment jsdom */
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import PriceDataManagementTab from "./PriceDataManagementTab";
// Deliberately do NOT mock @tremor/react. These tab components render standalone
// (inside antd Tabs / directly as a route page), no longer inside a Tremor
// <TabGroup>. A Tremor <TabPanel> root renders nothing without that context, so
// this asserts the component's content is visible on its own — reverting the root
// back to <TabPanel> makes the title disappear and fails this test.
vi.mock("@/components/price_data_reload", () => ({ default: () => <div>reload</div> }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => ({ accessToken: "sk-test" }) }));
vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({
useModelCostMap: () => ({ refetch: vi.fn() }),
}));
describe("PriceDataManagementTab", () => {
it("renders its content standalone, without a Tremor TabGroup ancestor", () => {
const { getByText } = render(<PriceDataManagementTab />);
expect(getByText("Price Data Management")).toBeInTheDocument();
});
});

View file

@ -1,4 +1,4 @@
import { TabPanel, Text, Title } from "@tremor/react";
import { Text, Title } from "@tremor/react";
import PriceDataReload from "@/components/price_data_reload";
import React from "react";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
@ -9,7 +9,7 @@ const PriceDataManagementTab = () => {
const { refetch: refetchModelCostMap } = useModelCostMap();
return (
<TabPanel>
<div>
<div className="p-6">
<div className="mb-6">
<Title>Price Data Management</Title>
@ -28,7 +28,7 @@ const PriceDataManagementTab = () => {
className="w-full"
/>
</div>
</TabPanel>
</div>
);
};

View file

@ -0,0 +1,52 @@
/* @vitest-environment jsdom */
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useModelDetailRouting } from "./detailNavigation";
// The detail overlay is driven by ?model=/?team= on the current path. Under the
// /ui static mount a router.push to the same path (query-only change) is a no-op,
// so navigation goes through history.pushState (client-side, no full reload).
vi.mock("next/navigation", () => ({ useSearchParams: () => new URLSearchParams(window.location.search) }));
describe("useModelDetailRouting", () => {
beforeEach(() => {
window.history.pushState(null, "", "/models-and-endpoints/");
});
it("openModel sets ?model= via history.pushState (no full navigation)", () => {
const spy = vi.spyOn(window.history, "pushState");
const { result } = renderHook(() => useModelDetailRouting());
act(() => result.current.openModel("abc-1"));
expect(spy).toHaveBeenCalledWith(null, "", expect.stringContaining("model=abc-1"));
spy.mockRestore();
});
it("openTeam sets ?team= and drops any model param", () => {
window.history.pushState(null, "", "/models-and-endpoints/?model=abc-1");
const spy = vi.spyOn(window.history, "pushState");
const { result } = renderHook(() => useModelDetailRouting());
act(() => result.current.openTeam("team-9"));
const url = spy.mock.calls.at(-1)?.[2] as string;
expect(url).toContain("team=team-9");
expect(url).not.toContain("model=");
spy.mockRestore();
});
it("close removes both model and team params", () => {
window.history.pushState(null, "", "/models-and-endpoints/?model=abc-1");
const spy = vi.spyOn(window.history, "pushState");
const { result } = renderHook(() => useModelDetailRouting());
act(() => result.current.close());
const url = spy.mock.calls.at(-1)?.[2] as string;
expect(url).not.toContain("model=");
expect(url).not.toContain("team=");
spy.mockRestore();
});
it("reads modelId and teamId from the query string", () => {
window.history.pushState(null, "", "/models-and-endpoints/?model=xyz");
const { result } = renderHook(() => useModelDetailRouting());
expect(result.current.modelId).toBe("xyz");
expect(result.current.teamId).toBeNull();
});
});

View file

@ -0,0 +1,51 @@
import { useSearchParams } from "next/navigation";
import { useCallback } from "react";
export interface ModelDetailRouting {
modelId: string | null;
teamId: string | null;
openModel: (id: string) => void;
openTeam: (id: string) => void;
close: () => void;
}
function navigateWithParams(mutate: (params: URLSearchParams) => void): void {
const params = new URLSearchParams(window.location.search);
mutate(params);
const qs = params.toString();
const url = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
window.history.pushState(null, "", url);
}
export function useModelDetailRouting(): ModelDetailRouting {
const searchParams = useSearchParams();
const openModel = useCallback((id: string) => {
navigateWithParams((params) => {
params.delete("team");
params.set("model", id);
});
}, []);
const openTeam = useCallback((id: string) => {
navigateWithParams((params) => {
params.delete("model");
params.set("team", id);
});
}, []);
const close = useCallback(() => {
navigateWithParams((params) => {
params.delete("model");
params.delete("team");
});
}, []);
return {
modelId: searchParams?.get("model") ?? null,
teamId: searchParams?.get("team") ?? null,
openModel,
openTeam,
close,
};
}

View file

@ -0,0 +1,54 @@
/* @vitest-environment jsdom */
import { render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import HealthStatusPage from "./page";
vi.mock("next/navigation", () => ({
usePathname: () => "/models-and-endpoints/health",
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
useSearchParams: () => new URLSearchParams(""),
}));
const mockHealthCheckComponent = vi.fn((_props: { all_models_on_proxy?: string[] }) => null);
vi.mock("@/components/model_dashboard/HealthCheckComponent", () => ({
default: (props: { all_models_on_proxy?: string[] }) => {
mockHealthCheckComponent(props);
return null;
},
}));
vi.mock("@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer", () => ({
transformModelData: () => ({ data: [] }),
}));
const mockUseModelsInfo = vi.fn();
vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ useModelsInfo: () => mockUseModelsInfo() }));
vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ useModelCostMap: () => ({ data: {} }) }));
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ useTeams: () => ({ data: [] }) }));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => ({ accessToken: "123" }) }));
describe("HealthStatusPage", () => {
beforeEach(() => {
mockHealthCheckComponent.mockClear();
});
it("passes deployment ids (not model names) to HealthCheckComponent as all_models_on_proxy", () => {
mockUseModelsInfo.mockReturnValue({
data: {
data: [
{ model_name: "gpt-4", model_info: { id: "deployment-id-1" } },
{ model_name: "gpt-4", model_info: { id: "deployment-id-2" } },
],
total_count: 2,
},
isLoading: false,
});
render(<HealthStatusPage />);
expect(mockHealthCheckComponent).toHaveBeenCalled();
const props = mockHealthCheckComponent.mock.calls[0][0];
expect(props.all_models_on_proxy).toEqual(["deployment-id-1", "deployment-id-2"]);
expect(props.all_models_on_proxy).not.toContain("gpt-4");
});
});

View file

@ -0,0 +1,63 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import type { PaginationState } from "@tanstack/react-table";
import HealthCheckComponent from "@/components/model_dashboard/HealthCheckComponent";
import { getDisplayModelName } from "@/components/view_model/model_name_display";
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { transformModelData } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer";
import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
const HEALTH_PAGE_SIZE = 50;
export default function HealthStatusPage() {
const { accessToken } = useAuthorized();
const { data: teams } = useTeams();
const { data: modelCostMapData } = useModelCostMap();
const { openModel } = useModelDetailRouting();
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: HEALTH_PAGE_SIZE });
const { data: healthModelDataResponse, isLoading } = useModelsInfo(pagination.pageIndex + 1, pagination.pageSize);
const getProviderFromModel = useCallback(
(model: string) => {
if (modelCostMapData && typeof modelCostMapData === "object" && model in modelCostMapData) {
return modelCostMapData[model]["litellm_provider"];
}
return "openai";
},
[modelCostMapData],
);
const processedHealthModelData = useMemo(() => {
if (!healthModelDataResponse?.data) {
return { data: [] };
}
return transformModelData(healthModelDataResponse, getProviderFromModel);
}, [healthModelDataResponse, getProviderFromModel]);
const healthModelIdsOnProxy = useMemo<string[]>(
() =>
healthModelDataResponse?.data
?.map((model: any) => model.model_info?.id)
.filter((id: string | undefined): id is string => Boolean(id)) ?? [],
[healthModelDataResponse?.data],
);
return (
<HealthCheckComponent
accessToken={accessToken}
modelData={processedHealthModelData}
all_models_on_proxy={healthModelIdsOnProxy}
getDisplayModelName={getDisplayModelName}
setSelectedModelId={openModel}
teams={teams ?? null}
isLoading={isLoading}
pagination={pagination}
onPaginationChange={setPagination}
rowCount={healthModelDataResponse?.total_count ?? 0}
/>
);
}

View file

@ -0,0 +1,126 @@
/* @vitest-environment jsdom */
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ModelsAndEndpointsLayout from "./layout";
const { mockPush, mockReplace, navState } = vi.hoisted(() => ({
mockPush: vi.fn(),
mockReplace: vi.fn(),
navState: { pathname: "/models-and-endpoints", search: "" },
}));
vi.mock("next/navigation", () => ({
usePathname: () => navState.pathname,
useRouter: () => ({ push: mockPush, replace: mockReplace }),
useSearchParams: () => new URLSearchParams(navState.search),
}));
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
vi.mock("@/components/molecules/cost_optimization_feedback_banner", () => ({ default: () => null }));
vi.mock("@/components/model_info_view", () => ({
default: ({ modelId }: { modelId: string }) => <div data-testid="model-info">model:{modelId}</div>,
}));
vi.mock("@/components/team/TeamInfo", () => ({
default: ({ teamId }: { teamId: string }) => <div data-testid="team-info">team:{teamId}</div>,
}));
const mockUseAuthorized = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized() }));
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ useTeams: () => ({ data: [] }) }));
vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: () => ({ data: { values: {} } }),
}));
vi.mock("@/app/(dashboard)/models-and-endpoints/useModelDashboardData", () => ({
useModelDashboardData: () => ({
availableModelGroups: [],
availableModelAccessGroups: [],
allModelsOnProxy: [],
isLoading: false,
}),
}));
const renderLayout = () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
return render(
<QueryClientProvider client={queryClient}>
<ModelsAndEndpointsLayout>
<div data-testid="tab-content">CHILD</div>
</ModelsAndEndpointsLayout>
</QueryClientProvider>,
);
};
describe("ModelsAndEndpointsLayout", () => {
beforeEach(() => {
navState.pathname = "/models-and-endpoints";
navState.search = "";
mockPush.mockClear();
mockReplace.mockClear();
mockUseAuthorized.mockReturnValue({
accessToken: "123",
token: "123",
userRole: "Admin",
userId: "123",
premiumUser: false,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});
it("renders the admin tab bar and the active tab's page content", () => {
const { getByRole, getByTestId } = renderLayout();
expect(getByRole("tab", { name: "LLM Credentials" })).toBeInTheDocument();
expect(getByRole("tab", { name: "Health Status" })).toBeInTheDocument();
expect(getByTestId("tab-content")).toHaveTextContent("CHILD");
});
it("navigates to a tab's path when its tab is clicked", async () => {
const { getByRole } = renderLayout();
await act(async () => {
getByRole("tab", { name: "Health Status" }).click();
});
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/models-and-endpoints\/health\/$/));
});
it("redirects to the base models path when the tab path is not permitted for the role", async () => {
const replaceMock = vi.fn();
const originalLocation = window.location;
Object.defineProperty(window, "location", {
configurable: true,
value: { replace: replaceMock, assign: vi.fn(), href: "http://localhost/", pathname: "/", search: "" },
});
mockUseAuthorized.mockReturnValue({
accessToken: "123",
token: "123",
userRole: "Internal User",
userId: "123",
premiumUser: false,
});
navState.pathname = "/models-and-endpoints/llm-credentials";
await act(async () => {
renderLayout();
});
expect(replaceMock).toHaveBeenCalledWith(expect.stringMatching(/\/models-and-endpoints\/$/));
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
});
it("renders the model detail overlay from ?model and hides the tabs and page content", () => {
navState.search = "model=abc-123";
const { getByTestId, queryByTestId, queryByRole } = renderLayout();
expect(getByTestId("model-info")).toHaveTextContent("model:abc-123");
expect(queryByTestId("tab-content")).toBeNull();
expect(queryByRole("tab", { name: "Health Status" })).toBeNull();
});
it("renders the team detail overlay from ?team", () => {
navState.search = "team=team-9";
const { getByTestId, queryByTestId } = renderLayout();
expect(getByTestId("team-info")).toHaveTextContent("team:team-9");
expect(queryByTestId("tab-content")).toBeNull();
});
});

View file

@ -0,0 +1,162 @@
"use client";
import type { ReactNode } from "react";
import { useEffect, useMemo, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { Tabs } from "antd";
import { RefreshIcon } from "@heroicons/react/outline";
import { useQueryClient } from "@tanstack/react-query";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles";
import CostOptimizationFeedbackBanner from "@/components/molecules/cost_optimization_feedback_banner";
import ModelInfoView from "@/components/model_info_view";
import TeamInfoView from "@/components/team/TeamInfo";
import { modelTabHref, slugFromPathname, type ModelTabSlug } from "@/app/(dashboard)/models-and-endpoints/tabRoutes";
import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData";
const BASE_TAB_KEY = "all-models";
const TAB_LABELS: Record<ModelTabSlug, string> = {
add: "Add Model",
"llm-credentials": "LLM Credentials",
"pass-through": "Pass-Through Endpoints",
health: "Health Status",
"retry-settings": "Model Retry Settings",
"model-group-alias": "Model Group Alias",
"price-data": "Price Data Reload",
};
export default function ModelsAndEndpointsLayout({ children }: { children: ReactNode }) {
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
const { data: teams, isLoading: teamsLoading } = useTeams();
const { data: uiSettings, isLoading: uiSettingsLoading } = useUISettings();
const pathname = usePathname();
const router = useRouter();
const queryClient = useQueryClient();
const { modelId, teamId, close } = useModelDetailRouting();
const { availableModelAccessGroups, allModelsOnProxy } = useModelDashboardData();
const [lastRefreshed, setLastRefreshed] = useState("");
const isProxyAdmin = userRole && isProxyAdminRole(userRole);
const isInternalUser = userRole && internalUserRoles.includes(userRole);
const isUserTeamAdmin = userID && isUserTeamAdminForAnyTeam(teams ?? null, userID);
const addModelDisabledForInternalUsers =
isInternalUser && uiSettings?.values?.disable_model_add_for_internal_users === true;
const shouldHideAddModelTab = !isProxyAdmin && (addModelDisabledForInternalUsers || !isUserTeamAdmin);
const isAdmin = all_admin_roles.includes(userRole);
const visibleSlugs = useMemo<Array<"" | ModelTabSlug>>(
() => [
"",
...(shouldHideAddModelTab ? [] : (["add"] as const)),
...(isAdmin
? (["llm-credentials", "pass-through", "health", "retry-settings", "model-group-alias", "price-data"] as const)
: []),
],
[shouldHideAddModelTab, isAdmin],
);
const activeSlug = slugFromPathname(pathname);
const isKnownSlug = visibleSlugs.some((slug) => slug === activeSlug);
const activeKey = isKnownSlug ? activeSlug || BASE_TAB_KEY : BASE_TAB_KEY;
useEffect(() => {
if (teamsLoading || uiSettingsLoading) {
return;
}
if (activeSlug !== "" && !isKnownSlug) {
window.location.replace(modelTabHref(""));
}
}, [activeSlug, isKnownSlug, teamsLoading, uiSettingsLoading]);
const allModelsLabel = isAdmin ? "All Models" : "Your Models";
const tabItems = visibleSlugs.map((slug) => {
const key = slug || BASE_TAB_KEY;
return {
key,
label: slug ? TAB_LABELS[slug] : allModelsLabel,
children: key === activeKey ? children : null,
};
});
const handleRefreshClick = () => {
setLastRefreshed(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
};
const invalidateModels = () => queryClient.invalidateQueries({ queryKey: ["models", "list"] });
if (teamId) {
return (
<div className="w-full h-full">
<TeamInfoView
teamId={teamId}
onClose={close}
accessToken={accessToken}
is_team_admin={userRole === "Admin"}
is_proxy_admin={userRole === "Proxy Admin"}
userModels={allModelsOnProxy}
editTeam={false}
onUpdate={invalidateModels}
premiumUser={premiumUser}
/>
</div>
);
}
return (
<div className="mx-4 h-[75vh]">
<div className="flex flex-col gap-2 p-8 w-full mt-2">
<div className="flex justify-between items-center mb-4">
<div>
<h2 className="text-lg font-semibold">Model Management</h2>
{isAdmin ? (
<p className="text-sm text-gray-600">Add and manage models for the proxy</p>
) : (
<p className="text-sm text-gray-600">Add models for teams you are an admin for.</p>
)}
</div>
</div>
<CostOptimizationFeedbackBanner />
{modelId ? (
<ModelInfoView
modelId={modelId}
onClose={close}
accessToken={accessToken}
userID={userID}
userRole={userRole}
onModelUpdate={invalidateModels}
modelAccessGroups={availableModelAccessGroups}
/>
) : (
<Tabs
activeKey={activeKey}
onChange={(key) => router.push(modelTabHref(key === BASE_TAB_KEY ? "" : key))}
items={tabItems}
tabBarExtraContent={{
right: (
<div className="flex items-center space-x-2 self-center">
{lastRefreshed && <span className="text-xs text-gray-500">Last Refreshed: {lastRefreshed}</span>}
<button
type="button"
onClick={handleRefreshClick}
aria-label="Refresh models"
className="cursor-pointer"
>
<RefreshIcon className="h-4 w-4 text-gray-500" />
</button>
</div>
),
}}
/>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,10 @@
"use client";
import { Form } from "antd";
import CredentialsPanel from "@/components/model_add/CredentialsPanel";
import { vertexCredentialsUploadProps } from "@/app/(dashboard)/models-and-endpoints/vertexCredentialsUpload";
export default function LlmCredentialsPage() {
const [form] = Form.useForm();
return <CredentialsPanel uploadProps={vertexCredentialsUploadProps(form)} />;
}

View file

@ -0,0 +1,39 @@
"use client";
import { useEffect, useState } from "react";
import ModelGroupAliasSettings from "@/components/model_group_alias_settings";
import { getCallbacksCall } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function ModelGroupAliasPage() {
const { accessToken, userId: userID, userRole } = useAuthorized();
const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({});
useEffect(() => {
if (!accessToken || !userID || !userRole) {
return;
}
let active = true;
void (async () => {
try {
const info = await getCallbacksCall(accessToken, userID, userRole);
if (active) {
setModelGroupAlias(info.router_settings?.model_group_alias || {});
}
} catch (error) {
console.error("Error fetching model group alias:", error);
}
})();
return () => {
active = false;
};
}, [accessToken, userID, userRole]);
return (
<ModelGroupAliasSettings
accessToken={accessToken}
initialModelGroupAlias={modelGroupAlias}
onAliasUpdate={setModelGroupAlias}
/>
);
}

View file

@ -1,11 +1,23 @@
"use client";
import ModelsAndEndpointsView from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { useState } from "react";
import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab";
import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData";
import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
export default function ModelsAndEndpointsPage() {
const { premiumUser } = useAuthorized();
const { data: teams } = useTeams();
return <ModelsAndEndpointsView premiumUser={premiumUser} teams={teams ?? null} />;
export default function AllModelsPage() {
const [selectedModelGroup, setSelectedModelGroup] = useState<string | null>(null);
const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData();
const { openModel, openTeam } = useModelDetailRouting();
return (
<AllModelsTab
selectedModelGroup={selectedModelGroup}
setSelectedModelGroup={setSelectedModelGroup}
availableModelGroups={availableModelGroups}
availableModelAccessGroups={availableModelAccessGroups}
setSelectedModelId={openModel}
setSelectedTeamId={openTeam}
/>
);
}

View file

@ -0,0 +1,11 @@
"use client";
import PassThroughSettings from "@/components/PassThroughSettings/PassThroughSettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function PassThroughPage() {
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
return (
<PassThroughSettings accessToken={accessToken} userRole={userRole} userID={userID} premiumUser={premiumUser} />
);
}

View file

@ -0,0 +1,7 @@
"use client";
import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab";
export default function PriceDataPage() {
return <PriceDataManagementTab />;
}

View file

@ -0,0 +1,100 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import ModelRetrySettingsTab from "@/app/(dashboard)/models-and-endpoints/components/ModelRetrySettingsTab";
import { getCallbacksCall } from "@/components/networking";
import { useUpdateRetryPolicy } from "@/app/(dashboard)/hooks/routerSettings/useUpdateRetryPolicy";
import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import NotificationsManager from "@/components/molecules/notifications_manager";
interface RetryPolicyObject {
[key: string]: { [retryPolicyKey: string]: number } | undefined;
}
interface GlobalRetryPolicyObject {
[retryPolicyKey: string]: number;
}
interface RouterSettings {
model_group_retry_policy?: RetryPolicyObject | null;
retry_policy?: GlobalRetryPolicyObject | null;
num_retries?: number | null;
}
export default function ModelRetrySettingsPage() {
const { accessToken, userId: userID, userRole } = useAuthorized();
const { availableModelGroups } = useModelDashboardData();
const updateRetryPolicy = useUpdateRetryPolicy(accessToken);
const [retryScope, setRetryScope] = useState<string | null>("global");
const [modelGroupRetryPolicy, setModelGroupRetryPolicy] = useState<RetryPolicyObject | null>(null);
const [globalRetryPolicy, setGlobalRetryPolicy] = useState<GlobalRetryPolicyObject | null>(null);
const [defaultRetry, setDefaultRetry] = useState<number>(0);
const fetchRetrySettings = useCallback(async () => {
if (!accessToken || !userID || !userRole) {
return null;
}
try {
const info = await getCallbacksCall(accessToken, userID, userRole);
return info.router_settings;
} catch (error) {
console.error("Error fetching router settings:", error);
return null;
}
}, [accessToken, userID, userRole]);
const applyRetrySettings = useCallback((routerSettings: RouterSettings) => {
setModelGroupRetryPolicy(routerSettings.model_group_retry_policy ?? null);
setGlobalRetryPolicy(routerSettings.retry_policy ?? null);
setDefaultRetry(routerSettings.num_retries ?? 2);
}, []);
useEffect(() => {
let active = true;
void (async () => {
const routerSettings = await fetchRetrySettings();
if (active && routerSettings) {
applyRetrySettings(routerSettings);
}
})();
return () => {
active = false;
};
}, [fetchRetrySettings, applyRetrySettings]);
const handleSaveRetrySettings = () => {
updateRetryPolicy.mutate(
{ retry_policy: globalRetryPolicy, model_group_retry_policy: modelGroupRetryPolicy },
{
onSuccess: () => {
NotificationsManager.success("Retry settings saved successfully");
void fetchRetrySettings().then((routerSettings) => {
if (routerSettings) {
applyRetrySettings(routerSettings);
}
});
},
onError: () => {
NotificationsManager.fromBackend("Failed to save retry settings");
},
},
);
};
return (
<ModelRetrySettingsTab
selectedModelGroup={retryScope}
setSelectedModelGroup={setRetryScope}
availableModelGroups={availableModelGroups}
globalRetryPolicy={globalRetryPolicy}
setGlobalRetryPolicy={setGlobalRetryPolicy}
defaultRetry={defaultRetry}
modelGroupRetryPolicy={modelGroupRetryPolicy}
setModelGroupRetryPolicy={setModelGroupRetryPolicy}
handleSaveRetrySettings={handleSaveRetrySettings}
isSaving={updateRetryPolicy.isPending}
/>
);
}

View file

@ -0,0 +1,38 @@
/* @vitest-environment jsdom */
import { describe, expect, it, vi } from "vitest";
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
import { MODEL_TAB_SLUGS, modelTabHref, slugFromPathname } from "./tabRoutes";
describe("slugFromPathname", () => {
it("returns empty string for the base path with or without a trailing slash", () => {
expect(slugFromPathname("/models-and-endpoints")).toBe("");
expect(slugFromPathname("/models-and-endpoints/")).toBe("");
});
it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => {
expect(slugFromPathname("/models-and-endpoints/add")).toBe("add");
expect(slugFromPathname("/ui/models-and-endpoints/llm-credentials/")).toBe("llm-credentials");
});
it("returns the raw segment for an unknown tab so the view can redirect to base", () => {
expect(slugFromPathname("/ui/models-and-endpoints/bogus")).toBe("bogus");
});
it("returns empty string when the models base segment is not in the path", () => {
expect(slugFromPathname("/teams")).toBe("");
});
});
describe("modelTabHref", () => {
it("builds the trailing-slash base href for the empty slug", () => {
expect(modelTabHref("")).toBe("/ui/models-and-endpoints/");
});
it("builds a trailing-slash href for every tab slug (required by static export)", () => {
for (const slug of MODEL_TAB_SLUGS) {
expect(modelTabHref(slug)).toBe(`/ui/models-and-endpoints/${slug}/`);
}
});
});

View file

@ -0,0 +1,29 @@
import { migratedHref } from "@/utils/migratedPages";
export const MODELS_BASE_SEGMENT = "models-and-endpoints";
export const MODEL_TAB_SLUGS = [
"add",
"llm-credentials",
"pass-through",
"health",
"retry-settings",
"model-group-alias",
"price-data",
] as const;
export type ModelTabSlug = (typeof MODEL_TAB_SLUGS)[number];
export function modelTabHref(slug: string): string {
const base = migratedHref(MODELS_BASE_SEGMENT);
return slug ? `${base}/${slug}/` : `${base}/`;
}
export function slugFromPathname(pathname: string): string {
const parts = pathname.split("/").filter(Boolean);
const idx = parts.indexOf(MODELS_BASE_SEGMENT);
if (idx === -1) {
return "";
}
return parts[idx + 1] ?? "";
}

View file

@ -0,0 +1,32 @@
import { useMemo } from "react";
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
export interface ModelDashboardData {
availableModelGroups: string[];
availableModelAccessGroups: string[];
allModelsOnProxy: string[];
isLoading: boolean;
}
export function useModelDashboardData(): ModelDashboardData {
const { data: modelDataResponse, isLoading } = useModelsInfo();
const availableModelGroups = useMemo(() => {
const groups = new Set<string>(modelDataResponse?.data?.map((model) => model.model_name) ?? []);
return Array.from(groups).sort();
}, [modelDataResponse?.data]);
const availableModelAccessGroups = useMemo(() => {
const groups = new Set<string>(
modelDataResponse?.data?.flatMap((model) => model.model_info?.access_groups ?? []) ?? [],
);
return Array.from(groups);
}, [modelDataResponse?.data]);
const allModelsOnProxy = useMemo(
() => modelDataResponse?.data?.map((model) => model.model_name) ?? [],
[modelDataResponse?.data],
);
return { availableModelGroups, availableModelAccessGroups, allModelsOnProxy, isLoading };
}

View file

@ -0,0 +1,29 @@
import type { FormInstance, UploadProps } from "antd";
import NotificationsManager from "@/components/molecules/notifications_manager";
export function vertexCredentialsUploadProps(form: FormInstance): UploadProps {
return {
name: "file",
accept: ".json",
pastable: false,
beforeUpload: (file) => {
if (file.type === "application/json") {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target) {
form.setFieldsValue({ vertex_credentials: event.target.result as string });
}
};
reader.readAsText(file);
}
return false;
},
onChange(info) {
if (info.file.status === "done") {
NotificationsManager.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === "error") {
NotificationsManager.fromBackend(`${info.file.name} file upload failed.`);
}
},
};
}