diff --git a/litellm/proxy/common_utils/admin_ui_utils.py b/litellm/proxy/common_utils/admin_ui_utils.py new file mode 100644 index 000000000..bb35ecd69 --- /dev/null +++ b/litellm/proxy/common_utils/admin_ui_utils.py @@ -0,0 +1,167 @@ +import os + + +def show_missing_vars_in_env(): + from fastapi.responses import HTMLResponse + + from litellm.proxy.proxy_server import master_key, prisma_client + + if prisma_client is None and master_key is None: + return HTMLResponse( + content=missing_keys_form( + missing_key_names="DATABASE_URL, LITELLM_MASTER_KEY" + ), + status_code=200, + ) + if prisma_client is None: + return HTMLResponse( + content=missing_keys_form(missing_key_names="DATABASE_URL"), status_code=200 + ) + + if master_key is None: + return HTMLResponse( + content=missing_keys_form(missing_key_names="LITELLM_MASTER_KEY"), + status_code=200, + ) + return None + + +# LiteLLM Admin UI - Non SSO Login +url_to_redirect_to = os.getenv("PROXY_BASE_URL", "") +url_to_redirect_to += "/login" +html_form = f""" + + + + LiteLLM Login + + + +
+

LiteLLM Login

+ +

By default Username is "admin" and Password is your set LiteLLM Proxy `MASTER_KEY`

+

If you need to set UI credentials / SSO docs here: https://docs.litellm.ai/docs/proxy/ui

+
+ + + + + +
+""" + + +def missing_keys_form(missing_key_names: str): + missing_keys_html_form = """ + + + + + + + Environment Setup Instructions + + +
+

Environment Setup Instructions

+

Please add the following variables to your environment variables:

+
+    LITELLM_MASTER_KEY="sk-1234" # Your master key for the proxy server. Can use this to send /chat/completion requests etc
+    LITELLM_SALT_KEY="sk-XXXXXXXX" # Can NOT CHANGE THIS ONCE SET - It is used to encrypt/decrypt credentials stored in DB. If value of 'LITELLM_SALT_KEY' changes your models cannot be retrieved from DB
+    DATABASE_URL="postgres://..." # Need a postgres database? (Check out Supabase, Neon, etc)
+    ## OPTIONAL ##
+    PORT=4000 # DO THIS FOR RENDER/RAILWAY
+    STORE_MODEL_IN_DB="True" # Allow storing models in db
+                
+

Missing Environment Variables

+

{missing_keys}

+
+ +
+

Need Help? Support

+

Discord: https://discord.com/invite/wuPM9dRgDw

+

Docs: https://docs.litellm.ai/docs/

