mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-21 10:28:03 +00:00
[Feature] UI - Model Info Page Health Check (#16416)
* Add test connection button to model info page * Unused import
This commit is contained in:
@@ -33,7 +33,7 @@ import { OPEN_AI_VOICE_SELECT_OPTIONS, OpenAIVoice } from "./chatConstants";
|
||||
import ChatImageRenderer from "./ChatImageRenderer";
|
||||
import ChatImageUpload from "./ChatImageUpload";
|
||||
import { createChatDisplayMessage, createChatMultimodalMessage } from "./ChatImageUtils";
|
||||
import { truncateString } from "./chatUtils";
|
||||
import { truncateString } from "../../utils/textUtils";
|
||||
import { generateCodeSnippet } from "./CodeSnippets";
|
||||
import EndpointSelector from "./EndpointSelector";
|
||||
import { makeAnthropicMessagesRequest } from "./llm_calls/anthropic_messages";
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { truncateString } from "./chatUtils";
|
||||
|
||||
describe("chatUtils", () => {
|
||||
describe("truncateString", () => {
|
||||
it("should truncate a string", () => {
|
||||
expect(truncateString("Hello, world!", 5)).toBe("Hello...");
|
||||
});
|
||||
|
||||
it("should return the original string if it is less than the max length", () => {
|
||||
expect(truncateString("Hello, world!", 20)).toBe("Hello, world!");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export function truncateString(str: string, maxLength: number) {
|
||||
return str.length > maxLength ? str.substring(0, maxLength) + "..." : str;
|
||||
}
|
||||
@@ -127,23 +127,23 @@ describe("ModelInfoView", () => {
|
||||
supported_openai_params: ["temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty"],
|
||||
};
|
||||
|
||||
const DEFAULT_ADMIN_PROPS = {
|
||||
modelId: "123",
|
||||
onClose: () => {},
|
||||
modelData: modelData,
|
||||
accessToken: "123",
|
||||
userID: "123",
|
||||
userRole: "Admin",
|
||||
editModel: false,
|
||||
setEditModalVisible: () => {},
|
||||
setSelectedModel: () => {},
|
||||
onModelUpdate: () => {},
|
||||
modelAccessGroups: [],
|
||||
};
|
||||
|
||||
describe("Edit Model", () => {
|
||||
it("should render the model info view", async () => {
|
||||
const { getByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={modelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={false}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Model Settings")).toBeInTheDocument();
|
||||
});
|
||||
@@ -158,86 +158,93 @@ describe("ModelInfoView", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { queryByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={nonDbModelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={false}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
const NON_DB_ADMIN_PROPS = {
|
||||
...DEFAULT_ADMIN_PROPS,
|
||||
modelData: nonDbModelData,
|
||||
};
|
||||
|
||||
const { queryByText } = render(<ModelInfoView {...NON_DB_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(queryByText("Edit Model")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render tags in the edit model", async () => {
|
||||
const { getByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={modelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={true}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Tags")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a test connection button", async () => {
|
||||
const { getByTestId } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId("test-connection-button")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a reuse credentials button", async () => {
|
||||
const { getByTestId } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId("reuse-credentials-button")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a delete model button", async () => {
|
||||
const { getByTestId } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId("delete-model-button")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a disabled delete model button if the model is not a DB model", async () => {
|
||||
const nonDbModelData = {
|
||||
...modelData,
|
||||
model_info: {
|
||||
...modelData.model_info,
|
||||
db_model: false,
|
||||
},
|
||||
};
|
||||
const NON_DB_ADMIN_PROPS = {
|
||||
...DEFAULT_ADMIN_PROPS,
|
||||
modelData: nonDbModelData,
|
||||
};
|
||||
const { getByTestId } = render(<ModelInfoView {...NON_DB_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId("delete-model-button")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render a disabled delete model button if the user is not an admin and model is not created by the user", async () => {
|
||||
const nonCreatedByUserModelData = {
|
||||
...modelData,
|
||||
model_info: {
|
||||
...modelData.model_info,
|
||||
created_by: "456",
|
||||
},
|
||||
};
|
||||
const NON_CREATED_BY_USER_ADMIN_PROPS = {
|
||||
...DEFAULT_ADMIN_PROPS,
|
||||
modelData: nonCreatedByUserModelData,
|
||||
userRole: "User",
|
||||
};
|
||||
const { getByTestId } = render(<ModelInfoView {...NON_CREATED_BY_USER_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByTestId("delete-model-button")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("View Model", () => {
|
||||
it("should render the model info view", async () => {
|
||||
const { getByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={modelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={false}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Model Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render tags in the view model", async () => {
|
||||
const { getByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={modelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={false}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Tags")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { ArrowLeftIcon, KeyIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
Tab,
|
||||
TabList,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Grid,
|
||||
Button as TremorButton,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Button as TremorButton,
|
||||
} from "@tremor/react";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline";
|
||||
import { Button, Form, Input, Modal, Select, Tooltip } from "antd";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
|
||||
import { truncateString } from "../utils/textUtils";
|
||||
import CacheControlSettings from "./add_model/cache_control_settings";
|
||||
import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal";
|
||||
import ReuseCredentialsModal from "./model_add/reuse_credentials";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import {
|
||||
modelDeleteCall,
|
||||
CredentialItem,
|
||||
credentialGetCall,
|
||||
credentialCreateCall,
|
||||
credentialGetCall,
|
||||
getGuardrailsList,
|
||||
modelDeleteCall,
|
||||
modelInfoV1Call,
|
||||
modelPatchUpdateCall,
|
||||
getGuardrailsList,
|
||||
tagListCall,
|
||||
testConnectionRequest,
|
||||
} from "./networking";
|
||||
import { Button, Form, Input, Select, Modal, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { getProviderLogoAndName } from "./provider_info_helpers";
|
||||
import { getDisplayModelName } from "./view_model/model_name_display";
|
||||
import ReuseCredentialsModal from "./model_add/reuse_credentials";
|
||||
import CacheControlSettings from "./add_model/cache_control_settings";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
|
||||
import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import NumericalInput from "./shared/numerical_input";
|
||||
import { Tag } from "./tag_management/types";
|
||||
import { getDisplayModelName } from "./view_model/model_name_display";
|
||||
|
||||
interface ModelInfoViewProps {
|
||||
modelId: string;
|
||||
@@ -262,6 +264,37 @@ export default function ModelInfoView({
|
||||
);
|
||||
}
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
NotificationsManager.info("Testing connection...");
|
||||
const response = await testConnectionRequest(
|
||||
accessToken,
|
||||
{
|
||||
custom_llm_provider: localModelData.litellm_params.custom_llm_provider,
|
||||
litellm_credential_name: localModelData.litellm_params.litellm_credential_name,
|
||||
model: localModelData.litellm_model_name,
|
||||
},
|
||||
{
|
||||
mode: localModelData.model_info?.mode,
|
||||
},
|
||||
localModelData.model_info?.mode,
|
||||
);
|
||||
|
||||
if (response.status === "success") {
|
||||
NotificationsManager.success("Connection test successful!");
|
||||
} else {
|
||||
throw new Error(response?.result?.error || response?.message || "Unknown error");
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
NotificationsManager.error("Error testing connection: " + truncateString(error.message, 100));
|
||||
} else {
|
||||
NotificationsManager.error("Error testing connection: " + String(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
if (!accessToken) return;
|
||||
@@ -323,26 +356,36 @@ export default function ModelInfoView({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{isAdmin && (
|
||||
<TremorButton
|
||||
icon={KeyIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsCredentialModalOpen(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
Re-use Credentials
|
||||
</TremorButton>
|
||||
)}
|
||||
{canEditModel && (
|
||||
<TremorButton
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center"
|
||||
>
|
||||
Delete Model
|
||||
</TremorButton>
|
||||
)}
|
||||
<TremorButton
|
||||
variant="secondary"
|
||||
icon={RefreshIcon}
|
||||
onClick={handleTestConnection}
|
||||
className="flex items-center gap-2"
|
||||
data-testid="test-connection-button"
|
||||
>
|
||||
Test Connection
|
||||
</TremorButton>
|
||||
|
||||
<TremorButton
|
||||
icon={KeyIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsCredentialModalOpen(true)}
|
||||
className="flex items-center"
|
||||
disabled={!isAdmin}
|
||||
data-testid="reuse-credentials-button"
|
||||
>
|
||||
Re-use Credentials
|
||||
</TremorButton>
|
||||
<TremorButton
|
||||
icon={TrashIcon}
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center text-red-500 border-red-500"
|
||||
disabled={!canEditModel}
|
||||
data-testid="delete-model-button"
|
||||
>
|
||||
Delete Model
|
||||
</TremorButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { formatLabel } from "./textUtils";
|
||||
import { formatLabel, truncateString } from "./textUtils";
|
||||
|
||||
describe("textUtils", () => {
|
||||
describe("formatLabel", () => {
|
||||
it("should format label", () => {
|
||||
expect(formatLabel("test_label")).toBe("Test Label");
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateString", () => {
|
||||
it("should truncate a string", () => {
|
||||
expect(truncateString("Hello, world!", 5)).toBe("Hello...");
|
||||
});
|
||||
|
||||
it("should return the original string if it is less than the max length", () => {
|
||||
expect(truncateString("Hello, world!", 20)).toBe("Hello, world!");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,3 +6,7 @@ export const formatLabel = (text: string): string => {
|
||||
const withSpaces = text.replace(/_/g, " ");
|
||||
return withSpaces.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
};
|
||||
|
||||
export function truncateString(str: string, maxLength: number) {
|
||||
return str.length > maxLength ? str.substring(0, maxLength) + "..." : str;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user