Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_list_vs_fil

This commit is contained in:
Shivam Rawat 2026-07-18 11:25:20 -07:00
commit e272e621e2
7 changed files with 191 additions and 423 deletions

View file

@ -1885,19 +1885,11 @@
"count": 1
}
},
"src/components/model_add/AddCredentialModal.tsx": {
"src/components/model_add/CredentialModal.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/model_add/EditCredentialModal.tsx": {
"no-restricted-imports": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/components/model_add/credentials.tsx": {
"no-restricted-imports": {
"count": 1

View file

@ -1,108 +0,0 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Providers } from "../provider_info_helpers";
import AddCredentialModal from "./AddCredentialModal";
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
...actual,
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
provider_display_name: Providers.OpenAI,
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [
{
key: "api_key",
label: "OpenAI API Key",
field_type: "password",
required: true,
},
{
key: "api_base",
label: "API Base",
field_type: "text",
placeholder: "https://api.openai.com/v1",
},
],
},
{
provider: "Anthropic",
provider_display_name: Providers.Anthropic,
litellm_provider: "anthropic",
default_model_placeholder: "claude-3-opus-20240229",
credential_fields: [
{
key: "api_key",
label: "Anthropic API Key",
field_type: "password",
required: true,
},
],
},
]),
};
});
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
const mockUploadProps = {
beforeUpload: vi.fn(),
onChange: vi.fn(),
};
describe("AddCredentialModal", () => {
it("should render", () => {
const queryClient = createQueryClient();
const onCancel = vi.fn();
const onAddCredential = vi.fn();
render(
<QueryClientProvider client={queryClient}>
<AddCredentialModal
open={true}
onCancel={onCancel}
onAddCredential={onAddCredential}
uploadProps={mockUploadProps}
/>
</QueryClientProvider>,
);
expect(screen.getByText("Add New Credential")).toBeInTheDocument();
expect(screen.getByLabelText("Credential Name:")).toBeInTheDocument();
expect(screen.getByLabelText("Provider:")).toBeInTheDocument();
});
it("should show the correct provider fields", async () => {
const queryClient = createQueryClient();
const onCancel = vi.fn();
const onAddCredential = vi.fn();
render(
<QueryClientProvider client={queryClient}>
<AddCredentialModal
open={true}
onCancel={onCancel}
onAddCredential={onAddCredential}
uploadProps={mockUploadProps}
/>
</QueryClientProvider>,
);
await waitFor(() => {
expect(screen.getByLabelText("OpenAI API Key")).toBeInTheDocument();
expect(screen.getByPlaceholderText("https://api.openai.com/v1")).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,140 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Providers } from "../provider_info_helpers";
import { CredentialItem } from "../networking";
import CredentialModal from "./CredentialModal";
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
...actual,
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
provider_display_name: Providers.OpenAI,
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [
{
key: "api_key",
label: "OpenAI API Key",
field_type: "password",
required: true,
},
{
key: "api_base",
label: "API Base",
field_type: "text",
placeholder: "https://api.openai.com/v1",
},
],
},
{
provider: "Anthropic",
provider_display_name: Providers.Anthropic,
litellm_provider: "anthropic",
default_model_placeholder: "claude-3-opus-20240229",
credential_fields: [
{
key: "api_key",
label: "Anthropic API Key",
field_type: "password",
required: true,
},
],
},
]),
};
});
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
const mockUploadProps = {
beforeUpload: vi.fn(),
onChange: vi.fn(),
};
const mockCredential: CredentialItem = {
credential_name: "test-credential",
credential_values: {
api_key: "test-api-key",
api_base: "https://api.test.com",
},
credential_info: {
custom_llm_provider: Providers.OpenAI,
},
};
const renderModal = (props: Partial<React.ComponentProps<typeof CredentialModal>> = {}) =>
render(
<QueryClientProvider client={createQueryClient()}>
<CredentialModal
open={true}
mode="add"
onCancel={vi.fn()}
onSubmit={vi.fn()}
uploadProps={mockUploadProps}
{...props}
/>
</QueryClientProvider>,
);
describe("CredentialModal", () => {
describe("add mode", () => {
it("renders the add title and an editable credential name", () => {
renderModal({ mode: "add" });
expect(screen.getByText("Add New Credential")).toBeInTheDocument();
expect(screen.getByText("Add Credential")).toBeInTheDocument();
const nameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement;
expect(nameInput.value).toBe("");
expect(nameInput.disabled).toBe(false);
});
it("shows provider-specific fields for the selected provider", async () => {
renderModal({ mode: "add" });
await waitFor(() => {
expect(screen.getByLabelText("OpenAI API Key")).toBeInTheDocument();
expect(screen.getByPlaceholderText("https://api.openai.com/v1")).toBeInTheDocument();
});
});
});
describe("edit mode", () => {
it("renders the edit title and update button", () => {
renderModal({ mode: "edit", existingCredential: mockCredential });
expect(screen.getByText("Edit Credential")).toBeInTheDocument();
expect(screen.getByText("Update Credential")).toBeInTheDocument();
});
it("prefills the credential name and disables it", async () => {
renderModal({ mode: "edit", existingCredential: mockCredential });
await waitFor(() => {
const nameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement;
expect(nameInput.value).toBe("test-credential");
expect(nameInput.disabled).toBe(true);
});
});
it("disables the name from the mode, not the credential's name value", () => {
renderModal({
mode: "edit",
existingCredential: { ...mockCredential, credential_name: "" },
});
expect((screen.getByLabelText("Credential Name:") as HTMLInputElement).disabled).toBe(true);
});
});
});

