mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 19:24:27 +00:00
feat(credentials/): working e2e flow for add / update models on LiteLLM UI
This commit is contained in:
parent
f7c033bc1b
commit
8fa56227d0
5 changed files with 132 additions and 27 deletions
|
@ -148,12 +148,13 @@ const AddModelTab: React.FC<AddModelTabProps> = ({
|
||||||
<Form.Item
|
<Form.Item
|
||||||
noStyle
|
noStyle
|
||||||
shouldUpdate={(prevValues, currentValues) =>
|
shouldUpdate={(prevValues, currentValues) =>
|
||||||
prevValues.credential_name !== currentValues.credential_name ||
|
prevValues.litellm_credential_name !== currentValues.litellm_credential_name ||
|
||||||
prevValues.provider !== currentValues.provider
|
prevValues.provider !== currentValues.provider
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{({ getFieldValue }) => {
|
{({ getFieldValue }) => {
|
||||||
const credentialName = getFieldValue('litellm_credential_name');
|
const credentialName = getFieldValue('litellm_credential_name');
|
||||||
|
console.log("🔑 Credential Name Changed:", credentialName);
|
||||||
// Only show provider specific fields if no credentials selected
|
// Only show provider specific fields if no credentials selected
|
||||||
if (!credentialName) {
|
if (!credentialName) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -15,33 +15,46 @@ import { Providers, providerLogoMap } from "../provider_info_helpers";
|
||||||
import type { FormInstance } from "antd";
|
import type { FormInstance } from "antd";
|
||||||
import ProviderSpecificFields from "../add_model/provider_specific_fields";
|
import ProviderSpecificFields from "../add_model/provider_specific_fields";
|
||||||
import { TextInput } from "@tremor/react";
|
import { TextInput } from "@tremor/react";
|
||||||
|
import { CredentialItem } from "../networking";
|
||||||
const { Title, Link } = Typography;
|
const { Title, Link } = Typography;
|
||||||
|
|
||||||
interface AddCredentialsModalProps {
|
interface AddCredentialsModalProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onAddCredential: (values: any) => void;
|
onAddCredential: (values: any) => void;
|
||||||
|
onUpdateCredential: (values: any) => void;
|
||||||
uploadProps: UploadProps;
|
uploadProps: UploadProps;
|
||||||
|
addOrEdit: "add" | "edit";
|
||||||
|
existingCredential: CredentialItem | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||||
isVisible,
|
isVisible,
|
||||||
onCancel,
|
onCancel,
|
||||||
onAddCredential,
|
onAddCredential,
|
||||||
uploadProps
|
onUpdateCredential,
|
||||||
|
uploadProps,
|
||||||
|
addOrEdit,
|
||||||
|
existingCredential
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
|
const [selectedProvider, setSelectedProvider] = useState<Providers>(Providers.OpenAI);
|
||||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||||
|
|
||||||
|
console.log(`existingCredential in add credentials tab: ${JSON.stringify(existingCredential)}`);
|
||||||
|
|
||||||
const handleSubmit = (values: any) => {
|
const handleSubmit = (values: any) => {
|
||||||
onAddCredential(values);
|
if (addOrEdit === "add") {
|
||||||
|
onAddCredential(values);
|
||||||
|
} else {
|
||||||
|
onUpdateCredential(values);
|
||||||
|
}
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Add New Credential"
|
title={addOrEdit === "add" ? "Add New Credential" : "Edit Credential"}
|
||||||
visible={isVisible}
|
visible={isVisible}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
onCancel();
|
onCancel();
|
||||||
|
@ -55,18 +68,31 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||||
onFinish={handleSubmit}
|
onFinish={handleSubmit}
|
||||||
layout="vertical"
|
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 */}
|
{/* Provider Selection */}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
rules={[{ required: true, message: "Required" }]}
|
rules={[{ required: true, message: "Required" }]}
|
||||||
label="Provider:"
|
label="Provider:"
|
||||||
name="custom_llm_provider"
|
name="custom_llm_provider"
|
||||||
tooltip="Select the credential provider"
|
tooltip="Helper to auto-populate provider specific fields"
|
||||||
>
|
>
|
||||||
<AntdSelect
|
<AntdSelect
|
||||||
showSearch={true}
|
showSearch={true}
|
||||||
value={selectedProvider}
|
value={existingCredential?.credential_info.custom_llm_provider || selectedProvider}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedProvider(value);
|
setSelectedProvider(value as Providers);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
|
{Object.entries(Providers).map(([providerEnum, providerDisplayName]) => (
|
||||||
|
@ -97,14 +123,7 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||||
</AntdSelect>
|
</AntdSelect>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/* Credential Name */}
|
|
||||||
<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" />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<ProviderSpecificFields
|
<ProviderSpecificFields
|
||||||
selectedProvider={selectedProvider}
|
selectedProvider={selectedProvider}
|
||||||
|
@ -132,7 +151,7 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({
|
||||||
<Button
|
<Button
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
>
|
>
|
||||||
Add Credential
|
{addOrEdit === "add" ? "Add Credential" : "Update Credential"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/outline";
|
||||||
import { UploadProps } from "antd/es/upload";
|
import { UploadProps } from "antd/es/upload";
|
||||||
import { PlusIcon } from "@heroicons/react/solid";
|
import { PlusIcon } from "@heroicons/react/solid";
|
||||||
import { credentialListCall, credentialCreateCall, credentialDeleteCall, CredentialItem, CredentialsResponse } from "@/components/networking"; // Assume this is your networking function
|
import { credentialListCall, credentialCreateCall, credentialDeleteCall, credentialUpdateCall, CredentialItem, CredentialsResponse } from "@/components/networking"; // Assume this is your networking function
|
||||||
import AddCredentialsTab from "./add_credentials_tab";
|
import AddCredentialsTab from "./add_credentials_tab";
|
||||||
import { Form, message } from "antd";
|
import { Form, message } from "antd";
|
||||||
interface CredentialsPanelProps {
|
interface CredentialsPanelProps {
|
||||||
|
@ -35,7 +35,36 @@ interface CredentialsPanelProps {
|
||||||
|
|
||||||
const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ accessToken, uploadProps, credentialList, fetchCredentials }) => {
|
const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ accessToken, uploadProps, credentialList, fetchCredentials }) => {
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false);
|
||||||
|
const [selectedCredential, setSelectedCredential] = useState<CredentialItem | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
console.log(`selectedCredential in credentials panel: ${JSON.stringify(selectedCredential)}`);
|
||||||
|
|
||||||
|
const restrictedFields = ['credential_name', 'custom_llm_provider'];
|
||||||
|
const handleUpdateCredential = async (values: any) => {
|
||||||
|
if (!accessToken) {
|
||||||
|
console.error('No access token found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter_credential_values = Object.entries(values)
|
||||||
|
.filter(([key]) => !restrictedFields.includes(key))
|
||||||
|
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||||
|
// Transform form values into credential structure
|
||||||
|
const newCredential = {
|
||||||
|
credential_name: values.credential_name,
|
||||||
|
credential_values: filter_credential_values,
|
||||||
|
credential_info: {
|
||||||
|
custom_llm_provider: values.custom_llm_provider,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await credentialUpdateCall(accessToken, values.credential_name, newCredential);
|
||||||
|
message.success('Credential updated successfully');
|
||||||
|
console.log(`response: ${JSON.stringify(response)}`);
|
||||||
|
setIsUpdateModalOpen(false);
|
||||||
|
fetchCredentials(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddCredential = async (values: any) => {
|
const handleAddCredential = async (values: any) => {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
@ -44,7 +73,7 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ accessToken, upload
|
||||||
}
|
}
|
||||||
|
|
||||||
const filter_credential_values = Object.entries(values)
|
const filter_credential_values = Object.entries(values)
|
||||||
.filter(([key]) => !['credential_name', 'custom_llm_provider'].includes(key))
|
.filter(([key]) => !restrictedFields.includes(key))
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {});
|
||||||
// Transform form values into credential structure
|
// Transform form values into credential structure
|
||||||
const newCredential = {
|
const newCredential = {
|
||||||
|
@ -143,8 +172,13 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ accessToken, upload
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
icon={PencilAltIcon}
|
icon={PencilAltIcon}
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
console.log(`credential being set: ${JSON.stringify(credential)}`);
|
||||||
|
setSelectedCredential(credential);
|
||||||
|
setIsUpdateModalOpen(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={TrashIcon}
|
icon={TrashIcon}
|
||||||
|
@ -175,6 +209,19 @@ const CredentialsPanel: React.FC<CredentialsPanelProps> = ({ accessToken, upload
|
||||||
isVisible={isAddModalOpen}
|
isVisible={isAddModalOpen}
|
||||||
onCancel={() => setIsAddModalOpen(false)}
|
onCancel={() => setIsAddModalOpen(false)}
|
||||||
uploadProps={uploadProps}
|
uploadProps={uploadProps}
|
||||||
|
addOrEdit="add"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isUpdateModalOpen && (
|
||||||
|
<AddCredentialsTab
|
||||||
|
form={form}
|
||||||
|
onAddCredential={handleAddCredential}
|
||||||
|
isVisible={isUpdateModalOpen}
|
||||||
|
existingCredential={selectedCredential}
|
||||||
|
onUpdateCredential={handleUpdateCredential}
|
||||||
|
uploadProps={uploadProps}
|
||||||
|
onCancel={() => setIsUpdateModalOpen(false)}
|
||||||
|
addOrEdit="edit"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -196,14 +196,7 @@ export const columns = (
|
||||||
return model.litellm_params.litellm_credential_name ? (
|
return model.litellm_params.litellm_credential_name ? (
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<Tooltip title={model.litellm_params.litellm_credential_name}>
|
<Tooltip title={model.litellm_params.litellm_credential_name}>
|
||||||
<Button
|
{model.litellm_params.litellm_credential_name.slice(0, 7)}...
|
||||||
size="xs"
|
|
||||||
variant="light"
|
|
||||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
|
||||||
onClick={() => setSelectedTeamId(model.model_info.team_id)}
|
|
||||||
>
|
|
||||||
{model.litellm_params.litellm_credential_name.slice(0, 7)}...
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -2648,6 +2648,51 @@ export const credentialDeleteCall = async (accessToken: String, credentialName:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const credentialUpdateCall = async (
|
||||||
|
accessToken: string,
|
||||||
|
credentialName: string,
|
||||||
|
formValues: Record<string, any> // Assuming formValues is an object
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
console.log("Form Values in credentialUpdateCall:", formValues); // Log the form values before making the API call
|
||||||
|
if (formValues.metadata) {
|
||||||
|
console.log("formValues.metadata:", formValues.metadata);
|
||||||
|
// if there's an exception JSON.parse, show it in the message
|
||||||
|
try {
|
||||||
|
formValues.metadata = JSON.parse(formValues.metadata);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error("Failed to parse metadata: " + error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...formValues, // Include formValues in the request body
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
console.error("Error response from the server:", errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("API Response:", data);
|
||||||
|
return data;
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const keyUpdateCall = async (
|
export const keyUpdateCall = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue