Merge pull request #27160 from BerriAI/litellm_/peaceful-gates-6e46e7

[Fix] Proxy: Break managed-resources import cycle on Python 3.13
This commit is contained in:
shin-berri
2026-05-04 21:11:53 -07:00
committed by GitHub
9 changed files with 112 additions and 30 deletions
+2 -2
View File
@@ -1475,7 +1475,7 @@ jobs:
- run:
name: Run tests
command: |
uv run --no-sync python -m pytest -v tests/otel_tests -x --junitxml=test-results/junit.xml --durations=5
uv run --no-sync python -m pytest -v tests/otel_tests --junitxml=test-results/junit.xml --durations=5
no_output_timeout: 15m
# Clean up first container
- run:
@@ -1935,7 +1935,7 @@ jobs:
name: Run Vertex AI, Google AI Studio Node.js tests
command: |
cd tests/pass_through_tests
npx jest . --verbose
NODE_OPTIONS=--experimental-vm-modules npx jest . --verbose
no_output_timeout: 30m
- run:
name: Run tests
@@ -11,8 +11,10 @@ unscoped query.
from typing import Any, Dict, List, Optional
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
from litellm.proxy._types import (
UserAPIKeyAuth,
user_api_key_has_admin_view as _user_has_admin_view,
)
def build_list_page(items: List[Any], has_more: bool = False) -> Dict[str, Any]:
+13
View File
@@ -2735,6 +2735,19 @@ class UserAPIKeyAuth(
)
def user_api_key_has_admin_view(user_api_key_dict: UserAPIKeyAuth) -> bool:
"""Return True if the caller's role grants unscoped read access to all
tenant resources (managed files, batches, vector stores, spend rows, etc).
Lives on _types.py so leaf modules (e.g. litellm.llms.base_llm.managed_resources)
can use it without pulling in litellm.proxy.utils via management_endpoints.
"""
return user_api_key_dict.user_role in (
LitellmUserRoles.PROXY_ADMIN,
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
)
class UserInfoResponse(LiteLLMPydanticObjectBase):
user_id: Optional[str]
user_info: Optional[Union[dict, BaseModel]]
@@ -46,6 +46,9 @@ litellm_settings:
cache: true
callbacks: ["otel", "prometheus"]
disable_end_user_cost_tracking_prometheus_only: True
# /metrics auth is on by default; tests/otel_tests/test_prometheus.py
# scrapes the endpoint without credentials, so opt out here.
require_auth_for_metrics_endpoint: False
guardrails:
- guardrail_name: "bedrock-pre-guard"
+17 -13
View File
@@ -11,14 +11,10 @@ from .parallel_request_limiter import _PROXY_MaxParallelRequestsHandler
from .parallel_request_limiter_v3 import _PROXY_MaxParallelRequestsHandler_v3
from .responses_id_security import ResponsesIDSecurity
### CHECK IF ENTERPRISE HOOKS ARE AVAILABLE ####
try:
from enterprise.enterprise_hooks import ENTERPRISE_PROXY_HOOKS
except ImportError:
ENTERPRISE_PROXY_HOOKS = {}
# List of all available hooks that can be enabled
# List of all available hooks that can be enabled.
# Defined before the enterprise import below so that any module re-imported
# transitively through `enterprise.enterprise_hooks` can resolve `PROXY_HOOKS`
# and `get_proxy_hook` from this partially-initialized module without circling.
PROXY_HOOKS = {
"max_budget_limiter": _PROXY_MaxBudgetLimiter,
"parallel_request_limiter": _PROXY_MaxParallelRequestsHandler_v3,
@@ -34,11 +30,6 @@ if os.getenv("LEGACY_MULTI_INSTANCE_RATE_LIMITING", "false").lower() == "true":
PROXY_HOOKS["parallel_request_limiter"] = _PROXY_MaxParallelRequestsHandler
### update PROXY_HOOKS with ENTERPRISE_PROXY_HOOKS ###
PROXY_HOOKS.update(ENTERPRISE_PROXY_HOOKS)
def get_proxy_hook(
hook_name: Union[
Literal[
@@ -58,3 +49,16 @@ def get_proxy_hook(
f"Unknown hook: {hook_name}. Available hooks: {list(PROXY_HOOKS.keys())}"
)
return PROXY_HOOKS[hook_name]
### CHECK IF ENTERPRISE HOOKS ARE AVAILABLE ####
try:
from enterprise.enterprise_hooks import ENTERPRISE_PROXY_HOOKS
except ImportError:
ENTERPRISE_PROXY_HOOKS = {}
### update PROXY_HOOKS with ENTERPRISE_PROXY_HOOKS ###
PROXY_HOOKS.update(ENTERPRISE_PROXY_HOOKS)
@@ -16,6 +16,7 @@ from litellm.proxy._types import (
NewProjectRequest,
UpdateProjectRequest,
UserAPIKeyAuth,
user_api_key_has_admin_view as _user_has_admin_view, # noqa: F401 re-exported
)
from litellm.proxy.utils import _premium_user_check
@@ -24,13 +25,6 @@ if TYPE_CHECKING:
from litellm.proxy.utils import PrismaClient, ProxyLogging
def _user_has_admin_view(user_api_key_dict: UserAPIKeyAuth) -> bool:
return (
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
)
def require_caller_user_id_for_non_admin(
user_api_key_dict: UserAPIKeyAuth,
) -> str:
@@ -64,6 +64,6 @@ async def test_key_logging_callbacks():
assert health_data["logging_callbacks"]["callbacks"] == ["gcs_bucket"]
assert health_data["logging_callbacks"]["status"] == "unhealthy"
assert (
"Failed to load vertex credentials"
"GCS_BUCKET_NAME is not set in the environment"
in health_data["logging_callbacks"]["details"]
)
+20 -5
View File
@@ -99,14 +99,29 @@ async def test_chat_completion_check_otel_spans():
await asyncio.sleep(3)
otel_spans = await get_otel_spans(session=session, key=key)
# /otel-spans requires proxy admin; use the master key.
otel_spans = await get_otel_spans(session=session, key="sk-1234")
print("otel_spans: ", otel_spans)
all_otel_spans = otel_spans["otel_spans"]
most_recent_parent = str(otel_spans["most_recent_parent"])
print("Most recent OTEL parent: ", most_recent_parent)
print("\n spans grouped by parent: ", otel_spans["spans_grouped_by_parent"])
parent_trace_spans = otel_spans["spans_grouped_by_parent"][most_recent_parent]
spans_grouped_by_parent = otel_spans["spans_grouped_by_parent"]
print("\n spans grouped by parent: ", spans_grouped_by_parent)
# The GET /otel-spans request itself produces auth spans that beat
# the chat-completion spans on start_time, so `most_recent_parent`
# points at the wrong trace. Pick the chat-completion trace by
# content: it's the one carrying the full set of expected markers.
chat_completion_markers = {
"postgres",
"redis",
"raw_gen_ai_request",
"batch_write_to_db",
}
parent_trace_spans = next(
spans
for spans in spans_grouped_by_parent.values()
if chat_completion_markers.issubset(spans)
)
print("Parent trace spans: ", parent_trace_spans)
@@ -0,0 +1,51 @@
"""Regression guard for the enterprise hook registration / import cycle.
Python 3.13 is stricter about partially-initialized modules and surfaces
cycles that Python 3.12 silently tolerated. The previous bug:
litellm.proxy.hooks.__init__
-> enterprise.enterprise_hooks
-> litellm_enterprise.proxy.hooks.managed_files
-> litellm.llms.base_llm.managed_resources.isolation
-> litellm.proxy.management_endpoints.common_utils
-> litellm.proxy.utils (re-enters litellm.proxy.hooks mid-init)
silently swallowed the ImportError in `hooks/__init__.py`, leaving
``managed_files`` unregistered and the /files endpoint returning 500.
"""
import pytest
from litellm.proxy.hooks import PROXY_HOOKS, get_proxy_hook
def test_managed_files_hook_registered():
pytest.importorskip("litellm_enterprise")
assert "managed_files" in PROXY_HOOKS
hook_cls = get_proxy_hook("managed_files")
assert hook_cls.__name__ == "_PROXY_LiteLLMManagedFiles"
def test_managed_vector_stores_hook_registered():
pytest.importorskip("litellm_enterprise")
assert "managed_vector_stores" in PROXY_HOOKS
hook_cls = get_proxy_hook("managed_vector_stores")
assert hook_cls.__name__ == "_PROXY_LiteLLMManagedVectorStores"
def test_isolation_module_does_not_pull_in_proxy_utils():
"""Layering guard: litellm.llms.* must not transitively import
litellm.proxy.utils, which would reintroduce the import cycle."""
import importlib
import sys
for mod in [
"litellm.proxy.utils",
"litellm.proxy.management_endpoints.common_utils",
"litellm.llms.base_llm.managed_resources.isolation",
]:
sys.modules.pop(mod, None)
importlib.import_module("litellm.llms.base_llm.managed_resources.isolation")
assert "litellm.proxy.utils" not in sys.modules
assert "litellm.proxy.management_endpoints.common_utils" not in sys.modules