View file

@ -1,23 +1,47 @@
import { TextInput } from "@tremor/react";
import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd";
import type { UploadProps } from "antd/es/upload";
import React, { useState } from "react";
import { useState } from "react";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { CredentialItem } from "../networking";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { resetCredentialFormOnProviderChange } from "./credential_form_helpers";
const { Link } = Typography;
interface AddCredentialsModalProps {
interface CredentialModalProps {
open: boolean;
onCancel: () => void;
onAddCredential: (values: any) => void;
onSubmit: (values: any) => void;
uploadProps: UploadProps;
mode: "add" | "edit";
existingCredential?: CredentialItem | null;
}
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCancel, onAddCredential, uploadProps }) => {
export default function CredentialModal({
open,
onCancel,
onSubmit,
uploadProps,
mode,
existingCredential = null,
}: CredentialModalProps) {
const isEdit = mode === "edit";
const [form] = Form.useForm();
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
const [selectedProvider, setSelectedProvider] = useState<Providers>(
(existingCredential?.credential_info.custom_llm_provider as Providers) ?? Providers.OpenAI,
);
const initialValues = existingCredential
? {
credential_name: existingCredential.credential_name,
custom_llm_provider: existingCredential.credential_info.custom_llm_provider,
...Object.fromEntries(
Object.entries(existingCredential.credential_values || {}).map(([key, value]) => [key, value ?? null]),
),
}
: undefined;
const handleSubmit = (values: any) => {
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
@ -26,32 +50,33 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCance
}
return acc;
}, {} as any);
onAddCredential(filteredValues);
onSubmit(filteredValues);
form.resetFields();
};
const closeAndReset = () => {
onCancel();
form.resetFields();
};
return (
<Modal
title="Add New Credential"
title={isEdit ? "Edit Credential" : "Add New Credential"}
open={open}
onCancel={() => {
onCancel();
form.resetFields();
}}
onCancel={closeAndReset}
footer={null}
width={600}
destroyOnHidden={isEdit}
>
<Form form={form} onFinish={handleSubmit} layout="vertical">
{/* Credential Name */}
<Form form={form} onFinish={handleSubmit} layout="vertical" initialValues={initialValues}>
<Form.Item
label="Credential Name:"
name="credential_name"
rules={[{ required: true, message: "Credential name is required" }]}
>
<TextInput placeholder="Enter a friendly name for these credentials" />
<TextInput placeholder="Enter a friendly name for these credentials" disabled={isEdit} />
</Form.Item>
{/* Provider Selection */}
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="Provider:"
@ -92,28 +117,19 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCance
<ProviderSpecificFields selectedProvider={selectedProvider} uploadProps={uploadProps} />
{/* Modal Footer */}
<div className="flex justify-between items-center">
<Tooltip title="Get help on our github">
<Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Link>
</Tooltip>
<div>
<Button
onClick={() => {
onCancel();
form.resetFields();
}}
style={{ marginRight: 10 }}
>
<Button onClick={closeAndReset} style={{ marginRight: 10 }}>
Cancel
</Button>
<Button htmlType="submit">{"Add Credential"}</Button>
<Button htmlType="submit">{isEdit ? "Update Credential" : "Add Credential"}</Button>
</div>
</div>
</Form>
</Modal>
);
};
export default AddCredentialsModal;
}

View file

@ -1,123 +0,0 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Providers } from "../provider_info_helpers";
import { CredentialItem } from "../networking";
import EditCredentialModal from "./EditCredentialModal";
vi.mock("../networking", async () => {
const actual = await vi.importActual("../networking");
return {
...actual,
getProviderCreateMetadata: vi.fn().mockResolvedValue([
{
provider: "OpenAI",
provider_display_name: Providers.OpenAI,
litellm_provider: "openai",
default_model_placeholder: "gpt-3.5-turbo",
credential_fields: [
{
key: "api_key",
label: "OpenAI API Key",
field_type: "password",
required: true,
},
{
key: "api_base",
label: "API Base",
field_type: "text",
placeholder: "https://api.openai.com/v1",
},
],
},
{
provider: "Anthropic",
provider_display_name: Providers.Anthropic,
litellm_provider: "anthropic",
default_model_placeholder: "claude-3-opus-20240229",
credential_fields: [
{
key: "api_key",
label: "Anthropic API Key",
field_type: "password",
required: true,
},
],
},
]),
};
});
const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
});
const mockUploadProps = {
beforeUpload: vi.fn(),
onChange: vi.fn(),
};
const mockCredential: CredentialItem = {
credential_name: "test-credential",
credential_values: {
api_key: "test-api-key",
api_base: "https://api.test.com",
},
credential_info: {
custom_llm_provider: Providers.OpenAI,
},
};
describe("EditCredentialModal", () => {
it("should render", () => {
const queryClient = createQueryClient();
const onCancel = vi.fn();
const onUpdateCredential = vi.fn();
render(
<QueryClientProvider client={queryClient}>
<EditCredentialModal
open={true}
onCancel={onCancel}
onUpdateCredential={onUpdateCredential}
uploadProps={mockUploadProps}
existingCredential={mockCredential}
/>
</QueryClientProvider>,
);
expect(screen.getByText("Edit Credential")).toBeInTheDocument();
expect(screen.getByLabelText("Credential Name:")).toBeInTheDocument();
expect(screen.getByLabelText("Provider:")).toBeInTheDocument();
});
it("should render initial values", async () => {
const queryClient = createQueryClient();
const onCancel = vi.fn();
const onUpdateCredential = vi.fn();
render(
<QueryClientProvider client={queryClient}>
<EditCredentialModal
open={true}
onCancel={onCancel}
onUpdateCredential={onUpdateCredential}
uploadProps={mockUploadProps}
existingCredential={mockCredential}
/>
</QueryClientProvider>,
);
await waitFor(() => {
const credentialNameInput = screen.getByLabelText("Credential Name:") as HTMLInputElement;
expect(credentialNameInput.value).toBe("test-credential");
expect(credentialNameInput.disabled).toBe(true);
});
});
});

View file

