mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 18:24:49 +00:00
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:
@@ -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]:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user