From 6c95a2d970867fa750f04f33126911f491f2a6dd Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 7 Nov 2026 07:44:55 +0900 Subject: [PATCH 1/3] fix(guardrails): check a model-level post_call response once across the proxy and deployment layers (#50300) Backport of #50300 to lts/1.108.x Cherry-picked from 2b4485dee53 (main) Conflicts in litellm/integrations/custom_guardrail.py and litellm/proxy/utils.py came from code the line does not have (the pydantic import, _REQUEST_FIELDS and _guardrails_listed_in in custom_guardrail.py, the stream token-id helpers in proxy/utils.py). Kept the line's code, added the pick's helpers and pydantic import, and defined _REQUEST_FIELDS in both files next to the line's own module-level TypeAdapter constants --- litellm/integrations/custom_guardrail.py | 132 +++- litellm/proxy/utils.py | 8 + .../test_custom_guardrail_post_call_once.py | 566 ++++++++++++++++++ .../proxy_logging/test_guardrail_pipeline.py | 1 + 4 files changed, 690 insertions(+), 17 deletions(-) create mode 100644 tests/unit/integrations/test_custom_guardrail_post_call_once.py diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 1cf71cf59e7..95c8063d7d2 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -1,6 +1,7 @@ import contextvars import copy import hashlib +import json import os import secrets from collections.abc import Iterable, Mapping, Sequence @@ -9,6 +10,7 @@ from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Optional, cast, get_args import httpx +from pydantic import BaseModel, TypeAdapter from litellm._internal_context import with_service_target from litellm._logging import verbose_logger @@ -71,6 +73,7 @@ GUARDRAIL_SESSIONS_TARGET: Final = "guardrail_sessions" _PRE_CALL_EXECUTED_TOKEN: Final = secrets.token_hex(16) _PRE_CALL_RAISED_TOKEN: Final = secrets.token_hex(16) _DEPLOYMENT_HOOK_RAN_TOKEN: Final = secrets.token_hex(16) +_POST_CALL_CHECKED_TOKEN: Final = secrets.token_hex(16) _GUARDRAIL_BLOCK_STATUS_CODES: Final = frozenset({400, 403, 422}) @@ -239,6 +242,98 @@ def _request_carries_marker(data: Mapping[str, object], marker: str | None) -> b return False +def _record_guardrail_marker( + data: dict[str, object], # mutable-ok: the request payload the marker is recorded on + marker: str | None, +) -> None: + if marker is None: + return + for meta_key in ("metadata", "litellm_metadata"): + meta = data.get(meta_key) + if isinstance(meta, dict): + executed = meta.get(PRE_CALL_EXECUTED_GUARDRAILS_KEY) + if isinstance(executed, list): + if marker not in executed: + executed.append(marker) + else: + meta[PRE_CALL_EXECUTED_GUARDRAILS_KEY] = [marker] + return + data["metadata"] = {PRE_CALL_EXECUTED_GUARDRAILS_KEY: [marker]} + + +_RESPONSE_METADATA_KEYS: Final = frozenset( + { + "id", + "created", + "created_at", + "model", + "usage", + "usage_info", + "system_fingerprint", + "_hidden_params", + "_response_ms", + } +) + + +def _response_fingerprint(response: object) -> str | None: + """Digest of everything but metadata set between layers, so an unknown field counts as content.""" + try: + fields: Final[Mapping[str, object]] = ( + response.model_dump(mode="json") + if isinstance(response, BaseModel) + else _REQUEST_FIELDS.validate_python(response) + ) + content: Final = {key: value for key, value in fields.items() if key not in _RESPONSE_METADATA_KEYS} + if not content: + return None + return hashlib.sha256( + json.dumps(content, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + ).hexdigest() + except (TypeError, ValueError): + return None + + +_RESPONSE_GUARDRAIL_CHECKS_KEY: Final = "guardrail_checks" + + +def _str_keyed_dict(value: object) -> Mapping[str, object] | None: + if not isinstance(value, dict): + return None + return cast("Mapping[str, object]", value) # cast-ok: response bodies and _hidden_params are keyed by field name + + +def _response_hidden_params(response: object) -> Mapping[str, object] | None: + as_dict: Final = _str_keyed_dict(response) + return _str_keyed_dict( + as_dict.get("_hidden_params") if as_dict is not None else getattr(response, "_hidden_params", None) + ) + + +def _write_response_marker(response: object, marker: str | None) -> None: + """Record a check on the response itself, so it survives the router copying request metadata per hop.""" + if marker is None: + return + hidden: Final = _response_hidden_params(response) + if not isinstance(hidden, dict): + return + hidden[_RESPONSE_GUARDRAIL_CHECKS_KEY] = tuple( + _str_collection(hidden.get(_RESPONSE_GUARDRAIL_CHECKS_KEY)) | {marker} + ) + + +def _response_carries_marker(response: object, marker: str | None) -> bool: + hidden: Final = _response_hidden_params(response) + return ( + marker is not None + and hidden is not None + and marker in _str_collection(hidden.get(_RESPONSE_GUARDRAIL_CHECKS_KEY)) + ) + + +_REQUEST_FIELDS: Final = TypeAdapter(Mapping[str, object]) + + class CustomGuardrail(CustomLogger): # If True, during_call runs async_moderation_hook instead of the unified apply_guardrail path. use_native_during_call_hook: ClassVar[bool] = False @@ -867,20 +962,7 @@ class CustomGuardrail(CustomLogger): With ``raised``, records instead that the hook raised as a policy pipeline step the pipeline then moved past. The deployment-level hook still runs it. """ - marker: Final = self._pre_call_marker(raised=raised) - if marker is None: - return - for meta_key in ("metadata", "litellm_metadata"): - meta = data.get(meta_key) - if isinstance(meta, dict): - executed = meta.get(PRE_CALL_EXECUTED_GUARDRAILS_KEY) - if isinstance(executed, list): - if marker not in executed: - executed.append(marker) - else: - meta[PRE_CALL_EXECUTED_GUARDRAILS_KEY] = [marker] - return - data["metadata"] = {PRE_CALL_EXECUTED_GUARDRAILS_KEY: [marker]} + _record_guardrail_marker(data, self._pre_call_marker(raised=raised)) def pre_call_hook_ran_or_raised(self, data: Mapping[str, object]) -> bool: """Whether this guardrail's ``async_pre_call_hook`` ran for this request, whatever it decided.""" @@ -1077,10 +1159,26 @@ class CustomGuardrail(CustomLogger): cast("dict[str, object]", request_data) # cast-ok: proxy request payloads are str-keyed ) - if not self._is_valid_response_type(result): - return None + returned: Final = result if self._is_valid_response_type(result) else None + if target is self: + return returned + checked: Final = returned if returned is not None else response + marker: Final = self._post_call_check_marker(checked) + _record_guardrail_marker( + cast("dict[str, object]", request_data), # cast-ok: proxy request payloads are str-keyed + marker, + ) + _write_response_marker(checked, marker) + return returned - return result + def _post_call_check_marker(self, response: object) -> str | None: + fingerprint: Final = _response_fingerprint(response) + return self._marker(f"{_POST_CALL_CHECKED_TOKEN}:{fingerprint}") if fingerprint is not None else None + + def post_call_already_checked(self, data: Mapping[str, object], response: object) -> bool: + """Whether a deployment-level hook of this request already ran this guardrail on this exact response.""" + marker: Final = self._post_call_check_marker(response) + return _request_carries_marker(data, marker) or _response_carries_marker(response, marker) async def async_logging_hook( self, diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 8479f8e45b9..45ca9fb737a 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -328,6 +328,7 @@ _VIEW_SETUP_POLL_INTERVAL_SECONDS: Final = 5.0 _VIEW_SETUP_DEADLINE_SECONDS: Final = 15 * 60.0 _VIEW_SETUP_GATE_TABLE: Final = '"LiteLLM_SpendLogs"' _VIEW_SETUP_GATE_PROBE_ROWS: Final = TypeAdapter(tuple[Mapping[str, bool], ...]) +_REQUEST_FIELDS: Final = TypeAdapter(Mapping[str, object]) _ViewSetupOutcome: TypeAlias = Literal["ready", "timed_out"] _ViewSetupAttempt: TypeAlias = Literal["ready", "table_missing"] | Exception @@ -4143,6 +4144,7 @@ class ProxyLogging: response = pipeline_response # rebind-ok: adopt the pipeline's replacement response, same contract as the callback loops below pipeline_managed: Final = pipeline_managed_guardrail_names(data, "post_call") + request_view: Final = _REQUEST_FIELDS.validate_python(data) guardrail_callbacks, other_callbacks = _partition_post_call_callbacks() try: # Merge model-level guardrails before checking which guardrails to run @@ -4173,6 +4175,9 @@ class ProxyLogging: ): continue + if callback.post_call_already_checked(request_view, response) is True: + continue + guardrail_response: Any | None = None if "apply_guardrail" in type(callback).__dict__ and not callback.use_native_lifecycle_hooks: @@ -4246,10 +4251,13 @@ class ProxyLogging: immediately before awaiting, and the unified hook pops it before its first suspension point, so concurrent guardrails never race on that key. """ + request_view: Final = _REQUEST_FIELDS.validate_python(data) async def _run_one(callback: CustomGuardrail) -> None: if callback.should_run_guardrail(data=guardrail_data, event_type=GuardrailEventHooks.post_call) is not True: return + if callback.post_call_already_checked(request_view, response) is True: + return if "apply_guardrail" in type(callback).__dict__ and not callback.use_native_lifecycle_hooks: data["guardrail_to_apply"] = callback await self._run_guardrail_with_metrics( diff --git a/tests/unit/integrations/test_custom_guardrail_post_call_once.py b/tests/unit/integrations/test_custom_guardrail_post_call_once.py new file mode 100644 index 00000000000..fb823156929 --- /dev/null +++ b/tests/unit/integrations/test_custom_guardrail_post_call_once.py @@ -0,0 +1,566 @@ +import asyncio +import json +from collections.abc import AsyncIterable, Callable, Iterator, Mapping +from dataclasses import dataclass +from types import MappingProxyType +from typing import Final, Literal +from unittest.mock import AsyncMock, patch + +import httpx +import pytest +import respx +from fastapi import HTTPException, Request, Response +from fastapi.responses import StreamingResponse +from mcp.types import Tool as MCPTool +from pydantic import BaseModel, TypeAdapter + +import litellm +from litellm.caching.caching import Cache +from litellm.integrations.custom_guardrail import CustomGuardrail +from litellm.llms.base_llm.ocr.transformation import OCRResponse +from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing +from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache +from litellm.proxy.utils import ProxyLogging +from litellm.responses.mcp.litellm_proxy_mcp_handler import LiteLLM_Proxy_MCP_Handler +from litellm.router import Router +from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.utils import CallTypes, Choices, GenericGuardrailAPIInputs, LLMResponseTypes, Message, ModelResponse + +pytestmark = pytest.mark.respx(assert_all_called=False) + +REPLY: Final = "hi from the model" +REWRITTEN: Final = "rewritten by the proxy" +MASKED: Final = "masked by G" + +Route = Literal["acompletion", "aresponses", "anthropic_messages"] + +_OPENAI_API_BASE: Final = "https://api.openai.com/v1" +_FAILING_API_BASE: Final = "https://primary.invalid/v1" +_MCP_API_BASE: Final = "https://mcp-rounds.invalid/v1" + +_CHAT_REPLY: Final[Mapping[str, object]] = { + "id": "chatcmpl-1", + "object": "chat.completion", + "created": 0, + "model": "gpt-4o", + "choices": [{"index": 0, "message": {"role": "assistant", "content": REPLY}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, +} +_CHAT_SSE: Final = ( + "".join( + f"data: {json.dumps(chunk)}\n\n" + for chunk in ( + { + "id": "chatcmpl-1", + "object": "chat.completion.chunk", + "created": 0, + "model": "gpt-4o", + "choices": [{"index": 0, "delta": {"role": "assistant", "content": REPLY}, "finish_reason": None}], + }, + { + "id": "chatcmpl-1", + "object": "chat.completion.chunk", + "created": 0, + "model": "gpt-4o", + "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], + }, + ) + ) + + "data: [DONE]\n\n" +) +_RESPONSES_REPLY: Final[Mapping[str, object]] = { + "id": "resp_1", + "object": "response", + "created_at": 0, + "status": "completed", + "model": "gpt-4o", + "output": [ + { + "type": "message", + "id": "msg_1", + "status": "completed", + "role": "assistant", + "content": [{"type": "output_text", "text": REPLY, "annotations": []}], + } + ], + "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2}, +} +_RESPONSES_SSE: Final = "".join( + f"data: {json.dumps(event)}\n\n" + for event in ( + {"type": "response.created", "sequence_number": 0, "response": {**_RESPONSES_REPLY, "output": []}}, + { + "type": "response.output_text.delta", + "sequence_number": 1, + "item_id": "msg_1", + "output_index": 0, + "content_index": 0, + "delta": REPLY, + }, + {"type": "response.completed", "sequence_number": 2, "response": _RESPONSES_REPLY}, + ) +) +_TOOL_CALL_ROUND: Final[Mapping[str, object]] = { + **_RESPONSES_REPLY, + "id": "resp_tool_call", + "output": [ + { + "type": "function_call", + "id": "fc_1", + "call_id": "call_echo", + "name": "echo", + "arguments": '{"value": "PING"}', + "status": "completed", + } + ], +} +_JSON_OBJECT: Final = TypeAdapter(dict[str, object]) + + +class _CountedGuardrail(CustomGuardrail): + checked: tuple[str, ...] = () + + +class ApplyGuardrailG(_CountedGuardrail): + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict[str, object], + input_type: Literal["request", "response"], + logging_obj: object = None, + ) -> GenericGuardrailAPIInputs: + self.checked = (*self.checked, " ".join(inputs.get("texts") or ())) + return inputs + + +def _reply_text(response: LLMResponseTypes) -> str: + match response: + case ModelResponse(choices=[Choices(message=Message(content=str() as text))]): + return text + case _: + return str(response) + + +class OwnHookG(_CountedGuardrail): + async def async_post_call_success_hook( + self, data: dict[str, object], user_api_key_dict: UserAPIKeyAuth, response: LLMResponseTypes + ) -> None: + self.checked = (*self.checked, _reply_text(response)) + + +class InheritedApplyGuardrailG(ApplyGuardrailG): + pass + + +class MaskingG(ApplyGuardrailG): + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict[str, object], + input_type: Literal["request", "response"], + logging_obj: object = None, + ) -> GenericGuardrailAPIInputs: + checked: Final = await super().apply_guardrail(inputs, request_data, input_type, logging_obj) + return {**checked, "texts": [MASKED for _ in checked.get("texts") or ()]} + + +class BlockingG(ApplyGuardrailG): + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict[str, object], + input_type: Literal["request", "response"], + logging_obj: object = None, + ) -> GenericGuardrailAPIInputs: + await super().apply_guardrail(inputs, request_data, input_type, logging_obj) + raise HTTPException(status_code=400, detail="blocked by G") + + +class RewritingGuardrail(CustomGuardrail): + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict[str, object], + input_type: Literal["request", "response"], + logging_obj: object = None, + ) -> GenericGuardrailAPIInputs: + return {**inputs, "texts": [REWRITTEN for _ in inputs.get("texts") or ()]} + + +class ToolCallBlockingGuardrail(CustomGuardrail): + async def apply_guardrail( + self, + inputs: GenericGuardrailAPIInputs, + request_data: dict[str, object], + input_type: Literal["request", "response"], + logging_obj: object = None, + ) -> GenericGuardrailAPIInputs: + if input_type == "response" and inputs.get("tool_calls"): + raise HTTPException(status_code=400, detail="tool calls are not allowed") + return inputs + + +def _guardrail_g( + style: type[_CountedGuardrail] = ApplyGuardrailG, default_on: bool = False, run_in_parallel: bool = False +) -> _CountedGuardrail: + return style( + guardrail_name="G", + event_hook=GuardrailEventHooks.post_call, + default_on=default_on, + run_in_parallel=run_in_parallel, + ) + + +def _known_gap(action_item: str) -> pytest.MarkDecorator: + return pytest.mark.xfail( + raises=AssertionError, + strict=True, + reason=f"broken before #40154, out of scope for LIT-10481, tracked as RCA {action_item}", + ) + + +def _deployment(model_name: str, guardrails: tuple[str, ...], api_base: str) -> dict[str, object]: + return { + "model_name": model_name, + "litellm_params": { + "model": "openai/gpt-4o", + "api_key": "sk-test", + "api_base": api_base, + **({"guardrails": list(guardrails)} if guardrails else {}), + }, + } + + +def _router(primary: tuple[str, ...], fallback: tuple[str, ...] | None = None, cached: bool = False) -> Router: + if fallback is None: + return Router(model_list=[_deployment("gpt", primary, _OPENAI_API_BASE)], cache_responses=cached) + return Router( + model_list=[ + _deployment("gpt", primary, _FAILING_API_BASE), + _deployment("gpt-backup", fallback, _OPENAI_API_BASE), + ], + fallbacks=[{"gpt": ["gpt-backup"]}], + num_retries=0, + ) + + +def _json_or_sse(json_reply: Mapping[str, object], sse_reply: str) -> Callable[[httpx.Request], httpx.Response]: + def reply(request: httpx.Request) -> httpx.Response: + if _JSON_OBJECT.validate_json(request.content).get("stream"): + return httpx.Response(200, headers={"content-type": "text/event-stream"}, content=sse_reply.encode()) + return httpx.Response(200, json=dict(json_reply)) + + return reply + + +async def _drain(stream: AsyncIterable[object]) -> str: + return "".join([str(chunk) async for chunk in stream]) + + +@pytest.fixture(autouse=True) +def provider(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch) -> Iterator[respx.MockRouter]: + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + litellm.in_memory_llm_clients_cache.flush_cache() + ProxyLogging._callback_capabilities_cache.clear() # pyright: ignore[reportPrivateUsage] # keyed on id(), which a collected guardrail from an earlier test can reuse + for endpoint in ("chat/completions", "responses"): + respx_mock.post(f"{_FAILING_API_BASE}/{endpoint}").mock( + return_value=httpx.Response(500, json={"error": {"message": "primary down"}}) + ) + respx_mock.post(f"{_OPENAI_API_BASE}/chat/completions").mock(side_effect=_json_or_sse(_CHAT_REPLY, _CHAT_SSE)) + respx_mock.post(f"{_OPENAI_API_BASE}/responses").mock(side_effect=_json_or_sse(_RESPONSES_REPLY, _RESPONSES_SSE)) + yield respx_mock + litellm.in_memory_llm_clients_cache.flush_cache() + + +@dataclass(frozen=True, slots=True) +class ProxyCase: + route: Route = "acompletion" + stream: bool = False + primary: tuple[str, ...] = ("G",) + fallback: tuple[str, ...] | None = None + request: tuple[str, ...] = () + default_on: bool = False + style: type[_CountedGuardrail] = ApplyGuardrailG + cached: bool = False + client_sees: str = REPLY + checks: tuple[str, ...] = (REPLY,) + + +PROXY_CASES: Final = ( + pytest.param(ProxyCase(), id="A-chat"), + pytest.param(ProxyCase(route="aresponses"), id="A-responses"), + pytest.param(ProxyCase(route="anthropic_messages"), id="A-messages"), + pytest.param(ProxyCase(request=("G",)), id="A-chat-model-and-request"), + pytest.param(ProxyCase(style=MaskingG, client_sees=MASKED), id="A-chat-g-masks-its-own-output"), + pytest.param(ProxyCase(stream=True), id="B-chat-stream"), + pytest.param(ProxyCase(route="aresponses", stream=True), id="B-responses-stream"), + pytest.param(ProxyCase(route="anthropic_messages", stream=True), id="B-messages-stream"), + pytest.param(ProxyCase(primary=(), fallback=("G",)), id="E-only-on-fallback"), + pytest.param( + ProxyCase(request=("rewriter",), client_sees=REWRITTEN, checks=(REPLY, REWRITTEN)), + id="G-rewritten-after-check", + ), + pytest.param(ProxyCase(fallback=("G",)), id="H-on-both-deployments"), + pytest.param(ProxyCase(primary=(), request=("G",)), id="I-request-chat"), + pytest.param(ProxyCase(route="aresponses", primary=(), request=("G",)), id="I-request-responses"), + pytest.param(ProxyCase(style=OwnHookG), id="J-A-chat-own-hook", marks=_known_gap("AI-5")), + pytest.param(ProxyCase(primary=(), fallback=("G",), style=OwnHookG), id="E-only-on-fallback-own-hook"), + pytest.param(ProxyCase(primary=(), request=("G",), style=OwnHookG), id="I-request-chat-own-hook"), + pytest.param( + ProxyCase(primary=(), request=("G",), style=InheritedApplyGuardrailG), + id="K-I-request-chat-inherited-apply-guardrail", + marks=_known_gap("AI-2"), + ), + pytest.param(ProxyCase(primary=("other",), default_on=True), id="L-default-on"), +) + +_ROUTE_PATHS: Final[Mapping[Route, str]] = MappingProxyType( + { + "acompletion": "/v1/chat/completions", + "aresponses": "/v1/responses", + "anthropic_messages": "/v1/messages", + } +) +_ROUTE_BODIES: Final[Mapping[Route, Mapping[str, object]]] = MappingProxyType( + { + "acompletion": {"messages": [{"role": "user", "content": "hello"}]}, + "aresponses": {"input": "hello"}, + "anthropic_messages": {"messages": [{"role": "user", "content": "hello"}], "max_tokens": 32}, + } +) + + +async def _proxy_reply(case: ProxyCase) -> str: + from litellm.proxy.proxy_server import ProxyConfig, select_data_generator + + path: Final = _ROUTE_PATHS[case.route] + router: Final = _router(case.primary, case.fallback, case.cached) + response: Final = await ProxyBaseLLMRequestProcessing( + data={ + "model": "gpt", + **_ROUTE_BODIES[case.route], + **({"stream": True} if case.stream else {}), + **({"guardrails": list(case.request)} if case.request else {}), + } + ).base_process_llm_request( + request=Request({"type": "http", "method": "POST", "path": path, "headers": [], "query_string": b""}), + fastapi_response=Response(), + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", request_route=path), + route_type=case.route, + proxy_logging_obj=ProxyLogging(user_api_key_cache=UserApiKeyCache()), + general_settings={}, + proxy_config=ProxyConfig(), + select_data_generator=select_data_generator, + llm_router=router, + is_streaming_request=case.stream, + ) + text: Final = ( + await _drain(response.body_iterator) + if isinstance(response, StreamingResponse) + else json.dumps(response.model_dump() if isinstance(response, BaseModel) else response) + ) + await asyncio.sleep(0) + return text + + +@pytest.mark.parametrize("case", PROXY_CASES) +async def test_a_proxy_request_checks_each_distinct_response_once( + monkeypatch: pytest.MonkeyPatch, case: ProxyCase +) -> None: + guardrail: Final = _guardrail_g(case.style, default_on=case.default_on) + rewriter: Final = RewritingGuardrail(guardrail_name="rewriter", event_hook=GuardrailEventHooks.post_call) + monkeypatch.setattr(litellm, "callbacks", [rewriter, guardrail]) + + reply: Final = await _proxy_reply(case) + + assert case.client_sees in reply + assert sorted(guardrail.checked) == sorted(case.checks) + + +@pytest.mark.parametrize("route", ["acompletion", "aresponses"]) +async def test_a_cache_hit_is_checked_like_a_fresh_response( + monkeypatch: pytest.MonkeyPatch, provider: respx.MockRouter, route: Route +) -> None: + guardrail: Final = _guardrail_g() + monkeypatch.setattr(litellm, "callbacks", [guardrail]) + monkeypatch.setattr(litellm, "cache", Cache(type="local")) + + case: Final = ProxyCase(route=route, cached=True) + replies: Final = (await _proxy_reply(case), await _proxy_reply(case)) + + assert provider.calls.call_count == 1, "the second request must be served from the cache" + assert all(REPLY in reply for reply in replies) + assert guardrail.checked == (REPLY, REPLY) + + +async def test_a_blocking_verdict_blocks_the_proxy_response_after_one_check(monkeypatch: pytest.MonkeyPatch) -> None: + guardrail: Final = _guardrail_g(BlockingG) + monkeypatch.setattr(litellm, "callbacks", [guardrail]) + + with pytest.raises(HTTPException, check=lambda e: e.detail == "blocked by G"): + await _proxy_reply(ProxyCase()) + + assert guardrail.checked == (REPLY,) + + +SdkCaller = Literal["sdk-call", "sdk-router"] + + +@dataclass(frozen=True, slots=True) +class SdkCase: + caller: SdkCaller = "sdk-call" + stream: bool = False + style: type[_CountedGuardrail] = ApplyGuardrailG + + +SDK_CASES: Final = ( + pytest.param(SdkCase(), id="C-sdk-call"), + pytest.param(SdkCase(caller="sdk-router"), id="C-sdk-router"), + pytest.param(SdkCase(style=OwnHookG), id="C-sdk-call-own-hook"), + pytest.param(SdkCase(stream=True), id="D-sdk-call-stream", marks=_known_gap("AI-4")), +) + + +async def _sdk_reply(case: SdkCase) -> str: + messages: Final = [{"role": "user", "content": "hello"}] + response: Final = await ( + _router(("G",)).acompletion(model="gpt", messages=messages, stream=case.stream) + if case.caller == "sdk-router" + else litellm.acompletion( + model="openai/gpt-4o", api_key="sk-test", guardrails=["G"], messages=messages, stream=case.stream + ) + ) + text: Final = await _drain(response) if isinstance(response, AsyncIterable) else str(response) + await asyncio.sleep(0) + return text + + +@pytest.mark.parametrize("case", SDK_CASES) +async def test_an_sdk_call_checks_its_response_once(monkeypatch: pytest.MonkeyPatch, case: SdkCase) -> None: + guardrail: Final = _guardrail_g(case.style) + monkeypatch.setattr(litellm, "callbacks", [guardrail]) + + reply: Final = await _sdk_reply(case) + + assert REPLY in reply + assert guardrail.checked == (REPLY,) + + +async def test_mcp_auto_execute_checks_the_tool_call_round_before_running_its_tools( + monkeypatch: pytest.MonkeyPatch, provider: respx.MockRouter +) -> None: + monkeypatch.setattr( + litellm, "callbacks", [ToolCallBlockingGuardrail(guardrail_name="G", event_hook=GuardrailEventHooks.post_call)] + ) + provider.post(f"{_MCP_API_BASE}/responses").mock( + side_effect=[httpx.Response(200, json=_TOOL_CALL_ROUND), httpx.Response(200, json=_RESPONSES_REPLY)] + ) + echo_tool: Final = MCPTool.model_validate( + {"name": "echo", "inputSchema": {"type": "object", "properties": {"value": {"type": "string"}}}}, + by_name=False, + ) + run_tools: Final = AsyncMock(return_value=[{"tool_call_id": "call_echo", "result": "PING", "name": "echo"}]) + + with ( + patch.object( + LiteLLM_Proxy_MCP_Handler, + "process_mcp_tools_without_openai_transform", + AsyncMock(return_value=([echo_tool], {"echo": "demo"})), + ), + patch.object(LiteLLM_Proxy_MCP_Handler, "execute_tool_calls", run_tools), + pytest.raises(HTTPException, check=lambda e: e.detail == "tool calls are not allowed"), + ): + await litellm.aresponses( + model="openai/gpt-4o", + input="Call echo with PING", + tools=[{"type": "mcp", "server_url": "litellm_proxy/mcp/demo", "require_approval": "never"}], + api_key="sk-test", + api_base=_MCP_API_BASE, + guardrails=["G"], + proxy_server_request={"url": "http://localhost:4000/v1/responses", "method": "POST"}, + litellm_metadata={"user_api_key_request_route": "/v1/responses"}, + ) + + run_tools.assert_not_awaited() + + +def _same_response(reply: ModelResponse) -> ModelResponse: + return reply + + +def _model_renamed(reply: ModelResponse) -> ModelResponse: + return reply.model_copy(update={"model": "public-alias"}) + + +def _rewritten(_: ModelResponse) -> ModelResponse: + return ModelResponse(choices=[{"message": {"role": "assistant", "content": REWRITTEN}}]) + + +PROXY_LOOP_CASES: Final = ( + pytest.param(_same_response, (REPLY,), id="A-same-response"), + pytest.param(_model_renamed, (REPLY,), id="A-model-renamed"), + pytest.param(_rewritten, (REPLY, REWRITTEN), id="G-rewritten"), +) + + +@pytest.mark.parametrize("parallel", [False, True], ids=["sequential", "parallel"]) +@pytest.mark.parametrize(("proxy_sees", "checks"), PROXY_LOOP_CASES) +async def test_each_proxy_post_call_loop_skips_only_output_the_deployment_checked( + monkeypatch: pytest.MonkeyPatch, + parallel: bool, + proxy_sees: Callable[[ModelResponse], ModelResponse], + checks: tuple[str, ...], +) -> None: + guardrail: Final = _guardrail_g(run_in_parallel=parallel) + monkeypatch.setattr(litellm, "callbacks", [guardrail]) + data: Final = { + "model": "gpt", + "proxy_server_request": {"url": "http://localhost:4000/v1/chat/completions"}, + "metadata": {"guardrails": ["G"]}, + } + reply: Final = ModelResponse(model="provider-model", choices=[{"message": {"role": "assistant", "content": REPLY}}]) + + await guardrail.async_post_call_success_deployment_hook( + request_data={**data, "guardrails": ["G"]}, response=reply, call_type=CallTypes.acompletion + ) + await ProxyLogging(user_api_key_cache=UserApiKeyCache()).post_call_success_hook( + data=data, response=proxy_sees(reply), user_api_key_dict=UserAPIKeyAuth() + ) + + assert sorted(guardrail.checked) == sorted(checks) + + +def _ocr_reply(markdown: str) -> OCRResponse: + return OCRResponse.model_validate( + {"pages": [{"index": 0, "markdown": markdown, "images": [], "dimensions": None}], "model": "mistral-ocr-latest"} + ) + + +@pytest.mark.parametrize( + ("proxy_sees", "checks"), + [ + pytest.param(REPLY, (REPLY,), id="A-ocr-same-pages"), + pytest.param(REWRITTEN, (REPLY, REWRITTEN), id="G-ocr-rewritten-pages"), + ], +) +async def test_an_ocr_response_is_skipped_only_when_its_pages_were_checked( + monkeypatch: pytest.MonkeyPatch, proxy_sees: str, checks: tuple[str, ...] +) -> None: + guardrail: Final = _guardrail_g() + monkeypatch.setattr(litellm, "callbacks", [guardrail]) + data: Final = { + "model": "ocr", + "document": {"type": "document_url", "document_url": "https://example.com/doc.pdf"}, + "proxy_server_request": {"url": "http://localhost:4000/v1/ocr"}, + "metadata": {"guardrails": ["G"], "user_api_key_request_route": "/v1/ocr"}, + } + + await guardrail.async_post_call_success_deployment_hook( + request_data={**data, "guardrails": ["G"]}, response=_ocr_reply(REPLY), call_type=CallTypes.aocr + ) + await ProxyLogging(user_api_key_cache=UserApiKeyCache()).post_call_success_hook( + data=data, response=_ocr_reply(proxy_sees), user_api_key_dict=UserAPIKeyAuth(request_route="/v1/ocr") + ) + + assert sorted(guardrail.checked) == sorted(checks) diff --git a/tests/unit/proxy/utils/proxy_logging/test_guardrail_pipeline.py b/tests/unit/proxy/utils/proxy_logging/test_guardrail_pipeline.py index fb3060b42a9..8d106531218 100644 --- a/tests/unit/proxy/utils/proxy_logging/test_guardrail_pipeline.py +++ b/tests/unit/proxy/utils/proxy_logging/test_guardrail_pipeline.py @@ -696,6 +696,7 @@ def _moderation_guardrail() -> MagicMock: cb.should_run_guardrail = MagicMock(return_value=True) cb.async_moderation_hook = AsyncMock(return_value=None) cb.async_post_call_success_hook = AsyncMock(return_value=None) + cb.post_call_already_checked = MagicMock(return_value=False) cb.run_in_parallel = False return cb From 47931a3bf2321a1e9015803d5f988da2a2f2d40c Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 7 Nov 2026 07:57:20 +0900 Subject: [PATCH 2/3] chore(deps): lock filelock 4.2.4 on 3.11+ and 3.20.3 on 3.10, as main does Widens the filelock range to <5 and adds main's two filelock constraints, matching main's pyproject.toml since #49204 --- pyproject.toml | 4 +- uv.lock | 432 ++++++++++++++++++++++++++----------------------- 2 files changed, 230 insertions(+), 206 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fca9ea6c881..ca25b5bc48e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ # When changing a floor, verify it installs + imports on every supported # Python with: `uv pip install --resolution=lowest-direct .` "fastuuid>=0.14.0,<1.0", - "filelock>=3.16.1,<4.0", + "filelock>=3.16.1,<5", "httpx[http2]>=0.28.0,<1.0", "openai>=2.20.0,<4.0.0", "python-dotenv>=1.0.0,<2.0", @@ -358,6 +358,8 @@ constraint-dependencies = [ "httplib2>=0.32.0", "setuptools>=84.1.0", "dnspython>=2.9.0; python_version >= '3.11'", + "filelock<3.20.4; python_version < '3.11'", + "filelock>=4.2.1; python_version >= '3.11'", ] override-dependencies = [ # a2a-sdk 1.x requires packaging>=24.0; lunary 1.4.x still caps at <24.0. diff --git a/uv.lock b/uv.lock index 0560d07f501..829abd54c5d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.10, <3.15" resolution-markers = [ "python_full_version >= '3.14'", @@ -10,17 +10,17 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-11-03T22:57:01.748805Z" exclude-newer-span = "P3D" [options.exclude-newer-package] -boto3 = "2026-10-21T19:31:00Z" -botocore = "2026-10-21T19:31:00Z" +mlflow-skinny = "2026-10-21T03:00:00Z" brotli = "2026-10-22T14:01:00Z" certifi = "2026-10-20T03:00:00Z" grpcio = "2026-10-22T15:00:00Z" +boto3 = "2026-10-21T19:31:00Z" +botocore = "2026-10-21T19:31:00Z" mlflow = "2026-10-21T03:00:00Z" -mlflow-skinny = "2026-10-21T03:00:00Z" mlflow-tracing = "2026-10-21T03:00:00Z" pypdf = "2026-10-28T17:01:00Z" @@ -33,6 +33,8 @@ members = [ constraints = [ { name = "aiohttp", specifier = ">=3.14.2,<4.0" }, { name = "dnspython", marker = "python_full_version >= '3.11'", specifier = ">=2.9.0" }, + { name = "filelock", marker = "python_full_version < '3.11'", specifier = "<3.20.4" }, + { name = "filelock", marker = "python_full_version >= '3.11'", specifier = ">=4.2.1" }, { name = "httplib2", specifier = ">=0.32.0" }, { name = "packaging", specifier = ">=24.0" }, { name = "setuptools", specifier = ">=84.1.0" }, @@ -239,9 +241,9 @@ name = "aiologic" version = "0.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a7/809482759f40079f4c4328c7318bf569ae25d457f5017aad30a1b9aafedc/aiologic-0.17.0.tar.gz", hash = "sha256:65aa058e858c94cd208badb188e7f00b54dcabb3ba85b34f794db98074d108b9", size = 251625, upload-time = "2026-06-14T12:24:35.367Z" } wheels = [ @@ -533,14 +535,14 @@ name = "aurelio-sdk" version = "0.0.19" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiofiles" }, - { name = "aiohttp" }, - { name = "colorlog" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "tornado", version = "6.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.14'" }, + { name = "aiofiles", marker = "python_full_version < '3.14'" }, + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "colorlog", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "python-dotenv", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "requests-toolbelt", marker = "python_full_version < '3.14'" }, + { name = "tornado", version = "6.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "tornado", version = "6.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/0e/c2e369ad173fb3d76448e46d10beb3dcc53388318933ddf8169a3f21a810/aurelio_sdk-0.0.19.tar.gz", hash = "sha256:14107e7440ff2efd0b4a08c52fb595e7680bd4bc973a0ddfb3b64157c6666b91", size = 15258, upload-time = "2025-03-24T14:37:32.203Z" } @@ -553,9 +555,9 @@ name = "aws-sdk-bedrock-runtime" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smithy-aws-core", extra = ["eventstream", "json"] }, - { name = "smithy-core" }, - { name = "smithy-http", extra = ["aiohttp"] }, + { name = "smithy-aws-core", extra = ["eventstream", "json"], marker = "python_full_version >= '3.12'" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, + { name = "smithy-http", extra = ["aiohttp"], marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/b3/9c225cbfe9f17ea2e3d75a0fdd0b325ef79839b9c09a376bda63a7bf3bb3/aws_sdk_bedrock_runtime-0.11.0.tar.gz", hash = "sha256:f2c45d34625bf6a7b56375e29a53a16b376880bda771e4bbf7d84491622eb193", size = 173854, upload-time = "2026-08-24T21:17:16.304Z" } wheels = [ @@ -564,7 +566,7 @@ wheels = [ [package.optional-dependencies] awscrt = [ - { name = "smithy-http", extra = ["awscrt"] }, + { name = "smithy-http", extra = ["awscrt"], marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -1247,7 +1249,7 @@ name = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ @@ -1271,7 +1273,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } wheels = [ @@ -1344,7 +1346,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } @@ -1614,8 +1616,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -1898,7 +1900,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -2058,11 +2060,29 @@ wheels = [ [[package]] name = "filelock" -version = "3.32.7" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/59/e19834834cb01a32febfbb0f8a23a9088088f5d45991824ff2bc3b5e8acb/filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7", size = 225154, upload-time = "2026-09-16T00:24:20.907Z" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/df/31098c5aeb4d966b553641472bd55fcf5fdfac953549894b8a765ba44e91/filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52", size = 100157, upload-time = "2026-09-16T00:24:19.543Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "filelock" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, ] [[package]] @@ -2481,11 +2501,11 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version >= '3.14'" }, + { name = "proto-plus", marker = "python_full_version >= '3.14'" }, + { name = "protobuf", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266, upload-time = "2025-10-03T00:07:34.778Z" } wheels = [ @@ -2494,8 +2514,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, + { name = "grpcio", marker = "python_full_version >= '3.14'" }, + { name = "grpcio-status", marker = "python_full_version >= '3.14'" }, ] [[package]] @@ -2509,11 +2529,11 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "googleapis-common-protos", marker = "python_full_version < '3.14'" }, + { name = "proto-plus", marker = "python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } wheels = [ @@ -2522,8 +2542,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, - { name = "grpcio-status" }, + { name = "grpcio", marker = "python_full_version < '3.14'" }, + { name = "grpcio-status", marker = "python_full_version < '3.14'" }, ] [[package]] @@ -2709,12 +2729,12 @@ resolution-markers = [ "python_full_version >= '3.14'", ] dependencies = [ - { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" } }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, + { name = "google-api-core", version = "2.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "google-auth", marker = "python_full_version >= '3.14'" }, + { name = "google-cloud-core", marker = "python_full_version >= '3.14'" }, + { name = "google-crc32c", marker = "python_full_version >= '3.14'" }, + { name = "google-resumable-media", marker = "python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/ef/7cefdca67a6c8b3af0ec38612f9e78e5a9f6179dd91352772ae1a9849246/google_cloud_storage-3.4.1.tar.gz", hash = "sha256:6f041a297e23a4b485fad8c305a7a6e6831855c208bcbe74d00332a909f82268", size = 17238203, upload-time = "2025-10-08T18:43:39.665Z" } wheels = [ @@ -2732,12 +2752,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" } }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, + { name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "google-auth", marker = "python_full_version < '3.14'" }, + { name = "google-cloud-core", marker = "python_full_version < '3.14'" }, + { name = "google-crc32c", marker = "python_full_version < '3.14'" }, + { name = "google-resumable-media", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } wheels = [ @@ -3446,7 +3466,8 @@ version = "1.32.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "filelock" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "filelock", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "fsspec" }, { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "httpx" }, @@ -4199,13 +4220,13 @@ name = "langchain-classic" version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, - { name = "langchain-text-splitters" }, - { name = "langsmith" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, + { name = "langchain-text-splitters", marker = "python_full_version >= '3.11'" }, + { name = "langsmith", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9b/78/84b5065816f348c39fefa4316f209f0135e8410216340a953bec17d9e4e4/langchain_classic-1.0.7.tar.gz", hash = "sha256:debbec8065e69b95108d2652e8d5c44f4516e19aa8d716c02ed2211c3aee099d", size = 10554118, upload-time = "2026-05-07T15:46:56.8Z" } wheels = [ @@ -4220,18 +4241,18 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "aiohttp" }, - { name = "dataclasses-json" }, - { name = "httpx-sse" }, - { name = "langchain" }, - { name = "langchain-core" }, - { name = "langsmith" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "pydantic-settings" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, - { name = "tenacity" }, + { name = "aiohttp", marker = "python_full_version < '3.11'" }, + { name = "dataclasses-json", marker = "python_full_version < '3.11'" }, + { name = "httpx-sse", marker = "python_full_version < '3.11'" }, + { name = "langchain", marker = "python_full_version < '3.11'" }, + { name = "langchain-core", marker = "python_full_version < '3.11'" }, + { name = "langsmith", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pydantic-settings", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version < '3.11'" }, + { name = "tenacity", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/49/2ff5354273809e9811392bc24bcffda545a196070666aef27bc6aacf1c21/langchain_community-0.3.31.tar.gz", hash = "sha256:250e4c1041539130f6d6ac6f9386cb018354eafccd917b01a4cff1950b80fd81", size = 33241237, upload-time = "2025-10-07T20:17:57.857Z" } wheels = [ @@ -4249,19 +4270,19 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "aiohttp" }, - { name = "dataclasses-json" }, - { name = "httpx-sse" }, - { name = "langchain-classic" }, - { name = "langchain-core" }, - { name = "langsmith" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "aiohttp", marker = "python_full_version >= '3.11'" }, + { name = "dataclasses-json", marker = "python_full_version >= '3.11'" }, + { name = "httpx-sse", marker = "python_full_version >= '3.11'" }, + { name = "langchain-classic", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, + { name = "langsmith", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pydantic-settings" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "sqlalchemy" }, - { name = "tenacity" }, + { name = "pydantic-settings", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "sqlalchemy", marker = "python_full_version >= '3.11'" }, + { name = "tenacity", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/97/a03585d42b9bdb6fbd935282d6e3348b10322a24e6ce12d0c99eb461d9af/langchain_community-0.4.1.tar.gz", hash = "sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85", size = 33241144, upload-time = "2025-10-27T15:20:32.504Z" } wheels = [ @@ -4319,7 +4340,7 @@ name = "langchain-text-splitters" version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } wheels = [ @@ -4581,7 +4602,8 @@ dependencies = [ { name = "certifi" }, { name = "click" }, { name = "fastuuid" }, - { name = "filelock" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "filelock", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "httpx", extra = ["http2"] }, { name = "huggingface-hub" }, { name = "importlib-metadata" }, @@ -4855,7 +4877,7 @@ requires-dist = [ { name = "fastapi", marker = "extra == 'proxy'", specifier = ">=0.136.3,<1.0" }, { name = "fastapi-sso", marker = "extra == 'proxy'", specifier = ">=0.19.0,<1.0" }, { name = "fastuuid", specifier = ">=0.14.0,<1.0" }, - { name = "filelock", specifier = ">=3.16.1,<4.0" }, + { name = "filelock", specifier = ">=3.16.1,<5" }, { name = "google-cloud-aiplatform", marker = "extra == 'google'", specifier = ">=1.133.0,<2.0" }, { name = "google-cloud-aiplatform", marker = "extra == 'proxy-runtime'", specifier = ">=1.133.0,<2.0" }, { name = "google-cloud-iam", marker = "extra == 'extra-proxy'", specifier = ">=2.19.1,<3.0" }, @@ -5051,10 +5073,10 @@ name = "litellm-admin-mcp" version = "0.1.0" source = { url = "https://github.com/BerriAI/liteadmin-mcp/archive/d35ec9c19c117d4c50cc1eccf6ce1296aac25a1a.tar.gz" } dependencies = [ - { name = "httpx2" }, - { name = "jsonschema" }, - { name = "mcp" }, - { name = "uvicorn" }, + { name = "httpx2", marker = "python_full_version >= '3.12'" }, + { name = "jsonschema", marker = "python_full_version >= '3.12'" }, + { name = "mcp", marker = "python_full_version >= '3.12'" }, + { name = "uvicorn", marker = "python_full_version >= '3.12'" }, ] sdist = { hash = "sha256:86b930f6706fb2da10d53d1798ee7fd14e44fb0afdef0da122cf0e6bb88fa2c3" } @@ -5160,16 +5182,16 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "aiohttp" }, - { name = "chevron" }, - { name = "jsonpickle" }, - { name = "langchain-community", version = "0.3.31", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyhumps" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "tenacity" }, + { name = "aiohttp", marker = "python_full_version < '3.11'" }, + { name = "chevron", marker = "python_full_version < '3.11'" }, + { name = "jsonpickle", marker = "python_full_version < '3.11'" }, + { name = "langchain-community", version = "0.3.31", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "pyhumps", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "setuptools", marker = "python_full_version < '3.11'" }, + { name = "tenacity", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/6f/9ca1acf766848aaf5f0ac4140c34c91ad0dbfad2654359699644be3352c9/lunary-1.4.36.tar.gz", hash = "sha256:53f002f385c83d9c0e6368e7999923acffbde987f53c5205c2c249c38ee2d75c", size = 20253, upload-time = "2026-02-09T20:49:30.56Z" } wheels = [ @@ -5187,16 +5209,16 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "aiohttp" }, - { name = "chevron" }, - { name = "jsonpickle" }, - { name = "langchain-community", version = "0.4.1", source = { registry = "https://pypi.org/simple" } }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyhumps" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "tenacity" }, + { name = "aiohttp", marker = "python_full_version >= '3.11'" }, + { name = "chevron", marker = "python_full_version >= '3.11'" }, + { name = "jsonpickle", marker = "python_full_version >= '3.11'" }, + { name = "langchain-community", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "pyhumps", marker = "python_full_version >= '3.11'" }, + { name = "requests", marker = "python_full_version >= '3.11'" }, + { name = "setuptools", marker = "python_full_version >= '3.11'" }, + { name = "tenacity", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/37/ef/1acbc6957585cc0110e648d787663871717ced3df27fcd3cb5e18fa418f3/lunary-1.4.37.tar.gz", hash = "sha256:1781091e9dceffcc28ebc4be7e085c9fec4102d98d7ca945ed0021e9ce03c36f", size = 20248, upload-time = "2026-02-12T08:15:02.091Z" } wheels = [ @@ -9091,10 +9113,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } wheels = [ @@ -9141,11 +9163,11 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "joblib" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } }, - { name = "threadpoolctl" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } wheels = [ @@ -9195,7 +9217,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -9257,7 +9279,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } @@ -9342,21 +9364,21 @@ name = "semantic-router" version = "0.1.15" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, - { name = "aurelio-sdk" }, - { name = "colorama" }, - { name = "colorlog" }, - { name = "litellm" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "aiohttp", marker = "python_full_version < '3.14'" }, + { name = "aurelio-sdk", marker = "python_full_version < '3.14'" }, + { name = "colorama", marker = "python_full_version < '3.14'" }, + { name = "colorlog", marker = "python_full_version < '3.14'" }, + { name = "litellm", marker = "python_full_version < '3.14'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "tiktoken" }, - { name = "tornado", version = "6.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or python_full_version >= '3.14'" }, + { name = "openai", marker = "python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "regex", marker = "python_full_version < '3.14'" }, + { name = "tiktoken", marker = "python_full_version < '3.14'" }, + { name = "tornado", version = "6.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "tornado", version = "6.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, - { name = "urllib3" }, + { name = "urllib3", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/a9/1a689e916e8b280f1fd8fb335cc059be626a22fe4533baa045d32fcd6de5/semantic_router-0.1.15.tar.gz", hash = "sha256:328256ddc3c2b713101ec69561d6585aecbf1198ea3461e1486289d8c3a35288", size = 95605, upload-time = "2026-05-23T12:58:15.444Z" } wheels = [ @@ -9430,9 +9452,9 @@ name = "smithy-aws-core" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aws-sdk-signers" }, - { name = "smithy-core" }, - { name = "smithy-http" }, + { name = "aws-sdk-signers", marker = "python_full_version >= '3.12'" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, + { name = "smithy-http", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/d3/501c0023548173416109ac42298ca33b708469dc922005770811a597949f/smithy_aws_core-0.11.0.tar.gz", hash = "sha256:29ee89976a520a87e3db557e03e115fdc21a0a60b81161e95174395a1b064da1", size = 38791, upload-time = "2026-08-24T21:16:59.631Z" } wheels = [ @@ -9441,10 +9463,10 @@ wheels = [ [package.optional-dependencies] eventstream = [ - { name = "smithy-aws-event-stream" }, + { name = "smithy-aws-event-stream", marker = "python_full_version >= '3.12'" }, ] json = [ - { name = "smithy-json" }, + { name = "smithy-json", marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -9452,7 +9474,7 @@ name = "smithy-aws-event-stream" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smithy-core" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/0e/6efb3a4ed92c0f1ada6de060ac92e7115a1e34d0ab1fb99a6056734a88ea/smithy_aws_event_stream-0.3.0.tar.gz", hash = "sha256:a0e227367a973144e205a075d0a424f95c92f26656a1018d08900da2ae547c49", size = 12818, upload-time = "2026-05-05T18:04:14.317Z" } wheels = [ @@ -9473,7 +9495,7 @@ name = "smithy-http" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smithy-core" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/78/b5f3113d6c8f0bc1f9777a7f5ca84b892d29efac05850e14f7d4f7e645b5/smithy_http-0.5.0.tar.gz", hash = "sha256:bb4a19672f7c7eeb872a308f777eb505281a5bafb1ee3d1ea9c760c06c352510", size = 31122, upload-time = "2026-08-24T21:16:56.488Z" } wheels = [ @@ -9482,11 +9504,11 @@ wheels = [ [package.optional-dependencies] aiohttp = [ - { name = "aiohttp" }, - { name = "yarl" }, + { name = "aiohttp", marker = "python_full_version >= '3.12'" }, + { name = "yarl", marker = "python_full_version >= '3.12'" }, ] awscrt = [ - { name = "awscrt" }, + { name = "awscrt", marker = "python_full_version >= '3.12'" }, ] [[package]] @@ -9494,8 +9516,8 @@ name = "smithy-json" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ijson" }, - { name = "smithy-core" }, + { name = "ijson", marker = "python_full_version >= '3.12'" }, + { name = "smithy-core", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/04164eefb3da7479f52f6535b4b39cc8384c292cb2bb74279f2acc4f4b4d/smithy_json-0.3.0.tar.gz", hash = "sha256:c81c7034587e01bc64767cbbecb05a7d65ca9070612fd94e8a03e80540290a22", size = 7956, upload-time = "2026-08-20T17:55:32.177Z" } wheels = [ @@ -9573,23 +9595,23 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, - { name = "tomli" }, + { name = "alabaster", marker = "python_full_version < '3.11'" }, + { name = "babel", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imagesize", marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "requests", marker = "python_full_version < '3.11'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -9604,23 +9626,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -9637,23 +9659,23 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -9801,8 +9823,8 @@ name = "standard-aifc" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts" }, - { name = "standard-chunk" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } wheels = [ @@ -9823,7 +9845,7 @@ name = "standard-sunau" version = "3.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "audioop-lts" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } wheels = [ @@ -9857,8 +9879,8 @@ name = "taskgroup" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup" }, - { name = "typing-extensions" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" } wheels = [ From 598138fdf052bf8fcf39df6d6391ddf4fcd59995 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sat, 7 Nov 2026 10:23:22 +0900 Subject: [PATCH 3/3] chore(deps): widen litellm-core filelock cap to <5 to match the root manifest --- packaging/litellm-core/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/litellm-core/pyproject.toml b/packaging/litellm-core/pyproject.toml index 0914deb074d..84d428368b7 100644 --- a/packaging/litellm-core/pyproject.toml +++ b/packaging/litellm-core/pyproject.toml @@ -11,7 +11,7 @@ authors = [ ] dependencies = [ "fastuuid>=0.14.0,<1.0", - "filelock>=3.16.1,<4.0", + "filelock>=3.16.1,<5", "httpx[http2]>=0.28.0,<1.0", "openai>=2.20.0,<4.0.0", "python-dateutil>=2.8.2,<3.0",