feat(credentials/): working e2e flow for add / update models on LiteLLM UI

This commit is contained in:
Krrish Dholakia 2025-03-12 21:13:00 -07:00
parent f7c033bc1b
commit 8fa56227d0
5 changed files with 132 additions and 27 deletions

View file

@ -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 (

View file

@ -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>

View file

@ -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>

View file

@ -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>
) : ( ) : (

View file

@ -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,