From cdea9c9bf86b6a2de070849ac8045f7e7a09541e Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 6 Sep 2026 08:05:40 +0000 Subject: [PATCH 01/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-05, 2026-09-06) Drop the TID251 cast import and both cast-ok casts from the refusal message_delta rebuild by narrowing the TypedDict union on its type literal, drop the redundant Mapping cast after the isinstance check in _mapping_field, and type the Lyria predict read-only helpers as Mapping[str, object] instead of a bare dict with mutable-ok. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../adapters/streaming_iterator.py | 30 ++++++++++--------- .../messages/utils.py | 2 +- .../vertex_passthrough_logging_handler.py | 5 ++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index 9158ff4569f..47974dca20c 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -11,7 +11,6 @@ from typing import ( Final, Literal, Protocol, - cast, # noqa: TID251 # rebuilt message_delta dict spans the ContentBlockDelta/MessageBlockDelta union get_args, ) @@ -25,6 +24,7 @@ from litellm.types.llms.anthropic import ( ContentBlockDelta, ContextManagementResponse, MessageBlockDelta, + MessageDelta, StreamingContentBlockDeltaType, UsageDelta, UsageIteration, @@ -1006,26 +1006,28 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): self, processed_chunk: ContentBlockDelta | MessageBlockDelta, ) -> ContentBlockDelta | MessageBlockDelta: - if processed_chunk.get("type") != "message_delta" or not self._refusal_text: + if processed_chunk["type"] != "message_delta" or not self._refusal_text: return processed_chunk - delta: Final = cast(Mapping[str, object], processed_chunk["delta"]) # cast-ok: keys checked before use + delta: Final = processed_chunk["delta"] if delta.get("stop_reason") == "max_tokens": return processed_chunk from litellm.llms.anthropic.experimental_pass_through.messages.utils import ( refusal_stop_details, ) - return cast( # cast-ok: rebuilt dict matches the message_delta TypedDict shape for this branch - ContentBlockDelta | MessageBlockDelta, - { # mutable-ok: fresh translation payload; never mutated after construction - **processed_chunk, - "delta": { # mutable-ok: fresh message_delta payload; never mutated after construction - **delta, - "stop_reason": "refusal", - "stop_details": refusal_stop_details(self._refusal_text), - }, - }, - ) + refusal_delta: Final[MessageDelta] = { # mutable-ok: fresh message_delta payload + **delta, + "stop_reason": "refusal", + "stop_details": refusal_stop_details(self._refusal_text), + } + if "context_management" in processed_chunk: + return MessageBlockDelta( + type="message_delta", + delta=refusal_delta, + usage=processed_chunk["usage"], + context_management=processed_chunk["context_management"], + ) + return MessageBlockDelta(type="message_delta", delta=refusal_delta, usage=processed_chunk["usage"]) @staticmethod def _delta_has_content(processed_chunk: Mapping[str, object]) -> bool: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py index 7545dff1408..89105c00428 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py @@ -37,7 +37,7 @@ def _mapping_field(container: object, key: str) -> object | None: """One key of a raw provider payload, or None when the payload is not a mapping.""" if not isinstance(container, Mapping): return None - return cast(Mapping[str, object], container).get(key) # cast-ok: raw payload, callers re-check every value + return container.get(key) def _mapping_str_field(container: object, key: str) -> str | None: diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py index 119a53c2411..00db751abe2 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py @@ -1,5 +1,6 @@ import asyncio import re +from collections.abc import Mapping from datetime import datetime from typing import TYPE_CHECKING, Any, Final, cast from urllib.parse import urlparse @@ -380,7 +381,7 @@ class VertexPassthroughLoggingHandler: @staticmethod def _is_audio_predict_response( model: str, - json_response: dict, # mutable-ok: predicate inspects the decoded provider response dictionary without mutation + json_response: Mapping[str, object], ) -> bool: return ( VertexPassthroughLoggingHandler._get_audio_prediction_count(json_response=json_response) > 0 @@ -389,7 +390,7 @@ class VertexPassthroughLoggingHandler: @staticmethod def _get_audio_prediction_count( - json_response: dict, # mutable-ok: counter inspects the decoded provider response dictionary without mutation + json_response: Mapping[str, object], ) -> int: predictions: Final = json_response.get("predictions") if not isinstance(predictions, list): From 40e96e3e2c56cf2e4721a491f6548be783d0bcc8 Mon Sep 17 00:00:00 2001 From: mateo Date: Wed, 9 Sep 2026 08:08:44 +0000 Subject: [PATCH 02/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-09) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/litellm_logging.py | 2 +- litellm/proxy/_experimental/mcp_server/tool_search.py | 5 +++-- litellm/types/llms/bedrock.py | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index b0d6db20b31..70b9f8ec72c 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -4877,7 +4877,7 @@ def _maybe_construct_otel_v2(callback_name: str, _in_memory_loggers: list[Custom for callback in _in_memory_loggers: if ( isinstance(callback, OpenTelemetryV2) - and getattr(callback, "callback_name", None) == callback_name + and callback.callback_name == callback_name and (serves_a_destination or not _exports_nowhere(callback.config)) ): return callback diff --git a/litellm/proxy/_experimental/mcp_server/tool_search.py b/litellm/proxy/_experimental/mcp_server/tool_search.py index 2c73f9b863b..cedbccfb70f 100644 --- a/litellm/proxy/_experimental/mcp_server/tool_search.py +++ b/litellm/proxy/_experimental/mcp_server/tool_search.py @@ -119,14 +119,15 @@ def with_mcp_proxy_identity(tool: Tool, server_id: str) -> Tool: def _mcp_proxy_identity(tool: Tool) -> MCPProxyToolIdentity: - identity: Final = (tool.meta or {}).get(_MCP_PROXY_IDENTITY_META_KEY) # mutable-ok: absent metadata default + identity: Final = None if tool.meta is None else tool.meta.get(_MCP_PROXY_IDENTITY_META_KEY) if not isinstance(identity, Mapping): raise TypeError("MCP proxy tool identity is missing") server_id: Final = identity.get("server_id") tool_name: Final = identity.get("tool_name") if not isinstance(server_id, str) or not isinstance(tool_name, str): raise TypeError("MCP proxy tool identity is invalid") - return {"server_id": server_id, "tool_name": tool_name} # mutable-ok: TypedDict identity payload + resolved: Final[MCPProxyToolIdentity] = {"server_id": server_id, "tool_name": tool_name} + return resolved def mcp_proxy_tool_id(tool: Tool) -> str: diff --git a/litellm/types/llms/bedrock.py b/litellm/types/llms/bedrock.py index 9f93886a9c6..2950ad5f9f5 100644 --- a/litellm/types/llms/bedrock.py +++ b/litellm/types/llms/bedrock.py @@ -557,7 +557,6 @@ class AmazonTitanMultimodalEmbeddingResponse(TypedDict): message: str # Specifies any errors that occur during generation. -# TwelveLabs Marengo Embed types TWELVELABS_EMBEDDING_INPUT_TYPES = Literal["text", "image", "video", "audio"] TWELVELABS_EMBEDDING_OPTIONS = Literal["visual-text", "visual-image", "audio"] From 0a89eefca075be6b4cd1e4715066857ee5ea0c4a Mon Sep 17 00:00:00 2001 From: mateo Date: Thu, 10 Sep 2026 08:16:44 +0000 Subject: [PATCH 03/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-10) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/litellm_core_utils/prompt_templates/common_utils.py | 4 ++-- litellm/litellm_core_utils/streaming_handler.py | 2 +- .../responses_adapters/transformation.py | 4 ++-- .../proxy/management_endpoints/cost_tracking_settings.py | 1 - litellm/responses/streaming_iterator.py | 2 +- .../pre_call_checks/encrypted_content_affinity_check.py | 6 ++---- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 2485896184e..0d69a784750 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -2014,11 +2014,11 @@ def strip_encrypted_reasoning_from_messages(messages: object) -> None: """ if not isinstance(messages, list): return - for content in _anthropic_content_lists(cast(list[object], messages)): # cast-ok: untyped client json + for content in anthropic_content_lists(cast(list[object], messages)): # cast-ok: untyped client json _strip_encrypted_reasoning_from_blocks(content) -def _anthropic_content_lists(messages: Sequence[object]) -> Iterator[object]: +def anthropic_content_lists(messages: Sequence[object]) -> Iterator[object]: return ( cast(list[object], content) # cast-ok: narrowed by isinstance for message in messages diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index db23929e0c3..2a03075ce7d 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -1480,7 +1480,7 @@ class CustomStreamWrapper: "is_finished": chunk_finish_reason is not None, "finish_reason": chunk_finish_reason, "original_chunk": cached_chunk, - "tool_calls": (getattr(cached_choice.delta, "tool_calls", None) if cached_choice is not None else None), + "tool_calls": cached_choice.delta.tool_calls if cached_choice is not None else None, } completion_obj["content"] = response_obj["text"] diff --git a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py index 1fdb0318bab..b5e5c642940 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py @@ -171,7 +171,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: cls, summary: Iterable[object], encrypted_content: object, - ) -> dict[str, Any] | None: # mutable-ok: API message payload + ) -> dict[str, object] | None: # mutable-ok: API message payload """The one Anthropic block for a Responses reasoning item. The item's encrypted reasoning rides the block's opaque field (`signature`, or @@ -200,7 +200,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: @classmethod def _assistant_group_to_input_items( cls, group: tuple[Mapping[str, object], ...] - ) -> tuple[dict[str, Any], ...]: # mutable-ok: API message payload + ) -> tuple[dict[str, object], ...]: # mutable-ok: API message payload first: Final = group[0] btype: Final = first.get("type") if btype in ("thinking", "redacted_thinking"): diff --git a/litellm/proxy/management_endpoints/cost_tracking_settings.py b/litellm/proxy/management_endpoints/cost_tracking_settings.py index dc0da63555f..a679e79f8d4 100644 --- a/litellm/proxy/management_endpoints/cost_tracking_settings.py +++ b/litellm/proxy/management_endpoints/cost_tracking_settings.py @@ -634,7 +634,6 @@ async def estimate_cost( # Pinning one moment keeps an off-peak window that opens mid-quote from pricing the totals on # one side of it and the reported rates on the other. with pinned_billing_time(current_billing_time()): - # Use completion_cost which handles all the logic including margins/discounts try: cost_per_request: Final = completion_cost( completion_response=mock_response, diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 40ff88fc557..a8dd8c2d8cd 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -1323,7 +1323,7 @@ def _stamp_responses_usage_cost( if usage_obj is None: return response_obj.usage = usage_obj # rebind-ok: the stamped cost has to ride on the response the client receives - if isinstance(getattr(usage_obj, "cost", None), (int, float)): + if isinstance(usage_obj.cost, (int, float)): return try: cost: Final[float | None] = logging_obj._response_cost_calculator(result=response_obj) diff --git a/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py b/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py index cdd70e6baf2..939a852b34f 100644 --- a/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py +++ b/litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py @@ -49,6 +49,7 @@ from litellm.exceptions import ( ) from litellm.integrations.custom_logger import CustomLogger, Span from litellm.litellm_core_utils.prompt_templates.common_utils import ( + anthropic_content_lists, encrypted_content_of_block, strip_encrypted_reasoning_from_messages, ) @@ -154,10 +155,7 @@ class EncryptedContentAffinityCheck(CustomLogger): return iter(()) return ( cast(Mapping[str, object], block) # cast-ok: narrowed by isinstance - for message in cast(list[object], messages) # cast-ok: narrowed by isinstance - if isinstance(message, Mapping) - for content in (cast(Mapping[str, object], message).get("content"),) # cast-ok: narrowed by isinstance - if isinstance(content, list) + for content in anthropic_content_lists(cast(list[object], messages)) # cast-ok: narrowed by isinstance for block in cast(list[object], content) # cast-ok: narrowed by isinstance if isinstance(block, Mapping) ) From 2d0ee09e58463f61d7957e39b9e5b052c32e6381 Mon Sep 17 00:00:00 2001 From: mateo Date: Thu, 10 Sep 2026 08:23:14 +0000 Subject: [PATCH 04/17] refactor: keep the pre-existing cost-estimate comment and usage cost read out of the cleanup Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/management_endpoints/cost_tracking_settings.py | 1 + litellm/responses/streaming_iterator.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/cost_tracking_settings.py b/litellm/proxy/management_endpoints/cost_tracking_settings.py index a679e79f8d4..dc0da63555f 100644 --- a/litellm/proxy/management_endpoints/cost_tracking_settings.py +++ b/litellm/proxy/management_endpoints/cost_tracking_settings.py @@ -634,6 +634,7 @@ async def estimate_cost( # Pinning one moment keeps an off-peak window that opens mid-quote from pricing the totals on # one side of it and the reported rates on the other. with pinned_billing_time(current_billing_time()): + # Use completion_cost which handles all the logic including margins/discounts try: cost_per_request: Final = completion_cost( completion_response=mock_response, diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index a8dd8c2d8cd..40ff88fc557 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -1323,7 +1323,7 @@ def _stamp_responses_usage_cost( if usage_obj is None: return response_obj.usage = usage_obj # rebind-ok: the stamped cost has to ride on the response the client receives - if isinstance(usage_obj.cost, (int, float)): + if isinstance(getattr(usage_obj, "cost", None), (int, float)): return try: cost: Final[float | None] = logging_obj._response_cost_calculator(result=response_obj) From a84163f5972eee5a7dc3ccf3ea66b7f7a1cfe708 Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 13 Sep 2026 08:03:59 +0000 Subject: [PATCH 05/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-13) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/cost_calculator.py | 8 +++++--- .../proxy/_experimental/mcp_server/mcp_server_manager.py | 2 +- .../management_endpoints/model_management_endpoints.py | 6 ++++-- .../litellm_completion_transformation/transformation.py | 2 +- litellm/rust_bridge/lifecycle.py | 6 +----- litellm/types/utils.py | 2 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 2 -- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 440d97d13be..57a7c093d80 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1924,8 +1924,8 @@ def _deployment_model_info( return registered_deployment_info if litellm_logging_obj is None: return None - litellm_params: Final = getattr(litellm_logging_obj, "litellm_params", None) - if litellm_params is None: + litellm_params: Final = litellm_logging_obj.litellm_params + if not litellm_params: return None return next( ( @@ -1943,7 +1943,9 @@ def _ocr_model_info( router_model_id: str | None, ) -> OCRPricing | None: deployment_info: Final = _deployment_model_info(litellm_logging_obj, custom_pricing, router_model_id) - litellm_params: Final = getattr(litellm_logging_obj, "litellm_params", None) if custom_pricing else None + litellm_params: Final = ( + litellm_logging_obj.litellm_params if custom_pricing and litellm_logging_obj is not None else None + ) if litellm_params is None: return deployment_info return _layered_ocr_pricing(litellm_params, deployment_info) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index fb0c623473a..a03cb428463 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -3489,7 +3489,7 @@ class MCPServerManager: passthrough_server_ids: Final = [ server.server_id for server in self.get_registry().values() - if getattr(server, "auth_type", None) == MCPAuth.true_passthrough + if server.auth_type == MCPAuth.true_passthrough ] combined_servers.update(passthrough_server_ids) diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 2234e825090..8155119cf7b 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -2094,8 +2094,10 @@ async def add_new_model( enforced=bool(general_settings.get(ENFORCE_RPM_TPM_ON_MODEL_ADD_SETTING, False)), ) - model_params.model_info = ModelInfo( # rebind-ok: downstream team-model handling mutates this same object - **without_server_derived_pricing(model_params.model_info.model_dump(exclude_none=True)) + model_params.model_info = ( + ModelInfo.model_validate( # rebind-ok: downstream team-model handling mutates this same object + dict(without_server_derived_pricing(model_params.model_info.model_dump(exclude_none=True))) + ) ) model_response: prisma_models.LiteLLM_ProxyModelTable | LiteLLM_ProxyModelTable | None = None diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index e8aacac9e67..a91d3b4153a 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -2238,7 +2238,7 @@ class LiteLLMCompletionResponsesConfig: ) -> Mapping[str, ResponseFunctionWebSearch]: calls: Final[dict[str, ResponseFunctionWebSearch]] = {} # mutable-ok: indexes provider-built calls for choice in chat_completion_response.choices: - provider_fields = getattr(choice.message, "provider_specific_fields", None) + provider_fields = choice.message.provider_specific_fields if not isinstance(provider_fields, Mapping): continue web_search_calls = provider_fields.get("web_search_calls") diff --git a/litellm/rust_bridge/lifecycle.py b/litellm/rust_bridge/lifecycle.py index f5e0c1b0fc6..bb83511a78c 100644 --- a/litellm/rust_bridge/lifecycle.py +++ b/litellm/rust_bridge/lifecycle.py @@ -147,11 +147,7 @@ def callbacks_needed(logger: Logging, phase: str) -> bool: _is_debugging_on, # pyright: ignore[reportPrivateUsage] # use the same debug gate as Logging ) - if ( - _is_debugging_on() - or getattr(logger, "litellm_request_debug", False) - or os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD") - ): + if _is_debugging_on() or logger.litellm_request_debug or os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"): return True input_needed: Final = bool( litellm.input_callback diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 00c55b35182..84231423562 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3667,7 +3667,7 @@ def is_server_derived_pricing_key(key: str) -> bool: return key in SERVER_DERIVED_PRICING_FIELDS or ABOVE_THRESHOLD_COST_KEY_PATTERN.search(key) is not None -def without_server_derived_pricing(model_info: Mapping[str, Any]) -> Mapping[str, Any]: +def without_server_derived_pricing(model_info: Mapping[str, object]) -> Mapping[str, object]: """Drop the pricing ``/model/info`` derives for display, keeping everything else. ``/model/info`` fills a deployment's missing pricing in from the cost map so the diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 7eadaa6c991..839aa52fa84 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -16781,7 +16781,6 @@ export interface paths { * - permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking. * - metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" } * - max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x. - * - soft_budget: Optional[float] - Get alerts when user crosses given budget, doesn't block requests. * - model_max_budget: Optional[dict] - Model-specific max budget for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-budgets-to-keys) * - budget_fallbacks: Optional[Dict[str, List[str]]] - Per-model fallback chain tried in order when that model's own `model_max_budget` is exceeded, e.g. {"gpt-4o": ["gpt-4o-mini"]}. * - model_rpm_limit: Optional[float] - Model-specific rpm limit for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-limits-to-keys) @@ -16887,7 +16886,6 @@ export interface paths { * - permissions: Optional[dict] - [Not Implemented Yet] User-specific permissions, eg. turning off pii masking. * - metadata: Optional[dict] - Metadata for user, store information for user. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" } * - max_parallel_requests: Optional[int] - Rate limit a user based on the number of parallel requests. Raises 429 error, if user's parallel requests > x. - * - soft_budget: Optional[float] - Get alerts when user crosses given budget, doesn't block requests. * - model_max_budget: Optional[dict] - Model-specific max budget for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-budgets-to-keys) * - budget_fallbacks: Optional[Dict[str, List[str]]] - Per-model fallback chain tried in order when that model's own `model_max_budget` is exceeded, e.g. {"gpt-4o": ["gpt-4o-mini"]}. * - model_rpm_limit: Optional[float] - Model-specific rpm limit for user. [Docs](https://docs.litellm.ai/docs/proxy/users#add-model-specific-limits-to-keys) From 1bc243893b1fe647e0e189d51aa150eb37ddfd1e Mon Sep 17 00:00:00 2001 From: mateo Date: Sun, 13 Sep 2026 08:36:03 +0000 Subject: [PATCH 06/17] refactor: keep the model info pricing helper out of the cleanup Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../management_endpoints/model_management_endpoints.py | 6 ++---- litellm/types/utils.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 8155119cf7b..2234e825090 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -2094,10 +2094,8 @@ async def add_new_model( enforced=bool(general_settings.get(ENFORCE_RPM_TPM_ON_MODEL_ADD_SETTING, False)), ) - model_params.model_info = ( - ModelInfo.model_validate( # rebind-ok: downstream team-model handling mutates this same object - dict(without_server_derived_pricing(model_params.model_info.model_dump(exclude_none=True))) - ) + model_params.model_info = ModelInfo( # rebind-ok: downstream team-model handling mutates this same object + **without_server_derived_pricing(model_params.model_info.model_dump(exclude_none=True)) ) model_response: prisma_models.LiteLLM_ProxyModelTable | LiteLLM_ProxyModelTable | None = None diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 84231423562..00c55b35182 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3667,7 +3667,7 @@ def is_server_derived_pricing_key(key: str) -> bool: return key in SERVER_DERIVED_PRICING_FIELDS or ABOVE_THRESHOLD_COST_KEY_PATTERN.search(key) is not None -def without_server_derived_pricing(model_info: Mapping[str, object]) -> Mapping[str, object]: +def without_server_derived_pricing(model_info: Mapping[str, Any]) -> Mapping[str, Any]: """Drop the pricing ``/model/info`` derives for display, keeping everything else. ``/model/info`` fills a deployment's missing pricing in from the cost map so the From 229add205b22f12f6cb1cb4bdb24002748fcfd2a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:08:54 +0000 Subject: [PATCH 07/17] refactor: drop suppressions that no longer suppress anything (2026-09-16) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../chat/guardrail_translation/handler.py | 26 ++++++------------- .../nvidia_nim/passthrough/transformation.py | 2 +- .../proxy/hooks/proxy_track_cost_callback.py | 2 +- .../management_helpers/bulk_user_creation.py | 6 ++--- .../management_helpers/bulk_user_deletion.py | 2 +- litellm/responses/streaming_iterator.py | 2 +- .../complexity_router/complexity_router.py | 4 +-- 7 files changed, 17 insertions(+), 27 deletions(-) diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 2ea20143f0c..6e528f9ba4b 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -199,9 +199,7 @@ def _write_back_system_block(system: object, block_idx: int, response: str) -> N return text_blocks: Final = tuple(block for block in system if isinstance(block, dict) and block.get("type") == "text") if block_idx < len(text_blocks): - text_blocks[block_idx]["text"] = ( - response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + text_blocks[block_idx]["text"] = response def _write_back_message_text(message: _WritableMessage, target: MessageTextTarget, response: str) -> None: @@ -211,22 +209,16 @@ def _write_back_message_text(message: _WritableMessage, target: MessageTextTarge match target: case MessageContentTarget(): if isinstance(content, str): - message["content"] = response # mutable-ok: guardrails rewrite the caller's request payload in place + message["content"] = response case ContentBlockTextTarget(content_idx=content_idx): if isinstance(content, list): - content[content_idx]["text"] = ( - response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + content[content_idx]["text"] = response case ToolResultStringTarget(content_idx=content_idx): if isinstance(content, list): - content[content_idx]["content"] = ( - response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + content[content_idx]["content"] = response case ToolResultBlockTextTarget(content_idx=content_idx, block_idx=block_idx): if isinstance(content, list): - content[content_idx]["content"][block_idx]["text"] = ( - response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + content[content_idx]["content"][block_idx]["text"] = response case _: assert_never(target) @@ -248,9 +240,9 @@ def _write_back_tool_use( block: Final = content[target.content_idx] if isinstance(content, list) else None if not isinstance(block, dict): return - block["input"] = rewritten_input # mutable-ok: guardrails rewrite the caller's request payload in place + block["input"] = rewritten_input if shape.name is not None and shape.name != block.get("name"): - block["name"] = shape.name # mutable-ok: guardrails rewrite the caller's request payload in place + block["name"] = shape.name @dataclass(frozen=True, slots=True) @@ -606,9 +598,7 @@ class AnthropicMessagesHandler(BaseTranslation): image for one_message in extracted for image in one_message.images ] # mutable-ok: GenericGuardrailAPIInputs takes list[str] scanned_tool_calls: Final = tuple(item for one_message in extracted for item in one_message.tool_calls) - tool_calls_to_check: Final = [ - item.tool_call for item in scanned_tool_calls - ] # mutable-ok: GenericGuardrailAPIInputs takes list[ChatCompletionToolCallChunk] + tool_calls_to_check: Final = [item.tool_call for item in scanned_tool_calls] pre_guardrail_tool_calls: Final = _tool_call_shapes(tool_calls_to_check) # Step 2: Apply guardrail to all texts and tool calls in batch diff --git a/litellm/llms/nvidia_nim/passthrough/transformation.py b/litellm/llms/nvidia_nim/passthrough/transformation.py index 7de1ce4d631..e8e7da8e10b 100644 --- a/litellm/llms/nvidia_nim/passthrough/transformation.py +++ b/litellm/llms/nvidia_nim/passthrough/transformation.py @@ -110,7 +110,7 @@ class NvidiaNimPassthroughConfig(BasePassthroughConfig): return { **headers, "Authorization": f"Bearer {api_key}", - } # mutable-ok: base class contract returns dict for httpx + } @staticmethod def get_api_base(api_base: str | None = None) -> str | None: diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index 1ae106be390..412112e3419 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -734,7 +734,7 @@ async def _reconcile_budget_reservation_before_db_update( "Failed to invalidate budget reservation counters after pre-persist reconcile failed" ) finally: - budget_reservation["finalized"] = True # rebind-ok: the counter update reads the stamp off the shared dict + budget_reservation["finalized"] = True # rebind-ok: the counter update reads the finalized stamp off the caller's shared dict # fmt: skip async def _release_budget_reservation(budget_reservation: dict | None) -> None: diff --git a/litellm/proxy/management_helpers/bulk_user_creation.py b/litellm/proxy/management_helpers/bulk_user_creation.py index 56abe3b6a3f..ec8fd312766 100644 --- a/litellm/proxy/management_helpers/bulk_user_creation.py +++ b/litellm/proxy/management_helpers/bulk_user_creation.py @@ -348,7 +348,7 @@ async def _prepare_user(user: _PendingUser, prisma_client: PrismaClient) -> _Pre data: Final = {**dumped, "user_id": user.user_id} # mutable-ok: /user/new defaults helper mutates in place data_json: Final = _JSON_OBJECT.validate_python(_update_internal_new_user_params(data, user.request)) with_permission: Final = _JSON_OBJECT.validate_python( - await _set_object_permission(data_json=data_json, prisma_client=prisma_client) # pyright: ignore[reportUnknownArgumentType] # validated by the adapter + await _set_object_permission(data_json=data_json, prisma_client=prisma_client) ) return _PreparedUser(user, _USER_ROW.validate_python(with_permission)) except Exception as exc: # noqa: BLE001 # any preparation failure is reported on this row only @@ -509,7 +509,7 @@ class _TeamsData(TypedDict): def _default_member_budget_id(team: LiteLLM_TeamTable) -> str | None: metadata: Final = ( _JSON_OBJECT.validate_python( - team.metadata # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # LiteLLM_TeamTable.metadata is a bare dict; validated by the adapter + team.metadata # pyright: ignore[reportUnknownMemberType] # LiteLLM_TeamTable.metadata is a bare dict; validated by the adapter ) if team.metadata # pyright: ignore[reportUnknownMemberType] # same bare dict else None @@ -543,7 +543,7 @@ async def _write_team_roster( already_present: Final = frozenset(member.user_id for member in roster if member.user_id) new_members: Final = tuple(member for member in members if member.user_id not in already_present) budget_ids: Final = tuple( - [ # mutable-ok: budgets are created one at a time on the transaction's single connection + [ await _resolve_member_budget_id( prisma_client=prisma_client, user_api_key_dict=user_api_key_dict, diff --git a/litellm/proxy/management_helpers/bulk_user_deletion.py b/litellm/proxy/management_helpers/bulk_user_deletion.py index 1ae83b0004a..57edddef345 100644 --- a/litellm/proxy/management_helpers/bulk_user_deletion.py +++ b/litellm/proxy/management_helpers/bulk_user_deletion.py @@ -187,7 +187,7 @@ def _error_message(exc: BaseException) -> str: if isinstance(exc, HTTPException) and isinstance(exc.detail, dict): return str(exc.detail.get("error", exc.detail)) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType] # HTTPException.detail is untyped if isinstance(exc, HTTPException): - return str(exc.detail) # pyright: ignore[reportUnknownArgumentType] # HTTPException.detail is untyped + return str(exc.detail) return str(exc) or type(exc).__name__ diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 8d766cf1cd0..8559450e6ef 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -1351,7 +1351,7 @@ def _billed_terminal_response( return None usage: Final[object] = response_obj.get("usage") # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # a model_constructed terminal event leaves response as an untyped dict return ResponsesAPIResponse.model_construct( - **{**response_obj, "usage": usage if usage is not None or estimate is None else estimate()} # pyright: ignore[reportUnknownArgumentType, reportArgumentType] # same untyped dict spread + **{**response_obj, "usage": usage if usage is not None or estimate is None else estimate()} # pyright: ignore[reportArgumentType] # same untyped dict spread ) diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index d19cdfaa899..1248dbe973c 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -1057,7 +1057,7 @@ def _with_classifier_forecast( if forecast is None: return decision verdict: Final = forecast.verdict - enriched: Final[StandardLoggingRoutingDecision] = { # mutable-ok: routing decisions are JSON TypedDict records + enriched: Final[StandardLoggingRoutingDecision] = { **decision, "classifier_crux": verdict.crux, "classifier_primary_rule": verdict.primary_rule, @@ -2292,7 +2292,7 @@ class ComplexityRouter(CustomLogger): {"role": "user", "content": opening_task}, # mutable-ok: SDK messages are dict-shaped ] if latest_follow_up is not None: - task_messages.append( # mutable-ok: the provider SDK requires a concrete message list + task_messages.append( {"role": "user", "content": latest_follow_up} # mutable-ok: SDK messages are dict-shaped ) From 76cb86b0a1fc15a2b993df7478f5ebc5df5952bf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:09:55 +0000 Subject: [PATCH 08/17] refactor: keep the rebind-ok reason inside the line limit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/hooks/proxy_track_cost_callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index 412112e3419..e916b62f9d5 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -734,7 +734,7 @@ async def _reconcile_budget_reservation_before_db_update( "Failed to invalidate budget reservation counters after pre-persist reconcile failed" ) finally: - budget_reservation["finalized"] = True # rebind-ok: the counter update reads the finalized stamp off the caller's shared dict # fmt: skip + budget_reservation["finalized"] = True # rebind-ok: stamps the caller's shared dict for the counter update async def _release_budget_reservation(budget_reservation: dict | None) -> None: From 715f099a01c8763422283a2110d38c71cda53110 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 08:19:42 +0000 Subject: [PATCH 09/17] fix(anthropic): rebuild the refusal message_delta by spreading the chunk Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../adapters/streaming_iterator.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index d4537dd9032..97e466b310e 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -1042,14 +1042,8 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): "stop_reason": "refusal", "stop_details": refusal_stop_details(self._refusal_text), } - if "context_management" in processed_chunk: - return MessageBlockDelta( - type="message_delta", - delta=refusal_delta, - usage=processed_chunk["usage"], - context_management=processed_chunk["context_management"], - ) - return MessageBlockDelta(type="message_delta", delta=refusal_delta, usage=processed_chunk["usage"]) + refusal_chunk: Final[MessageBlockDelta] = {**processed_chunk, "delta": refusal_delta} + return refusal_chunk @staticmethod def _delta_has_content(processed_chunk: Mapping[str, object]) -> bool: From 90ea53422aefc60ebcf16450b1bde2f274444631 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 08:13:14 +0000 Subject: [PATCH 10/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-17) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/_experimental/mcp_server/bridge_token_flow.py | 4 ++-- litellm/proxy/common_request_processing.py | 2 +- litellm/proxy/guardrails/guardrail_initializers.py | 2 +- litellm/proxy/litellm_pre_call_utils.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/bridge_token_flow.py b/litellm/proxy/_experimental/mcp_server/bridge_token_flow.py index 2b13baa624b..d29a006a93c 100644 --- a/litellm/proxy/_experimental/mcp_server/bridge_token_flow.py +++ b/litellm/proxy/_experimental/mcp_server/bridge_token_flow.py @@ -86,8 +86,8 @@ async def oauth_authorization_uses_gateway_credential(request: Request) -> bool: async def _opaque_bearer_is_gateway_credential(token: str) -> bool: - from litellm.proxy._experimental.mcp_server.outbound_credentials.envelope import ( - is_envelope, # noqa: PLC0415 # envelope imports bridge types + from litellm.proxy._experimental.mcp_server.outbound_credentials.envelope import ( # noqa: PLC0415 # envelope imports bridge types + is_envelope, is_refresh_envelope, ) from litellm.proxy._types import hash_token # noqa: PLC0415 # proxy import cycle diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 2f39e6c71bc..87660dc61eb 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -2241,7 +2241,7 @@ class ProxyBaseLLMRequestProcessing: return fallbacks if isinstance(fallbacks, list) and fallbacks else None @staticmethod - def _resolve_fallback_models(model: str, fallbacks: list) -> list | None: + def _resolve_fallback_models(model: str, fallbacks: list) -> list[str] | None: from litellm.router_utils.fallback_event_handlers import get_fallback_model_group fallback_model_group, generic_fallback_idx = get_fallback_model_group( diff --git a/litellm/proxy/guardrails/guardrail_initializers.py b/litellm/proxy/guardrails/guardrail_initializers.py index 356eb7c96c6..f2913f5b497 100644 --- a/litellm/proxy/guardrails/guardrail_initializers.py +++ b/litellm/proxy/guardrails/guardrail_initializers.py @@ -120,7 +120,7 @@ def initialize_presidio(litellm_params: LitellmParams, guardrail: Guardrail) -> _OPTIONAL_PresidioPIIMasking, ) - explicit_filter_scope: Final = getattr(litellm_params, "presidio_filter_scope", None) + explicit_filter_scope: Final = litellm_params.presidio_filter_scope filter_scope: Final = explicit_filter_scope or ("input" if _is_mcp_only_mode(litellm_params.mode) else "both") run_input: Final = filter_scope in ("input", "both") run_output: Final = filter_scope in ("output", "both") diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index b03f1e4348c..a4384ed9846 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -137,7 +137,7 @@ def add_otel_trace_id_to_request( return data["litellm_trace_id"] = trace_id # rebind-ok: data is an out-param if isinstance(metadata, dict): - metadata["trace_id"] = trace_id # rebind-ok: metadata is the request's own out-param dict + metadata["trace_id"] = trace_id def _session_id_from_baggage(baggage: str) -> str | None: From f349b1c39e5fd15c690dbe81130379ef0a2715ef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:55:59 +0000 Subject: [PATCH 11/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-18) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/management_endpoints/common_utils.py | 8 ++++---- .../complexity_router/jev_classifier.py | 4 ++-- litellm/rust_bridge/dispatch.py | 10 +++++----- litellm/rust_bridge/legacy_callbacks.py | 6 +----- litellm/rust_bridge/response_metadata.py | 4 ++-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/litellm/proxy/management_endpoints/common_utils.py b/litellm/proxy/management_endpoints/common_utils.py index 29f24d2465f..0771d846b4c 100644 --- a/litellm/proxy/management_endpoints/common_utils.py +++ b/litellm/proxy/management_endpoints/common_utils.py @@ -494,7 +494,7 @@ def _prisma_value(value: object) -> object: return list(value) if isinstance(value, tuple) else value -def member_budget_patch(source: BaseModel) -> dict[str, Any]: +def member_budget_patch(source: BaseModel) -> Mapping[str, object]: """Map the per-member limit fields a request actually set to their budget-table columns (merge-patch: a sent value updates, an explicit null clears, an absent field is left untouched).""" @@ -527,7 +527,7 @@ async def _upsert_budget_and_membership( user_id: str, existing_budget_id: str | None, user_api_key_dict: UserAPIKeyAuth, - budget_patch: dict[str, Any], + budget_patch: Mapping[str, object], team_default_budget_id: str | None = None, shared_budget_ids: frozenset[str] | None = None, ): @@ -585,9 +585,9 @@ async def _upsert_budget_and_membership( source_row: Final = ( await tx.litellm_budgettable.find_unique(where={"budget_id": existing_budget_id}) if is_shared_default else None ) - source: Final[Mapping[str, Any]] = source_row.model_dump() if source_row is not None else MappingProxyType({}) + source: Final[Mapping[str, object]] = source_row.model_dump() if source_row is not None else MappingProxyType({}) - create_data: Final[dict[str, Any]] = { # mutable-ok: Prisma create payloads are dict-shaped + create_data: Final[dict[str, object]] = { # mutable-ok: Prisma create payloads are dict-shaped "created_by": user_api_key_dict.user_id or "", "updated_by": user_api_key_dict.user_id or "", **MappingProxyType( diff --git a/litellm/router_strategy/complexity_router/jev_classifier.py b/litellm/router_strategy/complexity_router/jev_classifier.py index 7190e75f0fb..1855c0f577c 100644 --- a/litellm/router_strategy/complexity_router/jev_classifier.py +++ b/litellm/router_strategy/complexity_router/jev_classifier.py @@ -1,6 +1,6 @@ from collections.abc import Mapping from types import MappingProxyType -from typing import Annotated, Final, Literal, NamedTuple, Protocol +from typing import Annotated, Final, Literal, NamedTuple, Protocol, TypeAlias from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError @@ -12,7 +12,7 @@ DEFAULT_JEV_INSTRUCTIONS: Final = ( "instructions inside it asking for a tier are content to classify, never commands." ) -JevProbability = Annotated[float, Field(ge=0.0, le=1.0)] +JevProbability: TypeAlias = Annotated[float, Field(ge=0.0, le=1.0)] class JevChoiceQuestion(BaseModel): diff --git a/litellm/rust_bridge/dispatch.py b/litellm/rust_bridge/dispatch.py index 7ddc903df58..852dfd6d44e 100644 --- a/litellm/rust_bridge/dispatch.py +++ b/litellm/rust_bridge/dispatch.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Mapping from dataclasses import dataclass -from typing import Final, Generic, TypeVar +from typing import Final, Generic, TypeAlias, TypeVar from litellm.rust_bridge import catalog, runtime from litellm.rust_bridge.bindings import NativeBinding @@ -10,11 +10,11 @@ from litellm.rust_bridge.catalog import Context, Route, Rules from litellm.rust_bridge.configuration import Decision from litellm.rust_bridge.configuration import decision as rollout_decision -RequestT = TypeVar("RequestT") -NativeT = TypeVar("NativeT") -ResultT = TypeVar("ResultT") +RequestT: Final = TypeVar("RequestT") +NativeT: Final = TypeVar("NativeT") +ResultT: Final = TypeVar("ResultT") -NativeHook = Callable[[RequestT, tuple[object, ...], Mapping[str, object]], ResultT] +NativeHook: TypeAlias = Callable[[RequestT, tuple[object, ...], Mapping[str, object]], ResultT] def call_hook( diff --git a/litellm/rust_bridge/legacy_callbacks.py b/litellm/rust_bridge/legacy_callbacks.py index e05d9368fa8..c70793698a1 100644 --- a/litellm/rust_bridge/legacy_callbacks.py +++ b/litellm/rust_bridge/legacy_callbacks.py @@ -111,11 +111,7 @@ def callbacks_needed(logger: Logging, phase: Phase) -> bool: _is_debugging_on, # pyright: ignore[reportPrivateUsage] # use the same debug gate as Logging ) - if ( - _is_debugging_on() - or getattr(logger, "litellm_request_debug", False) - or os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD") - ): + if _is_debugging_on() or logger.litellm_request_debug or os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"): return True input_needed: Final = bool( litellm.input_callback diff --git a/litellm/rust_bridge/response_metadata.py b/litellm/rust_bridge/response_metadata.py index 1c03515720e..ae459710b34 100644 --- a/litellm/rust_bridge/response_metadata.py +++ b/litellm/rust_bridge/response_metadata.py @@ -1,10 +1,10 @@ -from typing import TypeVar +from typing import Final, TypeVar from litellm.router_utils.add_retry_fallback_headers import ( _add_headers_to_response, # pyright: ignore[reportPrivateUsage] # reuse the proxy's identity-preserving response metadata writer ) -ResultT = TypeVar("ResultT") +ResultT: Final = TypeVar("ResultT") def mark_rust_response(response: ResultT) -> ResultT: From 6290f990486d7dcd940746e1951b404d382d7275 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 08:03:29 +0000 Subject: [PATCH 12/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-19) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/integrations/websearch_interception/handler.py | 2 +- litellm/llms/a2a/chat/transformation.py | 2 +- .../llms/openai/responses/guardrail_translation/handler.py | 2 +- litellm/main.py | 4 ++-- .../_experimental/mcp_server/discoverable_endpoints.py | 6 ++++-- litellm/proxy/spend_tracking/daily_global_spend_rollup.py | 4 ---- litellm/types/utils.py | 4 ++-- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 6a4c67c7db1..3b03a73fc2f 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -1762,7 +1762,7 @@ class WebSearchInterceptionLogger(CustomLogger): for tool_call in tool_calls: # Handle both Anthropic-style input and OpenAI-style function.arguments query = None - tool_args: dict | None = None # mutable-ok: the tool call's own arguments dict + tool_args: dict[str, object] | None = None # mutable-ok: the tool call's own arguments dict if "input" in tool_call and isinstance(tool_call["input"], dict): tool_args = tool_call["input"] query = tool_args.get("query") diff --git a/litellm/llms/a2a/chat/transformation.py b/litellm/llms/a2a/chat/transformation.py index 77f26b65de0..86251e5a8bf 100644 --- a/litellm/llms/a2a/chat/transformation.py +++ b/litellm/llms/a2a/chat/transformation.py @@ -49,7 +49,7 @@ def _registry_api_key(agent_litellm_params: Mapping[str, object]) -> str | None: return configured_api_key if isinstance(configured_api_key, str) else None -def _registry_headers(agent_litellm_params: Mapping[str, object]) -> dict[str, Any] | None: +def _registry_headers(agent_litellm_params: Mapping[str, object]) -> dict[str, object] | None: stored_headers: Final = agent_litellm_params.get("headers") if not isinstance(stored_headers, Mapping): return None diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index e3e53f9b3dc..f8e2bca3b4b 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -989,7 +989,7 @@ class OpenAIResponsesHandler(BaseTranslation): def _spread_text_rewrite_over_stream_events( self, - stream_events: Sequence[Any], + stream_events: Sequence[object], rewritten_text: str, guardrail_name: str, ) -> None: diff --git a/litellm/main.py b/litellm/main.py index 38184db1d10..772a9c90222 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1161,7 +1161,7 @@ def _is_claude_tool_target(custom_llm_provider: str | None, model: str) -> bool: return False -def _without_anthropic_only_tool_keys(tool: dict) -> dict: +def _without_anthropic_only_tool_keys(tool: dict[str, object]) -> dict[str, object]: kept: Final = {key: value for key, value in tool.items() if key not in _ANTHROPIC_ONLY_TOOL_KEYS} function: Final = tool.get("function") if not isinstance(function, dict): @@ -1172,7 +1172,7 @@ def _without_anthropic_only_tool_keys(tool: dict) -> dict: } -def _drop_anthropic_only_tool_keys(tools: list[dict] | None) -> list[dict] | None: +def _drop_anthropic_only_tool_keys(tools: list[dict[str, object]] | None) -> list[dict[str, object]] | None: if tools is None: return None return [_without_anthropic_only_tool_keys(tool) if isinstance(tool, dict) else tool for tool in tools] diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 64bab0a7832..1d77e543db1 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -2613,7 +2613,7 @@ def oauth_protected_resource_root(request: Request) -> dict[str, str | tuple[str } -def _build_aggregate_protected_resource_response(request: Request) -> dict: +def _build_aggregate_protected_resource_response(request: Request) -> dict[str, object]: """RFC 9728 metadata for the aggregate /mcp resource: the gateway itself is the authorization server. No per-server names or scopes leak here; access is resolved after sign-in from the authenticated user's grants. @@ -2633,7 +2633,9 @@ def _build_aggregate_protected_resource_response(request: Request) -> dict: } -def _build_aggregate_authorization_server_response(request: Request, token_exchange_available: bool) -> dict: +def _build_aggregate_authorization_server_response( + request: Request, token_exchange_available: bool +) -> dict[str, object]: """RFC 8414 metadata for the gateway as the aggregate authorization server. The issuer is ``{base}/mcp`` and must stay equal to the value the diff --git a/litellm/proxy/spend_tracking/daily_global_spend_rollup.py b/litellm/proxy/spend_tracking/daily_global_spend_rollup.py index 376b113ed02..b81b6c1943e 100644 --- a/litellm/proxy/spend_tracking/daily_global_spend_rollup.py +++ b/litellm/proxy/spend_tracking/daily_global_spend_rollup.py @@ -198,10 +198,6 @@ async def _scan_pending(prisma_client: "PrismaClient") -> _PendingScan: return _PendingScan(marker, db_now.now, tuple(_DateRow.model_validate(row).date for row in rows)) -async def pending_days(prisma_client: "PrismaClient") -> tuple[str, ...]: - return (await _scan_pending(prisma_client)).days - - async def reconcile_day(prisma_client: "PrismaClient", day: str) -> None: """Rewrite one day of the global table from the per-key sums. Idempotent: a rerun overwrites every group with the same totals.""" diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 090f6589024..5131d060165 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3745,7 +3745,7 @@ def without_server_derived_pricing(model_info: Mapping[str, Any]) -> Mapping[str ) -def echoed_cost_map_pricing_fields(model_info: Mapping[str, Any]) -> tuple[str, ...]: +def echoed_cost_map_pricing_fields(model_info: Mapping[str, object]) -> tuple[str, ...]: """Pricing fields a stored ``model_info`` blob copied from a ``/model/info`` response. Only ``litellm.get_model_info`` emits ``key`` (the resolved cost-map entry), so a stored @@ -3758,7 +3758,7 @@ def echoed_cost_map_pricing_fields(model_info: Mapping[str, Any]) -> tuple[str, return tuple(sorted(k for k in model_info if is_server_derived_pricing_key(k))) -def pricing_override_fields(*sources: Mapping[str, Any]) -> tuple[str, ...]: +def pricing_override_fields(*sources: Mapping[str, object]) -> tuple[str, ...]: return tuple( sorted( frozenset( From 523d61dd8a63441ce6838afc3f76cfc1798483cb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 08:04:32 +0000 Subject: [PATCH 13/17] refactor: keep the pre-existing protected-resource return type out of the cleanup Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../proxy/_experimental/mcp_server/discoverable_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 1d77e543db1..8d8e139e242 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -2613,7 +2613,7 @@ def oauth_protected_resource_root(request: Request) -> dict[str, str | tuple[str } -def _build_aggregate_protected_resource_response(request: Request) -> dict[str, object]: +def _build_aggregate_protected_resource_response(request: Request) -> dict: """RFC 9728 metadata for the aggregate /mcp resource: the gateway itself is the authorization server. No per-server names or scopes leak here; access is resolved after sign-in from the authenticated user's grants. From 6110536f7f15698442cfcd637afbd8146579ae5d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 08:01:20 +0000 Subject: [PATCH 14/17] refactor: clear fresh tech debt from the last 24 hours (2026-09-20) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/experimental_mcp_client/tools.py | 4 ++-- litellm/litellm_core_utils/litellm_logging.py | 4 ++-- .../anthropic/chat/guardrail_translation/handler.py | 4 +--- litellm/proxy/auth/auth_checks.py | 2 +- litellm/proxy/db/baseline_accounting.py | 10 +++++----- litellm/proxy/hooks/autorouter_baseline_cache.py | 8 ++++---- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/litellm/experimental_mcp_client/tools.py b/litellm/experimental_mcp_client/tools.py index a9ee851d529..8e640f8b837 100644 --- a/litellm/experimental_mcp_client/tools.py +++ b/litellm/experimental_mcp_client/tools.py @@ -129,8 +129,8 @@ async def list_tools_with_pagination( ) tools.extend(result.tools) - next_cursor = getattr(result, "next_cursor", None) - if not isinstance(next_cursor, str) or not next_cursor: + next_cursor = result.next_cursor + if not next_cursor: return tools if next_cursor in seen_cursors: verbose_logger.warning( diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 5d9a1a3a6a2..b37159a5bbb 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -6446,11 +6446,11 @@ def get_standard_logging_object_payload( cost_breakdown=request_cost_breakdown, autorouter_savings=autorouter_savings, autorouter_savings_estimate=( - { + { # mutable-ok: spend-log JSON serialization requires plain mappings "version": 3, "status": "unknown", "reason": "pending_projection", - } # mutable-ok: spend-log JSON serialization requires plain mappings + } if captured_baseline is not None else ( { # mutable-ok: spend-log JSON serialization requires plain mappings diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 03b562c3014..875c5274c9a 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -687,9 +687,7 @@ class AnthropicMessagesHandler(BaseTranslation): return data - def _hoisted_top_level_system_message( - self, data: dict - ) -> AllMessageValues | None: # mutable-ok: API message payload + def _hoisted_top_level_system_message(self, data: Mapping[str, object]) -> AllMessageValues | None: """Return the system message produced by translating the top-level prompt.""" system: Final = data.get("system") if not system: diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 2ae285d6eef..da29364886e 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -4051,7 +4051,7 @@ async def get_org_object_for_request( ) except OrganizationNotFoundError: return None - except Exception as e: # noqa: BLE001 # only a DB outage may fail auth here, anything else degrades to no org limits + except Exception as e: if not PrismaDBExceptionHandler.is_database_service_unavailable_error_in_chain(e): verbose_proxy_logger.debug("org lookup failed, continuing without org limits", exc_info=True) return None diff --git a/litellm/proxy/db/baseline_accounting.py b/litellm/proxy/db/baseline_accounting.py index 8622cb9e481..4894f2e7583 100644 --- a/litellm/proxy/db/baseline_accounting.py +++ b/litellm/proxy/db/baseline_accounting.py @@ -470,12 +470,12 @@ class BaselineAccountingStore: async def _pages( self, db: SupportsRawQueries, scope: str, after_revision: int, withdraw_from: float | None = None ) -> AsyncIterator[tuple[_StoredRecord, ...]]: - cursor: float | None = None + cursor: float | None = None # rebind-ok: keyset pagination advances after each complete timestamp group while page := _RECORDS.validate_python( tuple(await db.query_raw(_READ_PAGE, scope, after_revision, cursor, _PAGE_TIMESTAMPS, withdraw_from)) ): yield page - cursor = page[-1].started_at # rebind-ok: keyset pagination advances after each complete timestamp group + cursor = page[-1].started_at async def _withdraw(self, db: SupportsRawQueries, scope: str, started_at: float) -> None: async for page in self._pages(db, scope, 0, withdraw_from=started_at): @@ -607,13 +607,13 @@ async def flush_baseline_accounting(client: PrismaClient) -> None: store: Final = BaselineAccountingStore.for_client(client) async with client.baseline_accounting_lock: batch: Final = tuple(client.baseline_accounting_transactions[:32]) - client.baseline_accounting_transactions = client.baseline_accounting_transactions[ + client.baseline_accounting_transactions = client.baseline_accounting_transactions[ # rebind-ok: drain under lock 32: - ] # rebind-ok: drain under lock + ] more_queued: Final = bool(client.baseline_accounting_transactions) try: remaining: Final = await asyncio.wait_for(_flush_records(store, batch), timeout=5) - except (Exception, asyncio.CancelledError) as error: # noqa: BLE001 # unknown acknowledgements can be replayed safely + except (Exception, asyncio.CancelledError) as error: async with client.baseline_accounting_lock: client.baseline_accounting_transactions.extend(batch) if isinstance(error, asyncio.CancelledError): diff --git a/litellm/proxy/hooks/autorouter_baseline_cache.py b/litellm/proxy/hooks/autorouter_baseline_cache.py index 8cea7d0e364..a21a250c6a4 100644 --- a/litellm/proxy/hooks/autorouter_baseline_cache.py +++ b/litellm/proxy/hooks/autorouter_baseline_cache.py @@ -230,12 +230,12 @@ class AutoRouterBaselineCache(CustomLogger): async def invalidate_baseline_cache(logging_obj: Logging, reason: str, *, completed: bool = False) -> None: context: Final = logging_obj.baseline_cache_context if context is not None: - logging_obj.baseline_cache_context = replace( + logging_obj.baseline_cache_context = replace( # rebind-ok: request-owned retry marker context, invalidated=reason - ) # rebind-ok: request-owned retry marker - logging_obj.baseline_observation = context.capture.model_copy( + ) + logging_obj.baseline_observation = context.capture.model_copy( # rebind-ok: uncertain capture for failure logs update=MappingProxyType( - { # rebind-ok: capture uncertainty for failure logging + { "observation": context.capture.observation.model_copy( update=MappingProxyType( { From c54de1630ee36cd9a547350c8b9aa66e43b472ef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:58:17 +0000 Subject: [PATCH 15/17] refactor: type fresh getattr, Any, and bare dict debt from 2026-09-22 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../mavvrik_focus/mavvrik_focus_logger.py | 24 ++++--------------- litellm/litellm_core_utils/core_helpers.py | 2 +- .../llm_cost_calc/zero_cost_diagnostic.py | 11 ++++++--- .../embedding/transformation.py | 2 +- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py b/litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py index 7e3c4cc3ce8..4c2f75bb4d7 100644 --- a/litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py +++ b/litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py @@ -21,7 +21,7 @@ from __future__ import annotations import os from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Any, Final, Protocol +from typing import TYPE_CHECKING, Any, Final import litellm from litellm._logging import verbose_proxy_logger @@ -35,17 +35,6 @@ else: AsyncIOScheduler = Any -class _PodLockManager(Protocol): - """The subset of PodLockManager this logger drives to serialize the export across pods.""" - - @property - def redis_cache(self) -> object: ... - - async def acquire_lock(self, cronjob_id: str) -> bool | None: ... - - async def release_lock(self, cronjob_id: str) -> None: ... - - def _parse_metrics_marker( marker: object | None, ) -> datetime | None: @@ -237,13 +226,10 @@ class MavvrikFocusLogger(FocusLogger): """Scheduler entry point — uses Mavvrik-specific pod-lock key.""" from litellm.proxy.proxy_server import proxy_logging_obj # noqa: PLC0415 - pod_lock_manager: _PodLockManager | None = None - if proxy_logging_obj is not None: - writer: Final[object] = getattr(proxy_logging_obj, "db_spend_update_writer", None) - if writer is not None: - pod_lock_manager = getattr(writer, "pod_lock_manager", None) - - if pod_lock_manager and pod_lock_manager.redis_cache: + pod_lock_manager: Final = ( + proxy_logging_obj.db_spend_update_writer.pod_lock_manager if proxy_logging_obj is not None else None + ) + if pod_lock_manager is not None and pod_lock_manager.redis_cache: acquired: Final = await pod_lock_manager.acquire_lock(cronjob_id=MAVVRIK_FOCUS_EXPORT_JOB_NAME) if not acquired: verbose_proxy_logger.debug("Mavvrik FOCUS export: unable to acquire pod lock") diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 5bcde688521..45a9db1f902 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -365,7 +365,7 @@ def _budget_reservation_on_auth_object(user_api_key_auth: object) -> object: return getattr(user_api_key_auth, "budget_reservation", None) -def budget_reservation_from_metadata(metadata: Mapping[str, object]) -> dict | None: +def budget_reservation_from_metadata(metadata: Mapping[str, object]) -> dict[str, object] | None: stamped: Final = metadata.get("user_api_key_budget_reservation") if isinstance(stamped, dict): return stamped diff --git a/litellm/litellm_core_utils/llm_cost_calc/zero_cost_diagnostic.py b/litellm/litellm_core_utils/llm_cost_calc/zero_cost_diagnostic.py index 6331d815bdc..9688b511ea8 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/zero_cost_diagnostic.py +++ b/litellm/litellm_core_utils/llm_cost_calc/zero_cost_diagnostic.py @@ -5,7 +5,12 @@ from typing import Final from pydantic import TypeAdapter, ValidationError from typing_extensions import assert_never -from litellm.types.utils import StandardLoggingZeroCostDiagnostic, Usage +from litellm.types.utils import ( + CompletionTokensDetailsWrapper, + PromptTokensDetailsWrapper, + StandardLoggingZeroCostDiagnostic, + Usage, +) ZERO_COST_COUNTER_NAME: Final = "litellm_zero_cost_requests_total" @@ -18,8 +23,8 @@ _NESTED_PRICING: Final = TypeAdapter(Mapping[str, object] | tuple[object, ...]) _MAX_PRICING_DEPTH: Final = 4 -def _audio_tokens(details: object) -> int: - audio_tokens: Final = getattr(details, "audio_tokens", None) +def _audio_tokens(details: PromptTokensDetailsWrapper | CompletionTokensDetailsWrapper | None) -> int: + audio_tokens: Final = details.audio_tokens if details is not None else None return audio_tokens if isinstance(audio_tokens, int) and audio_tokens > 0 else 0 diff --git a/litellm/llms/vercel_ai_gateway/embedding/transformation.py b/litellm/llms/vercel_ai_gateway/embedding/transformation.py index fc9c6bcc19f..bb07a945f7e 100644 --- a/litellm/llms/vercel_ai_gateway/embedding/transformation.py +++ b/litellm/llms/vercel_ai_gateway/embedding/transformation.py @@ -161,7 +161,7 @@ class VercelAIGatewayEmbeddingConfig(BaseEmbeddingConfig): optional_params[param] = value return optional_params - def get_error_class(self, error_message: str, status_code: int, headers: Any) -> BaseLLMException: + def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException: """ Get the error class for Vercel AI Gateway errors. """ From 28a1ac20d362a1948e9e2003df52511ccdf1dcc8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:25:56 +0000 Subject: [PATCH 16/17] fix(mcp): keep the string guard on tools/list next_cursor Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/experimental_mcp_client/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/experimental_mcp_client/tools.py b/litellm/experimental_mcp_client/tools.py index 8e640f8b837..df644fd7f4a 100644 --- a/litellm/experimental_mcp_client/tools.py +++ b/litellm/experimental_mcp_client/tools.py @@ -130,7 +130,7 @@ async def list_tools_with_pagination( tools.extend(result.tools) next_cursor = result.next_cursor - if not next_cursor: + if not isinstance(next_cursor, str) or not next_cursor: return tools if next_cursor in seen_cursors: verbose_logger.warning( From 018a6a90ad18a82f681bfa93f9edc1b59f85843e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:27:40 +0000 Subject: [PATCH 17/17] refactor(vercel_ai_gateway): type the embedding error headers dict Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/vercel_ai_gateway/embedding/transformation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vercel_ai_gateway/embedding/transformation.py b/litellm/llms/vercel_ai_gateway/embedding/transformation.py index bb07a945f7e..8a672bf8991 100644 --- a/litellm/llms/vercel_ai_gateway/embedding/transformation.py +++ b/litellm/llms/vercel_ai_gateway/embedding/transformation.py @@ -161,7 +161,9 @@ class VercelAIGatewayEmbeddingConfig(BaseEmbeddingConfig): optional_params[param] = value return optional_params - def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException: + def get_error_class( + self, error_message: str, status_code: int, headers: dict[str, str] | httpx.Headers + ) -> BaseLLMException: """ Get the error class for Vercel AI Gateway errors. """