+
+ + + """ + return missing_keys_html_form.format(missing_keys=missing_key_names) diff --git a/litellm/proxy/common_utils/encrypt_decrypt_utils.py b/litellm/proxy/common_utils/encrypt_decrypt_utils.py new file mode 100644 index 000000000..f0090046b --- /dev/null +++ b/litellm/proxy/common_utils/encrypt_decrypt_utils.py @@ -0,0 +1,89 @@ +import base64 +import os + +from litellm._logging import verbose_proxy_logger + +LITELLM_SALT_KEY = os.getenv("LITELLM_SALT_KEY", None) +verbose_proxy_logger.debug( + "LITELLM_SALT_KEY is None using master_key to encrypt/decrypt secrets stored in DB" +) + + +def encrypt_value_helper(value: str): + from litellm.proxy.proxy_server import master_key + + signing_key = LITELLM_SALT_KEY + if LITELLM_SALT_KEY is None: + signing_key = master_key + + try: + if isinstance(value, str): + encrypted_value = encrypt_value(value=value, signing_key=signing_key) # type: ignore + encrypted_value = base64.b64encode(encrypted_value).decode("utf-8") + + return encrypted_value + + raise ValueError( + f"Invalid value type passed to encrypt_value: {type(value)} for Value: {value}\n Value must be a string" + ) + except Exception as e: + raise e + + +def decrypt_value_helper(value: str): + from litellm.proxy.proxy_server import master_key + + signing_key = LITELLM_SALT_KEY + if LITELLM_SALT_KEY is None: + signing_key = master_key + + try: + if isinstance(value, str): + decoded_b64 = base64.b64decode(value) + value = decrypt_value(value=decoded_b64, signing_key=signing_key) # type: ignore + return value + except Exception as e: + verbose_proxy_logger.error(f"Error decrypting value: {value}\nError: {str(e)}") + # [Non-Blocking Exception. - this should not block decrypting other values] + pass + + +def encrypt_value(value: str, signing_key: str): + import hashlib + + import nacl.secret + import nacl.utils + + # get 32 byte master key # + hash_object = hashlib.sha256(signing_key.encode()) + hash_bytes = hash_object.digest() + + # initialize secret box # + box = nacl.secret.SecretBox(hash_bytes) + + # encode message # + value_bytes = value.encode("utf-8") + + encrypted = box.encrypt(value_bytes) + + return encrypted + + +def decrypt_value(value: bytes, signing_key: str) -> str: + import hashlib + + import nacl.secret + import nacl.utils + + # get 32 byte master key # + hash_object = hashlib.sha256(signing_key.encode()) + hash_bytes = hash_object.digest() + + # initialize secret box # + box = nacl.secret.SecretBox(hash_bytes) + + # Convert the bytes object to a string + plaintext = box.decrypt(value) + + plaintext = plaintext.decode("utf-8") # type: ignore + return plaintext # type: ignore diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index f954b9dd5..b673b26ab 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -140,7 +140,15 @@ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth ## Import All Misc routes here ## from litellm.proxy.caching_routes import router as caching_router +from litellm.proxy.common_utils.admin_ui_utils import ( + html_form, + show_missing_vars_in_env, +) from litellm.proxy.common_utils.debug_utils import router as debugging_endpoints_router +from litellm.proxy.common_utils.encrypt_decrypt_utils import ( + decrypt_value_helper, + encrypt_value_helper, +) from litellm.proxy.common_utils.http_parsing_utils import _read_request_body from litellm.proxy.common_utils.init_callbacks import initialize_callbacks_on_proxy from litellm.proxy.common_utils.openai_endpoint_utils import ( @@ -186,13 +194,9 @@ from litellm.proxy.utils import ( _get_projected_spend_over_limit, _is_projected_spend_over_limit, _is_valid_team_configs, - decrypt_value, - encrypt_value, get_error_message_str, get_instance_fn, hash_token, - html_form, - missing_keys_html_form, reset_budget, send_email, update_spend, @@ -1243,6 +1247,7 @@ class ProxyConfig: ## DB if prisma_client is not None and ( general_settings.get("store_model_in_db", False) == True + or store_model_in_db is True ): _tasks = [] keys = [ @@ -1885,16 +1890,8 @@ class ProxyConfig: # decrypt values for k, v in _litellm_params.items(): if isinstance(v, str): - # decode base64 - try: - decoded_b64 = base64.b64decode(v) - except Exception as e: - verbose_proxy_logger.error( - "Error decoding value - {}".format(v) - ) - continue # decrypt value - _value = decrypt_value(value=decoded_b64, master_key=master_key) + _value = decrypt_value_helper(value=v) # sanity check if string > size 0 if len(_value) > 0: _litellm_params[k] = _value @@ -1938,13 +1935,8 @@ class ProxyConfig: if isinstance(_litellm_params, dict): # decrypt values for k, v in _litellm_params.items(): - if isinstance(v, str): - # decode base64 - decoded_b64 = base64.b64decode(v) - # decrypt value - _litellm_params[k] = decrypt_value( - value=decoded_b64, master_key=master_key # type: ignore - ) + decrypted_value = decrypt_value_helper(value=v) + _litellm_params[k] = decrypted_value _litellm_params = LiteLLM_Params(**_litellm_params) else: verbose_proxy_logger.error( @@ -2005,10 +1997,8 @@ class ProxyConfig: environment_variables = config_data.get("environment_variables", {}) for k, v in environment_variables.items(): try: - if v is not None: - decoded_b64 = base64.b64decode(v) - value = decrypt_value(value=decoded_b64, master_key=master_key) # type: ignore - os.environ[k] = value + decrypted_value = decrypt_value_helper(value=v) + os.environ[k] = decrypted_value except Exception as e: verbose_proxy_logger.error( "Error setting env variable: %s - %s", k, str(e) @@ -5941,11 +5931,8 @@ async def add_new_model( _litellm_params_dict = model_params.litellm_params.dict(exclude_none=True) _orignal_litellm_model_name = model_params.litellm_params.model for k, v in _litellm_params_dict.items(): - if isinstance(v, str): - encrypted_value = encrypt_value(value=v, master_key=master_key) # type: ignore - model_params.litellm_params[k] = base64.b64encode( - encrypted_value - ).decode("utf-8") + encrypted_value = encrypt_value_helper(value=v) + model_params.litellm_params[k] = encrypted_value _data: dict = { "model_id": model_params.model_info.id, "model_name": model_params.model_name, @@ -6076,11 +6063,8 @@ async def update_model( ### ENCRYPT PARAMS ### for k, v in _new_litellm_params_dict.items(): - if isinstance(v, str): - encrypted_value = encrypt_value(value=v, master_key=master_key) # type: ignore - model_params.litellm_params[k] = base64.b64encode( - encrypted_value - ).decode("utf-8") + encrypted_value = encrypt_value_helper(value=v) + model_params.litellm_params[k] = encrypted_value ### MERGE WITH EXISTING DATA ### merged_dictionary = {} @@ -7198,10 +7182,9 @@ async def google_login(request: Request): ) ####### Detect DB + MASTER KEY in .env ####### - if prisma_client is None or master_key is None: - from fastapi.responses import HTMLResponse - - return HTMLResponse(content=missing_keys_html_form, status_code=200) + missing_env_vars = show_missing_vars_in_env() + if missing_env_vars is not None: + return missing_env_vars # get url from request redirect_url = os.getenv("PROXY_BASE_URL", str(request.base_url)) @@ -8404,11 +8387,8 @@ async def update_config(config_info: ConfigYAML): # encrypt updated_environment_variables # for k, v in _updated_environment_variables.items(): - if isinstance(v, str): - encrypted_value = encrypt_value(value=v, master_key=master_key) # type: ignore - _updated_environment_variables[k] = base64.b64encode( - encrypted_value - ).decode("utf-8") + encrypted_value = encrypt_value_helper(value=v) + _updated_environment_variables[k] = encrypted_value _existing_env_variables = config["environment_variables"] @@ -8825,11 +8805,8 @@ async def get_config(): env_vars_dict[_var] = None else: # decode + decrypt the value - decoded_b64 = base64.b64decode(env_variable) - _decrypted_value = decrypt_value( - value=decoded_b64, master_key=master_key - ) - env_vars_dict[_var] = _decrypted_value + decrypted_value = decrypt_value_helper(value=env_variable) + env_vars_dict[_var] = decrypted_value _data_to_return.append({"name": _callback, "variables": env_vars_dict}) elif _callback == "langfuse": @@ -8845,11 +8822,8 @@ async def get_config(): _langfuse_env_vars[_var] = None else: # decode + decrypt the value - decoded_b64 = base64.b64decode(env_variable) - _decrypted_value = decrypt_value( - value=decoded_b64, master_key=master_key - ) - _langfuse_env_vars[_var] = _decrypted_value + decrypted_value = decrypt_value_helper(value=env_variable) + _langfuse_env_vars[_var] = decrypted_value _data_to_return.append( {"name": _callback, "variables": _langfuse_env_vars} @@ -8870,10 +8844,7 @@ async def get_config(): _slack_env_vars[_var] = _value else: # decode + decrypt the value - decoded_b64 = base64.b64decode(env_variable) - _decrypted_value = decrypt_value( - value=decoded_b64, master_key=master_key - ) + _decrypted_value = decrypt_value_helper(value=env_variable) _slack_env_vars[_var] = _decrypted_value _alerting_types = proxy_logging_obj.slack_alerting_instance.alert_types @@ -8909,10 +8880,7 @@ async def get_config(): _email_env_vars[_var] = None else: # decode + decrypt the value - decoded_b64 = base64.b64decode(env_variable) - _decrypted_value = decrypt_value( - value=decoded_b64, master_key=master_key - ) + _decrypted_value = decrypt_value_helper(value=env_variable) _email_env_vars[_var] = _decrypted_value alerting_data.append( diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index ba1a61080..8d4eff99a 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -2705,178 +2705,6 @@ def _is_valid_team_configs(team_id=None, team_config=None, request_data=None): return -def encrypt_value(value: str, master_key: str): - import hashlib - - import nacl.secret - import nacl.utils - - # get 32 byte master key # - hash_object = hashlib.sha256(master_key.encode()) - hash_bytes = hash_object.digest() - - # initialize secret box # - box = nacl.secret.SecretBox(hash_bytes) - - # encode message # - value_bytes = value.encode("utf-8") - - encrypted = box.encrypt(value_bytes) - - return encrypted - - -def decrypt_value(value: bytes, master_key: str) -> str: - import hashlib - - import nacl.secret - import nacl.utils - - # get 32 byte master key # - hash_object = hashlib.sha256(master_key.encode()) - hash_bytes = hash_object.digest() - - # initialize secret box # - box = nacl.secret.SecretBox(hash_bytes) - - # Convert the bytes object to a string - plaintext = box.decrypt(value) - - plaintext = plaintext.decode("utf-8") # type: ignore - return plaintext # type: ignore - - -# LiteLLM Admin UI - Non SSO Login -url_to_redirect_to = os.getenv("PROXY_BASE_URL", "") -url_to_redirect_to += "/login" -html_form = f""" - - - - LiteLLM Login - - - -
-

