diff --git a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx index b7565b0494..62fd3e4eb6 100644 --- a/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx +++ b/ui/litellm-dashboard/src/components/add_model/provider_specific_fields.tsx @@ -3,132 +3,392 @@ import { Form, Select } from "antd"; import { TextInput, Text } from "@tremor/react"; import { Row, Col, Typography, Button as Button2, Upload, UploadProps } from "antd"; 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; + interface ProviderSpecificFieldsProps { selectedProvider: Providers; 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)[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.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 = ({ selectedProvider, 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; - 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 ( <> - {selectedProviderEnum === Providers.OpenAI || selectedProviderEnum === Providers.OpenAI_Text && ( - <> + {allFields.map((field) => ( + - - - - - - - - )} - - {selectedProviderEnum === Providers.Vertex_AI && ( - <> - - + {field.type === "select" ? ( + + ) : field.type === "upload" ? ( + + }>Click to Upload + + ) : ( + + )} - - - + {/* Special case for Vertex Credentials help text */} + {field.key === "vertex_credentials" && ( + + + + + Give litellm a gcp service account(.json file), so it + can make the relevant calls + + + + )} - - - }> - Click to Upload - - - - - - - - - Give litellm a gcp service account(.json file), so it - can make the relevant calls - - - - - )} - - {selectedProviderEnum === Providers.AssemblyAI && ( - - - - )} - - {(selectedProviderEnum === Providers.Azure || - selectedProviderEnum === Providers.Azure_AI_Studio || - selectedProviderEnum === Providers.OpenAI_Compatible || - selectedProviderEnum === Providers.OpenAI_Text_Compatible - ) && ( - - - - )} - - {selectedProviderEnum === Providers.Azure && ( - <> - - - - -
- - - + {/* Special case for Azure Base Model help text */} + {field.key === "base_model" && ( @@ -144,54 +404,9 @@ const ProviderSpecificFields: React.FC = ({ -
- - )} - - {selectedProviderEnum === Providers.Bedrock && ( - <> - - - - - - - - - - - - - )} - - {selectedProviderEnum != Providers.Bedrock && - selectedProviderEnum != Providers.Vertex_AI && - selectedProviderEnum != Providers.Ollama && - ( - - - - )} + )} +
+ ))} ); }; diff --git a/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx b/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx new file mode 100644 index 0000000000..50aa4e956e --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_add/reuse_credentials.tsx @@ -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 = ({ + 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 ( + { + onCancel(); + form.resetFields(); + }} + footer={null} + width={600} + > +
+ {/* Credential Name */} + + + + + {/* Display Credential Values of existingCredential, don't allow user to edit. Credential values is a dictionary */} + {Object.entries(existingCredential?.credential_values || {}).map(([key, value]) => ( + + + + ))} + + {/* Modal Footer */} +
+ + + Need Help? + + + +
+ + +
+
+
+
+ ); +}; + +export default ReuseCredentialsModal; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index bc6a831d62..7768aa71cb 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -14,13 +14,15 @@ import { TextInput, NumberInput, } from "@tremor/react"; -import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; -import { modelDeleteCall, modelUpdateCall } from "./networking"; -import { Button, Form, Input, InputNumber, message, Select } from "antd"; +import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline"; +import { modelDeleteCall, modelUpdateCall, CredentialItem, credentialGetCall, credentialCreateCall } from "./networking"; +import { Button, Form, Input, InputNumber, message, Select, Modal } from "antd"; import EditModelModal from "./edit_model/edit_model_modal"; import { handleEditModelSubmit } from "./edit_model/edit_model_modal"; import { getProviderLogoAndName } from "./provider_info_helpers"; 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 { modelId: string; @@ -48,11 +50,51 @@ export default function ModelInfoView({ const [form] = Form.useForm(); const [localModelData, setLocalModelData] = useState(modelData); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isCredentialModalOpen, setIsCredentialModalOpen] = useState(false); const [isDirty, setIsDirty] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isEditing, setIsEditing] = useState(false); + const [existingCredential, setExistingCredential] = useState(null); 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) => { try { @@ -143,8 +185,16 @@ export default function ModelInfoView({ Public Model Name: {getDisplayModelName(modelData)} {modelData.model_info.id} - {canEditModel && ( + {isAdmin && (
+ setIsCredentialModalOpen(true)} + className="flex items-center" + > + Re-use Credentials +
)} + + {isCredentialModalOpen && + !usingExistingCredential ? ( + setIsCredentialModalOpen(false)} + onAddCredential={handleReuseCredential} + existingCredential={existingCredential} + setIsCredentialModalOpen={setIsCredentialModalOpen} + /> + ): ( + setIsCredentialModalOpen(false)} + title="Using Existing Credential" + > + {modelData.litellm_params.litellm_credential_name} + + )} ); } \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 2cd122aba8..0c1ba991ac 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`;