mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-03 04:22:22 +00:00
Merge pull request #22316 from atapia27/reusable-credential-change
reusable-credentials
This commit is contained in:
@@ -55,4 +55,28 @@ describe("prepareModelAddRequest", () => {
|
||||
const [deployment] = deployments!;
|
||||
expect(deployment.litellmParamsObj.custom_llm_provider).toBe("petals");
|
||||
});
|
||||
|
||||
it("ignores litellm_credential_name inside LiteLLM Params JSON", async () => {
|
||||
const formValues = {
|
||||
model_mappings: [
|
||||
{
|
||||
public_name: "Public Model",
|
||||
litellm_model: "litellm/public",
|
||||
},
|
||||
],
|
||||
model_name: "custom-model-name",
|
||||
litellm_credential_name: "selected-credential",
|
||||
litellm_extra_params: JSON.stringify({
|
||||
litellm_credential_name: "from-json",
|
||||
timeout: 5,
|
||||
}),
|
||||
};
|
||||
|
||||
const deployments = await prepareModelAddRequest({ ...formValues }, "token", null);
|
||||
|
||||
expect(deployments).toHaveLength(1);
|
||||
const [deployment] = deployments!;
|
||||
expect(deployment.litellmParamsObj.litellm_credential_name).toBe("selected-credential");
|
||||
expect(deployment.litellmParamsObj.timeout).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +91,9 @@ export const prepareModelAddRequest = async (formValues: Record<string, any>, ac
|
||||
if (value && value != undefined) {
|
||||
try {
|
||||
litellmExtraParams = JSON.parse(value);
|
||||
if ("litellm_credential_name" in litellmExtraParams) {
|
||||
delete litellmExtraParams.litellm_credential_name;
|
||||
}
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend("Failed to parse LiteLLM Extra Params: " + error);
|
||||
throw new Error("Failed to parse litellm_extra_params: " + error);
|
||||
|
||||
@@ -23,6 +23,7 @@ vi.mock("./molecules/notifications_manager", () => ({
|
||||
vi.mock("./networking", () => ({
|
||||
modelInfoV1Call: vi.fn(),
|
||||
credentialGetCall: vi.fn(),
|
||||
credentialListCall: vi.fn(),
|
||||
getGuardrailsList: vi.fn(),
|
||||
tagListCall: vi.fn(),
|
||||
testConnectionRequest: vi.fn(),
|
||||
@@ -47,6 +48,7 @@ vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({
|
||||
const mockNotificationsManager = vi.mocked(NotificationsManager);
|
||||
const mockModelInfoV1Call = vi.mocked(networking.modelInfoV1Call);
|
||||
const mockCredentialGetCall = vi.mocked(networking.credentialGetCall);
|
||||
const mockCredentialListCall = vi.mocked(networking.credentialListCall);
|
||||
const mockGetGuardrailsList = vi.mocked(networking.getGuardrailsList);
|
||||
const mockTagListCall = vi.mocked(networking.tagListCall);
|
||||
const mockTestConnectionRequest = vi.mocked(networking.testConnectionRequest);
|
||||
@@ -63,6 +65,7 @@ describe("ModelInfoView", () => {
|
||||
model: "gpt-4",
|
||||
api_base: "https://api.openai.com/v1",
|
||||
custom_llm_provider: "openai",
|
||||
litellm_credential_name: "selected-credential",
|
||||
},
|
||||
model_info: {
|
||||
id: "123",
|
||||
@@ -125,6 +128,15 @@ describe("ModelInfoView", () => {
|
||||
credential_values: {},
|
||||
credential_info: {},
|
||||
});
|
||||
mockCredentialListCall.mockResolvedValue({
|
||||
credentials: [
|
||||
{
|
||||
credential_name: "selected-credential",
|
||||
credential_values: {},
|
||||
credential_info: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockGetGuardrailsList.mockResolvedValue({
|
||||
guardrails: [{ guardrail_name: "content_filter" }, { guardrail_name: "toxicity_filter" }],
|
||||
@@ -489,6 +501,57 @@ describe("ModelInfoView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show existing credentials field in edit mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /edit settings/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Existing Credentials")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should keep selector credential and ignore litellm_credential_name from LiteLLM Params json", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /edit settings/i }));
|
||||
|
||||
const litellmParamsInput = screen
|
||||
.getAllByRole("textbox")
|
||||
.find(
|
||||
(input) =>
|
||||
input.tagName === "TEXTAREA" &&
|
||||
(input as HTMLTextAreaElement).value.includes('"custom_llm_provider"'),
|
||||
);
|
||||
expect(litellmParamsInput).toBeDefined();
|
||||
if (!litellmParamsInput) {
|
||||
return;
|
||||
}
|
||||
expect((litellmParamsInput as HTMLTextAreaElement).value).not.toContain("litellm_credential_name");
|
||||
await user.clear(litellmParamsInput);
|
||||
await user.paste(`{"litellm_credential_name":"from-json","timeout":42}`);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockModelPatchUpdateCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const updatePayload = mockModelPatchUpdateCall.mock.calls[0][1];
|
||||
expect(updatePayload.litellm_params.litellm_credential_name).toBe("selected-credential");
|
||||
expect(updatePayload.litellm_params.litellm_credential_name).not.toBe("from-json");
|
||||
});
|
||||
|
||||
it("should display health check model field for wildcard models", async () => {
|
||||
const wildcardModelData = {
|
||||
...defaultModelData,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
CredentialItem,
|
||||
credentialCreateCall,
|
||||
credentialGetCall,
|
||||
credentialListCall,
|
||||
getGuardrailsList,
|
||||
modelDeleteCall,
|
||||
modelInfoV1Call,
|
||||
@@ -76,6 +77,7 @@ export default function ModelInfoView({
|
||||
const [isAutoRouterModalOpen, setIsAutoRouterModalOpen] = useState(false);
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
const [tagsList, setTagsList] = useState<Record<string, Tag>>({});
|
||||
const [credentialsList, setCredentialsList] = useState<CredentialItem[]>([]);
|
||||
|
||||
// Fetch model data using hook
|
||||
const { data: rawModelDataResponse, isLoading: isLoadingModel } = useModelsInfo(1, 50, undefined, modelId);
|
||||
@@ -192,10 +194,21 @@ export default function ModelInfoView({
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCredentials = async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
const response = await credentialListCall(accessToken);
|
||||
setCredentialsList(response.credentials || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch credentials:", error);
|
||||
}
|
||||
};
|
||||
|
||||
getExistingCredential();
|
||||
getModelInfo();
|
||||
fetchGuardrails();
|
||||
fetchTags();
|
||||
fetchCredentials();
|
||||
}, [accessToken, modelId]);
|
||||
|
||||
const handleReuseCredential = async (values: any) => {
|
||||
@@ -221,6 +234,7 @@ export default function ModelInfoView({
|
||||
let parsedExtraParams: Record<string, any> = {};
|
||||
try {
|
||||
parsedExtraParams = values.litellm_extra_params ? JSON.parse(values.litellm_extra_params) : {};
|
||||
delete parsedExtraParams.litellm_credential_name;
|
||||
} catch (e) {
|
||||
NotificationsManager.fromBackend("Invalid JSON in LiteLLM Params");
|
||||
setIsSaving(false);
|
||||
@@ -243,6 +257,11 @@ export default function ModelInfoView({
|
||||
output_cost_per_token: values.output_cost / 1_000_000,
|
||||
tags: values.tags,
|
||||
};
|
||||
if (values.litellm_credential_name) {
|
||||
updatedLitellmParams.litellm_credential_name = values.litellm_credential_name;
|
||||
} else {
|
||||
delete updatedLitellmParams.litellm_credential_name;
|
||||
}
|
||||
if (values.guardrails) {
|
||||
updatedLitellmParams.guardrails = values.guardrails;
|
||||
}
|
||||
@@ -617,7 +636,16 @@ export default function ModelInfoView({
|
||||
: [],
|
||||
tags: Array.isArray(localModelData.litellm_params?.tags) ? localModelData.litellm_params.tags : [],
|
||||
health_check_model: isWildcardModel ? localModelData.model_info?.health_check_model : null,
|
||||
litellm_extra_params: JSON.stringify(localModelData.litellm_params || {}, null, 2),
|
||||
litellm_credential_name: localModelData.litellm_params?.litellm_credential_name || "",
|
||||
litellm_extra_params: JSON.stringify(
|
||||
Object.fromEntries(
|
||||
Object.entries(localModelData.litellm_params || {}).filter(
|
||||
([key]) => key !== "litellm_credential_name",
|
||||
),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
}}
|
||||
layout="vertical"
|
||||
onValuesChange={() => setIsDirty(true)}
|
||||
@@ -991,6 +1019,33 @@ export default function ModelInfoView({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Existing Credentials</Text>
|
||||
{isEditing ? (
|
||||
<Form.Item name="litellm_credential_name" className="mb-0">
|
||||
<Select
|
||||
showSearch
|
||||
placeholder="Select or search for existing credentials"
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={[
|
||||
{ value: "", label: "None" },
|
||||
...credentialsList.map((credential) => ({
|
||||
value: credential.credential_name,
|
||||
label: credential.credential_name,
|
||||
})),
|
||||
]}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||
{localModelData.litellm_params?.litellm_credential_name || "Manual"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isWildcardModel && (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user