diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 3934d7f72ad..00000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index b59a0a57b50..f6c8415021c 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -1,5 +1,15 @@ model_list: - model_name: azure-gpt-35-turbo litellm_params: - model: topaz/chatgpt-v-2 - api_key: os.environ/AZURE_API_KEY \ No newline at end of file + model: azure/chatgpt-v-2 + api_key: os.environ/AZURE_API_KEY + api_base: os.environ/AZURE_API_BASE + +general_settings: + master_key: sk-1234 + enable_jwt_auth: True + litellm_jwtauth: + user_id_jwt_field: "sub" + team_id_jwt_field: "client_id" + user_id_upsert: True + custom_validate: custom_validate.my_custom_validate diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index dbf5270e59d..0f389a37287 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2,7 +2,17 @@ import enum import json import uuid from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Literal, + Optional, + Union, + get_type_hints, +) import httpx from pydantic import ( @@ -30,6 +40,8 @@ from litellm.types.utils import ( TextCompletionResponse, ) +from .types_utils.utils import get_instance_fn, validate_custom_validate_return_type + if TYPE_CHECKING: from opentelemetry.trace import Span as _Span @@ -2395,6 +2407,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): - public_key_ttl: Default - 600s. TTL for caching public JWT keys. - public_allowed_routes: list of allowed routes for authenticated but unknown litellm role jwt tokens. - enforce_rbac: If true, enforce RBAC for all routes. + - custom_validate: A custom function to validates the JWT token. See `auth_checks.py` for the specific routes """ @@ -2439,6 +2452,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): scope_mappings: Optional[List[ScopeMapping]] = None enforce_scope_based_access: bool = False enforce_team_based_model_access: bool = False + custom_validate: Optional[Callable[..., Literal[True]]] = None def __init__(self, **kwargs: Any) -> None: # get the attribute names for this Pydantic model @@ -2451,6 +2465,12 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): role_mappings = kwargs.get("role_mappings") scope_mappings = kwargs.get("scope_mappings") enforce_scope_based_access = kwargs.get("enforce_scope_based_access") + custom_validate = kwargs.get("custom_validate") + + if custom_validate is not None: + fn = get_instance_fn(custom_validate) + validate_custom_validate_return_type(fn) + kwargs["custom_validate"] = fn if invalid_keys: raise ValueError( diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index 88a8144b554..29f4b31f9cd 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -862,6 +862,14 @@ class JWTAuthManager: """Main authentication and authorization builder""" jwt_valid_token: dict = await jwt_handler.auth_jwt(token=api_key) + # Check custom validate + if jwt_handler.litellm_jwtauth.custom_validate: + if not jwt_handler.litellm_jwtauth.custom_validate(jwt_valid_token): + raise HTTPException( + status_code=403, + detail="Invalid JWT token", + ) + # Check RBAC rbac_role = jwt_handler.get_rbac_role(token=jwt_valid_token) await JWTAuthManager.check_rbac_role( diff --git a/litellm/proxy/common_utils/callback_utils.py b/litellm/proxy/common_utils/callback_utils.py index 9859b340573..2280e72e9b8 100644 --- a/litellm/proxy/common_utils/callback_utils.py +++ b/litellm/proxy/common_utils/callback_utils.py @@ -4,7 +4,7 @@ import litellm from litellm import get_secret from litellm._logging import verbose_proxy_logger from litellm.proxy._types import CommonProxyErrors, LiteLLMPromptInjectionParams -from litellm.proxy.utils import get_instance_fn +from litellm.proxy.types_utils.utils import get_instance_fn blue_color_code = "\033[94m" reset_color_code = "\033[0m" diff --git a/litellm/proxy/custom_validate.py b/litellm/proxy/custom_validate.py new file mode 100644 index 00000000000..c4e1478791a --- /dev/null +++ b/litellm/proxy/custom_validate.py @@ -0,0 +1,2 @@ +def my_custom_validate(token: str) -> bool: + return False diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 970af05f6db..c3257e47a1b 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -609,7 +609,7 @@ def create_pass_through_route( # check if target is an adapter.py or a url import uuid - from litellm.proxy.utils import get_instance_fn + from litellm.proxy.types_utils.utils import get_instance_fn try: if isinstance(target, CustomLogger): diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index cf246b7664e..d3870ae22b4 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -232,6 +232,7 @@ from litellm.proxy.spend_tracking.spend_management_endpoints import ( router as spend_management_router, ) from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload +from litellm.proxy.types_utils.utils import get_instance_fn from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( router as ui_crud_endpoints_router, ) @@ -245,7 +246,6 @@ from litellm.proxy.utils import ( _is_projected_spend_over_limit, _is_valid_team_configs, get_error_message_str, - get_instance_fn, hash_token, update_spend, ) diff --git a/litellm/proxy/types_utils/README.md b/litellm/proxy/types_utils/README.md new file mode 100644 index 00000000000..1d14cf513ba --- /dev/null +++ b/litellm/proxy/types_utils/README.md @@ -0,0 +1 @@ +Utility functions for proxy types.py \ No newline at end of file diff --git a/litellm/proxy/types_utils/utils.py b/litellm/proxy/types_utils/utils.py new file mode 100644 index 00000000000..788849b3d5c --- /dev/null +++ b/litellm/proxy/types_utils/utils.py @@ -0,0 +1,64 @@ +import importlib +import os +from typing import Any, Callable, Literal, Optional, get_type_hints + + +def get_instance_fn(value: str, config_file_path: Optional[str] = None) -> Any: + module_name = value + instance_name = None + try: + # Split the path by dots to separate module from instance + parts = value.split(".") + + # The module path is all but the last part, and the instance_name is the last part + module_name = ".".join(parts[:-1]) + instance_name = parts[-1] + + # If config_file_path is provided, use it to determine the module spec and load the module + if config_file_path is not None: + directory = os.path.dirname(config_file_path) + module_file_path = os.path.join(directory, *module_name.split(".")) + module_file_path += ".py" + + spec = importlib.util.spec_from_file_location(module_name, module_file_path) # type: ignore + if spec is None: + raise ImportError( + f"Could not find a module specification for {module_file_path}" + ) + module = importlib.util.module_from_spec(spec) # type: ignore + spec.loader.exec_module(module) # type: ignore + else: + # Dynamically import the module + module = importlib.import_module(module_name) + + # Get the instance from the module + instance = getattr(module, instance_name) + + return instance + except ImportError as e: + # Re-raise the exception with a user-friendly message + if instance_name and module_name: + raise ImportError( + f"Could not import {instance_name} from {module_name}" + ) from e + else: + raise e + except Exception as e: + raise e + + +def validate_custom_validate_return_type( + fn: Optional[Callable[..., Any]] +) -> Optional[Callable[..., Literal[True]]]: + if fn is None: + return None + + hints = get_type_hints(fn) + return_type = hints.get("return") + + if return_type != Literal[True]: + raise TypeError( + f"Custom validator must be annotated to return Literal[True], got {return_type}" + ) + + return fn diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 5e11f61522c..8042a781395 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1,7 +1,6 @@ import asyncio import copy import hashlib -import importlib import json import os import smtplib @@ -2222,51 +2221,6 @@ class PrismaClient: ) -### CUSTOM FILE ### -def get_instance_fn(value: str, config_file_path: Optional[str] = None) -> Any: - module_name = value - instance_name = None - try: - # Split the path by dots to separate module from instance - parts = value.split(".") - - # The module path is all but the last part, and the instance_name is the last part - module_name = ".".join(parts[:-1]) - instance_name = parts[-1] - - # If config_file_path is provided, use it to determine the module spec and load the module - if config_file_path is not None: - directory = os.path.dirname(config_file_path) - module_file_path = os.path.join(directory, *module_name.split(".")) - module_file_path += ".py" - - spec = importlib.util.spec_from_file_location(module_name, module_file_path) # type: ignore - if spec is None: - raise ImportError( - f"Could not find a module specification for {module_file_path}" - ) - module = importlib.util.module_from_spec(spec) # type: ignore - spec.loader.exec_module(module) # type: ignore - else: - # Dynamically import the module - module = importlib.import_module(module_name) - - # Get the instance from the module - instance = getattr(module, instance_name) - - return instance - except ImportError as e: - # Re-raise the exception with a user-friendly message - if instance_name and module_name: - raise ImportError( - f"Could not import {instance_name} from {module_name}" - ) from e - else: - raise e - except Exception as e: - raise e - - ### HELPER FUNCTIONS ### async def _cache_user_row(user_id: str, cache: DualCache, db: PrismaClient): """ diff --git a/tests/proxy_unit_tests/test_proxy_custom_logger.py b/tests/proxy_unit_tests/test_proxy_custom_logger.py index eb75c4abf79..ad60335152f 100644 --- a/tests/proxy_unit_tests/test_proxy_custom_logger.py +++ b/tests/proxy_unit_tests/test_proxy_custom_logger.py @@ -51,7 +51,7 @@ print("Testing proxy custom logger") def test_embedding(client): try: litellm.set_verbose = False - from litellm.proxy.utils import get_instance_fn + from litellm.proxy.types_utils.utils import get_instance_fn my_custom_logger = get_instance_fn( value="custom_callbacks.my_custom_logger", config_file_path=python_file_path @@ -122,7 +122,7 @@ def test_chat_completion(client): try: # Your test data litellm.set_verbose = False - from litellm.proxy.utils import get_instance_fn + from litellm.proxy.types_utils.utils import get_instance_fn my_custom_logger = get_instance_fn( value="custom_callbacks.my_custom_logger", config_file_path=python_file_path @@ -217,7 +217,7 @@ def test_chat_completion_stream(client): try: # Your test data litellm.set_verbose = False - from litellm.proxy.utils import get_instance_fn + from litellm.proxy.types_utils.utils import get_instance_fn my_custom_logger = get_instance_fn( value="custom_callbacks.my_custom_logger", config_file_path=python_file_path