LiteLLM Login

- -

By default Username is "admin" and Password is your set LiteLLM Proxy `MASTER_KEY`

-

If you need to set UI credentials / SSO docs here: https://docs.litellm.ai/docs/proxy/ui

-
- - - - - -
-""" - - -missing_keys_html_form = """ - - - - - - - Environment Setup Instructions - - -
-

Environment Setup Instructions

-

Please add the following configurations to your environment variables:

-
-LITELLM_MASTER_KEY="sk-1234" # make this unique. must start with `sk-`.
-DATABASE_URL="postgres://..." # Need a postgres database? (Check out Supabase, Neon, etc)
-
-## OPTIONAL ##
-PORT=4000 # DO THIS FOR RENDER/RAILWAY
-STORE_MODEL_IN_DB="True" # Allow storing models in db
-            
-
- - - """ - - def _to_ns(dt): return int(dt.timestamp() * 1e9) diff --git a/litellm/tests/test_config.py b/litellm/tests/test_config.py index cd61101a3..28d144e4d 100644 --- a/litellm/tests/test_config.py +++ b/litellm/tests/test_config.py @@ -2,23 +2,30 @@ ## Unit tests for ProxyConfig class -import sys, os +import os +import sys import traceback + from dotenv import load_dotenv load_dotenv() -import os, io +import io +import os sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path -import pytest, litellm -from pydantic import BaseModel, ConfigDict -from litellm.proxy.proxy_server import ProxyConfig -from litellm.proxy.utils import encrypt_value, ProxyLogging, DualCache -from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo from typing import Literal +import pytest +from pydantic import BaseModel, ConfigDict + +import litellm +from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value +from litellm.proxy.proxy_server import ProxyConfig +from litellm.proxy.utils import DualCache, ProxyLogging +from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo + class DBModel(BaseModel): model_id: str @@ -28,6 +35,7 @@ class DBModel(BaseModel): model_config = ConfigDict(protected_namespaces=()) + @pytest.mark.asyncio async def test_delete_deployment(): """