diff --git a/litellm/proxy/credential_endpoints/endpoints.py b/litellm/proxy/credential_endpoints/endpoints.py index 045ebccbda..84b35e3c52 100644 --- a/litellm/proxy/credential_endpoints/endpoints.py +++ b/litellm/proxy/credential_endpoints/endpoints.py @@ -6,6 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response import litellm from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.credential_accessor import CredentialAccessor from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper @@ -51,8 +52,10 @@ async def create_credential( detail={"error": CommonProxyErrors.db_not_connected_error.value}, ) - credential = CredentialHelperUtils.encrypt_credential_values(credential) - credentials_dict = credential.model_dump() + encrypted_credential = CredentialHelperUtils.encrypt_credential_values( + credential + ) + credentials_dict = encrypted_credential.model_dump() credentials_dict_jsonified = jsonify_object(credentials_dict) await prisma_client.db.litellm_credentialstable.create( data={ @@ -62,6 +65,9 @@ async def create_credential( } ) + ## ADD TO LITELLM ## + CredentialAccessor.upsert_credentials([credential]) + return {"success": True, "message": "Credential created successfully"} except Exception as e: verbose_proxy_logger.exception(e) @@ -146,6 +152,13 @@ async def delete_credential( await prisma_client.db.litellm_credentialstable.delete( where={"credential_name": credential_name} ) + + ## DELETE FROM LITELLM ## + litellm.credential_list = [ + cred + for cred in litellm.credential_list + if cred.credential_name != credential_name + ] return {"success": True, "message": "Credential deleted successfully"} except Exception as e: return handle_exception_on_proxy(e) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4b48a3865e..462a0aa17e 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1725,6 +1725,16 @@ class ProxyConfig: ) return {} + def load_credential_list(self, config: dict) -> List[CredentialItem]: + """ + Load the credential list from the database + """ + credential_list_dict = config.get("credential_list") + credential_list = [] + if credential_list_dict: + credential_list = [CredentialItem(**cred) for cred in credential_list_dict] + return credential_list + async def load_config( # noqa: PLR0915 self, router: Optional[litellm.Router], config_file_path: str ): @@ -2190,11 +2200,8 @@ class ProxyConfig: ) ## CREDENTIALS - credential_list_dict = config.get("credential_list") - if credential_list_dict: - litellm.credential_list = [ - CredentialItem(**cred) for cred in credential_list_dict - ] + credential_list_dict = self.load_credential_list(config=config) + litellm.credential_list = credential_list_dict return router, router.get_model_list(), general_settings def _load_alerting_settings(self, general_settings: dict): @@ -2854,11 +2861,39 @@ class ProxyConfig: credential_object.credential_values = decrypted_credential_values return credential_object + async def delete_credentials(self, db_credentials: List[CredentialItem]): + """ + Create all-up list of db credentials + local credentials + Compare to the litellm.credential_list + Delete any from litellm.credential_list that are not in the all-up list + """ + ## CONFIG credentials ## + config = await self.get_config(config_file_path=user_config_file_path) + credential_list = self.load_credential_list(config=config) + + ## COMBINED LIST ## + combined_list = db_credentials + credential_list + + ## DELETE ## + idx_to_delete = [] + for idx, credential in enumerate(litellm.credential_list): + if credential.credential_name not in [ + cred.credential_name for cred in combined_list + ]: + idx_to_delete.append(idx) + for idx in sorted(idx_to_delete, reverse=True): + litellm.credential_list.pop(idx) + async def get_credentials(self, prisma_client: PrismaClient): try: credentials = await prisma_client.db.litellm_credentialstable.find_many() credentials = [self.decrypt_credentials(cred) for cred in credentials] - CredentialAccessor.upsert_credentials(credentials) + await self.delete_credentials( + credentials + ) # delete credentials that are not in the all-up list + CredentialAccessor.upsert_credentials( + credentials + ) # upsert credentials that are in the all-up list except Exception as e: verbose_proxy_logger.exception( "litellm.proxy_server.py::get_credentials() - Error getting credentials from DB - {}".format( diff --git a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx index d2c71ba908..a999d76c42 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx @@ -8,7 +8,7 @@ import ProviderSpecificFields from "./provider_specific_fields"; import AdvancedSettings from "./advanced_settings"; import { Providers, providerLogoMap, getPlaceholder } from "../provider_info_helpers"; import type { Team } from "../key_team_helpers/key_list"; - +import { CredentialItem } from "../networking"; interface AddModelTabProps { form: FormInstance; handleOk: () => void; @@ -21,6 +21,7 @@ interface AddModelTabProps { showAdvancedSettings: boolean; setShowAdvancedSettings: (show: boolean) => void; teams: Team[] | null; + credentials: CredentialItem[]; } const { Title, Link } = Typography; @@ -37,6 +38,7 @@ const AddModelTab: React.FC = ({ showAdvancedSettings, setShowAdvancedSettings, teams, + credentials, }) => { return ( <> @@ -108,6 +110,38 @@ const AddModelTab: React.FC = ({ {/* Conditionally Render "Public Model Name" */} + {/* Credentials */} +
+ + Either select existing credentials OR enter new provider credentials below + +
+ + + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + options={credentials.map((credential) => ({ + value: credential.credential_name, + label: credential.credential_name + }))} + allowClear + /> + + +
+
+ OR +
+
+ Promise; } -interface CredentialsResponse { - credentials: CredentialItem[]; -} -interface CredentialItem { - credential_name: string | null; - credential_values: object; - credential_info: { - custom_llm_provider?: string; - description?: string; - required?: boolean; - }; -} -const CredentialsPanel: React.FC = ({ accessToken, uploadProps }) => { - const [credentialsList, setCredentialsList] = useState([]); +const CredentialsPanel: React.FC = ({ accessToken, uploadProps, credentialList, fetchCredentials }) => { const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [form] = Form.useForm(); @@ -63,24 +60,16 @@ const CredentialsPanel: React.FC = ({ accessToken, upload message.success('Credential added successfully'); console.log(`response: ${JSON.stringify(response)}`); setIsAddModalOpen(false); - form.resetFields(); + fetchCredentials(accessToken); }; + + useEffect(() => { if (!accessToken) { return; } - - const fetchCredentials = async () => { - try { - const response: CredentialsResponse = await credentialListCall(accessToken); - console.log(`credentials: ${JSON.stringify(response)}`); - setCredentialsList(response.credentials); - } catch (error) { - console.error('Error fetching credentials:', error); - } - }; - fetchCredentials(); + fetchCredentials(accessToken); }, [accessToken]); const renderProviderBadge = (provider: string) => { @@ -99,6 +88,18 @@ const CredentialsPanel: React.FC = ({ accessToken, upload ); }; + + const handleDeleteCredential = async (credentialName: string) => { + if (!accessToken) { + console.error('No access token found'); + return; + } + const response = await credentialDeleteCall(accessToken, credentialName); + console.log(`response: ${JSON.stringify(response)}`); + message.success('Credential deleted successfully'); + fetchCredentials(accessToken); + }; + return (
@@ -125,20 +126,33 @@ const CredentialsPanel: React.FC = ({ accessToken, upload - {(!credentialsList || credentialsList.length === 0) ? ( + {(!credentialList || credentialList.length === 0) ? ( No credentials configured ) : ( - credentialsList.map((credential: CredentialItem, index: number) => ( + credentialList.map((credential: CredentialItem, index: number) => ( {credential.credential_name} {renderProviderBadge(credential.credential_info?.custom_llm_provider as string || '-')} {credential.credential_info?.description || '-'} + + - + diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 505441debb..7abbb7deb4 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -36,6 +36,21 @@ export interface Organization { members: any[] | null; } +export interface CredentialItem { + credential_name: string; + credential_values: object; + credential_info: { + custom_llm_provider?: string; + description?: string; + required?: boolean; + }; +} + +export interface CredentialsResponse { + credentials: CredentialItem[]; +} + + const baseUrl = "/"; // Assuming the base URL is the root @@ -2606,6 +2621,34 @@ export const credentialListCall = async ( } }; +export const credentialDeleteCall = async (accessToken: String, credentialName: String) => { + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/credentials/${credentialName}` : `/credentials/${credentialName}`; + console.log("in credentialDeleteCall:", credentialName); + const response = await fetch(url, { + method: "DELETE", + 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(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 delete key:", error); + throw error; + } +}; + + export const keyUpdateCall = async ( accessToken: string, formValues: Record // Assuming formValues is an object