feat(ui/): allow admin to reuse existing model credentials

Prevents need to go to backend llm provider for getting credentials
This commit is contained in:
Krrish Dholakia 2025-03-14 12:52:49 -07:00
parent b75cd3b887
commit 70c44741f3
4 changed files with 598 additions and 159 deletions

View file

@ -3,132 +3,392 @@ import { Form, Select } from "antd";
import { TextInput, Text } from "@tremor/react"; import { TextInput, Text } from "@tremor/react";
import { Row, Col, Typography, Button as Button2, Upload, UploadProps } from "antd"; import { Row, Col, Typography, Button as Button2, Upload, UploadProps } from "antd";
import { UploadOutlined } from "@ant-design/icons"; import { UploadOutlined } from "@ant-design/icons";
import { Providers } from "../provider_info_helpers"; import { provider_map, Providers } from "../provider_info_helpers";
import { CredentialItem } from "../networking";
const { Link } = Typography; const { Link } = Typography;
interface ProviderSpecificFieldsProps { interface ProviderSpecificFieldsProps {
selectedProvider: Providers; selectedProvider: Providers;
uploadProps?: UploadProps; uploadProps?: UploadProps;
} }
interface ProviderCredentialField {
key: string;
label: string;
placeholder?: string;
tooltip?: string;
required?: boolean;
type?: "text" | "password" | "select" | "upload";
options?: string[];
defaultValue?: string;
}
export interface CredentialValues {
key: string;
value: string;
}
export const createCredentialFromModel = (provider: string, modelData: any): CredentialItem => {
console.log("provider", provider);
console.log("modelData", modelData);
const enumKey = Object.keys(provider_map).find(
key => provider_map[key].toLowerCase() === provider.toLowerCase()
);
if (!enumKey) {
throw new Error(`Provider ${provider} not found in provider_map`);
}
const providerEnum = Providers[enumKey as keyof typeof Providers];
const providerFields = PROVIDER_CREDENTIAL_FIELDS[providerEnum] || [];
const credentialValues: object = {};
console.log("providerFields", providerFields);
// Go through each field defined for this provider
providerFields.forEach(field => {
const value = modelData.litellm_params[field.key];
console.log("field", field);
console.log("value", value);
if (value !== undefined) {
(credentialValues as Record<string, string>)[field.key] = value.toString();
}
});
const credential: CredentialItem = {
credential_name: `${provider}-credential-${Math.floor(Math.random() * 1000000)}`,
credential_values: credentialValues,
credential_info: {
custom_llm_provider: provider,
description: `Credential for ${provider}. Created from model ${modelData.model_name}`,
}
}
return credential;
};
const PROVIDER_CREDENTIAL_FIELDS: Record<Providers, ProviderCredentialField[]> = {
[Providers.OpenAI]: [
{
key: "api_base",
label: "API Base",
type: "select",
options: [
"https://api.openai.com/v1",
"https://eu.api.openai.com"
],
defaultValue: "https://api.openai.com/v1"
},
{
key: "organization",
label: "OpenAI Organization ID",
placeholder: "[OPTIONAL] my-unique-org"
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true
}
],
[Providers.OpenAI_Text]: [
{
key: "api_base",
label: "API Base",
type: "select",
options: [
"https://api.openai.com/v1",
"https://eu.api.openai.com"
],
defaultValue: "https://api.openai.com/v1"
},
{
key: "organization",
label: "OpenAI Organization ID",
placeholder: "[OPTIONAL] my-unique-org"
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true
}
],
[Providers.Vertex_AI]: [
{
key: "vertex_project",
label: "Vertex Project",
placeholder: "adroit-cadet-1234..",
required: true
},
{
key: "vertex_location",
label: "Vertex Location",
placeholder: "us-east-1",
required: true
},
{
key: "vertex_credentials",
label: "Vertex Credentials",
required: true,
type: "upload"
}
],
[Providers.AssemblyAI]: [
{
key: "api_base",
label: "API Base",
type: "select",
required: true,
options: [
"https://api.assemblyai.com",
"https://api.eu.assemblyai.com"
]
},
{
key: "api_key",
label: "AssemblyAI API Key",
type: "password",
required: true
}
],
[Providers.Azure]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true
},
{
key: "api_version",
label: "API Version",
placeholder: "2023-07-01-preview",
tooltip: "By default litellm will use the latest version. If you want to use a different version, you can specify it here"
},
{
key: "base_model",
label: "Base Model",
placeholder: "azure/gpt-3.5-turbo"
},
{
key: "api_key",
label: "Azure API Key",
type: "password",
required: true
}
],
[Providers.Azure_AI_Studio]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true
},
{
key: "api_key",
label: "Azure API Key",
type: "password",
required: true
}
],
[Providers.OpenAI_Compatible]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true
}
],
[Providers.OpenAI_Text_Compatible]: [
{
key: "api_base",
label: "API Base",
placeholder: "https://...",
required: true
},
{
key: "api_key",
label: "OpenAI API Key",
type: "password",
required: true
}
],
[Providers.Bedrock]: [
{
key: "aws_access_key_id",
label: "AWS Access Key ID",
required: true,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
},
{
key: "aws_secret_access_key",
label: "AWS Secret Access Key",
required: true,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
},
{
key: "aws_region_name",
label: "AWS Region Name",
placeholder: "us-east-1",
required: true,
tooltip: "You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
}
],
[Providers.Ollama]: [], // No specific fields needed
[Providers.Anthropic]: [{
key: "api_key",
label: "API Key",
placeholder: "sk-",
type: "password",
required: true
}],
[Providers.Google_AI_Studio]: [{
key: "api_key",
label: "API Key",
placeholder: "aig-",
type: "password",
required: true
}],
[Providers.Groq]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.MistralAI]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Deepseek]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Cohere]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Databricks]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.xAI]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Cerebras]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Sambanova]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Perplexity]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.TogetherAI]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.Openrouter]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}],
[Providers.FireworksAI]: [{
key: "api_key",
label: "API Key",
type: "password",
required: true
}]
};
const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
selectedProvider, selectedProvider,
uploadProps uploadProps
}) => { }) => {
console.log(`Selected provider: ${selectedProvider}`);
console.log(`type of selectedProvider: ${typeof selectedProvider}`);
// cast selectedProvider to Providers
const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers; const selectedProviderEnum = Providers[selectedProvider as keyof typeof Providers] as Providers;
console.log(`selectedProviderEnum: ${selectedProviderEnum}`);
console.log(`type of selectedProviderEnum: ${typeof selectedProviderEnum}`); // Simply use the fields as defined in PROVIDER_CREDENTIAL_FIELDS
const allFields = React.useMemo(() => {
return PROVIDER_CREDENTIAL_FIELDS[selectedProviderEnum] || [];
}, [selectedProviderEnum]);
return ( return (
<> <>
{selectedProviderEnum === Providers.OpenAI || selectedProviderEnum === Providers.OpenAI_Text && ( {allFields.map((field) => (
<> <React.Fragment key={field.key}>
<Form.Item <Form.Item
label="API Base" label={field.label}
name="api_base" name={field.key}
rules={field.required ? [{ required: true, message: "Required" }] : undefined}
tooltip={field.tooltip}
className={field.key === "vertex_credentials" ? "mb-0" : undefined}
> >
<Select placeholder="Select API Base" defaultValue="https://api.openai.com/v1"> {field.type === "select" ? (
<Select.Option value="https://api.openai.com/v1">https://api.openai.com/v1</Select.Option> <Select
<Select.Option value="https://eu.api.openai.com">https://eu.api.openai.com</Select.Option> placeholder={field.placeholder}
</Select> defaultValue={field.defaultValue}
</Form.Item> >
{field.options?.map((option) => (
<Form.Item label="OpenAI Organization ID" name="organization"> <Select.Option key={option} value={option}>
<TextInput placeholder="[OPTIONAL] my-unique-org" /> {option}
</Form.Item> </Select.Option>
</> ))}
)} </Select>
) : field.type === "upload" ? (
{selectedProviderEnum === Providers.Vertex_AI && ( <Upload {...uploadProps}>
<> <Button2 icon={<UploadOutlined />}>Click to Upload</Button2>
<Form.Item </Upload>
rules={[{ required: true, message: "Required" }]} ) : (
label="Vertex Project" <TextInput
name="vertex_project" placeholder={field.placeholder}
> type={field.type === "password" ? "password" : "text"}
<TextInput placeholder="adroit-cadet-1234.." /> />
)}
</Form.Item> </Form.Item>
<Form.Item {/* Special case for Vertex Credentials help text */}
rules={[{ required: true, message: "Required" }]} {field.key === "vertex_credentials" && (
label="Vertex Location" <Row>
name="vertex_location" <Col span={10}></Col>
> <Col span={10}>
<TextInput placeholder="us-east-1" /> <Text className="mb-3 mt-1">
</Form.Item> Give litellm a gcp service account(.json file), so it
can make the relevant calls
</Text>
</Col>
</Row>
)}
<Form.Item {/* Special case for Azure Base Model help text */}
rules={[{ required: true, message: "Required" }]} {field.key === "base_model" && (
label="Vertex Credentials"
name="vertex_credentials"
className="mb-0"
>
<Upload {...uploadProps}>
<Button2 icon={<UploadOutlined />}>
Click to Upload
</Button2>
</Upload>
</Form.Item>
<Row>
<Col span={10}></Col>
<Col span={10}>
<Text className="mb-3 mt-1">
Give litellm a gcp service account(.json file), so it
can make the relevant calls
</Text>
</Col>
</Row>
</>
)}
{selectedProviderEnum === Providers.AssemblyAI && (
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="API Base"
name="api_base"
>
<Select placeholder="Select API Base">
<Select.Option value="https://api.assemblyai.com">https://api.assemblyai.com</Select.Option>
<Select.Option value="https://api.eu.assemblyai.com">https://api.eu.assemblyai.com</Select.Option>
</Select>
</Form.Item>
)}
{(selectedProviderEnum === Providers.Azure ||
selectedProviderEnum === Providers.Azure_AI_Studio ||
selectedProviderEnum === Providers.OpenAI_Compatible ||
selectedProviderEnum === Providers.OpenAI_Text_Compatible
) && (
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="API Base"
name="api_base"
>
<TextInput placeholder="https://..." />
</Form.Item>
)}
{selectedProviderEnum === Providers.Azure && (
<>
<Form.Item
label="API Version"
name="api_version"
tooltip="By default litellm will use the latest version. If you want to use a different version, you can specify it here"
>
<TextInput placeholder="2023-07-01-preview" />
</Form.Item>
<div>
<Form.Item
label="Base Model"
name="base_model"
className="mb-0"
>
<TextInput placeholder="azure/gpt-3.5-turbo" />
</Form.Item>
<Row> <Row>
<Col span={10}></Col> <Col span={10}></Col>
<Col span={10}> <Col span={10}>
@ -144,54 +404,9 @@ const ProviderSpecificFields: React.FC<ProviderSpecificFieldsProps> = ({
</Text> </Text>
</Col> </Col>
</Row> </Row>
</div> )}
</> </React.Fragment>
)} ))}
{selectedProviderEnum === Providers.Bedrock && (
<>
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="AWS Access Key ID"
name="aws_access_key_id"
tooltip="You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="AWS Secret Access Key"
name="aws_secret_access_key"
tooltip="You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="AWS Region Name"
name="aws_region_name"
tooltip="You can provide the raw key or the environment variable (e.g. `os.environ/MY_SECRET_KEY`)."
>
<TextInput placeholder="us-east-1" />
</Form.Item>
</>
)}
{selectedProviderEnum != Providers.Bedrock &&
selectedProviderEnum != Providers.Vertex_AI &&
selectedProviderEnum != Providers.Ollama &&
(
<Form.Item
rules={[{ required: true, message: "Required" }]}
label="API Key"
name="api_key"
tooltip="LLM API Credentials"
>
<TextInput placeholder="sk-" type="password" />
</Form.Item>
)}
</> </>
); );
}; };

View file

@ -0,0 +1,119 @@
import React, { useState } from "react";
import {
Card,
Form,
Button,
Tooltip,
Typography,
Select as AntdSelect,
Input,
Switch,
Modal
} from "antd";
import type { UploadProps } from "antd/es/upload";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import type { FormInstance } from "antd";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { TextInput } from "@tremor/react";
import { CredentialItem } from "../networking";
const { Title, Link } = Typography;
interface ReuseCredentialsModalProps {
isVisible: boolean;
onCancel: () => void;
onAddCredential: (values: any) => void;
existingCredential: CredentialItem | null;
setIsCredentialModalOpen: (isVisible: boolean) => void;
}
const ReuseCredentialsModal: React.FC<ReuseCredentialsModalProps> = ({
isVisible,
onCancel,
onAddCredential,
existingCredential,
setIsCredentialModalOpen
}) => {
const [form] = Form.useForm();
console.log(`existingCredential in add credentials tab: ${JSON.stringify(existingCredential)}`);
const handleSubmit = (values: any) => {
onAddCredential(values);
form.resetFields();
setIsCredentialModalOpen(false);
};
return (
<Modal
title="Reuse Credentials"
visible={isVisible}
onCancel={() => {
onCancel();
form.resetFields();
}}
footer={null}
width={600}
>
<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"
/>
</Form.Item>
{/* Display Credential Values of existingCredential, don't allow user to edit. Credential values is a dictionary */}
{Object.entries(existingCredential?.credential_values || {}).map(([key, value]) => (
<Form.Item
key={key}
label={key}
name={key}
initialValue={value}
>
<TextInput
placeholder={`Enter ${key}`}
disabled={true}
/>
</Form.Item>
))}
{/* 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"
>
Reuse Credentials
</Button>
</div>
</div>
</Form>
</Modal>
);
};
export default ReuseCredentialsModal;

View file

@ -14,13 +14,15 @@ import {
TextInput, TextInput,
NumberInput, NumberInput,
} from "@tremor/react"; } from "@tremor/react";
import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline";
import { modelDeleteCall, modelUpdateCall } from "./networking"; import { modelDeleteCall, modelUpdateCall, CredentialItem, credentialGetCall, credentialCreateCall } from "./networking";
import { Button, Form, Input, InputNumber, message, Select } from "antd"; import { Button, Form, Input, InputNumber, message, Select, Modal } from "antd";
import EditModelModal from "./edit_model/edit_model_modal"; import EditModelModal from "./edit_model/edit_model_modal";
import { handleEditModelSubmit } from "./edit_model/edit_model_modal"; import { handleEditModelSubmit } from "./edit_model/edit_model_modal";
import { getProviderLogoAndName } from "./provider_info_helpers"; import { getProviderLogoAndName } from "./provider_info_helpers";
import { getDisplayModelName } from "./view_model/model_name_display"; import { getDisplayModelName } from "./view_model/model_name_display";
import AddCredentialsModal from "./model_add/add_credentials_tab";
import ReuseCredentialsModal from "./model_add/reuse_credentials";
interface ModelInfoViewProps { interface ModelInfoViewProps {
modelId: string; modelId: string;
@ -48,11 +50,51 @@ export default function ModelInfoView({
const [form] = Form.useForm(); const [form] = Form.useForm();
const [localModelData, setLocalModelData] = useState(modelData); const [localModelData, setLocalModelData] = useState(modelData);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isCredentialModalOpen, setIsCredentialModalOpen] = useState(false);
const [isDirty, setIsDirty] = useState(false); const [isDirty, setIsDirty] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [existingCredential, setExistingCredential] = useState<CredentialItem | null>(null);
const canEditModel = userRole === "Admin"; const canEditModel = userRole === "Admin";
const isAdmin = userRole === "Admin";
const usingExistingCredential = modelData.litellm_params?.litellm_credential_name != null && modelData.litellm_params?.litellm_credential_name != undefined;
console.log("usingExistingCredential, ", usingExistingCredential);
console.log("modelData.litellm_params.litellm_credential_name, ", modelData.litellm_params.litellm_credential_name);
useEffect(() => {
const getExistingCredential = async () => {
console.log("accessToken, ", accessToken);
if (!accessToken) return;
if (usingExistingCredential) return;
let existingCredentialResponse = await credentialGetCall(accessToken, null, modelId);
console.log("existingCredentialResponse, ", existingCredentialResponse);
setExistingCredential({
credential_name: existingCredentialResponse["credential_name"],
credential_values: existingCredentialResponse["credential_values"],
credential_info: existingCredentialResponse["credential_info"]
});
}
getExistingCredential();
}, [accessToken, modelId]);
const handleReuseCredential = async (values: any) => {
console.log("values, ", values);
if (!accessToken) return;
let credentialItem = {
credential_name: values.credential_name,
model_id: modelId,
credential_info: {
"custom_llm_provider": localModelData.litellm_params?.custom_llm_provider,
}
}
message.info("Storing credential..");
let credentialResponse = await credentialCreateCall(accessToken, credentialItem);
console.log("credentialResponse, ", credentialResponse);
message.success("Credential stored successfully");
}
const handleModelUpdate = async (values: any) => { const handleModelUpdate = async (values: any) => {
try { try {
@ -143,8 +185,16 @@ export default function ModelInfoView({
<Title>Public Model Name: {getDisplayModelName(modelData)}</Title> <Title>Public Model Name: {getDisplayModelName(modelData)}</Title>
<Text className="text-gray-500 font-mono">{modelData.model_info.id}</Text> <Text className="text-gray-500 font-mono">{modelData.model_info.id}</Text>
</div> </div>
{canEditModel && ( {isAdmin && (
<div className="flex gap-2"> <div className="flex gap-2">
<TremorButton
icon={KeyIcon}
variant="secondary"
onClick={() => setIsCredentialModalOpen(true)}
className="flex items-center"
>
Re-use Credentials
</TremorButton>
<TremorButton <TremorButton
icon={TrashIcon} icon={TrashIcon}
variant="secondary" variant="secondary"
@ -507,6 +557,25 @@ export default function ModelInfoView({
</div> </div>
</div> </div>
)} )}
{isCredentialModalOpen &&
!usingExistingCredential ? (
<ReuseCredentialsModal
isVisible={isCredentialModalOpen}
onCancel={() => setIsCredentialModalOpen(false)}
onAddCredential={handleReuseCredential}
existingCredential={existingCredential}
setIsCredentialModalOpen={setIsCredentialModalOpen}
/>
): (
<Modal
open={isCredentialModalOpen}
onCancel={() => setIsCredentialModalOpen(false)}
title="Using Existing Credential"
>
<Text>{modelData.litellm_params.litellm_credential_name}</Text>
</Modal>
)}
</div> </div>
); );
} }

View file

@ -2652,6 +2652,42 @@ export const credentialListCall = async (
} }
}; };
export const credentialGetCall = async (accessToken: String, credentialName: String | null, modelId: String | null) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/credentials` : `/credentials`;
if (credentialName) {
url += `/by_name/${credentialName}`;
} else if (modelId) {
url += `/by_model/${modelId}`;
}
console.log("in credentialListCall");
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
handleError(errorData);
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log("/credentials 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 credentialDeleteCall = async (accessToken: String, credentialName: String) => { export const credentialDeleteCall = async (accessToken: String, credentialName: String) => {
try { try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`; const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`;