From 185dc2dca6cc090e32b8b69673018fdbc9c9155c Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 7 Nov 2026 01:53:59 -0800 Subject: [PATCH] fix(types): widen untyped request dicts for the allowed_tools refusal on lts/1.108.x The line types the hook loop's kwargs and optional_params as plain dicts, so passing them to the picked refusal helpers added unknown argument errors that main does not have. They are now cast to Mapping[str, object] at the call sites, and the loop settings reader takes a Mapping The new cast import in the agentic loop would grow the banned-API count, so the duplicate function-level cast import in llm_http_handler.py goes, since the module already imports cast --- .../litellm_core_utils/chat_completion_agentic_loop.py | 9 ++++++--- litellm/llms/custom_httpx/llm_http_handler.py | 5 ++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/chat_completion_agentic_loop.py b/litellm/litellm_core_utils/chat_completion_agentic_loop.py index 7abcd172ddc..e9c5dc2fbf8 100644 --- a/litellm/litellm_core_utils/chat_completion_agentic_loop.py +++ b/litellm/litellm_core_utils/chat_completion_agentic_loop.py @@ -4,7 +4,7 @@ import json from collections.abc import Iterable, Mapping from itertools import chain from types import MappingProxyType -from typing import Final +from typing import Final, cast from pydantic import TypeAdapter, ValidationError @@ -85,7 +85,7 @@ def _coerce_int(value: object, default: int) -> int: return int(value) if isinstance(value, (int, str)) else default -def _agentic_loop_settings(kwargs: dict[str, object]) -> tuple[int, int, list[str]]: +def _agentic_loop_settings(kwargs: Mapping[str, object]) -> tuple[int, int, list[str]]: depth: Final = _coerce_int(kwargs.get("_agentic_loop_depth"), 0) configured: Final = validated_max_agentic_loops( kwargs.get("max_agentic_loops"), field="litellm_params.max_agentic_loops" @@ -334,7 +334,10 @@ async def maybe_run_chat_completion_agentic_loop( import litellm callbacks: Final = litellm.callbacks + (getattr(logging_obj, "dynamic_success_callbacks", None) or []) - request_kwargs: Final = with_caller_tool_choice(kwargs, optional_params) + requested_optional_params: Final = cast( # cast-ok: optional_params is a str-keyed dict + "Mapping[str, object]", optional_params + ) + request_kwargs: Final = with_caller_tool_choice(kwargs, requested_optional_params) depth, max_loops, fingerprints = _agentic_loop_settings(request_kwargs) tools: Final = optional_params.get("tools", []) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index bd3b1cafd01..1da1d7b03fa 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -5965,8 +5965,6 @@ class BaseLLMHTTPHandler: else False ) if websearch_converted_stream and isinstance(response, dict): - from typing import cast - from litellm._logging import verbose_logger from litellm.llms.anthropic.pass_through.messages.fake_stream_iterator import ( FakeAnthropicMessagesStreamIterator, @@ -6209,6 +6207,7 @@ class BaseLLMHTTPHandler: callbacks: Final = litellm.callbacks + (logging_obj.dynamic_success_callbacks or []) tools: Final = optional_params.get("tools", []) depth, max_loops, fingerprints = self._get_agentic_loop_settings(kwargs=kwargs) + request_kwargs: Final = cast("Mapping[str, object]", kwargs) # cast-ok: kwargs is a str-keyed dict for callback in callbacks: if not isinstance(callback, CustomLogger): @@ -6243,7 +6242,7 @@ class BaseLLMHTTPHandler: # Safety guards must run OUTSIDE the callback try/except — they are # bounded-loop / cycle-break rails that must propagate to the caller. - refuse_a_call_the_choice_excludes(response, kwargs, callbacks, model) + refuse_a_call_the_choice_excludes(response, request_kwargs, callbacks, model) fingerprint = self._check_agentic_loop_safety( tool_calls=tool_calls, fingerprints=fingerprints,