mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
[Feature] UI - Add Tags to update model (#16140)
* Add Tags to update model * Test Key Page Tag Selector
This commit is contained in:
parent
dd053e1d12
commit
cf0d84c789
4 changed files with 207 additions and 48 deletions
|
|
@ -127,53 +127,120 @@ describe("ModelInfoView", () => {
|
|||
supported_openai_params: ["temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty"],
|
||||
};
|
||||
|
||||
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={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Model Settings")).toBeInTheDocument();
|
||||
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={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Model Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render an edit model button if the model is not a DB model", async () => {
|
||||
const nonDbModelData = {
|
||||
...modelData,
|
||||
model_info: {
|
||||
...modelData.model_info,
|
||||
db_model: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { queryByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={nonDbModelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={false}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
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={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Tags")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render an edit model button if the model is not a DB model", async () => {
|
||||
const nonDbModelData = {
|
||||
...modelData,
|
||||
model_info: {
|
||||
...modelData.model_info,
|
||||
db_model: false,
|
||||
},
|
||||
};
|
||||
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={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Model Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const { queryByText } = render(
|
||||
<ModelInfoView
|
||||
modelId="123"
|
||||
onClose={() => {}}
|
||||
modelData={nonDbModelData}
|
||||
accessToken="123"
|
||||
userID="123"
|
||||
userRole="Admin"
|
||||
editModel={false}
|
||||
setEditModalVisible={() => {}}
|
||||
setSelectedModel={() => {}}
|
||||
onModelUpdate={() => {}}
|
||||
modelAccessGroups={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(queryByText("Edit Model")).not.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={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("Tags")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
modelInfoV1Call,
|
||||
modelPatchUpdateCall,
|
||||
getGuardrailsList,
|
||||
tagListCall,
|
||||
} from "./networking";
|
||||
import { Button, Form, Input, Select, Modal, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
|
|
@ -33,6 +34,7 @@ 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 { Tag } from "./tag_management/types";
|
||||
|
||||
interface ModelInfoViewProps {
|
||||
modelId: string;
|
||||
|
|
@ -73,6 +75,7 @@ export default function ModelInfoView({
|
|||
const [copiedStates, setCopiedStates] = useState<Record<string, boolean>>({});
|
||||
const [isAutoRouterModalOpen, setIsAutoRouterModalOpen] = useState(false);
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const [tagsList, setTagsList] = useState<Record<string, Tag>>({});
|
||||
const canEditModel =
|
||||
(userRole === "Admin" || modelData?.model_info?.created_by === userID) && modelData?.model_info?.db_model;
|
||||
const isAdmin = userRole === "Admin";
|
||||
|
|
@ -84,6 +87,8 @@ export default function ModelInfoView({
|
|||
console.log("usingExistingCredential, ", usingExistingCredential);
|
||||
console.log("modelData.litellm_params.litellm_credential_name, ", modelData?.litellm_params?.litellm_credential_name);
|
||||
|
||||
console.log("tagsList, ", modelData.litellm_params?.tags);
|
||||
|
||||
useEffect(() => {
|
||||
const getExistingCredential = async () => {
|
||||
console.log("accessToken, ", accessToken);
|
||||
|
|
@ -132,9 +137,20 @@ export default function ModelInfoView({
|
|||
}
|
||||
};
|
||||
|
||||
const fetchTags = async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
const response = await tagListCall(accessToken);
|
||||
setTagsList(response);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch tags:", error);
|
||||
}
|
||||
};
|
||||
|
||||
getExistingCredential();
|
||||
getModelInfo();
|
||||
fetchGuardrails();
|
||||
fetchTags();
|
||||
}, [accessToken, modelId]);
|
||||
|
||||
const handleReuseCredential = async (values: any) => {
|
||||
|
|
@ -173,6 +189,7 @@ export default function ModelInfoView({
|
|||
stream_timeout: values.stream_timeout,
|
||||
input_cost_per_token: values.input_cost / 1_000_000,
|
||||
output_cost_per_token: values.output_cost / 1_000_000,
|
||||
tags: values.tags,
|
||||
};
|
||||
if (values.guardrails) {
|
||||
updatedLitellmParams.guardrails = values.guardrails;
|
||||
|
|
@ -476,6 +493,7 @@ export default function ModelInfoView({
|
|||
guardrails: Array.isArray(localModelData.litellm_params?.guardrails)
|
||||
? localModelData.litellm_params.guardrails
|
||||
: [],
|
||||
tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [],
|
||||
}}
|
||||
layout="vertical"
|
||||
onValuesChange={() => setIsDirty(true)}
|
||||
|
|
@ -691,7 +709,7 @@ export default function ModelInfoView({
|
|||
|
||||
<div>
|
||||
<Text className="font-medium">
|
||||
Guardrails{" "}
|
||||
Guardrails
|
||||
<Tooltip title="Apply safety guardrails to this model to filter content or enforce policies">
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||
|
|
@ -750,6 +768,54 @@ export default function ModelInfoView({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Tags</Text>
|
||||
{isEditing ? (
|
||||
<Form.Item name="tags" className="mb-0">
|
||||
<Select
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select existing tags or type to create new ones"
|
||||
optionFilterProp="children"
|
||||
tokenSeparators={[","]}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
options={Object.values(tagsList).map((tag: Tag) => ({
|
||||
value: tag.name,
|
||||
label: tag.name,
|
||||
title: tag.description || tag.name,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||
{localModelData.litellm_params?.tags ? (
|
||||
Array.isArray(localModelData.litellm_params.tags) ? (
|
||||
localModelData.litellm_params.tags.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{localModelData.litellm_params.tags.map((tag: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
"No tags assigned"
|
||||
)
|
||||
) : (
|
||||
localModelData.litellm_params.tags
|
||||
)
|
||||
) : (
|
||||
"Not Set"
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cache Control Section */}
|
||||
{isEditing ? (
|
||||
<CacheControlSettings
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import TagSelector from "./TagSelector";
|
||||
|
||||
describe("TagSelector", () => {
|
||||
it("should render the tag selector", () => {
|
||||
render(<TagSelector onChange={() => {}} accessToken="test-token" />);
|
||||
});
|
||||
|
||||
it("should allow creating new tags", () => {
|
||||
const { container } = render(<TagSelector onChange={() => {}} accessToken="test-token" />);
|
||||
const tagSelector = container.querySelector("input");
|
||||
expect(tagSelector).toBeInTheDocument();
|
||||
if (tagSelector) {
|
||||
fireEvent.change(tagSelector, { target: { value: "new-tag" } });
|
||||
expect(tagSelector).toHaveValue("new-tag");
|
||||
fireEvent.keyDown(tagSelector, { key: "Enter" });
|
||||
expect(tagSelector).toHaveValue("new-tag");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -23,16 +23,19 @@ const TagSelector: React.FC<TagSelectorProps> = ({ onChange, value, className, a
|
|||
setTags(Object.values(response));
|
||||
} catch (error) {
|
||||
console.error("Error fetching tags:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTags();
|
||||
}, []);
|
||||
}, [accessToken]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select tags"
|
||||
mode="tags"
|
||||
showSearch
|
||||
placeholder="Select or create tags"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
loading={loading}
|
||||
|
|
@ -43,7 +46,9 @@ const TagSelector: React.FC<TagSelectorProps> = ({ onChange, value, className, a
|
|||
title: tag.description || tag.name,
|
||||
}))}
|
||||
optionFilterProp="label"
|
||||
showSearch
|
||||
tokenSeparators={[","]}
|
||||
maxTagCount="responsive"
|
||||
allowClear
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue