Merge pull request #22316 from atapia27/reusable-credential-change

reusable-credentials
This commit is contained in:
yuneng-jiang
2026-03-03 22:59:54 -08:00
committed by GitHub
4 changed files with 146 additions and 1 deletions
@@ -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>