From 1449af6393ed2c5ed90aab945708dc082f76a7e8 Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 08:48:31 +0000 Subject: [PATCH 1/8] refactor(logging): resolve dynamic rate limiter callbacks lazily in the logger registry Part of LIT-9423 --- .../custom_logger_registry.py | 62 ++++++++++++++----- litellm/proxy/proxy_server.py | 2 +- .../test_custom_logger_registry.py | 17 +++++ 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index 8367c9ee00e..c40aee63bf7 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -8,9 +8,11 @@ Example: "prometheus" -> PrometheusLogger """ +import importlib +from dataclasses import dataclass +from types import MappingProxyType from typing import Final -from litellm import _custom_logger_compatible_callbacks_literal from litellm.integrations.agentops import AgentOps from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook from litellm.integrations.argilla import ArgillaLogger @@ -53,16 +55,46 @@ from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook i VectorStorePreCallHook, ) from litellm.integrations.zerobus import ZerobusLogger -from litellm.proxy.hooks.dynamic_rate_limiter import ( # noqa: F401 # legacy module exports - PROXY_DynamicRateLimitHandler, - _PROXY_DynamicRateLimitHandler, # pyright: ignore[reportPrivateUsage,reportUnusedImport] # backwards-compatible package export -) -from litellm.proxy.hooks.dynamic_rate_limiter_v3 import ( # noqa: F401 # legacy module exports - PROXY_DynamicRateLimitHandlerV3, - _PROXY_DynamicRateLimitHandlerV3, # pyright: ignore[reportPrivateUsage,reportUnusedImport] # backwards-compatible package export + +_DYNAMIC_RATE_LIMITER_MODULE: Final = "litellm.proxy.hooks.dynamic_rate_limiter" +_DYNAMIC_RATE_LIMITER_V3_MODULE: Final = "litellm.proxy.hooks.dynamic_rate_limiter_v3" +_LEGACY_EXPORT_MODULES: Final = MappingProxyType( + { + "PROXY_DynamicRateLimitHandler": _DYNAMIC_RATE_LIMITER_MODULE, + "_PROXY_DynamicRateLimitHandler": _DYNAMIC_RATE_LIMITER_MODULE, + "PROXY_DynamicRateLimitHandlerV3": _DYNAMIC_RATE_LIMITER_V3_MODULE, + "_PROXY_DynamicRateLimitHandlerV3": _DYNAMIC_RATE_LIMITER_V3_MODULE, + } ) +def __getattr__(name: str) -> object: + module: Final = _LEGACY_EXPORT_MODULES.get(name) + if module is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + return getattr(importlib.import_module(module), name) + + +@dataclass(frozen=True, slots=True) +class _LazyCallbackClass: + module: str + name: str + + def resolve(self) -> type: + loaded: Final[object] = getattr(importlib.import_module(self.module), self.name) + if not isinstance(loaded, type): + raise TypeError(f"{self.module}.{self.name} is not a class") + return loaded + + +def _resolve(callback_class: type | _LazyCallbackClass) -> type: + match callback_class: + case _LazyCallbackClass(): + return callback_class.resolve() + case _: + return callback_class + + class CustomLoggerRegistry: """ Registry mapping the callback class string to the class type. @@ -107,8 +139,10 @@ class CustomLoggerRegistry: "pointfive": PointFiveLogger, "zerobus": ZerobusLogger, "aws_sqs": SQSLogger, - "dynamic_rate_limiter": PROXY_DynamicRateLimitHandler, - "dynamic_rate_limiter_v3": PROXY_DynamicRateLimitHandlerV3, + "dynamic_rate_limiter": _LazyCallbackClass(_DYNAMIC_RATE_LIMITER_MODULE, "PROXY_DynamicRateLimitHandler"), + "dynamic_rate_limiter_v3": _LazyCallbackClass( + _DYNAMIC_RATE_LIMITER_V3_MODULE, "PROXY_DynamicRateLimitHandlerV3" + ), "vector_store_pre_call_hook": VectorStorePreCallHook, "dotprompt": DotpromptManager, "bitbucket": BitBucketPromptManager, @@ -165,7 +199,7 @@ class CustomLoggerRegistry: callback_str, callback_class, ) in cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE.items(): - if callback_class == class_type: + if _resolve(callback_class) == class_type: return callback_str return None @@ -186,16 +220,16 @@ class CustomLoggerRegistry: callback_str, callback_class, ) in cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE.items(): - if callback_class == class_type: + if _resolve(callback_class) == class_type: callback_strs.append(callback_str) return callback_strs @classmethod def get_class_type_for_custom_logger_name( cls, - custom_logger_name: _custom_logger_compatible_callbacks_literal, + custom_logger_name: str, ) -> type: """ Get the class type for a given custom logger name """ - return cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE[custom_logger_name] + return _resolve(cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE[custom_logger_name]) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index dac46855fd6..68c484508ab 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -19325,7 +19325,7 @@ def _hidden_runtime_callback_names(configured_callback_names: frozenset[str]) -> from litellm.litellm_core_utils.custom_logger_registry import CustomLoggerRegistry configured_classes: Final = tuple( - CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE[name] + CustomLoggerRegistry.get_class_type_for_custom_logger_name(name) for name in configured_callback_names if name in CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE ) diff --git a/tests/unit/litellm_core_utils/test_custom_logger_registry.py b/tests/unit/litellm_core_utils/test_custom_logger_registry.py index 5fc21d75e3e..0bf7a99440e 100644 --- a/tests/unit/litellm_core_utils/test_custom_logger_registry.py +++ b/tests/unit/litellm_core_utils/test_custom_logger_registry.py @@ -406,3 +406,20 @@ def setup_and_teardown(): if hasattr(litellm, "in_memory_llm_clients_cache"): litellm.in_memory_llm_clients_cache.flush_cache() yield + + +@pytest.mark.parametrize( + ("callback_name", "module_path", "class_name"), + ( + ("dynamic_rate_limiter", "litellm.proxy.hooks.dynamic_rate_limiter", "PROXY_DynamicRateLimitHandler"), + ("dynamic_rate_limiter_v3", "litellm.proxy.hooks.dynamic_rate_limiter_v3", "PROXY_DynamicRateLimitHandlerV3"), + ), +) +def test_lazy_proxy_callback_classes_resolve_in_registry_lookups( + callback_name: str, module_path: str, class_name: str +) -> None: + proxy_class: Final = getattr(importlib.import_module(module_path), class_name) + + assert CustomLoggerRegistry.get_class_type_for_custom_logger_name(callback_name) is proxy_class + assert CustomLoggerRegistry.get_callback_str_from_class_type(proxy_class) == callback_name + assert CustomLoggerRegistry.get_all_callback_strs_from_class_type(proxy_class) == [callback_name] From 7a0417efa52b704a327019019bd5e39ef89831c5 Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 08:58:33 +0000 Subject: [PATCH 2/8] test(logging): cover lazy loading and legacy exports of the rate limiter registry entries Part of LIT-9423 --- .../test_custom_logger_registry.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/unit/litellm_core_utils/test_custom_logger_registry.py b/tests/unit/litellm_core_utils/test_custom_logger_registry.py index 0bf7a99440e..5ec4f376f39 100644 --- a/tests/unit/litellm_core_utils/test_custom_logger_registry.py +++ b/tests/unit/litellm_core_utils/test_custom_logger_registry.py @@ -1,6 +1,8 @@ import asyncio import importlib import os +import subprocess +import sys from collections.abc import AsyncIterator from datetime import datetime from typing import Final, Literal @@ -423,3 +425,41 @@ def test_lazy_proxy_callback_classes_resolve_in_registry_lookups( assert CustomLoggerRegistry.get_class_type_for_custom_logger_name(callback_name) is proxy_class assert CustomLoggerRegistry.get_callback_str_from_class_type(proxy_class) == callback_name assert CustomLoggerRegistry.get_all_callback_strs_from_class_type(proxy_class) == [callback_name] + + +def test_registry_import_leaves_proxy_rate_limiter_modules_unloaded() -> None: + loaded: Final = subprocess.run( + [ + sys.executable, + "-c", + "import sys, litellm.litellm_core_utils.custom_logger_registry; " + "print(sorted(m for m in sys.modules if m.startswith('litellm.proxy.hooks.dynamic_rate_limiter')))", + ], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + + assert loaded == "[]" + + +@pytest.mark.parametrize( + ("legacy_name", "module_path"), + ( + ("PROXY_DynamicRateLimitHandler", "litellm.proxy.hooks.dynamic_rate_limiter"), + ("_PROXY_DynamicRateLimitHandler", "litellm.proxy.hooks.dynamic_rate_limiter"), + ("PROXY_DynamicRateLimitHandlerV3", "litellm.proxy.hooks.dynamic_rate_limiter_v3"), + ("_PROXY_DynamicRateLimitHandlerV3", "litellm.proxy.hooks.dynamic_rate_limiter_v3"), + ), +) +def test_legacy_rate_limiter_exports_resolve_to_proxy_objects(legacy_name: str, module_path: str) -> None: + registry_module: Final = importlib.import_module("litellm.litellm_core_utils.custom_logger_registry") + + assert getattr(registry_module, legacy_name) is getattr(importlib.import_module(module_path), legacy_name) + + +def test_registry_module_still_raises_for_unknown_attributes() -> None: + registry_module: Final = importlib.import_module("litellm.litellm_core_utils.custom_logger_registry") + + with pytest.raises(AttributeError): + getattr(registry_module, "PROXY_NotARealHandler") From 5ae6d21f9d58681f62a7bec9fb6318d9303d5a5e Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 09:01:16 +0000 Subject: [PATCH 3/8] test(logging): run the lazy-import check in an isolated child interpreter Part of LIT-9423 --- .../test_custom_logger_registry.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/unit/litellm_core_utils/test_custom_logger_registry.py b/tests/unit/litellm_core_utils/test_custom_logger_registry.py index 5ec4f376f39..bb9b5ad1cae 100644 --- a/tests/unit/litellm_core_utils/test_custom_logger_registry.py +++ b/tests/unit/litellm_core_utils/test_custom_logger_registry.py @@ -1,8 +1,6 @@ import asyncio import importlib import os -import subprocess -import sys from collections.abc import AsyncIterator from datetime import datetime from typing import Final, Literal @@ -18,6 +16,7 @@ from litellm.litellm_core_utils.custom_logger_registry import ( CustomLoggerRegistry, ) from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER +from tests.test_litellm_rust.support.child_interpreter import run_child_interpreter from tests._vcr_conftest_common import install_live_call_probe, record_vcr_outcome # clear prometheus collectors / registry @@ -428,19 +427,14 @@ def test_lazy_proxy_callback_classes_resolve_in_registry_lookups( def test_registry_import_leaves_proxy_rate_limiter_modules_unloaded() -> None: - loaded: Final = subprocess.run( - [ - sys.executable, - "-c", - "import sys, litellm.litellm_core_utils.custom_logger_registry; " - "print(sorted(m for m in sys.modules if m.startswith('litellm.proxy.hooks.dynamic_rate_limiter')))", - ], - capture_output=True, - text=True, - check=True, - ).stdout.strip() + result: Final = run_child_interpreter( + "import sys, litellm.litellm_core_utils.custom_logger_registry\n" + "print(sorted(m for m in sys.modules if m.startswith('litellm.proxy.hooks.dynamic_rate_limiter')))", + timeout=120, + ) - assert loaded == "[]" + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "[]" @pytest.mark.parametrize( From f247d7d4a471a9f596003c1649645b6e695c58d4 Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 19:03:25 +0000 Subject: [PATCH 4/8] test(logging): check the lazy rate limiter import in-process instead of a child interpreter --- .../test_custom_logger_registry.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/unit/litellm_core_utils/test_custom_logger_registry.py b/tests/unit/litellm_core_utils/test_custom_logger_registry.py index bb9b5ad1cae..f028e528418 100644 --- a/tests/unit/litellm_core_utils/test_custom_logger_registry.py +++ b/tests/unit/litellm_core_utils/test_custom_logger_registry.py @@ -1,6 +1,7 @@ import asyncio import importlib import os +import sys from collections.abc import AsyncIterator from datetime import datetime from typing import Final, Literal @@ -16,7 +17,6 @@ from litellm.litellm_core_utils.custom_logger_registry import ( CustomLoggerRegistry, ) from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER -from tests.test_litellm_rust.support.child_interpreter import run_child_interpreter from tests._vcr_conftest_common import install_live_call_probe, record_vcr_outcome # clear prometheus collectors / registry @@ -426,15 +426,21 @@ def test_lazy_proxy_callback_classes_resolve_in_registry_lookups( assert CustomLoggerRegistry.get_all_callback_strs_from_class_type(proxy_class) == [callback_name] -def test_registry_import_leaves_proxy_rate_limiter_modules_unloaded() -> None: - result: Final = run_child_interpreter( - "import sys, litellm.litellm_core_utils.custom_logger_registry\n" - "print(sorted(m for m in sys.modules if m.startswith('litellm.proxy.hooks.dynamic_rate_limiter')))", - timeout=120, +def test_registry_import_leaves_proxy_rate_limiter_modules_unloaded(monkeypatch: pytest.MonkeyPatch) -> None: + registry_module_name: Final = "litellm.litellm_core_utils.custom_logger_registry" + limiter_module_names: Final = ( + "litellm.proxy.hooks.dynamic_rate_limiter", + "litellm.proxy.hooks.dynamic_rate_limiter_v3", ) + for module_name in (registry_module_name, *limiter_module_names): + parent_name, _, attribute_name = module_name.rpartition(".") + monkeypatch.setattr(importlib.import_module(parent_name), attribute_name, importlib.import_module(module_name)) + monkeypatch.delitem(sys.modules, module_name) - assert result.returncode == 0, result.stderr - assert result.stdout.strip() == "[]" + fresh_registry_module: Final = importlib.import_module(registry_module_name) + + assert fresh_registry_module.CustomLoggerRegistry is not CustomLoggerRegistry + assert [name for name in limiter_module_names if name in sys.modules] == [] @pytest.mark.parametrize( From f4fb4936f5c4daa2c34f803f5790f98d8619fc4d Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 19:23:35 +0000 Subject: [PATCH 5/8] fix(sdk): resolve litellm.proxy lazily on attribute access The layer cleanup stopped import litellm from loading any proxy module, so litellm.proxy was no longer bound and litellm.proxy._types.X raised AttributeError. The module __getattr__ now imports litellm.proxy on first access, which keeps import litellm free of proxy._types and fixes the docs and code-quality checks on main --- litellm/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litellm/__init__.py b/litellm/__init__.py index 4196912488f..a6bbd132cbf 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -2386,6 +2386,11 @@ def __getattr__(name: str) -> Any: handler_func: Final = registry[name] return handler_func(name) + if name == "proxy": + import importlib + + return importlib.import_module("litellm.proxy") + if name == "harness" or name in _AGENT_EXPORTS: import importlib From 7f171e2e89a80988c4f0566d81fa715266a5e3aa Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sat, 10 Oct 2026 21:04:24 +0000 Subject: [PATCH 6/8] fix(types): drop unused TypeIs import from proxy/_types.py --- litellm/proxy/_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 7b2a63974d8..b6c81f17761 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -25,7 +25,6 @@ from typing_extensions import ( ReadOnly, Required, TypedDict, - TypeIs, # noqa: TID251 # narrows the before-validator input without copying it ) from litellm._uuid import uuid From afaf339471fa8889f7d9d806afa1d811bda014dc Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sun, 11 Oct 2026 18:15:11 +0000 Subject: [PATCH 7/8] refactor(logging): build the logger registry mapping lazily instead of lazy entries The two dynamic rate limiter entries are real classes again. The whole mapping is built on first read, so the proxy hooks still load only when the registry is used, and proxy_server.py reads the dict as before --- .../custom_logger_registry.py | 147 +++++++++--------- litellm/proxy/proxy_server.py | 2 +- .../test_custom_logger_registry.py | 13 +- 3 files changed, 84 insertions(+), 78 deletions(-) diff --git a/litellm/litellm_core_utils/custom_logger_registry.py b/litellm/litellm_core_utils/custom_logger_registry.py index c40aee63bf7..9f84a67c89e 100644 --- a/litellm/litellm_core_utils/custom_logger_registry.py +++ b/litellm/litellm_core_utils/custom_logger_registry.py @@ -8,11 +8,11 @@ Example: "prometheus" -> PrometheusLogger """ -import importlib -from dataclasses import dataclass -from types import MappingProxyType +import functools +from collections.abc import Mapping from typing import Final +from litellm import _custom_logger_compatible_callbacks_literal from litellm.integrations.agentops import AgentOps from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook from litellm.integrations.argilla import ArgillaLogger @@ -56,51 +56,45 @@ from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook i ) from litellm.integrations.zerobus import ZerobusLogger -_DYNAMIC_RATE_LIMITER_MODULE: Final = "litellm.proxy.hooks.dynamic_rate_limiter" -_DYNAMIC_RATE_LIMITER_V3_MODULE: Final = "litellm.proxy.hooks.dynamic_rate_limiter_v3" -_LEGACY_EXPORT_MODULES: Final = MappingProxyType( - { - "PROXY_DynamicRateLimitHandler": _DYNAMIC_RATE_LIMITER_MODULE, - "_PROXY_DynamicRateLimitHandler": _DYNAMIC_RATE_LIMITER_MODULE, - "PROXY_DynamicRateLimitHandlerV3": _DYNAMIC_RATE_LIMITER_V3_MODULE, - "_PROXY_DynamicRateLimitHandlerV3": _DYNAMIC_RATE_LIMITER_V3_MODULE, + +def _enterprise_callback_classes() -> Mapping[str, type]: + try: + from litellm_enterprise.enterprise_callbacks.pagerduty.pagerduty import ( + PagerDutyAlerting, + ) + from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import ( + ResendEmailLogger, + ) + from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import ( + SendGridEmailLogger, + ) + from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import ( + SMTPEmailLogger, + ) + + from litellm.integrations.generic_api.generic_api_callback import ( + GenericAPILogger, + ) + except ImportError: + return {} + return { + "pagerduty": PagerDutyAlerting, + "generic_api": GenericAPILogger, + "resend_email": ResendEmailLogger, + "sendgrid_email": SendGridEmailLogger, + "smtp_email": SMTPEmailLogger, } -) -def __getattr__(name: str) -> object: - module: Final = _LEGACY_EXPORT_MODULES.get(name) - if module is None: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - return getattr(importlib.import_module(module), name) +_ENTERPRISE_CALLBACK_CLASSES: Final = _enterprise_callback_classes() -@dataclass(frozen=True, slots=True) -class _LazyCallbackClass: - module: str - name: str +@functools.cache +def _callback_class_str_to_class_type() -> Mapping[str, type]: + from litellm.proxy.hooks.dynamic_rate_limiter import PROXY_DynamicRateLimitHandler + from litellm.proxy.hooks.dynamic_rate_limiter_v3 import PROXY_DynamicRateLimitHandlerV3 - def resolve(self) -> type: - loaded: Final[object] = getattr(importlib.import_module(self.module), self.name) - if not isinstance(loaded, type): - raise TypeError(f"{self.module}.{self.name} is not a class") - return loaded - - -def _resolve(callback_class: type | _LazyCallbackClass) -> type: - match callback_class: - case _LazyCallbackClass(): - return callback_class.resolve() - case _: - return callback_class - - -class CustomLoggerRegistry: - """ - Registry mapping the callback class string to the class type. - """ - - CALLBACK_CLASS_STR_TO_CLASS_TYPE = { + return { "lago": LagoLogger, "openmeter": OpenMeterLogger, "braintrust": BraintrustLogger, @@ -139,10 +133,8 @@ class CustomLoggerRegistry: "pointfive": PointFiveLogger, "zerobus": ZerobusLogger, "aws_sqs": SQSLogger, - "dynamic_rate_limiter": _LazyCallbackClass(_DYNAMIC_RATE_LIMITER_MODULE, "PROXY_DynamicRateLimitHandler"), - "dynamic_rate_limiter_v3": _LazyCallbackClass( - _DYNAMIC_RATE_LIMITER_V3_MODULE, "PROXY_DynamicRateLimitHandlerV3" - ), + "dynamic_rate_limiter": PROXY_DynamicRateLimitHandler, + "dynamic_rate_limiter_v3": PROXY_DynamicRateLimitHandlerV3, "vector_store_pre_call_hook": VectorStorePreCallHook, "dotprompt": DotpromptManager, "bitbucket": BitBucketPromptManager, @@ -153,36 +145,39 @@ class CustomLoggerRegistry: "vantage": VantageLogger, "posthog": PostHogLogger, "newrelic": NewRelicLogger, + **_ENTERPRISE_CALLBACK_CLASSES, } - try: - from litellm_enterprise.enterprise_callbacks.pagerduty.pagerduty import ( - PagerDutyAlerting, - ) - from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import ( - ResendEmailLogger, - ) - from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import ( - SendGridEmailLogger, - ) - from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import ( - SMTPEmailLogger, - ) - from litellm.integrations.generic_api.generic_api_callback import ( - GenericAPILogger, - ) +class _LazyCallbackClassMap: + def __get__(self, instance: object, owner: type | None = None) -> Mapping[str, type]: + return _callback_class_str_to_class_type() - enterprise_loggers = { - "pagerduty": PagerDutyAlerting, - "generic_api": GenericAPILogger, - "resend_email": ResendEmailLogger, - "sendgrid_email": SendGridEmailLogger, - "smtp_email": SMTPEmailLogger, - } - CALLBACK_CLASS_STR_TO_CLASS_TYPE.update(enterprise_loggers) - except ImportError: - pass # enterprise not installed + +_LEGACY_RATE_LIMITER_EXPORTS: Final = frozenset(("PROXY_DynamicRateLimitHandler", "_PROXY_DynamicRateLimitHandler")) +_LEGACY_RATE_LIMITER_V3_EXPORTS: Final = frozenset( + ("PROXY_DynamicRateLimitHandlerV3", "_PROXY_DynamicRateLimitHandlerV3") +) + + +def __getattr__(name: str) -> object: + if name in _LEGACY_RATE_LIMITER_EXPORTS: + from litellm.proxy.hooks import dynamic_rate_limiter + + return getattr(dynamic_rate_limiter, name) + if name in _LEGACY_RATE_LIMITER_V3_EXPORTS: + from litellm.proxy.hooks import dynamic_rate_limiter_v3 + + return getattr(dynamic_rate_limiter_v3, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +class CustomLoggerRegistry: + """ + Registry mapping the callback class string to the class type. + """ + + CALLBACK_CLASS_STR_TO_CLASS_TYPE = _LazyCallbackClassMap() @classmethod def get_callback_str_from_class_type(cls, class_type: type) -> str | None: @@ -199,7 +194,7 @@ class CustomLoggerRegistry: callback_str, callback_class, ) in cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE.items(): - if _resolve(callback_class) == class_type: + if callback_class == class_type: return callback_str return None @@ -220,16 +215,16 @@ class CustomLoggerRegistry: callback_str, callback_class, ) in cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE.items(): - if _resolve(callback_class) == class_type: + if callback_class == class_type: callback_strs.append(callback_str) return callback_strs @classmethod def get_class_type_for_custom_logger_name( cls, - custom_logger_name: str, + custom_logger_name: _custom_logger_compatible_callbacks_literal, ) -> type: """ Get the class type for a given custom logger name """ - return _resolve(cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE[custom_logger_name]) + return cls.CALLBACK_CLASS_STR_TO_CLASS_TYPE[custom_logger_name] diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 8f75d4a954a..089647c6297 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -19331,7 +19331,7 @@ def _hidden_runtime_callback_names(configured_callback_names: frozenset[str]) -> from litellm.litellm_core_utils.custom_logger_registry import CustomLoggerRegistry configured_classes: Final = tuple( - CustomLoggerRegistry.get_class_type_for_custom_logger_name(name) + CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE[name] for name in configured_callback_names if name in CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE ) diff --git a/tests/unit/litellm_core_utils/test_custom_logger_registry.py b/tests/unit/litellm_core_utils/test_custom_logger_registry.py index f028e528418..8c839cb05f0 100644 --- a/tests/unit/litellm_core_utils/test_custom_logger_registry.py +++ b/tests/unit/litellm_core_utils/test_custom_logger_registry.py @@ -416,11 +416,12 @@ def setup_and_teardown(): ("dynamic_rate_limiter_v3", "litellm.proxy.hooks.dynamic_rate_limiter_v3", "PROXY_DynamicRateLimitHandlerV3"), ), ) -def test_lazy_proxy_callback_classes_resolve_in_registry_lookups( +def test_lazy_proxy_callback_classes_are_real_classes_in_the_registry( callback_name: str, module_path: str, class_name: str ) -> None: proxy_class: Final = getattr(importlib.import_module(module_path), class_name) + assert CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE[callback_name] is proxy_class assert CustomLoggerRegistry.get_class_type_for_custom_logger_name(callback_name) is proxy_class assert CustomLoggerRegistry.get_callback_str_from_class_type(proxy_class) == callback_name assert CustomLoggerRegistry.get_all_callback_strs_from_class_type(proxy_class) == [callback_name] @@ -463,3 +464,13 @@ def test_registry_module_still_raises_for_unknown_attributes() -> None: with pytest.raises(AttributeError): getattr(registry_module, "PROXY_NotARealHandler") + + +def test_registry_maps_every_callback_name_to_a_class() -> None: + non_classes: Final = [ + name + for name, callback_class in CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE.items() + if not isinstance(callback_class, type) + ] + + assert non_classes == [] From aaa41a35cbb8fbe8efadadc39c182fb4fba726ec Mon Sep 17 00:00:00 2001 From: nate-berri Date: Sun, 11 Oct 2026 18:35:16 +0000 Subject: [PATCH 8/8] test(logging): cover the logger registry without the enterprise package --- .../test_custom_logger_registry.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/litellm_core_utils/test_custom_logger_registry.py b/tests/unit/litellm_core_utils/test_custom_logger_registry.py index 8c839cb05f0..eb5aaa6f187 100644 --- a/tests/unit/litellm_core_utils/test_custom_logger_registry.py +++ b/tests/unit/litellm_core_utils/test_custom_logger_registry.py @@ -474,3 +474,26 @@ def test_registry_maps_every_callback_name_to_a_class() -> None: ] assert non_classes == [] + + +def test_registry_builds_without_enterprise_callbacks_when_enterprise_is_missing( + monkeypatch: pytest.MonkeyPatch, +) -> None: + registry_module_name: Final = "litellm.litellm_core_utils.custom_logger_registry" + monkeypatch.setitem(sys.modules, "litellm_enterprise.enterprise_callbacks.pagerduty.pagerduty", None) + monkeypatch.setattr( + litellm.litellm_core_utils, "custom_logger_registry", importlib.import_module(registry_module_name) + ) + monkeypatch.delitem(sys.modules, registry_module_name) + + fresh_mapping: Final = importlib.import_module( + registry_module_name + ).CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE + + assert fresh_mapping.keys() == CustomLoggerRegistry.CALLBACK_CLASS_STR_TO_CLASS_TYPE.keys() - { + "pagerduty", + "generic_api", + "resend_email", + "sendgrid_email", + "smtp_email", + }