[Feature] UI - Add LiteLLM Params to Edit Model (#16496)

* Add LiteLLM Params to Edit Model

* Fixed tests
This commit is contained in:
yuneng-jiang 2025-11-11 18:52:11 -08:00 committed by GitHub
parent 7dd76bc4e3
commit b9759b4bfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 99 additions and 18 deletions

View file

@ -1,4 +1,4 @@
import { fireEvent, render, waitFor } from "@testing-library/react";
import { act, fireEvent, render, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AdvancedSettings from "./advanced_settings";
@ -31,4 +31,21 @@ describe("AdvancedSettings", () => {
expect(getByText("Tags")).toBeInTheDocument();
});
});
it("should render the litellm params", async () => {
const { getByText } = render(
<AdvancedSettings
showAdvancedSettings={true}
setShowAdvancedSettings={() => {}}
guardrailsList={[]}
tagsList={{}}
/>,
);
act(() => {
fireEvent.click(getByText("Advanced Settings"));
});
await waitFor(() => {
expect(getByText("LiteLLM Params")).toBeInTheDocument();
});
});
});

View file

@ -7,6 +7,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
import { Team } from "../key_team_helpers/key_list";
import CacheControlSettings from "./cache_control_settings";
import { Tag } from "../tag_management/types";
import { formItemValidateJSON } from "../../utils/textUtils";
const { Link } = Typography;
interface AdvancedSettingsProps {
@ -40,18 +41,6 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
return Promise.resolve();
};
const validateJSON = (_: any, value: string) => {
if (!value) {
return Promise.resolve();
}
try {
JSON.parse(value);
return Promise.resolve();
} catch (error) {
return Promise.reject("Please enter valid JSON");
}
};
// Handle custom pricing changes
const handleCustomPricingChange = (checked: boolean) => {
setCustomPricing(checked);
@ -233,7 +222,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
name="litellm_extra_params"
tooltip="Optional litellm params used for making a litellm.completion() call."
className="mb-4 mt-4"
rules={[{ validator: validateJSON }]}
rules={[{ validator: formItemValidateJSON }]}
>
<TextArea
rows={4}
@ -260,7 +249,7 @@ const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({
name="model_info_params"
tooltip="Optional model info params. Returned when calling `/model/info` endpoint."
className="mb-0"
rules={[{ validator: validateJSON }]}
rules={[{ validator: formItemValidateJSON }]}
>
<TextArea
rows={4}

View file

@ -175,6 +175,13 @@ describe("ModelInfoView", () => {
expect(getByText("Tags")).toBeInTheDocument();
});
});
it("should render the litellm params in the edit model", async () => {
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByText("LiteLLM Params")).toBeInTheDocument();
});
});
});
it("should render a test connection button", async () => {

View file

@ -17,7 +17,7 @@ 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 { formItemValidateJSON, 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";
@ -178,8 +178,19 @@ export default function ModelInfoView({
console.log("values.model_name, ", values.model_name);
// Parse LiteLLM extra params from JSON text area
let parsedExtraParams: Record<string, any> = {};
try {
parsedExtraParams = values.litellm_extra_params ? JSON.parse(values.litellm_extra_params) : {};
} catch (e) {
NotificationsManager.fromBackend("Invalid JSON in LiteLLM Params");
setIsSaving(false);
return;
}
let updatedLitellmParams = {
...localModelData.litellm_params,
...values.litellm_params,
...parsedExtraParams,
model: values.litellm_model_name,
api_base: values.api_base,
custom_llm_provider: values.custom_llm_provider,
@ -537,6 +548,7 @@ export default function ModelInfoView({
? localModelData.litellm_params.guardrails
: [],
tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [],
litellm_extra_params: JSON.stringify(localModelData.litellm_params || {}, null, 2),
}}
layout="vertical"
onValuesChange={() => setIsDirty(true)}
@ -909,6 +921,39 @@ export default function ModelInfoView({
</div>
)}
</div>
<div>
<Text className="font-medium">
LiteLLM Params
<Tooltip title="Optional litellm params used for making a litellm.completion() call. Some params are automatically added by LiteLLM.">
<a
href="https://docs.litellm.ai/docs/completion/input"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</Text>
{isEditing ? (
<Form.Item name="litellm_extra_params" rules={[{ validator: formItemValidateJSON }]}>
<Input.TextArea
rows={4}
placeholder='{
"rpm": 100,
"timeout": 0,
"stream_timeout": 0
}'
/>
</Form.Item>
) : (
<div className="mt-1 p-2 bg-gray-50 rounded">
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
{JSON.stringify(localModelData.litellm_params, null, 2)}
</pre>
</div>
)}
</div>
<div>
<Text className="font-medium">Team ID</Text>
<div className="mt-1 p-2 bg-gray-50 rounded">{modelData.model_info.team_id || "Not Set"}</div>

View file

@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { formatLabel, truncateString } from "./textUtils";
import { formatLabel, formItemValidateJSON, truncateString } from "./textUtils";
describe("formatLabel", () => {
it("should format label", () => {
@ -16,3 +16,14 @@ describe("truncateString", () => {
expect(truncateString("Hello, world!", 20)).toBe("Hello, world!");
});
});
describe("formItemValidateJSON", () => {
it("should resolve for a valid JSON", async () => {
const validObj = { a: 1, b: "x", c: true, d: [1, 2], e: { f: "y" } };
await expect(formItemValidateJSON({}, JSON.stringify(validObj))).resolves.toBeUndefined();
});
it("should reject with an error message for invalid JSON", async () => {
await expect(formItemValidateJSON({}, "invalid JSON")).rejects.toBe("Please enter valid JSON");
});
});

View file

@ -10,3 +10,15 @@ export const formatLabel = (text: string): string => {
export function truncateString(str: string, maxLength: number) {
return str.length > maxLength ? str.substring(0, maxLength) + "..." : str;
}
export const formItemValidateJSON = (_: any, value: string) => {
if (!value) {
return Promise.resolve();
}
try {
JSON.parse(value);
return Promise.resolve();
} catch (error) {
return Promise.reject("Please enter valid JSON");
}
};