@ -1,150 +0,0 @@
import { TextInput } from "@tremor/react";
import { Select as AntdSelect, Button, Form, Modal, Tooltip, Typography } from "antd";
import type { UploadProps } from "antd/es/upload";
import { useEffect, useState } from "react";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { CredentialItem } from "../networking";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { resetCredentialFormOnProviderChange } from "./credential_form_helpers";
const { Link } = Typography;
interface EditCredentialsModalProps {
open: boolean;
onCancel: () => void;
onUpdateCredential: (values: any) => void;
uploadProps: UploadProps;
existingCredential: CredentialItem | null;
}
export default function EditCredentialsModal({
open,
onCancel,
onUpdateCredential,
uploadProps,
existingCredential,
}: EditCredentialsModalProps) {
const [form] = Form.useForm();
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.Anthropic);
const handleSubmit = (values: any) => {
const filteredValues = Object.entries(values).reduce((acc, [key, value]) => {
if (value !== "" && value !== undefined && value !== null) {
acc[key] = value;
}
return acc;
}, {} as any);
onUpdateCredential(filteredValues);
form.resetFields();
};
useEffect(() => {
if (existingCredential) {
// Spread all credential_values dynamically, converting undefined/null to null for form compatibility
const credentialValues = Object.entries(existingCredential.credential_values || {}).reduce(
(acc, [key, value]) => {
acc[key] = value ?? null;
return acc;
},
{} as Record<string, any>,
);
form.setFieldsValue({
credential_name: existingCredential.credential_name,
custom_llm_provider: existingCredential.credential_info.custom_llm_provider,
...credentialValues,
});
setSelectedProvider(existingCredential.credential_info.custom_llm_provider as Providers);
}
}, [existingCredential]);
return (
<Modal
title="Edit Credential"
open={open}
onCancel={() => {
onCancel();
form.resetFields();
}}
footer={null}
width={600}
destroyOnHidden={true}
>
<Form form={form} onFinish={handleSubmit} layout="vertical">
{/* Credential Name */}
<Form.Item
label="Credential Name:"
name="credential_name"
rules={[{ required: true, message: "Credential name is required" }]}
initialValue={existingCredential?.credential_name}
>
<TextInput
placeholder="Enter a friendly name for these credentials"
disabled={existingCredential?.credential_name ? true : false}
/>
</Form.Item>
{/* Provider Selection */}
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="Provider:"
name="custom_llm_provider"
tooltip="Helper to auto-populate provider specific fields"
>
<AntdSelect
showSearch
onChange={(value) => {
resetCredentialFormOnProviderChange(form, value as Providers, setSelectedProvider);
}}
>
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
<img
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
const target = e.target as HTMLImageElement;
const parent = target.parentElement;
if (parent) {
const fallbackDiv = document.createElement("div");
fallbackDiv.className =
"w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs";
fallbackDiv.textContent = providerDisplayName.charAt(0);
parent.replaceChild(fallbackDiv, target);
}
}}
/>
<span>{providerDisplayName}</span>
</div>
</AntdSelect.Option>
))}
</AntdSelect>
</Form.Item>
<ProviderSpecificFields selectedProvider={selectedProvider} uploadProps={uploadProps} />
{/* Modal Footer */}
<div className="flex justify-between items-center">
<Tooltip title="Get help on our github">
<Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Link>
</Tooltip>
<div>
<Button
onClick={() => {
onCancel();
form.resetFields();
}}
style={{ marginRight: 10 }}
>
Cancel
</Button>
<Button htmlType="submit">{"Update Credential"}</Button>
</div>
</div>
</Form>
</Modal>
);
}

View file

@ -22,8 +22,7 @@ import { UploadProps } from "antd/es/upload";
import { useState } from "react";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import NotificationsManager from "../molecules/notifications_manager";
import AddCredentialsTab from "./AddCredentialModal";
import EditCredentialsModal from "./EditCredentialModal";
import CredentialModal from "./CredentialModal";
import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { isProxyAdminRole } from "@/utils/roles";
@ -201,18 +200,20 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ uploadProps }) => {
</Card>
{isAddModalOpen && (
<AddCredentialsTab
onAddCredential={handleAddCredential}
<CredentialModal
mode="add"
onSubmit={handleAddCredential}
open={isAddModalOpen}
onCancel={() => setIsAddModalOpen(false)}
uploadProps={uploadProps}
/>
)}
{isUpdateModalOpen && (
<EditCredentialsModal
<CredentialModal
mode="edit"
open={isUpdateModalOpen}
existingCredential={selectedCredential}
onUpdateCredential={handleUpdateCredential}
onSubmit={handleUpdateCredential}
uploadProps={uploadProps}
onCancel={() => setIsUpdateModalOpen(false)}
/>