Paginate All Models Tab

This commit is contained in:
yuneng-jiang
2026-01-21 11:53:15 -08:00
parent aa4b0e0149
commit 7cf80a9283
6 changed files with 937 additions and 157 deletions
@@ -0,0 +1,627 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import React, { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
useModelsInfo,
useModelHub,
useAllProxyModels,
useSelectedTeamModels,
type ProxyModel,
type AllProxyModelsResponse,
type PaginatedModelInfoResponse,
} from "./useModels";
vi.mock("@/components/networking", () => ({
modelInfoCall: vi.fn(),
modelHubCall: vi.fn(),
modelAvailableCall: vi.fn(),
}));
const mockUseAuthorized = vi.fn();
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => mockUseAuthorized(),
}));
import { modelInfoCall, modelHubCall, modelAvailableCall } from "@/components/networking";
const mockProxyModel: ProxyModel = {
id: "model-1",
object: "model",
created: 1234567890,
owned_by: "openai",
};
const mockPaginatedModelInfoResponse: PaginatedModelInfoResponse = {
data: [{ id: "model-1", name: "Test Model" }],
total_count: 1,
current_page: 1,
total_pages: 1,
size: 50,
};
const mockAllProxyModelsResponse: AllProxyModelsResponse = {
data: [mockProxyModel],
};
describe("useModelsInfo", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
vi.clearAllMocks();
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
});
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("should render without crashing", () => {
(modelInfoCall as any).mockResolvedValue(mockPaginatedModelInfoResponse);
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current).toBeDefined();
});
it("should return models data when query is successful", async () => {
(modelInfoCall as any).mockResolvedValue(mockPaginatedModelInfoResponse);
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockPaginatedModelInfoResponse);
expect(result.current.error).toBeNull();
expect(modelInfoCall).toHaveBeenCalledWith(
"test-access-token",
"test-user-id",
"Admin",
1,
50
);
expect(modelInfoCall).toHaveBeenCalledTimes(1);
});
it("should use custom page and size parameters", async () => {
(modelInfoCall as any).mockResolvedValue(mockPaginatedModelInfoResponse);
const { result } = renderHook(() => useModelsInfo(2, 25), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(modelInfoCall).toHaveBeenCalledWith(
"test-access-token",
"test-user-id",
"Admin",
2,
25
);
});
it("should handle error when modelInfoCall fails", async () => {
const errorMessage = "Failed to fetch models";
const testError = new Error(errorMessage);
(modelInfoCall as any).mockRejectedValue(testError);
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(testError);
expect(result.current.data).toBeUndefined();
expect(modelInfoCall).toHaveBeenCalledTimes(1);
});
it("should not execute query when accessToken is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: null,
userId: "test-user-id",
userRole: "Admin",
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelInfoCall).not.toHaveBeenCalled();
});
it("should not execute query when userId is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: null,
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelInfoCall).not.toHaveBeenCalled();
});
it("should not execute query when userRole is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: null,
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelInfoCall).not.toHaveBeenCalled();
});
it("should not execute query when all required auth values are missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: null,
userId: null,
userRole: null,
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useModelsInfo(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelInfoCall).not.toHaveBeenCalled();
});
});
describe("useModelHub", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
vi.clearAllMocks();
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
});
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("should render without crashing", () => {
(modelHubCall as any).mockResolvedValue({ data: [] });
const { result } = renderHook(() => useModelHub(), { wrapper });
expect(result.current).toBeDefined();
});
it("should return model hub data when query is successful", async () => {
const mockHubData = { data: [{ id: "hub-1", name: "Test Hub" }] };
(modelHubCall as any).mockResolvedValue(mockHubData);
const { result } = renderHook(() => useModelHub(), { wrapper });
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockHubData);
expect(result.current.error).toBeNull();
expect(modelHubCall).toHaveBeenCalledWith("test-access-token");
expect(modelHubCall).toHaveBeenCalledTimes(1);
});
it("should handle error when modelHubCall fails", async () => {
const errorMessage = "Failed to fetch model hub";
const testError = new Error(errorMessage);
(modelHubCall as any).mockRejectedValue(testError);
const { result } = renderHook(() => useModelHub(), { wrapper });
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(testError);
expect(result.current.data).toBeUndefined();
expect(modelHubCall).toHaveBeenCalledTimes(1);
});
it("should not execute query when accessToken is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: null,
userId: "test-user-id",
userRole: "Admin",
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useModelHub(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelHubCall).not.toHaveBeenCalled();
});
});
describe("useAllProxyModels", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
vi.clearAllMocks();
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
});
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("should render without crashing", () => {
(modelAvailableCall as any).mockResolvedValue(mockAllProxyModelsResponse);
const { result } = renderHook(() => useAllProxyModels(), { wrapper });
expect(result.current).toBeDefined();
});
it("should return all proxy models data when query is successful", async () => {
(modelAvailableCall as any).mockResolvedValue(mockAllProxyModelsResponse);
const { result } = renderHook(() => useAllProxyModels(), { wrapper });
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockAllProxyModelsResponse);
expect(result.current.error).toBeNull();
expect(modelAvailableCall).toHaveBeenCalledWith(
"test-access-token",
"test-user-id",
"Admin",
true
);
expect(modelAvailableCall).toHaveBeenCalledTimes(1);
});
it("should handle error when modelAvailableCall fails", async () => {
const errorMessage = "Failed to fetch proxy models";
const testError = new Error(errorMessage);
(modelAvailableCall as any).mockRejectedValue(testError);
const { result } = renderHook(() => useAllProxyModels(), { wrapper });
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(testError);
expect(result.current.data).toBeUndefined();
expect(modelAvailableCall).toHaveBeenCalledTimes(1);
});
it("should not execute query when accessToken is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: null,
userId: "test-user-id",
userRole: "Admin",
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useAllProxyModels(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
it("should not execute query when userId is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: null,
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useAllProxyModels(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
it("should not execute query when userRole is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: null,
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useAllProxyModels(), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
});
describe("useSelectedTeamModels", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
vi.clearAllMocks();
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
});
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("should render without crashing", () => {
(modelAvailableCall as any).mockResolvedValue(mockAllProxyModelsResponse);
const { result } = renderHook(() => useSelectedTeamModels("team-1"), { wrapper });
expect(result.current).toBeDefined();
});
it("should return team models data when query is successful", async () => {
(modelAvailableCall as any).mockResolvedValue(mockAllProxyModelsResponse);
const { result } = renderHook(() => useSelectedTeamModels("team-1"), { wrapper });
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockAllProxyModelsResponse);
expect(result.current.error).toBeNull();
expect(modelAvailableCall).toHaveBeenCalledWith(
"test-access-token",
"test-user-id",
"Admin",
true,
"team-1"
);
expect(modelAvailableCall).toHaveBeenCalledTimes(1);
});
it("should handle error when modelAvailableCall fails", async () => {
const errorMessage = "Failed to fetch team models";
const testError = new Error(errorMessage);
(modelAvailableCall as any).mockRejectedValue(testError);
const { result } = renderHook(() => useSelectedTeamModels("team-1"), { wrapper });
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(testError);
expect(result.current.data).toBeUndefined();
expect(modelAvailableCall).toHaveBeenCalledTimes(1);
});
it("should not execute query when teamID is null", () => {
const { result } = renderHook(() => useSelectedTeamModels(null), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
it("should not execute query when accessToken is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: null,
userId: "test-user-id",
userRole: "Admin",
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useSelectedTeamModels("team-1"), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
it("should not execute query when userId is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: null,
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useSelectedTeamModels("team-1"), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
it("should not execute query when userRole is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: null,
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useSelectedTeamModels("team-1"), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
it("should not execute query when teamID is missing and other auth values are present", () => {
const { result } = renderHook(() => useSelectedTeamModels(null), { wrapper });
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
});
@@ -14,21 +14,31 @@ export interface AllProxyModelsResponse {
data: ProxyModel[];
}
export interface PaginatedModelInfoResponse {
data: any[];
total_count: number;
current_page: number;
total_pages: number;
size: number;
}
const modelKeys = createQueryKeys("models");
const modelHubKeys = createQueryKeys("modelHub");
const allProxyModelsKeys = createQueryKeys("allProxyModels");
const selectedTeamModelsKeys = createQueryKeys("selectedTeamModels");
export const useModelsInfo = () => {
export const useModelsInfo = (page: number = 1, size: number = 50) => {
const { accessToken, userId, userRole } = useAuthorized();
return useQuery({
return useQuery<PaginatedModelInfoResponse>({
queryKey: modelKeys.list({
filters: {
...(userId && { userId }),
...(userRole && { userRole }),
page,
size,
},
}),
queryFn: async () => await modelInfoCall(accessToken!, userId!, userRole!),
queryFn: async () => await modelInfoCall(accessToken!, userId!, userRole!, page, size),
enabled: Boolean(accessToken && userId && userRole),
});
};
@@ -94,7 +94,8 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
}, [modelDataResponse?.data]);
const allModelsOnProxy = useMemo<string[]>(() => {
return modelDataResponse?.data?.map((model: any) => model.model_name);
if (!modelDataResponse?.data) return [];
return modelDataResponse.data.map((model: any) => model.model_name);
}, [modelDataResponse?.data]);
const getProviderFromModel = (model: string) => {
@@ -1,13 +1,18 @@
import * as useAuthorizedModule from "@/app/(dashboard)/hooks/useAuthorized";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AllModelsTab from "./AllModelsTab";
// Mock the useModelsInfo hook
const mockUseModelsInfo = vi.fn(() => ({ data: { data: [] } })) as any;
const mockUseModelsInfo = vi.fn(() => ({
data: { data: [], total_count: 0, current_page: 1, total_pages: 1, size: 50 },
isLoading: false,
error: null,
})) as any;
vi.mock("../../hooks/models/useModels", () => ({
useModelsInfo: () => mockUseModelsInfo(),
useModelsInfo: (page?: number, size?: number) => mockUseModelsInfo(page, size),
}));
// Mock the useModelCostMap hook
@@ -51,6 +56,21 @@ const createModelCostMapMock = (data: Record<string, any>) => ({
error: null,
});
// Helper function to create paginated model data mock
const createPaginatedModelData = (
models: any[],
totalCount: number = models.length,
currentPage: number = 1,
totalPages: number = 1,
size: number = 50,
) => ({
data: models,
total_count: totalCount,
current_page: currentPage,
total_pages: totalPages,
size: size,
});
describe("AllModelsTab", () => {
const mockSetSelectedModelGroup = vi.fn();
const mockSetSelectedModelId = vi.fn();
@@ -84,7 +104,11 @@ describe("AllModelsTab", () => {
});
it("should render with empty data", () => {
mockUseModelsInfo.mockReturnValueOnce({ data: { data: [] } });
mockUseModelsInfo.mockReturnValueOnce({
data: createPaginatedModelData([], 0, 1, 1, 50),
isLoading: false,
error: null,
});
mockUseTeams.mockReturnValueOnce({
data: [],
@@ -130,28 +154,26 @@ describe("AllModelsTab", () => {
}),
);
const modelData = {
data: [
{
model_name: "gpt-4-accessible",
model_info: {
id: "model-1",
access_via_team_ids: ["team-456"],
access_groups: [],
},
const modelData = createPaginatedModelData([
{
model_name: "gpt-4-accessible",
model_info: {
id: "model-1",
access_via_team_ids: ["team-456"],
access_groups: [],
},
{
model_name: "gpt-3.5-turbo-blocked",
model_info: {
id: "model-2",
access_via_team_ids: ["team-789"],
access_groups: [],
},
},
{
model_name: "gpt-3.5-turbo-blocked",
model_info: {
id: "model-2",
access_via_team_ids: ["team-789"],
access_groups: [],
},
],
};
},
], 2, 1, 1, 50);
mockUseModelsInfo.mockReturnValue({ data: modelData });
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null });
render(<AllModelsTab {...defaultProps} />);
@@ -191,28 +213,26 @@ describe("AllModelsTab", () => {
}),
);
const modelData = {
data: [
{
model_name: "gpt-4-sales",
model_info: {
id: "model-sales-1",
access_via_team_ids: [],
access_groups: ["sales-model-group"],
},
const modelData = createPaginatedModelData([
{
model_name: "gpt-4-sales",
model_info: {
id: "model-sales-1",
access_via_team_ids: [],
access_groups: ["sales-model-group"],
},
{
model_name: "gpt-4-engineering",
model_info: {
id: "model-eng-1",
access_via_team_ids: [],
access_groups: ["engineering-model-group"],
},
},
{
model_name: "gpt-4-engineering",
model_info: {
id: "model-eng-1",
access_via_team_ids: [],
access_groups: ["engineering-model-group"],
},
],
};
},
], 2, 1, 1, 50);
mockUseModelsInfo.mockReturnValue({ data: modelData });
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null });
render(<AllModelsTab {...defaultProps} />);
@@ -236,30 +256,28 @@ describe("AllModelsTab", () => {
}),
);
const modelData = {
data: [
{
model_name: "gpt-4-personal",
model_info: {
id: "model-personal-1",
direct_access: true,
access_via_team_ids: [],
access_groups: [],
},
const modelData = createPaginatedModelData([
{
model_name: "gpt-4-personal",
model_info: {
id: "model-personal-1",
direct_access: true,
access_via_team_ids: [],
access_groups: [],
},
{
model_name: "gpt-4-team-only",
model_info: {
id: "model-team-1",
direct_access: false,
access_via_team_ids: ["team-123"],
access_groups: [],
},
},
{
model_name: "gpt-4-team-only",
model_info: {
id: "model-team-1",
direct_access: false,
access_via_team_ids: ["team-123"],
access_groups: [],
},
],
};
},
], 2, 1, 1, 50);
mockUseModelsInfo.mockReturnValue({ data: modelData });
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null });
render(<AllModelsTab {...defaultProps} />);
@@ -283,42 +301,40 @@ describe("AllModelsTab", () => {
}),
);
const modelData = {
data: [
{
model_name: "gpt-4-config",
litellm_model_name: "gpt-4-config",
provider: "openai",
model_info: {
id: "model-config-1",
db_model: false,
direct_access: true,
access_via_team_ids: [],
access_groups: [],
created_by: "user-123",
created_at: "2024-01-01",
updated_at: "2024-01-01",
},
const modelData = createPaginatedModelData([
{
model_name: "gpt-4-config",
litellm_model_name: "gpt-4-config",
provider: "openai",
model_info: {
id: "model-config-1",
db_model: false,
direct_access: true,
access_via_team_ids: [],
access_groups: [],
created_by: "user-123",
created_at: "2024-01-01",
updated_at: "2024-01-01",
},
{
model_name: "gpt-4-db",
litellm_model_name: "gpt-4-db",
provider: "openai",
model_info: {
id: "model-db-1",
db_model: true,
direct_access: true,
access_via_team_ids: [],
access_groups: [],
created_by: "user-123",
created_at: "2024-01-01",
updated_at: "2024-01-01",
},
},
{
model_name: "gpt-4-db",
litellm_model_name: "gpt-4-db",
provider: "openai",
model_info: {
id: "model-db-1",
db_model: true,
direct_access: true,
access_via_team_ids: [],
access_groups: [],
created_by: "user-123",
created_at: "2024-01-01",
updated_at: "2024-01-01",
},
],
};
},
], 2, 1, 1, 50);
mockUseModelsInfo.mockReturnValue({ data: modelData });
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null });
render(<AllModelsTab {...defaultProps} />);
@@ -342,27 +358,25 @@ describe("AllModelsTab", () => {
}),
);
const modelData = {
data: [
{
model_name: "gpt-4-config",
litellm_model_name: "gpt-4-config",
provider: "openai",
model_info: {
id: "model-config-1",
db_model: false,
direct_access: true,
access_via_team_ids: [],
access_groups: [],
created_by: "user-123",
created_at: "2024-01-01",
updated_at: "2024-01-01",
},
const modelData = createPaginatedModelData([
{
model_name: "gpt-4-config",
litellm_model_name: "gpt-4-config",
provider: "openai",
model_info: {
id: "model-config-1",
db_model: false,
direct_access: true,
access_via_team_ids: [],
access_groups: [],
created_by: "user-123",
created_at: "2024-01-01",
updated_at: "2024-01-01",
},
],
};
},
], 1, 1, 1, 50);
mockUseModelsInfo.mockReturnValue({ data: modelData });
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null });
render(<AllModelsTab {...defaultProps} />);
@@ -370,4 +384,110 @@ describe("AllModelsTab", () => {
expect(screen.getByText("Defined in config")).toBeInTheDocument();
});
});
it("should handle pagination: Previous button is disabled on first page and Next button works", async () => {
mockUseTeams.mockReturnValue({
data: [],
isLoading: false,
error: null,
refetch: vi.fn(),
});
mockUseModelCostMap.mockReturnValue(
createModelCostMapMock({
"gpt-4-page1": { litellm_provider: "openai" },
"gpt-4-page2": { litellm_provider: "openai" },
}),
);
// Mock first page response (page 1 of 2)
const page1Data = createPaginatedModelData(
[
{
model_name: "gpt-4-page1",
model_info: {
id: "model-page1-1",
direct_access: true,
access_via_team_ids: [],
access_groups: [],
},
},
],
2, // total_count
1, // current_page
2, // total_pages
50, // size
);
// Set up mock to return page1Data for page 1
mockUseModelsInfo.mockImplementation((page: number = 1) => {
return { data: page1Data, isLoading: false, error: null };
});
render(<AllModelsTab {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("Showing 1 - 1 of 1 results")).toBeInTheDocument();
});
// Check that Previous button is disabled on first page
const previousButton = screen.getByRole("button", { name: /previous/i });
expect(previousButton).toBeDisabled();
// Check that Next button is enabled (since we're on page 1 of 2)
const nextButton = screen.getByRole("button", { name: /next/i });
expect(nextButton).not.toBeDisabled();
});
it("should handle pagination: Next button is disabled on last page", async () => {
mockUseTeams.mockReturnValue({
data: [],
isLoading: false,
error: null,
refetch: vi.fn(),
});
mockUseModelCostMap.mockReturnValue(
createModelCostMapMock({
"gpt-4-page2": { litellm_provider: "openai" },
}),
);
// Mock single page response (page 1 of 1 - last page)
const singlePageData = createPaginatedModelData(
[
{
model_name: "gpt-4-page2",
model_info: {
id: "model-page2-1",
direct_access: true,
access_via_team_ids: [],
access_groups: [],
},
},
],
1, // total_count
1, // current_page
1, // total_pages (only 1 page, so this is the last page)
50, // size
);
mockUseModelsInfo.mockImplementation(() => {
return { data: singlePageData, isLoading: false, error: null };
});
render(<AllModelsTab {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("Showing 1 - 1 of 1 results")).toBeInTheDocument();
});
// When there's only 1 page (last page), Next should be disabled
const nextButton = screen.getByRole("button", { name: /next/i });
expect(nextButton).toBeDisabled();
// Previous should also be disabled on the first (and only) page
const previousButton = screen.getByRole("button", { name: /previous/i });
expect(previousButton).toBeDisabled();
});
});
@@ -31,11 +31,26 @@ const AllModelsTab = ({
setSelectedModelId,
setSelectedTeamId,
}: AllModelsTabProps) => {
const { data: rawModelData, isLoading: isLoadingModelsInfo } = useModelsInfo();
const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap();
const { userId, userRole, premiumUser } = useAuthorized();
const { data: teams } = useTeams();
const [modelNameSearch, setModelNameSearch] = useState<string>("");
const [modelViewMode, setModelViewMode] = useState<ModelViewMode>("current_team");
const [currentTeam, setCurrentTeam] = useState<Team | "personal">("personal");
const [showFilters, setShowFilters] = useState<boolean>(false);
const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState<string | null>(null);
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [currentPage, setCurrentPage] = useState<number>(1);
const [pageSize] = useState<number>(50);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 50,
});
const { data: rawModelData, isLoading: isLoadingModelsInfo } = useModelsInfo(currentPage, pageSize);
const isLoading = isLoadingModelsInfo || isLoadingModelCostMap;
const getProviderFromModel = (model: string) => {
if (modelCostMapData !== null && modelCostMapData !== undefined) {
if (typeof modelCostMapData == "object" && model in modelCostMapData) {
@@ -50,18 +65,23 @@ const AllModelsTab = ({
return transformModelData(rawModelData, getProviderFromModel);
}, [rawModelData, modelCostMapData]);
const [modelNameSearch, setModelNameSearch] = useState<string>("");
const [modelViewMode, setModelViewMode] = useState<ModelViewMode>("current_team");
const [currentTeam, setCurrentTeam] = useState<Team | "personal">("personal");
const [showFilters, setShowFilters] = useState<boolean>(false);
const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState<string | null>(null);
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 50,
});
const isLoading = isLoadingModelsInfo || isLoadingModelCostMap;
// Get pagination metadata from the response
const paginationMeta = useMemo(() => {
if (!rawModelData) {
return {
total_count: 0,
current_page: 1,
total_pages: 1,
size: pageSize,
};
}
return {
total_count: rawModelData.total_count ?? 0,
current_page: rawModelData.current_page ?? 1,
total_pages: rawModelData.total_pages ?? 1,
size: rawModelData.size ?? pageSize,
};
}, [rawModelData, pageSize]);
const filteredData = useMemo(() => {
if (!modelData || !modelData.data || modelData.data.length === 0) {
@@ -114,6 +134,7 @@ const AllModelsTab = ({
setSelectedModelAccessGroupFilter(null);
setCurrentTeam("personal");
setModelViewMode("current_team");
setCurrentPage(1);
setPagination({ pageIndex: 0, pageSize: 50 });
};
@@ -334,10 +355,7 @@ const AllModelsTab = ({
) : (
<span className="text-sm text-gray-700">
{filteredData.length > 0
? `Showing ${pagination.pageIndex * pagination.pageSize + 1} - ${Math.min(
(pagination.pageIndex + 1) * pagination.pageSize,
filteredData.length,
)} of ${filteredData.length} results`
? `Showing 1 - ${filteredData.length} of ${filteredData.length} results`
: "Showing 0 results"}
</span>
)}
@@ -347,15 +365,16 @@ const AllModelsTab = ({
<Skeleton.Button active style={{ width: 84, height: 30 }} />
) : (
<button
onClick={() =>
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: prev.pageIndex - 1 }))
}
disabled={pagination.pageIndex === 0}
className={`px-3 py-1 text-sm border rounded-md ${
pagination.pageIndex === 0
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "hover:bg-gray-50"
}`}
onClick={() => {
const newPage = currentPage - 1;
setCurrentPage(newPage);
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
}}
disabled={currentPage === 1}
className={`px-3 py-1 text-sm border rounded-md ${currentPage === 1
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "hover:bg-gray-50"
}`}
>
Previous
</button>
@@ -365,15 +384,16 @@ const AllModelsTab = ({
<Skeleton.Button active style={{ width: 56, height: 30 }} />
) : (
<button
onClick={() =>
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: prev.pageIndex + 1 }))
}
disabled={pagination.pageIndex >= Math.ceil(filteredData.length / pagination.pageSize) - 1}
className={`px-3 py-1 text-sm border rounded-md ${
pagination.pageIndex >= Math.ceil(filteredData.length / pagination.pageSize) - 1
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "hover:bg-gray-50"
}`}
onClick={() => {
const newPage = currentPage + 1;
setCurrentPage(newPage);
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
}}
disabled={currentPage >= paginationMeta.total_pages}
className={`px-3 py-1 text-sm border rounded-md ${currentPage >= paginationMeta.total_pages
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "hover:bg-gray-50"
}`}
>
Next
</button>
@@ -391,8 +411,8 @@ const AllModelsTab = ({
setSelectedModelId,
setSelectedTeamId,
getDisplayModelName,
() => {},
() => {},
() => { },
() => { },
expandedRows,
setExpandedRows,
)}
@@ -2007,15 +2007,17 @@ export const regenerateKeyCall = async (accessToken: string, keyToRegenerate: st
let ModelListerrorShown = false;
let errorTimer: NodeJS.Timeout | null = null;
export const modelInfoCall = async (accessToken: string, userID: string, userRole: string) => {
export const modelInfoCall = async (accessToken: string, userID: string, userRole: string, page: number = 1, size: number = 50) => {
/**
* Get all models on proxy
*/
try {
console.log("modelInfoCall:", accessToken, userID, userRole);
console.log("modelInfoCall:", accessToken, userID, userRole, page, size);
let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/model/info` : `/v2/model/info`;
const params = new URLSearchParams();
params.append("include_team_models", "true");
params.append("page", page.toString());
params.append("size", size.toString());
if (params.toString()) {
url += `?${params.toString()}`;
}