diff --git a/CLAUDE.md b/CLAUDE.md index 41678432989..12c9e803cd4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ Never edit or commit `ruff-strict-budget.json`, `type-discipline-budget.json`, ` If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and pushing `reportAny` / `reportExplicitAny` closer to their basedpyright ceilings, just validate it in the caller with Pydantic (a model or `TypeAdapter` that returns the typed thing or raises will do) and then pass the now typed variable in -If you get an LIT001 or LIT002 fail, refactor the code to follow functional programming best practices rather than introducing mutable data structures. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `MappingProxyType()` / `frozenset()` instead of seeding an empty `list`/`dict`/`set` and mutating it over time. Ideally, `# mutable-ok` is never used; reach for it only as a genuine last resort when an immutable rewrite is truly impossible, and always pair it with a real reason +If you get an LIT001 fail, refactor the code to follow functional programming best practices rather than introducing mutable sequences or sets. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `frozenset()` instead of seeding an empty `list`/`set` and appending to it over time. Ideally, `# mutable-ok` is never used; reach for it only as a last resort when an immutable rewrite is impossible, and always pair it with a real reason. Plain `dict` is allowed: most of the Python ecosystem takes and returns dicts, and converting to `MappingProxyType` at every boundary costs more than it protects. Never deep copy a value just to hand out an immutable view; a defensive copy of a self-referential or large object is worse than the mutation it guards against Every lint or type suppression must name the exact rule inside brackets and carry a reason comment, e.g. `# pyright: ignore[reportArgumentType] # stubs lack async overload` or `# noqa: TID251 # `. `# type: ignore` is banned (LIT009): pyrightconfig.json sets `enableTypeIgnoreComments` to false, so it silently does nothing @@ -91,7 +91,7 @@ Follow these coding conventions for new/updated code (a three-line fix in a lega - Composition over inheritance - Never-nester: early returns over deep nesting - Don't throw; model failures as values (One function (e.g., raise_public) maps error union to existing public exception contracts via exhaustive match + assert_never) -- No mutation; don't reassign variables, global or local. Instead of mutable lists and dicts, prefer tuples, frozen dataclasses (with slots=True), `MappingProxyType`, etc. +- No mutation; don't reassign variables, global or local. Prefer tuples over lists and frozensets over sets. For structured records prefer frozen dataclasses (with slots=True) or `ReadOnly` TypedDicts over dicts. A `dict` is fine for a mapping with arbitrary keys, especially at a library boundary; `MappingProxyType` is optional there, and never worth a copy - Annotate every variable with `: Final` (LIT010). Unpacking and walrus targets cannot carry the annotation, so they are implicitly final. Don't rebind them. Never rebind or mutate function parameters (LIT011); `self`/`cls` attribute stores are the exception. If rebinding or in-place mutation is truly unavoidable, suppress with `# rebind-ok: ` - Qualify every TypedDict field with `ReadOnly[...]` (LIT012), which nests freely with `Required` / `NotRequired` / `Annotated` in any order. If making the key writable is truly unavoidable, suppress with `# writable-ok: ` - Use dependency injection diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py b/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py index f0f85178672..8b17cf13cc4 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py @@ -616,7 +616,7 @@ class _ENTERPRISE_SecretDetection(CustomGuardrail): data["prompt"] = self.redact_text(prompt, source="prompt") return 1 if isinstance(prompt, list): - data["prompt"] = [ # mutable-ok: data["prompt"] is a list on the wire + data["prompt"] = [ self.redact_text(item, source="prompt") if isinstance(item, str) and item else item diff --git a/litellm/_redis.py b/litellm/_redis.py index c5acdcb038b..edaa0bb9f25 100644 --- a/litellm/_redis.py +++ b/litellm/_redis.py @@ -203,7 +203,7 @@ def _str_to_bool(value: str) -> bool: def _coerce_redis_kwargs_types( redis_kwargs: Mapping[str, object], client: type | tuple[type, ...] = redis.Redis, -) -> dict[str, object]: # mutable-ok: a caller mutates the returned kwargs before constructing its client +) -> dict[str, object]: """Coerces string values to the numeric/boolean type ``client``'s constructor declares for that parameter. ``client`` may be a tuple of client classes; a parameter's type is taken from the first signature that declares it, which @@ -233,7 +233,7 @@ def _coerce_redis_kwargs_types( "socket_keepalive": bool, } ) - result: Final = dict(redis_kwargs) # mutable-ok: per-key try/except coercion below needs to drop individual keys + result: Final = dict(redis_kwargs) for key, value in redis_kwargs.items(): if not isinstance(value, str): continue @@ -803,7 +803,7 @@ def _async_auth_kwargs(redis_kwargs: dict) -> dict: superseded: Final = frozenset({"redis_connect_func", "username", "password"}) kept: Final = ((k, v) for k, v in redis_kwargs.items() if k not in superseded) - return dict(kept, credential_provider=credential_provider) # mutable-ok: the branches below mutate these kwargs + return dict(kept, credential_provider=credential_provider) def get_redis_client(**env_overrides): diff --git a/litellm/batches/batch_utils.py b/litellm/batches/batch_utils.py index 87f8fd3946e..10ce7e256cf 100644 --- a/litellm/batches/batch_utils.py +++ b/litellm/batches/batch_utils.py @@ -110,7 +110,7 @@ async def _handle_completed_batch( return BatchCostUsageResult( cost=0.0, usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), - models=[], # mutable-ok: no output file means no model was ever priced; BatchCostUsageResult.models requires list[str] + models=[], successful_requests=0, failed_requests=await count_error_file_failed_requests( batch, custom_llm_provider=custom_llm_provider, litellm_params=litellm_params diff --git a/litellm/caching/dual_cache.py b/litellm/caching/dual_cache.py index 81e2af45686..04bf892e606 100644 --- a/litellm/caching/dual_cache.py +++ b/litellm/caching/dual_cache.py @@ -222,9 +222,7 @@ class DualCache(BaseCache): if value is not None: self.in_memory_cache.set_cache(key, value, **self._backfill_kwargs(kwargs)) - return list( # mutable-ok: public list contract - redis_result.get(key) if value is None else value for key, value in zip(keys, result) - ) + return list(redis_result.get(key) if value is None else value for key, value in zip(keys, result)) except Exception as e: log_redis_failure( verbose_logger, logging.ERROR, "LiteLLM Cache: exception in batch_get_cache", e, with_traceback=True diff --git a/litellm/caching/evicted_client_closer.py b/litellm/caching/evicted_client_closer.py index eee7e2ea289..16fd3ce12b5 100644 --- a/litellm/caching/evicted_client_closer.py +++ b/litellm/caching/evicted_client_closer.py @@ -236,7 +236,7 @@ class EvictedClientCloser: the front rather than having to be searched for. """ with self._queue_lock: - bucket: Final = self._buckets.setdefault(_bucket_key(pending), deque()) # mutable-ok: FIFO by design + bucket: Final = self._buckets.setdefault(_bucket_key(pending), deque()) while bucket and bucket[0].client_ref() is None: bucket.popleft() self._pending_count -= 1 diff --git a/litellm/caching/redis_cluster_node_isolation.py b/litellm/caching/redis_cluster_node_isolation.py index 0035801018b..00d6ceba20b 100644 --- a/litellm/caching/redis_cluster_node_isolation.py +++ b/litellm/caching/redis_cluster_node_isolation.py @@ -142,9 +142,7 @@ def get_litellm_async_redis_cluster_class( # noqa: C901 # supports redis-py ve self._litellm_reinit_requests = 0 self._litellm_tolerated_timeouts = 0 super().__init__(*args, **kwargs) - self._litellm_consecutive_timeouts: dict[ # mutable-ok: per-node counter updated on the command hot path - str, int - ] = {} + self._litellm_consecutive_timeouts: dict[str, int] = {} @property def _initialize(self) -> bool: diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 5a6debc4af5..75c4851f298 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -124,13 +124,13 @@ def _reasoning_input_items(msg: "AllMessageValues") -> list[dict[str, object]]: blocks are the fallback for turns that arrived over another API surface. """ items: Final = _get_reasoning_items(msg) - stored: Final = [_reasoning_item_to_response_input(item) for item in items] # mutable-ok: API message payload + stored: Final = [_reasoning_item_to_response_input(item) for item in items] if stored: return stored raw_blocks: Final = msg.get("thinking_blocks") or () blocks: Final = cast("Iterable[ChatCompletionThinkingBlock]", raw_blocks) # cast-ok: untyped client json replayed: Final = responses_reasoning_items_from_thinking_blocks(blocks) - return [dict(item) for item in replayed] # mutable-ok: API message payload + return [dict(item) for item in replayed] def _build_reasoning_item( @@ -439,7 +439,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): input_items.extend(_reasoning_input_items(msg)) if content: input_items.append( - { # mutable-ok: API message payload + { "type": "message", "role": "assistant", "content": self._convert_content_to_responses_format(content, "assistant"), @@ -473,7 +473,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): if role == "assistant": input_items.extend(_reasoning_input_items(msg)) input_items.append( - { # mutable-ok: API message payload + { "type": "message", "role": role, "content": self._convert_content_to_responses_format(content, cast(str, role)), @@ -1311,7 +1311,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): ): super().__init__(streaming_response, sync_stream, json_mode) self._chat_completion_id: str | None = None - self._tool_call_index_map: dict[int, int] = {} # mutable-ok: per-stream accumulator state + self._tool_call_index_map: dict[int, int] = {} def _handle_string_chunk( self, str_line: Union[str, "BaseModel"] @@ -1332,7 +1332,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): @staticmethod def _sequential_tool_call_index( - tool_call_index_map: dict[int, int] | None, # mutable-ok: per-stream state, remapped in place + tool_call_index_map: dict[int, int] | None, output_index: int, ) -> int: """Chat-completions tool_call indices must be 0-based and sequential, but @@ -1345,13 +1345,13 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): if tool_call_index_map is None: return output_index if output_index not in tool_call_index_map: - tool_call_index_map[output_index] = len(tool_call_index_map) # mutable-ok: per-stream accumulator state + tool_call_index_map[output_index] = len(tool_call_index_map) return tool_call_index_map[output_index] @staticmethod def translate_responses_chunk_to_openai_stream( parsed_chunk: dict | BaseModel, - tool_call_index_map: dict[int, int] | None = None, # mutable-ok: per-stream state, remapped in place + tool_call_index_map: dict[int, int] | None = None, ) -> "ModelResponseStream": """ Translate a Responses API streaming chunk to OpenAI chat completion streaming format. @@ -1482,7 +1482,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): # tool call; per-stream callers already received it via # output_item.added and the argument delta events return ModelResponseStream( - choices=[ # mutable-ok: ModelResponseStream coerces only list choices + choices=[ StreamingChoices( index=0, delta=Delta( @@ -1587,7 +1587,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): ) ], usage=usage, - provider_specific_fields=dict(provider_metadata) or None, # mutable-ok: field is typed dict + provider_specific_fields=dict(provider_metadata) or None, ) else: pass diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index f5319776213..e16568df448 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -2552,9 +2552,7 @@ class ResponsesWebSocketTokenUsageProcessor(BaseTokenUsageProcessor): collected_usage_objects: Final = ResponsesWebSocketTokenUsageProcessor.collect_usage_from_responses_ws_results( results ) - return ResponsesWebSocketTokenUsageProcessor.combine_usage_objects( - list(collected_usage_objects) # mutable-ok: combine_usage_objects requires a list parameter - ) + return ResponsesWebSocketTokenUsageProcessor.combine_usage_objects(list(collected_usage_objects)) _TRANSCRIPTION_COMPLETED_EVENT_TYPE: Final = "conversation.item.input_audio_transcription.completed" diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index ee01a53ecb3..56f6be5afab 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -653,7 +653,7 @@ class MCPClient: follow_redirects=True, event_hooks=MappingProxyType( {"response": [capture_upstream_error_response], "request": [guard] if guard else []} - ), # mutable-ok: httpx types require lists of hooks + ), ) return factory diff --git a/litellm/experimental_mcp_client/tools.py b/litellm/experimental_mcp_client/tools.py index 51d2139ef3b..a1fb09f0cf2 100644 --- a/litellm/experimental_mcp_client/tools.py +++ b/litellm/experimental_mcp_client/tools.py @@ -171,9 +171,7 @@ async def load_mcp_tools( """ tools: Final = await list_tools_with_pagination(session) if format == "openai": - return [ # mutable-ok: public API returns a list - transform_mcp_tool_to_openai_tool(mcp_tool=tool) for tool in tools - ] + return [transform_mcp_tool_to_openai_tool(mcp_tool=tool) for tool in tools] return tools diff --git a/litellm/integrations/SlackAlerting/slack_alerting.py b/litellm/integrations/SlackAlerting/slack_alerting.py index caac8e888fd..fe08543ca5f 100644 --- a/litellm/integrations/SlackAlerting/slack_alerting.py +++ b/litellm/integrations/SlackAlerting/slack_alerting.py @@ -1115,7 +1115,7 @@ Model Info: message=message, level=level, alert_type=AlertType.model_deprecation_warnings, - alerting_metadata={ # mutable-ok: send_alert takes a dict payload + alerting_metadata={ "deprecated_count": len(snapshot.deprecated), "imminent_count": len(snapshot.imminent), "upcoming_count": len(snapshot.upcoming), @@ -1229,8 +1229,8 @@ Model Info: try: existing_invitations: Final = TypeAdapter(list[InvitationModel]).validate_python( await InvitationLinkRepository(prisma_client).table.find_many( # pyright: ignore[reportAny] # untyped prisma boundary (any-ok), result validated by TypeAdapter - where={"user_id": recipient_user_id}, # mutable-ok: prisma find_many requires a dict where filter - order={"created_at": "desc"}, # mutable-ok: prisma find_many requires a dict order arg + where={"user_id": recipient_user_id}, + order={"created_at": "desc"}, ), from_attributes=True, ) @@ -1996,7 +1996,7 @@ Model Info: message="\n\n".join(event.message for event in typed_events), level="High", alert_type=alert_type, - alerting_metadata={}, # mutable-ok: send_alert takes a dict payload + alerting_metadata={}, ) for event in typed_events: await self.internal_usage_cache.async_set_cache( diff --git a/litellm/integrations/azure_sentinel/azure_sentinel.py b/litellm/integrations/azure_sentinel/azure_sentinel.py index db5f790615f..84dfc770e8f 100644 --- a/litellm/integrations/azure_sentinel/azure_sentinel.py +++ b/litellm/integrations/azure_sentinel/azure_sentinel.py @@ -337,7 +337,7 @@ class AzureSentinelLogger(CustomBatchLogger): Raises a NON Blocking verbose_logger.exception if an error occurs """ batch_to_send: Final = tuple(self.log_queue) - self.log_queue = [] # mutable-ok: queue ownership is detached before the async send + self.log_queue = [] try: undelivered: Final = await self._async_send_batch_to_api( log_queue=batch_to_send, @@ -360,7 +360,7 @@ class AzureSentinelLogger(CustomBatchLogger): Sends the batch of audit logs to Azure Monitor Logs Ingestion API """ batch_to_send: Final = tuple(self.audit_log_queue) - self.audit_log_queue = [] # mutable-ok: queue ownership is detached before the async send + self.audit_log_queue = [] try: undelivered: Final = await self._async_send_batch_to_api( log_queue=batch_to_send, @@ -384,7 +384,7 @@ class AzureSentinelLogger(CustomBatchLogger): queue: list[_QueuedPayload], log_type: str, ) -> list[_QueuedPayload]: - merged: Final = [*undelivered, *queue] # mutable-ok: queue trimming returns a mutable logger queue + merged: Final = [*undelivered, *queue] overflow: Final = len(merged) - self.max_queue_size if overflow <= 0: return merged diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 39adea30828..4eb347c1a51 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -310,7 +310,7 @@ class CustomGuardrail(CustomLogger): def inject_advisory_message( self, - data: dict[str, Any], # mutable-ok: caller's dict is mutated in place, matching mark_pre_call_hook_ran + data: dict[str, Any], message: str, ) -> bool: """ @@ -340,7 +340,7 @@ class CustomGuardrail(CustomLogger): land and degrade to blocking instead of silently letting the flagged request through unmodified. """ - advisory_message: Final = {"role": "system", "content": message} # mutable-ok: plain dict for live request + advisory_message: Final = {"role": "system", "content": message} existing_messages: Final = data.get("messages") existing_input: Final = data.get("input") existing_instructions: Final = data.get("instructions") @@ -351,7 +351,7 @@ class CustomGuardrail(CustomLogger): # model to disregard a trailing warning. Prefer it over "input" # whenever present. if isinstance(existing_messages, list): - messages_with_instructions_note: Final = [ # mutable-ok: fresh list + messages_with_instructions_note: Final = [ *existing_messages, advisory_message, ] @@ -363,7 +363,7 @@ class CustomGuardrail(CustomLogger): # real, read field (e.g. a chat-completions call carrying a stray # "input"), so write to both when both are present. if isinstance(existing_messages, list): - messages_with_input_note: Final = [*existing_messages, advisory_message] # mutable-ok: fresh list + messages_with_input_note: Final = [*existing_messages, advisory_message] data["messages"] = messages_with_input_note # rebind-ok: mutates caller's dict by design # The Responses API reads "input", not "messages" -- appending only to # "messages" would leave the advisory unreachable for that endpoint. @@ -377,10 +377,10 @@ class CustomGuardrail(CustomLogger): # non-delivery so the caller degrades to blocking. return False if isinstance(existing_messages, list): - messages_without_input_note: Final = [*existing_messages, advisory_message] # mutable-ok: fresh list + messages_without_input_note: Final = [*existing_messages, advisory_message] data["messages"] = messages_without_input_note # rebind-ok: mutates caller's dict by design return True - sole_message: Final = [advisory_message] # mutable-ok: plain list for the live JSON request + sole_message: Final = [advisory_message] data["messages"] = sole_message # rebind-ok: mutates caller's dict by design return True @@ -877,10 +877,10 @@ class CustomGuardrail(CustomLogger): async def async_logging_hook( self, - kwargs: dict, # mutable-ok: CustomLogger.async_logging_hook contract + kwargs: dict, result: object, call_type: str, - ) -> tuple[dict, object]: # mutable-ok: CustomLogger.async_logging_hook contract + ) -> tuple[dict, object]: """logging_only: run apply_guardrail on copies of the logged request/response and record the verdict.""" from litellm.llms import get_guardrail_translation_mapping @@ -918,10 +918,10 @@ class CustomGuardrail(CustomLogger): async def _scan_logged_call( self, - kwargs: dict, # mutable-ok: CustomLogger.async_logging_hook contract + kwargs: dict, result: object, translation: "BaseTranslation", - scratch_metadata: dict, # mutable-ok: apply_guardrail records its verdict into request metadata + scratch_metadata: dict, ) -> None: optional_params: Final = kwargs.get("optional_params") or {} scratch_input: Final = copy.deepcopy(kwargs.get("messages") or kwargs.get("input")) @@ -1482,7 +1482,7 @@ def _original_inputs_for( kwargs: Mapping[str, object], request_data: Mapping[str, object], event_type: GuardrailEventHooks | None, -) -> dict | None: # mutable-ok: matches _process_response(original_inputs=) signature +) -> dict | None: """Baseline the hook's return value is compared against to decide "allow" vs "mask". Hooks may edit their argument in place and return it, so the baseline is always a deep diff --git a/litellm/integrations/datadog/datadog.py b/litellm/integrations/datadog/datadog.py index 77b12d1e3fa..06b2975725d 100644 --- a/litellm/integrations/datadog/datadog.py +++ b/litellm/integrations/datadog/datadog.py @@ -397,7 +397,7 @@ class DataDogLogger( verbose_logger.debug("[DATADOG MOCK] Batch of %s events successfully mocked", len(batch_to_send)) except BatchSendCancelled as cancelled: - self.log_queue = list(cancelled.undelivered) + self.log_queue # mutable-ok: logger queue remains appendable + self.log_queue = list(cancelled.undelivered) + self.log_queue raise asyncio.CancelledError() from cancelled except Exception as e: self.log_queue = batch_to_send + self.log_queue @@ -425,7 +425,7 @@ class DataDogLogger( drop_error_message=DD_ERRORS.DATADOG_413_ERROR.value, non_success_handler=requeue_after_http_error, ) - return list(undelivered) # mutable-ok: caller prepends records to the logger queue + return list(undelivered) @staticmethod def _exceeds_intake_limits(chunk: Sequence[DatadogPayload]) -> bool: diff --git a/litellm/integrations/datadog/datadog_llm_obs.py b/litellm/integrations/datadog/datadog_llm_obs.py index c64a12c6d75..c03dac7e8d8 100644 --- a/litellm/integrations/datadog/datadog_llm_obs.py +++ b/litellm/integrations/datadog/datadog_llm_obs.py @@ -131,7 +131,7 @@ def _guardrail_entry_without_prompt_carriers(entry: Mapping[str, object]) -> Map Built as an allow-list rather than a deny-list: a key neither set classifies is dropped, so a guardrail that records its own extra detail cannot put the caller's prompt on a redacted span. """ - return { # mutable-ok: a fresh record built per entry, handed straight to the span serializer + return { field: REDACTED_BY_LITELM_STRING if field in PROMPT_CARRYING_GUARDRAIL_FIELDS else value for field, value in entry.items() if field in _CLASSIFIED_GUARDRAIL_FIELDS diff --git a/litellm/integrations/newrelic/newrelic_metrics.py b/litellm/integrations/newrelic/newrelic_metrics.py index da952b78d3f..e1350e606ed 100644 --- a/litellm/integrations/newrelic/newrelic_metrics.py +++ b/litellm/integrations/newrelic/newrelic_metrics.py @@ -109,7 +109,7 @@ def _metric_record_from_payload(standard_logging_object: StandardLoggingPayload) def _bucket_metrics(bucket_records: tuple[NewRelicMetricRecord, ...]) -> tuple[NewRelicMetric, ...]: first: Final = bucket_records[0] - attributes: Final[Mapping[str, str]] = { # mutable-ok: JSON leaf; safe_dumps stringifies MappingProxyType + attributes: Final[Mapping[str, str]] = { key: value[:NEWRELIC_METRIC_ATTRIBUTE_MAX_LEN] for key, value in ( ("team_id", first.team_id), @@ -150,7 +150,7 @@ def _team_budget_gauges(record: NewRelicMetricRecord) -> tuple[NewRelicMetric, . team_max_budget: Final = record.team_max_budget if team_max_budget is None: return () - attributes: Final[Mapping[str, str]] = { # mutable-ok: JSON leaf; safe_dumps stringifies MappingProxyType + attributes: Final[Mapping[str, str]] = { key: value[:NEWRELIC_METRIC_ATTRIBUTE_MAX_LEN] for key, value in (("team_id", record.team_id), ("team_alias", record.team_alias)) if value @@ -265,7 +265,7 @@ class NewRelicMetricsLogger(CustomBatchLogger): dropped, NEWRELIC_METRICS_MAX_DRAIN_PASSES, ) - self.log_queue[:] = list(survivors) # mutable-ok: leave late arrivals for the next serialized drain + self.log_queue[:] = list(survivors) async def _drain_flush_once(self) -> None: """Attempt every queued record once, in ``batch_size`` chunks, without diff --git a/litellm/integrations/otel/model/request_io.py b/litellm/integrations/otel/model/request_io.py index 4e80fb91993..a315dadba2d 100644 --- a/litellm/integrations/otel/model/request_io.py +++ b/litellm/integrations/otel/model/request_io.py @@ -79,7 +79,7 @@ def stream_output(chunks: Sequence[object], data: Mapping[str, object]) -> str | def _assembled_chat_stream(chunks: Sequence[object], data: Mapping[str, object]) -> object: try: return litellm.stream_chunk_builder( # pyright: ignore[reportUnknownMemberType] # upstream types chunks as a bare list - chunks=list(chunks), # mutable-ok: stream_chunk_builder takes a list + chunks=list(chunks), messages=_MESSAGES.validate_python(data.get("messages")), ) except (litellm.APIError, ValidationError): diff --git a/litellm/integrations/otel/plumbing/otlp_json.py b/litellm/integrations/otel/plumbing/otlp_json.py index b4b659f1e01..6ee1436635c 100644 --- a/litellm/integrations/otel/plumbing/otlp_json.py +++ b/litellm/integrations/otel/plumbing/otlp_json.py @@ -62,7 +62,7 @@ def encode_spans_json(spans: Sequence[ReadableSpan]) -> bytes: class OTLPJsonSpanExporter(OTLPSpanExporter): - def __init__(self, endpoint: str | None, headers: dict[str, str]) -> None: # mutable-ok: SDK __init__ takes Dict + def __init__(self, endpoint: str | None, headers: dict[str, str]) -> None: super().__init__(endpoint=endpoint, headers=headers) self._session.headers["Content-Type"] = JSON_CONTENT_TYPE diff --git a/litellm/integrations/otel/plumbing/providers.py b/litellm/integrations/otel/plumbing/providers.py index 81f22c8c642..3dc4b611047 100644 --- a/litellm/integrations/otel/plumbing/providers.py +++ b/litellm/integrations/otel/plumbing/providers.py @@ -516,9 +516,9 @@ class TenantFanOutSpanProcessor(SpanProcessor): self._lock: Final = threading.Condition() self._closed = False # guarded by ``_lock``: an unlocked read races the teardown it gates self._build: Final = processor_factory if processor_factory is not None else _destination_processor - self._processors: OrderedDict[object, SpanProcessor] = OrderedDict() # mutable-ok: bounded LRU - self._retired: OrderedDict[int, SpanProcessor] = OrderedDict() # mutable-ok: drains as exports finish - self._exporting: dict[int, int] = {} # mutable-ok: per-processor in-flight export count + self._processors: OrderedDict[object, SpanProcessor] = OrderedDict() + self._retired: OrderedDict[int, SpanProcessor] = OrderedDict() + self._exporting: dict[int, int] = {} self._drain: Final = drain_pool if drain_pool is not None else _DrainPool(capacity=pending_drains) def on_start(self, span: SDKSpan, parent_context: Context | None = None) -> None: @@ -576,9 +576,7 @@ class TenantFanOutSpanProcessor(SpanProcessor): live: Final = tuple((id(p), p) for p in (*self._processors.values(), *self._retired.values())) closing: Final = tuple(p for ident, p in live if ident not in self._exporting) self._processors.clear() - self._retired = OrderedDict( # mutable-ok: the same bounded map, keeping only what is still exporting - (ident, p) for ident, p in live if ident in self._exporting - ) + self._retired = OrderedDict((ident, p) for ident, p in live if ident in self._exporting) for processor in closing: self._drain.submit(processor) self._drain.close(timeout=max(0.0, deadline - time.monotonic())) diff --git a/litellm/integrations/otel/plumbing/routing.py b/litellm/integrations/otel/plumbing/routing.py index f78d18d943c..be428a3dd8f 100644 --- a/litellm/integrations/otel/plumbing/routing.py +++ b/litellm/integrations/otel/plumbing/routing.py @@ -171,12 +171,10 @@ class TenantTracerCache: # thread-pool workers concurrently with the event loop, so cache # updates, span counts, and retirement must be atomic. self._lock: Final = threading.Lock() - self._providers: OrderedDict[_RouteKey, TracerProvider] = ( - OrderedDict() # mutable-ok: bounded LRU; eviction needs in-place ordered mutation - ) - self._open_span_counts: dict[TracerProvider, int] = {} # mutable-ok: live refcount state + self._providers: OrderedDict[_RouteKey, TracerProvider] = OrderedDict() + self._open_span_counts: dict[TracerProvider, int] = {} # Oldest-first so an overflow of draining providers sheds the stalest. - self._retired: OrderedDict[TracerProvider, None] = OrderedDict() # mutable-ok: draining evicted providers + self._retired: OrderedDict[TracerProvider, None] = OrderedDict() # An owned exporter is routable only when its kind actually resolves to a # header-carrying OTLP exporter. A denylist would accept a typo'd or # unavailable kind, which ``_exporter_from_spec`` falls back to a @@ -395,7 +393,7 @@ class TenantTracerCache: if project_headers and kind not in _GRPC_KINDS else base ) - update: Final = { # mutable-ok: model_copy(update=...) requires a plain dict + update: Final = { field: value for field, value in (("headers", routed), ("endpoint", endpoint)) if (field == "headers" and routed != spec.headers) diff --git a/litellm/integrations/otel/presets/destinations.py b/litellm/integrations/otel/presets/destinations.py index 2bf9bfa5261..46163d922e8 100644 --- a/litellm/integrations/otel/presets/destinations.py +++ b/litellm/integrations/otel/presets/destinations.py @@ -145,7 +145,7 @@ def destination_for( endpoint, protocol = resolved return OtelDestination( endpoint=endpoint, - headers=MappingProxyType(dict(headers)), # mutable-ok: MappingProxyType needs a concrete mapping to wrap + headers=MappingProxyType(dict(headers)), resource_attributes=MappingProxyType({"service.name": service_name}) if service_name else _NO_ATTRS, callback_name=callback_name, protocol=protocol, diff --git a/litellm/integrations/otel/presets/langfuse.py b/litellm/integrations/otel/presets/langfuse.py index 9149e0c0d94..3ff3b521d29 100644 --- a/litellm/integrations/otel/presets/langfuse.py +++ b/litellm/integrations/otel/presets/langfuse.py @@ -30,7 +30,7 @@ def langfuse_preset( if not allow_missing_credentials: raise return base.model_copy( - update={ # mutable-ok: pydantic model_copy takes a plain update mapping + update={ "exporters": credential_gated_exporters(base.exporters, ExporterOwner.LANGFUSE_OTEL), "mapper_names": mappers, } diff --git a/litellm/integrations/otel/presets/weave.py b/litellm/integrations/otel/presets/weave.py index 644cd39ad36..856e784460a 100644 --- a/litellm/integrations/otel/presets/weave.py +++ b/litellm/integrations/otel/presets/weave.py @@ -31,7 +31,7 @@ def weave_preset( if not allow_missing_credentials: raise return base.model_copy( - update={ # mutable-ok: pydantic model_copy takes a plain update mapping + update={ "exporters": credential_gated_exporters(base.exporters, ExporterOwner.WEAVE_OTEL), "mapper_names": mappers, } diff --git a/litellm/integrations/pointfive/logger.py b/litellm/integrations/pointfive/logger.py index c352dac11e7..de800f09e7f 100644 --- a/litellm/integrations/pointfive/logger.py +++ b/litellm/integrations/pointfive/logger.py @@ -207,9 +207,7 @@ class PointFiveLogger(CustomBatchLogger): the excluded-field list and this callback's own setting are applied here, then the global, per-request and header settings that only the framework's predicate knows. """ - details: Final = self.redact_standard_logging_payload_from_model_call_details( - dict(kwargs) # mutable-ok: both framework helpers take the call details as a dict - ) + details: Final = self.redact_standard_logging_payload_from_model_call_details(dict(kwargs)) payload: Final = details.get("standard_logging_object") if not isinstance(payload, dict): return None diff --git a/litellm/integrations/pointfive/upload_client.py b/litellm/integrations/pointfive/upload_client.py index 56ba6689017..d3708d48661 100644 --- a/litellm/integrations/pointfive/upload_client.py +++ b/litellm/integrations/pointfive/upload_client.py @@ -147,7 +147,7 @@ class PointFiveUploadClient: response: Final = await self.http_client.post( self.api_url + path, json=request.model_dump(by_alias=True), - headers={ # mutable-ok: AsyncHTTPHandler.post types headers as dict + headers={ "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", }, @@ -172,7 +172,7 @@ class PointFiveUploadClient: if isinstance(destination, PointFiveUploadFailure): return destination url, host = destination - headers: Final = dict(PUT_HEADERS, Host=host) if host else dict(PUT_HEADERS) # mutable-ok: put wants dict + headers: Final = dict(PUT_HEADERS, Host=host) if host else dict(PUT_HEADERS) try: await self.http_client.put(url, data=body, headers=headers, follow_redirects=False) except httpx.HTTPStatusError as e: diff --git a/litellm/integrations/prometheus.py b/litellm/integrations/prometheus.py index 2528f07f92c..6c299f8e238 100644 --- a/litellm/integrations/prometheus.py +++ b/litellm/integrations/prometheus.py @@ -1232,7 +1232,7 @@ class PrometheusLogger(CustomLogger): return metric_class(*args, **kwargs) kept: Final = tuple(name for name in original_labelnames if name not in self.exclude_labels) - kept_kwargs: Final = {**kwargs, "labelnames": kept} # mutable-ok: ** needs a mapping to override labelnames + kept_kwargs: Final = {**kwargs, "labelnames": kept} real_metric: Final = metric_class(*args, **kept_kwargs) return _ExcludedLabelMetric(real_metric, original_labelnames, self.exclude_labels) diff --git a/litellm/integrations/s3_v2.py b/litellm/integrations/s3_v2.py index 972ac79e306..e5eb696a7ec 100644 --- a/litellm/integrations/s3_v2.py +++ b/litellm/integrations/s3_v2.py @@ -239,7 +239,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): def _sign_put( self, credentials: "Credentials", url: str, json_string: str, headers: Mapping[str, str] - ) -> dict[str, str]: # mutable-ok: [LIT001] AsyncHTTPHandler.put/HTTPHandler.put only accept dict headers + ) -> dict[str, str]: """ ``RefreshableCredentials`` (IMDS roles) may refresh between the access key, secret and token reads SigV4 performs, producing a mixed-generation signature that S3 rejects with 403. diff --git a/litellm/integrations/shadow_eval_logger.py b/litellm/integrations/shadow_eval_logger.py index cdc108a6b4e..b4bdcab9055 100644 --- a/litellm/integrations/shadow_eval_logger.py +++ b/litellm/integrations/shadow_eval_logger.py @@ -748,7 +748,7 @@ def _as_active_job(record: object, attempts: int, spend: float) -> ActiveShadowE except ValidationError as e: verbose_logger.debug("shadow_eval: skipping unsamplable job row: %s", e) return None - return job.model_copy(update={"attempts": attempts, "spend": spend}) # mutable-ok: pydantic update payload + return job.model_copy(update={"attempts": attempts, "spend": spend}) _jobs_cache: Final = InMemoryCache(max_size_in_memory=4, default_ttl=_JOBS_CACHE_TTL_SECONDS) @@ -785,7 +785,7 @@ class ShadowEvalLogger(CustomLogger): self._inflight_shadow_tasks: int = 0 # Starts per job since the last cache fill, never decremented within a # generation; the refill absorbs written rows and resets. - self._job_starts: dict[str, int] = {} # mutable-ok: per-generation counter + self._job_starts: dict[str, int] = {} async def _active_jobs(self) -> Mapping[tuple[str, str], tuple[ActiveShadowEvalJob, ...]]: """Active jobs by (target_type, target_id), cache-first. A target holds at most @@ -799,23 +799,22 @@ class ShadowEvalLogger(CustomLogger): return _EMPTY_JOBS try: records: Final = await prisma.db.litellm_shadowevaljob.find_many( - where={ # mutable-ok: Prisma filter + where={ "stopped_at": None, - "ends_at": {"gt": datetime.now(timezone.utc)}, # mutable-ok: Prisma filter + "ends_at": {"gt": datetime.now(timezone.utc)}, }, ) grouped: Final = ( await prisma.db.litellm_shadowevalattempt.group_by( by=["job_id"], count=True, - # mutable-ok: Prisma aggregate spec sum={"judge_cost": True, "shadow_cost": True, "shadow_classifier_cost": True}, - where={"job_id": {"in": [str(record.id) for record in records]}}, # mutable-ok: Prisma filter + where={"job_id": {"in": [str(record.id) for record in records]}}, ) if records else () ) - attempt_stats: Final = { # mutable-ok: frozen snapshot of the grouped read + attempt_stats: Final = { str(row["job_id"]): ( int(row["_count"]["_all"]), _leg_eval_spend(row["_sum"] or _EMPTY_METADATA), @@ -886,13 +885,13 @@ class ShadowEvalLogger(CustomLogger): payload: Final[StandardLoggingPayload | None] = kwargs.get("standard_logging_object") # pyright: ignore[reportAssignmentType] # untyped callback kwargs if payload is None: return - raw_meta: Final = get_litellm_metadata_from_kwargs(dict(kwargs)) # mutable-ok: helper needs dict + raw_meta: Final = get_litellm_metadata_from_kwargs(dict(kwargs)) request_metadata: Final = raw_meta if isinstance(raw_meta, Mapping) else _EMPTY_METADATA if request_metadata.get(INTERNAL_CALL_ORIGIN_METADATA_KEY): return # internal sub-call (our own shadow/judge, a classifier), not user traffic # redaction rewrites logged content before callbacks run, so this hook # only ever sees placeholders for a redacted request - if should_redact_message_logging(dict(kwargs)): # mutable-ok: predicate takes a plain dict + if should_redact_message_logging(dict(kwargs)): return metadata: Final = payload.get("metadata") or _EMPTY_METADATA # Each identity the request resolved to is a candidate target; JWT-auth @@ -928,7 +927,7 @@ class ShadowEvalLogger(CustomLogger): sample: Final = _judgeable_sample( ops, kwargs, - MappingProxyType(dict(payload.get("model_parameters") or {})), # mutable-ok: frozen snapshot + MappingProxyType(dict(payload.get("model_parameters") or {})), response_obj, ) if sample is None: @@ -961,7 +960,7 @@ class ShadowEvalLogger(CustomLogger): real_cache_hit=real_cache_hit, control_tier=control_tier, shadow_params=shadow_params, - parent_metadata=MappingProxyType(dict(request_metadata)), # mutable-ok: frozen snapshot + parent_metadata=MappingProxyType(dict(request_metadata)), ) ).add_done_callback(self._release_shadow_slot) except Exception as e: # noqa: BLE001 # logging hooks must never fail the request @@ -1175,7 +1174,7 @@ class ShadowEvalLogger(CustomLogger): return try: await prisma.db.litellm_shadowevalattempt.create( - data={ # mutable-ok: Prisma payload + data={ "job_id": job.id, "request_id": request_id, "router_name": router_name, @@ -1210,18 +1209,16 @@ class ShadowEvalLogger(CustomLogger): router: Final = self._router_provider() if router is None: return _CallFailure("no router configured on this pod") - shadow_metadata: Final[dict[str, object]] = ( # mutable-ok: router writes its routing decision back - sanitized_forwardable_call_metadata(parent_metadata, SHADOW_EVAL_ROUTER_CALL_ORIGIN) + shadow_metadata: Final[dict[str, object]] = sanitized_forwardable_call_metadata( + parent_metadata, SHADOW_EVAL_ROUTER_CALL_ORIGIN ) try: response: Final = await router.acompletion( model=target_model, - messages=[ # mutable-ok: provider transforms rewrite messages in place, so the router gets its own copy - dict(m) for m in messages - ], # pyright: ignore[reportArgumentType] # snapshot of the SDK's own message dicts + messages=[dict(m) for m in messages], # pyright: ignore[reportArgumentType] # snapshot of the SDK's own message dicts metadata=shadow_metadata, num_retries=0, - fallbacks=[], # mutable-ok: SDK kwarg; a failed shadow is a recorded error, never a spend multiplier + fallbacks=[], **shadow_params, ) except Exception as e: # noqa: BLE001 # provider errors become error rows, not crashes @@ -1270,12 +1267,12 @@ class ShadowEvalLogger(CustomLogger): if m.get("content") is not None ) judge_metadata: Final = sanitized_forwardable_call_metadata(parent_metadata, SHADOW_EVAL_JUDGE_CALL_ORIGIN) - judge_messages: Final = [ # mutable-ok: SDK takes a list - {"role": "system", "content": PAIRWISE_JUDGE_SYSTEM_PROMPT}, # mutable-ok: SDK message + judge_messages: Final = [ + {"role": "system", "content": PAIRWISE_JUDGE_SYSTEM_PROMPT}, { "role": "user", "content": _judge_user_prompt(conversation, response_a, response_b, _tool_definitions_text(tools)), - }, # mutable-ok: SDK message + }, ] try: response: Final = await judge_acompletion( diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 587da997f94..4f58d9d04b1 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -1594,7 +1594,7 @@ class WebSearchInterceptionLogger(CustomLogger): user_api_key_metadata: Final[StandardLoggingUserAPIKeyMetadata] = ( LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(user_api_key_dict=user_api_key_auth) ) - return { # mutable-ok: litellm's metadata channel is a plain dict its logging path reads and enriches + return { **user_api_key_metadata, "model_group": search_tool_name, "user_api_key": user_api_key_auth.api_key, diff --git a/litellm/interactions/background_cost_polling.py b/litellm/interactions/background_cost_polling.py index 51325354e7d..c76cd66d307 100644 --- a/litellm/interactions/background_cost_polling.py +++ b/litellm/interactions/background_cost_polling.py @@ -79,9 +79,7 @@ async def _fetch_interaction(context: BackgroundInteractionPollContext) -> Inter custom_llm_provider=context.custom_llm_provider, api_key=context.api_key, api_base=context.api_base, - **{ - "no-log": True - }, # mutable-ok: "no-log" is not a valid identifier, so it can only be passed through a mapping + **{"no-log": True}, ) @@ -240,7 +238,7 @@ class _ActiveBackgroundPoll: context: BackgroundInteractionPollContext -_ACTIVE_POLLS: dict[str, _ActiveBackgroundPoll] = {} # mutable-ok: asyncio needs strong refs to running poll tasks +_ACTIVE_POLLS: dict[str, _ActiveBackgroundPoll] = {} def _discard_poll(interaction_id: str, task: "asyncio.Task[None]") -> None: diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index aa7d6ca1699..76d15313e22 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -500,8 +500,8 @@ def safe_deep_copy(data): def independent_snapshot( - data: dict, # mutable-ok: caller-defined request-payload shape -) -> dict: # mutable-ok: caller-defined request-payload shape + data: dict, +) -> dict: """ A copy of ``data`` whose top-level keys are deep-copied independently where possible -- always attempted, regardless of @@ -522,7 +522,7 @@ def independent_snapshot( """ sanitized: Final = { key: ( - { # mutable-ok: same request-payload shape as data + { inner_key: ("placeholder" if inner_key == "litellm_parent_otel_span" else inner_value) for inner_key, inner_value in value.items() } @@ -544,15 +544,13 @@ def independent_snapshot( and isinstance(original_value, dict) and "litellm_parent_otel_span" in original_value ): - return { # mutable-ok: same request-payload shape as data + return { **copied_value, "litellm_parent_otel_span": original_value["litellm_parent_otel_span"], } return copied_value - return { # mutable-ok: same request-payload shape as data - key: _copied_value(key, value) for key, value in sanitized.items() - } + return {key: _copied_value(key, value) for key, value in sanitized.items()} def filter_exceptions_from_params(data: object, max_depth: int = 20) -> Any: diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index 91a22144805..b5e6713311f 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -195,7 +195,7 @@ def mark_litellm_import_complete() -> None: @dataclass(frozen=True, slots=True) class ModelCostMapReloaded: - model_cost_map: dict # mutable-ok: adopted as litellm.model_cost, whose consumer contract is a plain mutable dict + model_cost_map: dict revision: str | None = None etag: str | None = None @@ -537,7 +537,7 @@ def _finalize_loaded_model_cost_map(loaded: ModelCostMapReloaded) -> ModelCostMa def adopt_model_cost_map( - new_model_cost_map: dict, # mutable-ok: public API preserves the mutable cost-map contract + new_model_cost_map: dict, ) -> int: import litellm from litellm import utils @@ -639,7 +639,7 @@ def get_model_cost_map( if isinstance(outcome, _FetchAttemptRetryable) and max_attempts > 1: threading.Thread( target=_retry_remote_fetch_in_background, - kwargs={ # mutable-ok: threading requires a mutable keyword-arguments mapping + kwargs={ "url": url, "timeout": timeout, "max_attempts": max_attempts, diff --git a/litellm/litellm_core_utils/internal_call_metadata.py b/litellm/litellm_core_utils/internal_call_metadata.py index 87f007ca1d5..88318532ea9 100644 --- a/litellm/litellm_core_utils/internal_call_metadata.py +++ b/litellm/litellm_core_utils/internal_call_metadata.py @@ -112,16 +112,16 @@ def sanitize_user_api_key_auth(auth: object) -> object: """Copy of the auth object with its budget reservation removed; the cost callback falls back to reading the reservation from inside the auth object.""" if isinstance(auth, dict): - return {k: v for k, v in auth.items() if k != "budget_reservation"} # mutable-ok: SDK metadata value + return {k: v for k, v in auth.items() if k != "budget_reservation"} reservation: Final[object] = getattr(auth, "budget_reservation", None) model_copy: Final[object] = getattr(auth, "model_copy", None) if reservation is not None and callable(model_copy): - return model_copy(update={"budget_reservation": None}) # mutable-ok: pydantic update payload + return model_copy(update={"budget_reservation": None}) return auth -def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]: # mutable-ok: SDK metadata kwarg - return { # mutable-ok: SDK metadata kwarg +def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]: + return { k: sanitize_user_api_key_auth(v) if k == _USER_API_KEY_AUTH_KEY else v for k, v in parent_metadata.items() if k not in BUDGET_RESERVATION_METADATA_KEYS @@ -131,17 +131,15 @@ def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]: # m def forwarded_internal_call_metadata( parent_metadata: Mapping[str, object] | None, call_origin: InternalCallOrigin, -) -> dict[str, object]: # mutable-ok: SDK metadata kwarg +) -> dict[str, object]: """Parent metadata, minus its budget reservation, stamped with the sub-call's origin. For sub-calls made inside the parent request (classifier, embeddings), where the parent's full context still describes the call being made. """ if not parent_metadata: - return {} # mutable-ok: SDK metadata kwarg - return _sanitized(parent_metadata) | { # mutable-ok: SDK metadata kwarg - INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin - } + return {} + return _sanitized(parent_metadata) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin} def parent_session_kwargs(request_kwargs: Mapping[str, object] | None) -> Mapping[str, str]: @@ -160,11 +158,11 @@ def effective_turn_off_message_logging(request_kwargs: Mapping[str, object] | No def sanitized_forwardable_call_metadata( parent_metadata: Mapping[str, object], call_origin: InternalCallOrigin, -) -> dict[str, object]: # mutable-ok: SDK metadata kwarg +) -> dict[str, object]: """Just the caller's identity, stamped with the sub-call's origin. For sub-calls detached from the parent request (shadow eval), which outlive it and must not inherit per-request state such as its routing decision or logging payload. """ identity: Final = {k: v for k, v in parent_metadata.items() if k in FORWARDABLE_IDENTITY_METADATA_KEYS} - return _sanitized(identity) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin} # mutable-ok: SDK metadata kwarg + return _sanitized(identity) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin} diff --git a/litellm/litellm_core_utils/json_fragment_accumulator.py b/litellm/litellm_core_utils/json_fragment_accumulator.py index 81d18dd0119..19e0b17d852 100644 --- a/litellm/litellm_core_utils/json_fragment_accumulator.py +++ b/litellm/litellm_core_utils/json_fragment_accumulator.py @@ -21,20 +21,18 @@ class JSONFragmentAccumulator: def __init__(self) -> None: self._chunks: list[str] = [] # mutable-ok: O(1) append; string concat would copy the buffer each time - self._buffer: str = ( - "" # mutable-ok: lazily materialized join of _chunks, rebuilt only when _chunks is non-empty - ) - self._offset: int = 0 # mutable-ok: cursor past already-consumed values; avoids re-slicing on every pop - self._could_close: bool = False # mutable-ok: cached heuristic; rescanning past fragments was itself O(n^2) + self._buffer: str = "" + self._offset: int = 0 + self._could_close: bool = False def __bool__(self) -> bool: return bool(self._chunks) or self._offset < len(self._buffer) def append(self, fragment: str) -> None: - self._chunks.append(fragment) # mutable-ok: see __init__ + self._chunks.append(fragment) stripped: Final = fragment.rstrip() if stripped: - self._could_close = stripped[-1] in ("}", "]") # mutable-ok: see __init__ + self._could_close = stripped[-1] in ("}", "]") def could_close_json(self) -> bool: """ @@ -50,9 +48,9 @@ class JSONFragmentAccumulator: if not self._chunks: return unconsumed: Final = self._buffer[self._offset :] - self._buffer = unconsumed + "".join(self._chunks) # mutable-ok: merge pending fragments, once per append batch - self._offset = 0 # mutable-ok: see __init__ - self._chunks = [] # mutable-ok: see __init__ + self._buffer = unconsumed + "".join(self._chunks) + self._offset = 0 + self._chunks = [] def pop_next_value(self) -> tuple[bool, object]: """ @@ -69,7 +67,7 @@ class JSONFragmentAccumulator: while start < length and self._buffer[start].isspace(): start += 1 if start >= length: - self._offset = start # mutable-ok: see __init__ + self._offset = start return False, None decoder: Final = json.JSONDecoder() try: @@ -77,11 +75,11 @@ class JSONFragmentAccumulator: except json.JSONDecodeError: return False, None decoded, end_index = cast("tuple[object, int]", raw_value) # cast-ok: raw_decode returns tuple[Any, int] - self._offset = end_index # mutable-ok: see __init__ + self._offset = end_index if self._offset >= len(self._buffer): - self._buffer = "" # mutable-ok: see __init__ - self._offset = 0 # mutable-ok: see __init__ - self._could_close = False # mutable-ok: buffer is empty, nothing can close + self._buffer = "" + self._offset = 0 + self._could_close = False return True, decoded def snapshot(self) -> str: @@ -90,8 +88,8 @@ class JSONFragmentAccumulator: def set(self, value: str) -> None: """Replace the buffer's contents with a single fragment.""" - self._chunks = [] # mutable-ok: see __init__ - self._buffer = value # mutable-ok: see __init__ - self._offset = 0 # mutable-ok: see __init__ + self._chunks = [] + self._buffer = value + self._offset = 0 stripped: Final = value.rstrip() - self._could_close = bool(stripped) and stripped[-1] in ("}", "]") # mutable-ok: see __init__ + self._could_close = bool(stripped) and stripped[-1] in ("}", "]") diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 9ba9fd082f3..8adca93ff0d 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -619,7 +619,7 @@ class Logging(LiteLLMLoggingBaseClass): self.caching_details: CachingDetails | None = None # Timing for results that cannot carry ``_hidden_params`` (plain-dict /v1/messages # responses and the bridge stream wrappers); see ``update_response_metadata``. - self.response_timing_metrics: Mapping[str, float] = {} # mutable-ok: kept deep-copyable + self.response_timing_metrics: Mapping[str, float] = {} # Passthrough endpoint guardrails config for field targeting self.passthrough_guardrails_config: dict[str, Any] | None = None @@ -642,7 +642,7 @@ class Logging(LiteLLMLoggingBaseClass): def set_response_timing_metrics(self, timing_metrics: Mapping[str, float]) -> None: """Keep ``_response_ms`` / ``litellm_overhead_time_ms`` for a result that has no ``_hidden_params``.""" - self.response_timing_metrics = dict(timing_metrics) # mutable-ok: kept deep-copyable + self.response_timing_metrics = dict(timing_metrics) def process_dynamic_callbacks(self): """ @@ -969,7 +969,7 @@ class Logging(LiteLLMLoggingBaseClass): prompt_management_logger: CustomLogger | None = None, prompt_label: str | None = None, prompt_version: int | None = None, - request_kwargs: dict[str, object] | None = None, # mutable-ok: marker stamped into live request kwargs + request_kwargs: dict[str, object] | None = None, injected_for_every_deployment: bool = False, ) -> tuple[str, list[AllMessageValues], dict]: from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook @@ -1020,7 +1020,7 @@ class Logging(LiteLLMLoggingBaseClass): tools: list[dict] | None = None, prompt_label: str | None = None, prompt_version: int | None = None, - request_kwargs: dict[str, object] | None = None, # mutable-ok: marker stamped into live request kwargs + request_kwargs: dict[str, object] | None = None, injected_for_every_deployment: bool = False, ) -> tuple[str, list[AllMessageValues], dict]: from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook @@ -3881,7 +3881,7 @@ class Logging(LiteLLMLoggingBaseClass): if result.status == "completed": return InteractionsAPIResponse( **result.model_dump( - exclude={ # mutable-ok: pydantic types exclude as set[str], which a frozenset does not satisfy + exclude={ "event_type", "delta", "index", @@ -4944,9 +4944,7 @@ def _has_operator_exporter(config: "OpenTelemetryV2Config") -> bool: def _only_the_gated_exporter(config: "OpenTelemetryV2Config") -> "OpenTelemetryV2Config": - return config.model_copy( - update={"exporters": [spec for spec in config.exporters if _is_gated(spec)]} # mutable-ok: model_copy update - ) + return config.model_copy(update={"exporters": [spec for spec in config.exporters if _is_gated(spec)]}) def _is_gated(spec: "ExporterSpec") -> bool: @@ -5398,7 +5396,7 @@ class StandardLoggingPayloadSetup: if key not in user_metadata } ) - return {**user_metadata, **model_metadata} # mutable-ok: function contract returns a plain dict + return {**user_metadata, **model_metadata} @staticmethod def get_standard_logging_metadata( @@ -6237,9 +6235,7 @@ def get_standard_logging_object_payload( if clean_hidden_params["litellm_overhead_time_ms"] is None and status == "success": # /v1/messages dict results and the bridge stream wrappers keep it on the logging object; # failure payloads stay None like every response type that carries its own _hidden_params - timing_metrics: Final = ( - getattr(logging_obj, "response_timing_metrics", None) or {} # mutable-ok: empty fallback - ) + timing_metrics: Final = getattr(logging_obj, "response_timing_metrics", None) or {} clean_hidden_params["litellm_overhead_time_ms"] = timing_metrics.get("litellm_overhead_time_ms") model_cost_information: Final = StandardLoggingPayloadSetup.get_model_cost_information( diff --git a/litellm/litellm_core_utils/llm_cost_calc/guardrail_cost.py b/litellm/litellm_core_utils/llm_cost_calc/guardrail_cost.py index 54cdf2cb8ff..19adf1a30a7 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/guardrail_cost.py +++ b/litellm/litellm_core_utils/llm_cost_calc/guardrail_cost.py @@ -79,7 +79,7 @@ def bedrock_guardrail_cost_by_unit( pricing: Final = _bedrock_guardrail_pricing(aws_region_name) if pricing is None: return None - return { # mutable-ok: stamped into guardrail_information, which safe_dumps only serializes as a plain dict + return { counter: _priced_units(units, pricing.guardrail_cost_per_unit.get(counter)) for counter, units in usage_units.items() } diff --git a/litellm/litellm_core_utils/llm_judge.py b/litellm/litellm_core_utils/llm_judge.py index b632d3a9af9..6b366d5f182 100644 --- a/litellm/litellm_core_utils/llm_judge.py +++ b/litellm/litellm_core_utils/llm_judge.py @@ -27,7 +27,7 @@ def default_router_provider() -> Router | None: return llm_router -def parse_json_verdict(raw: str) -> dict[str, object]: # mutable-ok: plain parsed-JSON payload +def parse_json_verdict(raw: str) -> dict[str, object]: """Parse a judge's JSON verdict, tolerating markdown fences and surrounding prose.""" text = raw.strip() # rebind-ok: progressively narrowed to the JSON payload fenced: Final = JSON_FENCE_RE.search(text) @@ -44,7 +44,7 @@ def parse_json_verdict(raw: str) -> dict[str, object]: # mutable-ok: plain pars parsed = json.loads(text[start : end + 1]) if not isinstance(parsed, dict): raise ValueError("judge response is not a JSON object") - return {str(k): v for k, v in parsed.items()} # mutable-ok: plain parsed-JSON payload + return {str(k): v for k, v in parsed.items()} def extract_text_from_content(content: object) -> str: diff --git a/litellm/litellm_core_utils/llm_request_utils.py b/litellm/litellm_core_utils/llm_request_utils.py index 04824a5bf39..7f9557003fd 100644 --- a/litellm/litellm_core_utils/llm_request_utils.py +++ b/litellm/litellm_core_utils/llm_request_utils.py @@ -16,9 +16,7 @@ def _form_field_value(value: object) -> str: def _flatten_form_field(key: str, value: object) -> tuple[tuple[str, str], ...]: pending_fields: Final[ # mutable-ok: depth-capped stack walks nested JSON into multipart names list[tuple[str, object, int]] - ] = [ # mutable-ok: depth-capped stack walks nested JSON into multipart names - (key, value, 0) - ] + ] = [(key, value, 0)] flat_fields: Final[list[tuple[str, str]]] = [] # mutable-ok: local accumulator while pending_fields: current_key, current_value, depth = pending_fields.pop() @@ -48,9 +46,7 @@ def _is_form_scalar(value: object) -> bool: def _flatten_form_data_field(key: str, value: object) -> tuple[tuple[str, str | tuple[str, ...]], ...]: pending_fields: Final[ # mutable-ok: depth-capped stack walks nested JSON into multipart names list[tuple[str, object, int]] - ] = [ # mutable-ok: depth-capped stack walks nested JSON into multipart names - (key, value, 0) - ] + ] = [(key, value, 0)] flat_fields: Final[list[tuple[str, str | tuple[str, ...]]]] = [] # mutable-ok: local accumulator while pending_fields: current_key, current_value, depth = pending_fields.pop() diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index 87524d86c61..9ea730a873f 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -372,9 +372,7 @@ from collections import defaultdict def _handle_invalid_parallel_tool_calls( - tool_calls: list[ - ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall - ], # mutable-ok: patched in place via slice assignment + tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall], ): """ Handle hallucinated parallel tool call from openai - https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653 diff --git a/litellm/litellm_core_utils/llm_response_utils/response_metadata.py b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py index c83c266a17e..d7aa73f2d48 100644 --- a/litellm/litellm_core_utils/llm_response_utils/response_metadata.py +++ b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py @@ -30,7 +30,7 @@ def response_timing_metrics( """ total_response_time_ms: Final = (end_time - start_time).total_seconds() * 1000 if not include_overhead: - return {"_response_ms": total_response_time_ms} # mutable-ok: read-only timing result + return {"_response_ms": total_response_time_ms} caching_details: Final = logging_obj.caching_details cache_duration_ms: Final = ( caching_details.get("cache_duration_ms") diff --git a/litellm/litellm_core_utils/logging_utils.py b/litellm/litellm_core_utils/logging_utils.py index 44daef42e14..a7aaba72a07 100644 --- a/litellm/litellm_core_utils/logging_utils.py +++ b/litellm/litellm_core_utils/logging_utils.py @@ -308,7 +308,7 @@ def _set_duration_in_model_call_details( def speech_request_body(model: str, voice: str, optional_params: Mapping[str, object]) -> Mapping[str, object]: """Speech request body for telemetry, without the caller headers the provider SDKs take as request kwargs rather than body fields.""" - return { # mutable-ok: loggers isinstance-check the request body as a dict + return { "model": model, "voice": voice, **{key: value for key, value in optional_params.items() if key != "extra_headers"}, diff --git a/litellm/litellm_core_utils/logging_worker.py b/litellm/litellm_core_utils/logging_worker.py index 5ccc5632646..6b0c409a196 100644 --- a/litellm/litellm_core_utils/logging_worker.py +++ b/litellm/litellm_core_utils/logging_worker.py @@ -61,7 +61,7 @@ class LoggingWorker: self._queue: asyncio.Queue[LoggingTask] | None = None self._worker_task: asyncio.Task | None = None self._running_tasks: set[asyncio.Task] = set() - self._dequeued_tasks: dict[int, LoggingTask] = {} # mutable-ok: refs so flush can rescue never-started tasks + self._dequeued_tasks: dict[int, LoggingTask] = {} self._sem: asyncio.Semaphore | None = None self._bound_loop: asyncio.AbstractEventLoop | None = None self._last_aggressive_clear_time: float = 0.0 diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 2485896184e..4193e692879 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -1217,7 +1217,7 @@ def _mergeable_branch( branch: object, seen_refs: frozenset[str], depth: int, - expanded_refs: dict[str, Mapping[str, object] | None], # mutable-ok: per-call memo bounding repeated $ref work + expanded_refs: dict[str, Mapping[str, object] | None], ) -> Mapping[str, object] | None: if not isinstance(branch, dict) or depth > _MAX_SCHEMA_FLATTEN_DEPTH: return None @@ -1250,7 +1250,7 @@ def _flatten_schema_against_root( root: Mapping[str, object], seen_refs: frozenset[str], depth: int, - expanded_refs: dict[str, Mapping[str, object] | None], # mutable-ok: per-call memo bounding repeated $ref work + expanded_refs: dict[str, Mapping[str, object] | None], ) -> Mapping[str, object]: raw_branch_groups: Final = tuple( ( @@ -1282,7 +1282,7 @@ def _flatten_schema_against_root( if not is_object_schema: return schema - merged_properties: Final = { # mutable-ok: tool parameters are JSON dicts + merged_properties: Final = { name: value for source in (*reversed(branches), schema) for name, value in _schema_properties(source).items() } required_names: Final = _schema_required_names(schema).union( @@ -1290,7 +1290,7 @@ def _flatten_schema_against_root( ) kept: Final = MappingProxyType({key: value for key, value in schema.items() if key not in dropped}) required_update: Final = MappingProxyType({"required": sorted(required_names)}) if required_names else _EMPTY_SCHEMA - return { # mutable-ok: tool parameters are JSON dicts + return { **kept, "type": "object", "properties": merged_properties, @@ -1317,7 +1317,7 @@ def flatten_top_level_schema_combinators(schema: Mapping[str, object]) -> Mappin OpenAI's own validation still applies. Non-object schemas pass through unchanged and the input is never mutated. """ - return _flatten_schema_against_root(schema, schema, frozenset(), 0, {}) # mutable-ok: fresh per-call $ref memo + return _flatten_schema_against_root(schema, schema, frozenset(), 0, {}) _SUBSCHEMA_KEYWORDS: Final = frozenset( @@ -1362,7 +1362,7 @@ def drop_non_python_regex_patterns(schema: Mapping[str, object]) -> Mapping[str, at more schema levels than a JSON parser admits, so a cyclic schema built in code cannot spin it. """ - rebuilt: dict[int, Mapping[str, object]] = {} # mutable-ok: per-call memo of rewritten nodes, deepest level first + rebuilt: dict[int, Mapping[str, object]] = {} for level in reversed(tuple(islice(_schema_levels(schema), _MAX_SCHEMA_NESTING))): rebuilt.update( (id(node), rewritten) @@ -1392,7 +1392,7 @@ def _subschemas(node: Mapping[str, object]) -> Iterator[Mapping[str, object]]: def _node_without_non_python_regex( node: Mapping[str, object], rebuilt: Mapping[int, Mapping[str, object]] ) -> Mapping[str, object]: - kept: Final = { # mutable-ok: tool parameters are JSON dicts + kept: Final = { key: _keyword_value_rebuilt(key, value, rebuilt) for key, value in node.items() if key != "pattern" or not isinstance(value, str) or _is_python_regex(value) @@ -1402,14 +1402,14 @@ def _node_without_non_python_regex( def _keyword_value_rebuilt(key: str, value: object, rebuilt: Mapping[int, Mapping[str, object]]) -> object: if key in _SUBSCHEMA_MAP_KEYWORDS and isinstance(value, dict): - kept: Final = { # mutable-ok: tool parameters are JSON dicts + kept: Final = { name: rebuilt.get(id(sub), sub) for name, sub in value.items() if key != "patternProperties" or not isinstance(name, str) or _is_python_regex(name) } return value if len(kept) == len(value) and all(kept[name] is value[name] for name in kept) else kept if key in _SUBSCHEMA_LIST_KEYWORDS and isinstance(value, list): - items: Final = [rebuilt.get(id(sub), sub) for sub in value] # mutable-ok: tool parameters are JSON lists + items: Final = [rebuilt.get(id(sub), sub) for sub in value] return value if all(new is old for new, old in zip(items, value, strict=True)) else items if key in _SUBSCHEMA_KEYWORDS and isinstance(value, dict): return rebuilt.get(id(value), value) @@ -1441,7 +1441,7 @@ def tool_with_sanitized_parameters( sanitized: Final = sanitize(parameters) if sanitized is parameters: return tool - return {**tool, "function": {**function, "parameters": sanitized}} # mutable-ok: request tools are JSON dicts + return {**tool, "function": {**function, "parameters": sanitized}} def _get_image_mime_type_from_url(url: str) -> str | None: @@ -1700,7 +1700,7 @@ _MarkedT: Final = TypeVar("_MarkedT", bound=Mapping[str, object]) def with_prompt_cache_breakpoint(target: _MarkedT, marker: object) -> _MarkedT: if marker is None: return target - marked: Final = {**target, "prompt_cache_breakpoint": marker} # mutable-ok: API message payload + marked: Final = {**target, "prompt_cache_breakpoint": marker} return cast(_MarkedT, marked) # cast-ok: same block shape as the input plus the marker key @@ -1714,9 +1714,7 @@ def strip_litellm_internal_message_fields(message: AllMessageValues) -> AllMessa return message return cast( # cast-ok: same TypedDict minus internal keys AllMessageValues, - { # mutable-ok: provider transforms mutate message dicts in place downstream - key: value for key, value in message.items() if key not in LITELLM_INTERNAL_MESSAGE_FIELDS - }, + {key: value for key, value in message.items() if key not in LITELLM_INTERNAL_MESSAGE_FIELDS}, ) @@ -2166,11 +2164,9 @@ def _split_images_from_tool_message( ) if not image_parts: return message, () - remaining_parts = [ # mutable-ok: tool message content must stay a json list - part for part in content if not _is_image_url_part(part) - ] + remaining_parts = [part for part in content if not _is_image_url_part(part)] new_content = remaining_parts if remaining_parts else TOOL_RESULT_IMAGE_PLACEHOLDER - rewritten = {**message, "content": new_content} # mutable-ok: chat messages are plain json dicts + rewritten = {**message, "content": new_content} return cast(AllMessageValues, rewritten), image_parts # cast-ok: dict spread keeps keys like cache_control @@ -2178,14 +2174,12 @@ def _hoist_images_in_tool_message_run( run: Iterable[AllMessageValues], ) -> list[AllMessageValues]: # mutable-ok: message pipelines type messages as mutable lists split_results = tuple(_split_images_from_tool_message(message) for message in run) - hoisted_images = [ # mutable-ok: user message content must be a json list - image for _, images in split_results for image in images - ] - rewritten_messages = [message for message, _ in split_results] # mutable-ok: pipelines mutate message lists + hoisted_images = [image for _, images in split_results for image in images] + rewritten_messages = [message for message, _ in split_results] if not hoisted_images: return rewritten_messages boundary_part = ChatCompletionTextObject(type="text", text=TOOL_RESULT_IMAGE_BOUNDARY) - hoisted_content = [boundary_part, *hoisted_images] # mutable-ok: user message content must be a json list + hoisted_content = [boundary_part, *hoisted_images] rewritten_messages.append(ChatCompletionUserMessage(role="user", content=hoisted_content)) return rewritten_messages @@ -2209,7 +2203,7 @@ def hoist_images_from_tool_messages( """ if not any(_tool_message_carries_image(message) for message in messages): return messages - return [ # mutable-ok: pipelines mutate message lists + return [ rewritten_message for is_tool_run, run in groupby(messages, key=lambda message: message.get("role") == "tool") for rewritten_message in (_hoist_images_in_tool_message_run(run) if is_tool_run else run) @@ -2231,11 +2225,9 @@ def _drop_tool_reference_parts(message: AllMessageValues) -> AllMessageValues: if not _tool_message_carries_tool_reference(message): return message content = cast(list, message.get("content")) # cast-ok: shape checked by _tool_message_carries_tool_reference - remaining_parts = [ # mutable-ok: tool message content must stay a json list - part for part in content if not _is_tool_reference_part(part) - ] + remaining_parts = [part for part in content if not _is_tool_reference_part(part)] new_content = remaining_parts if remaining_parts else "" - rewritten = {**message, "content": new_content} # mutable-ok: chat messages are plain json dicts + rewritten = {**message, "content": new_content} return cast(AllMessageValues, rewritten) # cast-ok: dict spread keeps keys like cache_control @@ -2253,7 +2245,7 @@ def drop_tool_reference_parts_from_tool_messages( """ if not any(_tool_message_carries_tool_reference(message) for message in messages): return messages - return [_drop_tool_reference_parts(message) for message in messages] # mutable-ok: pipelines mutate message lists + return [_drop_tool_reference_parts(message) for message in messages] def _attempt_json_repair(s: str) -> object | None: diff --git a/litellm/litellm_core_utils/prompt_templates/image_handling.py b/litellm/litellm_core_utils/prompt_templates/image_handling.py index c9933422cc3..59bb06270f0 100644 --- a/litellm/litellm_core_utils/prompt_templates/image_handling.py +++ b/litellm/litellm_core_utils/prompt_templates/image_handling.py @@ -243,28 +243,28 @@ def _inferred_format(file: Mapping[str, object], url: str) -> Mapping[str, str]: def _inlined_image_url(image_url: Mapping[str, object] | None, data_url: str) -> Mapping[str, object] | str: - return {**image_url, "url": data_url} if image_url is not None else data_url # mutable-ok: json-serialized part + return {**image_url, "url": data_url} if image_url is not None else data_url def _inlined_file(file: Mapping[str, object], url: str, data_url: str) -> Mapping[str, object]: - kept: Final = {k: v for k, v in file.items() if k != "file_id"} # mutable-ok: json-serialized message part - return {**kept, **_inferred_format(file, url), "file_data": data_url} # mutable-ok: json-serialized part + kept: Final = {k: v for k, v in file.items() if k != "file_id"} + return {**kept, **_inferred_format(file, url), "file_data": data_url} def _base64_source(url: str, data_url: str) -> Mapping[str, str]: fetched_media_type, data = data_url.removeprefix("data:").split(";base64,", 1) media_type: Final = "application/pdf" if url.lower().endswith(".pdf") else fetched_media_type - return {"type": "base64", "media_type": media_type, "data": data} # mutable-ok: json-serialized message part + return {"type": "base64", "media_type": media_type, "data": data} def _inline(remote: _RemoteImage | _RemoteFile | _RemoteSource, data_url: str) -> Mapping[str, object]: match remote: case _RemoteImage(part, image_url, _): - return {**part, "image_url": _inlined_image_url(image_url, data_url)} # mutable-ok: json-serialized part + return {**part, "image_url": _inlined_image_url(image_url, data_url)} case _RemoteFile(part, file, url): - return {**part, "file": _inlined_file(file, url, data_url)} # mutable-ok: json-serialized message part + return {**part, "file": _inlined_file(file, url, data_url)} case _RemoteSource(part, _, url): - return {**part, "source": _base64_source(url, data_url)} # mutable-ok: json-serialized message part + return {**part, "source": _base64_source(url, data_url)} def _content_parts(message: Mapping[str, object]) -> tuple[object, ...]: @@ -286,10 +286,8 @@ def _inline_message( parts: Final = _content_parts(message) if not parts: return message - inlined_parts: Final = [ # mutable-ok: content must stay a list for the transforms' isinstance checks - _inline_part(part, data_urls, should_inline) for part in parts - ] - inlined_message: Final = {**message, "content": inlined_parts} # mutable-ok: json-serialized message + inlined_parts: Final = [_inline_part(part, data_urls, should_inline) for part in parts] + inlined_message: Final = {**message, "content": inlined_parts} return inlined_message # pyright: ignore[reportReturnType] # the same message with its remote parts inlined @@ -326,6 +324,4 @@ async def async_inline_remote_media( return messages data_urls: Final = await _fetch_data_urls(remote_urls) inlined: Final = MappingProxyType(dict(zip(remote_urls, data_urls, strict=True))) - return [ # mutable-ok: transform_request takes a list - _inline_message(message, inlined, should_inline) for message in messages - ] + return [_inline_message(message, inlined, should_inline) for message in messages] diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index 90698296142..7399765cb2c 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -461,12 +461,8 @@ class ChunkProcessor: def get_combined_tool_content( self, tool_call_chunks: Sequence["_ToolCallChunk"] - ) -> list[ - ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall - ]: # mutable-ok: assigned verbatim to Message.tool_calls, a list field - tool_calls_list: list[ - ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall - ] = [] # mutable-ok: see return type + ) -> list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall]: + tool_calls_list: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] = [] tool_call_map: Final[dict[int, dict[str, Any]]] = {} # Map to store tool calls by index for chunk in tool_call_chunks: diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 6a5a8832cc6..90af1bd4572 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -209,9 +209,7 @@ def _provider_hidden_params( hidden: Final[object] = getattr(chunk, "_hidden_params", None) parsed: Final = _parsed_provider_hidden_params(hidden) provider_specific_fields: Final[object | None] = ( - dict(parsed.provider_specific_fields) # mutable-ok: stream assembly merges provider metadata into this dict - if parsed is not None and parsed.provider_specific_fields - else None + dict(parsed.provider_specific_fields) if parsed is not None and parsed.provider_specific_fields else None ) params: Final[Mapping[str, object]] = MappingProxyType( { diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 9d50345d70d..1dd0f851f73 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -179,7 +179,7 @@ def _rewritten_event(event: Mapping[str, object], rewrite_event: _SSEEventRewrit section: Final = None if rewrite is None else event.get(rewrite.section) if rewrite is None or not isinstance(section, Mapping): return event - return {**event, rewrite.section: {**section, rewrite.field: rewrite.value}} # mutable-ok: json.dumps needs a dict + return {**event, rewrite.section: {**section, rewrite.field: rewrite.value}} def _tool_call_shapes(tool_calls: Sequence[object]) -> tuple[_ToolCallShape, ...]: @@ -455,9 +455,7 @@ class AnthropicMessagesHandler(BaseTranslation): # Exclude only the trusted top-level prompt. In-sequence system entries are untrusted # and must stay aligned with texts_to_check for positional masking. When the top-level # prompt is included, the pre-existing count mismatch disables positional masking. - translation_source: Final = { # mutable-ok: API message payload - key: value for key, value in data.items() if key != "system" - } + translation_source: Final = {key: value for key, value in data.items() if key != "system"} chat_completion_compatible_request: Final = self._translate_to_openai(translation_source) full_structured_messages: Final = cast( @@ -502,10 +500,8 @@ class AnthropicMessagesHandler(BaseTranslation): for msg_idx, message in enumerate(messages) ) scanned: Final = tuple(item for one_message in extracted for item in one_message.scanned) - texts_to_check: Final = [item.text for item in scanned] # mutable-ok: GenericGuardrailAPIInputs takes list[str] - images_to_check: Final = [ - image for one_message in extracted for image in one_message.images - ] # mutable-ok: GenericGuardrailAPIInputs takes list[str] + texts_to_check: Final = [item.text for item in scanned] + images_to_check: Final = [image for one_message in extracted for image in one_message.images] # Step 2: Apply guardrail to all texts in batch if texts_to_check: @@ -581,33 +577,29 @@ 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: dict) -> AllMessageValues | None: """Return the system message produced by translating the top-level prompt.""" system: Final = data.get("system") if not system: return None probe: Final = self._translate_to_openai( - { # mutable-ok: API message payload + { "model": data.get("model") or "", - "messages": [], # mutable-ok: API message payload + "messages": [], "system": system, } ) - hoisted: Final = probe.get("messages") or [] # mutable-ok: API message payload + hoisted: Final = probe.get("messages") or [] return hoisted[0] if hoisted else None @staticmethod def _openai_system_message_to_anthropic( message: Mapping[str, object], - ) -> dict[str, object] | None: # mutable-ok: API message payload + ) -> dict[str, object] | None: """Convert an OpenAI system message to the client's Anthropic-shaped entry.""" content: Final = message.get("content") if isinstance(content, str): - return ( - {"role": "system", "content": content} if content else None # mutable-ok: API message payload - ) # mutable-ok: API message payload + return {"role": "system", "content": content} if content else None if not isinstance(content, list): return None blocks: Final[list[dict[str, object]]] = [] # mutable-ok: API message payload @@ -617,21 +609,19 @@ class AnthropicMessagesHandler(BaseTranslation): text = block.get("text") if not isinstance(text, str) or not text: continue - anthropic_block: dict[str, object] = { # mutable-ok: API message payload + anthropic_block: dict[str, object] = { "type": "text", "text": text, - } # mutable-ok: API message payload + } cache_control = block.get("cache_control") if cache_control: anthropic_block["cache_control"] = deepcopy(cache_control) blocks.append(anthropic_block) - return ( - {"role": "system", "content": blocks} if blocks else None # mutable-ok: API message payload - ) # mutable-ok: API message payload + return {"role": "system", "content": blocks} if blocks else None @staticmethod def _fold_leading_systems_into_top_level( - data: dict[str, object], # mutable-ok: API message payload + data: dict[str, object], leading_systems: Sequence[object], include_existing_system: bool, ) -> None: @@ -712,7 +702,7 @@ class AnthropicMessagesHandler(BaseTranslation): @staticmethod def _write_back_structured_messages( - data: dict, # mutable-ok: API message payload + data: dict, structured_messages: list, # mutable-ok: API message payload hoisted_system_message: object = None, preserve_system_messages: bool = False, @@ -731,7 +721,7 @@ class AnthropicMessagesHandler(BaseTranslation): for group in group_tool_exchanges(run): converted.extend( anthropic_messages_pt( - messages=[run[index] for index in group], # mutable-ok: API message payload + messages=[run[index] for index in group], model=model, llm_provider="anthropic", ) @@ -957,24 +947,16 @@ class AnthropicMessagesHandler(BaseTranslation): match target: case MessageContentTarget(): if isinstance(content, str): - message["content"] = ( - guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + message["content"] = guardrail_response case ContentBlockTextTarget(content_idx=content_idx): if isinstance(content, list): - content[content_idx]["text"] = ( - guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + content[content_idx]["text"] = guardrail_response case ToolResultStringTarget(content_idx=content_idx): if isinstance(content, list): - content[content_idx]["content"] = ( - guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + content[content_idx]["content"] = guardrail_response case ToolResultBlockTextTarget(content_idx=content_idx, block_idx=block_idx): if isinstance(content, list): - content[content_idx]["content"][block_idx]["text"] = ( - guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place - ) + content[content_idx]["content"][block_idx]["text"] = guardrail_response case _: assert_never(target) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 5d3ae444b42..1579fbb1829 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -371,7 +371,7 @@ class AnthropicChatCompletion(BaseLLM): transform_params: Final = {**optional_params, "is_vertex_request": is_vertex_request} - def finish_request(request_data: dict) -> tuple[dict, dict]: # mutable-ok: rewritten in place downstream + def finish_request(request_data: dict) -> tuple[dict, dict]: """Filter beta headers and emit pre_call, returning `(headers, data)`. The pair stays mutable because the streaming path rewrites it in @@ -460,7 +460,7 @@ class AnthropicChatCompletion(BaseLLM): # before transforming: whichever path runs emits pre_call exactly once. # `get_config` merges the class-level defaults (Anthropic's required # `max_tokens` among them) that `transform_request` would have applied. - rust_optional_params: Final = { # mutable-ok: json.dumps in the bridge rejects a mappingproxy + rust_optional_params: Final = { **AnthropicConfig.get_config(model=model), **optional_params, } @@ -473,8 +473,8 @@ class AnthropicChatCompletion(BaseLLM): stream=stream, ) if serves_via_rust: - rust_logging_args: Final = { # mutable-ok: logging callbacks read additional_args as a plain dict - "complete_input_dict": { # mutable-ok: same, and it is serialized alongside its parent + rust_logging_args: Final = { + "complete_input_dict": { "model": model, "messages": messages, **rust_optional_params, @@ -650,7 +650,7 @@ class ModelResponseIterator: # Accumulate web_search_tool_result blocks for multi-turn reconstruction # See: https://github.com/BerriAI/litellm/issues/17737 self.web_search_results: list[dict[str, object]] = [] - self._web_search_calls: dict[str, object] = {} # mutable-ok: provider call state by id + self._web_search_calls: dict[str, object] = {} # Accumulate compaction blocks for multi-turn reconstruction self.compaction_blocks: list[dict[str, object]] = [] @@ -825,7 +825,7 @@ class ModelResponseIterator: return content_block_start def _web_search_call_snapshot(self) -> dict[str, object]: - return dict(self._web_search_calls) # mutable-ok: stream payload snapshot + return dict(self._web_search_calls) def _complete_web_search_call(self, result: dict[str, object]) -> None: tool_use_id: Final = result.get("tool_use_id") @@ -833,7 +833,7 @@ class ModelResponseIterator: return self._web_search_calls[tool_use_id] = build_web_search_call( tool_id=tool_use_id, - tool_input=self._server_tool_inputs.get(tool_use_id, {}), # mutable-ok: empty provider input + tool_input=self._server_tool_inputs.get(tool_use_id, {}), result=result, ) @@ -942,7 +942,7 @@ class ModelResponseIterator: self._web_search_calls[self._current_server_tool_id] = build_web_search_call( self._current_server_tool_id, tool_input, - {"content": []}, # mutable-ok: no provider result yet + {"content": []}, status="in_progress", ) provider_specific_fields["web_search_calls"] = self._web_search_call_snapshot() diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 0f99441a115..1cbdb27502a 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1853,10 +1853,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): self, model: str, messages: list[AllMessageValues], # mutable-ok: BaseConfig signature - optional_params: dict[str, object], # mutable-ok: BaseConfig signature - litellm_params: dict[str, object], # mutable-ok: BaseConfig signature - headers: dict[str, object], # mutable-ok: BaseConfig signature - ) -> dict[str, object]: # mutable-ok: BaseConfig signature + optional_params: dict[str, object], + litellm_params: dict[str, object], + headers: dict[str, object], + ) -> dict[str, object]: return self.transform_request( model=model, messages=await async_inline_remote_media(messages, should_inline=self.inlines_remote_media), @@ -2067,7 +2067,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params.pop("output_config", None) data.pop("output_config", None) return - format_only: Final = {"format": preserved_format} # mutable-ok: json body + format_only: Final = {"format": preserved_format} optional_params["output_config"] = format_only # rebind-ok: out-param store data["output_config"] = format_only # rebind-ok: out-param store return @@ -2472,7 +2472,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) -> list[object]: content: Final = completion_response.get("content") blocks: Final = content if isinstance(content, Sequence) else () - inputs: Final = { # mutable-ok: indexes provider server inputs + inputs: Final = { call_id: tool_input for block in blocks if isinstance(block, Mapping) @@ -2481,10 +2481,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): and isinstance((call_id := block.get("id")), str) and isinstance((tool_input := block.get("input")), Mapping) } - return [ # mutable-ok: provider-neutral response items + return [ build_web_search_call( tool_id=tool_use_id, - tool_input=inputs.get(tool_use_id, {}), # mutable-ok: empty provider input + tool_input=inputs.get(tool_use_id, {}), result=result, ) for result in web_search_results diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 87c4ec8938e..e4968b63700 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -618,7 +618,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): @staticmethod def maybe_drop_disabled_thinking( model: str, - optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param, as in _maybe_drop_speed_param + optional_params: MutableMapping[str, object], custom_llm_provider: str, ) -> None: """Omit ``thinking={'type': 'disabled'}`` for always-on-thinking models @@ -638,7 +638,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): @staticmethod def translate_legacy_thinking_for_adaptive_model( model: str, - optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param like the sibling helpers + optional_params: MutableMapping[str, object], custom_llm_provider: str, ) -> None: """Translate legacy ``thinking.type=enabled`` to adaptive for the @@ -1207,22 +1207,22 @@ def strip_thinking_blocks_from_anthropic_messages(messages: list[Any]) -> list[A return out -def _without_encrypted_reasoning_blocks(message: dict) -> dict | None: # mutable-ok: Anthropic message payload shape +def _without_encrypted_reasoning_blocks(message: dict) -> dict | None: if not isinstance(message, Mapping): return message content: Final = message.get("content") if not isinstance(content, list): return message - kept: Final = [b for b in content if not is_encrypted_reasoning_block(b)] # mutable-ok: API message payload + kept: Final = [b for b in content if not is_encrypted_reasoning_block(b)] if len(kept) == len(content): return message if not kept: return None - return {**message, "content": kept} # mutable-ok: API message payload + return {**message, "content": kept} def strip_encrypted_reasoning_blocks_from_anthropic_messages( - messages: Sequence[dict], # mutable-ok: Anthropic message payload shape + messages: Sequence[dict], ) -> list[dict]: # mutable-ok: AnthropicMessagesRequest.messages is typed list[dict] """ Drop thinking / redacted_thinking blocks that carry another provider's encrypted @@ -1230,7 +1230,7 @@ def strip_encrypted_reasoning_blocks_from_anthropic_messages( Anthropic, which cannot verify them. Anthropic's own signed blocks are kept. """ stripped: Final = (_without_encrypted_reasoning_blocks(m) for m in messages) - return [m for m in stripped if m is not None] # mutable-ok: API message payload + return [m for m in stripped if m is not None] def strip_thinking_blocks_from_anthropic_messages_request_dict( @@ -1512,10 +1512,10 @@ def _flatten_web_search_results_in_message(message: object) -> object: } ) rewritten: Final = tuple(_rewrite_replayed_web_search_block(block, flattenable, queries) for block in content) - return {**message, "content": [b for b in rewritten if b is not None]} # mutable-ok: JSON wire format + return {**message, "content": [b for b in rewritten if b is not None]} -def flatten_unencrypted_web_search_results_in_anthropic_messages( # mutable-ok: as sibling sanitizers +def flatten_unencrypted_web_search_results_in_anthropic_messages( messages: list[Any], ) -> list[Any]: """ @@ -1530,49 +1530,47 @@ def flatten_unencrypted_web_search_results_in_anthropic_messages( # mutable-ok: evidence in the conversation instead of 400ing the follow-up turn, and leaves genuine Anthropic-issued blocks untouched. """ - return [_flatten_web_search_results_in_message(m) for m in messages] # mutable-ok: JSON wire format + return [_flatten_web_search_results_in_message(m) for m in messages] def _without_provider_specific_fields(block: object) -> object: if not isinstance(block, dict) or "provider_specific_fields" not in block: return block - return {k: v for k, v in block.items() if k != "provider_specific_fields"} # mutable-ok: JSON wire format + return {k: v for k, v in block.items() if k != "provider_specific_fields"} def _strip_provider_specific_fields_in_message(message: object) -> object: if not isinstance(message, dict) or not isinstance(message.get("content"), list): return message - content: Final = [_without_provider_specific_fields(b) for b in message["content"]] # mutable-ok: JSON wire format - return {**message, "content": content} # mutable-ok: JSON wire format + content: Final = [_without_provider_specific_fields(b) for b in message["content"]] + return {**message, "content": content} def strip_provider_specific_fields_from_anthropic_messages( messages: Sequence[object], ) -> Sequence[object]: - return [_strip_provider_specific_fields_in_message(m) for m in messages] # mutable-ok: JSON wire format + return [_strip_provider_specific_fields_in_message(m) for m in messages] -def _normalized_cache_control(cache_control: object) -> dict[str, str] | None: # mutable-ok: JSON wire format +def _normalized_cache_control(cache_control: object) -> dict[str, str] | None: if not isinstance(cache_control, Mapping): return None cache_type: Final = cache_control.get("type") - return {"type": cache_type if isinstance(cache_type, str) else "ephemeral"} # mutable-ok: JSON wire format + return {"type": cache_type if isinstance(cache_type, str) else "ephemeral"} -def _with_portable_cache_control(block: Mapping[str, object]) -> dict[str, object]: # mutable-ok: JSON wire format +def _with_portable_cache_control(block: Mapping[str, object]) -> dict[str, object]: if "cache_control" not in block: - return dict(block) # mutable-ok: JSON wire format + return dict(block) normalized: Final = _normalized_cache_control(block["cache_control"]) - rest: Final = {key: value for key, value in block.items() if key != "cache_control"} # mutable-ok: JSON wire format - return rest if normalized is None else {**rest, "cache_control": normalized} # mutable-ok: JSON wire format + rest: Final = {key: value for key, value in block.items() if key != "cache_control"} + return rest if normalized is None else {**rest, "cache_control": normalized} def _with_portable_cache_control_in_blocks(blocks: object) -> object: if isinstance(blocks, str) or not isinstance(blocks, Sequence): return blocks - return [ # mutable-ok: JSON wire format - _with_portable_cache_control(block) if isinstance(block, Mapping) else block for block in blocks - ] + return [_with_portable_cache_control(block) if isinstance(block, Mapping) else block for block in blocks] def _with_portable_cache_control_in_content_block(block: object) -> object: @@ -1581,7 +1579,7 @@ def _with_portable_cache_control_in_content_block(block: object) -> object: portable: Final = _with_portable_cache_control(block) if portable.get("type") != "tool_result" or "content" not in portable: return portable - return { # mutable-ok: JSON wire format + return { **portable, "content": _with_portable_cache_control_in_blocks(portable["content"]), } @@ -1593,20 +1591,16 @@ def _with_portable_cache_control_in_message(message: object) -> object: content: Final = message["content"] if isinstance(content, str) or not isinstance(content, Sequence): return message - return { # mutable-ok: JSON wire format + return { **message, - "content": [ # mutable-ok: JSON wire format - _with_portable_cache_control_in_content_block(block) for block in content - ], + "content": [_with_portable_cache_control_in_content_block(block) for block in content], } def _with_portable_cache_control_in_messages(messages: object) -> object: if isinstance(messages, str) or not isinstance(messages, Sequence): return messages - return [ # mutable-ok: JSON wire format - _with_portable_cache_control_in_message(message) for message in messages - ] + return [_with_portable_cache_control_in_message(message) for message in messages] def _with_portable_cache_control_in_scoped_value(key: str, value: object) -> object: @@ -1621,7 +1615,7 @@ def _with_portable_cache_control_in_scoped_value(key: str, value: object) -> obj def normalize_cache_control_in_anthropic_payload( payload: Mapping[str, object], -) -> dict[str, object]: # mutable-ok: JSON wire format +) -> dict[str, object]: """ Return a copy of an Anthropic /v1/messages payload with every ``cache_control`` entry reduced to ``{"type": }`` @@ -1638,9 +1632,7 @@ def normalize_cache_control_in_anthropic_payload( dropped entirely. The caller's payload is never mutated. """ portable: Final = _with_portable_cache_control(payload) - return { # mutable-ok: JSON wire format - key: _with_portable_cache_control_in_scoped_value(key, value) for key, value in portable.items() - } + return {key: _with_portable_cache_control_in_scoped_value(key, value) for key, value in portable.items()} def process_anthropic_headers(headers: httpx.Headers | dict) -> dict: @@ -1667,7 +1659,7 @@ def _anthropic_model_entry( source: Final[Mapping[str, object]] = ( MappingProxyType({"source_model": model["id"]}) if listed_id is not None else MappingProxyType({}) ) - return { # mutable-ok: JSON response body, serialized by the route and never mutated + return { "type": "model", "id": listed_id or model["id"], **source, @@ -1698,10 +1690,8 @@ def create_anthropic_model_list_response( created_at: Final = ( datetime.fromtimestamp(DEFAULT_MODEL_CREATED_AT_TIME, tz=timezone.utc).isoformat().replace("+00:00", "Z") ) - data: Final = [ # mutable-ok: JSON response body, serialized by the route and never mutated - _anthropic_model_entry(model, created_at, display_names, listed_ids) for model in models - ] - return { # mutable-ok: JSON response body, serialized by the route and never mutated + data: Final = [_anthropic_model_entry(model, created_at, display_names, listed_ids) for model in models] + return { "data": data, "has_more": False, "first_id": data[0]["id"] if data else None, 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 e7179aad25b..16b2bb6fd87 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -1041,9 +1041,9 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): 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": { **delta, "stop_reason": "refusal", "stop_details": refusal_stop_details(self._refusal_text), diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 8ff9f2e0679..a3aded39ce9 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1249,7 +1249,7 @@ class LiteLLMAnthropicMessagesAdapter: case ({"type": "text", "text": str(text)},): return text case _: - return list(parts) # mutable-ok: content must be a json list + return list(parts) def _tool_result_part(self, item: object) -> ToolMessageContentPart | None: if isinstance(item, str): diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/response_cache.py b/litellm/llms/anthropic/experimental_pass_through/messages/response_cache.py index 86dfe8ff451..68ad983022a 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/response_cache.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/response_cache.py @@ -42,7 +42,7 @@ class AnthropicMessagesStreamCacheWriter: self.caching_handler = caching_handler self.collected_chunks: list[bytes] = [] # mutable-ok: rebuilding a tuple per SSE chunk is quadratic self.persisted = False - self._hidden_params: dict[str, object] = dict( # mutable-ok: callers stamp cache_key in here + self._hidden_params: dict[str, object] = dict( stream._hidden_params if isinstance(stream, AnthropicMessagesStreamingResponse) else _EMPTY_MAPPING ) @@ -88,9 +88,7 @@ class AnthropicMessagesStreamCacheWriter: try: events: Final = _split_sse_events(collected_stream.decode("utf-8")) - cached_payload: Final = { - CACHED_STREAM_EVENTS_KEY: events - } # mutable-ok: cache backends serialize plain dicts + cached_payload: Final = {CACHED_STREAM_EVENTS_KEY: events} await litellm.cache.async_add_cache( cached_payload, dynamic_cache_object=self.caching_handler.dual_cache, @@ -107,12 +105,12 @@ class CachedAnthropicMessagesStreamIterator(BaseAnthropicMessagesStreamingIterat litellm_logging_obj: "LiteLLMLoggingObj", request_body: Mapping[str, object], ) -> None: - body: Final = dict(request_body) # mutable-ok: the base iterator takes a plain dict + body: Final = dict(request_body) super().__init__(litellm_logging_obj=litellm_logging_obj, request_body=body) self.chunks: Final[tuple[bytes, ...]] = tuple(event.encode("utf-8") for event in events) self.current_index = 0 self.logged = False - self._hidden_params: dict[str, object] = {"cache_hit": True} # mutable-ok: callers stamp cache_key in here + self._hidden_params: dict[str, object] = {"cache_hit": True} litellm_logging_obj.model_call_details["cache_hit"] = True def __aiter__(self) -> "CachedAnthropicMessagesStreamIterator": @@ -122,7 +120,7 @@ class CachedAnthropicMessagesStreamIterator(BaseAnthropicMessagesStreamingIterat if self.current_index >= len(self.chunks): if not self.logged: self.logged = True - chunks: Final = list(self.chunks) # mutable-ok: the logging handler takes a list + chunks: Final = list(self.chunks) await self._handle_streaming_logging(chunks) raise StopAsyncIteration chunk: Final = self.chunks[self.current_index] diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py index 7d01aee5d98..84c88b5f49e 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py @@ -186,7 +186,7 @@ def _sse_event(event_type: str, payload: Mapping[str, object]) -> bytes: def _incomplete_stream_error_sse_event() -> bytes: - return _sse_event( # mutable-ok: one-shot JSON payload, never mutated after construction + return _sse_event( "error", {"type": "error", "error": {"type": "api_error", "message": INCOMPLETE_STREAM_ERROR_MESSAGE}}, ) @@ -207,15 +207,15 @@ def _anthropic_content_block_start_and_deltas( match block.get("type"): case "tool_use": return ( - { # mutable-ok: one-shot payload + { "id": block.get("id"), "name": block.get("name"), - "input": {}, # mutable-ok: one-shot payload + "input": {}, "type": "tool_use", }, ( - { # mutable-ok: one-shot payload - "partial_json": json.dumps(block.get("input") or {}), # mutable-ok: one-shot payload + { + "partial_json": json.dumps(block.get("input") or {}), "type": "input_json_delta", }, ), @@ -223,23 +223,23 @@ def _anthropic_content_block_start_and_deltas( case "thinking": signature: Final = block.get("signature") signature_deltas: Final = ( - ({"signature": signature, "type": "signature_delta"},) # mutable-ok: one-shot payload + ({"signature": signature, "type": "signature_delta"},) if isinstance(signature, str) and signature else () ) return ( - {"thinking": "", "signature": "", "type": "thinking"}, # mutable-ok: one-shot payload + {"thinking": "", "signature": "", "type": "thinking"}, ( - {"thinking": block.get("thinking") or "", "type": "thinking_delta"}, # mutable-ok: one-shot payload + {"thinking": block.get("thinking") or "", "type": "thinking_delta"}, *signature_deltas, ), ) case "redacted_thinking": - return ({"type": "redacted_thinking", "data": block.get("data")}, ()) # mutable-ok: one-shot JSON payload + return ({"type": "redacted_thinking", "data": block.get("data")}, ()) case _: return ( - {"type": "text", "text": ""}, # mutable-ok: one-shot JSON payload - ({"type": "text_delta", "text": block.get("text") or ""},), # mutable-ok: one-shot JSON payload + {"type": "text", "text": ""}, + ({"type": "text_delta", "text": block.get("text") or ""},), ) @@ -263,51 +263,51 @@ def anthropic_messages_response_as_sse_events(response: AnthropicMessagesRespons # a zero output_tokens - those are only known once generation finishes, so # copying the completed response's final values here would let a client # treat the message as already finished, or double-count output tokens. - message_start_usage: Final = { # mutable-ok: one-shot JSON payload + message_start_usage: Final = { **(response.get("usage") or {}), "output_tokens": 0, } - message_start_payload: Final = { # mutable-ok: one-shot JSON payload, never mutated after construction + message_start_payload: Final = { "type": "message_start", - "message": { # mutable-ok: one-shot JSON payload + "message": { **response, - "content": [], # mutable-ok: one-shot JSON payload + "content": [], "stop_reason": None, "stop_sequence": None, "usage": message_start_usage, }, } - message_delta_payload: Final = { # mutable-ok: one-shot JSON payload, never mutated after construction + message_delta_payload: Final = { "type": "message_delta", - "delta": { # mutable-ok: one-shot JSON payload + "delta": { "stop_reason": response.get("stop_reason"), "stop_sequence": response.get("stop_sequence"), }, - "usage": response.get("usage") or {}, # mutable-ok: one-shot JSON payload + "usage": response.get("usage") or {}, } return ( _sse_event("message_start", message_start_payload), *content_events, _sse_event("message_delta", message_delta_payload), - _sse_event("message_stop", {"type": "message_stop"}), # mutable-ok: one-shot JSON payload + _sse_event("message_stop", {"type": "message_stop"}), ) def _anthropic_content_block_events(index: int, block: Mapping[str, object]) -> tuple[bytes, ...]: start_block, deltas = _anthropic_content_block_start_and_deltas(block) - start_payload: Final = { # mutable-ok: one-shot payload + start_payload: Final = { "type": "content_block_start", "index": index, "content_block": start_block, } - stop_payload: Final = { # mutable-ok: one-shot payload + stop_payload: Final = { "type": "content_block_stop", "index": index, } delta_events: Final = tuple( _sse_event( "content_block_delta", - {"type": "content_block_delta", "index": index, "delta": delta}, # mutable-ok: one-shot payload + {"type": "content_block_delta", "index": index, "delta": delta}, ) for delta in deltas ) diff --git a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py index f753e87fee3..771117cb0be 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py @@ -118,20 +118,20 @@ class AnthropicResponsesStreamWrapper: if block_idx < 0: redacted_idx: Final = self._open_block( item_id, - {"type": "redacted_thinking", "data": signature}, # mutable-ok: API message payload + {"type": "redacted_thinking", "data": signature}, ) - stop: Final = {"type": "content_block_stop", "index": redacted_idx} # mutable-ok: API message payload + stop: Final = {"type": "content_block_stop", "index": redacted_idx} self._chunk_queue.append(stop) return if signature is not None: self._chunk_queue.append( - { # mutable-ok: API message payload + { "type": "content_block_delta", "index": block_idx, - "delta": {"type": "signature_delta", "signature": signature}, # mutable-ok: API message payload + "delta": {"type": "signature_delta", "signature": signature}, } ) - self._chunk_queue.append({"type": "content_block_stop", "index": block_idx}) # mutable-ok: API message payload + self._chunk_queue.append({"type": "content_block_stop", "index": block_idx}) def _process_event(self, event: object) -> None: """Convert one Responses API event into zero or more Anthropic chunks queued for emission.""" @@ -223,10 +223,10 @@ class AnthropicResponsesStreamWrapper: if part_block_idx < 0 or not isinstance(summary_index, int) or summary_index == 0: return self._chunk_queue.append( - { # mutable-ok: API message payload + { "type": "content_block_delta", "index": part_block_idx, - "delta": { # mutable-ok: API message payload + "delta": { "type": "thinking_delta", "thinking": REASONING_SUMMARY_PART_SEPARATOR, }, @@ -244,7 +244,7 @@ class AnthropicResponsesStreamWrapper: return block_idx = self._open_block( item_id, - {"type": "thinking", "thinking": "", "signature": ""}, # mutable-ok: API message payload + {"type": "thinking", "thinking": "", "signature": ""}, ) self._chunk_queue.append( { @@ -327,16 +327,10 @@ class AnthropicResponsesStreamWrapper: else AnthropicUsage(input_tokens=0, output_tokens=0) ) - message_delta_payload: Final = { # mutable-ok: fresh message_delta payload built per chunk + message_delta_payload: Final = { "stop_reason": stop_reason, "stop_sequence": None, - **( - { # mutable-ok: fresh message_delta stop_details entry built per chunk - "stop_details": refusal_stop_details(refusal_text) - } - if stop_reason == "refusal" - else {} # mutable-ok: empty spread placeholder for non-refusal stop - ), + **({"stop_details": refusal_stop_details(refusal_text)} if stop_reason == "refusal" else {}), } self._chunk_queue.append( 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..dcfbc3239cb 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py @@ -98,7 +98,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: @staticmethod def _translate_anthropic_document_block_to_file_part( block: Mapping[str, object], - ) -> dict[str, str] | None: # mutable-ok: API message payload + ) -> dict[str, str] | None: """Convert an Anthropic document block to a Responses input_file part.""" raw_source: Final = block.get("source") if not isinstance(raw_source, Mapping): @@ -115,7 +115,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: ) raw_title: Final = block.get("title") filename: Final = raw_title if isinstance(raw_title, str) and raw_title else "document.pdf" - return { # mutable-ok: API message payload + return { "type": "input_file", "filename": filename, "file_data": f"data:{media_type};base64,{data}", @@ -124,21 +124,19 @@ class LiteLLMAnthropicToResponsesAPIAdapter: url: Final = source.get("url") if not isinstance(url, str) or not url: return None - return {"type": "input_file", "file_url": url} # mutable-ok: API message payload + return {"type": "input_file", "file_url": url} return None @staticmethod def _tool_result_output_value( output_text: str, - file_parts: tuple[dict[str, str], ...], # mutable-ok: json content parts + file_parts: tuple[dict[str, str], ...], ) -> str | list[dict[str, str]]: # mutable-ok: API message payload """Plain string output, or a part list when document file parts are present.""" if not file_parts: return output_text - text_parts: Final = ( - [{"type": "input_text", "text": output_text}] if output_text else [] # mutable-ok: API message payload - ) - return [*text_parts, *file_parts] # mutable-ok: API message payload + text_parts: Final = [{"type": "input_text", "text": output_text}] if output_text else [] + return [*text_parts, *file_parts] @staticmethod def _translate_midturn_system_content_to_responses( @@ -146,15 +144,11 @@ class LiteLLMAnthropicToResponsesAPIAdapter: ) -> list[dict[str, object]]: # mutable-ok: API message payload """Convert in-sequence system content to Responses input-text parts.""" if isinstance(content, str): - return ( - [{"type": "input_text", "text": content}] if content else [] # mutable-ok: API message payload - ) # mutable-ok: API message payload + return [{"type": "input_text", "text": content}] if content else [] if not isinstance(content, list): - return [] # mutable-ok: API message payload - return [ # mutable-ok: API message payload - with_prompt_cache_breakpoint( - {"type": "input_text", "text": text}, block.get("prompt_cache_breakpoint") - ) # mutable-ok: API message payload + return [] + return [ + with_prompt_cache_breakpoint({"type": "input_text", "text": text}, block.get("prompt_cache_breakpoint")) for block in content if isinstance(block, dict) and block.get("type") == "text" and (text := block.get("text")) # pyright: ignore[reportUnnecessaryIsInstance] # untrusted client payload ] @@ -171,7 +165,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: cls, summary: Iterable[object], encrypted_content: object, - ) -> dict[str, Any] | None: # mutable-ok: API message payload + ) -> dict[str, Any] | None: """The one Anthropic block for a Responses reasoning item. The item's encrypted reasoning rides the block's opaque field (`signature`, or @@ -198,21 +192,19 @@ class LiteLLMAnthropicToResponsesAPIAdapter: return "thinking" if block.get("type") in ("thinking", "redacted_thinking") else f"block:{index}" @classmethod - def _assistant_group_to_input_items( - cls, group: tuple[Mapping[str, object], ...] - ) -> tuple[dict[str, Any], ...]: # mutable-ok: API message payload + def _assistant_group_to_input_items(cls, group: tuple[Mapping[str, object], ...]) -> tuple[dict[str, Any], ...]: first: Final = group[0] btype: Final = first.get("type") if btype in ("thinking", "redacted_thinking"): replayed: Final = responses_reasoning_items_from_thinking_blocks(group) - return tuple(dict(item) for item in replayed) # mutable-ok: API message payload + return tuple(dict(item) for item in replayed) if btype == "tool_use": return ( - { # mutable-ok: API message payload + { "type": "function_call", "call_id": first.get("id", ""), "name": first.get("name", ""), - "arguments": json.dumps(first.get("input", {})), # mutable-ok: API message payload + "arguments": json.dumps(first.get("input", {})), }, ) return () @@ -241,7 +233,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: system_parts = self._translate_midturn_system_content_to_responses(m.get("content")) if system_parts: input_items.append( - { # mutable-ok: API message payload + { "type": "message", "role": "system", "content": system_parts, @@ -324,8 +316,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: else TOOL_RESULT_IMAGE_PLACEHOLDER ) tool_image_parts.extend( - {"type": "input_image", "image_url": url} # mutable-ok: json content part - for url in image_urls + {"type": "input_image", "image_url": url} for url in image_urls ) else: output_text = str(inner) @@ -338,15 +329,15 @@ class LiteLLMAnthropicToResponsesAPIAdapter: } ) if tool_image_parts: - boundary_part = { # mutable-ok: json content part + boundary_part = { "type": "input_text", "text": TOOL_RESULT_IMAGE_BOUNDARY, } input_items.append( - { # mutable-ok: json input item + { "type": "message", "role": "user", - "content": [boundary_part, *tool_image_parts], # mutable-ok: json content list + "content": [boundary_part, *tool_image_parts], } ) if user_parts: @@ -375,7 +366,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: for item in self._assistant_group_to_input_items(tuple(block for _, block in group)) ) asst_parts: list[dict[str, Any]] = [ # mutable-ok: API message payload - {"type": "output_text", "text": block.get("text", "")} # mutable-ok: API message payload + {"type": "output_text", "text": block.get("text", "")} for block in blocks if block.get("type") == "text" ] @@ -533,7 +524,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: if developer_parts: input_items.insert( 0, - { # mutable-ok: API message payload + { "type": "message", "role": "developer", "content": developer_parts, @@ -545,7 +536,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: "input": input_items, } if include_encrypted_reasoning: - responses_kwargs["include"] = [RESPONSES_INCLUDE_ENCRYPTED_REASONING] # mutable-ok: API request payload + responses_kwargs["include"] = [RESPONSES_INCLUDE_ENCRYPTED_REASONING] if system and not developer_parts: if isinstance(system, str): diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 587165e6991..dd4081dadfc 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -1407,7 +1407,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict + additional_args={ "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(azure_client.base_url), }, @@ -1451,7 +1451,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict + additional_args={ "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(azure_client.base_url), }, diff --git a/litellm/llms/azure/chat/gpt_transformation.py b/litellm/llms/azure/chat/gpt_transformation.py index ed16d7f3de0..51eb3a206c2 100644 --- a/litellm/llms/azure/chat/gpt_transformation.py +++ b/litellm/llms/azure/chat/gpt_transformation.py @@ -44,7 +44,7 @@ def sanitized_tools_update(optional_params: Mapping[str, object]) -> Mapping[str tools: Final = optional_params.get("tools") if not isinstance(tools, list): return _NO_TOOLS_UPDATE - sanitized: Final = [ # mutable-ok: request tools are a JSON list + sanitized: Final = [ tool_with_sanitized_parameters(tool, flatten_combinators_and_drop_non_python_regex_patterns) if isinstance(tool, dict) else tool diff --git a/litellm/llms/azure/chat/o_series_transformation.py b/litellm/llms/azure/chat/o_series_transformation.py index 09d8075e857..80911e64feb 100644 --- a/litellm/llms/azure/chat/o_series_transformation.py +++ b/litellm/llms/azure/chat/o_series_transformation.py @@ -109,7 +109,7 @@ class AzureOpenAIO1Config(OpenAIOSeriesConfig): headers: dict, ) -> dict: model = model.replace("o_series/", "") # handle o_series/my-random-deployment-name - flattened_params: Final = { # mutable-ok: transform_request's contract takes a plain JSON params dict + flattened_params: Final = { **optional_params, **sanitized_tools_update(optional_params), } diff --git a/litellm/llms/azure/search/transformation.py b/litellm/llms/azure/search/transformation.py index 0754c9b1fda..1b0e357e9d6 100644 --- a/litellm/llms/azure/search/transformation.py +++ b/litellm/llms/azure/search/transformation.py @@ -278,18 +278,18 @@ class BingGroundingSearchConfig(BaseSearchConfig): def validate_environment( self, - headers: dict[str, str], # mutable-ok: BaseSearchConfig.validate_environment signature + headers: dict[str, str], api_key: str | None = None, api_base: str | None = None, **kwargs: object, # kwargs-ok: BaseSearchConfig.validate_environment signature - ) -> dict[str, str]: # mutable-ok: the http handler passes this straight to httpx as headers + ) -> dict[str, str]: """ Validate environment and return headers. Returns a new dict rather than mutating ``headers``: the http handler calls this a second time after ``litellm/search/main.py`` already did, so it has to be idempotent. """ - return { # mutable-ok: httpx requires a plain dict of headers + return { **headers, **self._auth_header(api_key, api_base), "Content-Type": "application/json", @@ -330,7 +330,7 @@ class BingGroundingSearchConfig(BaseSearchConfig): def get_complete_url( self, api_base: str | None, - optional_params: dict[str, object], # mutable-ok: BaseSearchConfig.get_complete_url signature + optional_params: dict[str, object], data: dict[str, object] | list[dict[str, object]] | None = None, # mutable-ok: base signature **kwargs: object, # kwargs-ok: BaseSearchConfig.get_complete_url signature ) -> str: @@ -348,9 +348,9 @@ class BingGroundingSearchConfig(BaseSearchConfig): def transform_search_request( self, query: str | list[str], # mutable-ok: BaseSearchConfig.transform_search_request signature - optional_params: dict[str, object], # mutable-ok: base signature + optional_params: dict[str, object], **kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_request signature - ) -> dict[str, object]: # mutable-ok: the http handler passes this straight to httpx as the JSON body + ) -> dict[str, object]: """ Transform Search request to the Foundry Responses API format. @@ -387,7 +387,7 @@ class BingGroundingSearchConfig(BaseSearchConfig): raise self.get_error_class( error_message=f"response does not match the Foundry Responses API schema: {e}", status_code=raw_response.status_code, - headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature + headers=dict(raw_response.headers), ) if parsed.status == "failed": detail: Final = ( @@ -408,7 +408,7 @@ class BingGroundingSearchConfig(BaseSearchConfig): return self.get_error_class( error_message=detail, status_code=_UPSTREAM_ERROR_STATUS, - headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature + headers=dict(raw_response.headers), ) def _priced(self, results: tuple[SearchResult, ...]) -> SearchResponse: @@ -416,23 +416,19 @@ class BingGroundingSearchConfig(BaseSearchConfig): inherit the connection-mode ``bing_grounding/search`` price; zero its per-query cost while leaving connection mode to the cost map.""" response: Final = SearchResponse( - results=list(results), # mutable-ok: SearchResponse.results is list[SearchResult] + results=list(results), object="search", ) if get_secret_str(CONNECTION_ID_ENV): return response - response._hidden_params[ - "additional_headers" - ] = { # mutable-ok: response_cost_calculator writes into _hidden_params - _RESPONSE_COST_HEADER: 0.0 - } + response._hidden_params["additional_headers"] = {_RESPONSE_COST_HEADER: 0.0} return response def get_error_class( self, error_message: str, status_code: int, - headers: dict[str, str], # mutable-ok: BaseSearchConfig.get_error_class signature + headers: dict[str, str], ) -> Exception: detail: Final = _unwrap_error_detail(error_message).rstrip(". ") return BaseLLMException( diff --git a/litellm/llms/azure_ai/azure_model_router/transformation.py b/litellm/llms/azure_ai/azure_model_router/transformation.py index 9e35e396e15..c88c2ead545 100644 --- a/litellm/llms/azure_ai/azure_model_router/transformation.py +++ b/litellm/llms/azure_ai/azure_model_router/transformation.py @@ -102,7 +102,7 @@ class AzureModelRouterConfig(AzureAIStudioConfig): if selected_model: # Rebuilt rather than mutated in place: ModelResponseBase declares _hidden_params as a # class-level dict, so an in-place write can bleed into unrelated responses. - transformed_response._hidden_params = { # pyright: ignore[reportPrivateUsage] # ModelResponse exposes no public hidden-params setter # mutable-ok: ModelResponse requires _hidden_params to be a plain dict + transformed_response._hidden_params = { # pyright: ignore[reportPrivateUsage] # ModelResponse exposes no public hidden-params setter **get_hidden_params_dict(transformed_response), AZURE_MODEL_ROUTER_SELECTED_MODEL_KEY: selected_model, } diff --git a/litellm/llms/azure_ai/chat/transformation.py b/litellm/llms/azure_ai/chat/transformation.py index 00e1c1e25ba..9fd0756d1c2 100644 --- a/litellm/llms/azure_ai/chat/transformation.py +++ b/litellm/llms/azure_ai/chat/transformation.py @@ -91,11 +91,11 @@ class AzureAIStudioConfig(OpenAIConfig): def map_openai_params( self, - non_default_params: dict[str, object], # mutable-ok: OpenAIConfig.map_openai_params signature - optional_params: dict[str, object], # mutable-ok: OpenAIConfig.map_openai_params signature + non_default_params: dict[str, object], + optional_params: dict[str, object], model: str, drop_params: bool, - ) -> dict[str, object]: # mutable-ok: OpenAIConfig.map_openai_params signature + ) -> dict[str, object]: if not azureAIGPT5Config.is_model_gpt_5_model(model): return super().map_openai_params( non_default_params=non_default_params, diff --git a/litellm/llms/azure_ai/ocr/cohere_parse_transformation.py b/litellm/llms/azure_ai/ocr/cohere_parse_transformation.py index 121f970c59b..08c7bdb6329 100644 --- a/litellm/llms/azure_ai/ocr/cohere_parse_transformation.py +++ b/litellm/llms/azure_ai/ocr/cohere_parse_transformation.py @@ -39,7 +39,7 @@ class AzureAICohereParseConfig(CohereParseConfig): api_base: str | None = None, litellm_params: Mapping[str, object] | None = None, **kwargs: object, # kwargs-ok: BaseOCRConfig.validate_environment signature - ) -> dict[str, str]: # mutable-ok: BaseOCRConfig signature + ) -> dict[str, str]: resolved_base: Final = api_base or get_secret_str(AZURE_AI_API_BASE_ENV_VAR) if resolved_base is None: raise ValueError( @@ -47,7 +47,7 @@ class AzureAICohereParseConfig(CohereParseConfig): "or pass api_base parameter" ) resolved_key: Final = api_key or get_secret_str(AZURE_AI_API_KEY_ENV_VAR) - return { # mutable-ok: BaseOCRConfig signature + return { **get_azure_ai_auth_headers(api_key=resolved_key, litellm_params=litellm_params), "Content-Type": "application/json", **headers, diff --git a/litellm/llms/azure_ai/passthrough/transformation.py b/litellm/llms/azure_ai/passthrough/transformation.py index f2be1d95593..987aea26c78 100644 --- a/litellm/llms/azure_ai/passthrough/transformation.py +++ b/litellm/llms/azure_ai/passthrough/transformation.py @@ -147,13 +147,13 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig): litellm_params: Mapping[str, object], api_key: str | None = None, api_base: str | None = None, - ) -> dict[str, str]: # mutable-ok: base class contract returns dict for httpx + ) -> dict[str, str]: auth_headers: Final = get_azure_ai_auth_headers( api_key=api_key, litellm_params=litellm_params, api_key_header=api_key_header_for_base(api_base), ) - return {**headers, **auth_headers} # mutable-ok: base class contract returns dict for httpx + return {**headers, **auth_headers} def logging_non_streaming_response( self, @@ -170,7 +170,7 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig): model=model, custom_llm_provider=custom_llm_provider, httpx_response=httpx_response, - request_data=dict(request_data), # mutable-ok: AzurePassthroughConfig wants a dict + request_data=dict(request_data), logging_obj=logging_obj, endpoint=endpoint, ) @@ -196,7 +196,7 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig): ocr_config.get_complete_url( api_base=relayed_origin, model=model, - optional_params={}, # mutable-ok: BaseOCRConfig wants a dict + optional_params={}, ) ) known_prefixes: Final = (model, model_group_from(logging_obj.litellm_params)) diff --git a/litellm/llms/base_llm/files/transformation.py b/litellm/llms/base_llm/files/transformation.py index 6d16a1cea69..08d92f54c1b 100644 --- a/litellm/llms/base_llm/files/transformation.py +++ b/litellm/llms/base_llm/files/transformation.py @@ -164,7 +164,7 @@ class BaseFilesConfig(BaseConfig): self, raw_response: httpx.Response, optional_params: Mapping[str, object], - litellm_params: dict, # mutable-ok: carries provider stashes from the request transform to the response one + litellm_params: dict, ) -> tuple[str, dict[str, str]] | None: """Request for the page after `raw_response`, or None once the listing is complete.""" return None diff --git a/litellm/llms/base_llm/guardrail_translation/base_translation.py b/litellm/llms/base_llm/guardrail_translation/base_translation.py index f1143425ced..b8cdeca27ca 100644 --- a/litellm/llms/base_llm/guardrail_translation/base_translation.py +++ b/litellm/llms/base_llm/guardrail_translation/base_translation.py @@ -117,7 +117,7 @@ class BaseTranslation(ABC): @staticmethod def merge_user_api_key_metadata_into_request( - request_data: dict[str, Any], # mutable-ok: proxy hooks share and mutate the request payload dict in place + request_data: dict[str, Any], user_api_key_dict: Optional["UserAPIKeyAuth"], ) -> None: """ diff --git a/litellm/llms/base_llm/ocr/transformation.py b/litellm/llms/base_llm/ocr/transformation.py index bd67dbf1a2a..8c7308932b7 100644 --- a/litellm/llms/base_llm/ocr/transformation.py +++ b/litellm/llms/base_llm/ocr/transformation.py @@ -155,7 +155,7 @@ class BaseOCRConfig: return dynamic_api_key or api_key, dynamic_api_base or api_base def get_health_check_document(self) -> DocumentType: - return { # mutable-ok: litellm.aocr rejects any document that is not a dict + return { "type": "document_url", "document_url": HEALTH_CHECK_PDF_DATA_URI, } diff --git a/litellm/llms/base_llm/search/transformation.py b/litellm/llms/base_llm/search/transformation.py index 9eca3e69909..52c5d056d49 100644 --- a/litellm/llms/base_llm/search/transformation.py +++ b/litellm/llms/base_llm/search/transformation.py @@ -185,12 +185,12 @@ class BaseSearchConfig: def sign_request( self, - headers: dict[str, str], # mutable-ok: matches the request header dict every other hook on this base takes - optional_params: dict[str, object], # mutable-ok: matches every other hook on this base + headers: dict[str, str], + optional_params: dict[str, object], request_data: dict[str, object] | list[dict[str, object]], # mutable-ok: transform_search_request's body api_base: str, api_key: str | None = None, - ) -> tuple[dict[str, str], bytes | None]: # mutable-ok: the handler passes these headers straight to httpx + ) -> tuple[dict[str, str], bytes | None]: """ OPTIONAL @@ -268,7 +268,7 @@ class BaseSearchConfig: return self.get_error_class( error_message=error.response.text, status_code=error.response.status_code, - headers=dict(error.response.headers), # mutable-ok: provider error factories require dict headers + headers=dict(error.response.headers), ) def get_error_class( diff --git a/litellm/llms/base_llm/vector_store/transformation.py b/litellm/llms/base_llm/vector_store/transformation.py index 07b60cb4b72..28f6348e4d9 100644 --- a/litellm/llms/base_llm/vector_store/transformation.py +++ b/litellm/llms/base_llm/vector_store/transformation.py @@ -48,8 +48,8 @@ class LiteLLMVectorStoreEmbeddingExecutor: return litellm.embedding( # pyright: ignore[reportCallIssue, reportUnknownMemberType, reportUnknownVariableType] # provider kwargs are intentionally dynamic model=model, - input=[query], # mutable-ok: LiteLLM embedding requires a mutable input list - **dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream # mutable-ok: kwargs require a concrete dict + input=[query], + **dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream ) async def aembed(self, model: str, query: str, configuration: Mapping[str, object]) -> EmbeddingResponse: @@ -57,8 +57,8 @@ class LiteLLMVectorStoreEmbeddingExecutor: return await litellm.aembedding( # pyright: ignore[reportUnknownMemberType] # provider kwargs are intentionally dynamic model=model, - input=[query], # mutable-ok: LiteLLM embedding requires a mutable input list - **dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream # mutable-ok: kwargs require a concrete dict + input=[query], + **dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream ) @@ -105,7 +105,7 @@ class RouterVectorStoreEmbeddingExecutor: return LiteLLMVectorStoreEmbeddingExecutor().embed(model, query, embedding_kwargs) return self.router.embedding( # pyright: ignore[reportUnknownMemberType] # Router embedding input retains a legacy untyped list model=model, - input=[query], # mutable-ok: Router embedding requires a mutable input list + input=[query], **embedding_kwargs, # pyright: ignore[reportArgumentType] # provider kwargs are intentionally dynamic ) @@ -115,7 +115,7 @@ class RouterVectorStoreEmbeddingExecutor: return await LiteLLMVectorStoreEmbeddingExecutor().aembed(model, query, embedding_kwargs) return await self.router.aembedding( # pyright: ignore[reportUnknownMemberType] # Router embedding input retains a legacy untyped list model=model, - input=[query], # mutable-ok: Router embedding requires a mutable input list + input=[query], **embedding_kwargs, # pyright: ignore[reportArgumentType] # provider kwargs are intentionally dynamic ) @@ -429,4 +429,4 @@ class BaseDirectVectorStoreConfig(BaseVectorStoreConfig): return BaseVectorStoreAuthCredentials() def get_vector_store_endpoints_by_type(self) -> VectorStoreIndexEndpoints: - return VectorStoreIndexEndpoints(read=[], write=[]) # mutable-ok: the TypedDict declares list fields + return VectorStoreIndexEndpoints(read=[], write=[]) diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index f52c1cec6a8..5611163390a 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -1461,7 +1461,7 @@ class BaseAWSLLM(SignsRequestsWithAWS): @overload def _get_boto_credentials_from_optional_params( self, - optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place + optional_params: dict, model: str | None = None, bearer_token: None = None, ) -> Boto3CredentialsInfo: ... @@ -1469,7 +1469,7 @@ class BaseAWSLLM(SignsRequestsWithAWS): @overload def _get_boto_credentials_from_optional_params( self, - optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place + optional_params: dict, model: str | None = None, *, bearer_token: str, diff --git a/litellm/llms/bedrock/chat/converse_handler.py b/litellm/llms/bedrock/chat/converse_handler.py index d397420cb17..347239b9c2c 100644 --- a/litellm/llms/bedrock/chat/converse_handler.py +++ b/litellm/llms/bedrock/chat/converse_handler.py @@ -407,7 +407,7 @@ class BedrockConverseLLM(BaseAWSLLM): # resolved so both paths sign as the same principal. Bearer-token auth # resolves no SigV4 principal at all, and each path reads that token # itself. - rust_optional_params: Final = { # mutable-ok: json.dumps in the bridge rejects a mappingproxy + rust_optional_params: Final = { **optional_params, **_sigv4_principal(credentials), "aws_region_name": aws_region_name, @@ -421,8 +421,8 @@ class BedrockConverseLLM(BaseAWSLLM): stream=stream, ) if serves_via_rust: - rust_logging_args: Final = { # mutable-ok: logging callbacks read additional_args as a plain dict - "complete_input_dict": { # mutable-ok: same, and it is serialized alongside its parent + rust_logging_args: Final = { + "complete_input_dict": { "messages": messages, **optional_params, }, diff --git a/litellm/llms/bedrock/claude_platform/common_utils.py b/litellm/llms/bedrock/claude_platform/common_utils.py index fb7f2185ec5..57b26b7c92e 100644 --- a/litellm/llms/bedrock/claude_platform/common_utils.py +++ b/litellm/llms/bedrock/claude_platform/common_utils.py @@ -22,7 +22,7 @@ class BedrockClaudePlatformMixin(BaseAWSLLM): self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index cb2c70e74c8..d48d749857c 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -109,7 +109,7 @@ def merge_bedrock_aws_request_params( server. Requests may still provide AWS credentials when the deployment has no static credentials configured. """ - request_params: Final = {**optional_params, **litellm_params} # mutable-ok: AWS helpers require a plain dict + request_params: Final = {**optional_params, **litellm_params} has_static_deployment_credentials: Final = all( isinstance(litellm_params.get(key), str) and bool(litellm_params.get(key)) for key in ("aws_access_key_id", "aws_secret_access_key", "aws_region_name") @@ -231,7 +231,7 @@ def _bedrock_model_supports(model: str, key: str) -> bool: def apply_bedrock_invoke_structured_output( model: str, - request_body: dict[str, object], # mutable-ok: edited in place like siblings + request_body: dict[str, object], ) -> None: """ Route Anthropic structured-output params to what the Bedrock model supports. @@ -256,7 +256,7 @@ def apply_bedrock_invoke_structured_output( if isinstance(existing_output_config, dict): existing_output_config["format"] = schema_format else: - request_body["output_config"] = {"format": schema_format} # rebind-ok: out-param # mutable-ok: json + request_body["output_config"] = {"format": schema_format} # rebind-ok: out-param return verbose_logger.warning( @@ -273,7 +273,7 @@ def apply_bedrock_invoke_structured_output( def strip_unsupported_bedrock_invoke_output_config_keys( model: str, - request_body: dict[str, object], # mutable-ok: edited in place like siblings + request_body: dict[str, object], ) -> None: """ Drop ``output_config`` keys the Bedrock model does not accept. @@ -309,7 +309,7 @@ def strip_unsupported_bedrock_invoke_output_config_keys( if preserved_format is None: request_body.pop("output_config", None) else: - request_body["output_config"] = {"format": preserved_format} # rebind-ok: out-param # mutable-ok: json + request_body["output_config"] = {"format": preserved_format} # rebind-ok: out-param def normalize_custom_field_on_tools(request_body: dict) -> None: diff --git a/litellm/llms/bedrock/embed/embedding.py b/litellm/llms/bedrock/embed/embedding.py index 7efdfd3cebb..69c4d2421b9 100644 --- a/litellm/llms/bedrock/embed/embedding.py +++ b/litellm/llms/bedrock/embed/embedding.py @@ -59,14 +59,14 @@ class BedrockEmbedding(BaseAWSLLM): @overload def _load_credentials( self, - optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place + optional_params: dict, bearer_token: None = None, ) -> tuple[Credentials, str]: ... @overload def _load_credentials( self, - optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place + optional_params: dict, bearer_token: str, ) -> tuple[None, str]: ... diff --git a/litellm/llms/bedrock/files/transformation.py b/litellm/llms/bedrock/files/transformation.py index 6e2b0c12090..7e72dd9df58 100644 --- a/litellm/llms/bedrock/files/transformation.py +++ b/litellm/llms/bedrock/files/transformation.py @@ -1390,9 +1390,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig): url: Final = f"{target.endpoint_url}/{bucket_name}/" listing_query: Final = _listing_query(configured_prefix, purpose) continuation_query: Final = (("continuation-token", continuation_token),) if continuation_token else () - query: Final[dict[str, str]] = dict( # mutable-ok: the base files contract returns the query as a dict - listing_query + continuation_query - ) + query: Final[dict[str, str]] = dict(listing_query + continuation_query) signed_headers: Final = self._sign_s3_request_without_body( method="GET", api_base=f"{url}?{urlencode(query, quote_via=quote, safe='')}", @@ -1426,7 +1424,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig): _listed_managed_file(entry, bucket_name, configured_bucket_name, allow_legacy_cloud_file_ids) for entry in listing.iterfind("{*}Contents") ) - return [ # mutable-ok: the base files contract returns a list + return [ listed_file for listed_file in listed_files if listed_file is not None and (purpose is None or listed_file.purpose == purpose) @@ -1471,7 +1469,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig): request_params=target.request_params, ) litellm_params[S3_SIGNED_REQUEST_HEADERS_PARAM] = signed_headers # rebind-ok: handed to validate_environment - return url, {} # mutable-ok: the base files contract returns the query as a dict + return url, {} def _s3_request_target( self, @@ -1488,7 +1486,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig): ) region_preference: Final = request_params.s3_region_name or request_params.aws_region_name aws_region_name: Final = self._get_aws_region_name( - optional_params={"aws_region_name": region_preference}, # mutable-ok: BaseAWSLLM takes a dict + optional_params={"aws_region_name": region_preference}, model="", ) endpoint_url: Final = ( @@ -1534,7 +1532,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig): aws_request: Final = AWSRequest( # any-ok: botocore AWSRequest is untyped method=method, url=api_base, - headers={"x-amz-content-sha256": empty_body_hash}, # mutable-ok: botocore AWSRequest takes a dict + headers={"x-amz-content-sha256": empty_body_hash}, ) auth: Final = S3SigV4Auth(credentials, "s3", aws_region_name) # any-ok: botocore untyped auth.add_auth(aws_request) # any-ok: botocore request mutation is untyped diff --git a/litellm/llms/bedrock/image_edit/amazon_nova_canvas_image_edit_transformation.py b/litellm/llms/bedrock/image_edit/amazon_nova_canvas_image_edit_transformation.py index acb0cc8dcb7..133ddcdef05 100644 --- a/litellm/llms/bedrock/image_edit/amazon_nova_canvas_image_edit_transformation.py +++ b/litellm/llms/bedrock/image_edit/amazon_nova_canvas_image_edit_transformation.py @@ -233,7 +233,7 @@ class BedrockAmazonNovaCanvasImageEditConfig(BaseImageEditConfig): self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) diff --git a/litellm/llms/bedrock/image_edit/stability_transformation.py b/litellm/llms/bedrock/image_edit/stability_transformation.py index bc9a64f587a..ae3eec61f9b 100644 --- a/litellm/llms/bedrock/image_edit/stability_transformation.py +++ b/litellm/llms/bedrock/image_edit/stability_transformation.py @@ -89,7 +89,7 @@ class BedrockStabilityImageEditConfig(BaseImageEditConfig): self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index a715d150b4c..bd4faab3952 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -84,7 +84,7 @@ class AmazonAnthropicClaudeMessagesConfig( self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) diff --git a/litellm/llms/bedrock/passthrough/transformation.py b/litellm/llms/bedrock/passthrough/transformation.py index 6a120f41cb6..632e1e79482 100644 --- a/litellm/llms/bedrock/passthrough/transformation.py +++ b/litellm/llms/bedrock/passthrough/transformation.py @@ -129,7 +129,7 @@ class BedrockPassthroughConfig(BaseAWSLLM, BedrockModelInfo, BedrockEventStreamD self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) diff --git a/litellm/llms/bedrock/realtime/handler.py b/litellm/llms/bedrock/realtime/handler.py index ca2370303f2..ed22f3676ba 100644 --- a/litellm/llms/bedrock/realtime/handler.py +++ b/litellm/llms/bedrock/realtime/handler.py @@ -262,7 +262,7 @@ class BedrockRealtime(BaseAWSLLM): if logged_events: GLOBAL_LOGGING_WORKER.ensure_initialized_and_enqueue( logging_obj.dispatch_success_handlers( - list(logged_events), # mutable-ok: realtime spend logging requires a list result + list(logged_events), prefer_async_handlers=True, ) ) diff --git a/litellm/llms/bedrock/realtime/transformation.py b/litellm/llms/bedrock/realtime/transformation.py index 3b972961940..a3e7da06537 100644 --- a/litellm/llms/bedrock/realtime/transformation.py +++ b/litellm/llms/bedrock/realtime/transformation.py @@ -127,7 +127,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) @@ -887,7 +887,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): id=f"resp_{uuid.uuid4()}", status="completed", conversation_id=f"conv_{uuid.uuid4()}", - usage=dict(usage), # mutable-ok: OpenAIRealtimeResponseDoneObject types usage as plain dict + usage=dict(usage), ), ) return (leftover_done,) diff --git a/litellm/llms/bedrock/search/transformation.py b/litellm/llms/bedrock/search/transformation.py index e7d706c3731..01bd1391cc5 100644 --- a/litellm/llms/bedrock/search/transformation.py +++ b/litellm/llms/bedrock/search/transformation.py @@ -165,11 +165,11 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): def validate_environment( self, - headers: dict, # mutable-ok: BaseSearchConfig hands providers the mutable request header dict + headers: dict, api_key: str | None = None, api_base: str | None = None, **kwargs: object, # kwargs-ok: BaseSearchConfig.validate_environment forwards provider-specific extras - ) -> dict: # mutable-ok: the handler passes these headers straight to httpx, which wants a dict + ) -> dict: """ Set MCP transport headers. Per the MCP Streamable HTTP transport spec, the client MUST accept both application/json and text/event-stream, and @@ -178,7 +178,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): Authentication itself happens in sign_request(): bearer token for CUSTOM_JWT gateways, AWS SigV4 for AWS_IAM gateways. """ - return { # mutable-ok: httpx request headers are a dict + return { **headers, "Content-Type": "application/json", "Accept": "application/json, text/event-stream", @@ -189,7 +189,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): def get_complete_url( self, api_base: str | None, - optional_params: dict, # mutable-ok: BaseSearchConfig passes optional params as a dict + optional_params: dict, data: dict | list[dict] | None = None, # mutable-ok: BaseSearchConfig request bodies are JSON dicts **kwargs: object, # kwargs-ok: BaseSearchConfig.get_complete_url forwards provider-specific extras ) -> str: @@ -205,9 +205,9 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): def transform_search_request( self, query: str | list[str], # mutable-ok: BaseSearchConfig accepts a list of queries - optional_params: dict, # mutable-ok: BaseSearchConfig passes optional params as a dict + optional_params: dict, **kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_request forwards provider-specific extras - ) -> dict: # mutable-ok: the JSON-RPC body is serialized as a JSON object + ) -> dict: """ Transform Search request to an MCP tools/call request. @@ -234,13 +234,13 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): "Other gateway tools cannot be invoked through this provider." ) - return { # mutable-ok: JSON-RPC request bodies are JSON objects + return { "jsonrpc": "2.0", "id": 1, "method": "tools/call", - "params": { # mutable-ok: JSON-RPC request bodies are JSON objects + "params": { "name": tool_name, - "arguments": { # mutable-ok: JSON-RPC request bodies are JSON objects + "arguments": { "query": joined_query[:AGENTCORE_MAX_QUERY_LENGTH], "maxResults": optional_params.get("max_results", AGENTCORE_DEFAULT_MAX_RESULTS), }, @@ -249,12 +249,12 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): def sign_request( self, - headers: dict[str, str], # mutable-ok: BaseSearchConfig hands providers the mutable request header dict - optional_params: dict[str, object], # mutable-ok: BaseSearchConfig passes optional params as a dict + headers: dict[str, str], + optional_params: dict[str, object], request_data: dict[str, object] | list[dict[str, object]], # mutable-ok: request bodies are JSON dicts api_base: str, api_key: str | None = None, - ) -> tuple[dict[str, str], bytes | None]: # mutable-ok: BaseSearchConfig.sign_request returns httpx headers + ) -> tuple[dict[str, str], bytes | None]: """ Authenticate the MCP request. @@ -286,7 +286,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): default_api_base=api_base if gateway_host_match else None, ) if bearer_token: - bearer_headers: Final = { # mutable-ok: httpx request headers are a dict + bearer_headers: Final = { **headers, "Authorization": f"Bearer {bearer_token}", } @@ -302,7 +302,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): signing_params: Final = ( optional_params if optional_params.get("aws_region_name") is not None - else { # mutable-ok: BaseAWSLLM._sign_request takes optional params as a dict + else { **optional_params, "aws_region_name": self._signing_region(api_base), } @@ -398,7 +398,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): structured: Final = result.get("structuredContent") if isinstance(result, Mapping) else None items: Final = text_items or _result_items(structured) - results: Final = [_to_search_result(item) for item in items] # mutable-ok: pydantic list field + results: Final = [_to_search_result(item) for item in items] return SearchResponse(results=results, object="search") @@ -448,7 +448,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): self, error_message: str, status_code: int, - headers: dict, # mutable-ok: BaseSearchConfig.get_error_class takes the response headers as a dict + headers: dict, ) -> Exception: return BedrockError( status_code=status_code, diff --git a/litellm/llms/bedrock/vector_stores/transformation.py b/litellm/llms/bedrock/vector_stores/transformation.py index 27c90c9d71e..4ca9e7b8e85 100644 --- a/litellm/llms/bedrock/vector_stores/transformation.py +++ b/litellm/llms/bedrock/vector_stores/transformation.py @@ -43,7 +43,7 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM): self, error_message: str, status_code: int, - headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict + headers: dict[str, object] | httpx.Headers, ) -> BedrockError: return BedrockError(status_code=status_code, message=error_message, headers=headers) diff --git a/litellm/llms/bedrock_mantle/passthrough/transformation.py b/litellm/llms/bedrock_mantle/passthrough/transformation.py index e6b831efa57..07ebff436ab 100644 --- a/litellm/llms/bedrock_mantle/passthrough/transformation.py +++ b/litellm/llms/bedrock_mantle/passthrough/transformation.py @@ -55,7 +55,7 @@ class BedrockMantlePassthroughConfig(BedrockPassthroughConfig): model: str, custom_llm_provider: str, httpx_response: Response, - request_data: dict, # mutable-ok: mirrors the inherited BedrockPassthroughConfig signature + request_data: dict, logging_obj: Logging, endpoint: str, ) -> Optional["CostResponseTypes"]: diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index 53a3e634adf..0dde02d0b85 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -214,15 +214,11 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI summary, sorted(_BEDROCK_MANTLE_OPENAI_PATH_SUPPORTED_REASONING_SUMMARIES), ) - stripped: Final = { # mutable-ok: map_openai_params contract returns a plain dict - key: value for key, value in reasoning.items() if key != "summary" - } + stripped: Final = {key: value for key, value in reasoning.items() if key != "summary"} return ( - {**params, "reasoning": stripped} # mutable-ok: map_openai_params contract returns a plain dict + {**params, "reasoning": stripped} if stripped - else { # mutable-ok: map_openai_params contract returns a plain dict - key: value for key, value in params.items() if key != "reasoning" - } + else {key: value for key, value in params.items() if key != "reasoning"} ) def transform_responses_api_request( @@ -372,7 +368,7 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI "Bedrock Mantle Responses API: rewrote Codex input item type(s) %s that Mantle rejects.", rewritten_types, ) - kept: Final = [item for item, _ in normalized if item is not None] # mutable-ok: ResponseInputParam is a list + kept: Final = [item for item, _ in normalized if item is not None] return kept # pyright: ignore[reportReturnType] # Codex passthrough items sit outside the OpenAI input union def map_openai_params( diff --git a/litellm/llms/cohere/ocr/transformation.py b/litellm/llms/cohere/ocr/transformation.py index b55ff4a3cbf..535383682ed 100644 --- a/litellm/llms/cohere/ocr/transformation.py +++ b/litellm/llms/cohere/ocr/transformation.py @@ -139,13 +139,13 @@ class CohereParseConfig(BaseOCRConfig): """Cohere Parse, an image-only document understanding endpoint returning markdown or blocks.""" def get_supported_ocr_params(self, model: str) -> list[str]: # mutable-ok: BaseOCRConfig signature - return list(COHERE_PARSE_SUPPORTED_PARAMS) # mutable-ok: BaseOCRConfig signature + return list(COHERE_PARSE_SUPPORTED_PARAMS) def get_api_key_env_var(self) -> str | None: return COHERE_API_KEY_ENV_VAR def get_health_check_document(self) -> DocumentType: - return { # mutable-ok: litellm.aocr rejects any document that is not a dict + return { "type": "image_url", "image_url": COHERE_PARSE_HEALTH_CHECK_IMAGE_DATA_URI, } @@ -158,7 +158,7 @@ class CohereParseConfig(BaseOCRConfig): non_default_params: Mapping[str, object], optional_params: Mapping[str, object], model: str, - ) -> dict[str, object]: # mutable-ok: BaseOCRConfig signature + ) -> dict[str, object]: output_format: Final = non_default_params.get(COHERE_PARSE_OUTPUT_FORMAT_PARAM) if output_format is not None and output_format not in COHERE_PARSE_OUTPUT_FORMATS: raise UnsupportedParamsError( @@ -179,7 +179,7 @@ class CohereParseConfig(BaseOCRConfig): ) if value is not None ) - return {**optional_params, **dict(overrides)} # mutable-ok: BaseOCRConfig signature + return {**optional_params, **dict(overrides)} def validate_environment( self, @@ -189,14 +189,14 @@ class CohereParseConfig(BaseOCRConfig): api_base: str | None = None, litellm_params: Mapping[str, object] | None = None, **kwargs: object, # kwargs-ok: BaseOCRConfig.validate_environment signature - ) -> dict[str, str]: # mutable-ok: BaseOCRConfig signature + ) -> dict[str, str]: resolved_key: Final = api_key or get_secret_str(COHERE_API_KEY_ENV_VAR) if resolved_key is None: raise ValueError( f"Missing {COHERE_API_KEY_ENV_VAR} - set it in the environment or pass api_key to " "litellm.ocr()/litellm.aocr()" ) - return { # mutable-ok: BaseOCRConfig signature + return { "Authorization": f"Bearer {resolved_key}", "Content-Type": "application/json", **headers, @@ -242,7 +242,7 @@ class CohereParseConfig(BaseOCRConfig): optional_params.get(COHERE_PARSE_OUTPUT_FORMAT_PARAM, COHERE_PARSE_DEFAULT_OUTPUT_FORMAT) ), } - return OCRRequestData(data=dict(body), files=None) # mutable-ok: OCRRequestData.data is a dict + return OCRRequestData(data=dict(body), files=None) def transform_ocr_request( self, @@ -276,9 +276,7 @@ class CohereParseConfig(BaseOCRConfig): ) -> OCRResponse: native: Final = _NATIVE_RESPONSE_ADAPTER.validate_python(raw_response.json()) parsed: Final = _CohereParseResponse.model_validate(native) - pages: Final = [ # mutable-ok: OCRResponse.pages is a list - _normalize_page(page, position) for position, page in enumerate(parsed.pages) - ] + pages: Final = [_normalize_page(page, position) for position, page in enumerate(parsed.pages)] billed_pages: Final = _billed_pages(parsed) response: Final = OCRResponse( pages=pages, diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 2fe4130a310..49d58f4e28f 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -544,8 +544,8 @@ class BaseLLMHTTPHandler: ) def sign_and_log( - transformed: dict[str, object], # mutable-ok: async_completion takes dict - ) -> tuple[dict[str, object], dict[str, object], bytes | None]: # mutable-ok: async_completion takes dict + transformed: dict[str, object], + ) -> tuple[dict[str, object], dict[str, object], bytes | None]: data: Final = {**transformed, **extra_body} if extra_body is not None else transformed signed: Final = cast( # cast-ok: sign_request is declared as a bare dict "tuple[dict[str, object], bytes | None]", @@ -577,8 +577,8 @@ class BaseLLMHTTPHandler: return data, signed[0], signed[1] def dispatch_async( - data: dict[str, object], # mutable-ok: async_completion takes dict - signed_headers: dict[str, object], # mutable-ok: async_completion takes dict + data: dict[str, object], + signed_headers: dict[str, object], signed_json_body: bytes | None, ) -> Coroutine[object, object, ModelResponse | CustomStreamWrapper]: async_client: Final = client if isinstance(client, AsyncHTTPHandler) else None @@ -2817,7 +2817,7 @@ class BaseLLMHTTPHandler: ) if self._has_agentic_completion_hook(logging_obj): - agentic_kwargs: Final = dict(litellm_params) # mutable-ok: agentic hooks mutate kwargs in place + agentic_kwargs: Final = dict(litellm_params) final_response: Final = run_async_function( self._call_agentic_completion_hooks, response=initial_response, @@ -3008,7 +3008,7 @@ class BaseLLMHTTPHandler: logging_obj=logging_obj, ) - agentic_kwargs: Final = dict(litellm_params) # mutable-ok: agentic hooks mutate kwargs in place + agentic_kwargs: Final = dict(litellm_params) final_response: Final = await self._call_agentic_completion_hooks( response=initial_response, model=model, @@ -4988,9 +4988,7 @@ class BaseLLMHTTPHandler: files_per_page: Final = self._files_per_listing_page( response, provider_config, logging_obj, litellm_params, headers, sync_httpx_client, timeout ) - return [ # mutable-ok: the files contract returns the listing as a list - listed_file for page_files in files_per_page for listed_file in page_files - ] + return [listed_file for page_files in files_per_page for listed_file in page_files] async def async_list_files( self, @@ -5045,17 +5043,15 @@ class BaseLLMHTTPHandler: files_per_page: Final = self._files_per_async_listing_page( response, provider_config, logging_obj, litellm_params, headers, async_httpx_client, timeout ) - return [ # mutable-ok: the files contract returns the listing as a list - listed_file async for page_files in files_per_page for listed_file in page_files - ] + return [listed_file async for page_files in files_per_page for listed_file in page_files] def _files_per_listing_page( self, first_page: httpx.Response, provider_config: BaseFilesConfig, logging_obj: LiteLLMLoggingObj, - litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict - headers: dict, # mutable-ok: handed to validate_environment, which types it as a dict + litellm_params: dict, + headers: dict, client: HTTPHandler, timeout: float | httpx.Timeout | None, ) -> Iterator[list[OpenAIFileObject]]: # mutable-ok: each page arrives as the list the files contract returns @@ -5082,8 +5078,8 @@ class BaseLLMHTTPHandler: first_page: httpx.Response, provider_config: BaseFilesConfig, logging_obj: LiteLLMLoggingObj, - litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict - headers: dict, # mutable-ok: handed to validate_environment, which types it as a dict + litellm_params: dict, + headers: dict, client: AsyncHTTPHandler, timeout: float | httpx.Timeout | None, ) -> AsyncIterator[list[OpenAIFileObject]]: # mutable-ok: each page arrives as the list the files contract returns @@ -5108,9 +5104,9 @@ class BaseLLMHTTPHandler: def _next_listing_page_headers( self, provider_config: BaseFilesConfig, - headers: dict, # mutable-ok: handed to validate_environment, which types it as a dict - litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict - ) -> dict: # mutable-ok: validate_environment returns the header dict the files contract types + headers: dict, + litellm_params: dict, + ) -> dict: return provider_config.validate_environment( api_key=litellm_params.get("api_key"), headers=headers, @@ -5124,9 +5120,9 @@ class BaseLLMHTTPHandler: self, latest_page: httpx.Response, provider_config: BaseFilesConfig, - litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict + litellm_params: dict, listed_count: int, - ) -> tuple[str, dict[str, str]] | None: # mutable-ok: the base files contract returns the query as a dict + ) -> tuple[str, dict[str, str]] | None: if listed_count >= MAX_FILE_LIST_LIMIT: return None return provider_config.transform_list_files_next_request( @@ -9863,7 +9859,7 @@ class BaseLLMHTTPHandler: logging_obj.pre_call( input="", api_key="", - additional_args={ # mutable-ok: pre_call's additional_args contract is a dict + additional_args={ "query": query, "vector_store_id": vector_store_id, "api_base": endpoint, @@ -9899,7 +9895,7 @@ class BaseLLMHTTPHandler: query=query, vector_store_search_optional_params=vector_store_search_optional_params, litellm_logging_obj=logging_obj, - litellm_params=dict(litellm_params), # mutable-ok: snapshot GenericLiteLLMParams into the Mapping shape + litellm_params=dict(litellm_params), embedding_executor=embedding_executor, timeout=timeout, ) @@ -10039,7 +10035,7 @@ class BaseLLMHTTPHandler: query=query, vector_store_search_optional_params=vector_store_search_optional_params, litellm_logging_obj=logging_obj, - litellm_params=dict(litellm_params), # mutable-ok: snapshot GenericLiteLLMParams into the Mapping shape + litellm_params=dict(litellm_params), embedding_executor=embedding_executor, timeout=timeout, ) diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index ea19a7c7ddf..4e428a23392 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -129,7 +129,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig): forward_images: Final = any( isinstance(message.get("content"), list) for message in messages ) and supports_vision(model=model, custom_llm_provider="deepseek") - transformed: Final = [ # mutable-ok: provider messages must stay JSON-array lists the base transform mutates + transformed: Final = [ self._forward_or_collapse_content(message=message, forward_images=forward_images) for message in messages ] @@ -155,7 +155,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig): collapsed: Final = convert_content_list_to_str(message=message) if not collapsed or collapsed == content: return message - collapsed_message: Final = {**message, "content": collapsed} # mutable-ok: wire messages are plain JSON dicts + collapsed_message: Final = {**message, "content": collapsed} return cast(AllMessageValues, collapsed_message) # cast-ok: TypedDict spread narrows to dict def _is_vision_forwardable_content(self, message: AllMessageValues, content: Sequence[object]) -> bool: @@ -204,8 +204,8 @@ class DeepSeekChatConfig(OpenAIGPTConfig): search_text: Final = extract_search_results_text(message_fields.get("search_results")) if not search_text: return message - forwarded_content: Final = [*content, {"type": "text", "text": search_text}] # mutable-ok: JSON-array content - forwarded: Final = { # mutable-ok: wire messages are plain JSON dicts + forwarded_content: Final = [*content, {"type": "text", "text": search_text}] + forwarded: Final = { **{key: value for key, value in message_fields.items() if key != "search_results"}, "content": forwarded_content, } diff --git a/litellm/llms/fal_ai/image_generation/gpt_image_2_transformation.py b/litellm/llms/fal_ai/image_generation/gpt_image_2_transformation.py index b91ae8ce2b0..80450c3ab63 100644 --- a/litellm/llms/fal_ai/image_generation/gpt_image_2_transformation.py +++ b/litellm/llms/fal_ai/image_generation/gpt_image_2_transformation.py @@ -60,12 +60,10 @@ class FalAIGPTImage2Config(FalAIBaseConfig): endpoint: Final[str] = model if model.startswith(self.MODEL_PREFIX) else f"{self.MODEL_PREFIX}{model}" return f"{base_url}/{endpoint}" - def get_supported_openai_params( # mutable-ok: base class contract returns a list - self, model: str - ) -> list[OpenAIImageGenerationOptionalParams]: - return list(SUPPORTED_OPENAI_PARAMS) # mutable-ok: base class contract returns a list + def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]: + return list(SUPPORTED_OPENAI_PARAMS) - def map_openai_params( # mutable-ok: base class contract returns a dict + def map_openai_params( self, non_default_params: Mapping[str, object], optional_params: Mapping[str, object], @@ -88,7 +86,7 @@ class FalAIGPTImage2Config(FalAIBaseConfig): if key in self.PARAM_TRANSLATION and self.PARAM_TRANSLATION[key] not in optional_params } ) - return {**optional_params, **translated_params} # mutable-ok: base class contract returns a dict + return {**optional_params, **translated_params} def _translate_value(self, key: str, value: object) -> object: if key == "size": @@ -113,7 +111,7 @@ class FalAIGPTImage2Config(FalAIBaseConfig): normalized: Final[str] = self.OPENAI_QUALITY_ALIASES.get(quality, quality) return normalized if normalized in self.SUPPORTED_QUALITIES else "auto" - def transform_image_generation_request( # mutable-ok: base class contract returns a dict + def transform_image_generation_request( self, model: str, prompt: str, @@ -121,4 +119,4 @@ class FalAIGPTImage2Config(FalAIBaseConfig): litellm_params: Mapping[str, object], headers: Mapping[str, str], ) -> dict: - return {"prompt": prompt, **optional_params} # mutable-ok: base class contract returns a dict + return {"prompt": prompt, **optional_params} diff --git a/litellm/llms/fireworks_ai/chat/transformation.py b/litellm/llms/fireworks_ai/chat/transformation.py index 05160d83c12..35cfae41cc8 100644 --- a/litellm/llms/fireworks_ai/chat/transformation.py +++ b/litellm/llms/fireworks_ai/chat/transformation.py @@ -69,7 +69,7 @@ def _extract_fireworks_hidden_params(payload: dict) -> dict: def _json_schema_response_format(schema: object, name: str) -> Mapping[str, object]: - return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}} # mutable-ok: JSON request body + return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}} EFFORT_KWARG_KEYS: Final = frozenset({"enable_thinking", "thinking", "reasoning_budget", "low_effort"}) @@ -339,12 +339,10 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig): return optional_params - def map_extra_body_params( - self, optional_params: Mapping[str, object], model: str - ) -> dict: # mutable-ok: http handler pops extra_body off the returned dict + def map_extra_body_params(self, optional_params: Mapping[str, object], model: str) -> dict: extra_body: Final = optional_params.get("extra_body") if not isinstance(extra_body, dict): - return dict(optional_params) # mutable-ok: JSON request body + return dict(optional_params) stripped: Final = tuple(sorted(k for k in extra_body if k in NIM_VLLM_STRIP_PARAMS)) if stripped: @@ -368,11 +366,11 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig): if k not in _EXTRA_BODY_CONSUMED_PARAMS and (k != "response_format" or "response_format" not in optional_params) ) - base: Final = {k: v for k, v in optional_params.items() if k != "extra_body"} # mutable-ok: JSON request body - return { # mutable-ok: JSON request body + base: Final = {k: v for k, v in optional_params.items() if k != "extra_body"} + return { **base, - **dict(promoted), # mutable-ok: JSON request body - **({"extra_body": dict(remaining)} if remaining else {}), # mutable-ok: JSON request body + **dict(promoted), + **({"extra_body": dict(remaining)} if remaining else {}), } @staticmethod @@ -441,12 +439,12 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig): if extra_body.get("guided_json") is not None: return (("response_format", _json_schema_response_format(extra_body["guided_json"], "response")),) if extra_body.get("guided_grammar") is not None: - grammar_response_format: Final = { # mutable-ok: JSON request body + grammar_response_format: Final = { "type": "grammar", "grammar": extra_body["guided_grammar"], } return (("response_format", grammar_response_format),) - choice_schema: Final = { # mutable-ok: JSON request body + choice_schema: Final = { "type": "string", "enum": extra_body["guided_choice"], } diff --git a/litellm/llms/fireworks_ai/common_utils.py b/litellm/llms/fireworks_ai/common_utils.py index 21a630a76d7..fe5bd108666 100644 --- a/litellm/llms/fireworks_ai/common_utils.py +++ b/litellm/llms/fireworks_ai/common_utils.py @@ -109,4 +109,4 @@ class FireworksAIMixin: def _add_session_affinity_header(self, headers: dict, litellm_params: dict) -> dict: pinned: Final = with_fireworks_session_affinity(headers, litellm_params) - return dict(pinned) # mutable-ok: the HTTP handler updates the returned headers in place + return dict(pinned) diff --git a/litellm/llms/fireworks_ai/completion/transformation.py b/litellm/llms/fireworks_ai/completion/transformation.py index 4f0e302003a..eec2f6ea33b 100644 --- a/litellm/llms/fireworks_ai/completion/transformation.py +++ b/litellm/llms/fireworks_ai/completion/transformation.py @@ -54,18 +54,14 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig optional_params[k] = v return optional_params - def map_extra_body_params( - self, optional_params: Mapping[str, object], model: str - ) -> dict: # mutable-ok: returned dict is spread into the OpenAI SDK call as kwargs + def map_extra_body_params(self, optional_params: Mapping[str, object], model: str) -> dict: raw_extra_body: Final = optional_params.get("extra_body") - initial_body: Final = ( - dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {} # mutable-ok: JSON request body - ) + initial_body: Final = dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {} stripped_body: Final = self._strip_unsupported_params(initial_body, model) moved_body: Final = self._move_native_params_into_extra_body(stripped_body, optional_params) effort_body: Final = self._translate_chat_template_kwargs(moved_body, optional_params, model) final_body: Final = self._translate_guided_into_extra_body(effort_body, optional_params) - base: Final = { # mutable-ok: JSON request body + base: Final = { k: v for k, v in optional_params.items() if k not in ("extra_body", "response_format", "reasoning_effort", "thinking") @@ -75,9 +71,7 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig return base @staticmethod - def _strip_unsupported_params( - extra_body: Mapping[str, object], model: str - ) -> dict: # mutable-ok: JSON request body + def _strip_unsupported_params(extra_body: Mapping[str, object], model: str) -> dict: stripped: Final = tuple(sorted(k for k in extra_body if k in _TEXT_COMPLETION_STRIP_PARAMS)) if stripped: verbose_logger.debug( @@ -85,15 +79,13 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig stripped, model, ) - return { # mutable-ok: JSON request body - k: v for k, v in extra_body.items() if k not in _TEXT_COMPLETION_STRIP_PARAMS - } + return {k: v for k, v in extra_body.items() if k not in _TEXT_COMPLETION_STRIP_PARAMS} @staticmethod def _move_native_params_into_extra_body( extra_body: Mapping[str, object], optional_params: Mapping[str, object] - ) -> dict: # mutable-ok: JSON request body - moved: Final = dict(extra_body) # mutable-ok: JSON request body + ) -> dict: + moved: Final = dict(extra_body) for key in ("response_format", "reasoning_effort", "thinking"): value = optional_params.get(key) if value is None: @@ -105,13 +97,11 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig def _translate_chat_template_kwargs( self, extra_body: Mapping[str, object], optional_params: Mapping[str, object], model: str - ) -> dict: # mutable-ok: JSON request body + ) -> dict: chat_template_kwargs: Final = extra_body.get("chat_template_kwargs") if chat_template_kwargs is None: - return dict(extra_body) # mutable-ok: JSON request body - result: Final = { # mutable-ok: JSON request body - k: v for k, v in extra_body.items() if k != "chat_template_kwargs" - } + return dict(extra_body) + result: Final = {k: v for k, v in extra_body.items() if k != "chat_template_kwargs"} if not isinstance(chat_template_kwargs, dict): verbose_logger.debug( "fireworks_ai dropping chat_template_kwargs for model=%s; expected an object, got %s.", @@ -140,18 +130,18 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig model, ) return result - return {**result, "reasoning_effort": effort} # mutable-ok: JSON request body + return {**result, "reasoning_effort": effort} @staticmethod def _translate_guided_into_extra_body( extra_body: Mapping[str, object], optional_params: Mapping[str, object] - ) -> dict: # mutable-ok: JSON request body + ) -> dict: guided_response_format: Final = FireworksAIConfig.translate_guided_params(extra_body, optional_params) - remaining: Final = { # mutable-ok: JSON request body + remaining: Final = { k: v for k, v in extra_body.items() if k not in ("guided_json", "guided_grammar", "guided_choice") } if guided_response_format: - return { # mutable-ok: JSON request body + return { **remaining, guided_response_format[0][0]: guided_response_format[0][1], } diff --git a/litellm/llms/fireworks_ai/responses/transformation.py b/litellm/llms/fireworks_ai/responses/transformation.py index f7dd774ea18..30e6f053cc1 100644 --- a/litellm/llms/fireworks_ai/responses/transformation.py +++ b/litellm/llms/fireworks_ai/responses/transformation.py @@ -111,9 +111,7 @@ def _with_instruction_items_folded( joined: Final = "\n\n".join(chunk for chunk in (instructions, *folded.values()) if chunk) return ( instructions if not folded else joined or None, - [ # mutable-ok: the base class takes the input items as a list - _developer_item_as_system(item) for index, item in enumerate(items) if index not in folded - ], + [_developer_item_as_system(item) for index, item in enumerate(items) if index not in folded], ) @@ -127,7 +125,7 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig): headers: Mapping[str, str], model: str, litellm_params: GenericLiteLLMParams | None, - ) -> dict: # mutable-ok: overrides the base class signature + ) -> dict: params: Final = litellm_params or GenericLiteLLMParams() api_key: Final = resolve_fireworks_api_key(params.api_key) if api_key is None: @@ -136,7 +134,7 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig): {"Content-Type": "application/json", **headers, "Authorization": f"Bearer {api_key}"} ) pinned: Final = with_fireworks_session_affinity(authorized, _session_params(params)) - return dict(pinned) # mutable-ok: the HTTP handler updates the returned headers in place + return dict(pinned) def get_complete_url(self, api_base: str | None, litellm_params: Mapping[str, object]) -> str: base: Final = (api_base or get_secret_str("FIREWORKS_API_BASE") or FIREWORKS_AI_DEFAULT_API_BASE).rstrip("/") @@ -146,10 +144,10 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig): self, model: str, input: str | ResponseInputParam, - response_api_optional_request_params: dict, # mutable-ok: overrides the base class signature + response_api_optional_request_params: dict, litellm_params: GenericLiteLLMParams, - headers: dict, # mutable-ok: overrides the base class signature - ) -> dict: # mutable-ok: overrides the base class signature + headers: dict, + ) -> dict: instructions_param: Final[object] = response_api_optional_request_params.get("instructions") validated_input: Final = self._validate_input_param(input) instructions, folded_input = ( @@ -158,7 +156,7 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig): else (instructions_param, _developer_items_as_system(validated_input)) ) instruction_entries: Final = () if instructions is None else (("instructions", instructions),) - folded_params: Final = { # mutable-ok: the base class takes the optional params as a dict + folded_params: Final = { key: value for key, value in ( *((key, value) for key, value in response_api_optional_request_params.items() if key != "instructions"), diff --git a/litellm/llms/gemini/audio_transcription/transformation.py b/litellm/llms/gemini/audio_transcription/transformation.py index c8dd7a9a5ff..ff9814a8905 100644 --- a/litellm/llms/gemini/audio_transcription/transformation.py +++ b/litellm/llms/gemini/audio_transcription/transformation.py @@ -47,7 +47,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): def get_supported_openai_params( self, model: str ) -> list[OpenAIAudioTranscriptionOptionalParams]: # mutable-ok: BaseAudioTranscriptionConfig signature - return ["language", "response_format", "timestamp_granularities"] # mutable-ok: base contract returns a list + return ["language", "response_format", "timestamp_granularities"] @property def supports_subtitle_synthesis(self) -> bool: @@ -59,16 +59,16 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): optional_params: Mapping[str, object], model: str, drop_params: bool, - ) -> dict: # mutable-ok: BaseAudioTranscriptionConfig signature + ) -> dict: supported_params: Final = frozenset(self.get_supported_openai_params(model)) accepted: Final = tuple((k, v) for k, v in non_default_params.items() if k in supported_params) - return dict((*optional_params.items(), *accepted)) # mutable-ok: base contract returns a plain dict + return dict((*optional_params.items(), *accepted)) def get_error_class( self, error_message: str, status_code: int, - headers: dict | Headers, # mutable-ok: base signature and BaseLLMException take dict | Headers + headers: dict | Headers, ) -> BaseLLMException: return GeminiError(status_code=status_code, message=error_message, headers=headers) @@ -81,14 +81,14 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): litellm_params: Mapping[str, object], api_key: str | None = None, api_base: str | None = None, - ) -> dict: # mutable-ok: BaseAudioTranscriptionConfig signature + ) -> dict: resolved_api_key: Final = GeminiModelInfo.get_api_key(api_key) if not resolved_api_key: raise GeminiError( status_code=401, message="Google API key is required. Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable.", ) - return { # mutable-ok: the http handler passes these headers straight to httpx + return { **headers, "Content-Type": "application/json", "x-goog-api-key": resolved_api_key, @@ -125,7 +125,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): audio_input=audio_input, transcription_config=_build_transcription_config(optional_params), ) - return AudioTranscriptionRequestData(data=dict(request)) # mutable-ok: AudioTranscriptionRequestData wants dict + return AudioTranscriptionRequestData(data=dict(request)) def transform_audio_transcription_response( self, @@ -159,7 +159,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): if (word := _annotation_to_word(annotation)) is not None ) if words: - response["words"] = list(words) # mutable-ok: verbose_json words is a JSON array + response["words"] = list(words) last_word_end: Final = words[-1].get("end") if last_word_end is not None: response["duration"] = last_word_end @@ -244,7 +244,7 @@ def _annotation_to_word(annotation: GeminiTranscriptionWordAnnotation) -> Mappin ("end", _parse_offset_seconds(annotation.end_offset)), ("speaker", annotation.speaker), ) - return {key: value for key, value in entries if value is not None} # mutable-ok: word entries serialize to JSON + return {key: value for key, value in entries if value is not None} def _parse_offset_seconds(offset: str | None) -> float | None: diff --git a/litellm/llms/gemini/google_genai/guardrail_translation/__init__.py b/litellm/llms/gemini/google_genai/guardrail_translation/__init__.py index 494a72d6999..12c962387c6 100644 --- a/litellm/llms/gemini/google_genai/guardrail_translation/__init__.py +++ b/litellm/llms/gemini/google_genai/guardrail_translation/__init__.py @@ -7,7 +7,7 @@ from litellm.llms.gemini.google_genai.guardrail_translation.handler import ( ) from litellm.types.utils import CallTypes -guardrail_translation_mappings: Final = { # mutable-ok: discover_guardrail_translation_mappings only accepts isinstance(mappings, dict) +guardrail_translation_mappings: Final = { CallTypes.generate_content: GoogleGenAIGenerateContentHandler, CallTypes.agenerate_content: GoogleGenAIGenerateContentHandler, CallTypes.generate_content_stream: GoogleGenAIGenerateContentHandler, diff --git a/litellm/llms/gemini/google_genai/guardrail_translation/handler.py b/litellm/llms/gemini/google_genai/guardrail_translation/handler.py index e13e1e63cbb..c9e8d6b7150 100644 --- a/litellm/llms/gemini/google_genai/guardrail_translation/handler.py +++ b/litellm/llms/gemini/google_genai/guardrail_translation/handler.py @@ -96,7 +96,7 @@ def _part_texts(text_parts: Sequence[object]) -> tuple[str, ...]: def _texts_payload( texts: Sequence[str], ) -> list[str]: # mutable-ok: GenericGuardrailAPIInputs.texts is declared list[str] - return list(texts) # mutable-ok: GenericGuardrailAPIInputs.texts is declared list[str] + return list(texts) def _write_back_texts(text_parts: Sequence[object], guardrailed_texts: Sequence[str] | None) -> None: @@ -157,7 +157,7 @@ class GoogleGenAIGenerateContentHandler(BaseTranslation): async def process_input_messages( self, - data: dict, # mutable-ok: base handler contract passes the proxy's request dict through to apply_guardrail + data: dict, guardrail_to_apply: "CustomGuardrail", litellm_logging_obj: Optional["LiteLLMLoggingObj"] = None, ) -> object: @@ -245,11 +245,11 @@ class GoogleGenAIGenerateContentHandler(BaseTranslation): user_api_key_dict: Optional["UserAPIKeyAuth"], context_key: str, context_value: object, - ) -> dict: # mutable-ok: CustomGuardrail.apply_guardrail requires a plain dict request payload + ) -> dict: base: Final = request_data if request_data is not None else _EMPTY_REQUEST_DATA user_metadata: Final = self.transform_user_api_key_dict_to_metadata(user_api_key_dict) context_pairs: Final = ((context_key, context_value),) if context_key not in base else () metadata_pairs: Final = ( (("litellm_metadata", user_metadata),) if user_metadata and "litellm_metadata" not in base else () ) - return dict((*base.items(), *context_pairs, *metadata_pairs)) # mutable-ok: apply_guardrail takes a plain dict + return dict((*base.items(), *context_pairs, *metadata_pairs)) diff --git a/litellm/llms/gigachat/chat/streaming.py b/litellm/llms/gigachat/chat/streaming.py index 0a4cbd8e520..8aeb955de13 100644 --- a/litellm/llms/gigachat/chat/streaming.py +++ b/litellm/llms/gigachat/chat/streaming.py @@ -30,7 +30,7 @@ class GigaChatModelResponseIterator: def chunk_parser(self, chunk: Mapping[str, object]) -> GenericStreamingChunk: """Parse a single streaming chunk from GigaChat.""" - choices: Sequence = chunk.get("choices") or () # mutable-ok: tuple literal as default + choices: Sequence = chunk.get("choices") or () if not choices: return GenericStreamingChunk( text="", @@ -42,7 +42,7 @@ class GigaChatModelResponseIterator: ) choice: Final = choices[0] - delta: Mapping[str, object] = choice.get("delta") or {} # mutable-ok: empty dict default for get + delta: Mapping[str, object] = choice.get("delta") or {} chunk_finish_reason: Final = choice.get("finish_reason") # Extract text content @@ -74,7 +74,7 @@ class GigaChatModelResponseIterator: ) finish_reason = "tool_calls" - usage_data: Final = chunk.get("usage") or {} # mutable-ok: empty dict default + usage_data: Final = chunk.get("usage") or {} if usage_data and isinstance(usage_data, dict): validated_usage: Final = {k: int(v) for k, v in usage_data.items()} usage = convert_usage(validated_usage) diff --git a/litellm/llms/gigachat/chat/transformation.py b/litellm/llms/gigachat/chat/transformation.py index 89920ebd27b..f0f944ec109 100644 --- a/litellm/llms/gigachat/chat/transformation.py +++ b/litellm/llms/gigachat/chat/transformation.py @@ -104,14 +104,14 @@ class GigaChatConfig(BaseConfig): def validate_environment( self, - headers: dict, # mutable-ok: mutates in place per GigaChat OAuth setup + headers: dict, model: str, messages: Sequence[AllMessageValues], optional_params: Mapping[str, object], litellm_params: Mapping[str, object], api_key: str | None = None, api_base: str | None = None, - ) -> dict: # mutable-ok: base class contract returns dict for httpx + ) -> dict: """ Set up headers with OAuth token. """ @@ -129,7 +129,7 @@ class GigaChatConfig(BaseConfig): def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: base class contract returns list """Return list of supported OpenAI parameters.""" - return [ # mutable-ok: base class contract returns list + return [ "stream", "temperature", "top_p", @@ -146,10 +146,10 @@ class GigaChatConfig(BaseConfig): def map_openai_params( self, non_default_params: Mapping[str, object], - optional_params: dict, # mutable-ok: mutated in place per GigaChat mapping + optional_params: dict, model: str, drop_params: bool, - ) -> dict: # mutable-ok: base class contract returns dict + ) -> dict: """Map OpenAI parameters to GigaChat parameters.""" for param, value in non_default_params.items(): if param == "stream": @@ -188,7 +188,7 @@ class GigaChatConfig(BaseConfig): schema_name = json_schema.get("name", "structured_output") schema = json_schema.get("schema", {}) - function_def = { # mutable-ok: request payload for httpx + function_def = { "name": schema_name, "description": f"Output structured response: {schema_name}", "parameters": schema, @@ -203,7 +203,7 @@ class GigaChatConfig(BaseConfig): ), function_def, ] - optional_params["function_call"] = {"name": schema_name} # mutable-ok: request payload + optional_params["function_call"] = {"name": schema_name} optional_params["_structured_output"] = True return optional_params @@ -321,7 +321,7 @@ class GigaChatConfig(BaseConfig): optional_params: Mapping[str, object], litellm_params: Mapping[str, object], headers: Mapping[str, object], - ) -> dict: # mutable-ok: request payload sent to httpx + ) -> dict: """Transform OpenAI request to GigaChat format.""" giga_messages: Final = self._transform_messages(messages) diff --git a/litellm/llms/gigachat/embedding/transformation.py b/litellm/llms/gigachat/embedding/transformation.py index 0db4475be8f..927f5e944b6 100644 --- a/litellm/llms/gigachat/embedding/transformation.py +++ b/litellm/llms/gigachat/embedding/transformation.py @@ -112,7 +112,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig): "input": ["text1", "text2", ...] } """ - normalized_input: Final = [input] if isinstance(input, str) else input # mutable-ok: preserve list API + normalized_input: Final = [input] if isinstance(input, str) else input return { "model": model.removeprefix("gigachat/"), "input": normalized_input, diff --git a/litellm/llms/gigachat/passthrough/transformation.py b/litellm/llms/gigachat/passthrough/transformation.py index e1f73d04275..5951fddb562 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -49,14 +49,14 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): def validate_environment( self, - headers: dict, # mutable-ok: mutates in place to set OAuth headers + headers: dict, model: str, messages: Sequence[AllMessageValues], optional_params: Mapping[str, object], litellm_params: Mapping[str, object], api_key: str | None = None, api_base: str | None = None, - ) -> dict: # mutable-ok: base class contract returns dict for httpx + ) -> dict: """ Set up headers with OAuth token. """ @@ -93,16 +93,14 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): raw_messages: Final = request_data.get("messages") litellm_model_response: Final = provider_chat_config.transform_response( model=model, - messages=list(raw_messages) - if isinstance(raw_messages, list) - else [], # mutable-ok: transform_response wants a list + messages=list(raw_messages) if isinstance(raw_messages, list) else [], raw_response=httpx_response, model_response=ModelResponse(), logging_obj=logging_obj, - optional_params={}, # mutable-ok: empty dict kwarg for transform_response - litellm_params={}, # mutable-ok: empty dict kwarg for transform_response + optional_params={}, + litellm_params={}, api_key="", - request_data=dict(request_data), # mutable-ok: transform_response wants a dict + request_data=dict(request_data), encoding=encoding, ) @@ -123,10 +121,10 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): raw_response=httpx_response, model_response=EmbeddingResponse(), logging_obj=logging_obj, - optional_params={}, # mutable-ok: empty dict kwarg for transform_embedding_response + optional_params={}, api_key="", - request_data=dict(request_data), # mutable-ok: transform_embedding_response wants a dict - litellm_params={}, # mutable-ok: empty dict kwarg for transform_embedding_response + request_data=dict(request_data), + litellm_params={}, ) ) diff --git a/litellm/llms/groq/chat/transformation.py b/litellm/llms/groq/chat/transformation.py index 41a2df17c6f..03452100c25 100644 --- a/litellm/llms/groq/chat/transformation.py +++ b/litellm/llms/groq/chat/transformation.py @@ -271,7 +271,7 @@ class GroqChatConfig(OpenAILikeChatConfig): if not any(tool.get("type") == "browser_search" for tool in optional_params.get("tools") or ()): optional_params = self._add_tools_to_optional_params( optional_params=optional_params, - tools=[{"type": "browser_search"}], # mutable-ok: request tools must be json dicts in a list + tools=[{"type": "browser_search"}], ) return optional_params diff --git a/litellm/llms/hosted_vllm/image_edit/transformation.py b/litellm/llms/hosted_vllm/image_edit/transformation.py index 3b8cc437168..15b5ce2e988 100644 --- a/litellm/llms/hosted_vllm/image_edit/transformation.py +++ b/litellm/llms/hosted_vllm/image_edit/transformation.py @@ -8,7 +8,7 @@ PARAMS_VLLM_OMNI_DOES_NOT_ACCEPT: Final = frozenset({"mask", "quality", "input_f class HostedVLLMImageEditConfig(OpenAIImageEditConfig): def get_supported_openai_params(self, model: str) -> list: # mutable-ok: BaseImageEditConfig contract - return [ # mutable-ok: BaseImageEditConfig returns list + return [ param for param in super().get_supported_openai_params(model) if param not in PARAMS_VLLM_OMNI_DOES_NOT_ACCEPT @@ -16,20 +16,20 @@ class HostedVLLMImageEditConfig(OpenAIImageEditConfig): def validate_environment( self, - headers: dict, # mutable-ok: BaseImageEditConfig contract + headers: dict, model: str, api_key: str | None = None, - litellm_params: dict | None = None, # mutable-ok: BaseImageEditConfig contract + litellm_params: dict | None = None, api_base: str | None = None, - ) -> dict: # mutable-ok: BaseImageEditConfig contract + ) -> dict: resolved_key: Final = api_key or get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key" - return {**headers, "Authorization": f"Bearer {resolved_key}"} # mutable-ok: httpx headers are a dict + return {**headers, "Authorization": f"Bearer {resolved_key}"} def get_complete_url( self, model: str, api_base: str | None, - litellm_params: dict, # mutable-ok: BaseImageEditConfig contract + litellm_params: dict, ) -> str: resolved_api_base: Final = api_base or get_secret_str("HOSTED_VLLM_API_BASE") if resolved_api_base is None: diff --git a/litellm/llms/hosted_vllm/videos/transformation.py b/litellm/llms/hosted_vllm/videos/transformation.py index 96cbfc3cf70..f040d7bfee9 100644 --- a/litellm/llms/hosted_vllm/videos/transformation.py +++ b/litellm/llms/hosted_vllm/videos/transformation.py @@ -135,7 +135,7 @@ class HostedVLLMVideoConfig(OpenAIVideoConfig): """ def get_supported_openai_params(self, model: str) -> list: # mutable-ok: BaseVideoConfig contract - return [ # mutable-ok: BaseVideoConfig returns list + return [ *super().get_supported_openai_params(model), *_VLLM_OMNI_VIDEO_PARAMS, ] @@ -145,31 +145,29 @@ class HostedVLLMVideoConfig(OpenAIVideoConfig): video_create_optional_params: VideoCreateOptionalRequestParams, model: str, drop_params: bool, - ) -> dict: # mutable-ok: BaseVideoConfig contract; extra_body merge mutates this dict - return { # mutable-ok: VideoGenerationRequestUtils.update/pop extra_body onto this mapping - key: value for key, value in video_create_optional_params.items() if value is not None - } + ) -> dict: + return {key: value for key, value in video_create_optional_params.items() if value is not None} def validate_environment( self, - headers: dict, # mutable-ok: BaseVideoConfig contract + headers: dict, model: str, api_key: str | None = None, litellm_params: GenericLiteLLMParams | None = None, - ) -> dict: # mutable-ok: BaseVideoConfig contract + ) -> dict: resolved_key: Final = ( (litellm_params.api_key if litellm_params is not None else None) or api_key or get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key" ) - return {**headers, "Authorization": f"Bearer {resolved_key}"} # mutable-ok: httpx headers are a dict + return {**headers, "Authorization": f"Bearer {resolved_key}"} def get_complete_url( self, model: str, api_base: str | None, - litellm_params: dict, # mutable-ok: BaseVideoConfig contract + litellm_params: dict, ) -> str: resolved_api_base: Final = api_base or get_secret_str("HOSTED_VLLM_API_BASE") if resolved_api_base is None: @@ -187,14 +185,14 @@ class HostedVLLMVideoConfig(OpenAIVideoConfig): model: str, prompt: str, api_base: str, - video_create_optional_request_params: dict, # mutable-ok: BaseVideoConfig contract + video_create_optional_request_params: dict, litellm_params: GenericLiteLLMParams, - headers: dict, # mutable-ok: BaseVideoConfig contract - ) -> tuple[dict, RequestFiles, str]: # mutable-ok: BaseVideoConfig contract - data: Final = { # mutable-ok: BaseVideoConfig contract returns a data dict + headers: dict, + ) -> tuple[dict, RequestFiles, str]: + data: Final = { "model": model, "prompt": prompt, - **{ # mutable-ok: spread remaining Omni form fields into that data dict + **{ key: _form_value(key, value) for key, value in video_create_optional_request_params.items() if key not in _EXCLUDED_FORM_KEYS and value is not None diff --git a/litellm/llms/litellm_proxy/skills/transformation.py b/litellm/llms/litellm_proxy/skills/transformation.py index 9fc2d2cbb45..f83c605a3d2 100644 --- a/litellm/llms/litellm_proxy/skills/transformation.py +++ b/litellm/llms/litellm_proxy/skills/transformation.py @@ -222,9 +222,7 @@ class LiteLLMSkillsTransformationHandler: user_api_key_dict=user_api_key_dict, ) - skills: Final = [ # mutable-ok: ListSkillsResponse.data needs list[Skill]; never mutated after - self.db_skill_to_response(s) for s in db_skills - ] + skills: Final = [self.db_skill_to_response(s) for s in db_skills] return ListSkillsResponse( data=skills, has_more=len(skills) >= limit, diff --git a/litellm/llms/meta/realtime/transformation.py b/litellm/llms/meta/realtime/transformation.py index 1b8943f0cee..f8cebf14f8e 100644 --- a/litellm/llms/meta/realtime/transformation.py +++ b/litellm/llms/meta/realtime/transformation.py @@ -440,7 +440,7 @@ class _TurnState: class MuseEventTransformer: def __init__(self, *, turn_limit: int = 128) -> None: - self._turns: dict[str, _TurnState] = {} # mutable-ok: bounded, insertion-ordered per-turn emit state + self._turns: dict[str, _TurnState] = {} self._turn_limit: Final = turn_limit self._active_turn_id: str | None = None self._mode: MuseMode = "ENDPOINTING" @@ -580,10 +580,10 @@ class MetaRealtimeConfig(BaseRealtimeConfig): def validate_environment( self, - headers: dict[str, str], # mutable-ok: BaseRealtimeConfig contract + headers: dict[str, str], model: str, api_key: str | None = None, - ) -> dict[str, str]: # mutable-ok: BaseRealtimeConfig contract + ) -> dict[str, str]: token: Final = api_key or get_secret_str("META_API_KEY") if token is None: raise ValueError("api_key is required for Meta API calls") @@ -652,7 +652,7 @@ class MetaRealtimeConfig(BaseRealtimeConfig): ) -> RealtimeResponseTypedDict: payload: Final = message.decode("utf-8") if isinstance(message, bytes) else message result: Final[RealtimeResponseTypedDict] = { - "response": list(self._backend_events(payload)), # mutable-ok: RealtimeResponseTypedDict.response is a list + "response": list(self._backend_events(payload)), "current_output_item_id": realtime_response_transform_input.get("current_output_item_id"), "current_response_id": realtime_response_transform_input.get("current_response_id"), "current_delta_chunks": realtime_response_transform_input.get("current_delta_chunks"), diff --git a/litellm/llms/minimax/messages/transformation.py b/litellm/llms/minimax/messages/transformation.py index d4c24c65cfa..d949388b37c 100644 --- a/litellm/llms/minimax/messages/transformation.py +++ b/litellm/llms/minimax/messages/transformation.py @@ -51,14 +51,14 @@ class MinimaxMessagesConfig(AnthropicMessagesConfig): def validate_anthropic_messages_environment( self, - headers: dict, # mutable-ok: mirrors the legacy base override signature + headers: dict, model: str, messages: list[Any], # mutable-ok: mirrors the legacy base override signature - optional_params: dict, # mutable-ok: mirrors the legacy base override signature - litellm_params: dict, # mutable-ok: mirrors the legacy base override signature + optional_params: dict, + litellm_params: dict, api_key: str | None = None, api_base: str | None = None, - ) -> tuple[dict, str | None]: # mutable-ok: mirrors the legacy base override signature + ) -> tuple[dict, str | None]: return super().validate_anthropic_messages_environment( headers=headers, model=model, diff --git a/litellm/llms/mistral/audio_speech/transformation.py b/litellm/llms/mistral/audio_speech/transformation.py index 2b3264dc756..afddbf74a9a 100644 --- a/litellm/llms/mistral/audio_speech/transformation.py +++ b/litellm/llms/mistral/audio_speech/transformation.py @@ -54,7 +54,7 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig): ) def get_supported_openai_params(self, model: str) -> list: # mutable-ok: base class contract returns a plain list - return ["voice", "response_format"] # mutable-ok: base class contract returns a plain list + return ["voice", "response_format"] def _map_openai_voice(self, voice_id: str) -> str: return self.OPENAI_VOICE_ALIASES.get(voice_id.lower(), voice_id) @@ -78,12 +78,12 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig): voice: object = None, drop_params: bool = False, kwargs: Mapping[str, object] | None = None, - ) -> tuple[str | None, dict]: # mutable-ok: base class contract returns a plain dict + ) -> tuple[str | None, dict]: response_format: Final = optional_params.get("response_format") ref_audio: Final = kwargs.get("ref_audio") if kwargs else None voice_id_kwarg: Final = kwargs.get("voice_id") if kwargs else None mapped_voice: Final = self._resolve_voice_id(voice) or self._resolve_voice_id(voice_id_kwarg) - mapped_params: Final = { # mutable-ok: base class contract returns a plain dict + mapped_params: Final = { key: value for key, value in (("response_format", response_format), ("ref_audio", ref_audio)) if isinstance(value, str) @@ -96,14 +96,14 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig): model: str, api_key: str | None = None, api_base: str | None = None, - ) -> dict: # mutable-ok: base class contract returns a plain dict + ) -> dict: resolved_key: Final = api_key or get_secret_str("MISTRAL_API_KEY") if resolved_key is None: raise MistralTextToSpeechException( status_code=401, message="Mistral API key is required. Set MISTRAL_API_KEY or pass api_key.", ) - return { # mutable-ok: base class contract returns a plain dict + return { **headers, "Authorization": f"Bearer {resolved_key}", "Content-Type": "application/json", @@ -201,7 +201,7 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig): self, error_message: str, status_code: int, - headers: dict | httpx.Headers, # mutable-ok: BaseLLMException takes a plain dict or httpx.Headers + headers: dict | httpx.Headers, ) -> BaseLLMException: return MistralTextToSpeechException( message=error_message, diff --git a/litellm/llms/mongodb/vector_stores/transformation.py b/litellm/llms/mongodb/vector_stores/transformation.py index a59f39d3be8..389b8ed67fe 100644 --- a/litellm/llms/mongodb/vector_stores/transformation.py +++ b/litellm/llms/mongodb/vector_stores/transformation.py @@ -133,7 +133,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): return BaseVectorStoreAuthCredentials() def get_vector_store_endpoints_by_type(self) -> VectorStoreIndexEndpoints: - return VectorStoreIndexEndpoints(read=[], write=[]) # mutable-ok: the TypedDict declares list fields + return VectorStoreIndexEndpoints(read=[], write=[]) @staticmethod def _reject_unknown_params(litellm_params: Mapping[str, object]) -> None: @@ -186,7 +186,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): def validate_environment( self, headers: Mapping[str, object], litellm_params: GenericLiteLLMParams | None - ) -> dict[str, object]: # mutable-ok: the shared HTTP handler requires writable headers + ) -> dict[str, object]: if litellm_params is None: raise config_error("Configure api_base and api_key for the MongoDB BETA sidecar.") self._reject_unknown_params(MappingProxyType(dict(litellm_params))) @@ -197,7 +197,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): **headers, "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", - } # mutable-ok: writable HTTP headers + } def get_complete_url(self, api_base: str | None, litellm_params: Mapping[str, object]) -> str: if not api_base: @@ -272,7 +272,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): api_base: str, embedding_response: EmbeddingResponse, timeout: object, - ) -> tuple[str, dict[str, object]]: # mutable-ok: the provider contract returns a writable JSON request body + ) -> tuple[str, dict[str, object]]: if not embedding_response.data: raise config_error( "The embedding model returned no embedding for the search query. Check litellm_embedding_model." @@ -283,7 +283,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): limit: Final = cls._limit(optional_params) return ( f"{api_base}/v1/vector_stores/{quote(vector_store_id, safe='')}/search", - { # mutable-ok: JSON transport requires a dict + { "query": query_text, "query_vector": tuple(vector), "mongodb_database": params.require_database(), @@ -306,7 +306,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): litellm_params: Mapping[str, object], extra_body: Mapping[str, object] | None = None, embedding_executor: VectorStoreEmbeddingExecutor | None = None, - ) -> tuple[str, dict[str, object]]: # mutable-ok: the provider contract returns a writable JSON request body + ) -> tuple[str, dict[str, object]]: params: Final = self._params(litellm_params, vector_store_search_optional_params, extra_body) query_text: Final = self._query_text(query) response: Final = (embedding_executor or self.embedding_executor).embed( @@ -332,7 +332,7 @@ class MongoDBVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig): litellm_params: Mapping[str, object], extra_body: Mapping[str, object] | None = None, embedding_executor: VectorStoreEmbeddingExecutor | None = None, - ) -> tuple[str, dict[str, object]]: # mutable-ok: the provider contract returns a writable JSON request body + ) -> tuple[str, dict[str, object]]: params: Final = self._params(litellm_params, vector_store_search_optional_params, extra_body) query_text: Final = self._query_text(query) response: Final = await (embedding_executor or self.embedding_executor).aembed( diff --git a/litellm/llms/nimble/search/transformation.py b/litellm/llms/nimble/search/transformation.py index 7485686d230..d34a8d42356 100644 --- a/litellm/llms/nimble/search/transformation.py +++ b/litellm/llms/nimble/search/transformation.py @@ -88,11 +88,11 @@ class NimbleSearchConfig(BaseSearchConfig): def validate_environment( self, - headers: dict[str, str], # mutable-ok: BaseSearchConfig.validate_environment signature + headers: dict[str, str], api_key: str | None = None, api_base: str | None = None, **kwargs: object, # kwargs-ok: BaseSearchConfig.validate_environment signature - ) -> dict[str, str]: # mutable-ok: the http handler passes this straight to httpx as headers + ) -> dict[str, str]: """ Validate environment and return headers. @@ -108,7 +108,7 @@ class NimbleSearchConfig(BaseSearchConfig): ) if not resolved_api_key: raise ValueError("NIMBLE_API_KEY is not set. Set `NIMBLE_API_KEY` environment variable.") - return { # mutable-ok: httpx requires a plain dict of headers + return { **headers, "Authorization": f"Bearer {resolved_api_key}", "Content-Type": "application/json", @@ -119,7 +119,7 @@ class NimbleSearchConfig(BaseSearchConfig): def get_complete_url( self, api_base: str | None, - optional_params: dict[str, object], # mutable-ok: BaseSearchConfig.get_complete_url signature + optional_params: dict[str, object], data: dict[str, object] | list[dict[str, object]] | None = None, # mutable-ok: base signature **kwargs: object, # kwargs-ok: BaseSearchConfig.get_complete_url signature ) -> str: @@ -131,9 +131,9 @@ class NimbleSearchConfig(BaseSearchConfig): def transform_search_request( self, query: str | list[str], # mutable-ok: BaseSearchConfig.transform_search_request signature - optional_params: dict[str, object], # mutable-ok: base signature + optional_params: dict[str, object], **kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_request signature - ) -> dict[str, object]: # mutable-ok: the http handler passes this straight to httpx as the JSON body + ) -> dict[str, object]: """ Transform Search request to Nimble API format. @@ -156,7 +156,7 @@ class NimbleSearchConfig(BaseSearchConfig): {param: value for param, value in optional_params.items() if param not in unified_params} ) - return { # mutable-ok: httpx requires a plain dict for the JSON body + return { **_domain_filters(optional_params.get("search_domain_filter")), **passthrough, "query": " ".join(query) if isinstance(query, list) else query, @@ -188,11 +188,11 @@ class NimbleSearchConfig(BaseSearchConfig): raise self.get_error_class( error_message=f"response does not match the documented /v2/search schema: {e}", status_code=raw_response.status_code, - headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature + headers=dict(raw_response.headers), ) return SearchResponse( - results=[ # mutable-ok: SearchResponse.results is declared list[SearchResult] + results=[ SearchResult( title=result.title or "", url=result.url or "", @@ -210,7 +210,7 @@ class NimbleSearchConfig(BaseSearchConfig): self, error_message: str, status_code: int, - headers: dict[str, str], # mutable-ok: BaseSearchConfig.get_error_class signature + headers: dict[str, str], ) -> Exception: detail: Final = _unwrap_error_detail(error_message).rstrip(". ") return BaseLLMException( diff --git a/litellm/llms/nvidia_nim/rerank/ranking_transformation.py b/litellm/llms/nvidia_nim/rerank/ranking_transformation.py index 976b5c2211c..e9beb14a208 100644 --- a/litellm/llms/nvidia_nim/rerank/ranking_transformation.py +++ b/litellm/llms/nvidia_nim/rerank/ranking_transformation.py @@ -80,7 +80,7 @@ class NvidiaNimRankingConfig(NvidiaNimRerankConfig): def map_cohere_rerank_params( self, - non_default_params: dict | None, # mutable-ok: matches BaseRerankConfig's request contract + non_default_params: dict | None, model: str, drop_params: bool, query: str, @@ -92,7 +92,7 @@ class NvidiaNimRankingConfig(NvidiaNimRerankConfig): max_chunks_per_doc: int | None = None, max_tokens_per_doc: int | None = None, instruction: str | None = None, - ) -> dict: # mutable-ok: LiteLLM provider transforms return mutable request dictionaries + ) -> dict: """ Keep Cohere's top_n as-is instead of mapping it to top_k. @@ -141,9 +141,7 @@ class NvidiaNimRankingConfig(NvidiaNimRerankConfig): self._client_side_top_n = top_n clean_model: Final = self._get_clean_model_name(model) - filtered_params: Final = { # mutable-ok: the base transformer requires a mutable request dictionary - k: v for k, v in optional_rerank_params.items() if k not in ("top_n", "top_k") - } + filtered_params: Final = {k: v for k, v in optional_rerank_params.items() if k not in ("top_n", "top_k")} return super().transform_rerank_request( model=clean_model, optional_rerank_params=filtered_params, @@ -158,9 +156,9 @@ class NvidiaNimRankingConfig(NvidiaNimRerankConfig): model_response: RerankResponse, logging_obj: LiteLLMLoggingObj, api_key: str | None = None, - request_data: dict | None = None, # mutable-ok: matches BaseRerankConfig's response contract - optional_params: dict | None = None, # mutable-ok: matches BaseRerankConfig's response contract - litellm_params: dict | None = None, # mutable-ok: matches BaseRerankConfig's response contract + request_data: dict | None = None, + optional_params: dict | None = None, + litellm_params: dict | None = None, ) -> RerankResponse: """ Convert the native ranking response, then apply top_n client-side. @@ -168,9 +166,9 @@ class NvidiaNimRankingConfig(NvidiaNimRerankConfig): /v1/ranking returns rankings sorted by relevance, but sort before truncating in case a server returns them unsorted. """ - resolved_request_data: Final = request_data or {} # mutable-ok: the base transformer requires a dictionary - resolved_optional_params: Final = optional_params or {} # mutable-ok: response options are keyed lookups - resolved_litellm_params: Final = litellm_params or {} # mutable-ok: the base transformer requires a dictionary + resolved_request_data: Final = request_data or {} + resolved_optional_params: Final = optional_params or {} + resolved_litellm_params: Final = litellm_params or {} response: Final = super().transform_rerank_response( model=model, diff --git a/litellm/llms/nvidia_nim/rerank/transformation.py b/litellm/llms/nvidia_nim/rerank/transformation.py index 93e00dad9a1..15cfdb6bece 100644 --- a/litellm/llms/nvidia_nim/rerank/transformation.py +++ b/litellm/llms/nvidia_nim/rerank/transformation.py @@ -215,7 +215,7 @@ class NvidiaNimRerankConfig(BaseRerankConfig): elif isinstance(doc, dict): # Preserve only the structured passage fields supported by the # selected rerank route. - supported_fields: NvidiaNimPassageObject = {} # mutable-ok: assembling a request TypedDict + supported_fields: NvidiaNimPassageObject = {} if "text" in self.SUPPORTED_PASSAGE_FIELDS and "text" in doc: supported_fields["text"] = doc["text"] if "image" in self.SUPPORTED_PASSAGE_FIELDS and "image" in doc: diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index a1340ba1952..7a715da8d3f 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -356,10 +356,10 @@ class OllamaConfig(BaseConfig): self, model: str, messages: list[AllMessageValues], # mutable-ok: BaseConfig signature - optional_params: dict[str, object], # mutable-ok: BaseConfig signature - litellm_params: dict[str, object], # mutable-ok: BaseConfig signature - headers: dict[str, object], # mutable-ok: BaseConfig signature - ) -> dict[str, object]: # mutable-ok: BaseConfig signature + optional_params: dict[str, object], + litellm_params: dict[str, object], + headers: dict[str, object], + ) -> dict[str, object]: return self.transform_request( model=model, messages=await async_inline_remote_media(messages, should_inline=inline_remote_image_urls), diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 9b410cf073e..a42aae70073 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -458,7 +458,7 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): if self._targets_openai_hosted_endpoint(provider, raw_api_base if isinstance(raw_api_base, str) else None) else drop_non_python_regex_patterns ) - sanitized: Final = [ # mutable-ok: request tools are a JSON list + sanitized: Final = [ tool_with_sanitized_parameters(tool, sanitize) if isinstance(tool, dict) else tool for tool in tools ] return MappingProxyType({"tools": sanitized}) @@ -582,9 +582,7 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): for choice in choices: ## HANDLE JSON MODE - anthropic returns single function call] tool_calls = choice["message"].get("tool_calls", None) - new_tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | None = ( - None # mutable-ok: holds _handle_invalid_parallel_tool_calls' list; Message.__init__ expects list - ) + new_tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | None = None message_content = choice["message"].get("content", None) if tool_calls is not None: _openai_tool_calls = [] @@ -819,7 +817,7 @@ class OpenAIUnknownModelConfig(OpenAIGPTConfig): forward reasoning_effort and let the server decide whether it is supported.""" def get_supported_openai_params(self, model: str) -> list: # mutable-ok: inherited contract - return super().get_supported_openai_params(model) + ["reasoning_effort"] # mutable-ok: inherited contract + return super().get_supported_openai_params(model) + ["reasoning_effort"] class OpenAIChatCompletionStreamingHandler(BaseModelResponseIterator): diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index 58ff03e6a0d..2d6ea3830ae 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -609,7 +609,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): guardrail_to_apply: "CustomGuardrail", litellm_logging_obj: "LiteLLMLoggingObj | None", user_api_key_dict: "UserAPIKeyAuth | None", - request_data: dict[str, object] | None, # mutable-ok: same request-payload shape the hooks take + request_data: dict[str, object] | None, deliver_ended_stream_rewrites: bool, ) -> None: """Ended-stream path: rebuild the full response, run the non-streaming @@ -1051,8 +1051,8 @@ class OpenAIChatCompletionsHandler(BaseTranslation): target_choice_index: Final = next(iter(stream_choice_indices)) await self._apply_guardrail_responses_to_output_streaming( responses=responses_so_far, - guardrailed_texts=list(changed), # mutable-ok: callee takes lists - task_mappings=[(target_choice_index, None) for _ in changed], # mutable-ok: callee takes lists + guardrailed_texts=list(changed), + task_mappings=[(target_choice_index, None) for _ in changed], ) @staticmethod diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index 5f04ebe0c01..f7decbea314 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -343,9 +343,7 @@ _SDK_OPTION_KEYS: Final = frozenset(("extra_headers", "extra_query", "extra_body def _embedding_request_without_sdk_defaults( data: Mapping[str, object], timeout: float | httpx.Timeout ) -> tuple[Mapping[str, object], RequestOptions]: - body: Final = { # mutable-ok: the SDK json-encodes the body and needs a plain dict - k: v for k, v in data.items() if k not in _SDK_OPTION_KEYS - } + body: Final = {k: v for k, v in data.items() if k not in _SDK_OPTION_KEYS} extra_headers: Final = _EXTRA_HEADERS_ADAPTER.validate_python(data.get("extra_headers")) or _NO_EXTRA_HEADERS options: Final = make_request_options( extra_headers=types.MappingProxyType({**extra_headers, RAW_RESPONSE_HEADER: "true"}), @@ -1418,17 +1416,15 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj.pre_call( input=prompt, api_key=openai_aclient.api_key, - additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict - "headers": {"Authorization": f"Bearer {openai_aclient.api_key}"}, # mutable-ok: logged header map + additional_args={ + "headers": {"Authorization": f"Bearer {openai_aclient.api_key}"}, "api_base": str(openai_aclient.base_url), "acompletion": True, "complete_input_dict": data, }, ) - request_data: Final = ( # mutable-ok: the OpenAI SDK takes the request body as a dict - {**data, "extra_headers": headers} if headers else data - ) + request_data: Final = {**data, "extra_headers": headers} if headers else data response = await openai_aclient.images.generate(**request_data, timeout=timeout) stringified_response: Final = response.model_dump() ## LOGGING @@ -1512,9 +1508,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): ) ## COMPLETION CALL - request_data: Final = ( # mutable-ok: the OpenAI SDK takes the request body as a dict - {**data, "extra_headers": headers} if headers else data - ) + request_data: Final = {**data, "extra_headers": headers} if headers else data _response: Final = openai_client.images.generate(**request_data, timeout=timeout) response: Final = _response.model_dump() @@ -1600,7 +1594,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict + additional_args={ "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(sync_client.base_url), }, @@ -1646,7 +1640,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict + additional_args={ "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(openai_client.base_url), }, diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 2fe11d9f7bd..46ab4e86c41 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -229,10 +229,10 @@ def _rewritten_input_item(item: Mapping[str, object], rewritten: object) -> Mapp return None rewritten_content: Final = rewritten.get("content") if isinstance(item.get(field), str) and isinstance(rewritten_content, str): - return {**item, field: rewritten_content} # mutable-ok: request input items must stay JSON-plain dicts + return {**item, field: rewritten_content} rewritten_row: Final = cast("AllMessageValues", rewritten) # cast-ok: guardrails hand back chat-shaped rows converted_items, _ = LiteLLMResponsesTransformationHandler().convert_chat_completion_messages_to_responses_api( - [rewritten_row] # mutable-ok: converter signature takes a list + [rewritten_row] ) if len(converted_items) != 1 or not isinstance(converted_items[0], Mapping): return None @@ -240,7 +240,7 @@ def _rewritten_input_item(item: Mapping[str, object], rewritten: object) -> Mapp converted_value: Final = first_converted.get(field) if converted_value is None: return None - return {**item, field: converted_value} # mutable-ok: request input items must stay JSON-plain dicts + return {**item, field: converted_value} def _is_tool_call_item(item: object) -> bool: @@ -488,7 +488,7 @@ class OpenAIResponsesHandler(BaseTranslation): ) written_back: Final = self._written_back_request_fields(data, structured_messages, guardrailed_inputs) if written_back is not None: - data["input"] = list(written_back.input) # mutable-ok: JSON body + data["input"] = list(written_back.input) if written_back.instructions is None: data.pop("instructions", None) else: @@ -587,7 +587,7 @@ class OpenAIResponsesHandler(BaseTranslation): ) -> None: if guardrailed_tools is None: return - data["tools"] = list( # mutable-ok: downstream wants a list # rebind-ok: in-place request rewrite + data["tools"] = list( # rebind-ok: in-place request rewrite merge_guardrailed_tools(original_tools, flattened_tool_groups, guardrailed_tools) ) diff --git a/litellm/llms/openai/responses/guardrail_translation/tool_merge.py b/litellm/llms/openai/responses/guardrail_translation/tool_merge.py index b596adfad6f..35cebabc793 100644 --- a/litellm/llms/openai/responses/guardrail_translation/tool_merge.py +++ b/litellm/llms/openai/responses/guardrail_translation/tool_merge.py @@ -90,7 +90,7 @@ def _rebuilt_member(member: Tool, flattened: Tool, guardrailed: Tool, namespace_ if key not in _CHAT_TOOL_TOP_LEVEL_KEYS and flattened.get(key) != value } ) - return {**member, **changed_extras, **changed_function} # mutable-ok: json.dumps rejects MappingProxyType + return {**member, **changed_extras, **changed_function} def _rebuilt_function_members( @@ -134,7 +134,7 @@ def _rebuilt_namespace( ) if not rebuilt_members: return () - return ({**original, "tools": list(rebuilt_members)},) # mutable-ok: json.dumps needs a plain dict and list + return ({**original, "tools": list(rebuilt_members)},) def _merged_original( diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 833ae206024..bfdd355213e 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -313,7 +313,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): is left alone because the API accepts both.""" if tools is None: return None - decoded: Final = [ # mutable-ok: request tools are a JSON list + decoded: Final = [ self._tool_with_object_parameters(model=model, index=index, tool=tool) for index, tool in enumerate(tools) ] return cast("Sequence[ALL_RESPONSES_API_TOOL_PARAMS]", decoded) # cast-ok: dict spread keeps each tool's shape @@ -326,7 +326,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return tool decoded: Final = safe_json_loads(parameters) if isinstance(parameters, str) else None if isinstance(decoded, dict): - return {**tool, "parameters": decoded} # mutable-ok: request tools are JSON dicts + return {**tool, "parameters": decoded} raise litellm.BadRequestError( message=( f"Invalid type for 'tools[{index}].parameters': expected an object, " @@ -383,7 +383,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): genuine_prefix: Final = TOOL_CALL_ITEM_ID_PREFIX_BY_TYPE.get(item_type) if isinstance(item_type, str) else None if genuine_prefix is None or not isinstance(item_id, str) or item_id.startswith(genuine_prefix): return item - return {key: value for key, value in item.items() if key != "id"} # mutable-ok: outgoing JSON request item + return {key: value for key, value in item.items() if key != "id"} def _sanitized_tool_schemas_for_openai( self, @@ -452,14 +452,14 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) if not parameters_update and not tools_update: return entry - return {**entry, **parameters_update, **tools_update} # mutable-ok: request tools are JSON dicts + return {**entry, **parameters_update, **tools_update} @staticmethod def _sanitized_tools( tools: Sequence[object], sanitize: Callable[[Mapping[str, object]], Mapping[str, object]], ) -> Sequence[object]: - sanitized: Final = [ # mutable-ok: request tools are a JSON list + sanitized: Final = [ OpenAIResponsesAPIConfig._sanitized_tool_entry(item, sanitize) if isinstance(item, dict) else item for item in tools ] diff --git a/litellm/llms/openai_like/messages/transformation.py b/litellm/llms/openai_like/messages/transformation.py index ac99617521c..0768eaf4f73 100644 --- a/litellm/llms/openai_like/messages/transformation.py +++ b/litellm/llms/openai_like/messages/transformation.py @@ -69,10 +69,10 @@ class OpenAILikeAnthropicMessagesConfig(AnthropicMessagesConfig): self, model: str, messages: list[dict], # mutable-ok: matches dict-typed base signature - anthropic_messages_optional_request_params: dict, # mutable-ok: matches dict-typed base signature + anthropic_messages_optional_request_params: dict, litellm_params: GenericLiteLLMParams, - headers: dict, # mutable-ok: matches dict-typed base signature - ) -> dict: # mutable-ok: matches dict-typed base signature + headers: dict, + ) -> dict: """ Anthropic ignores prompt-caching hints it cannot honor, but strict non-Anthropic implementations of the Messages API 400 the whole request diff --git a/litellm/llms/sagemaker/chat/transformation.py b/litellm/llms/sagemaker/chat/transformation.py index 04995f32d97..644f479e922 100644 --- a/litellm/llms/sagemaker/chat/transformation.py +++ b/litellm/llms/sagemaker/chat/transformation.py @@ -64,10 +64,10 @@ class SagemakerChatConfig(OpenAIGPTConfig, BaseAWSLLM): self, model: str, messages: list[AllMessageValues], # mutable-ok: matches the base chat transform signature - optional_params: dict, # mutable-ok: matches the base chat transform signature - litellm_params: dict, # mutable-ok: matches the base chat transform signature - headers: dict, # mutable-ok: matches the base chat transform signature - ) -> dict: # mutable-ok: the handler sends this body straight to httpx + optional_params: dict, + litellm_params: dict, + headers: dict, + ) -> dict: request: Final = super().transform_request( model=model, messages=messages, diff --git a/litellm/llms/snowflake/chat/transformation.py b/litellm/llms/snowflake/chat/transformation.py index f65b0876202..40f14805a76 100644 --- a/litellm/llms/snowflake/chat/transformation.py +++ b/litellm/llms/snowflake/chat/transformation.py @@ -118,7 +118,7 @@ def _convert_image_url_to_anthropic(block: Mapping[str, object]) -> object: anthropic_process_openai_file_message({"type": "file", "file": {"file_data": url}}) if select_anthropic_content_block_type_for_file(_data_uri_media_type(url)) == "document" else create_anthropic_image_param( - image_url if isinstance(image_url, dict) else url, # mutable-ok: caller's JSON block + image_url if isinstance(image_url, dict) else url, format=_image_url_field(image_url, "format"), is_bedrock_invoke=True, ) @@ -126,7 +126,7 @@ def _convert_image_url_to_anthropic(block: Mapping[str, object]) -> object: cache_control: Final = block.get("cache_control") if cache_control is None: return converted - return {**converted, "cache_control": cache_control} # mutable-ok: JSON wire block + return {**converted, "cache_control": cache_control} def _image_url_field(image_url: object, key: str) -> str | None: @@ -142,7 +142,7 @@ def _data_uri_media_type(url: str) -> str: def _convert_image_url_blocks_to_anthropic(content: object) -> object: if not isinstance(content, list): return content - return [ # mutable-ok: JSON wire blocks + return [ _convert_image_url_to_anthropic(block) if isinstance(block, Mapping) and block.get("type") == "image_url" else block @@ -160,7 +160,7 @@ def _convert_tool_result_to_anthropic( and non-list shapes it does not model are handled here. """ if not isinstance(content, list): - plain: Final[dict[str, object]] = { # mutable-ok: JSON wire block + plain: Final[dict[str, object]] = { "type": "tool_result", "tool_use_id": tool_call_id, "content": content if isinstance(content, str) else json.dumps(content), @@ -172,7 +172,7 @@ def _convert_tool_result_to_anthropic( ) if cache_control is None: return converted - return {**converted, "cache_control": cache_control} # mutable-ok: JSON wire block + return {**converted, "cache_control": cache_control} def _signed_thinking_blocks(msg: object) -> list[dict[str, object]]: # mutable-ok: JSON wire blocks @@ -183,20 +183,16 @@ def _signed_thinking_blocks(msg: object) -> list[dict[str, object]]: # mutable- """ blocks: Final = msg.get("thinking_blocks") if isinstance(msg, dict) else getattr(msg, "thinking_blocks", None) if not isinstance(blocks, list): - return [] # mutable-ok: JSON wire blocks - return [ # mutable-ok: JSON wire blocks + return [] + return [ dict(block) for block in blocks if isinstance(block, Mapping) and (block.get("signature") or block.get("type") == "redacted_thinking") ] -def _clean_input_schema(schema: object) -> object: # mutable-ok: JSON schema copy - return ( - {key: value for key, value in schema.items() if key != "$schema"} - if isinstance(schema, Mapping) - else schema # mutable-ok: JSON schema copy - ) # mutable-ok: JSON schema copy +def _clean_input_schema(schema: object) -> object: + return {key: value for key, value in schema.items() if key != "$schema"} if isinstance(schema, Mapping) else schema class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): @@ -293,15 +289,13 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): anthropic_tools.append(anthropic_tool) else: anthropic_tools.append( - {**tool, "input_schema": _clean_input_schema(tool["input_schema"])} # mutable-ok: JSON wire tool + {**tool, "input_schema": _clean_input_schema(tool["input_schema"])} if "input_schema" in tool else tool ) return anthropic_tools - def _extract_system_and_messages( # mutable-ok: JSON wire messages - self, messages: list[AllMessageValues] - ) -> tuple[list[dict] | None, list[dict]]: + def _extract_system_and_messages(self, messages: list[AllMessageValues]) -> tuple[list[dict] | None, list[dict]]: """ Split messages into system prompt and conversation turns for Anthropic format. @@ -324,15 +318,13 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): if role == "system": if isinstance(content, str) and content: - system_parts.append({"type": "text", "text": content}) # mutable-ok: JSON wire system block + system_parts.append({"type": "text", "text": content}) elif isinstance(content, list): system_parts.extend( - { # mutable-ok: JSON wire system block + { "type": "text", "text": block.get("text", ""), - **( - {"cache_control": block["cache_control"]} if "cache_control" in block else {} - ), # mutable-ok: JSON wire block + **({"cache_control": block["cache_control"]} if "cache_control" in block else {}), } for block in content if isinstance(block, Mapping) and block.get("type") == "text" @@ -393,15 +385,13 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): ): conversation[-1]["content"].append(tool_result_block) else: - conversation.append( - {"role": "user", "content": [tool_result_block]} # mutable-ok: JSON wire message - ) # mutable-ok: JSON wire message + conversation.append({"role": "user", "content": [tool_result_block]}) else: - conversation.append( # mutable-ok: JSON wire message - { # mutable-ok: JSON wire message + conversation.append( + { "role": role, "content": _convert_image_url_blocks_to_anthropic(content), - } # mutable-ok: JSON wire message + } ) system: Final[list[dict] | None] = system_parts if system_parts else None # mutable-ok: JSON wire messages @@ -430,10 +420,10 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): self, model: str, messages: list[AllMessageValues], # mutable-ok: BaseConfig signature - optional_params: dict[str, object], # mutable-ok: BaseConfig signature - litellm_params: dict[str, object], # mutable-ok: BaseConfig signature - headers: dict[str, object], # mutable-ok: BaseConfig signature - ) -> dict[str, object]: # mutable-ok: BaseConfig signature + optional_params: dict[str, object], + litellm_params: dict[str, object], + headers: dict[str, object], + ) -> dict[str, object]: inlined_messages: Final = await async_inline_remote_media(messages) if _is_claude_model(model) else messages return self.transform_request(model, inlined_messages, optional_params, litellm_params, headers) @@ -510,19 +500,17 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): model_name: Final = model.removeprefix("snowflake/") - body: Final[dict[str, object]] = normalize_cache_control_in_anthropic_payload( # mutable-ok: JSON wire body - { # mutable-ok: JSON wire body + body: Final[dict[str, object]] = normalize_cache_control_in_anthropic_payload( + { "model": model_name, "messages": conversation, "stream": stream, **optional_params, - **extra_body, # mutable-ok: JSON wire body + **extra_body, } ) if system is not None: - body["system"] = normalize_cache_control_in_anthropic_payload( # mutable-ok: JSON wire payload - {"system": system} # mutable-ok: JSON wire payload - )["system"] + body["system"] = normalize_cache_control_in_anthropic_payload({"system": system})["system"] if "max_tokens" not in body: body["max_tokens"] = 4096 # reasonable default; Anthropic API max varies by model diff --git a/litellm/llms/tinyfish/search/transformation.py b/litellm/llms/tinyfish/search/transformation.py index 460394c6f2d..d5ed7da3815 100644 --- a/litellm/llms/tinyfish/search/transformation.py +++ b/litellm/llms/tinyfish/search/transformation.py @@ -251,7 +251,7 @@ class TinyfishSearchConfig(BaseSearchConfig): return self._wrap_error( error_message=error.response.text, status_code=error.response.status_code, - headers=dict(error.response.headers), # mutable-ok: existing error wrapper requires dict headers + headers=dict(error.response.headers), ) def _wrap_error( diff --git a/litellm/llms/together_ai/chat/transformation.py b/litellm/llms/together_ai/chat/transformation.py index 449cd3ecbc5..948bd3c8e14 100644 --- a/litellm/llms/together_ai/chat/transformation.py +++ b/litellm/llms/together_ai/chat/transformation.py @@ -178,9 +178,7 @@ def _without_litellm_internal_fields(message: AllMessageValues) -> AllMessageVal return message return cast( # cast-ok: rebuilding the same TypedDict minus internal keys loses the narrowed type "AllMessageValues", - { # mutable-ok: TypedDict rebuild minus internal keys - key: value for key, value in message.items() if key not in LITELLM_INTERNAL_ASSISTANT_FIELDS - }, + {key: value for key, value in message.items() if key not in LITELLM_INTERNAL_ASSISTANT_FIELDS}, ) @@ -210,9 +208,7 @@ class TogetherAIChatConfig(OpenAIGPTConfig): """Together consumes replayed assistant `reasoning_content` (preserved thinking via `chat_template_kwargs: {"clear_thinking": false}`), so it must stay in the payload; only litellm-internal fields are stripped before sending.""" - stripped: Final = [ # mutable-ok: super() requires a list - _without_litellm_internal_fields(message) for message in messages - ] + stripped: Final = [_without_litellm_internal_fields(message) for message in messages] if is_async: return super()._transform_messages(stripped, model, is_async=True) return super()._transform_messages(stripped, model, is_async=False) @@ -221,7 +217,7 @@ class TogetherAIChatConfig(OpenAIGPTConfig): supported_params: Final = super().get_supported_openai_params(model) if not _supports_together_reasoning(model): return supported_params - return [ # mutable-ok: the inherited contract returns a plain list; building fresh avoids mutating the base class's value + return [ *supported_params, "reasoning_effort", ] diff --git a/litellm/llms/valkey/vector_stores/transformation.py b/litellm/llms/valkey/vector_stores/transformation.py index b250f71cf3f..50485899818 100644 --- a/litellm/llms/valkey/vector_stores/transformation.py +++ b/litellm/llms/valkey/vector_stores/transformation.py @@ -185,9 +185,7 @@ class ValkeyVectorStoreConfig(BaseDirectVectorStoreConfig): @staticmethod def _to_result(doc: "Document", text_field: str) -> VectorStoreSearchResult: - content: Final = [ # mutable-ok: VectorStoreSearchResult declares a list of content parts - VectorStoreResultContent(text=str(getattr(doc, text_field, "")), type="text") - ] + content: Final = [VectorStoreResultContent(text=str(getattr(doc, text_field, "")), type="text")] return VectorStoreSearchResult( score=1.0 - float(getattr(doc, DISTANCE_FIELD_NAME)), content=content, @@ -235,11 +233,11 @@ class ValkeyVectorStoreConfig(BaseDirectVectorStoreConfig): if embedding_executor is not None else self.embedding_fn( model=params.require_embedding_model(), - input=[query_text], # mutable-ok: the injected embedding callable requires list input + input=[query_text], **(params.litellm_embedding_config or _EMPTY_EMBEDDING_CONFIG), ) ) - vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} # mutable-ok: redis-py API + vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} if self.sync_client is not None: raw: Final = self.sync_client.ft(vector_store_id).search(knn, query_params=vec_params) @@ -283,11 +281,11 @@ class ValkeyVectorStoreConfig(BaseDirectVectorStoreConfig): if embedding_executor is not None else await self.aembedding_fn( model=params.require_embedding_model(), - input=[query_text], # mutable-ok: the injected embedding callable requires list input + input=[query_text], **(params.litellm_embedding_config or _EMPTY_EMBEDDING_CONFIG), ) ) - vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} # mutable-ok: redis-py API + vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} if self.async_client is not None: raw: Final = await self.async_client.ft(vector_store_id).search( # pyright: ignore[reportGeneralTypeIssues] # types-redis 4.6 stubs shadow redis 5.3.1 and type the async client's ft() as the sync Search, so search() returns a non-awaitable Result; it is a coroutine at runtime diff --git a/litellm/llms/vertex_ai/audio_transcription/gemini_transcribe_transformation.py b/litellm/llms/vertex_ai/audio_transcription/gemini_transcribe_transformation.py index f4db5eb110c..8d9c22828a5 100644 --- a/litellm/llms/vertex_ai/audio_transcription/gemini_transcribe_transformation.py +++ b/litellm/llms/vertex_ai/audio_transcription/gemini_transcribe_transformation.py @@ -62,7 +62,7 @@ class VertexGeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig, VertexB optional_params: Mapping[str, object], model: str, drop_params: bool, - ) -> dict[str, object]: # mutable-ok: BaseAudioTranscriptionConfig signature + ) -> dict[str, object]: supported_params: Final = frozenset(self.get_supported_openai_params(model)) mapped: Final = { **optional_params, @@ -86,7 +86,7 @@ class VertexGeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig, VertexB self, error_message: str, status_code: int, - headers: dict | Headers, # mutable-ok: base signature and VertexAIError take dict | Headers + headers: dict | Headers, ) -> BaseLLMException: return VertexAIError(status_code=status_code, message=error_message, headers=headers) @@ -99,7 +99,7 @@ class VertexGeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig, VertexB litellm_params: Mapping[str, object], api_key: str | None = None, api_base: str | None = None, - ) -> dict[str, str]: # mutable-ok: BaseAudioTranscriptionConfig signature + ) -> dict[str, str]: vertex_params: Final = dict(litellm_params) access_token, project_id = self._ensure_access_token( credentials=self.safe_get_vertex_ai_credentials(vertex_params), diff --git a/litellm/llms/vertex_ai/batches/handler.py b/litellm/llms/vertex_ai/batches/handler.py index a15ea4d845b..35f9e13c7c3 100644 --- a/litellm/llms/vertex_ai/batches/handler.py +++ b/litellm/llms/vertex_ai/batches/handler.py @@ -199,7 +199,7 @@ class VertexAIBatchPrediction(VertexLLM): def _resolve_fine_tuned_endpoint_model( self, vertex_batch_request: VertexAIBatchPredictionJob, - headers: dict[str, str], # mutable-ok: HTTPHandler.get only accepts dict headers + headers: dict[str, str], sync_handler: HTTPHandler, api_base: str | None, vertex_location: str, diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 14aebcaabaf..31e985ed05d 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -1257,11 +1257,7 @@ class VertexAITokenCounter(BaseTokenCounter): ) resolved_contents: Final = ( - contents - if contents is not None - else _gemini_convert_messages_with_history( - messages=messages or [] # mutable-ok: fallback for None messages; helper signature requires list - ) + contents if contents is not None else _gemini_convert_messages_with_history(messages=messages or []) ) count_tokens_params: Final = { diff --git a/litellm/llms/vertex_ai/interactions/transformation.py b/litellm/llms/vertex_ai/interactions/transformation.py index 0764a8bea62..d6f7923951c 100644 --- a/litellm/llms/vertex_ai/interactions/transformation.py +++ b/litellm/llms/vertex_ai/interactions/transformation.py @@ -89,9 +89,9 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): headers: Mapping[str, str], model: str, litellm_params: GenericLiteLLMParams | None, - ) -> dict: # mutable-ok: BaseInteractionsAPIConfig declares plain-dict headers + ) -> dict: access_token, _ = self._mint(litellm_params or GenericLiteLLMParams()) - return { # mutable-ok: BaseInteractionsAPIConfig declares plain-dict headers + return { "Content-Type": "application/json", "Authorization": f"Bearer {access_token}", **headers, @@ -117,9 +117,9 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): api_base: str, litellm_params: GenericLiteLLMParams, url_suffix: str = "", - ) -> tuple[str, dict]: # mutable-ok: BaseInteractionsAPIConfig declares a plain-dict request body + ) -> tuple[str, dict]: target: Final = self._target(api_base or None, litellm_params) - return f"{target.interaction_url(interaction_id)}{url_suffix}", {} # mutable-ok: same base contract + return f"{target.interaction_url(interaction_id)}{url_suffix}", {} def transform_get_interaction_request( self, @@ -127,7 +127,7 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): api_base: str, litellm_params: GenericLiteLLMParams, headers: Mapping[str, str], - ) -> tuple[str, dict]: # mutable-ok: BaseInteractionsAPIConfig declares a plain-dict request body + ) -> tuple[str, dict]: return self._interaction_by_id_request(interaction_id, api_base, litellm_params) def transform_delete_interaction_request( @@ -136,7 +136,7 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): api_base: str, litellm_params: GenericLiteLLMParams, headers: Mapping[str, str], - ) -> tuple[str, dict]: # mutable-ok: BaseInteractionsAPIConfig declares a plain-dict request body + ) -> tuple[str, dict]: return self._interaction_by_id_request(interaction_id, api_base, litellm_params) def transform_cancel_interaction_request( @@ -145,5 +145,5 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): api_base: str, litellm_params: GenericLiteLLMParams, headers: Mapping[str, str], - ) -> tuple[str, dict]: # mutable-ok: BaseInteractionsAPIConfig declares a plain-dict request body + ) -> tuple[str, dict]: return self._interaction_by_id_request(interaction_id, api_base, litellm_params, url_suffix=":cancel") diff --git a/litellm/llms/vertex_ai/text_to_speech/transformation.py b/litellm/llms/vertex_ai/text_to_speech/transformation.py index d382f43495f..b4049be4f5b 100644 --- a/litellm/llms/vertex_ai/text_to_speech/transformation.py +++ b/litellm/llms/vertex_ai/text_to_speech/transformation.py @@ -43,9 +43,7 @@ else: LiteLLMLoggingObj = Any HttpxBinaryResponseContent = Any -_LyriaVoice: TypeAlias = ( - str | dict | None -) # mutable-ok: inherited interface supports structured provider voice dictionaries +_LyriaVoice: TypeAlias = str | dict | None class VertexAITextToSpeechConfig(BaseTextToSpeechConfig, VertexBase): @@ -501,21 +499,17 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): def get_supported_openai_params( self, model: str ) -> list: # mutable-ok: inherited provider interface returns a concrete parameter list - return [ # mutable-ok: inherited provider interface requires a concrete parameter list - "response_format" - ] + return ["response_format"] def map_openai_params( self, model: str, - optional_params: dict, # mutable-ok: inherited provider interface accepts a concrete parameter dictionary + optional_params: dict, voice: _LyriaVoice = None, drop_params: bool = False, - kwargs: dict | None = None, # mutable-ok: inherited provider interface accepts a concrete keyword dictionary - ) -> tuple[str | None, dict]: # mutable-ok: inherited provider interface returns concrete mapped parameters - mapped_params: Final = dict( # mutable-ok: mapping drops unsupported parameters before provider dispatch - optional_params - ) + kwargs: dict | None = None, + ) -> tuple[str | None, dict]: + mapped_params: Final = dict(optional_params) base_model: Final = model.removeprefix("vertex_ai/") model_info: Final = self._get_model_info(model=model) unsupported_params: Final = tuple( @@ -554,7 +548,7 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): self, model: str, api_base: str | None, - litellm_params: dict, # mutable-ok: inherited provider interface accepts concrete LiteLLM parameters + litellm_params: dict, ) -> str: base_model: Final = model.removeprefix("vertex_ai/") model_info: Final = self._get_model_info(model=model) @@ -582,7 +576,7 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): return VertexAIInteractionsConfig(mint_access_token=mint_access_token).get_complete_url( api_base=api_base, model=base_model, - litellm_params={ # mutable-ok: interactions dispatch expects a concrete parameter dictionary + litellm_params={ **litellm_params, "vertex_project": project, "vertex_location": "global", @@ -603,9 +597,9 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): model: str, input: str, voice: str | None, - optional_params: dict, # mutable-ok: inherited provider interface accepts concrete mapped parameters - litellm_params: dict, # mutable-ok: inherited provider interface accepts concrete LiteLLM parameters - headers: dict, # mutable-ok: inherited provider interface accepts and updates concrete HTTP headers + optional_params: dict, + litellm_params: dict, + headers: dict, ) -> TextToSpeechRequestData: access_token, project = self._ensure_access_token( credentials=self.safe_get_vertex_ai_credentials(litellm_params), @@ -613,7 +607,7 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): custom_llm_provider="vertex_ai", ) headers.update( - { # mutable-ok: HTTP dispatch requires a concrete header dictionary + { "Authorization": f"Bearer {access_token}", "x-goog-user-project": project, "Content-Type": "application/json", @@ -621,28 +615,24 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): ) base_model: Final = model.removeprefix("vertex_ai/") model_info: Final = self._get_model_info(model=model) - request_body: Final[dict[str, object]] = ( # mutable-ok: HTTP dispatch requires a concrete provider payload - { # mutable-ok: predict dispatch requires a concrete provider request dictionary - "instances": [ # mutable-ok: predict dispatch requires a concrete instances list - {"prompt": input} # mutable-ok: predict dispatch requires a concrete instance dictionary - ], - "parameters": { # mutable-ok: predict dispatch requires a concrete parameters dictionary - "sample_count": 1 - }, + request_body: Final[dict[str, object]] = ( + { + "instances": [{"prompt": input}], + "parameters": {"sample_count": 1}, } if model_info["vertex_ai_audio_api"] == "lyria_predict" - else { # mutable-ok: interactions dispatch requires a concrete provider request dictionary + else { "model": base_model, "input": input, **( - { # mutable-ok: interactions dispatch requires a nested response-format dictionary - "response_format": { # mutable-ok: interactions response format is a concrete provider payload + { + "response_format": { "type": "audio", "mime_type": "audio/wav", } } if optional_params.get("response_format") == "wav" - else {} # mutable-ok: no response override is merged for non-WAV output + else {} ), } ) diff --git a/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py b/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py index 58cf7c7e702..4abc85e80fc 100644 --- a/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py +++ b/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py @@ -137,8 +137,8 @@ class VertexGemmaConfig(OpenAIGPTConfig): def _sync_post( client: HTTPHandler | httpx.Client | None, api_base: str, - headers: dict[str, str], # mutable-ok: forwarded to post(headers: dict | None) - request_data: dict[str, Any], # mutable-ok: forwarded to post(json: dict | ...) + headers: dict[str, str], + request_data: dict[str, Any], timeout: float | httpx.Timeout | None, ) -> httpx.Response: if isinstance(client, HTTPHandler): @@ -172,8 +172,8 @@ class VertexGemmaConfig(OpenAIGPTConfig): async def _async_post( client: AsyncHTTPHandler | httpx.AsyncClient | None, api_base: str, - headers: dict[str, str], # mutable-ok: forwarded to post(headers: dict | None) - request_data: dict[str, Any], # mutable-ok: forwarded to post(json: dict | ...) + headers: dict[str, str], + request_data: dict[str, Any], timeout: float | httpx.Timeout | None, ) -> httpx.Response: from litellm.llms.custom_httpx.http_handler import get_async_httpx_client diff --git a/litellm/llms/voyage/embedding/transformation_contextual.py b/litellm/llms/voyage/embedding/transformation_contextual.py index 870de8756bb..1ce2e6e3f29 100644 --- a/litellm/llms/voyage/embedding/transformation_contextual.py +++ b/litellm/llms/voyage/embedding/transformation_contextual.py @@ -65,11 +65,11 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig): def map_openai_params( self, - non_default_params: dict, # mutable-ok: base class signature - optional_params: dict, # mutable-ok: base class signature + non_default_params: dict, + optional_params: dict, model: str, drop_params: bool, - ) -> dict: # mutable-ok: base class signature + ) -> dict: """ Map OpenAI params to Voyage params @@ -83,7 +83,7 @@ class VoyageContextualEmbeddingConfig(BaseEmbeddingConfig): def validate_environment( self, - headers: dict, # mutable-ok: base class signature + headers: dict, model: str, messages: list[AllMessageValues], optional_params: dict, diff --git a/litellm/llms/wandb/chat/transformation.py b/litellm/llms/wandb/chat/transformation.py index fdd6644f03d..f891898f443 100644 --- a/litellm/llms/wandb/chat/transformation.py +++ b/litellm/llms/wandb/chat/transformation.py @@ -14,7 +14,7 @@ class WandbConfig(OpenAIGPTConfig): def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: inherited contract supported_params: Final = super().get_supported_openai_params(model) if litellm.supports_reasoning(model=model, custom_llm_provider="wandb"): - return supported_params + ["reasoning_effort"] # mutable-ok: inherited contract + return supported_params + ["reasoning_effort"] return supported_params def map_openai_params( diff --git a/litellm/llms/xai/responses/transformation.py b/litellm/llms/xai/responses/transformation.py index 646d6798783..6fcbab434a6 100644 --- a/litellm/llms/xai/responses/transformation.py +++ b/litellm/llms/xai/responses/transformation.py @@ -297,7 +297,7 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig): def transform_streaming_response( self, model: str, - parsed_chunk: dict, # mutable-ok: overrides the base class signature + parsed_chunk: dict, logging_obj: LiteLLMLoggingObj, ) -> ResponsesAPIStreamingResponse: event: Final = super().transform_streaming_response( diff --git a/litellm/main.py b/litellm/main.py index 17edafcdfca..982a6cb1ffe 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -7647,11 +7647,11 @@ async def amoderation( }, custom_llm_provider=custom_llm_provider, ) - moderation_request: Final = {"input": input, "model": model} # mutable-ok: logged as the raw request body + moderation_request: Final = {"input": input, "model": model} litellm_logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict + additional_args={ "complete_input_dict": moderation_request, "api_base": str(_openai_client.base_url), }, @@ -8715,8 +8715,8 @@ def _stream_builder_response_cost(response: ModelResponse, logging_obj: Optional def _joined_streamed_citations(streamed_citations: "tuple[object, ...]") -> "list[object]": if all(isinstance(citation, list) for citation in streamed_citations): - return list(streamed_citations) # mutable-ok: JSON list field - return [list(streamed_citations)] # mutable-ok: JSON list field + return list(streamed_citations) + return [list(streamed_citations)] def _stream_builder_model_map_cost(response: ModelResponse) -> float | None: @@ -8982,11 +8982,9 @@ def stream_chunk_builder( fields["citation"] for fields in provider_field_dicts if fields.get("citation") is not None ) citation_fields: Final = ( - {"citations": _joined_streamed_citations(streamed_citations)} # mutable-ok: JSON dict field - if streamed_citations - else {} # mutable-ok: JSON dict field + {"citations": _joined_streamed_citations(streamed_citations)} if streamed_citations else {} ) - combined_provider_fields: Final = { # mutable-ok: Message.provider_specific_fields is a plain dict field + combined_provider_fields: Final = { key: value for fields in (citation_fields, *provider_field_dicts) for key, value in fields.items() diff --git a/litellm/passthrough/main.py b/litellm/passthrough/main.py index 73d8bab686b..b26fb38b2de 100644 --- a/litellm/passthrough/main.py +++ b/litellm/passthrough/main.py @@ -82,7 +82,7 @@ class AsyncPassthroughStreamingResponse(AsyncGenerator[bytes, bytes]): self._spend = _SpendCollection(provider_config, litellm_logging_obj) self._flush_scheduled = False self._background_tasks: set[asyncio.Task] = set() # mutable-ok: instance set for background task tracking - self._hidden_params: dict[str, object] = {} # mutable-ok: router attaches response headers here in place + self._hidden_params: dict[str, object] = {} @property def status_code(self) -> int: diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index b0d57cb6228..152d08e1cd9 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -792,10 +792,10 @@ class MCPRequestHandler: requested_name: str, authorization_value: str, litellm_api_key: str, - mcp_server_auth_headers: dict[str, dict[str, str]] | None, # mutable-ok: existing MCP sink shape + mcp_server_auth_headers: dict[str, dict[str, str]] | None, request: Request, route: str, - ) -> tuple[UserAPIKeyAuth, dict[str, dict[str, str]] | None]: # mutable-ok: existing MCP sink shape + ) -> tuple[UserAPIKeyAuth, dict[str, dict[str, str]] | None]: if is_bridge_envelope_shaped(authorization_value): return await MCPRequestHandler._admit_dcr_bridge_delegate( server=server, diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 789b2ffaef4..a8d2b8d374c 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -631,7 +631,6 @@ async def get_all_mcp_servers( where: Final[prisma_db_types.LiteLLM_MCPServerTableWhereInput] = ( {"approval_status": approval_status} if approval_status is not None - # mutable-ok: prisma where-inputs must be plain dicts, and both `NOT` and `not` drop # NULL rows (measured), so the OR is the only NULL-preserving way to exclude drafts else {"OR": [{"approval_status": None}, {"approval_status": {"not": MCPApprovalStatus.draft}}]} ) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_debug.py b/litellm/proxy/_experimental/mcp_server/mcp_debug.py index 1f157aefdc3..9424e810845 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_debug.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_debug.py @@ -165,9 +165,7 @@ class MCPAuthDiagnostics: { "x-mcp-debug-auth-resolution": AuthResolution.multiple.value, "x-mcp-debug-auth-resolutions": json.dumps( - { - server_id: source.value for server_id, source in self._outcomes[:32] - }, # mutable-ok: JSON encoder requires a concrete dict + {server_id: source.value for server_id, source in self._outcomes[:32]}, separators=(",", ":"), ensure_ascii=True, ), @@ -196,7 +194,7 @@ class _DiagnosticSend: self._start = None headers: Final = MappingProxyType({**self._headers, **self._resolution()}) await self._send( - { # mutable-ok: ASGI send consumes a mutable message mapping + { **start, "headers": tuple(start.get("headers", ())) + tuple((key.encode(), value.encode()) for key, value in headers.items()), @@ -422,10 +420,8 @@ def _sensitive_field(key: str) -> bool: def _redact_object( fields: Mapping[str, JsonValue], -) -> dict[str, JsonValue]: # mutable-ok: the standard JSON encoder requires dict objects - return { # mutable-ok: construct the JSON object once for the standard parser and encoder - key: REDACTED if _sensitive_field(key) else value for key, value in fields.items() - } +) -> dict[str, JsonValue]: + return {key: REDACTED if _sensitive_field(key) else value for key, value in fields.items()} def _header_secret_values(name: str, value: str) -> tuple[str, ...]: diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index fb0c623473a..44744488ea6 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1161,10 +1161,10 @@ def _openapi_forwarded_extra_headers( def _resolve_openapi_tool_auth( mcp_server: MCPServer, mcp_auth_header: str | None, - mcp_server_auth_headers: Mapping[str, str | dict[str, str]] | None, # mutable-ok: sink shape - raw_headers: dict[str, str] | None, # mutable-ok: sink takes a concrete dict + mcp_server_auth_headers: Mapping[str, str | dict[str, str]] | None, + raw_headers: dict[str, str] | None, user_api_key_auth: UserAPIKeyAuth | None, -) -> tuple[str | None, dict[str, str] | None, str | dict[str, str] | None]: # mutable-ok: sink shapes +) -> tuple[str | None, dict[str, str] | None, str | dict[str, str] | None]: """The caller's upstream credential for one ``spec_path`` server, for both OpenAPI dispatch arms. A per-server ``x-mcp-{alias}-authorization`` wins over the deprecated global / BYOK @@ -1650,7 +1650,7 @@ def _create_elicitation_callback(): def _record_mcp_guardrail_evaluations( - synthetic_llm_data: dict[str, Any], # mutable-ok: `_sync_guardrail_info_to_logging_obj` takes a concrete dict + synthetic_llm_data: dict[str, Any], litellm_logging_obj: "LiteLLMLoggingObj | None", ) -> None: """Bridge guardrail decision records off an MCP synthetic request onto the request's logger. @@ -1690,9 +1690,7 @@ class _DiscoveryCache(Generic[_DiscoveryItem]): self._ttl = ttl self._adapter = adapter self._entries = InMemoryCache(max_size_in_memory=_DISCOVERY_CACHE_LIMIT, max_size_per_item=64, clock=clock) - self._pending: dict[ - _DiscoveryKey, asyncio.Task[list[_DiscoveryItem]] - ] = {} # mutable-ok: constant-time fetch registration + self._pending: dict[_DiscoveryKey, asyncio.Task[list[_DiscoveryItem]]] = {} self._waiters: dict[asyncio.Task[list[_DiscoveryItem]], int] = {} # mutable-ok: constant-time waiter accounting def invalidate(self, server_id: str) -> None: @@ -2374,7 +2372,7 @@ class MCPServerManager: used_aliases: Final = set() # server_id -> the config server_name that claimed it, so a pinned id cannot silently # overwrite another server's entry in self.config_mcp_servers. - assigned_server_ids: MutableMapping[str, str] = {} # mutable-ok: per-load collision index + assigned_server_ids: MutableMapping[str, str] = {} _validate_config_server_names(mcp_servers_config) identifier_owners: Final = _config_identifier_owners(mcp_servers_config, mcp_aliases) @@ -4792,7 +4790,7 @@ class MCPServerManager: try: client: Final = get_async_httpx_client( llm_provider=httpxSpecialProvider.MCP, - params={"timeout": MCP_METADATA_TIMEOUT}, # mutable-ok: HTTP client factory requires a dict + params={"timeout": MCP_METADATA_TIMEOUT}, ) response: Final = await client.get(server_url) response.raise_for_status() diff --git a/litellm/proxy/_experimental/mcp_server/oauth_identity_binding.py b/litellm/proxy/_experimental/mcp_server/oauth_identity_binding.py index f02e6c85d9b..c31560a9f63 100644 --- a/litellm/proxy/_experimental/mcp_server/oauth_identity_binding.py +++ b/litellm/proxy/_experimental/mcp_server/oauth_identity_binding.py @@ -41,11 +41,11 @@ _JWKS_CACHE_TTL_SECONDS: Final = 3600 _jwks_cache: Final = InMemoryCache(default_ttl=_JWKS_CACHE_TTL_SECONDS) JwksFetcher: TypeAlias = Callable[ - [MCPOAuthIdentityBinding], # mutable-ok: Callable parameter syntax requires a list + [MCPOAuthIdentityBinding], Awaitable[Sequence[Mapping[str, object]]], ] CallerPrincipalLoader: TypeAlias = Callable[ - [str, MCPOAuthIdentityBinding], # mutable-ok: Callable parameter syntax requires a list + [str, MCPOAuthIdentityBinding], Awaitable[str | None], ] @@ -57,7 +57,7 @@ class VerifiedRefreshToken: StoredRefreshTokenLoader: TypeAlias = Callable[ - [str, str, MCPOAuthIdentityBinding], # mutable-ok: Callable parameter syntax requires a list + [str, str, MCPOAuthIdentityBinding], Awaitable[VerifiedRefreshToken | None], ] @@ -128,7 +128,7 @@ def _select_signing_key(id_token: str, keys: Sequence[Mapping[str, object]]) -> kid: Final = header.get("kid") for key in keys: if kid is None or key.get("kid") == kid: - return jwt.PyJWK(dict(key)) # mutable-ok: PyJWT requires a concrete JWK dictionary + return jwt.PyJWK(dict(key)) return _BindingRejection( code="oauth_identity_binding_failed", description=f"id_token signing key (kid={kid!r}) not found in the issuer's JWKS", diff --git a/litellm/proxy/_experimental/mcp_server/outbound_credentials/sso_assertion_refresher.py b/litellm/proxy/_experimental/mcp_server/outbound_credentials/sso_assertion_refresher.py index 7600fd7ab8a..bca5848febe 100644 --- a/litellm/proxy/_experimental/mcp_server/outbound_credentials/sso_assertion_refresher.py +++ b/litellm/proxy/_experimental/mcp_server/outbound_credentials/sso_assertion_refresher.py @@ -287,7 +287,7 @@ class SSOAssertionRefresher: client_id=config.client_id, client_secret=config.client_secret.get_secret_value(), ) - form: Final = { # mutable-ok: the RFC 6749 form body is a wire format the HTTP client takes as a mapping + form: Final = { "grant_type": _REFRESH_GRANT_TYPE, "refresh_token": carried_refresh_token, **client_auth.body, diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 7a97e995570..0185b5727c4 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -1623,7 +1623,7 @@ if MCP_AVAILABLE: "MCP tools/list preview timed out after %s seconds while paginating upstream tools", listing_deadline, ) - return { # mutable-ok: error response payload + return { "status": "error", "error": True, "message": f"Timed out listing tools after {listing_deadline} seconds. " diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 7feb1fd468d..6ec3d0c8952 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -550,10 +550,8 @@ if MCP_AVAILABLE: ) opts: Final = ( base_options.model_copy( - update={ # mutable-ok: Pydantic update payload - "capabilities": base_options.capabilities.model_copy( - update={"prompts": None, "resources": None} # mutable-ok: Pydantic update payload - ) + update={ + "capabilities": base_options.capabilities.model_copy(update={"prompts": None, "resources": None}) } ) if _mcp_proxy_mode.get() @@ -847,7 +845,7 @@ if MCP_AVAILABLE: ) if _mcp_proxy_mode.get(): - return [Tool.model_validate(d) for d in get_mcp_proxy_tool_definitions()] # mutable-ok: MCP SDK list + return [Tool.model_validate(d) for d in get_mcp_proxy_tool_definitions()] if getattr( getattr(user_api_key_auth, "object_permission", None), "mcp_tool_search_enabled", @@ -1002,9 +1000,7 @@ if MCP_AVAILABLE: if _mcp_proxy_mode.get() and name not in MCP_PROXY_TOOL_NAMES: return CallToolResult( - content=[ # mutable-ok: MCP result content - TextContent(type="text", text=f"Tool {name} is unavailable on /mcp/proxy") - ], + content=[TextContent(type="text", text=f"Tool {name} is unavailable on /mcp/proxy")], isError=True, ) @@ -1014,7 +1010,7 @@ if MCP_AVAILABLE: proxy_logging_obj: Final = ( await _build_virtual_call_logging_obj( name=name, - arguments=arguments or {}, # mutable-ok: logging pipeline payload + arguments=arguments or {}, user_api_key_auth=user_api_key_auth, raw_headers=raw_headers, client_ip=client_ip, @@ -1025,7 +1021,7 @@ if MCP_AVAILABLE: try: proxy_result: Final = await handle_mcp_proxy_tool( name=name, - arguments=arguments or {}, # mutable-ok: proxy handler payload + arguments=arguments or {}, user_api_key_dict=user_api_key_auth, client_ip=client_ip, mcp_servers=mcp_servers, @@ -1048,7 +1044,7 @@ if MCP_AVAILABLE: ) if not isinstance(exc, MCPUpstreamAuthError): await request_logging_obj.post_call_failure_hook( - request_data={ # mutable-ok: failure hook mutates its request payload + request_data={ "name": name, "arguments": arguments, "litellm_logging_obj": proxy_logging_obj, @@ -2316,9 +2312,7 @@ if MCP_AVAILABLE: if mcp_proxy_mode: from litellm.proxy._experimental.mcp_server.tool_search import with_mcp_proxy_identity - filtered_tools = [ # mutable-ok: MCP tool pipeline - with_mcp_proxy_identity(tool, server.server_id) for tool in filtered_tools - ] + filtered_tools = [with_mcp_proxy_identity(tool, server.server_id) for tool in filtered_tools] else: filtered_tools = apply_tool_overrides(filtered_tools, server) diff --git a/litellm/proxy/_experimental/mcp_server/tool_search.py b/litellm/proxy/_experimental/mcp_server/tool_search.py index 2c73f9b863b..9e244242055 100644 --- a/litellm/proxy/_experimental/mcp_server/tool_search.py +++ b/litellm/proxy/_experimental/mcp_server/tool_search.py @@ -111,22 +111,18 @@ _MCP_PROXY_IDENTITY_META_KEY: Final[str] = "litellm.ai/proxy_tool_identity" def with_mcp_proxy_identity(tool: Tool, server_id: str) -> Tool: identity: Final[MCPProxyToolIdentity] = {"server_id": server_id, "tool_name": tool.name} - return tool.model_copy( # mutable-ok: Pydantic requires mutable update and metadata mappings - update={ # mutable-ok: Pydantic update payload - "meta": {**(tool.meta or {}), _MCP_PROXY_IDENTITY_META_KEY: identity} # mutable-ok: metadata mapping - } - ) + return tool.model_copy(update={"meta": {**(tool.meta or {}), _MCP_PROXY_IDENTITY_META_KEY: identity}}) 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 = (tool.meta or {}).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 + return {"server_id": server_id, "tool_name": tool_name} def mcp_proxy_tool_id(tool: Tool) -> str: @@ -140,7 +136,7 @@ def _proxy_search_result(hit: MCPToolSearchHit) -> MCPProxySearchResult: "name": hit.tool.name, "description": hit.tool.description or "", } - return {**base, "score": hit.score} if hit.score is not None else base # mutable-ok: wire result payload + return {**base, "score": hit.score} if hit.score is not None else base def _proxy_schema_result(tool: Tool) -> MCPProxySchemaResult: @@ -152,7 +148,7 @@ def _proxy_schema_result(tool: Tool) -> MCPProxySchemaResult: } if tool.outputSchema is None: return base - return {**base, "outputSchema": tool.outputSchema} # mutable-ok: wire schema payload + return {**base, "outputSchema": tool.outputSchema} def _tool_text(tool: Tool) -> str: @@ -252,7 +248,7 @@ class VirtualToolDefinition(TypedDict): def _json_array(*items: str) -> Sequence[str]: - return list(items) # mutable-ok: jsonschema's metaschema only accepts a JSON array for required + return list(items) _MCP_TOOL_SEARCH_DEFINITION: Final[VirtualToolDefinition] = { @@ -371,7 +367,7 @@ def _text_tool_result(text: str, is_error: bool) -> CallToolResult: from mcp.types import CallToolResult, TextContent return CallToolResult( - content=[TextContent(type="text", text=text)], # mutable-ok: CallToolResult accepts only list content + content=[TextContent(type="text", text=text)], isError=is_error, ) @@ -495,14 +491,14 @@ async def handle_mcp_tool_search( async def handle_mcp_proxy_tool( name: str, - arguments: dict[str, object], # mutable-ok: MCP dispatcher passes mutable call arguments + arguments: dict[str, object], user_api_key_dict: UserAPIKeyAuth, client_ip: str | None = None, mcp_servers: list[str] | None = None, # mutable-ok: preserve MCP scope container for existing resolver mcp_auth_header: str | None = None, - mcp_server_auth_headers: dict[str, dict[str, str]] | None = None, # mutable-ok: preserve forwarded headers - oauth2_headers: dict[str, str] | None = None, # mutable-ok: preserve forwarded headers - raw_headers: dict[str, str] | None = None, # mutable-ok: preserve request headers + mcp_server_auth_headers: dict[str, dict[str, str]] | None = None, + oauth2_headers: dict[str, str] | None = None, + raw_headers: dict[str, str] | None = None, litellm_logging_obj: LiteLLMLoggingObj | None = None, ) -> CallToolResult: from fastapi import HTTPException @@ -524,7 +520,7 @@ async def handle_mcp_proxy_tool( raw_headers=raw_headers, mcp_proxy_mode=True, ) - tools_by_id: Final = {mcp_proxy_tool_id(tool): tool for tool in listing.tools} # mutable-ok: lookup index + tools_by_id: Final = {mcp_proxy_tool_id(tool): tool for tool in listing.tools} if name == MCP_PROXY_SEARCH_TOOL_NAME: llm_router: Final = proxy_server.llm_router @@ -561,7 +557,7 @@ async def handle_mcp_proxy_tool( if name != MCP_PROXY_CALL_TOOL_NAME: raise HTTPException(status_code=400, detail=f"Unknown MCP proxy tool: {name}") - tool_arguments: Final = arguments.get("arguments", {}) # mutable-ok: JSON Schema validator consumes mapping + tool_arguments: Final = arguments.get("arguments", {}) if not isinstance(tool_arguments, dict): return _text_tool_result("arguments must be an object", is_error=True) try: diff --git a/litellm/proxy/_experimental/mcp_server/toolset_db.py b/litellm/proxy/_experimental/mcp_server/toolset_db.py index 48bad178927..1e8b730dff3 100644 --- a/litellm/proxy/_experimental/mcp_server/toolset_db.py +++ b/litellm/proxy/_experimental/mcp_server/toolset_db.py @@ -136,7 +136,7 @@ async def update_mcp_toolset( tool list, so a null ``toolset_name`` or ``tools`` is a no-op rather than a clear; emptying the tool selection is an explicit ``[]``, which cannot be mistaken for a caller that left the field out.""" - data_dict: Final = dict( # mutable-ok: Prisma requires a plain dict for JSON query serialization + data_dict: Final = dict( ( (field, json.dumps(value) if field == "tools" else value) for field, value in data.model_dump(exclude_unset=True).items() diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 49e0247aad9..55b93dccfc0 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3784,7 +3784,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase): pointfive: CallbackOnUI = CallbackOnUI( litellm_callback_name="pointfive", ui_callback_name="PointFive", - litellm_callback_params=[ # mutable-ok: the registry field is typed list + litellm_callback_params=[ "POINTFIVE_API_KEY", "POINTFIVE_API_URL", ], diff --git a/litellm/proxy/agent_endpoints/agent_registry.py b/litellm/proxy/agent_endpoints/agent_registry.py index c7b6bca72cf..4b84b463089 100644 --- a/litellm/proxy/agent_endpoints/agent_registry.py +++ b/litellm/proxy/agent_endpoints/agent_registry.py @@ -130,9 +130,7 @@ def _dump_agent_params(raw: Mapping[str, object]) -> dict[str, object]: _AGENT_PARAMS_MASKER: Final = SensitiveDataMasker() _REDACT_AGENT_PARAMS_MAX_DEPTH: Final = 10 -_AGENT_PARAMS_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter( - dict[str, object] -) # mutable-ok: safe_dumps() and AgentResponse.litellm_params both require a real dict, not a Mapping +_AGENT_PARAMS_ADAPTER: Final[TypeAdapter[dict[str, object]]] = TypeAdapter(dict[str, object]) _AGENT_PARAMS_SEQUENCE_ADAPTER: Final[TypeAdapter[tuple[object, ...]]] = TypeAdapter(tuple[object, ...]) _EMPTY_LITELLM_PARAMS: Final[Mapping[str, object]] = MappingProxyType({}) @@ -184,7 +182,7 @@ def _redact_agent_params_tree(value: object, _depth: int) -> object: else _redact_agent_params_tree(nested_value, _depth + 1) ) for key, nested_value in typed_params.items() - } # mutable-ok: consumed by json.dumps()/AgentResponse.litellm_params, both of which require a real dict + } def parse_agent_litellm_params(value: object) -> Mapping[str, object]: @@ -307,7 +305,7 @@ def _restore_redacted_litellm_params( key: value for key in all_keys if (value := _resolved_agent_param_value(key, incoming, existing, _depth)) is not _MISSING_AGENT_PARAM - } # mutable-ok: fed to safe_dumps() for JSON-column storage, which requires a real dict + } class GrantMigrationResult(NamedTuple): @@ -678,9 +676,7 @@ class AgentRegistry: # existing row is read up front to restore any sensitive key the # caller echoed back redacted (or omitted) rather than persisting # the marker -- or nothing -- over the real stored credential. - existing_row: Final = await agents_table(prisma_client).find_unique( - where={"agent_id": agent_id} # mutable-ok: prisma's query builder rejects a Mapping/MappingProxyType - ) + existing_row: Final = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) existing_litellm_params: Final = parse_agent_litellm_params( existing_row.litellm_params if existing_row is not None else None ) diff --git a/litellm/proxy/agent_endpoints/endpoints.py b/litellm/proxy/agent_endpoints/endpoints.py index aa8979a73c6..f9c515de15e 100644 --- a/litellm/proxy/agent_endpoints/endpoints.py +++ b/litellm/proxy/agent_endpoints/endpoints.py @@ -144,13 +144,11 @@ async def _attach_keys_to_agents(agents: Sequence[AgentResponse], prisma_client) def _redact_agent_litellm_params_dict( litellm_params: Mapping[str, object], -) -> dict[str, object]: # mutable-ok: AgentResponse.litellm_params is declared as a plain dict, not Mapping +) -> dict[str, object]: """Type-narrowing wrapper: a dict in always yields a dict back from ``redact_sensitive_agent_litellm_params``, which the function's general (possible-JSON-string, possibly-None) signature can't express.""" - return dict( # mutable-ok: AgentResponse.litellm_params is declared as a plain dict, not Mapping - parse_agent_litellm_params(redact_sensitive_agent_litellm_params(litellm_params)) - ) + return dict(parse_agent_litellm_params(redact_sensitive_agent_litellm_params(litellm_params))) def _redact_sensitive_agent_fields( diff --git a/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py b/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py index 7c6a4571948..78af282941d 100644 --- a/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py +++ b/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py @@ -584,7 +584,7 @@ async def update_plugin( _validate_plugin_source(request.source) existing: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.find_unique( - where={"name": plugin_name} # mutable-ok: prisma query arguments must be plain dicts + where={"name": plugin_name} ) if not existing: raise _error_response(404, f"Plugin '{plugin_name}' not found") @@ -592,8 +592,8 @@ async def update_plugin( manifest: Final[Mapping[str, object]] = _build_plugin_manifest(plugin_name, request) plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.update( - where={"name": plugin_name}, # mutable-ok: prisma query arguments must be plain dicts - data={ # mutable-ok: prisma query arguments must be plain dicts + where={"name": plugin_name}, + data={ "version": request.version, "description": request.description, "manifest_json": json.dumps(manifest), diff --git a/litellm/proxy/anthropic_endpoints/skills_endpoints.py b/litellm/proxy/anthropic_endpoints/skills_endpoints.py index 4426c0b547a..ab513b1acc4 100644 --- a/litellm/proxy/anthropic_endpoints/skills_endpoints.py +++ b/litellm/proxy/anthropic_endpoints/skills_endpoints.py @@ -66,7 +66,7 @@ async def _search_skills( to_response: Final = LiteLLMSkillsTransformationHandler().db_skill_to_response match outcome: case SkillSearchHits(hits): - skills: Final = [ # mutable-ok: ListSkillsResponse.data requires list[Skill]; never mutated after + skills: Final = [ to_response(hit.skill).model_copy(update=MappingProxyType({"search_score": hit.score})) for hit in hits ] return ListSkillsResponse(data=skills, has_more=False, next_page=None) diff --git a/litellm/proxy/anthropic_endpoints/streaming_model_restamp.py b/litellm/proxy/anthropic_endpoints/streaming_model_restamp.py index 7da5e5099fc..f748a754fe0 100644 --- a/litellm/proxy/anthropic_endpoints/streaming_model_restamp.py +++ b/litellm/proxy/anthropic_endpoints/streaming_model_restamp.py @@ -30,7 +30,7 @@ def _restamped_event(event: Mapping[str, object], requested_model: str) -> Mappi return None if message.get("model") == requested_model: return None - return {**event, "message": {**message, "model": requested_model}} # mutable-ok: SSE payload, re-serialized as is + return {**event, "message": {**message, "model": requested_model}} def _restamped_data_line(line: str, requested_model: str) -> str | None: diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 576585ee9a3..0a63821e2fd 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -1511,12 +1511,12 @@ _RESTRICTED_COLUMNS: Final = ("budget_id", "allowed_model_region", "default_mode def _column_is_set(column: str) -> Mapping[str, object]: """``column IS NOT NULL`` as a plain dict, which is the only shape prisma's builder accepts.""" - return {column: {"not": None}} # mutable-ok: prisma's query builder isinstance-checks for dict + return {column: {"not": None}} def _restricted_end_user_where() -> Mapping[str, object]: """Prisma filter selecting every end-user row that carries a restriction auth enforces.""" - return {"OR": [{"blocked": True}, *map(_column_is_set, _RESTRICTED_COLUMNS)]} # mutable-ok: prisma needs dict/list + return {"OR": [{"blocked": True}, *map(_column_is_set, _RESTRICTED_COLUMNS)]} class _RegistryNotCached: @@ -2364,7 +2364,7 @@ async def _backfill_null_user_email( db_row: Final = await user_repo.find_by_id(user_row.user_id) if db_row is None: return user_row - email_update: Final = {"user_email": db_row.user_email} # mutable-ok: model_copy update payload is dict-shaped + email_update: Final = {"user_email": db_row.user_email} updated_row: Final = user_row.model_copy(update=email_update) await user_api_key_cache.async_set_cache( key=user_row.user_id, @@ -2695,7 +2695,7 @@ async def invalidate_team_member_spend_state( ) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail={ # mutable-ok: HTTPException.detail takes a dict + detail={ "error": "Spend was reset in the database, but Redis is unreachable and still " "holds the pre-reset counter. Retry once Redis is reachable." }, @@ -4356,7 +4356,7 @@ async def stamp_matched_model_access_groups( return () if not matched: return () - matched_groups: Final = list(matched) # mutable-ok: the auth field is typed list[str] | None + matched_groups: Final = list(matched) valid_token.matched_model_access_groups = matched_groups # rebind-ok: request-scoped carrier for the writer return matched diff --git a/litellm/proxy/auth/auth_exception_handler.py b/litellm/proxy/auth/auth_exception_handler.py index b36c8a038fc..92c9952ab05 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -84,7 +84,7 @@ def _with_requester_ip_address(request_data: dict[str, object], requester_ip: st base: Final[Mapping[str, object]] = metadata if isinstance(metadata, Mapping) else EMPTY_MAPPING if base.get("requester_ip_address"): return request_data - return {**request_data, key: {**base, "requester_ip_address": requester_ip}} # mutable-ok: logging needs dicts + return {**request_data, key: {**base, "requester_ip_address": requester_ip}} class UserAPIKeyAuthExceptionHandler: diff --git a/litellm/proxy/auth/auth_object_prefetch.py b/litellm/proxy/auth/auth_object_prefetch.py index 52e26e885c9..0aa59bd81b6 100644 --- a/litellm/proxy/auth/auth_object_prefetch.py +++ b/litellm/proxy/auth/auth_object_prefetch.py @@ -237,7 +237,7 @@ def _validate_row( try: columns: Final = _RowValues.validate_python(row_value) if row in _REFRESH_STAMPED_ROWS: - stamped: Final = {**columns, "last_refreshed_at": refreshed_at} # mutable-ok: validators write into it + stamped: Final = {**columns, "last_refreshed_at": refreshed_at} return model_type.model_validate(stamped) return model_type.model_validate(columns) except ValidationError as e: diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index be65c3b39ec..daf7f927a6e 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -1256,7 +1256,7 @@ def enforce_batch_enqueued_token_limit_is_admin_only( return raise HTTPException( status_code=403, - detail={ # mutable-ok: HTTPException.detail has no immutable form + detail={ "error": f"Only proxy admins can set {BATCH_ENQUEUED_TOKEN_LIMIT_METADATA_KEY} on a {entity}. " "It replaces the standard rate limit checks for batch submissions." }, diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index 4304542fc83..5ad117a0c8d 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -197,7 +197,7 @@ class JWTHandler: self.http_handler = HTTPHandler() self.leeway = 0 # Per-cache-key locks so a TTL lapse triggers one refresh instead of one per in-flight request. - self._refresh_locks: dict[str, asyncio.Lock] = {} # mutable-ok: lock registry, keyed by JWKS url + self._refresh_locks: dict[str, asyncio.Lock] = {} def update_environment( self, diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 22826f48b52..424a26ee35c 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -1215,7 +1215,7 @@ async def _read_request_body_deferring_parse_failure( try: parsed_body: Final = await _read_request_body(request=request) except ProxyException as parse_exception: - return {}, parse_exception # mutable-ok: request_data is a plain dict across the whole auth path + return {}, parse_exception return populate_request_with_path_params(request_data=parsed_body, request=request), None @@ -1234,7 +1234,7 @@ async def _record_unparsable_body_failure( try: await proxy_logging_obj.post_call_failure_hook( # pyright: ignore[reportUnknownMemberType] # bare dict in sig - request_data={}, # mutable-ok: the failure hook seeds the call id and metadata onto this dict + request_data={}, original_exception=body_parse_exception, user_api_key_dict=user_api_key_dict, error_type=ProxyErrorTypes.bad_request_error, @@ -1511,7 +1511,7 @@ async def _user_api_key_auth_builder( do_standard_jwt_auth = False # Fall through to virtual key checks if valid_token.user_id is not None and valid_token.user_email is None: - mapped_claims = jwt_claims or {} # mutable-ok: empty-dict fallback for the None-claims case + mapped_claims = jwt_claims or {} mapped_user_email = jwt_handler.get_user_email(token=mapped_claims, default_value=None) mapped_jwt_user_id: Final = jwt_handler.get_user_id(token=mapped_claims, default_value=None) if mapped_user_email is not None and mapped_jwt_user_id == valid_token.user_id: diff --git a/litellm/proxy/batches_endpoints/endpoints.py b/litellm/proxy/batches_endpoints/endpoints.py index 5c4bacd757c..165b5bd3b82 100644 --- a/litellm/proxy/batches_endpoints/endpoints.py +++ b/litellm/proxy/batches_endpoints/endpoints.py @@ -533,7 +533,7 @@ async def retrieve_batch( if poller_owns_accounting: litellm_metadata = data.get("litellm_metadata") if not isinstance(litellm_metadata, dict): - litellm_metadata = {} # mutable-ok: the suppression flag must live inside litellm_metadata for the success handler to read it, and this request carried no mapping to extend + litellm_metadata = {} data["litellm_metadata"] = litellm_metadata litellm_metadata["batch_ignore_default_logging"] = True diff --git a/litellm/proxy/client/cli/commands/agents.py b/litellm/proxy/client/cli/commands/agents.py index 93ed0eaba03..9d93b5681a3 100644 --- a/litellm/proxy/client/cli/commands/agents.py +++ b/litellm/proxy/client/cli/commands/agents.py @@ -236,9 +236,7 @@ def prepare_codex( _Preparer: TypeAlias = Callable[[str, str, Mapping[str, str]], Sequence[str]] -_PREPARERS: Final[Mapping[str, _Preparer]] = MappingProxyType( - {"pi": prepare_pi, "codex": prepare_codex} # mutable-ok: MappingProxyType freezes the provider registry -) +_PREPARERS: Final[Mapping[str, _Preparer]] = MappingProxyType({"pi": prepare_pi, "codex": prepare_codex}) def agent_launch_args(command: str, base_url: str) -> list[str]: diff --git a/litellm/proxy/client/cli/commands/claude_settings.py b/litellm/proxy/client/cli/commands/claude_settings.py index 1473e40070f..85d2ef00ed5 100644 --- a/litellm/proxy/client/cli/commands/claude_settings.py +++ b/litellm/proxy/client/cli/commands/claude_settings.py @@ -322,8 +322,8 @@ def with_status_line(settings: Mapping[str, JsonValue], command: str) -> Mapping ours: Final = existing is None or (isinstance(existing_command, str) and command.split()[-1] in existing_command) if not ours: return settings - entry: Final = dict((("type", "command"), ("command", command))) # mutable-ok: JSON document - return dict(chain(settings.items(), ((STATUS_LINE_KEY, entry),))) # mutable-ok: JSON document + entry: Final = dict((("type", "command"), ("command", command))) + return dict(chain(settings.items(), ((STATUS_LINE_KEY, entry),))) def merge_claude_settings( @@ -346,7 +346,7 @@ def merge_claude_settings( """ raw_env: Final = settings.get(ENV_KEY, {}) current_env: Final = raw_env if isinstance(raw_env, dict) else {} - env: Final = dict( # mutable-ok: JSON document handed to json.dump, which rejects a read-only mapping + env: Final = dict( chain( ( (ENABLE_TOOL_SEARCH_KEY, ENABLE_TOOL_SEARCH_VALUE), @@ -358,7 +358,7 @@ def merge_claude_settings( ((key, tier_model) for key in ANTHROPIC_DEFAULT_MODEL_ENV_KEYS if tier_model is not None), ) ) - return dict( # mutable-ok: JSON document handed to json.dump, which rejects a read-only mapping + return dict( chain( ( (key, value) @@ -390,7 +390,7 @@ def _lookup(settings: Mapping[str, JsonValue], path: str) -> OwnedValue: def _with_key(container: Mapping[str, JsonValue], key: str, owned: OwnedValue) -> Mapping[str, JsonValue]: - return dict( # mutable-ok: JSON document handed to json.dump, which rejects a read-only mapping + return dict( chain(((k, v) for k, v in container.items() if k != key), ((key, owned.value),) if owned.present else ()) ) @@ -573,7 +573,7 @@ def unconfigure_claude_settings( ) target: Final = _write_target(settings_path) file_removed: Final = not settings and not (receipt.file_existed and target.exists()) - kept_receipt: Final = ( # mutable-ok: pydantic serializes the update as given and rejects a mappingproxy + kept_receipt: Final = ( receipt.model_copy(update={"written": {item.key: _fingerprint(absent) for item in withheld}}) if withheld else None diff --git a/litellm/proxy/client/cli/commands/codex_settings.py b/litellm/proxy/client/cli/commands/codex_settings.py index 686eaa47ff0..cf3a21a5302 100644 --- a/litellm/proxy/client/cli/commands/codex_settings.py +++ b/litellm/proxy/client/cli/commands/codex_settings.py @@ -106,7 +106,7 @@ def _with(document: TOMLDocument, path: str, snapshot: str | None) -> TOMLDocume if section and section not in document and snapshot is not None: contents: Final = tomlkit.parse(tomlkit.dumps(MappingProxyType({key: tomlkit.parse(snapshot).item("value")}))) return tomlkit.parse(document.as_string() + "\n" + tomlkit.dumps(MappingProxyType({section: contents}))) - # mutable-ok: TOMLKit editing requires private node mutation to preserve comments and order + updated: Final = tomlkit.parse(document.as_string()) parent: Final = _table(_mapping(updated).get(section)) if section else updated if parent is None: diff --git a/litellm/proxy/client/cli/commands/configure.py b/litellm/proxy/client/cli/commands/configure.py index 7988f8aef3c..e44aa387e59 100644 --- a/litellm/proxy/client/cli/commands/configure.py +++ b/litellm/proxy/client/cli/commands/configure.py @@ -210,7 +210,7 @@ def _pick_model(listed: Sequence[str]) -> str | None: def _pick_codex_model(listed: Sequence[str]) -> str: - choices: Final = list(listed) # mutable-ok: InquirerPy's choices parameter requires a list + choices: Final = list(listed) return str(inquirer.fuzzy(message="Model Codex starts on (type to filter):", choices=choices).execute()) diff --git a/litellm/proxy/client/cli/commands/pi.py b/litellm/proxy/client/cli/commands/pi.py index 9810e81ae36..0fee5caf528 100644 --- a/litellm/proxy/client/cli/commands/pi.py +++ b/litellm/proxy/client/cli/commands/pi.py @@ -94,7 +94,7 @@ def fetch_model_listing( try: resp: Final = get( url, - headers={"Authorization": f"Bearer {api_key}", **headers}, # mutable-ok: requests headers require a dict + headers={"Authorization": f"Bearer {api_key}", **headers}, timeout=10, ) except requests.RequestException as e: @@ -141,7 +141,7 @@ def fetch_model_limits( try: resp: Final = get( url, - headers={"Authorization": f"Bearer {api_key}"}, # mutable-ok: requests headers require a dict + headers={"Authorization": f"Bearer {api_key}"}, timeout=10, ) if resp.status_code != 200: @@ -166,34 +166,30 @@ def models_json_path(env: Mapping[str, str]) -> Path: return root / "models.json" -def _model_entry( - model_id: str, limits: Mapping[str, ModelLimits] -) -> dict[str, JsonValue]: # mutable-ok: JSON object is serialized +def _model_entry(model_id: str, limits: Mapping[str, ModelLimits]) -> dict[str, JsonValue]: limit: Final = limits.get(model_id) - context: Final[dict[str, JsonValue]] = ( # mutable-ok: JSON field - {"contextWindow": limit.context_window} if limit and limit.context_window else {} # mutable-ok: JSON field + context: Final[dict[str, JsonValue]] = ( + {"contextWindow": limit.context_window} if limit and limit.context_window else {} ) - output: Final[dict[str, JsonValue]] = ( # mutable-ok: JSON field - {"maxTokens": limit.max_tokens} if limit and limit.max_tokens else {} - ) # mutable-ok: JSON field - return {"id": model_id, **context, **output} # mutable-ok: JSON serialization requires a mutable object + output: Final[dict[str, JsonValue]] = {"maxTokens": limit.max_tokens} if limit and limit.max_tokens else {} + return {"id": model_id, **context, **output} def provider_block( base_url: str, model_ids: tuple[str, ...], limits: Mapping[str, ModelLimits] = _NO_LIMITS, -) -> dict[str, JsonValue]: # mutable-ok: JSON object is serialized +) -> dict[str, JsonValue]: """openai-completions is the one API shape every LiteLLM model serves. Real contextWindow/maxTokens matter: pi otherwise assumes 128k/16384, which breaks compaction thresholds and over-asks models with smaller output caps. """ - return { # mutable-ok: JSON serialization requires a mutable object + return { "baseUrl": base_url.rstrip("/") + "/v1", "api": "openai-completions", "apiKey": f"${LITELLM_PROXY_API_KEY_ENV}", - "models": [_model_entry(model_id, limits) for model_id in model_ids], # mutable-ok: JSON array + "models": [_model_entry(model_id, limits) for model_id in model_ids], } @@ -208,17 +204,15 @@ def sync_models_json( ) -> PiSyncError | None: """Replace only the litellm provider entry, leaving the rest of the file intact.""" try: - current: Final = ( # mutable-ok: JSON object default - _MODELS_FILE_ADAPTER.validate_json(path.read_text()) if path.exists() else {} - ) + current: Final = _MODELS_FILE_ADAPTER.validate_json(path.read_text()) if path.exists() else {} except (OSError, ValidationError) as e: return PiSyncError(f"Could not read {path} as a JSON object: {e}. Fix or move the file, then retry.") - existing_providers: Final = current.get("providers", {}) # mutable-ok: JSON object default + existing_providers: Final = current.get("providers", {}) if not isinstance(existing_providers, dict): return PiSyncError(f'"providers" in {path} is not an object; fix or move the file, then retry.') - updated: Final = { # mutable-ok: JSON serialization requires a mutable object + updated: Final = { **current, - "providers": { # mutable-ok: JSON serialization requires a mutable object + "providers": { **existing_providers, PI_PROVIDER_NAME: provider_block(base_url, model_ids, limits), }, diff --git a/litellm/proxy/client/cli/commands/statusline_script.py b/litellm/proxy/client/cli/commands/statusline_script.py index a8abeb68978..6523bedfa42 100644 --- a/litellm/proxy/client/cli/commands/statusline_script.py +++ b/litellm/proxy/client/cli/commands/statusline_script.py @@ -188,7 +188,7 @@ def fetch_session(credentials: Credentials, session_id: str) -> Fetched: query: Final = urlencode((("session_id", session_id),)) request: Final = urllib.request.Request( f"{credentials.base_url}{SESSION_ENDPOINT}?{query}", - headers={ # mutable-ok: urllib.request.Request takes a dict + headers={ "Authorization": f"Bearer {credentials.api_key}", "Accept": "application/json", }, @@ -282,7 +282,7 @@ def _read_cache(path: Path) -> Mapping[str, object]: def _write_cache(path: Path, session: Session | None, fetched_at: float) -> None: """Staged beside the entry and renamed into place, so a refresh reading the entry never sees a torn write.""" entry: Final = session._asdict() if session else None - body: Final = json.dumps({"fetched_at": fetched_at, "session": entry}) # mutable-ok: json.dumps takes a dict + body: Final = json.dumps({"fetched_at": fetched_at, "session": entry}) if not _own_private_dir(path.parent): return try: @@ -364,7 +364,7 @@ def codex_stop_message( if session is None: return "" text: Final = render(model_label(session.last_model, config_dir), session, config_dir, use_color=False) - return json.dumps({"systemMessage": f"\n{text}"}) # mutable-ok: json.dumps takes a dict + return json.dumps({"systemMessage": f"\n{text}"}) def run(stdin: IO[str], stdout: IO[str], env: Mapping[str, str], fetch: Fetch = fetch_session) -> None: diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index fc603701e44..0f87ac8e648 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -1514,7 +1514,7 @@ def _timing_values( """ if hidden_params.get("_response_ms") is not None or not use_logging_obj or logging_obj is None: return hidden_params - return getattr(logging_obj, "response_timing_metrics", None) or {} # mutable-ok: empty fallback + return getattr(logging_obj, "response_timing_metrics", None) or {} class ProxyBaseLLMRequestProcessing: @@ -1531,7 +1531,7 @@ class ProxyBaseLLMRequestProcessing: Proxy/custom headers win on key collisions. """ - excluded_headers: Final = { # mutable-ok: set of header names to exclude from forwarding + excluded_headers: Final = { "transfer-encoding", "content-encoding", "set-cookie", @@ -1544,7 +1544,7 @@ class ProxyBaseLLMRequestProcessing: "upgrade", } - merged_headers: Final = { # mutable-ok: dict comprehension for merged headers forwarded to httpx + merged_headers: Final = { key: value for key, value in dict(response_headers or {}).items() if key.lower() not in excluded_headers } merged_headers.update(custom_headers) @@ -3496,9 +3496,7 @@ class ProxyBaseLLMRequestProcessing: error_body: Final = await http_status_error.response.aread() error_text: Final = error_body.decode("utf-8") - error_headers: Final = { # mutable-ok: HTTPException takes a plain header dict - k: v if isinstance(v, str) else str(v) for k, v in safe_headers.items() - } + error_headers: Final = {k: v if isinstance(v, str) else str(v) for k, v in safe_headers.items()} raise HTTPException( status_code=http_status_error.response.status_code, detail={"error": error_text}, diff --git a/litellm/proxy/common_utils/config_includes.py b/litellm/proxy/common_utils/config_includes.py index c1bb5ae952f..a3402207e52 100644 --- a/litellm/proxy/common_utils/config_includes.py +++ b/litellm/proxy/common_utils/config_includes.py @@ -52,7 +52,7 @@ class ConfigReader(Protocol): def _merged_value(base_value: object, included_value: object) -> object: if isinstance(included_value, list) and isinstance(base_value, list): - return [*base_value, *included_value] # mutable-ok: a merged config value stays the plain list the proxy loads + return [*base_value, *included_value] return included_value @@ -129,4 +129,4 @@ async def resolve_includes( applies to configs on disk and to configs hosted in a bucket. """ merged: Final = await _resolve(config, _pending_from(config, location), frozenset((location,)), resolve, read) - return dict(merged) # mutable-ok: the proxy mutates the config it loads + return dict(merged) diff --git a/litellm/proxy/common_utils/reset_budget_job.py b/litellm/proxy/common_utils/reset_budget_job.py index 35e74418628..5b18f1bb651 100644 --- a/litellm/proxy/common_utils/reset_budget_job.py +++ b/litellm/proxy/common_utils/reset_budget_job.py @@ -212,12 +212,8 @@ def _queue_budget_linked_resets( one transaction, so the reverse order lets the zero re-match a row the decrement just moved into the (0, cap] range and erase its carried spend.""" for budget_id, cap in cascade.rollover_caps.items(): - writes.queue_spend_zero( - where={"budget_id": budget_id, **extra, "spend": {"gt": 0, "lte": cap}} - ) # mutable-ok: prisma where filter must be a dict - writes.queue_spend_decrement( - where={"budget_id": budget_id, **extra, "spend": {"gt": cap}}, amount=cap - ) # mutable-ok: prisma where filter must be a dict + writes.queue_spend_zero(where={"budget_id": budget_id, **extra, "spend": {"gt": 0, "lte": cap}}) + writes.queue_spend_decrement(where={"budget_id": budget_id, **extra, "spend": {"gt": cap}}, amount=cap) plain_ids: Final = tuple(bid for bid in cascade.budget_ids if bid not in cascade.rollover_caps) if plain_ids: writes.queue_spend_zero(where=_budget_link_where(plain_ids, extra)) @@ -239,16 +235,10 @@ def _queue_enduser_resets(writes: LinkedSpendResetWrites, cascade: "_BudgetCasca return cap: Final = cascade.rollover_caps.get(default_budget_id) if cap is None: - writes.queue_spend_zero( - where={"budget_id": None, **_SPENT_ROWS_WHERE} - ) # mutable-ok: prisma where filter must be a dict + writes.queue_spend_zero(where={"budget_id": None, **_SPENT_ROWS_WHERE}) return - writes.queue_spend_zero( - where={"budget_id": None, "spend": {"gt": 0, "lte": cap}} - ) # mutable-ok: prisma where filter must be a dict - writes.queue_spend_decrement( - where={"budget_id": None, "spend": {"gt": cap}}, amount=cap - ) # mutable-ok: prisma where filter must be a dict + writes.queue_spend_zero(where={"budget_id": None, "spend": {"gt": 0, "lte": cap}}) + writes.queue_spend_decrement(where={"budget_id": None, "spend": {"gt": cap}}, amount=cap) @dataclass(frozen=True, slots=True) @@ -656,13 +646,13 @@ class ResetBudgetJob: log_subject="model access groups", ) rollover_caps: Final[Mapping[str, float]] = MappingProxyType( - { # mutable-ok: MappingProxyType wraps a one-shot dict comprehension + { b.budget_id: cap for b in budgets_to_reset if b.budget_id is not None and (cap := _rollover_cap(b.max_budget)) is not None } if _rollover_enabled() - else {} # mutable-ok: empty sentinel immediately frozen by MappingProxyType + else {} ) endusers: Final[tuple[_EndUserRow, ...]] = await self._collect_endusers_to_reset(budget_ids) return _BudgetCascade( diff --git a/litellm/proxy/common_utils/semantic_text_index.py b/litellm/proxy/common_utils/semantic_text_index.py index b8d3595163e..156a9cc9881 100644 --- a/litellm/proxy/common_utils/semantic_text_index.py +++ b/litellm/proxy/common_utils/semantic_text_index.py @@ -54,7 +54,7 @@ class _EmbeddingRequest(BaseModel): model: str input: tuple[str, ...] - metadata: dict[str, object] # mutable-ok: the router mutates the metadata dict it is handed + metadata: dict[str, object] def cosine_similarity(left: Vector, right: Vector) -> float: @@ -63,10 +63,10 @@ def cosine_similarity(left: Vector, right: Vector) -> float: return dot / norms if norms else 0.0 -def embedding_spend_metadata(user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]: # mutable-ok: router mutates it +def embedding_spend_metadata(user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]: from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup - return { # mutable-ok: the router mutates the metadata dict it is handed + return { **LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(user_api_key_dict), "user_api_key": user_api_key_dict.api_key, } @@ -78,9 +78,9 @@ def router_embedder( """Embeds through the router after the same key rate-limit, budget and guardrail pre-call hooks /embeddings runs.""" async def embed(texts: Sequence[str]) -> Sequence[Vector]: - request: Final = { # mutable-ok: pre_call_hook mutates the request dict in place + request: Final = { "model": embedding_model, - "input": list(texts), # mutable-ok: Router.aembedding accepts only str | list input + "input": list(texts), "metadata": embedding_spend_metadata(user_api_key_dict), } processed: Final = _EmbeddingRequest.model_validate( @@ -90,7 +90,7 @@ def router_embedder( ) response: Final = await router.aembedding( model=processed.model, - input=list(processed.input), # mutable-ok: Router.aembedding accepts only str | list input + input=list(processed.input), metadata=processed.metadata, ) return tuple(item.embedding for item in _EmbeddingData.model_validate(response.model_dump()).data) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index eaa03c5d7f7..2d69772e94c 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -376,14 +376,12 @@ class DBSpendUpdateWriter: spend_logs: Final = SpendLogsRepository(prisma_client).table try: claimed: Final = await spend_logs.create_many( - data=[prisma_client.jsonify_object(row)], # mutable-ok: prisma create_many takes a list + data=[prisma_client.jsonify_object(row)], skip_duplicates=True, ) if claimed == 1: return True - existing: Final = await spend_logs.find_unique( - where={"request_id": request_id} # mutable-ok: prisma where clause - ) + existing: Final = await spend_logs.find_unique(where={"request_id": request_id}) except Exception as e: # noqa: BLE001 # prisma raises its own hierarchy; an unreachable DB queues the row like any other spend log verbose_proxy_logger.warning( "Could not claim spend row %s for a batch's cost, queueing it: %s", request_id, e @@ -425,7 +423,7 @@ class DBSpendUpdateWriter: data=prisma_client.jsonify_object( MappingProxyType({field: value for field, value in row.items() if field != "request_id"}) ), - where={ # mutable-ok: prisma where clause + where={ "request_id": request_id, "call_type": CallTypes.aretrieve_batch.value, "status": "success", @@ -1137,7 +1135,7 @@ class DBSpendUpdateWriter: ): verbose_proxy_logger.debug("acquired lock for spend updates") - uncommitted: dict[str, Any] = {} # mutable-ok: tracks popped categories still needing commit + uncommitted: dict[str, Any] = {} try: ( @@ -1150,7 +1148,7 @@ class DBSpendUpdateWriter: window_spend_update_transactions, ) = await self.redis_update_buffer.get_all_transactions_from_redis_buffer_pipeline() - uncommitted = { # mutable-ok: drives which popped categories still need re-queuing + uncommitted = { "db_spend_update_transactions": db_spend_update_transactions, "daily_spend_update_transactions": daily_spend_update_transactions, "daily_team_spend_update_transactions": daily_team_spend_update_transactions, @@ -1241,9 +1239,7 @@ class DBSpendUpdateWriter: exc=e, ) finally: - to_restore = { # mutable-ok: transient kwargs payload consumed immediately below - name: txns for name, txns in uncommitted.items() if txns is not None - } + to_restore = {name: txns for name, txns in uncommitted.items() if txns is not None} if to_restore: await self.redis_update_buffer.restore_transactions_to_redis(**to_restore) await self.pod_lock_manager.release_lock( diff --git a/litellm/proxy/db/db_url_settings.py b/litellm/proxy/db/db_url_settings.py index 54021e68980..e6e97cb1eb3 100644 --- a/litellm/proxy/db/db_url_settings.py +++ b/litellm/proxy/db/db_url_settings.py @@ -142,7 +142,7 @@ PEM_CERT_HEADER: Final = b"-----BEGIN CERTIFICATE-----" PG_SSL_REQUEST: Final = struct.pack("!ii", 8, 80877103) TLS_PROBE_TIMEOUT_SECONDS: Final = 10.0 -RootCertResolver: TypeAlias = Callable[[str, str, int], str] # mutable-ok: Callable parameter syntax +RootCertResolver: TypeAlias = Callable[[str, str, int], str] class _VerifiedChainSource(Protocol): diff --git a/litellm/proxy/db/gateway_request_tracking.py b/litellm/proxy/db/gateway_request_tracking.py index c9ace68db33..71589e83870 100644 --- a/litellm/proxy/db/gateway_request_tracking.py +++ b/litellm/proxy/db/gateway_request_tracking.py @@ -62,7 +62,7 @@ class GatewayRequestAccumulator: """Sink for the request-metrics middleware. ``record`` is sync and never awaits.""" def __init__(self) -> None: - self._counts: dict[GatewayRequestKey, GatewayRequestCounts] = {} # mutable-ok: bounded fold, drained per flush + self._counts: dict[GatewayRequestKey, GatewayRequestCounts] = {} def record(self, *, category: BillableCategory, route: str, status_code: int) -> None: key: Final = GatewayRequestKey(date=_utc_date(), category=category.value, route=route) @@ -70,7 +70,7 @@ class GatewayRequestAccumulator: def drain(self) -> GatewayRequestSnapshot: drained: Final = self._counts - self._counts = {} # mutable-ok: the fold restarts empty; the drained map is handed off whole + self._counts = {} return drained def restore(self, snapshot: GatewayRequestSnapshot) -> None: @@ -91,12 +91,12 @@ class GatewayRequestAccumulator: overcount on a dropped acknowledgement beats losing a whole interval to every database blip, so the trade is deliberate. """ - self._counts = dict(fold_counts(chain(self._counts.items(), snapshot.items()))) # mutable-ok: fold replaced + self._counts = dict(fold_counts(chain(self._counts.items(), snapshot.items()))) def fold_counts(items: Iterable[tuple[GatewayRequestKey, GatewayRequestCounts]]) -> GatewayRequestSnapshot: """Sum counts key-wise; the result stays bounded by (date x category x route).""" - folded: Final[dict[GatewayRequestKey, GatewayRequestCounts]] = {} # mutable-ok: local fold returned once + folded: Final[dict[GatewayRequestKey, GatewayRequestCounts]] = {} for key, counts in items: existing = folded.get(key, _EMPTY) folded[key] = GatewayRequestCounts( diff --git a/litellm/proxy/db/shadow_eval_funnel.py b/litellm/proxy/db/shadow_eval_funnel.py index 9181d3f5035..1487f4ee972 100644 --- a/litellm/proxy/db/shadow_eval_funnel.py +++ b/litellm/proxy/db/shadow_eval_funnel.py @@ -19,7 +19,7 @@ ShadowEvalFunnelStage = Literal["not_sampled", "unjudgeable", "shed", "withheld" FUNNEL_STAGES: Final[tuple[ShadowEvalFunnelStage, ...]] = ("not_sampled", "unjudgeable", "shed", "withheld") -_pending: dict[str, dict[ShadowEvalFunnelStage, int]] = {} # mutable-ok: module-level queue, single event loop +_pending: dict[str, dict[ShadowEvalFunnelStage, int]] = {} _FUNNEL_PLACEHOLDERS: Final = ", ".join(f"${n + 2}" for n in range(len(FUNNEL_STAGES))) @@ -40,14 +40,14 @@ def pending_shadow_eval_funnel_events() -> int: def record_shadow_eval_funnel_event(job_id: str, stage: ShadowEvalFunnelStage) -> None: """Count one skipped request for one job leg; synchronous so the hook's read-modify- write cannot interleave with the flush's snapshot on the shared event loop.""" - counters: Final = _pending.setdefault(job_id, dict.fromkeys(FUNNEL_STAGES, 0)) # mutable-ok: queue entry + counters: Final = _pending.setdefault(job_id, dict.fromkeys(FUNNEL_STAGES, 0)) counters[stage] += 1 async def flush_shadow_eval_funnel(prisma_client: "PrismaClient") -> None: if not _pending: return - batch: Final = dict(_pending) # mutable-ok: snapshot drained from the queue + batch: Final = dict(_pending) _pending.clear() for job_id, counters in batch.items(): try: diff --git a/litellm/proxy/discovery_endpoints/agent_skills_endpoints.py b/litellm/proxy/discovery_endpoints/agent_skills_endpoints.py index 3084cbfd84f..2050cc40e6d 100644 --- a/litellm/proxy/discovery_endpoints/agent_skills_endpoints.py +++ b/litellm/proxy/discovery_endpoints/agent_skills_endpoints.py @@ -42,7 +42,7 @@ _ARCHIVE_CACHE: Final = InMemoryCache( _NON_SLUG_PATTERN: Final = re.compile(r"[^a-z0-9]+") _FALLBACK_SKILL_NAME: Final = "skill" -router: Final = APIRouter(tags=["public", "skills"]) # mutable-ok: fastapi types tags as list[str | Enum] +router: Final = APIRouter(tags=["public", "skills"]) class ZipArchiveResponse(Response): diff --git a/litellm/proxy/example_config_yaml/team_metadata_validator_e2e.py b/litellm/proxy/example_config_yaml/team_metadata_validator_e2e.py index 965eaf4ff16..af4a59efb06 100644 --- a/litellm/proxy/example_config_yaml/team_metadata_validator_e2e.py +++ b/litellm/proxy/example_config_yaml/team_metadata_validator_e2e.py @@ -48,7 +48,7 @@ async def _validate_via_http(payload: TeamMetadataValidationPayload, service_url client: Final = get_async_httpx_client(llm_provider=httpxSpecialProvider.GuardrailCallback) response: Final = await client.post( service_url, - json={ # mutable-ok: httpx serializes the request body from a plain dict + json={ "operation": payload.operation, "metadata": payload.metadata, }, diff --git a/litellm/proxy/guardrails/_content_utils.py b/litellm/proxy/guardrails/_content_utils.py index 7529fe99f52..2a2ef5217b8 100644 --- a/litellm/proxy/guardrails/_content_utils.py +++ b/litellm/proxy/guardrails/_content_utils.py @@ -107,14 +107,14 @@ def _coerce_input_to_messages(input_value: object) -> list[dict[str, object]]: elif item.get("type") == "reasoning": if "content" in item: messages.append( - { # mutable-ok: append reasoning content + { "role": item.get("role") or "assistant", "content": item["content"], } ) if isinstance(item.get("summary"), list): messages.append( - { # mutable-ok: append reasoning summary + { "role": item.get("role") or "assistant", "content": item["summary"], } @@ -197,7 +197,7 @@ def walk_user_text(data: dict[str, Any], visit: Callable[[str], str]) -> int: elif isinstance(item, dict): if _part_text(item) is not None: visited += 1 - input_value[idx] = {**item, "text": visit(item["text"])} # mutable-ok: rewrite text part in place + input_value[idx] = {**item, "text": visit(item["text"])} elif item.get("type") == "reasoning": if "content" in item: item["content"] = _rewrite_content(item["content"]) diff --git a/litellm/proxy/guardrails/auto_router_compression.py b/litellm/proxy/guardrails/auto_router_compression.py index c37b9fff1f0..6623ceb9c74 100644 --- a/litellm/proxy/guardrails/auto_router_compression.py +++ b/litellm/proxy/guardrails/auto_router_compression.py @@ -139,7 +139,7 @@ def _active_compression_guardrails() -> tuple["CustomGuardrail", ...]: async def arm_pre_call( - data: dict[str, object], # mutable-ok: arms the live request dict in place + data: dict[str, object], llm_router: "Router | None", ) -> None: """Apply an auto router's compression policy, if any, before guardrails run. @@ -194,14 +194,14 @@ async def arm_pre_call( existing: Final = tuple(requested) if isinstance(requested, (list, tuple)) else () if policy.model not in existing: # A list: litellm_pre_call_utils isinstance-checks this key and drops a tuple. - metadata["guardrails"] = [*existing, policy.model] # mutable-ok: this key's contract is a list + metadata["guardrails"] = [*existing, policy.model] def _as_routing_messages( messages: Iterable[Mapping[str, object]], ) -> list[dict[str, object]]: # mutable-ok: shape fixed by the pre-routing hook protocol """A fresh, independently mutable copy, the shape the pre-routing hook takes.""" - return [dict(message) for message in messages] # mutable-ok: shape fixed by the pre-routing hook protocol + return [dict(message) for message in messages] async def messages_for_routing( @@ -248,7 +248,7 @@ async def messages_for_routing( model: Final = request_kwargs.get("model") # Throwaway: apply_guardrail writes stats here, so routing never double-counts into # extract_compression_saved_tokens. - stats_sink: Final = {"messages": messages, "model": model} # mutable-ok: apply_guardrail writes its stats here + stats_sink: Final = {"messages": messages, "model": model} result: Final = await guardrail.apply_guardrail( inputs=inputs, request_data=stats_sink, diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index afb9997f2e6..8ff8ac2e644 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -1256,7 +1256,7 @@ async def patch_guardrail( litellm_params=LitellmParams(**existing_litellm_params), guardrail_info=existing_guardrail.get( "guardrail_info", - {}, # mutable-ok: Guardrail's own constructor takes a plain dict + {}, ), ), prisma_client=prisma_client, diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/alice/__init__.py index 75ea16f7a88..99bed196895 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/alice/__init__.py @@ -24,11 +24,11 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail" return _alice_guardrail_callback -guardrail_initializer_registry: Final = { # mutable-ok: module-level registry, built once and never mutated +guardrail_initializer_registry: Final = { SupportedGuardrailIntegrations.ALICE.value: initialize_guardrail, } -guardrail_class_registry: Final = { # mutable-ok: module-level registry, built once and never mutated +guardrail_class_registry: Final = { SupportedGuardrailIntegrations.ALICE.value: AliceGuardrail, } diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice/alice.py b/litellm/proxy/guardrails/guardrail_hooks/alice/alice.py index 9cabac2d0fa..e39702dfc6f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice/alice.py +++ b/litellm/proxy/guardrails/guardrail_hooks/alice/alice.py @@ -161,7 +161,7 @@ class AliceGuardrail(CustomGuardrail): self.unreachable_fallback: Literal["fail_closed", "fail_open"] = unreachable_fallback if "supported_event_hooks" not in kwargs: - kwargs["supported_event_hooks"] = [ # mutable-ok: CustomGuardrail.__init__ requires a list here + kwargs["supported_event_hooks"] = [ GuardrailEventHooks.pre_call, GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, @@ -173,7 +173,7 @@ class AliceGuardrail(CustomGuardrail): async def apply_guardrail( self, inputs: GenericGuardrailAPIInputs, - request_data: dict[str, object], # mutable-ok: overrides CustomGuardrail.apply_guardrail's plain-dict contract + request_data: dict[str, object], input_type: Literal["request", "response"], logging_obj: Optional["LiteLLMLoggingObj"] = None, ) -> GenericGuardrailAPIInputs: @@ -213,12 +213,12 @@ class AliceGuardrail(CustomGuardrail): ) -> AliceVerdict: response: Final = await self.async_handler.post( url=self.api_base, - json={ # mutable-ok: one-shot HTTP request body, never mutated after construction + json={ "input_type": input_type, "inputs": _json_safe(inputs), "request_data": _json_safe(request_data, strip_keys=_CREDENTIAL_KEYS_TO_STRIP), }, - headers={ # mutable-ok: one-shot HTTP headers, never mutated after construction + headers={ "Content-Type": "application/json", "af-api-key": self.alice_api_key, }, @@ -270,8 +270,8 @@ class AliceGuardrail(CustomGuardrail): rather than being silently skipped, so content Alice meant to replace can never reach the model unmasked alongside content that was replaced. """ - texts: Final = inputs.get("texts") or [] # mutable-ok: empty-list fallback, replaced wholesale below - replacements: Final = verdict.get("replacements") or [] # mutable-ok: empty-list fallback for iteration only + texts: Final = inputs.get("texts") or [] + replacements: Final = verdict.get("replacements") or [] if not replacements: raise self._mask_rejected(verdict) @@ -281,7 +281,7 @@ class AliceGuardrail(CustomGuardrail): text = replacement.get("text") if not (isinstance(index, int) and isinstance(text, str) and 0 <= index < len(texts)): raise self._mask_rejected(verdict) - texts[index] = text # mutable-ok: item assignment into the local working copy above + texts[index] = text inputs["texts"] = texts @@ -352,9 +352,7 @@ def _json_safe( } if isinstance(value, (list, tuple, set, frozenset)): - return [ # mutable-ok: return value is a one-shot list, discarded by the caller after use - _json_safe(item, depth + 1, nested, strip_keys) for item in islice(value, _MAX_ITEMS) - ] + return [_json_safe(item, depth + 1, nested, strip_keys) for item in islice(value, _MAX_ITEMS)] dump: Final = getattr(value, "model_dump", None) if callable(dump): diff --git a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py index a0724b75ec7..f3527747082 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py +++ b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py @@ -43,7 +43,7 @@ if TYPE_CHECKING: # Per-invocation billing counters. A ContextVar rather than request metadata: the # decorator can swap out ``request_data``, metadata is client-forgeable, and # concurrent guardrails run in separate tasks with their own context copy. -_billing_usage_stash: Final[ContextVar[dict[str, int] | None]] = ContextVar( # mutable-ok: task-local stash +_billing_usage_stash: Final[ContextVar[dict[str, int] | None]] = ContextVar( "azure_prompt_shield_billing_usage", default=None ) @@ -61,7 +61,7 @@ def _resolved_secret_value(value: object) -> object: return value -def _updated_param(litellm_params: "LitellmParams | dict", key: str) -> object: # mutable-ok: DB dict +def _updated_param(litellm_params: "LitellmParams | dict", key: str) -> object: """Read one param from a Mapping or a pydantic object, including pydantic extras (cost_tier / price_per_1000_text_records live there), which the base class ``vars()`` loop never sees.""" @@ -157,7 +157,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai async def async_make_request( self, user_prompt: str, - usage_accumulator: MutableMapping[str, int], # mutable-ok: callee-filled accumulator + usage_accumulator: MutableMapping[str, int], ) -> "AzurePromptShieldGuardrailResponse": """ Make a request to the Azure Prompt Shield API. @@ -223,7 +223,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai logging_obj: "LiteLLMLoggingObj | None" = None, ) -> GenericGuardrailAPIInputs: _billing_usage_stash.set(None) - usage: Final[dict[str, int]] = {} # mutable-ok: per-invocation billing accumulator + usage: Final[dict[str, int]] = {} try: for text in inputs.get("texts") or (): if text: @@ -258,7 +258,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai if user_prompt: verbose_proxy_logger.debug("Azure Prompt Shield: User prompt: %s", user_prompt) - usage: Final[dict[str, int]] = {} # mutable-ok: per-invocation billing accumulator + usage: Final[dict[str, int]] = {} try: await self.async_make_request( user_prompt=user_prompt, @@ -270,7 +270,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai verbose_proxy_logger.warning("Azure Prompt Shield: No user prompt found") return None - def update_in_memory_litellm_params(self, litellm_params: "LitellmParams | dict") -> None: # mutable-ok: DB dict + def update_in_memory_litellm_params(self, litellm_params: "LitellmParams | dict") -> None: """Apply updated params in place, re-resolving billing and credentials. Pricing is read via ``_updated_param`` (the values are pydantic extras, and @@ -281,7 +281,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai """ cost_tier: Final = _resolved_cost_tier(_updated_param(litellm_params, "cost_tier")) price: Final = _resolved_price(_updated_param(litellm_params, "price_per_1000_text_records"), cost_tier) - resolved_credentials: dict[str, object] = {} # mutable-ok: staged before mutation + resolved_credentials: dict[str, object] = {} for cred_key in ("api_key", "api_base"): cred_value = _updated_param(litellm_params, cred_key) if isinstance(cred_value, str) and cred_value.startswith("os.environ/"): @@ -299,7 +299,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai def _record_billing_usage(self, usage: Mapping[str, int]) -> None: """Stash this invocation's usage counters for the ``_process_*`` call the decorator runs next in the same asyncio task; overwrites any leftover.""" - _billing_usage_stash.set(dict(usage) if usage else None) # mutable-ok: fresh snapshot, popped by _process_* + _billing_usage_stash.set(dict(usage) if usage else None) def _pop_billing_tracing_detail(self) -> GuardrailTracingDetail | None: """Build the billing tracing detail from the stashed usage counters, priced @@ -326,14 +326,14 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai def _process_response( self, - response: dict | None, # mutable-ok: matches CustomGuardrail._process_response signature - request_data: dict, # mutable-ok: matches CustomGuardrail._process_response signature + response: dict | None, + request_data: dict, start_time: float | None = None, end_time: float | None = None, duration: float | None = None, event_type: GuardrailEventHooks | None = None, - original_inputs: dict | None = None, # mutable-ok: matches CustomGuardrail._process_response signature - ) -> dict | None: # mutable-ok: matches CustomGuardrail._process_response return + original_inputs: dict | None = None, + ) -> dict | None: """Override to attach the Azure billing tracing detail (usage counters and estimated cost) and the ``azure`` provider label to the recorded guardrail information. Follows the OpenAI moderation override pattern @@ -359,7 +359,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai def _process_error( self, e: Exception, - request_data: dict, # mutable-ok: matches CustomGuardrail._process_error signature + request_data: dict, start_time: float | None = None, end_time: float | None = None, duration: float | None = None, diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index 6a7ac4361b9..55367f36a81 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -964,7 +964,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): credentials: "Credentials | None", aws_region_name: str, api_key: str | None, - request_data: dict | None, # mutable-ok: proxy request body dict, mutated by the logging helper + request_data: dict | None, event_type: GuardrailEventHooks, start_time: "datetime", allow_chunking: bool, @@ -1035,7 +1035,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): len(batches), self.chunk_budget_chars, ) - batch_results: Final = [ # mutable-ok: await needs a list comprehension; frozen to a tuple below + batch_results: Final = [ await self._apply_guardrail_content_with_chunking( content=batch, base_request_data=base_request_data, @@ -1102,7 +1102,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): credentials: "Credentials | None", aws_region_name: str, api_key: str | None, - request_data: dict | None, # mutable-ok: proxy request body dict, mutated by the logging helper + request_data: dict | None, event_type: GuardrailEventHooks, start_time: "datetime", completed_chunk_usages: list[BedrockGuardrailUsage], # mutable-ok: passed through to the single-call layer @@ -1152,7 +1152,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): credentials: "Credentials | None", aws_region_name: str, api_key: str | None, - request_data: dict | None, # mutable-ok: proxy request body dict, mutated by the logging helper + request_data: dict | None, event_type: GuardrailEventHooks, start_time: "datetime", completed_chunk_usages: list[BedrockGuardrailUsage], # mutable-ok: billed-chunk usage accumulator @@ -1176,10 +1176,10 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): AWS billed them to ``completed_chunk_usages``, and the attempt log sums those with the blocking call's own usage. """ - bedrock_request_data: Final = { # mutable-ok: outbound JSON request body + bedrock_request_data: Final = { **base_request_data, "content": content, - } # mutable-ok: outbound JSON request body + } prepared_request: Final = await run_aws_signing( self._prepare_request, credentials=credentials, @@ -1188,7 +1188,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): aws_region_name=aws_region_name, api_key=api_key, ) - headers_dict: Final = dict(prepared_request.headers) # mutable-ok: the masking helper requires a dict + headers_dict: Final = dict(prepared_request.headers) verbose_proxy_logger.debug( "Bedrock AI request body: %s, url %s, headers: %s", bedrock_request_data, @@ -1243,8 +1243,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): def _log_apply_guardrail_attempt( self, httpx_response: httpx.Response, - json_response: dict, # mutable-ok: raw AWS JSON payload - request_data: dict | None, # mutable-ok: proxy request body dict, mutated by the logging helper + json_response: dict, + request_data: dict | None, event_type: GuardrailEventHooks, start_time: "datetime", aws_region_name: str | None, @@ -1259,7 +1259,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): (blocking_usage,) if isinstance(blocking_usage, dict) else () ) logged_json_response: Final = ( - { # mutable-ok: raw AWS JSON payload carrying the total billed usage + { **json_response, "usage": self._sum_usage_counters(billed_usages), } @@ -1272,7 +1272,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response=logged_json_response, - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + request_data=request_data or {}, guardrail_status=self._get_bedrock_guardrail_response_status(response=httpx_response), start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1284,7 +1284,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): def _log_apply_guardrail_success( self, merged_response: BedrockGuardrailResponse, - request_data: dict | None, # mutable-ok: proxy request body dict, mutated by the logging helper + request_data: dict | None, event_type: GuardrailEventHooks, start_time: "datetime", aws_region_name: str | None, @@ -1301,8 +1301,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): tracing_detail: Final = self._build_tracing_detail(merged_response, aws_region_name=aws_region_name) self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, - guardrail_json_response=dict(merged_response), # mutable-ok: logging helper requires a dict - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + guardrail_json_response=dict(merged_response), + request_data=request_data or {}, guardrail_status=( "guardrail_failed_to_respond" if "Exception" in str((merged_response.get("Output") or {}).get("__type", "")) @@ -1318,7 +1318,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): def _log_apply_guardrail_failure( self, detail: object, - request_data: dict | None, # mutable-ok: proxy request body dict, mutated by the logging helper + request_data: dict | None, event_type: GuardrailEventHooks, start_time: "datetime", aws_region_name: str | None, @@ -1330,12 +1330,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): every failed attempt chunking made along the way. Chunk calls AWS billed before the failure still carry their usage and cost.""" billed_usage: Final = self._sum_usage_counters(completed_chunk_usages) if completed_chunk_usages else None - error_payload: Final = {"error": str(detail)} # mutable-ok: logging helper requires a dict - json_response: Final = ( - {**error_payload, "usage": billed_usage} # mutable-ok: logging helper requires a dict - if billed_usage is not None - else error_payload - ) + error_payload: Final = {"error": str(detail)} + json_response: Final = {**error_payload, "usage": billed_usage} if billed_usage is not None else error_payload tracing_detail: Final = ( self._build_tracing_detail(BedrockGuardrailResponse(usage=billed_usage), aws_region_name=aws_region_name) if billed_usage is not None @@ -1344,7 +1340,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response=json_response, - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + request_data=request_data or {}, guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1359,7 +1355,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): (``grounding_source``, ``query``, or the ``guard_content`` the response itself is tagged with once grounding is present).""" for item in content: - if (item.get("text") or {}).get("qualifiers"): # mutable-ok: read-only empty fallback + if (item.get("text") or {}).get("qualifiers"): return True return False @@ -1567,9 +1563,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): """ logical_units: Final = BedrockGuardrail._group_fragment_units(chunk_results) per_unit_outputs: Final = tuple(BedrockGuardrail._merge_logical_unit_outputs(unit) for unit in logical_units) - merged_outputs: Final = [ # mutable-ok: logged payload; redaction only traverses dict/list - output for outputs, _ in per_unit_outputs for output in outputs - ] + merged_outputs: Final = [output for outputs, _ in per_unit_outputs for output in outputs] any_masked: Final = any(masked for _, masked in per_unit_outputs) actions: Final = tuple( @@ -1580,18 +1574,16 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): merged_action: Final = ( "GUARDRAIL_INTERVENED" if "GUARDRAIL_INTERVENED" in actions else (actions[-1] if actions else None) ) - merged_assessments: Final = [ # mutable-ok: logged payload; redaction only traverses dict/list + merged_assessments: Final = [ assessment for chunk_result in chunk_results - for assessment in (chunk_result.response.get("assessments") or []) # mutable-ok: logged payload + for assessment in (chunk_result.response.get("assessments") or []) ] any_usage_reported: Final = any(chunk_result.response.get("usage") for chunk_result in chunk_results) merged: Final[BedrockGuardrailResponse] = cast( # cast-ok: TypedDict assembled from a comprehension BedrockGuardrailResponse, - { # mutable-ok: builds the TypedDict payload - key: value for chunk_result in chunk_results for key, value in chunk_result.response.items() - }, + {key: value for chunk_result in chunk_results for key, value in chunk_result.response.items()}, ) if merged_action is not None: merged["action"] = merged_action @@ -1614,17 +1606,14 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): this code does not know about (AWS has added several) is still summed and reported instead of being silently dropped to zero.""" return BedrockGuardrail._sum_usage_counters( - tuple( - chunk_result.response.get("usage") or {} # mutable-ok: read-only empty fallback - for chunk_result in chunk_results - ) + tuple(chunk_result.response.get("usage") or {} for chunk_result in chunk_results) ) @staticmethod def _sum_usage_counters(usages: Sequence[BedrockGuardrailUsage]) -> BedrockGuardrailUsage: return cast( # cast-ok: TypedDict assembled from a comprehension BedrockGuardrailUsage, - { # mutable-ok: builds the TypedDict payload + { key: sum(usage.get(key) or 0 for usage in usages) for key in dict.fromkeys(key for usage in usages for key in usage) }, @@ -1692,9 +1681,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): return tuple(result.response.get("outputs") or result.response.get("output") or ()) def fragment_text(result: BedrockContentChunkResult) -> str: - source: Final = (result.content[0].get("text") or {}).get( # mutable-ok: read-only fallback - "text" - ) or "" + source: Final = (result.content[0].get("text") or {}).get("text") or "" outputs: Final = fragment_outputs(result) masked: Final = outputs[0].get("text") if outputs else None return masked if masked is not None else source @@ -1709,10 +1696,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): return tuple(chunk_outputs), bool(chunk_outputs) if not chunk_outputs: return tuple( - BedrockGuardrailOutput( - text=(item.get("text") or {}).get("text") or "" # mutable-ok: read-only fallback - ) - for item in chunk_result.content + BedrockGuardrailOutput(text=(item.get("text") or {}).get("text") or "") for item in chunk_result.content ), False return tuple(chunk_outputs), True @@ -1767,10 +1751,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): if log_transport_failure: self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, - guardrail_json_response={ # mutable-ok: logging helper requires a dict - "error": detail_message - }, - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + guardrail_json_response={"error": detail_message}, + request_data=request_data or {}, guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1785,7 +1767,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response={"error": str(e)}, - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + request_data=request_data or {}, guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1915,7 +1897,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response={"error": detail_message}, - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + request_data=request_data or {}, guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1931,7 +1913,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response={"error": str(e)}, - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + request_data=request_data or {}, guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1949,7 +1931,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): self.add_standard_logging_guardrail_information_to_request_data( guardrail_provider=self.guardrail_provider, guardrail_json_response=self._sanitize_invoke_checks_response_for_logging(json_response), - request_data=request_data or {}, # mutable-ok: logging helper requires a dict + request_data=request_data or {}, guardrail_status=self._get_invoke_checks_status(bool(violations)), start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -2182,9 +2164,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ) -> GuardrailTracingDetail: if not isinstance(usage, dict): return _NO_TRACING_DETAIL - usage_units: Final = { # mutable-ok: json.dumps'd into spend log metadata downstream - key: value for key, value in usage.items() if isinstance(value, int) - } + usage_units: Final = {key: value for key, value in usage.items() if isinstance(value, int)} if not usage_units: return _NO_TRACING_DETAIL cost_by_unit: Final = bedrock_guardrail_cost_by_unit(usage_units=usage_units, aws_region_name=aws_region_name) diff --git a/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py index 9eac143be88..e7641378f3a 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/conduct/__init__.py @@ -40,10 +40,10 @@ def initialize_guardrail( return _callback -guardrail_initializer_registry: Final = { # mutable-ok: module-level registry, built once and never mutated +guardrail_initializer_registry: Final = { SupportedGuardrailIntegrations.CONDUCT.value: initialize_guardrail, } -guardrail_class_registry: Final = { # mutable-ok: module-level registry, built once and never mutated +guardrail_class_registry: Final = { SupportedGuardrailIntegrations.CONDUCT.value: ConductGuardrail, } diff --git a/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py b/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py index c87f8c016b1..1b7c2913a2b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py +++ b/litellm/proxy/guardrails/guardrail_hooks/conduct/conduct.py @@ -69,7 +69,7 @@ class ConductVerdict(BaseModel): def record_decision( guardrail: CustomGuardrail, - request_data: dict[str, object], # mutable-ok: the logging helper writes metadata into it + request_data: dict[str, object], decision: ConductDecision, ) -> None: guardrail.add_standard_logging_guardrail_information_to_request_data( @@ -128,7 +128,7 @@ else: async def apply_guardrail( self, inputs: GenericGuardrailAPIInputs, - request_data: dict[str, object], # mutable-ok: CustomGuardrail.apply_guardrail contract + request_data: dict[str, object], input_type: Literal["request", "response"], logging_obj: LiteLLMLoggingObj | None = None, ) -> GenericGuardrailAPIInputs: diff --git a/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py b/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py index 8fed1f906e5..15a0d71c973 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py +++ b/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py @@ -378,7 +378,7 @@ class CrowdStrikeAIDRHandler(CustomGuardrail): if transformed_signal: raise HTTPException( status_code=500, - detail={ # mutable-ok: one-shot HTTPException detail payload, never mutated after construction + detail={ "error": "CrowdStrike AIDR returned a transformed response litellm could not parse; " "failing closed instead of dropping the delivered redactions", "guardrail_name": self.guardrail_name, diff --git a/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py b/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py index d5ef1e949b8..9ebe26f8b3c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py +++ b/litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py @@ -361,11 +361,11 @@ class CustomCodeGuardrail(CustomGuardrail): ) end_time: Final = time.time() self.add_standard_logging_guardrail_information_to_request_data( - guardrail_json_response={ # mutable-ok: logging helper requires a dict + guardrail_json_response={ "action": "flag", "reason": flag_reason, "input_type": input_type, - "metadata": result.get("metadata") or {}, # mutable-ok: logging helper requires a dict + "metadata": result.get("metadata") or {}, }, request_data=request_data, guardrail_status="guardrail_flagged", diff --git a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py index fa113aa4d33..13b4eb0a621 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py +++ b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py @@ -880,7 +880,7 @@ class HeadroomGuardrail(CustomGuardrail): self, kwargs: dict[str, Any], call_type: CallTypes | None, - ) -> dict[str, Any] | None: # mutable-ok: overrides CustomLogger hook whose contract is a plain dict + ) -> dict[str, Any] | None: base_result: Final = await super().async_pre_call_deployment_hook(kwargs, call_type) effective: Final = base_result if base_result is not None else kwargs if call_type not in _STREAM_CONVERTIBLE_CALL_TYPES: @@ -889,7 +889,7 @@ class HeadroomGuardrail(CustomGuardrail): return base_result if not has_headroom_retrieve_tool(effective.get("tools")): return base_result - return { # mutable-ok: the hook contract is a plain dict the router merges into the request kwargs + return { **effective, "stream": False, HEADROOM_CONVERTED_STREAM_KEY: True, diff --git a/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py b/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py index 68914a1989e..3396bc0ae08 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py +++ b/litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py @@ -177,7 +177,7 @@ def _scannable_text(content: object) -> str: return str(content or "") parts: Final[Sequence[object]] = content - text_parts: Final = [item for item in parts if not _is_image_part(item)] # mutable-ok: sent as a list repr + text_parts: Final = [item for item in parts if not _is_image_part(item)] return str(text_parts or "") diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index 2f98a9afbd8..3e85ada750d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -115,7 +115,7 @@ def _pre_masking_scope_indices( def _apply_redacted_messages_back_preserving_fields( guardrail: "LakeraAIGuardrail", - data: dict[str, object], # mutable-ok: writes the redacted result back into the caller's request dict in place + data: dict[str, object], redacted_messages: Sequence[AllMessageValues], ) -> None: """Write masked content back to ``data["messages"]`` without losing fields @@ -126,12 +126,12 @@ def _apply_redacted_messages_back_preserving_fields( Responses-API ``input`` string, with no chat messages to merge into).""" original_messages: Final = data.get("messages") if not isinstance(original_messages, list): - redacted_list: Final = list(redacted_messages) # mutable-ok: apply_redacted_messages_back requires a list + redacted_list: Final = list(redacted_messages) apply_redacted_messages_back(data, redacted_list) return scope_indices: Final = _pre_masking_scope_indices(guardrail, original_messages) guardrailed_scoped: Final = tuple( - { # mutable-ok: fresh dict per iteration, not stored beyond this comprehension + { **original_messages[original_idx], "content": redacted["content"], } @@ -225,13 +225,11 @@ def _build_lakera_inspection_messages(data: Mapping[str, object]) -> Sequence[Ma would have silently mishandled a PII/redaction hit found there.""" instructions: Final = data.get("instructions") leading: Final[Sequence[Mapping[str, str]]] = ( - [{"role": "system", "content": instructions}] # mutable-ok: fresh list/dict, not stored - if isinstance(instructions, str) and instructions - else [] # mutable-ok: fresh empty list, not stored + [{"role": "system", "content": instructions}] if isinstance(instructions, str) and instructions else [] ) - return [ # mutable-ok: fresh list, not stored + return [ *leading, - *build_inspection_messages(dict(data)), # mutable-ok: fresh shallow copy for the dict[str, Any] param + *build_inspection_messages(dict(data)), ] @@ -494,7 +492,7 @@ class LakeraAIGuardrail(CustomGuardrail): def _mask_unwritable_instructions_pii_in_place( self, - data: dict[str, object], # mutable-ok: writes the redacted result back into the caller's request dict in place + data: dict[str, object], inspected_messages: Sequence[AllMessageValues], lakera_response: LakeraAIResponse | None, masked_entity_count: dict[str, int], @@ -777,7 +775,7 @@ class LakeraAIGuardrail(CustomGuardrail): choice_indices.append(i) # Use a copy of original_messages so _mask_pii_in_messages does not mutate data["messages"] - post_call_messages: Final = list(copy.deepcopy(original_messages)) + response_messages # mutable-ok: needs list + post_call_messages: Final = list(copy.deepcopy(original_messages)) + response_messages # Call Lakera guardrail lakera_guardrail_response, _ = await self.call_v2_guard( diff --git a/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py b/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py index fde40111d49..793f9a467af 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py +++ b/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py @@ -482,8 +482,8 @@ class ModelArmorGuardrail(CustomGuardrail, VertexBase): if existing is None: return armor_response if isinstance(existing, list): - return [*existing, armor_response] # mutable-ok: logging pipeline requires list[dict], not tuple - return [existing, armor_response] # mutable-ok: logging pipeline requires list[dict], not tuple + return [*existing, armor_response] + return [existing, armor_response] def _process_response( self, @@ -967,8 +967,8 @@ class ModelArmorGuardrail(CustomGuardrail, VertexBase): output_item=output_item, output_idx=output_idx, texts_to_check=texts, - images_to_check=[], # mutable-ok: the extractor's images sink, unused here - task_mappings=[], # mutable-ok: the extractor's task-mapping sink, unused here + images_to_check=[], + task_mappings=[], tool_calls_to_check=tool_calls, ) return "".join((*texts, *(json.dumps(tool_call) for tool_call in tool_calls))) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 70ea21320ee..7b5f34f18a0 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -163,7 +163,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): # Per-loop semaphores bounding chunked-analyze fan-out across ALL # concurrent oversized blocks/requests on this instance, not per call - self._loop_chunk_semaphores: _LoopSemaphores = {} # mutable-ok: per-loop semaphore cache + self._loop_chunk_semaphores: _LoopSemaphores = {} if mock_testing is True: # for testing purposes only return @@ -453,7 +453,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): self, text: str, presidio_config: PresidioPerRequestConfig | None, - request_data: dict, # mutable-ok: shared per-request state dict, matching analyze_text's parameter + request_data: dict, ) -> list[PresidioAnalyzeResponseItem]: # mutable-ok: analyze_text's declared return type requires list """ Analyze an oversized text by splitting it into overlapping chunks. diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index e20f0b320b9..226718fc406 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -579,9 +579,7 @@ class ToolPermissionGuardrail(CustomGuardrail): verbose_proxy_logger.info("Blocking %s unauthorized tool uses", len(denied_tools)) - error_by_tool_use_id: Final[ - Mapping[object, str] - ] = { # mutable-ok: read-only lookup, never mutated after construction + error_by_tool_use_id: Final[Mapping[object, str]] = { tool_call.id: self._create_permission_error_result(tool_call, error).content for tool_call, error in denied_tools } @@ -596,9 +594,9 @@ class ToolPermissionGuardrail(CustomGuardrail): message for message in (_denied_message(block) for block in content) if message is not None ) kept_blocks: Final = tuple(block for block in content if _denied_message(block) is None) - new_content: Final = [ # mutable-ok: response content is a JSON array on the wire + new_content: Final = [ *kept_blocks, - {"type": "text", "text": "\n".join(error_messages)}, # mutable-ok: content block is a JSON object + {"type": "text", "text": "\n".join(error_messages)}, ] response["content"] = new_content # rebind-ok: the guardrail rewrites the provider response in place diff --git a/litellm/proxy/guardrails/guardrail_registry.py b/litellm/proxy/guardrails/guardrail_registry.py index 0dc50cd6196..aebf9625cd9 100644 --- a/litellm/proxy/guardrails/guardrail_registry.py +++ b/litellm/proxy/guardrails/guardrail_registry.py @@ -477,7 +477,7 @@ class InMemoryGuardrailHandler: Guardrail id to CustomGuardrail object mapping """ - self.guardrail_id_to_sibling_callbacks: dict[str, GuardrailCallbacks] = {} # mutable-ok: per-id registry + self.guardrail_id_to_sibling_callbacks: dict[str, GuardrailCallbacks] = {} self._sources: dict[str, Literal["db", "config"]] = {} """ diff --git a/litellm/proxy/health_check.py b/litellm/proxy/health_check.py index b1e4f6fd9c3..fc2278986fc 100644 --- a/litellm/proxy/health_check.py +++ b/litellm/proxy/health_check.py @@ -505,11 +505,7 @@ def _finalize_strategy_router_endpoints( return ( tuple(e for e in kept_healthy if verdict_for(e) is None), tuple(e for e in unhealthy_endpoints if keep(e)) - + tuple( - dict(e, error=error) # mutable-ok: the /health payload must stay a plain JSON-serializable dict - for e in kept_healthy - if (error := verdict_for(e)) is not None - ), + + tuple(dict(e, error=error) for e in kept_healthy if (error := verdict_for(e)) is not None), ) @@ -917,7 +913,7 @@ async def perform_health_check( if router is not None else () ) - checked: Final = requested + list(dependency_probes) # mutable-ok: _perform_health_check takes a list + checked: Final = requested + list(dependency_probes) if instrumentation_enabled: logger.debug( diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index db6ec754c6e..410eb88bc54 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -547,7 +547,7 @@ async def health_services_endpoint( ) ms_teams_response: Final = await proxy_logging_obj.slack_alerting_instance.async_http_handler.post( url=ms_teams_webhook_url, - headers=dict(MS_TEAMS_ALERT_HEADERS), # mutable-ok: async_http_handler.post only accepts dict headers + headers=dict(MS_TEAMS_ALERT_HEADERS), data=json.dumps(build_ms_teams_payload(ms_teams_test_message)), ) if ms_teams_response.status_code >= 400: diff --git a/litellm/proxy/hooks/batch_rate_limiter.py b/litellm/proxy/hooks/batch_rate_limiter.py index dcd34a1d9cb..de6b8aeed27 100644 --- a/litellm/proxy/hooks/batch_rate_limiter.py +++ b/litellm/proxy/hooks/batch_rate_limiter.py @@ -110,9 +110,7 @@ class BatchFileUsage(BaseModel): # each target a different model, so the project's per-model ITPM/OTPM # quota for a row's actual model must be charged with that row's own # tokens -- see `_create_project_io_descriptors_for_models`. - per_model_usage: dict[str, dict[str, int]] = Field( - default_factory=dict - ) # mutable-ok: accumulated incrementally per row while parsing the batch file + per_model_usage: dict[str, dict[str, int]] = Field(default_factory=dict) class _PROXY_BatchRateLimiter(CustomLogger): @@ -287,7 +285,7 @@ class _PROXY_BatchRateLimiter(CustomLogger): for descriptor in model_descriptors: extra_descriptors.append(descriptor) extra_increments.append( - { # mutable-ok: atomic limiter API requires mutable increment records + { "requests": 0, "tokens": usage.get("output_tokens", 0) if descriptor["key"] == PROJECT_OTPM_DESCRIPTOR_KEY @@ -424,7 +422,7 @@ class _PROXY_BatchRateLimiter(CustomLogger): body: Final[Mapping[str, object]] = ( MappingProxyType(_BATCH_BODY_ADAPTER.validate_python(raw_body)) if isinstance(raw_body, Mapping) - else MappingProxyType({}) # mutable-ok: immediately frozen empty fallback + else MappingProxyType({}) ) # `max_tokens`/`max_completion_tokens` cap chat completions; `/v1/responses` # rows cap output with `max_output_tokens` instead -- omitting it here @@ -692,7 +690,7 @@ class _PROXY_BatchRateLimiter(CustomLogger): ) increments: list[IncrementAmounts] = [ # mutable-ok: reassigned below to append project IO increments - { # mutable-ok: atomic limiter API requires mutable increment records + { "requests": batch_usage.request_count, "tokens": batch_usage.total_tokens, } diff --git a/litellm/proxy/hooks/model_max_budget_limiter.py b/litellm/proxy/hooks/model_max_budget_limiter.py index efaaab277a9..c06e5977ef8 100644 --- a/litellm/proxy/hooks/model_max_budget_limiter.py +++ b/litellm/proxy/hooks/model_max_budget_limiter.py @@ -212,7 +212,7 @@ async def build_model_max_budget_usage( async def _current_window_spends(cache: DualCache, spend_keys: Sequence[str]) -> tuple[float, ...]: """Redis holds the window total across replicas; the in-memory copy is one replica's share.""" - keys: Final = list(spend_keys) # mutable-ok: both batch readers annotate their key argument as list + keys: Final = list(spend_keys) redis_cache: Final = cache.redis_cache if redis_cache is not None: shared: Final = await redis_cache.async_batch_get_cache(key_list=keys) diff --git a/litellm/proxy/hooks/parallel_request_limiter_v3.py b/litellm/proxy/hooks/parallel_request_limiter_v3.py index c398abff099..c8a95ac5cd3 100644 --- a/litellm/proxy/hooks/parallel_request_limiter_v3.py +++ b/litellm/proxy/hooks/parallel_request_limiter_v3.py @@ -843,8 +843,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): config_field: Final = "config" if "config" in data or "generationConfig" not in data else "generationConfig" config: Final = data.get(config_field) if config is None or isinstance(config, dict): - data[config_field] = { # rebind-ok: routed request needs cap # mutable-ok: downstream needs dict - **(config or {}), # mutable-ok: downstream native routing requires a mutable request config + data[config_field] = { # rebind-ok: routed request needs cap + **(config or {}), "maxOutputTokens": effective_cap, } return @@ -1813,7 +1813,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if not descriptor_groups: return RateLimitResponse( overall_code="OK", - statuses=[], # mutable-ok: response contract requires a status list + statuses=[], ) applied: Final[list[list[AtomicCounterMeta]]] = [] statuses: Final[list[RateLimitStatus]] = [] @@ -2012,7 +2012,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): ], ) descriptor_state.append( - { # mutable-ok: local atomic-counter state is updated during pass two + { "window_expired": window_expired, "current": current_counter, "window_start": str(now_int if window_expired else int(window_start)), @@ -2087,7 +2087,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): ) for d in descriptors if d["key"] not in (PROJECT_ITPM_DESCRIPTOR_KEY, PROJECT_OTPM_DESCRIPTOR_KEY) - and (d.get("rate_limit") or {}).get("tokens_per_unit") is not None # mutable-ok: optional descriptor + and (d.get("rate_limit") or {}).get("tokens_per_unit") is not None ] if not tpm_descriptors: return RateLimitResponse(overall_code="OK", statuses=[]) @@ -2163,23 +2163,16 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): configured, or if the reservation failed), for the caller to stash for post-call reconciliation. """ - itpm_descriptors: Final = [ # mutable-ok: atomic limiter API requires lists - d for d in descriptors if d["key"] == PROJECT_ITPM_DESCRIPTOR_KEY - ] - otpm_descriptors: Final = [ # mutable-ok: atomic limiter API requires lists - d for d in descriptors if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY - ] + itpm_descriptors: Final = [d for d in descriptors if d["key"] == PROJECT_ITPM_DESCRIPTOR_KEY] + otpm_descriptors: Final = [d for d in descriptors if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY] if not itpm_descriptors and not otpm_descriptors: - return RateLimitResponse(overall_code="OK", statuses=[]), 0, 0 # mutable-ok: response contract uses a list + return RateLimitResponse(overall_code="OK", statuses=[]), 0, 0 itpm_response: Final = ( await self.atomic_check_and_increment_by_n( descriptors=itpm_descriptors, - increments=[ # mutable-ok: atomic limiter API requires mutable increment records - {"tokens": estimated_input_tokens} # mutable-ok: atomic limiter increment record - for _ in itpm_descriptors - ], + increments=[{"tokens": estimated_input_tokens} for _ in itpm_descriptors], parent_otel_span=parent_otel_span, ) if itpm_descriptors @@ -2192,25 +2185,20 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if otpm_descriptors: otpm_response: Final = await self.atomic_check_and_increment_by_n( descriptors=otpm_descriptors, - increments=[ # mutable-ok: atomic limiter API requires mutable increment records - {"tokens": estimated_output_tokens} # mutable-ok: atomic limiter increment record - for _ in otpm_descriptors - ], + increments=[{"tokens": estimated_output_tokens} for _ in otpm_descriptors], parent_otel_span=parent_otel_span, ) if otpm_response["overall_code"] == "OVER_LIMIT": if itpm_reserved > 0: await self._refund_reserved_tokens( - scopes=[ # mutable-ok: reservation rollback accepts collected scopes - (d["key"], d["value"]) for d in itpm_descriptors - ], + scopes=[(d["key"], d["value"]) for d in itpm_descriptors], amount=itpm_reserved, reservation_windows=itpm_response.get("reservation_windows", frozenset()), parent_otel_span=parent_otel_span, ) return otpm_response, 0, 0 statuses: Final = ( - [ # mutable-ok: response contract uses a list + [ *itpm_response["statuses"], *otpm_response["statuses"], ] @@ -2996,12 +2984,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): return itpm_limit_for_project_model: Final = ( - get_model_rate_limit_from_metadata(user_api_key_dict, "project_metadata", "model_itpm_limit") - or {} # mutable-ok: metadata helper returns an optional mapping + get_model_rate_limit_from_metadata(user_api_key_dict, "project_metadata", "model_itpm_limit") or {} ) otpm_limit_for_project_model: Final = ( - get_model_rate_limit_from_metadata(user_api_key_dict, "project_metadata", "model_otpm_limit") - or {} # mutable-ok: metadata helper returns an optional mapping + get_model_rate_limit_from_metadata(user_api_key_dict, "project_metadata", "model_otpm_limit") or {} ) model_itpm_limit: Final = itpm_limit_for_project_model.get(requested_model) @@ -3016,7 +3002,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): RateLimitDescriptor( key=PROJECT_ITPM_DESCRIPTOR_KEY, value=descriptor_value, - rate_limit={ # mutable-ok: descriptor TypedDict requires a runtime dict + rate_limit={ "requests_per_unit": None, "tokens_per_unit": model_itpm_limit, "window_size": self.window_size, @@ -3028,7 +3014,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): RateLimitDescriptor( key=PROJECT_OTPM_DESCRIPTOR_KEY, value=descriptor_value, - rate_limit={ # mutable-ok: descriptor TypedDict requires a runtime dict + rate_limit={ "requests_per_unit": None, "tokens_per_unit": model_otpm_limit, "window_size": self.window_size, @@ -3142,12 +3128,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if not isinstance(content, list): sanitized.append(message) continue - filtered_content = [ # mutable-ok: token_counter requires list content blocks + filtered_content = [ block for block in content if not (isinstance(block, dict) and block.get("type") == "input_audio") ] - sanitized.append( # mutable-ok: token_counter requires mutable message dicts - {**message, "content": filtered_content} # mutable-ok: token_counter requires message dicts - ) + sanitized.append({**message, "content": filtered_content}) return sanitized @staticmethod @@ -3305,21 +3289,17 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if not isinstance(data, dict): return stash: Final = claim_request_stash_for_data(data) - io_token_descriptors: Final = [ # mutable-ok: reservation API requires descriptor lists + io_token_descriptors: Final = [ d for d in descriptors if d["key"] in (PROJECT_ITPM_DESCRIPTOR_KEY, PROJECT_OTPM_DESCRIPTOR_KEY) ] if not io_token_descriptors: return - configured_otpm_limits: Final = [ # mutable-ok: min calculation materializes validated limits + configured_otpm_limits: Final = [ int(v) for d in io_token_descriptors if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY - for v in [ # mutable-ok: comprehension binds the optional descriptor value - (d.get("rate_limit") or {}).get( # mutable-ok: optional descriptor fallback - "tokens_per_unit" - ) - ] + for v in [(d.get("rate_limit") or {}).get("tokens_per_unit")] if v is not None ] min_configured_otpm_limit: Final = min(configured_otpm_limits) if configured_otpm_limits else None @@ -3654,10 +3634,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): (d["key"], d["value"]) for d in descriptors if d["key"] not in (PROJECT_ITPM_DESCRIPTOR_KEY, PROJECT_OTPM_DESCRIPTOR_KEY) - and (d.get("rate_limit") or {}).get( # mutable-ok: optional descriptor fallback - "tokens_per_unit" - ) - is not None + and (d.get("rate_limit") or {}).get("tokens_per_unit") is not None ) tpm_reservation_scopes = tuple( # rebind-ok: record successful reservation scopes stash.reserved_scopes @@ -3932,11 +3909,11 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if self.window_guarded_token_increment_script is not None: try: await self.window_guarded_token_increment_script( - keys=[ # mutable-ok: Redis script interface requires a key list + keys=[ window_key, operation["key"], ], - args=[ # mutable-ok: Redis script interface requires an argument list + args=[ expected_window_start, operation["increment_value"], operation["ttl"] or 0, diff --git a/litellm/proxy/hooks/responses_id_security.py b/litellm/proxy/hooks/responses_id_security.py index 7e7f70d6f7e..bd5815f3029 100644 --- a/litellm/proxy/hooks/responses_id_security.py +++ b/litellm/proxy/hooks/responses_id_security.py @@ -89,7 +89,7 @@ def _rewrite_advertised_id( if not isinstance(payload_id, str): return event - rewritten: Final = {**payload, "id": rewrite(payload_id)} # mutable-ok: pydantic cannot serialize a frozen map + rewritten: Final = {**payload, "id": rewrite(payload_id)} setattr(event, "response", rewritten) return event diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 59971e54e46..6b76e603409 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -515,7 +515,7 @@ def _strip_client_message_redaction_opt_out(data: dict[str, object]) -> None: def _strip_client_callback_credentials( - data: dict[str, Any], # mutable-ok: strips in place on the request body the pre-call pipeline threads through + data: dict[str, Any], ) -> None: """Drop callback credentials and destinations supplied by the caller. @@ -579,7 +579,7 @@ def _strip_client_pricing_overrides(data: dict[str, object]) -> None: def _strip_router_reserved_metadata( - data: dict[str, Any], # mutable-ok: strips in place on the request body the pre-call pipeline threads through + data: dict[str, Any], ) -> None: """Drop the router-owned fallback stamps from any client-supplied metadata bucket.""" for metadata_key in ("metadata", "litellm_metadata"): @@ -756,7 +756,7 @@ def _is_llm_inference_route(request: Request) -> bool: def apply_missing_session_id_policy( - data: dict[str, object], # mutable-ok: stamps session ids in place on the request body the pipeline threads through + data: dict[str, object], _metadata_variable_name: str, general_settings: Mapping[str, object] | None, request: Request, @@ -3033,7 +3033,7 @@ def _add_guardrails_from_policies_in_metadata( def add_guardrails_from_auth_metadata( user_api_key_dict: UserAPIKeyAuth, - data: dict, # mutable-ok: writes guardrails into the live request dict, same contract as the helpers it wraps + data: dict, metadata_variable_name: str, ) -> None: """Resolve key, team, and project guardrails, direct and via policies, onto the request metadata.""" diff --git a/litellm/proxy/management_endpoints/access_group_endpoints.py b/litellm/proxy/management_endpoints/access_group_endpoints.py index a6cc5140b15..c0eaa23a209 100644 --- a/litellm/proxy/management_endpoints/access_group_endpoints.py +++ b/litellm/proxy/management_endpoints/access_group_endpoints.py @@ -226,9 +226,9 @@ async def _teams_touching(team_table: _TeamTable, records: Sequence[_AccessGroup """Team rows listed on any of the groups or carrying any of them in access_group_ids.""" group_ids: Final = tuple(record.access_group_id for record in records) stored_team_ids: Final = _ids_across(records, lambda record: record.assigned_team_ids) - carrying: Final = {"access_group_ids": {"hasSome": group_ids}} # mutable-ok: prisma where is a dict - listed: Final = {"team_id": {"in": stored_team_ids}} # mutable-ok: prisma where is a dict - return await team_table.find_many(where={"OR": (carrying, listed)}) # mutable-ok: prisma where is a dict + carrying: Final = {"access_group_ids": {"hasSome": group_ids}} + listed: Final = {"team_id": {"in": stored_team_ids}} + return await team_table.find_many(where={"OR": (carrying, listed)}) async def _attached_team_ids_for( @@ -242,7 +242,7 @@ async def _attached_team_ids_for( async def _require_teams_exist(tx: _AccessGroupTx, team_ids: Sequence[str]) -> None: if not team_ids: return - where: Final = {"team_id": {"in": team_ids}} # mutable-ok: prisma where is a dict + where: Final = {"team_id": {"in": team_ids}} found: Final = await tx.litellm_teamtable.find_many(where=where) missing: Final = frozenset(team_ids) - frozenset(team.team_id for team in found) if missing: diff --git a/litellm/proxy/management_endpoints/auto_router_endpoints.py b/litellm/proxy/management_endpoints/auto_router_endpoints.py index 50716e5d474..f83a6aefd6d 100644 --- a/litellm/proxy/management_endpoints/auto_router_endpoints.py +++ b/litellm/proxy/management_endpoints/auto_router_endpoints.py @@ -220,7 +220,7 @@ async def _authorize_router_dry_run(user_api_key_dict: UserAPIKeyAuth, team_id: if team_id is None: raise HTTPException( status_code=403, - detail={ # mutable-ok: HTTPException detail must be a plain mapping to keep this route's {"error": ...} response shape + detail={ "error": f"User does not have permission to dry-run an auto router. Your role={user_api_key_dict.user_role}. Call as a PROXY_ADMIN, or as a team admin by specifying a team_id." }, ) @@ -228,20 +228,16 @@ async def _authorize_router_dry_run(user_api_key_dict: UserAPIKeyAuth, team_id: if prisma_client is None: raise HTTPException( status_code=500, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": CommonProxyErrors.db_not_connected_error.value - }, + detail={"error": CommonProxyErrors.db_not_connected_error.value}, ) team_row: Final = await _team_table(prisma_client).find_unique( - where={"team_id": team_id}, # mutable-ok: Prisma query filters are dict-shaped + where={"team_id": team_id}, ) if team_row is None: raise HTTPException( status_code=400, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": f"Team id={team_id} does not exist in db" - }, + detail={"error": f"Team id={team_id} does not exist in db"}, ) ModelManagementAuthChecks.can_user_make_team_model_call( @@ -312,8 +308,8 @@ async def _authorize_models_this_test_can_call( @router.post( "/auto_router/validate_complexity_router_config", - tags=["model management"], # mutable-ok: fastapi's decorator signature types tags as a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: fastapi's decorator signature types dependencies as a list + tags=["model management"], + dependencies=[Depends(user_api_key_auth)], response_model=ComplexityRouterConfigValidationResponse, status_code=status.HTTP_200_OK, ) @@ -341,8 +337,8 @@ async def validate_complexity_router_config( @router.post( "/auto_router/test_routing", - tags=["model management"], # mutable-ok: fastapi's decorator signature types tags as a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: fastapi's decorator signature types dependencies as a list + tags=["model management"], + dependencies=[Depends(user_api_key_auth)], response_model=AutoRouterRoutingTestResponse, status_code=status.HTTP_200_OK, ) @@ -397,9 +393,7 @@ async def preview_auto_router_routing( if llm_router is None: raise HTTPException( status_code=500, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": CommonProxyErrors.no_llm_router.value - }, + detail={"error": CommonProxyErrors.no_llm_router.value}, ) await _authorize_models_this_test_can_call( @@ -417,10 +411,10 @@ async def preview_auto_router_routing( ) request_kwargs: Final = LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata( - data={ # mutable-ok: the request-metadata helper takes and returns request kwargs as a dict + data={ **data.wire_body(), - "metadata": {}, # mutable-ok: the request-metadata helper writes the auth fields into this dict - "proxy_server_request": {"body": None}, # mutable-ok: the snapshot owner fills body in place + "metadata": {}, + "proxy_server_request": {"body": None}, }, user_api_key_dict=user_api_key_dict, _metadata_variable_name="metadata", @@ -437,17 +431,13 @@ async def preview_auto_router_routing( verbose_proxy_logger.exception("Auto router routing test failed. Due to error - %s", e) raise HTTPException( status_code=400, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": f"Could not route this prompt: {e}" - }, + detail={"error": f"Could not route this prompt: {e}"}, ) from e if hook_response is None or hook_response.routing_decision is None: raise HTTPException( status_code=400, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": "The router made no decision for this prompt. Check that at least one tier has a model." - }, + detail={"error": "The router made no decision for this prompt. Check that at least one tier has a model."}, ) available_models: Final = await get_available_models_for_user( @@ -1200,8 +1190,7 @@ async def _leg_attempt_counts(prisma_client: "PrismaClient", legs: Sequence[_Leg if not legs: return MappingProxyType({}) rows: Final = _ATTEMPT_COUNT_ROWS.validate_python( - await _query_raw(prisma_client, _ATTEMPT_COUNTS_SQL, [leg.id for leg in legs]) # mutable-ok: query param - or () + await _query_raw(prisma_client, _ATTEMPT_COUNTS_SQL, [leg.id for leg in legs]) or () ) return MappingProxyType({row.job_id: row for row in rows}) @@ -1251,7 +1240,7 @@ def _target_labels( """Display labels by (target_type, target_id): a key's (alias, masked name), a team's (alias, None), a user's (email, None).""" return MappingProxyType( - { # mutable-ok: MappingProxyType needs a dict to wrap + { key: value for key, value in chain( ((("key", row.token), (row.key_alias, row.key_name)) for row in key_rows), @@ -1287,33 +1276,21 @@ async def _with_target_labels( team_ids: Final = _target_ids_of(responses, "team") user_ids: Final = _target_ids_of(responses, "user") key_rows: Final = ( - await _verification_tokens(prisma_client).find_many( - where={"token": {"in": list(tokens)}} # mutable-ok: Prisma filter - ) - if tokens - else () + await _verification_tokens(prisma_client).find_many(where={"token": {"in": list(tokens)}}) if tokens else () ) team_rows: Final = ( - await _team_rows(prisma_client).find_many( - where={"team_id": {"in": list(team_ids)}} # mutable-ok: Prisma filter - ) - if team_ids - else () + await _team_rows(prisma_client).find_many(where={"team_id": {"in": list(team_ids)}}) if team_ids else () ) user_rows: Final = ( - await _user_rows(prisma_client).find_many( - where={"user_id": {"in": list(user_ids)}} # mutable-ok: Prisma filter - ) - if user_ids - else () + await _user_rows(prisma_client).find_many(where={"user_id": {"in": list(user_ids)}}) if user_ids else () ) labels: Final = _target_labels(key_rows or (), team_rows or (), user_rows or ()) return tuple( response.model_copy( - update={ # mutable-ok: pydantic update payload + update={ "targets": tuple( target.model_copy( - update={ # mutable-ok: pydantic update payload + update={ "target_alias": labels.get((target.target_type, target.target_id), _NO_TARGET_LABELS)[0], "key_name": labels.get((target.target_type, target.target_id), _NO_TARGET_LABELS)[1], } @@ -1337,7 +1314,7 @@ async def _shadow_eval_results( turns the router sent to X, did X beat the baseline" in reverse; the per-target slices answer "which target's traffic does the router suit". Reads are bounded by the job's own attempts (<= the sum of its targets' max_turns) via the job_id index.""" - leg_ids: Final = [leg.id for leg in legs] # mutable-ok: query param + leg_ids: Final = [leg.id for leg in legs] by_tier: Final = _ATTEMPT_AGG_ROWS.validate_python( await _query_raw(prisma_client, _ATTEMPT_AGG_BY_TIER_SQL, leg_ids) or () ) @@ -1351,10 +1328,8 @@ async def _shadow_eval_results( await _query_raw(prisma_client, _ATTEMPT_AGG_BY_LEG_SQL, leg_ids) or () ) verdicts_by_target: Final[Mapping[tuple[str, str], ShadowEvalSlice]] = MappingProxyType( - { # mutable-ok: MappingProxyType needs a dict to wrap - target_by_leg[slice.group]: slice.model_copy( - update={"group": target_by_leg[slice.group][1]} # mutable-ok: pydantic update payload - ) + { + target_by_leg[slice.group]: slice.model_copy(update={"group": target_by_leg[slice.group][1]}) for slice in _slices(by_leg) } ) @@ -1437,23 +1412,17 @@ async def start_shadow_eval( status_code=400, detail=f"Not a configured auto-router: {', '.join(repr(n) for n in unconfigured)}" ) token_rows: Final = ( - await _verification_tokens(prisma_client).find_many( - where={"token": {"in": list(data.api_key_ids)}} # mutable-ok: Prisma filter - ) + await _verification_tokens(prisma_client).find_many(where={"token": {"in": list(data.api_key_ids)}}) if data.api_key_ids else () ) team_rows: Final = ( - await _team_rows(prisma_client).find_many( - where={"team_id": {"in": list(data.team_ids)}} # mutable-ok: Prisma filter - ) + await _team_rows(prisma_client).find_many(where={"team_id": {"in": list(data.team_ids)}}) if data.team_ids else () ) user_rows: Final = ( - await _user_rows(prisma_client).find_many( - where={"user_id": {"in": list(data.user_ids)}} # mutable-ok: Prisma filter - ) + await _user_rows(prisma_client).find_many(where={"user_id": {"in": list(data.user_ids)}}) if data.user_ids else () ) @@ -1512,12 +1481,11 @@ async def start_shadow_eval( # deliberate. Sweep and claim filter on exact (target_type, id) pairs so a team id # that happens to equal a key hash never matches the other kind's slot. for target_type, ids in requested_by_type: - await prisma_client.db.execute_raw(_SWEEP_FINISHED_JOBS_SQL, list(ids), target_type) # mutable-ok: query param + await prisma_client.db.execute_raw(_SWEEP_FINISHED_JOBS_SQL, list(ids), target_type) claimed: Final = await _shadow_eval_jobs(prisma_client).find_many( - where={ # mutable-ok: Prisma filter - "OR": [ # mutable-ok: Prisma filter - {"target_type": target_type, "target_id": {"in": list(ids)}} # mutable-ok: Prisma filter - for target_type, ids in requested_by_type + where={ + "OR": [ + {"target_type": target_type, "target_id": {"in": list(ids)}} for target_type, ids in requested_by_type ], "direction": data.direction, "stopped_at": None, @@ -1535,12 +1503,12 @@ async def start_shadow_eval( now: Final = datetime.now(timezone.utc) group_id: Final = str(uuid4()) ends_at: Final = now + timedelta(days=data.duration_days) - shared_config: Final = { # mutable-ok: Prisma payload + shared_config: Final = { "group_id": group_id, # a pre-router_names pod samples router_name alone, so it must be a real arm "router_name": data.router_names[0], - "router_names": list(data.router_names), # mutable-ok: Prisma payload - "models": list(data.models), # mutable-ok: Prisma payload + "router_names": list(data.router_names), + "models": list(data.models), "direction": data.direction, "baseline_model": data.baseline_model, "judge_model": data.judge_model, @@ -1557,13 +1525,13 @@ async def start_shadow_eval( # (DATABASE_URL_READ_REPLICA) could otherwise return empty. leg_ids: Final = tuple(str(uuid4()) for _ in requested_targets) await _shadow_eval_jobs(prisma_client).create_many( - data=[ # mutable-ok: Prisma payload - { # mutable-ok: Prisma payload + data=[ + { **shared_config, "id": leg_id, "target_type": target_type, "target_id": target_id, - } # mutable-ok: Prisma payload + } for leg_id, (target_type, target_id) in zip(leg_ids, requested_targets) ] ) @@ -1582,7 +1550,7 @@ async def start_shadow_eval( # (null coverage). A failed seed degrades this job to exactly that, nothing worse. try: await _shadow_eval_funnel(prisma_client).create_many( - data=[{"job_id": leg_id} for leg_id in leg_ids], # mutable-ok: Prisma payload + data=[{"job_id": leg_id} for leg_id in leg_ids], skip_duplicates=True, ) except Exception as seed_err: # noqa: BLE001 # coverage is advisory; the job must still start @@ -1679,38 +1647,31 @@ async def get_shadow_eval_job( if prisma_client is None: raise HTTPException(status_code=500, detail=CommonProxyErrors.db_not_connected_error.value) legs: Final = _LEG_ROWS.validate_python( - await _shadow_eval_jobs(prisma_client).find_many( - where={"group_id": job_id} # mutable-ok: Prisma filter - ) - or () + await _shadow_eval_jobs(prisma_client).find_many(where={"group_id": job_id}) or () ) if not legs: raise HTTPException(status_code=404, detail=f"No shadow eval job {job_id}") - leg_ids: Final = [leg.id for leg in legs] # mutable-ok: query param + leg_ids: Final = [leg.id for leg in legs] totals: Final = _ATTEMPT_TOTALS_ROWS.validate_python( await _query_raw(prisma_client, _ATTEMPT_TOTALS_SQL, leg_ids) or () ) latest_error: Final = await _shadow_eval_attempts(prisma_client).find_first( - where={"job_id": {"in": leg_ids}, "outcome": "error"}, # mutable-ok: Prisma filter - order={"created_at": "desc"}, # mutable-ok: Prisma order + where={"job_id": {"in": leg_ids}, "outcome": "error"}, + order={"created_at": "desc"}, ) labeled: Final = await _with_target_labels( prisma_client, (_group_response(job_id, legs, await _leg_attempt_counts(prisma_client, legs)),) ) results, verdicts_by_target = await _shadow_eval_results(prisma_client, legs) return labeled[0].model_copy( - update={ # mutable-ok: pydantic update payload + update={ "judged_count": totals[0].judged_count if totals else 0, "error_count": totals[0].error_count if totals else 0, "judge_spend": round(totals[0].judge_spend, 6) if totals else 0.0, "last_error": latest_error.error if latest_error else None, "results": results, "targets": tuple( - target.model_copy( - update={ # mutable-ok: pydantic update payload - "verdicts": verdicts_by_target.get((target.target_type, target.target_id)) - } - ) + target.model_copy(update={"verdicts": verdicts_by_target.get((target.target_type, target.target_id))}) for target in labeled[0].targets ), } @@ -1744,10 +1705,7 @@ async def stop_shadow_eval_job( _STOP_JOB_SQL, job_id, operator, stamp.replace(tzinfo=None).isoformat() ) legs: Final = _LEG_ROWS.validate_python( - await _shadow_eval_jobs(prisma_client).find_many( - where={"group_id": job_id} # mutable-ok: Prisma filter - ) - or () + await _shadow_eval_jobs(prisma_client).find_many(where={"group_id": job_id}) or () ) if not legs: raise HTTPException(status_code=404, detail=f"No shadow eval job {job_id}") diff --git a/litellm/proxy/management_endpoints/budget_management_endpoints.py b/litellm/proxy/management_endpoints/budget_management_endpoints.py index 81a607aaa43..881d3309f6e 100644 --- a/litellm/proxy/management_endpoints/budget_management_endpoints.py +++ b/litellm/proxy/management_endpoints/budget_management_endpoints.py @@ -95,7 +95,7 @@ async def new_budget( budget_obj.budget_reset_at = get_budget_reset_time(budget_duration=budget_obj.budget_duration) budget_obj_json: Final = budget_obj.model_dump(exclude_none=True) - budget_obj_jsonified: Final[dict[str, object]] = jsonify_object(budget_obj_json) # mutable-ok: prisma create input + budget_obj_jsonified: Final[dict[str, object]] = jsonify_object(budget_obj_json) try: response: Final = await BudgetRepository(prisma_client).table.create( data={ diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index 44ed0017e42..6fa3592ffd6 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -260,7 +260,7 @@ def _entity_metadata( ) -> dict[str, object]: """The metadata payload for one entity breakdown bucket, empty when the caller passed none.""" stored: Final = entity_metadata_field.get(entity_id) if entity_metadata_field else None - return stored if stored is not None else {} # mutable-ok: payload pydantic validates into its own dict + return stored if stored is not None else {} def update_breakdown_metrics( @@ -1076,7 +1076,7 @@ def _aggregate_grouping_sets_records_sync( # bucket itself is still assigned unconditionally: a legacy row predating the # api_requests column backfills to all zeroes, and skipping those would drop a # provider the base build reported. - provider_metrics = metrics.model_copy(update={"flat_cost": 0.0}) # mutable-ok: pydantic update payload + provider_metrics = metrics.model_copy(update={"flat_cost": 0.0}) provider = record.custom_llm_provider or "unknown" assign_metric_with_metadata(breakdown.providers, provider, provider_metrics) elif level == _GROUP_DATE_PROVIDER_API_KEY: @@ -1265,10 +1265,10 @@ def _fold_entity_rollups_sync( results: Sequence[DailySpendData], entity_rows: Sequence[_EntityRollupRow], api_key_metadata: Mapping[str, _KeyMetadataDict], - entity_metadata_field: Mapping[str, dict[str, object]] | None, # mutable-ok: shared field shape + entity_metadata_field: Mapping[str, dict[str, object]] | None, ) -> None: """Write breakdown.entities onto the already-built per-day results.""" - by_date: Final = {day.date.strftime("%Y-%m-%d"): day for day in results} # mutable-ok: local fold index + by_date: Final = {day.date.strftime("%Y-%m-%d"): day for day in results} for row in entity_rows: day = by_date.get(row.date) @@ -1393,7 +1393,7 @@ async def get_daily_activity_aggregated( prisma_client, entity_api_keys, _spend_logs_window(frozenset(r.date for r in entity_records)) ) if entity_api_keys - else {} # mutable-ok: matches the helper's dict return + else {} ) await asyncio.to_thread( _fold_entity_rollups_sync, diff --git a/litellm/proxy/management_endpoints/config_override_endpoints.py b/litellm/proxy/management_endpoints/config_override_endpoints.py index 84593460704..5bd708b56e3 100644 --- a/litellm/proxy/management_endpoints/config_override_endpoints.py +++ b/litellm/proxy/management_endpoints/config_override_endpoints.py @@ -155,7 +155,7 @@ HASHICORP_SENSITIVE_FIELDS: Final[set[str]] = { # --- CyberArk Conjur constants --- -CYBERARK_ENV_VAR_MAPPING: Final[dict[str, str]] = { # mutable-ok: module-level env mapping +CYBERARK_ENV_VAR_MAPPING: Final[dict[str, str]] = { "cyberark_api_base": "CYBERARK_API_BASE", "cyberark_account": "CYBERARK_ACCOUNT", "cyberark_username": "CYBERARK_USERNAME", @@ -305,13 +305,13 @@ async def _persist_cyberark_config( encrypted_data: Final = proxy_config._encrypt_env_variables(dict(config_data)) # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage config_value: Final = safe_dumps(encrypted_data) await _config_overrides_table(prisma_client).upsert( - where={"config_type": "cyberark"}, # mutable-ok: prisma upsert payload - data={ # mutable-ok: prisma upsert payload - "create": { # mutable-ok: prisma upsert payload + where={"config_type": "cyberark"}, + data={ + "create": { "config_type": "cyberark", "config_value": config_value, }, - "update": { # mutable-ok: prisma upsert payload + "update": { "config_value": config_value, }, }, @@ -648,8 +648,8 @@ async def test_hashicorp_vault_connection( @router.post( "/config_overrides/cyberark", - tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata + tags=["Config Overrides"], + dependencies=[Depends(user_api_key_auth)], ) async def update_cyberark_config( config: CyberArkConfig, @@ -678,15 +678,13 @@ async def update_cyberark_config( detail=CommonProxyErrors.db_not_connected_error.value, ) - config_data: dict[str, object] = config.model_dump(exclude_none=True) # mutable-ok: merged # rebind-ok: stripped + config_data: dict[str, object] = config.model_dump(exclude_none=True) # rebind-ok: stripped # Merge ALL fields the user didn't send: try DB first, fall back to env vars. # Omitted field = keep existing; empty string = clear/remove the field. - existing_record: Final = await _config_overrides_table(prisma_client).find_unique( - where={"config_type": "cyberark"} # mutable-ok: prisma where clause - ) - existing_decrypted: dict[str, object] | None = None # mutable-ok: DB payload # rebind-ok: set when record exists - env_values: dict[str, str | None] = {} # mutable-ok: env snapshot # rebind-ok: populated when no DB record exists + existing_record: Final = await _config_overrides_table(prisma_client).find_unique(where={"config_type": "cyberark"}) + existing_decrypted: dict[str, object] | None = None # rebind-ok: set when record exists + env_values: dict[str, str | None] = {} # rebind-ok: populated when no DB record exists if existing_record is not None and existing_record.config_value is not None: existing_data: Final = _parse_config_value(existing_record.config_value) existing_decrypted = proxy_config._decrypt_db_variables(existing_data) # pyright: ignore[reportPrivateUsage] # rebind-ok: populated when a prior record decrypts @@ -699,7 +697,7 @@ async def update_cyberark_config( if field not in config_data and env_values.get(field): config_data[field] = env_values[field] - config_data = {k: v for k, v in config_data.items() if v != ""} # mutable-ok: dict # rebind-ok: "" means clear + config_data = {k: v for k, v in config_data.items() if v != ""} # rebind-ok: "" means clear has_api_base: Final = bool(config_data.get("cyberark_api_base")) has_api_key_auth: Final = bool(config_data.get("cyberark_api_key")) @@ -755,7 +753,7 @@ async def update_cyberark_config( litellm_changed_by=litellm_changed_by, ) - return { # mutable-ok: JSON response payload + return { "message": "CyberArk configuration updated successfully", "status": "success", } @@ -763,8 +761,8 @@ async def update_cyberark_config( @router.get( "/config_overrides/cyberark", - tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata + tags=["Config Overrides"], + dependencies=[Depends(user_api_key_auth)], response_model=ConfigOverrideSettingsResponse, ) async def get_cyberark_config( @@ -794,9 +792,7 @@ async def get_cyberark_config( field_schema: Final = _build_field_schema(CyberArkConfig) - db_record: Final = await _config_overrides_table(prisma_client).find_unique( - where={"config_type": "cyberark"} - ) # mutable-ok: prisma where clause + db_record: Final = await _config_overrides_table(prisma_client).find_unique(where={"config_type": "cyberark"}) if db_record is not None and db_record.config_value is not None: config_data: Final = _parse_config_value(db_record.config_value) @@ -821,8 +817,8 @@ async def get_cyberark_config( @router.delete( "/config_overrides/cyberark", - tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata + tags=["Config Overrides"], + dependencies=[Depends(user_api_key_auth)], ) async def delete_cyberark_config( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection @@ -846,10 +842,8 @@ async def delete_cyberark_config( detail=CommonProxyErrors.db_not_connected_error.value, ) - existing_record: Final = await _config_overrides_table(prisma_client).find_unique( - where={"config_type": "cyberark"} # mutable-ok: prisma where clause - ) - before_config: dict[str, object] | None = None # mutable-ok: audit snapshot # rebind-ok: set when decrypts + existing_record: Final = await _config_overrides_table(prisma_client).find_unique(where={"config_type": "cyberark"}) + before_config: dict[str, object] | None = None # rebind-ok: set when decrypts if existing_record is not None and existing_record.config_value is not None: try: before_config = proxy_config._decrypt_db_variables(_parse_config_value(existing_record.config_value)) # pyright: ignore[reportPrivateUsage] # rebind-ok: populated when the prior record decrypts @@ -858,9 +852,7 @@ async def delete_cyberark_config( deleted = False # rebind-ok: set true once the DB row is removed try: - await _config_overrides_table(prisma_client).delete( - where={"config_type": "cyberark"} - ) # mutable-ok: prisma where clause + await _config_overrides_table(prisma_client).delete(where={"config_type": "cyberark"}) deleted = True # rebind-ok: set true once the DB row is removed except RecordNotFoundError: verbose_proxy_logger.debug("No existing CyberArk config record to delete") @@ -877,7 +869,7 @@ async def delete_cyberark_config( litellm_changed_by=litellm_changed_by, ) - return { # mutable-ok: JSON response payload + return { "message": "CyberArk configuration deleted successfully", "status": "success", } @@ -885,8 +877,8 @@ async def delete_cyberark_config( @router.post( "/config_overrides/cyberark/test_connection", - tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata + tags=["Config Overrides"], + dependencies=[Depends(user_api_key_auth)], ) async def test_cyberark_connection( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection @@ -921,7 +913,7 @@ async def test_cyberark_connection( try: async_client: Final = get_async_httpx_client( llm_provider=httpxSpecialProvider.SecretManager, - params={"ssl_verify": client.ssl_verify}, # mutable-ok: httpx client params + params={"ssl_verify": client.ssl_verify}, ) whoami_url: Final = f"{client.conjur_addr}/whoami" response: Final = await async_client.get(whoami_url, headers=headers) @@ -932,7 +924,7 @@ async def test_cyberark_connection( detail=f"CyberArk token validation failed: {e}", ) - return { # mutable-ok: JSON response payload + return { "status": "success", "message": f"Successfully connected to CyberArk Conjur at {client.conjur_addr}", } diff --git a/litellm/proxy/management_endpoints/cost_tracking_settings.py b/litellm/proxy/management_endpoints/cost_tracking_settings.py index dc0da63555f..81396e174b5 100644 --- a/litellm/proxy/management_endpoints/cost_tracking_settings.py +++ b/litellm/proxy/management_endpoints/cost_tracking_settings.py @@ -530,23 +530,19 @@ async def update_block_requests_for_models_without_pricing( if prisma_client is None: raise HTTPException( status_code=500, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": CommonProxyErrors.db_not_connected_error.value - }, + detail={"error": CommonProxyErrors.db_not_connected_error.value}, ) if store_model_in_db is not True: raise HTTPException( status_code=500, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": "Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature." - }, + detail={"error": "Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature."}, ) try: config = await proxy_config.get_config() if "litellm_settings" not in config: - config["litellm_settings"] = {} # mutable-ok: config is a plain-dict payload for save_config + config["litellm_settings"] = {} config["litellm_settings"]["block_requests_for_models_without_pricing"] = request.enabled await proxy_config.save_config(new_config=config) @@ -558,9 +554,7 @@ async def update_block_requests_for_models_without_pricing( verbose_proxy_logger.error("Error updating block_requests_for_models_without_pricing: %s", e) raise HTTPException( status_code=500, - detail={ # mutable-ok: HTTPException detail must be a plain mapping - "error": f"Failed to update setting: {e!s}" - }, + detail={"error": f"Failed to update setting: {e!s}"}, ) diff --git a/litellm/proxy/management_endpoints/gateway_request_endpoints.py b/litellm/proxy/management_endpoints/gateway_request_endpoints.py index 33c078274fb..898c801d347 100644 --- a/litellm/proxy/management_endpoints/gateway_request_endpoints.py +++ b/litellm/proxy/management_endpoints/gateway_request_endpoints.py @@ -93,7 +93,7 @@ def _fold_by_route(rows: Sequence[_AggregateRow]) -> tuple[GatewayRequestBreakdo @router.get( "/gateway/daily/activity", - tags=["Budget & Spend Tracking"], # mutable-ok: fastapi's decorator signature types tags as a list + tags=["Budget & Spend Tracking"], response_model=GatewayRequestActivityResponse, ) async def get_gateway_daily_activity( diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 95ccb7bbe0b..ed475473905 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -622,7 +622,7 @@ def raise_on_invalid_key_logging_config(metadata: Mapping[str, object] | None) - """ error: Final = logging_metadata_config_error(metadata) if error is not None: - raise HTTPException(status_code=400, detail={"error": error}) # mutable-ok: FastAPI detail contract + raise HTTPException(status_code=400, detail={"error": error}) def common_key_access_checks( @@ -5609,7 +5609,7 @@ def _advance_one_key_budget_window(window: Mapping[str, object]) -> Mapping[str, if not isinstance(duration, str) or not duration: return window new_reset_at: Final = datetime.now(timezone.utc) + timedelta(seconds=duration_in_seconds(duration)) - return { # mutable-ok: this is the JSON payload persisted to budget_limits' Json column, which requires a plain dict + return { **window, "reset_at": new_reset_at.isoformat(), } @@ -5643,9 +5643,9 @@ async def _reset_key_budget_windows( # prisma-client-py's typed update() takes plain dict literals for `where`/`data`; there is no # frozen-mapping equivalent to pass instead. - reset_payload: Final = {"budget_limits": json.dumps(reset_windows, default=str)} # mutable-ok: prisma data kwarg + reset_payload: Final = {"budget_limits": json.dumps(reset_windows, default=str)} await VerificationTokenRepository(prisma_client).table.update( - where={"token": hashed_api_key}, # mutable-ok: prisma where kwarg + where={"token": hashed_api_key}, data=reset_payload, ) diff --git a/litellm/proxy/management_endpoints/management_v1/budgets.py b/litellm/proxy/management_endpoints/management_v1/budgets.py index cc2fefc426f..005bdc18ec8 100644 --- a/litellm/proxy/management_endpoints/management_v1/budgets.py +++ b/litellm/proxy/management_endpoints/management_v1/budgets.py @@ -114,7 +114,7 @@ def _scope(caller: UserAPIKeyAuth) -> Scope: # budget_duration is deliberately absent from `sortable`: the column holds strings # like "7d" and "30d", so a lexicographic ORDER BY puts "30d" ahead of "7d". BUDGET_FILTERS: Final[Mapping[str, FilterSpec]] = MappingProxyType( - { # mutable-ok: an immutable mapping has no literal form; MappingProxyType freezes this one and it never escapes + { "budget_duration": FilterSpec(type=str, ops=frozenset(("in", "is_null"))), "max_budget": FilterSpec(type=float, ops=frozenset(("gte", "lte", "is_null"))), "created_at": FilterSpec(type=datetime, ops=frozenset(("gte", "lte"))), diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 4c97bbaf5de..6ba35eb3bb5 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -1682,7 +1682,7 @@ if MCP_AVAILABLE: if LitellmUserRoles.PROXY_ADMIN != user_api_key_dict.user_role: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict + detail={ "error": "User does not have permission to import mcp servers. You can only import mcp servers if you are a PROXY_ADMIN." }, ) @@ -1745,9 +1745,7 @@ if MCP_AVAILABLE: classified: Final = tuple(_classify(index, conversion) for index, conversion in enumerate(conversions)) outcomes: Final = tuple( - [ - await _create(entry) if isinstance(entry, ConvertedConnector) else entry for entry in classified - ] # mutable-ok: await is illegal in a generator expression here + [await _create(entry) if isinstance(entry, ConvertedConnector) else entry for entry in classified] ) imported: Final = tuple(entry for entry in outcomes if isinstance(entry, MCPConnectorImportResult)) @@ -2289,7 +2287,7 @@ if MCP_AVAILABLE: if binding is not None and binding.mode == "enforce": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ # mutable-ok: FastAPI exception detail requires a JSON-serializable dictionary + detail={ "error": "oauth_identity_binding_enforced", "error_description": ( "Direct credential storage is disabled for this server: its OAuth identity " @@ -2752,7 +2750,7 @@ if MCP_AVAILABLE: if not relay_eligible: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict + detail={ "error": ( "per_server_oauth_discovery is only supported for auth_type oauth2 with oauth2_flow " "authorization_code and without delegate_auth_to_upstream." diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 2234e825090..a924adc304b 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -2550,7 +2550,7 @@ async def update_useful_links( def _validated_labeled_tiers( - tier_labels: dict[ComplexityTier, str], # mutable-ok: Pydantic materializes JSON object fields as dicts + tier_labels: dict[ComplexityTier, str], ) -> tuple[tuple[ComplexityTier, str], ...]: """Validate tier labels once for both prompt-preview transports.""" try: @@ -2585,7 +2585,7 @@ class AutoRouterClassifierPromptPreviewRequest(BaseModel): which must not reach access logs through a URL.""" tier_definitions: tuple[TierDefinition, ...] | None = None - tier_labels: dict[ComplexityTier, str] | None = None # mutable-ok: FastAPI parses JSON object fields into dicts + tier_labels: dict[ComplexityTier, str] | None = None classification_rubric: ClassificationRubric | None = None context_window_size: Annotated[int, Field(ge=0)] = DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE classification_prompt: str | None = None @@ -2598,8 +2598,8 @@ class AutoRouterClassifierPromptPreviewRequest(BaseModel): @router.post( "/auto_router/classifier/default_prompt", description="Get the system prompt an auto-router's LLM classifier sends for an edited tier set", - tags=["model management"], # mutable-ok: fastapi's decorator signature types tags as a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: fastapi's decorator signature types dependencies as a list + tags=["model management"], + dependencies=[Depends(user_api_key_auth)], ) async def preview_auto_router_classifier_prompt( request: AutoRouterClassifierPromptPreviewRequest, @@ -2610,7 +2610,7 @@ async def preview_auto_router_classifier_prompt( Built by the same function the live classifier uses, so the preview cannot drift from what the router sends. Payload validity beyond a renderable definition stays the dry-run's job. """ - labeled_tiers: Final = _validated_labeled_tiers(request.tier_labels or {}) # mutable-ok: Pydantic field default + labeled_tiers: Final = _validated_labeled_tiers(request.tier_labels or {}) system_prompt: Final = ( custom_tier_classification_prompt( request.tier_definitions, @@ -2633,8 +2633,8 @@ async def preview_auto_router_classifier_prompt( @router.get( "/auto_router/classifier/default_prompt", description="Get the built-in system prompt used by an auto-router's LLM classifier", - tags=["model management"], # mutable-ok: fastapi's decorator signature types tags as a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: fastapi's decorator signature types dependencies as a list + tags=["model management"], + dependencies=[Depends(user_api_key_auth)], ) async def get_auto_router_classifier_default_prompt( context_window_size: int = DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE, diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index ceb67e3eee8..7afef8cc971 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -579,7 +579,6 @@ async def _users_named_by_member_value( subject: Final = value.strip() email: Final[_CaseInsensitiveMatch] = {"equals": subject, "mode": "insensitive"} rows: Final = await _table(UserRepository(prisma_client)).find_many( - # mutable-ok: the Prisma serializer requires concrete dicts and a concrete list where={"OR": [{"sso_user_id": subject}, {"user_email": email}]}, take=take, ) @@ -601,11 +600,11 @@ async def _accounts_named_by_member_value(value: str, prisma_client: PrismaClien email: Final[_CaseInsensitiveMatch] = {"equals": subject, "mode": "insensitive"} users: Final = _table(UserRepository(prisma_client)) rows: Final = await users.find_many( - where={ # mutable-ok: Prisma filter - "OR": [ # mutable-ok: Prisma filter - {"user_id": value}, # mutable-ok: Prisma filter - {"sso_user_id": subject}, # mutable-ok: Prisma filter - {"user_email": email}, # mutable-ok: Prisma filter + where={ + "OR": [ + {"user_id": value}, + {"sso_user_id": subject}, + {"user_email": email}, ], }, take=2, @@ -2918,7 +2917,7 @@ async def patch_group( if updated_team is None: raise HTTPException( status_code=404, - detail={"error": f"Group not found with ID: {group_id}"}, # mutable-ok: FastAPI detail contract + detail={"error": f"Group not found with ID: {group_id}"}, ) # Convert to SCIM format and return diff --git a/litellm/proxy/management_endpoints/team_callback_endpoints.py b/litellm/proxy/management_endpoints/team_callback_endpoints.py index 1932e89717b..bbb713a90e8 100644 --- a/litellm/proxy/management_endpoints/team_callback_endpoints.py +++ b/litellm/proxy/management_endpoints/team_callback_endpoints.py @@ -57,7 +57,7 @@ _CALLBACK_VARS_REDACTED: Final = "***REDACTED***" def _callback_config_error(message: str) -> HTTPException: - return HTTPException(status_code=400, detail={"error": message}) # mutable-ok: FastAPI detail contract + return HTTPException(status_code=400, detail={"error": message}) def _validate_team_callback(data: "AddTeamCallback") -> None: @@ -106,10 +106,9 @@ def _mask_sensitive_callback_vars(callbacks: TeamCallbackMetadata) -> None: classified as sensitive would give the caller something it cannot use and cannot tell apart from a real value. - Masking in place rather than rebuilding the mapping keeps this under the - LIT002 mutable-collection-construction budget. It is safe because the only - caller passes an object it just built from a decrypted deep copy of the - row, so nothing here is reachable from the team's stored metadata. + Masking in place is safe because the only caller passes an object it just + built from a decrypted deep copy of the row, so nothing here is reachable + from the team's stored metadata. """ if not callbacks.callback_vars: return @@ -230,7 +229,7 @@ def _callback_error(status_code: int, message: str) -> HTTPException: """Build the ``{"error": ...}`` failure body the team callback endpoints return.""" return HTTPException( status_code=status_code, - detail={"error": message}, # mutable-ok: the error response body is a JSON object + detail={"error": message}, ) @@ -355,9 +354,7 @@ async def add_team_callbacks( # the stored ones and the credentials are encrypted at rest. decrypted_logging: Final = decrypt_callback_vars(team_metadata).get("logging") stored_entries: Final = decrypted_logging if isinstance(decrypted_logging, list) else () - stored_entry_vars: Final = [ # mutable-ok: read-only input to the check, never stored - entry.get("callback_vars") or {} for entry in stored_entries - ] + stored_entry_vars: Final = [entry.get("callback_vars") or {} for entry in stored_entries] family_error: Final = cross_entry_family_error(data.callback_vars, stored_entry_vars) if family_error is not None: raise HTTPException( @@ -391,7 +388,7 @@ async def add_team_callbacks( # `object_permission` is included so `_refresh_cached_team` doesn't # write a cached team with the relation nulled out — see # team_model_add for the full rationale. - include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal + include={"object_permission": True}, ) if new_team_row is None: @@ -433,8 +430,8 @@ async def add_team_callbacks( @router.delete( "/team/{team_id:path}/callback/{callback_name}", - tags=["team management"], # mutable-ok: FastAPI's route decorator takes a list of tags - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI's route decorator takes a list of dependencies + tags=["team management"], + dependencies=[Depends(user_api_key_auth)], response_model=TeamCallbackDeleteResponse, ) @management_endpoint_wrapper @@ -505,22 +502,22 @@ async def delete_team_callback( registered_callbacks: Final = team_metadata.get("logging") entries: Final = registered_callbacks if isinstance(registered_callbacks, list) else () - remaining_callbacks: Final = [ # mutable-ok: metadata["logging"] is isinstance-checked for list downstream + remaining_callbacks: Final = [ entry for entry in entries if not (isinstance(entry, dict) and entry.get("callback_name") == callback_name) ] if len(remaining_callbacks) == len(entries): raise _callback_error(404, f"callback_name = {callback_name} is not registered for team_id = {team_id}.") - updated_metadata: Final = {**team_metadata, "logging": remaining_callbacks} # mutable-ok: persisted as JSON + updated_metadata: Final = {**team_metadata, "logging": remaining_callbacks} encrypted_metadata: Final = encrypt_callback_vars(updated_metadata) team_metadata_json: Final = json.dumps(encrypted_metadata) updated_team: Final = await TeamRepository(prisma_client).table.update( - where={"team_id": team_id}, # mutable-ok: prisma where takes a dict literal - data={"metadata": team_metadata_json}, # mutable-ok: prisma data takes a dict literal + where={"team_id": team_id}, + data={"metadata": team_metadata_json}, # `object_permission` is included so `_refresh_cached_team` doesn't write a # cached team with the relation nulled out, see team_model_add for the rationale. - include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal + include={"object_permission": True}, ) if updated_team is None: @@ -648,7 +645,7 @@ async def disable_team_logging( team_metadata["callback_settings"] = team_callback_settings_obj.model_dump() # _get_dynamic_logging_metadata stops at metadata["logging"], where the API # and Admin UI register callbacks, without ever reading callback_settings. - team_metadata["logging"] = [] # mutable-ok: the disabled state is persisted as an empty JSON array + team_metadata["logging"] = [] team_metadata = encrypt_callback_vars(team_metadata) team_metadata_json: Final = json.dumps(team_metadata) @@ -659,7 +656,7 @@ async def disable_team_logging( # `object_permission` is included so `_refresh_cached_team` doesn't # write a cached team with the relation nulled out — see # team_model_add for the full rationale. - include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal + include={"object_permission": True}, ) if updated_team is None: diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 0b7f69bbb7f..1775bde0262 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -250,7 +250,7 @@ class _RawTeamRow(_TeamIdRow, _ModelDumpRow, _ObjectPermissionRow, _TeamBudgetRo @property def members_with_roles( self, - ) -> Sequence[dict[str, object]] | None: ... # mutable-ok: prisma deserializes this JSON column into plain dicts + ) -> Sequence[dict[str, object]] | None: ... @property def organization_id(self) -> str | None: ... @@ -2195,7 +2195,7 @@ async def update_team( if "metadata" in updated_kv: stored_metadata: Final[Mapping[str, JsonValue] | None] = ( - { # mutable-ok: the validator payload's isinstance guard requires a plain dict + { key: value for key, value in existing_team_row.metadata.items() if key not in TeamMemberBudgetHandler.SYSTEM_MANAGED_METADATA_KEYS @@ -2760,7 +2760,7 @@ def _resolve_member_identity(member: Member, updated_users: Sequence[LiteLLM_Use None, ) return member.model_copy( - update={ # mutable-ok: pydantic update payload + update={ "user_id": resolved_user_id, "user_email": resolved_user_email, } @@ -2879,11 +2879,7 @@ async def _resolve_existing_member_user_ids( return frozenset() found: Final = await _user_id_rows_db(UserRepository(prisma_client)).find_many( - where={ # mutable-ok: Prisma query filters are dict-shaped - "user_id": { # mutable-ok: Prisma query filters are dict-shaped - "in": sorted(requested_user_ids) - } - } + where={"user_id": {"in": sorted(requested_user_ids)}} ) return frozenset(user.user_id for user in found or () if user.user_id is not None) @@ -2937,7 +2933,7 @@ def _validate_member_user_id_provisioning( remaining: Final = len(unknown_user_ids) - _MAX_REPORTED_UNKNOWN_USER_IDS raise HTTPException( status_code=403, - detail={ # mutable-ok: HTTPException detail must be a plain mapping to keep this route's {"error": ...} response shape + detail={ "error": ( "Only proxy admins can add a user_id that does not exist yet: {}{}. " "Add the member by user_email to invite a new user, or ask a proxy admin " @@ -2953,11 +2949,7 @@ def _members_audit_value(members: Sequence[Member]) -> str: The audit-log columns hold a JSON object, so the member list is nested under a key rather than serialized as a top-level array. """ - return safe_dumps( - { # mutable-ok: the audit-log JSON column rejects a top-level array, so this value must be an object - "members_with_roles": tuple(member.model_dump() for member in members) - } - ) + return safe_dumps({"members_with_roles": tuple(member.model_dump() for member in members)}) async def _create_team_member_add_audit_logs( @@ -3644,7 +3636,7 @@ def _check_not_resetting_own_spend(user_id: str, user_api_key_dict: UserAPIKeyAu def _raise_reset_spend_error(status_code: int, message: str) -> NoReturn: - detail: Final = {"error": message} # mutable-ok: HTTPException.detail takes a dict + detail: Final = {"error": message} raise HTTPException(status_code=status_code, detail=detail) @@ -3678,7 +3670,7 @@ def _validate_team_member_reset_spend_value( @router.post( "/team/{team_id}/member/{user_id}/reset_spend", - tags=["team management"], # mutable-ok: FastAPI's `tags` param is typed as list[str], not Sequence + tags=["team management"], dependencies=(Depends(user_api_key_auth),), ) @management_endpoint_wrapper @@ -3714,12 +3706,10 @@ async def reset_team_member_spend_fn( await _verify_team_access(team_obj=team_obj, user_api_key_dict=user_api_key_dict) _check_not_resetting_own_spend(user_id=user_id, user_api_key_dict=user_api_key_dict) - membership_where: Final = { # mutable-ok: prisma client requires a plain dict where= argument - "user_id_team_id": {"user_id": user_id, "team_id": team_id} # mutable-ok: same prisma where= argument - } + membership_where: Final = {"user_id_team_id": {"user_id": user_id, "team_id": team_id}} _membership_row: Final = await _team_membership_db(prisma_client).find_unique( where=membership_where, - include={"litellm_budget_table": True}, # mutable-ok: prisma client requires a plain dict include= argument + include={"litellm_budget_table": True}, ) if _membership_row is None: _raise_reset_spend_error(status.HTTP_404_NOT_FOUND, f"User {user_id} is not a member of team {team_id}.") @@ -3730,7 +3720,7 @@ async def reset_team_member_spend_fn( await _team_membership_db(prisma_client).update( where=membership_where, - data={"spend": reset_to}, # mutable-ok: prisma client requires a plain dict data= argument + data={"spend": reset_to}, ) await invalidate_team_member_spend_state( @@ -3740,7 +3730,7 @@ async def reset_team_member_spend_fn( new_spend=reset_to, ) - return { # mutable-ok: matches this router's established untyped-response-dict convention + return { "team_id": team_id, "user_id": user_id, "spend": reset_to, @@ -4344,15 +4334,7 @@ async def _hydrate_member_user_details( """Attach ``user_alias`` and fill in a missing ``user_email`` from ``LiteLLM_UserTable`` in one query.""" user_ids: Final = frozenset(m.user_id for m in members if m.user_id is not None) user_rows: Final[Sequence[prisma_models.LiteLLM_UserTable]] = ( - await _user_db(prisma_client).find_many( - where={ # mutable-ok: Prisma query filters are dict-shaped - "user_id": { # mutable-ok: Prisma query filters are dict-shaped - "in": sorted(user_ids) - } - } - ) - if user_ids - else () + await _user_db(prisma_client).find_many(where={"user_id": {"in": sorted(user_ids)}}) if user_ids else () ) user_by_id: Final = MappingProxyType({u.user_id: u for u in user_rows}) @@ -4509,9 +4491,7 @@ async def team_info( prisma_client=prisma_client, members=resolved_team_info.members_with_roles, ) - hydrated_team_info: Final = resolved_team_info.model_copy( - update={"members_with_roles": hydrated_members} # mutable-ok: pydantic update payload - ) + hydrated_team_info: Final = resolved_team_info.model_copy(update={"members_with_roles": hydrated_members}) response_object: Final = TeamInfoResponseObject( team_id=team_id, @@ -4764,7 +4744,7 @@ async def unblock_team( @router.get( "/team/metadata_schema", - tags=["team management"], # mutable-ok: fastapi's decorator signature types tags as a list + tags=["team management"], dependencies=(Depends(user_api_key_auth),), response_model=TeamMetadataSchemaResponse, ) @@ -6014,13 +5994,13 @@ async def _append_permissions_to_all_teams(prisma_client: PrismaClient, permissi def _daily_activity_error(*, status_code: int, message: str) -> HTTPException: """Single construction site for the `{"error": ...}` detail shape the /team/daily/activity endpoints have always returned.""" - return HTTPException(status_code=status_code, detail={"error": message}) # mutable-ok: FastAPI JSON detail + return HTTPException(status_code=status_code, detail={"error": message}) class _TeamDailyActivityScope(NamedTuple): team_ids: list[str] | None # mutable-ok: downstream daily-activity signatures take str | list unions exclude_team_ids: list[str] | None # mutable-ok: downstream daily-activity signatures take str | list unions - team_alias_metadata: dict[str, dict[str, object]] # mutable-ok: entity_metadata_field shape + team_alias_metadata: dict[str, dict[str, object]] api_key_filter: str | list[str] | None # mutable-ok: downstream daily-activity signatures take str | list unions @@ -6323,7 +6303,7 @@ class _TeamUserSpendDbRow(TypedDict): @router.get( "/team/spend/by_user", response_model=TeamUserSpendResponse, - tags=["team management"], # mutable-ok: fastapi route tags must be a list + tags=["team management"], ) async def get_team_spend_by_user( user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 1ba90725eff..4fce712e081 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -1528,9 +1528,7 @@ async def get_generic_sso_response( if generic_include_token_claims else response ) - received_response = { # mutable-ok: preserve the existing dict return contract - key: value for key, value in claims.items() if key not in _OAUTH_TOKEN_FIELDS - } + received_response = {key: value for key, value in claims.items() if key not in _OAUTH_TOKEN_FIELDS} return generic_response_convertor( response=claims, jwt_handler=jwt_handler, @@ -1671,7 +1669,7 @@ async def get_generic_sso_response( return result or {}, received_response, access_token_payload, sso_assertion -RetentionCheck: TypeAlias = Callable[[], Awaitable[bool]] # mutable-ok: Callable parameter syntax +RetentionCheck: TypeAlias = Callable[[], Awaitable[bool]] async def warn_if_id_jag_assertion_uncaptured( diff --git a/litellm/proxy/management_helpers/object_permission_utils.py b/litellm/proxy/management_helpers/object_permission_utils.py index daab38d3662..5093d137877 100644 --- a/litellm/proxy/management_helpers/object_permission_utils.py +++ b/litellm/proxy/management_helpers/object_permission_utils.py @@ -344,7 +344,7 @@ async def reject_ambiguous_mcp_tool_permission_keys( return raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ # mutable-ok: HTTPException.detail has no immutable form; same shape as the sibling errors here + detail={ "error": ( f"Ambiguous mcp_tool_permissions key: {collisions}. " "Key tool permissions by server_id when servers share a name or alias." diff --git a/litellm/proxy/management_helpers/resource_display_names.py b/litellm/proxy/management_helpers/resource_display_names.py index 31b7b68d233..f3a97da1b12 100644 --- a/litellm/proxy/management_helpers/resource_display_names.py +++ b/litellm/proxy/management_helpers/resource_display_names.py @@ -20,7 +20,7 @@ async def mcp_server_display_names( if not server_ids: return MappingProxyType({}) wanted: Final = frozenset(server_ids) - where: Final = {"server_id": {"in": tuple(wanted)}} # mutable-ok: prisma where is a dict + where: Final = {"server_id": {"in": tuple(wanted)}} rows: Final = await MCPServerRepository(prisma_client).table.find_many(where=where) from_config: Final = { server_id: server.alias or server.server_name or server.name @@ -40,7 +40,7 @@ async def agent_display_names( if not agent_ids: return MappingProxyType({}) wanted: Final = frozenset(agent_ids) - where: Final = {"agent_id": {"in": tuple(wanted)}} # mutable-ok: prisma where is a dict + where: Final = {"agent_id": {"in": tuple(wanted)}} rows: Final = await AgentsRepository(prisma_client).table.find_many(where=where) from_registry: Final = { alias_id: agent.agent_name @@ -56,6 +56,6 @@ async def key_display_names(prisma_client: PrismaClient, tokens: Sequence[str]) """token hash -> key_alias for the keys that have one.""" if not tokens: return MappingProxyType({}) - where: Final = {"token": {"in": tuple(frozenset(tokens))}} # mutable-ok: prisma where is a dict + where: Final = {"token": {"in": tuple(frozenset(tokens))}} rows: Final = await VerificationTokenRepository(prisma_client).table.find_many(where=where) return MappingProxyType({row.token: row.key_alias for row in rows if row.key_alias}) diff --git a/litellm/proxy/management_helpers/team_metadata_validation.py b/litellm/proxy/management_helpers/team_metadata_validation.py index 7bc66c240c7..8755522e206 100644 --- a/litellm/proxy/management_helpers/team_metadata_validation.py +++ b/litellm/proxy/management_helpers/team_metadata_validation.py @@ -111,7 +111,7 @@ async def run_team_metadata_validation( if premium_user is not True: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ # mutable-ok: HTTPException.detail has no immutable form + detail={ "error": f"custom_team_metadata_validate is an Enterprise feature. {CommonProxyErrors.not_premium_user.value}" }, ) @@ -120,9 +120,7 @@ async def run_team_metadata_validation( ): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail={ # mutable-ok: HTTPException.detail has no immutable form - "error": "custom_team_metadata_validate must be an async function" - }, + detail={"error": "custom_team_metadata_validate must be an async function"}, ) try: @@ -131,15 +129,13 @@ async def run_team_metadata_validation( except Exception: # noqa: BLE001 # fail closed: any validator failure must block the team write raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail={"error": unavailable_message}, # mutable-ok: HTTPException.detail has no immutable form + detail={"error": unavailable_message}, ) if not result.valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ # mutable-ok: HTTPException.detail has no immutable form - "error": result.error_message or DEFAULT_TEAM_METADATA_VALIDATION_REJECTED_MESSAGE - }, + detail={"error": result.error_message or DEFAULT_TEAM_METADATA_VALIDATION_REJECTED_MESSAGE}, ) diff --git a/litellm/proxy/middleware/admission_control_middleware.py b/litellm/proxy/middleware/admission_control_middleware.py index e347428be83..c336b97349a 100644 --- a/litellm/proxy/middleware/admission_control_middleware.py +++ b/litellm/proxy/middleware/admission_control_middleware.py @@ -224,7 +224,7 @@ def create_prometheus_admission_metrics() -> AdmissionControlMetrics | None: "litellm_admission_queued_requests", "Number of requests queued by this worker", ), - rejected_counter=Counter( # mutable-ok: Prometheus requires runtime Counter construction + rejected_counter=Counter( "litellm_admission_rejected_requests_total", "Number of requests rejected by this worker", labelnames=("reason",), @@ -296,9 +296,9 @@ def _overloaded_response(state: AdmissionControlState) -> JSONResponse: stats: Final = state.get_stats() return JSONResponse( status_code=503, - headers={"retry-after": "1"}, # mutable-ok: Starlette expects a plain headers mapping - content={ # mutable-ok: Starlette serializes a plain response mapping - "error": { # mutable-ok: nested response mapping + headers={"retry-after": "1"}, + content={ + "error": { "message": ( f"Worker at capacity: {stats.admitted} in-flight, {stats.queued} queued requests. Retry later." ), diff --git a/litellm/proxy/openai_files_endpoints/batch_guardrails.py b/litellm/proxy/openai_files_endpoints/batch_guardrails.py index 53d51db2b7f..dd9f7ebe395 100644 --- a/litellm/proxy/openai_files_endpoints/batch_guardrails.py +++ b/litellm/proxy/openai_files_endpoints/batch_guardrails.py @@ -187,7 +187,7 @@ class _ParsedRecord: def _rejected(message: str) -> HTTPException: - return HTTPException(status_code=400, detail={"error": message}) # mutable-ok: FastAPI detail shape + return HTTPException(status_code=400, detail={"error": message}) def raise_public(failure: BatchScanFailure) -> NoReturn: @@ -355,9 +355,7 @@ def build_scan_metadata(request_metadata: Mapping[str, object]) -> Mapping[str, Passing the whole thing through would carry values that cannot be copied, such as the parent OTel span, and would hand every record proxy state it has no business seeing. """ - return MappingProxyType( - {key: value for key, value in request_metadata.items() if key in _SCAN_METADATA_KEYS} - ) # mutable-ok: MappingProxyType freezes the comprehension + return MappingProxyType({key: value for key, value in request_metadata.items() if key in _SCAN_METADATA_KEYS}) async def _scan_record( @@ -380,7 +378,7 @@ async def _scan_record( url=url if isinstance(url, str) else None, ) - scan_input: Final[dict[str, object]] = copy.deepcopy(body) # mutable-ok: pre_call_hook mutates the dict it is given + scan_input: Final[dict[str, object]] = copy.deepcopy(body) own_injected: Final = MappingProxyType({key: body[key] for key in _INJECTED_KEYS if key in body}) for injected in _INJECTED_KEYS: scan_input.pop(injected, None) @@ -390,12 +388,12 @@ async def _scan_record( # and `tags` are nested containers otherwise shared with the upload request and with every # other record in the window. The narrowing above already removed what cannot be copied. for injected in _SCAN_METADATA_BAGS: - scan_input[injected] = copy.deepcopy(dict(scan_metadata)) # mutable-ok: guardrails write here + scan_input[injected] = copy.deepcopy(dict(scan_metadata)) try: # The chain hands back the body it produced, which may be a replacement for the dict it was # given rather than that same dict mutated, so this is what gets compared. - scanned: Final[dict] = await proxy_logging_obj.pre_call_hook( # mutable-ok: the guardrails' own dict + scanned: Final[dict] = await proxy_logging_obj.pre_call_hook( user_api_key_dict=user_api_key_dict, data=scan_input, call_type=call_type, @@ -423,7 +421,7 @@ async def _scan_record( return _Redaction( line_number=record.line_number, custom_id=custom_id, - text=json.dumps({**record.payload, "body": scanned}), # mutable-ok: json.dumps needs a plain dict + text=json.dumps({**record.payload, "body": scanned}), ) @@ -546,7 +544,7 @@ def rewrite_batch_input_file(file_source: BinaryIO, result: BatchScanResult) -> """ redacted: Final = MappingProxyType( {change.line_number: change for change in result.changes if isinstance(change, RecordRedacted)} - ) # mutable-ok: MappingProxyType freezes the lookup table + ) dropped: Final = frozenset(change.line_number for change in result.changes if isinstance(change, RecordDropped)) output: Final = tempfile.SpooledTemporaryFile( # noqa: SIM115 # the caller uploads this handle diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 38a907892b4..26e0668fb47 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -501,7 +501,7 @@ def get_team_provider_credentials( def apply_team_provider_credentials( - data: dict, # mutable-ok: credentials are merged into the request payload in place, same contract as prepare_data_with_credentials + data: dict, llm_router: Optional["Router"], user_api_key_dict: "UserAPIKeyAuth", custom_llm_provider: str, @@ -1166,7 +1166,7 @@ async def map_raw_file_ids_to_unified( if not raw_file_ids or not prisma_client: return MappingProxyType({}) managed_files: Final = await ManagedFileRepository(prisma_client).table.find_many( - where={"flat_model_file_ids": {"hasSome": sorted(raw_file_ids)}} # mutable-ok: prisma where is a plain dict + where={"flat_model_file_ids": {"hasSome": sorted(raw_file_ids)}} ) return MappingProxyType( { diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 28a8bab1f24..1530675ff09 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -187,7 +187,7 @@ def get_passthrough_router_request_metadata(user_api_key_dict: UserAPIKeyAuth) - """ from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup - request_data: Final = {"litellm_metadata": {}} # mutable-ok: builder + litellm mutate this in place + request_data: Final = {"litellm_metadata": {}} LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata( data=request_data, user_api_key_dict=user_api_key_dict, @@ -555,7 +555,7 @@ async def milvus_proxy_route( detail=f"collectionName must be a string. Got {type(_raw_collection_name).__name__}", ) collection_name: str | None = _raw_collection_name # rebind-ok: locally scoped conversion - extra_headers = {} # mutable-ok: dict for extra headers; rebind-ok: reassigned later from credentials + extra_headers = {} base_target_url: str | None = None if not collection_name: raise HTTPException( @@ -1209,7 +1209,7 @@ def _resolve_comprehend_medical_region() -> str | None: @router.post( "/comprehendmedical/{operation}", - tags=["AWS Comprehend Medical Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list + tags=["AWS Comprehend Medical Pass-through", "pass-through"], ) async def comprehend_medical_proxy_route( operation: str, @@ -1286,7 +1286,7 @@ async def comprehend_medical_proxy_route( @router.post( "/comprehendmedical", - tags=["AWS Comprehend Medical Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list + tags=["AWS Comprehend Medical Pass-through", "pass-through"], ) async def comprehend_medical_sdk_proxy_route( request: Request, @@ -2443,7 +2443,7 @@ class _OpenAIWebsocketRelay(Protocol): *, websocket: WebSocket, target: str, - custom_headers: dict[str, str], # mutable-ok: the relay takes a plain dict of upstream headers + custom_headers: dict[str, str], user_api_key_dict: UserAPIKeyAuth, forward_headers: bool, endpoint: str, @@ -2533,9 +2533,7 @@ async def openai_websocket_proxy_route( ) query_string: Final = websocket.url.query wss_target: Final = f"{wss_base}{'&' if '?' in wss_base else '?'}{query_string}" if query_string else wss_base - custom_headers: Final = { # mutable-ok: websocket_passthrough_request requires a plain dict of upstream headers - "Authorization": f"Bearer {openai_api_key}" - } + custom_headers: Final = {"Authorization": f"Bearer {openai_api_key}"} await websocket.accept(subprotocol=negotiated_subprotocol) @@ -2987,8 +2985,8 @@ def create_generic_websocket_passthrough_endpoint( @router.api_route( "/gigachat/{endpoint:path}", - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route methods - tags=["Gigachat Pass-through", "pass-through"], # mutable-ok: FastAPI route tags + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], + tags=["Gigachat Pass-through", "pass-through"], ) async def gigachat_proxy_route( endpoint: str, @@ -3024,9 +3022,7 @@ async def gigachat_proxy_route( request_body, llm_router ) # rebind-ok: conditionally set to True elif any(word in endpoint for word in ("completions", "embeddings")): - raise HTTPException( - status_code=400, detail={"error": "Model is required in request body"} - ) # mutable-ok: HTTPException detail dict + raise HTTPException(status_code=400, detail={"error": "Model is required in request body"}) # If router model, use dedicated router passthrough handler # This uses the same common processing path as non-router models @@ -3127,9 +3123,7 @@ async def handle_gigachat_passthrough_router_model( is_streaming: Final = request_body.get("stream", False) # pyright: ignore[reportUnknownVariableType] # request_body is dict[Unknown, Unknown] - data: dict[str, Any] = await _read_request_body( - request=request - ) # mutable-ok: mutated in place by proxy pipeline; pyright: ignore[reportExplicitAny] # Any needed for proxy pipeline + data: dict[str, Any] = await _read_request_body(request=request) # Any needed for proxy pipeline if user_api_key_dict is not None: auth_metadata: Final = { metadata_key: value @@ -3160,7 +3154,7 @@ async def handle_gigachat_passthrough_router_model( data["json"] = request_body data["custom_llm_provider"] = "gigachat" - keys: Final = [ # mutable-ok: list of keys to remove from data + keys: Final = [ "gigachat_auth_url", "gigachat_access_token", "gigachat_scope", @@ -3172,7 +3166,7 @@ async def handle_gigachat_passthrough_router_model( client: Final = get_async_httpx_client( llm_provider=LlmProviders.GIGACHAT, - params={ # mutable-ok: httpx client params + params={ "timeout": httpx.Timeout(timeout=600.0, connect=5.0), }, ) diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/comprehend_medical_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/comprehend_medical_passthrough_logging_handler.py index 0d82cabdf36..7d289b80457 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/comprehend_medical_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/comprehend_medical_passthrough_logging_handler.py @@ -67,7 +67,7 @@ class ComprehendMedicalPassthroughLoggingHandler: ) model_name: Final = f"comprehendmedical/{operation}" - updated_kwargs: Final = { # mutable-ok: the logging pipeline requires a plain kwargs dict + updated_kwargs: Final = { **kwargs, "model": model_name, "custom_llm_provider": "comprehendmedical", diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py index 93fe3c5b31b..6aef278963d 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py @@ -118,10 +118,8 @@ def _content_parts(message: Mapping[str, object]) -> Sequence[object]: def _without_remote_high_detail_images(message: Mapping[str, object]) -> Mapping[str, object]: if not isinstance(message.get("content"), list): return message - kept_parts: Final = [ # mutable-ok: token_counter reads message content only when it is a list - part for part in _content_parts(message) if not _is_remote_high_detail_image(part) - ] - return {**message, "content": kept_parts} # mutable-ok: token_counter rejects any message that is not a dict + kept_parts: Final = [part for part in _content_parts(message) if not _is_remote_high_detail_image(part)] + return {**message, "content": kept_parts} def count_relayed_prompt_tokens(model: str, messages: Sequence[Mapping[str, object]] | None) -> int: @@ -130,9 +128,7 @@ def count_relayed_prompt_tokens(model: str, messages: Sequence[Mapping[str, obje remote_high_detail_images: Final = sum( 1 for message in messages for part in _content_parts(message) if _is_remote_high_detail_image(part) ) - local_messages: Final = [ # mutable-ok: token_counter takes a list of messages - _without_remote_high_detail_images(message) for message in messages - ] + local_messages: Final = [_without_remote_high_detail_images(message) for message in messages] return ( litellm.token_counter(model=model, messages=local_messages) + high_detail_image_token_upper_bound() * remote_high_detail_images 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..161ec0651b2 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 @@ -337,10 +337,10 @@ class VertexPassthroughLoggingHandler: @staticmethod def _handle_audio_predict_response( - json_response: dict, # mutable-ok: passthrough logging receives the decoded provider response dictionary + json_response: dict, logging_obj: LiteLLMLoggingObj, model: str, - kwargs: dict, # mutable-ok: passthrough logging enriches the shared callback metadata dictionary + kwargs: dict, ) -> PassThroughEndpointLoggingTypedDict: prediction_count: Final = VertexPassthroughLoggingHandler._get_audio_prediction_count( json_response=json_response @@ -367,12 +367,10 @@ class VertexPassthroughLoggingHandler: kwargs["model"] = model # rebind-ok: callback metadata records the resolved model kwargs["custom_llm_provider"] = "vertex_ai" # rebind-ok: callback metadata records the resolved provider - standard_pass_through_response_object: Final[ - StandardPassThroughResponseObject - ] = { # mutable-ok: callback contract requires a concrete response dictionary + standard_pass_through_response_object: Final[StandardPassThroughResponseObject] = { "response": json_response, } - return { # mutable-ok: passthrough logging contract requires a concrete result dictionary + return { "result": standard_pass_through_response_object, "kwargs": kwargs, } @@ -380,7 +378,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: dict, ) -> bool: return ( VertexPassthroughLoggingHandler._get_audio_prediction_count(json_response=json_response) > 0 @@ -389,7 +387,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: dict, ) -> int: predictions: Final = json_response.get("predictions") if not isinstance(predictions, list): diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index ea4ede7e513..c1d35a40dfc 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -771,7 +771,7 @@ def _build_passthrough_failure_request_payload( class _TeamCallbackWiring: success_callbacks: "list[str | Callable | CustomLogger] | None" = None # mutable-ok: Logging.__init__ arg failure_callbacks: "list[str | Callable | CustomLogger] | None" = None # mutable-ok: Logging.__init__ arg - logging_kwargs: dict[str, str | dict[str, str]] | None = None # mutable-ok: Logging.__init__ arg + logging_kwargs: dict[str, str | dict[str, str]] | None = None def _resolve_team_callback_wiring( @@ -816,16 +816,16 @@ def _resolve_team_callback_wiring( logging_kwargs: Final = ( None if not callback_vars - else { # mutable-ok: Logging arg + else { **callback_vars, TRUSTED_CALLBACK_VARS_FIELD: callback_vars, - "metadata": {}, # mutable-ok: Logging arg - "model_info": {}, # mutable-ok: Logging arg + "metadata": {}, + "model_info": {}, } ) return _TeamCallbackWiring( - success_callbacks=None if success_callbacks is None else [*success_callbacks], # mutable-ok: Logging arg - failure_callbacks=None if failure_callbacks is None else [*failure_callbacks], # mutable-ok: Logging arg + success_callbacks=None if success_callbacks is None else [*success_callbacks], + failure_callbacks=None if failure_callbacks is None else [*failure_callbacks], logging_kwargs=logging_kwargs, ) @@ -879,7 +879,7 @@ async def _relay_reporting_failures( stream: AsyncGenerator[bytes, None], upstream_status: int, user_api_key_dict: UserAPIKeyAuth, - request_payload: dict, # mutable-ok: post_call_failure_hook lifts fields onto request_data in place + request_payload: dict, ) -> AsyncGenerator[bytes, None]: from litellm.proxy.proxy_server import proxy_logging_obj @@ -2087,7 +2087,7 @@ def _rewrite_vertex_live_setup_model(text_data: str, setup_model_rewriter: Calla rewritten_model: Final = setup_model_rewriter(setup_model) if rewritten_model == setup_model: return text_data - return json.dumps({**message, "setup": {**setup, "model": rewritten_model}}) # mutable-ok: one-shot json payload + return json.dumps({**message, "setup": {**setup, "model": rewritten_model}}) def _truncated_close_reason(reason: str) -> str: diff --git a/litellm/proxy/policy_engine/pipeline_executor.py b/litellm/proxy/policy_engine/pipeline_executor.py index ad45781d5d2..f82cfc3436e 100644 --- a/litellm/proxy/policy_engine/pipeline_executor.py +++ b/litellm/proxy/policy_engine/pipeline_executor.py @@ -128,7 +128,7 @@ class _StreamRewriteObserver(CustomGuardrail): async def apply_guardrail( self, inputs: GenericGuardrailAPIInputs, - request_data: dict, # mutable-ok: matches CustomGuardrail.apply_guardrail + request_data: dict, input_type: Literal["request", "response"], logging_obj: "LiteLLMLoggingObj | None" = None, ) -> GenericGuardrailAPIInputs: @@ -155,7 +155,7 @@ class _ScannedTextRecorder(CustomGuardrail): async def apply_guardrail( self, inputs: GenericGuardrailAPIInputs, - request_data: dict, # mutable-ok: matches CustomGuardrail.apply_guardrail + request_data: dict, input_type: Literal["request", "response"], logging_obj: "LiteLLMLoggingObj | None" = None, ) -> GenericGuardrailAPIInputs: @@ -195,7 +195,7 @@ class _LegacyHookStreamAdapter(CustomGuardrail): async def apply_guardrail( self, inputs: GenericGuardrailAPIInputs, - request_data: dict, # mutable-ok: matches CustomGuardrail.apply_guardrail + request_data: dict, input_type: Literal["request", "response"], logging_obj: "LiteLLMLoggingObj | None" = None, ) -> GenericGuardrailAPIInputs: @@ -237,26 +237,24 @@ class _LegacyHookStreamAdapter(CustomGuardrail): def _prepare_hook_input( step: PipelineStep, callback: CustomGuardrail, - data: dict, # mutable-ok: same request-payload shape the hooks mutate - raw_request_snapshot: dict | None, # mutable-ok: same request-payload shape as data -) -> tuple[dict, bool]: # mutable-ok: returns that same request-payload dict + data: dict, + raw_request_snapshot: dict | None, +) -> tuple[dict, bool]: """Inject the step's guardrail name into metadata so should_run_guardrail() allows it, and pick the payload the step scans: a scan_raw_request step evaluates the pristine pre-pipeline snapshot instead of `data` (which earlier pass_data steps in this same pipeline may have already rewritten), same reason the normal sequential/parallel guardrail loops do this.""" if "metadata" not in data: - data["metadata"] = {} # mutable-ok: request metadata bucket, hooks mutate it - data["metadata"]["guardrails"] = [ - step.guardrail - ] # mutable-ok: guardrails list is part of the request-payload shape + data["metadata"] = {} + data["metadata"]["guardrails"] = [step.guardrail] scans_raw_request: Final = callback.scan_raw_request - hook_input: Final[dict] = ( # mutable-ok: same request-payload shape as data + hook_input: Final[dict] = ( independent_snapshot(raw_request_snapshot) if scans_raw_request and raw_request_snapshot is not None else data ) if hook_input is not data: - hook_input.setdefault("metadata", {})["guardrails"] = [step.guardrail] # mutable-ok: request metadata shape + hook_input.setdefault("metadata", {})["guardrails"] = [step.guardrail] return hook_input, scans_raw_request @@ -284,7 +282,7 @@ class PipelineExecutor: user_api_key_dict: Any, call_type: str, policy_name: str, - raw_request_snapshot: dict | None = None, # mutable-ok: same request-payload shape as data + raw_request_snapshot: dict | None = None, streaming_chunks: list[Any] | None = None, # mutable-ok: shared buffered-stream chunks, read per step endpoint_translation: "BaseTranslation | None" = None, ) -> PipelineExecutionResult: @@ -400,7 +398,7 @@ class PipelineExecutor: callback: CustomGuardrail, endpoint_translation: "BaseTranslation", streaming_chunks: list[object], # mutable-ok: shared buffered-stream chunks the translation rewrites in place - hook_input: dict[str, object], # mutable-ok: same request-payload shape as data + hook_input: dict[str, object], user_api_key_dict: "UserAPIKeyAuth", litellm_logging_obj: "LiteLLMLoggingObj | None", ) -> None: @@ -460,7 +458,7 @@ class PipelineExecutor: data: dict, user_api_key_dict: Any, call_type: str, - raw_request_snapshot: dict | None = None, # mutable-ok: same request-payload shape as data + raw_request_snapshot: dict | None = None, streaming_chunks: list[Any] | None = None, # mutable-ok: shared buffered-stream chunks, read per step endpoint_translation: "BaseTranslation | None" = None, ) -> tuple[ @@ -550,7 +548,7 @@ class PipelineExecutor: {"response": response}, None, None, - ) # mutable-ok: modified-data contract is a plain dict + ) return ("pass", response if isinstance(response, dict) else None, None, None) except Exception as e: @@ -597,22 +595,22 @@ class PipelineExecutor: def _allow_result( step_results: Sequence[PipelineStepResult], - working_data: dict, # mutable-ok: same request-payload shape as execute_steps' data - request_data: dict, # mutable-ok: same request-payload shape as execute_steps' data + working_data: dict, + request_data: dict, ) -> PipelineExecutionResult: """Build the terminal-allow result, propagating pipeline modifications without the per-step guardrail override.""" restored: Final = _restore_request_guardrails(working_data, request_data) return PipelineExecutionResult( terminal_action="allow", - step_results=list(step_results), # mutable-ok: PipelineExecutionResult field is a list + step_results=list(step_results), modified_data=restored if restored != request_data else None, ) def _restore_request_guardrails( - working_data: dict, # mutable-ok: same request-payload shape as execute_steps' data - request_data: dict, # mutable-ok: same request-payload shape as execute_steps' data -) -> dict: # mutable-ok: merged back into the request dict, which downstream code mutates + working_data: dict, + request_data: dict, +) -> dict: """ Restore the request's own metadata["guardrails"] activation list. @@ -625,13 +623,13 @@ def _restore_request_guardrails( return working_data request_metadata: Final = request_data.get("metadata") original_guardrails: Final = request_metadata.get("guardrails") if isinstance(request_metadata, dict) else None - stripped: Final = {k: v for k, v in working_metadata.items() if k != "guardrails"} # mutable-ok: request dict + stripped: Final = {k: v for k, v in working_metadata.items() if k != "guardrails"} if original_guardrails is not None: - restored: Final = {**stripped, "guardrails": original_guardrails} # mutable-ok: request dict - return {**working_data, "metadata": restored} # mutable-ok: request dict + restored: Final = {**stripped, "guardrails": original_guardrails} + return {**working_data, "metadata": restored} if not stripped and not isinstance(request_metadata, dict): - return {k: v for k, v in working_data.items() if k != "metadata"} # mutable-ok: request dict - return {**working_data, "metadata": stripped} # mutable-ok: request dict + return {k: v for k, v in working_data.items() if k != "metadata"} + return {**working_data, "metadata": stripped} _GUARDRAIL_INFORMATION_KEY: Final = "standard_logging_guardrail_information" @@ -644,7 +642,7 @@ def _recorded_guardrail_information(source: Mapping[str, object]) -> list[Standa def _append_guardrail_information( - request_data: dict[str, object], # mutable-ok: same request-payload shape as execute_steps' data + request_data: dict[str, object], entries: Sequence[StandardLoggingGuardrailInformation], ) -> None: if not entries: @@ -659,7 +657,7 @@ def _append_guardrail_information( def _carry_working_guardrail_information( working_data: Mapping[str, object], - request_data: dict[str, object], # mutable-ok: same request-payload shape as execute_steps' data + request_data: dict[str, object], ) -> None: recorded: Final = _recorded_guardrail_information(working_data) existing: Final = _recorded_guardrail_information(request_data) diff --git a/litellm/proxy/policy_engine/response_retrieval.py b/litellm/proxy/policy_engine/response_retrieval.py index d284c44397e..42ad02ad736 100644 --- a/litellm/proxy/policy_engine/response_retrieval.py +++ b/litellm/proxy/policy_engine/response_retrieval.py @@ -88,7 +88,7 @@ def _post_call_pipelines_for_context(context: PolicyMatchContext) -> tuple[Polic if not matches: return (), MappingProxyType({}) applied_policy_names: Final = PolicyMatcher.get_policies_with_matching_conditions( - policy_names=[match["policy_name"] for match in matches], # mutable-ok: the matcher takes a list + policy_names=[match["policy_name"] for match in matches], context=context, ) post_call_pipelines: Final = tuple( @@ -102,7 +102,7 @@ def _post_call_pipelines_for_context(context: PolicyMatchContext) -> tuple[Polic def attach_post_call_pipelines_to_retrieval( - data: dict[str, object], # mutable-ok: request-state dict the policy engine hooks all write in place + data: dict[str, object], user_api_key_dict: "UserAPIKeyAuth", llm_router: "Router | None", ) -> None: @@ -140,9 +140,7 @@ def attach_post_call_pipelines_to_retrieval( add_guardrail_to_applied_guardrails_header(request_data=data, guardrail_name=step.guardrail) add_policy_sources_to_metadata( request_data=data, - policy_sources={ # mutable-ok: add_policy_sources_to_metadata takes a dict - policy_name: policy_sources[policy_name] for policy_name, _pipeline in added - }, + policy_sources={policy_name: policy_sources[policy_name] for policy_name, _pipeline in added}, ) verbose_proxy_logger.debug( "Policy engine: attached post_call pipelines to the retrieval of background response %s (model group %s): %s", diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d9f8e04ebda..c35939c14ff 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3467,7 +3467,7 @@ async def increment_spend_counters_pipeline(pending: Sequence[PendingSpendIncrem ) return ttl: Final = redis_cache.get_ttl() - increment_list: Final = [ # mutable-ok: async_increment_pipeline signature requires list[RedisPipelineIncrementOperation] + increment_list: Final = [ RedisPipelineIncrementOperation(key=item.counter_key, increment_value=item.increment, ttl=ttl) for item in pending ] @@ -4651,7 +4651,7 @@ def validate_auto_router_capability_limits(model_list: Sequence[Mapping[str, obj raise ValueError(f"config.yaml model_list: {' '.join(violations)} {AUTO_ROUTER_LICENSE_REMEDY}") -def pin_complexity_router_model_id(model: dict) -> None: # mutable-ok: out-param, model_info is stamped in place +def pin_complexity_router_model_id(model: dict) -> None: """ Stamps `model_info.id` from the raw litellm_params before plugin resolution swaps dotted-path strings for live instances. `_delete_deployment` re-reads the raw config @@ -4664,7 +4664,7 @@ def pin_complexity_router_model_id(model: dict) -> None: # mutable-ok: out-para return model_info = model.get("model_info") if not isinstance(model_info, dict): - model_info = {} # mutable-ok: fresh model_info stamped onto the raw yaml model dict + model_info = {} model["model_info"] = model_info # rebind-ok: out-param, stamped in place if model_info.get("id") is None: model_info["id"] = litellm.Router.generate_model_id( @@ -4736,8 +4736,8 @@ class ProxyConfig: self.config: dict[str, Any] = {} self._last_semantic_filter_config: dict[str, object] | None = None self._last_hashicorp_vault_config: dict[str, object] | None = None - self._last_cyberark_config: dict[str, object] | None = None # mutable-ok: change-detection cache - self._cyberark_boot_env: dict[str, str | None] | None = None # mutable-ok: deployment env snapshot, set once + self._last_cyberark_config: dict[str, object] | None = None + self._cyberark_boot_env: dict[str, str | None] | None = None self.worker_registry: list[WorkerRegistryEntry] = [] self.config_sync_subscriber: ConfigSyncSubscriber | None = None self.auth_cache_invalidation_subscriber: AuthCacheInvalidationSubscriber | None = None @@ -4754,7 +4754,7 @@ class ProxyConfig: # precedence over stale DB-cached values for these specific keys # during periodic config reloads (_update_general_settings). self._yaml_general_settings_keys: set[str] = set() # mutable-ok: populated once at startup, read-only thereafter # fmt: skip - self._yaml_spend_log_cleanup_bounds: dict[str, object] = {} # mutable-ok: snapshot of YAML bounds at load time # fmt: skip + self._yaml_spend_log_cleanup_bounds: dict[str, object] = {} # fmt: skip def is_yaml(self, config_file_path: str) -> bool: if not os.path.isfile(config_file_path): @@ -5812,11 +5812,11 @@ class ProxyConfig: # Record which keys were explicitly set in the YAML config file. # These keys take precedence over DB-cached values during periodic # reloads (see _update_general_settings). - self._yaml_general_settings_keys = set(general_settings.keys()) # mutable-ok: snapshot of YAML keys at load time # fmt: skip + self._yaml_general_settings_keys = set(general_settings.keys()) # fmt: skip # The VALUES matter for the cleanup bounds, not just which keys were # set: clearing one from the dashboard has to fall back to what the # YAML declared, and a set of names cannot answer that. - self._yaml_spend_log_cleanup_bounds = { # mutable-ok: snapshot of YAML bounds at load time # fmt: skip + self._yaml_spend_log_cleanup_bounds = { # fmt: skip key: general_settings[key] for key in SPEND_LOG_CLEANUP_BOUND_SETTINGS if key in general_settings } @@ -7733,7 +7733,7 @@ class ProxyConfig: await call_with_db_reconnect_retry( prisma_client, lambda: ConfigOverridesRepository(prisma_client).table.find_unique( - where={"config_type": "cyberark"} # mutable-ok: prisma where clause + where={"config_type": "cyberark"} ), reason="init_cyberark_config_override_lookup_failure", ), @@ -9650,9 +9650,7 @@ class ProxyStartupEvent: try: config_table: Final = prisma_client.db.litellm_config - row: Final = await config_table.find_unique( - where={"param_name": TUNING_BASELINE_PARAM_NAME} # mutable-ok: Prisma rejects mappingproxy input - ) + row: Final = await config_table.find_unique(where={"param_name": TUNING_BASELINE_PARAM_NAME}) if row is not None: stored: Final = row.param_value decoded: Final = json.loads(stored) if isinstance(stored, str) else stored @@ -9661,21 +9659,19 @@ class ProxyStartupEvent: str(identity): str(fingerprint) for identity, fingerprint in (decoded.items() if isinstance(decoded, Mapping) else ()) } - ) # mutable-ok: MappingProxyType owns the completed immutable baseline + ) snapshot: Final = snapshot_tuning_baselines(deployments) try: await config_table.create( - data={ # mutable-ok: Prisma rejects mappingproxy input + data={ "param_name": TUNING_BASELINE_PARAM_NAME, - "param_value": json.dumps(dict(snapshot)), # mutable-ok: json only serializes concrete mappings + "param_value": json.dumps(dict(snapshot)), } ) verbose_proxy_logger.info("Recorded heuristic-v1 tuning baseline for %s auto-router(s)", len(snapshot)) return snapshot except UniqueViolationError: - competing_row: Final = await config_table.find_unique( - where={"param_name": TUNING_BASELINE_PARAM_NAME} # mutable-ok: Prisma rejects mappingproxy input - ) + competing_row: Final = await config_table.find_unique(where={"param_name": TUNING_BASELINE_PARAM_NAME}) competing_value: Final = None if competing_row is None else competing_row.param_value competing_decoded: Final = ( json.loads(competing_value) if isinstance(competing_value, str) else competing_value @@ -9687,7 +9683,7 @@ class ProxyStartupEvent: competing_decoded.items() if isinstance(competing_decoded, Mapping) else () ) } - ) # mutable-ok: MappingProxyType owns the completed immutable baseline + ) except Exception as e: # noqa: BLE001 # enforcement is skipped for this boot; refusing every tuned router on a DB blip is the one outcome the gate forbids verbose_proxy_logger.warning("Heuristic-v1 tuning baseline unavailable, gate not enforced this boot: %s", e) return None @@ -10923,7 +10919,7 @@ async def model_info( fallback_type=None, llm_router=llm_router, ) - return {**response, "id": internal_to_public.get(resolved_model_id, model_id)} # mutable-ok: response id differs + return {**response, "id": internal_to_public.get(resolved_model_id, model_id)} def _blocked_response_usage(original_response: object | None) -> "litellm.Usage": @@ -17443,7 +17439,7 @@ _GENERAL_SETTINGS_UI_LITELLM_FIELDS: Final[dict[str, GeneralSettingsUILiteLLMFie "tab": "prompt_caching", "description": "Empty uses Anthropic's 5m default. 1h suits long sessions but doubles the cache write cost.", }, - "budget_rollover": { # mutable-ok: registry literal, frozen with its siblings below + "budget_rollover": { "type": "Boolean", "description": ( "Carry spend beyond max_budget into the next window when budgets reset, instead of " @@ -18908,7 +18904,7 @@ async def _stream_mcp_asgi_response(handle_fn, scope: dict, receive) -> "Streami @app.api_route( "/mcp/proxy", - methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], # mutable-ok: FastAPI route methods + methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], ) async def proxy_mcp_route(request: Request) -> Response: """Serve the fixed three-tool MCP proxy surface.""" @@ -18923,7 +18919,7 @@ async def proxy_mcp_route(request: Request) -> Response: token: Final = _mcp_proxy_mode.set(True) try: - scope: Final = dict(request.scope) # mutable-ok: ASGI scope rewrite + scope: Final = dict(request.scope) scope["_original_path"] = scope.get("path", "") scope["path"] = BASE_MCP_ROUTE return await _stream_mcp_asgi_response(handle_streamable_http_mcp, scope, request.receive) diff --git a/litellm/proxy/public_endpoints/public_endpoints.py b/litellm/proxy/public_endpoints/public_endpoints.py index 94a59828451..89a2b88bf15 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -532,7 +532,7 @@ async def get_autorouter_presets( @router.get( "/public/autorouter_presets", - tags=["public", "auto router"], # mutable-ok: FastAPI route tags take a list + tags=["public", "auto router"], response_model=dict[str, AutoRouterPresetRecord], ) async def get_public_autorouter_presets() -> Mapping[str, AutoRouterPresetRecord]: diff --git a/litellm/proxy/public_endpoints/public_v1/model_hub.py b/litellm/proxy/public_endpoints/public_v1/model_hub.py index b0e688740e4..93971a84547 100644 --- a/litellm/proxy/public_endpoints/public_v1/model_hub.py +++ b/litellm/proxy/public_endpoints/public_v1/model_hub.py @@ -225,7 +225,7 @@ def _executor( @router.get( "/model_hub", - tags=["public", "model management"], # mutable-ok: fastapi types tags as list[str | Enum] + tags=["public", "model management"], dependencies=(Depends(user_api_key_auth),), response_model=ListResponse[ModelGroupInfoProxy], ) @@ -275,7 +275,7 @@ async def public_model_hub_list( @router.get( "/model_hub/{facet}", - tags=["public", "model management"], # mutable-ok: fastapi types tags as list[str | Enum] + tags=["public", "model management"], dependencies=(Depends(user_api_key_auth),), response_model=FacetListResponse, ) diff --git a/litellm/proxy/rag_endpoints/endpoints.py b/litellm/proxy/rag_endpoints/endpoints.py index d6a402e1860..821ddf9e037 100644 --- a/litellm/proxy/rag_endpoints/endpoints.py +++ b/litellm/proxy/rag_endpoints/endpoints.py @@ -740,7 +740,7 @@ async def rag_query( merged_retrieval_config: Final = { **retrieval_config, **store_data, - } # mutable-ok: litellm.aquery requires a plain dict payload + } # Add litellm data request_data: dict[str, object] = {} diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index 5907ffc64eb..85940af9ba9 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -76,33 +76,27 @@ def _convert_tool_envelope(obj: object, *, to_chat: bool) -> object: return obj nested: Final = obj.get(tool_type) nested_source: Final = nested if isinstance(nested, dict) else _EMPTY_TOOL_PAYLOAD - payload: Final = { # mutable-ok: tool entries are embedded verbatim in the JSON request body + payload: Final = { key: _convert_tool_payload_value(key, nested_source[key] if key in nested_source else obj[key], to_chat=to_chat) for key in payload_keys if key in nested_source or key in obj } if "name" not in payload: return obj - return {"type": tool_type, tool_type: payload} if to_chat else {"type": tool_type, **payload} # mutable-ok: same + return {"type": tool_type, tool_type: payload} if to_chat else {"type": tool_type, **payload} -def _normalize_tool_dialect( - data: dict, *, to_chat: bool -) -> dict: # mutable-ok: the parsed request body contract is a plain dict +def _normalize_tool_dialect(data: dict, *, to_chat: bool) -> dict: tools: Final = data.get("tools") tool_choice: Final = data.get("tool_choice") normalized_tools: Final = ( - [ - _convert_tool_envelope(tool, to_chat=to_chat) for tool in tools - ] # mutable-ok: body's tools stays a plain JSON list - if isinstance(tools, list) - else tools + [_convert_tool_envelope(tool, to_chat=to_chat) for tool in tools] if isinstance(tools, list) else tools ) normalized_choice: Final = _convert_tool_envelope(tool_choice, to_chat=to_chat) if normalized_tools == tools and normalized_choice == tool_choice: return data replaceable: Final = (("tools", normalized_tools), ("tool_choice", normalized_choice)) - return {**data, **{key: value for key, value in replaceable if key in data}} # mutable-ok: plain body dict + return {**data, **{key: value for key, value in replaceable if key in data}} def _is_chat_completions_body(data: Mapping[str, object]) -> bool: @@ -140,28 +134,26 @@ def _router_can_serve(model: str, llm_router: "Router | None") -> bool: return bool(llm_router.pattern_router.get_pattern(model)) -def _resolve_cursor_model_variant( - data: dict, llm_router: "Router | None" -) -> dict: # mutable-ok: the parsed request body contract is a plain dict +def _resolve_cursor_model_variant(data: dict, llm_router: "Router | None") -> dict: model: Final = data.get("model") if not isinstance(model, str) or _router_can_serve(model, llm_router): return data variant: Final = _parse_cursor_model_variant(model) if variant.base_model == model or not _router_can_serve(variant.base_model, llm_router): return data - resolved: Final = {**data, "model": variant.base_model} # mutable-ok: plain body dict + resolved: Final = {**data, "model": variant.base_model} if variant.reasoning_effort is None: return resolved if _is_chat_completions_body(data): if "reasoning_effort" in data: return resolved - return {**resolved, "reasoning_effort": variant.reasoning_effort} # mutable-ok: plain body dict + return {**resolved, "reasoning_effort": variant.reasoning_effort} reasoning: Final = data.get("reasoning") if isinstance(reasoning, dict): if reasoning.get("effort"): return resolved - return {**resolved, "reasoning": {**reasoning, "effort": variant.reasoning_effort}} # mutable-ok: same - return {**resolved, "reasoning": {"effort": variant.reasoning_effort}} # mutable-ok: plain body dict + return {**resolved, "reasoning": {**reasoning, "effort": variant.reasoning_effort}} + return {**resolved, "reasoning": {"effort": variant.reasoning_effort}} async def _resolve_cursor_model_variant_before_auth(request: Request) -> None: @@ -549,9 +541,7 @@ async def cursor_chat_completions( # Rebuild rather than pop: _read_request_body can return the request-scope # cached parsed-body dict itself, and removing keys from it corrupts the # cache's key snapshot so later readers get an empty body - body_without_stream_options: Final = { # mutable-ok: base_process_llm_request mutates the body dict in place - key: value for key, value in raw_body.items() if key != "stream_options" - } + body_without_stream_options: Final = {key: value for key, value in raw_body.items() if key != "stream_options"} data: Final = _normalize_tool_dialect(body_without_stream_options, to_chat=False) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 20b4708c193..b8f71067af2 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -190,7 +190,7 @@ class MockTestingParamsDisabledError(HTTPException): def __init__(self, params: tuple[str, ...]): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, - detail={ # mutable-ok: HTTPException.detail has no immutable form; same shape as the sibling errors here + detail={ "error": ( f"Mock testing request params are disabled on this proxy: {', '.join(params)}. " f"An admin can enable them by setting `general_settings.{MOCK_TESTING_CONFIG_KEY}: true` " @@ -461,7 +461,7 @@ async def route_request( async def _route_request_single_attempt( # noqa: ANN202 # returns unawaited provider coroutines; the inferred union keeps route_request's callers typed - data: dict, # mutable-ok: request body is the proxy-wide mutable dict contract shared with route_request + data: dict, llm_router: LitellmRouter | None, user_model: str | None, route_type: RouteType, diff --git a/litellm/proxy/spend_tracking/budget_reservation.py b/litellm/proxy/spend_tracking/budget_reservation.py index 6074a50a69b..7328bd2c078 100644 --- a/litellm/proxy/spend_tracking/budget_reservation.py +++ b/litellm/proxy/spend_tracking/budget_reservation.py @@ -396,7 +396,7 @@ async def invalidate_budget_reservation_counters( async def release_or_invalidate_budget_reservation( - budget_reservation: dict | None, # mutable-ok: stamps finalized on the caller's shared reservation dict + budget_reservation: dict | None, ) -> None: """Reconcile a still-open reservation on a terminal path that settles no cost. @@ -991,7 +991,7 @@ async def _release_applied_entries_best_effort( for entry in entries: try: await _set_reserved_entries_actual_cost( - entries=[entry], # mutable-ok: the reconcile takes the reservation's list of entries + entries=[entry], actual_cost=0.0, default_reserved_cost=default_reserved_cost, ) diff --git a/litellm/proxy/spend_tracking/key_metadata_recovery.py b/litellm/proxy/spend_tracking/key_metadata_recovery.py index 29688b61b3d..3d28b7b6c7a 100644 --- a/litellm/proxy/spend_tracking/key_metadata_recovery.py +++ b/litellm/proxy/spend_tracking/key_metadata_recovery.py @@ -156,7 +156,7 @@ async def _emails_for_user_ids( return _EMPTY_EMAILS users: Final = await _db_or_empty( lambda: UserRepository(prisma_client).table.find_many( - where={"user_id": {"in": list(user_ids)}}, # mutable-ok: Prisma find_many where= is a dict + where={"user_id": {"in": list(user_ids)}}, ), "Failed user_email recovery for %d user ids: %s", len(user_ids), diff --git a/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py b/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py index 0e6412a2c64..07a3b819c06 100644 --- a/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py +++ b/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py @@ -228,8 +228,8 @@ async def _upsert_ptu_daily_row( rename must not move the row. ``model_group`` carries the operator-facing name, which is outside the key and is what the usage views display. """ - where: Final = { # mutable-ok: prisma upsert filter payload - "team_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint": { # mutable-ok: prisma composite-key filter + where: Final = { + "team_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint": { "team_id": team_id, "date": date_str, "api_key": PTU_SENTINEL_API_KEY, @@ -242,8 +242,8 @@ async def _upsert_ptu_daily_row( now: Final = datetime.now(timezone.utc) await prisma_client.db.litellm_dailyteamspend.upsert( where=where, - data={ # mutable-ok: prisma upsert data payload - "create": { # mutable-ok: prisma create payload + data={ + "create": { "team_id": team_id, "date": date_str, "api_key": PTU_SENTINEL_API_KEY, @@ -254,7 +254,7 @@ async def _upsert_ptu_daily_row( "endpoint": "", "ptu_flat_cost": flat_cost, }, - "update": { # mutable-ok: prisma update payload + "update": { "model_group": model_name, "ptu_flat_cost": flat_cost, "updated_at": now, @@ -502,9 +502,9 @@ async def _existing_sentinel_keys( The row's ``model`` column holds the deployment id, so this is an exact identity and survives a rename. Nothing here reads the display name. """ - date_range: Final = {"gte": start.isoformat(), "lte": end.isoformat()} # mutable-ok: prisma range filter + date_range: Final = {"gte": start.isoformat(), "lte": end.isoformat()} rows: Final = await prisma_client.db.litellm_dailyteamspend.find_many( - where={"api_key": PTU_SENTINEL_API_KEY, "date": date_range} # mutable-ok: prisma find filter + where={"api_key": PTU_SENTINEL_API_KEY, "date": date_range} ) return frozenset( ( @@ -728,11 +728,11 @@ def _prune_filter(*, date_str: str, cutoff: datetime, chunk: "tuple[str, ...]") Returns a plain dict because the query builder serialises the mapping it is handed and rejects a read-only view of one. """ - return { # mutable-ok: prisma delete filter + return { "date": date_str, "api_key": PTU_SENTINEL_API_KEY, - "updated_at": {"lt": cutoff}, # mutable-ok: prisma comparison filter - "model": {"in": chunk}, # mutable-ok: prisma membership filter + "updated_at": {"lt": cutoff}, + "model": {"in": chunk}, } diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 0ec2788fbaa..47a6233a753 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -2954,17 +2954,17 @@ async def _fetch_session_representatives( ORDER BY {_SESSION_GROUP_KEY_SQL}, call_type IN {_MCP_CALL_TYPES_SQL}, "startTime" DESC ) AS session_representatives """ - rep_rows: Final[Sequence[dict[str, object]]] = await _query_raw( # mutable-ok: rows are enriched in place + rep_rows: Final[Sequence[dict[str, object]]] = await _query_raw( prisma_client, rep_query, *sql_params, - [session_key for session_key, _ in session_keys], # mutable-ok: prisma serializes array params from a list - [api_key for _, api_key in session_keys], # mutable-ok: prisma serializes array params from a list + [session_key for session_key, _ in session_keys], + [api_key for _, api_key in session_keys], ) - rep_by_key: Final[Mapping[tuple[str, str], dict[str, object]]] = MappingProxyType( # mutable-ok: same rows + rep_by_key: Final[Mapping[tuple[str, str], dict[str, object]]] = MappingProxyType( {(str(row["session_id"] or row["request_id"]), str(row["api_key"])): row for row in rep_rows} ) - return [rep_by_key[key] for key in session_keys if key in rep_by_key] # mutable-ok: rows are enriched in place + return [rep_by_key[key] for key in session_keys if key in rep_by_key] async def _count_grouped_sessions( @@ -3087,7 +3087,7 @@ async def _ui_session_grouped_spend_logs( session_keys=session_keys, ) if session_keys - else [] # mutable-ok: downstream enrichment mutates rows in place + else [] ) _hydrate_spend_log_metadata(data) @@ -3102,7 +3102,7 @@ async def _ui_session_grouped_spend_logs( enrich_session_counts=True, total_is_capped=total_is_capped, ) - return {**response, "next_session_cursor": next_cursor, "has_more": has_more} # mutable-ok: FastAPI response body + return {**response, "next_session_cursor": next_cursor, "has_more": has_more} class RequestResponsePayload(NamedTuple): @@ -3424,9 +3424,7 @@ async def view_spend_logs( start_date_iso: Final = start_date_obj.isoformat() end_date_iso: Final = end_date_obj.isoformat() - filter_query: Final[ - dict[str, object] - ] = { # mutable-ok: legacy filters are extended for optional parameters + filter_query: Final[dict[str, object]] = { "startTime": { "gte": start_date_iso, # Greater than or equal to Start Date "lte": end_date_iso, # Less than or equal to End Date diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index c12d071dd36..9640c530fbe 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -792,9 +792,7 @@ async def _validate_default_organization_exists(organization_id: str) -> None: if prisma_client is None: raise HTTPException( status_code=500, - detail={ # mutable-ok: HTTPException detail must be a plain dict for FastAPI JSON serialization - "error": "Database not connected. Please connect a database." - }, + detail={"error": "Database not connected. Please connect a database."}, ) organization_exists: Final = await OrganizationRepository(prisma_client).exists( @@ -803,7 +801,7 @@ async def _validate_default_organization_exists(organization_id: str) -> None: if not organization_exists: raise HTTPException( status_code=400, - detail={ # mutable-ok: HTTPException detail must be a plain dict for FastAPI JSON serialization + detail={ "error": f"Organization not found: {organization_id}. " "An organization must exist before it can be set as the default organization for new teams." }, @@ -1367,8 +1365,8 @@ async def update_mcp_semantic_filter_settings( @router.get( "/get/mcp_tool_search_settings", - tags=["Settings"], # mutable-ok: FastAPI's route decorator only accepts a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI's route decorator only accepts a list + tags=["Settings"], + dependencies=[Depends(user_api_key_auth)], response_model=MCPToolSearchSettingsResponse, ) async def get_mcp_tool_search_settings( @@ -1393,8 +1391,8 @@ async def get_mcp_tool_search_settings( @router.patch( "/update/mcp_tool_search_settings", - tags=["Settings"], # mutable-ok: FastAPI's route decorator only accepts a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI's route decorator only accepts a list + tags=["Settings"], + dependencies=[Depends(user_api_key_auth)], ) async def update_mcp_tool_search_settings( settings: MCPToolSearchSettings, diff --git a/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py b/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py index 893fda797cd..0ff61592d9f 100644 --- a/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py @@ -66,8 +66,8 @@ def parse_user_banner(raw_settings: object) -> UserBanner: @router.get( "/get/user_banner", - tags=["UI Settings"], # mutable-ok: FastAPI's route decorator only accepts a list - dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI's route decorator only accepts a list + tags=["UI Settings"], + dependencies=[Depends(user_api_key_auth)], response_model=UserBanner, ) async def get_user_banner() -> UserBanner: @@ -86,7 +86,7 @@ async def get_user_banner() -> UserBanner: @router.patch( "/update/user_banner", - tags=["UI Settings"], # mutable-ok: FastAPI's route decorator only accepts a list + tags=["UI Settings"], response_model=UpdateUserBannerResponse, ) async def update_user_banner( diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index c095586b6c9..8ac20929337 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -502,15 +502,11 @@ def _partition_post_call_callbacks() -> tuple[tuple[CustomGuardrail, ...], tuple return (guardrails, others) -def _merge_pipeline_metadata_bucket( - data: dict, bucket_key: str, modified_bucket_value: object -) -> None: # mutable-ok: request payload dict, written in place +def _merge_pipeline_metadata_bucket(data: dict, bucket_key: str, modified_bucket_value: object) -> None: if not isinstance(modified_bucket_value, dict): return modified_bucket: Final = cast("dict[str, object]", modified_bucket_value) # cast-ok: metadata buckets are str-keyed - surviving_writes: Final = { - key: value for key, value in modified_bucket.items() if key != "guardrails" - } # mutable-ok: merged into the live request metadata bucket in place + surviving_writes: Final = {key: value for key, value in modified_bucket.items() if key != "guardrails"} existing_bucket: Final = data.get(bucket_key) if isinstance(existing_bucket, dict): cast("dict[str, object]", existing_bucket).update(surviving_writes) # cast-ok: metadata buckets are str-keyed @@ -518,9 +514,7 @@ def _merge_pipeline_metadata_bucket( data[bucket_key] = surviving_writes -def _merge_pipeline_metadata_writes( - data: dict, modified_data: Mapping[str, object] -) -> None: # mutable-ok: request payload dict, written in place +def _merge_pipeline_metadata_writes(data: dict, modified_data: Mapping[str, object]) -> None: """ Copy metadata-bucket writes from a pipeline's working copy back onto the request. @@ -581,16 +575,14 @@ def _guardrails_run_standalone_pre_call(data: Mapping[str, object]) -> frozenset def _without_names( - bucket: dict[str, object], # mutable-ok: the applied_* header slots live in the request-state dict hooks write + bucket: dict[str, object], slot: str, names: frozenset[str], ) -> None: claimed: Final = bucket.get(slot) if not isinstance(claimed, list): return - remaining: Final = [ # mutable-ok: the slot stays a list, the shape every applied_* header writer appends to - name for name in claimed if name not in names - ] + remaining: Final = [name for name in claimed if name not in names] if remaining: bucket[slot] = remaining # rebind-ok: the slot lives in the shared request-state dict, rewritten in place else: @@ -598,7 +590,7 @@ def _without_names( def _withdraw_deferred_claims( - data: dict[str, object], # mutable-ok: same request-payload shape as post_call_success_hook's data + data: dict[str, object], deferred: Sequence[tuple[str, "GuardrailPipeline"]], ) -> None: outside_by_policy: Final = MappingProxyType( @@ -615,9 +607,7 @@ def _withdraw_deferred_claims( sources: Final = bucket.get("policy_sources") if not isinstance(sources, dict): return - remaining_sources: Final = { # mutable-ok: policy_sources stays a dict, the shape its writer updates in place - name: reason for name, reason in sources.items() if name not in withdrawn_policies - } + remaining_sources: Final = {name: reason for name, reason in sources.items() if name not in withdrawn_policies} if remaining_sources: bucket["policy_sources"] = remaining_sources else: @@ -625,7 +615,7 @@ def _withdraw_deferred_claims( def _defer_post_call_pipelines( - data: dict[str, object], # mutable-ok: same request-payload shape as post_call_success_hook's data + data: dict[str, object], response: ResponsesAPIResponse, ) -> None: deferred: Final = _post_call_pipelines(data) @@ -1731,11 +1721,11 @@ class ProxyLogging: async def _run_sequential_guardrail_callback( self, callback: CustomGuardrail, - data: dict, # mutable-ok: matches _process_guardrail_callback's own request-payload typing - raw_request_snapshot: dict | None, # mutable-ok: same request-payload shape as data + data: dict, + raw_request_snapshot: dict | None, user_api_key_dict: UserAPIKeyAuth, call_type: CallTypesLiteral, - ) -> dict: # mutable-ok: callers reassign the loop's own data from this return value + ) -> dict: """ Run one guardrail from the sequential pre_call loop and return what the rest of the loop should carry forward. @@ -1755,9 +1745,7 @@ class ProxyLogging: """ scans_raw_request: Final = callback.scan_raw_request should_use_raw_snapshot: Final = scans_raw_request and raw_request_snapshot is not None - input_data: Final = ( # mutable-ok: same request-payload shape as data - independent_snapshot(raw_request_snapshot) if should_use_raw_snapshot else data - ) + input_data: Final = independent_snapshot(raw_request_snapshot) if should_use_raw_snapshot else data # _process_guardrail_callback always calls mark_pre_call_hook_ran on a # successful run, which unconditionally stamps bookkeeping metadata onto # the dict regardless of whether the guardrail's own hook mutated @@ -1767,9 +1755,7 @@ class ProxyLogging: # raw_request_snapshot itself) so the comparison isolates the guardrail's # own content mutation from this bookkeeping noise without risking a # premature marker write into shared state. - expected_if_unmutated: Final[dict | None] = ( # mutable-ok: same request-payload shape as data - independent_snapshot(input_data) if scans_raw_request else None - ) + expected_if_unmutated: Final[dict | None] = independent_snapshot(input_data) if scans_raw_request else None if expected_if_unmutated is not None: callback.mark_pre_call_hook_ran(expected_if_unmutated) result: Final = await self._process_guardrail_callback( @@ -1915,9 +1901,9 @@ class ProxyLogging: user_api_key_dict: UserAPIKeyAuth, call_type: str, event_hook: str, - raw_request_snapshot: dict | None = None, # mutable-ok: same request-payload shape as data + raw_request_snapshot: dict | None = None, response: LLMResponseTypes | None = None, - ) -> tuple[dict, LLMResponseTypes | None]: # mutable-ok: returns the request-payload dict onward + ) -> tuple[dict, LLMResponseTypes | None]: """ Execute guardrail pipelines if any are configured for this request. @@ -1942,9 +1928,7 @@ class ProxyLogging: if pipeline.mode != event_hook: continue - step_input: dict = ( - {**data, "response": current_response} if current_response is not None else data - ) # mutable-ok: same request-payload shape as data + step_input: dict = {**data, "response": current_response} if current_response is not None else data result: PipelineExecutionResult = await PipelineExecutor.execute_steps( steps=pipeline.steps, @@ -2059,7 +2043,7 @@ class ProxyLogging: caps: Final = ProxyLogging._callback_capabilities() if caps.has_content_enforcer: return True - probe: Final = {"metadata": dict(request_metadata)} # mutable-ok: should_run_guardrail takes a dict + probe: Final = {"metadata": dict(request_metadata)} return any( isinstance(callback, CustomGuardrail) and callback.should_run_guardrail(data=probe, event_type=GuardrailEventHooks.pre_call) @@ -2149,9 +2133,7 @@ class ProxyLogging: isinstance(cb, CustomGuardrail) and cb.scan_raw_request for cb in ProxyLogging._callback_capabilities().resolved_callbacks ) - raw_request_snapshot: Final[dict | None] = ( # mutable-ok: same request-payload shape as data - independent_snapshot(data) if needs_raw_request_snapshot else None - ) + raw_request_snapshot: Final[dict | None] = independent_snapshot(data) if needs_raw_request_snapshot else None try: # Execute guardrail pipelines before the normal callback loop @@ -2280,7 +2262,7 @@ class ProxyLogging: self, guardrails: tuple[CustomGuardrail, ...], data: dict, - raw_request_snapshot: dict | None, # mutable-ok: same request-payload shape as data + raw_request_snapshot: dict | None, user_api_key_dict: UserAPIKeyAuth, call_type: CallTypesLiteral, ) -> None: @@ -2305,7 +2287,7 @@ class ProxyLogging: sequential guardrail already masked or rewrote. """ - def _input_for(callback: CustomGuardrail) -> dict: # mutable-ok: same request-payload shape as data + def _input_for(callback: CustomGuardrail) -> dict: if not callback.scan_raw_request or raw_request_snapshot is None: return data return independent_snapshot(raw_request_snapshot) @@ -3120,7 +3102,7 @@ class ProxyLogging: async def _run_post_call_pipelines( self, - data: dict[str, object], # mutable-ok: same request-payload shape as post_call_success_hook's data + data: dict[str, object], user_api_key_dict: UserAPIKeyAuth, response: LLMResponseTypes, ) -> LLMResponseTypes | None: @@ -3645,7 +3627,7 @@ class ProxyLogging: self, response: "AsyncGenerator[object, None]", user_api_key_dict: UserAPIKeyAuth, - request_data: dict, # mutable-ok: same request-payload shape the hooks mutate + request_data: dict, pipelines: "tuple[tuple[str, GuardrailPipeline], ...]", translation: "tuple[str, BaseTranslation]", ) -> "AsyncGenerator[Any, None]": @@ -3994,9 +3976,7 @@ class PrismaClient: spend_logs_queue_monitor_task: "asyncio.Task[None] | None" = None tool_usage_transactions: list["ToolUsageTransaction"] = [] _tool_usage_transactions_lock = asyncio.Lock() - autorouter_turn_transactions: ClassVar[ - list["AutoRouterTurnTransaction"] - ] = [] # mutable-ok: drained queue, mirrors tool_usage_transactions + autorouter_turn_transactions: ClassVar[list["AutoRouterTurnTransaction"]] = [] _autorouter_turn_transactions_lock = asyncio.Lock() # How long a health probe failure waits for an in-flight planned engine diff --git a/litellm/repositories/autorouter_session_repository.py b/litellm/repositories/autorouter_session_repository.py index d05ef9421ca..82e2c091728 100644 --- a/litellm/repositories/autorouter_session_repository.py +++ b/litellm/repositories/autorouter_session_repository.py @@ -28,7 +28,7 @@ class AutoRouterSessionRepository(BaseRepository[LiteLLM_AutoRouterSession]): row under the caller's api_key, so a key can only ever see what it wrote itself. """ record: Final = await self.table.find_first( - where={"api_key": api_key, "session_id": session_id}, # mutable-ok: Prisma where filter must be a dict - order={"last_turn_at": "desc"}, # mutable-ok: Prisma order clause must be a dict + where={"api_key": api_key, "session_id": session_id}, + order={"last_turn_at": "desc"}, ) return self._to_model(record) diff --git a/litellm/repositories/model_repository.py b/litellm/repositories/model_repository.py index d24eb8ffc62..a72bb414a7b 100644 --- a/litellm/repositories/model_repository.py +++ b/litellm/repositories/model_repository.py @@ -107,9 +107,7 @@ class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]): async def find_all_except(self, model_id: str) -> Sequence[LiteLLM_ProxyModelTable]: """Find every model except the row currently being updated.""" - records: Final = await self.table.find_many( - where={"model_id": {"not": model_id}} # mutable-ok: Prisma requires plain dicts for query serialization - ) + records: Final = await self.table.find_many(where={"model_id": {"not": model_id}}) return tuple(self._to_model_list(records)) async def find_by_team_id(self, team_id: str) -> list[LiteLLM_ProxyModelTable]: diff --git a/litellm/repositories/unit_of_work.py b/litellm/repositories/unit_of_work.py index a497d0580db..87bea018979 100644 --- a/litellm/repositories/unit_of_work.py +++ b/litellm/repositories/unit_of_work.py @@ -25,12 +25,8 @@ from litellm.repositories.prisma_protocols import BatchTable, PrismaBatch def _spend_reset_data(budget_reset_at: datetime | None, spend_decrement: float | None) -> Mapping[str, object]: - spend: Final[object] = ( - {"decrement": spend_decrement} # mutable-ok: prisma update payload must be a dict - if spend_decrement is not None - else 0 - ) - return {"spend": spend, "budget_reset_at": budget_reset_at} # mutable-ok: prisma update payload must be a dict + spend: Final[object] = {"decrement": spend_decrement} if spend_decrement is not None else 0 + return {"spend": spend, "budget_reset_at": budget_reset_at} @dataclass(frozen=True, slots=True) @@ -41,7 +37,7 @@ class KeySpendResetWrites: self, token: str, budget_reset_at: datetime | None, spend_decrement: float | None = None ) -> None: self.table.update( - where={"token": token}, # mutable-ok: prisma where filter must be a dict + where={"token": token}, data=_spend_reset_data(budget_reset_at, spend_decrement), ) @@ -54,7 +50,7 @@ class UserSpendResetWrites: self, user_id: str, budget_reset_at: datetime | None, spend_decrement: float | None = None ) -> None: self.table.update( - where={"user_id": user_id}, # mutable-ok: prisma where filter must be a dict + where={"user_id": user_id}, data=_spend_reset_data(budget_reset_at, spend_decrement), ) @@ -67,7 +63,7 @@ class TeamSpendResetWrites: self, team_id: str, budget_reset_at: datetime | None, spend_decrement: float | None = None ) -> None: self.table.update( - where={"team_id": team_id}, # mutable-ok: prisma where filter must be a dict + where={"team_id": team_id}, data=_spend_reset_data(budget_reset_at, spend_decrement), ) @@ -84,7 +80,7 @@ class LinkedSpendResetWrites: cascade's read and its commit survives the reset instead of being erased.""" self.table.update_many( where=where, - data={"spend": {"decrement": amount}}, # mutable-ok: prisma update payload must be a dict + data={"spend": {"decrement": amount}}, ) diff --git a/litellm/repositories/user_banner_repository.py b/litellm/repositories/user_banner_repository.py index c1ed977e048..4e113dff988 100644 --- a/litellm/repositories/user_banner_repository.py +++ b/litellm/repositories/user_banner_repository.py @@ -12,14 +12,12 @@ class UserBannerRepository(PrismaTableRepository["prisma_models.LiteLLM_UISettin table_name = "litellm_uisettings" async def get_raw_settings(self) -> object: - db_record: Final = await self.table.find_unique( - where={"id": USER_BANNER_ROW_ID} # mutable-ok: prisma filters are plain dicts - ) + db_record: Final = await self.table.find_unique(where={"id": USER_BANNER_ROW_ID}) return db_record.ui_settings if db_record is not None else None async def upsert_settings(self, payload: str) -> None: - row: Final = {"id": USER_BANNER_ROW_ID, "ui_settings": payload} # mutable-ok: prisma rows are plain dicts + row: Final = {"id": USER_BANNER_ROW_ID, "ui_settings": payload} await self.table.upsert( - where={"id": USER_BANNER_ROW_ID}, # mutable-ok: prisma filters are plain dicts - data={"create": row, "update": {"ui_settings": payload}}, # mutable-ok: prisma payloads are plain dicts + where={"id": USER_BANNER_ROW_ID}, + data={"create": row, "update": {"ui_settings": payload}}, ) diff --git a/litellm/repositories/user_repository.py b/litellm/repositories/user_repository.py index 87eb45f262d..c6ea4fde22f 100644 --- a/litellm/repositories/user_repository.py +++ b/litellm/repositories/user_repository.py @@ -229,8 +229,8 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]): Returns the number of rows updated: 0 means another writer already set an email. """ updated_count: Final[int] = await self.table.update_many( - where={"user_id": user_id, "user_email": None}, # mutable-ok: Prisma query filters are dict-shaped - data={"user_email": user_email}, # mutable-ok: Prisma update payloads are dict-shaped + where={"user_id": user_id, "user_email": None}, + data={"user_email": user_email}, ) return updated_count diff --git a/litellm/responses/litellm_completion_transformation/session_handler.py b/litellm/responses/litellm_completion_transformation/session_handler.py index f749977eb82..1d6a47d6365 100644 --- a/litellm/responses/litellm_completion_transformation/session_handler.py +++ b/litellm/responses/litellm_completion_transformation/session_handler.py @@ -122,7 +122,7 @@ class ResponsesSessionHandler: elif isinstance(_response_input_param, dict): response_input_param = cast( ResponseInputParam, - [_response_input_param], # mutable-ok: a lone input item still has to arrive as a list + [_response_input_param], ) if response_input_param: @@ -317,4 +317,4 @@ class ResponsesSessionHandler: return spend_logs verbose_proxy_logger.debug("Found no spend logs for previous response id %s", response_id) - return [] # mutable-ok: an empty result the caller only reads + return [] diff --git a/litellm/responses/litellm_completion_transformation/streaming_iterator.py b/litellm/responses/litellm_completion_transformation/streaming_iterator.py index 126b976e2c5..1306bd89a19 100644 --- a/litellm/responses/litellm_completion_transformation/streaming_iterator.py +++ b/litellm/responses/litellm_completion_transformation/streaming_iterator.py @@ -117,7 +117,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): self._pending_tool_events: list[BaseLiteLLMOpenAIResponseObject] = [] self._tool_output_index_by_call_id: dict[str, int] = {} self._tool_args_by_call_id: dict[str, str] = {} - self._tool_item_id_by_call_id: dict[str, str] = {} # mutable-ok: filled per call id as tool call events stream + self._tool_item_id_by_call_id: dict[str, str] = {} self._tool_call_id_by_index: dict[int, str] = {} self._ambiguous_tool_call_indexes: set[int] = set() self._next_tool_output_index: int = 1 # output_index=0 reserved for the message item @@ -138,7 +138,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): self._namespace_tool_names = LiteLLMCompletionResponsesConfig.namespace_tool_name_map( self.responses_api_request.get("tools") ) - self._web_search_calls: dict[str, object] = {} # mutable-ok: latest call by provider id + self._web_search_calls: dict[str, object] = {} self._queued_web_search_call_ids: set[str] = set() # mutable-ok: emitted call ids def _get_or_assign_tool_output_index(self, call_id: str) -> int: @@ -193,7 +193,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): output_index = self._get_or_assign_tool_output_index(call_id) self._web_search_calls[call_id] = item if status == "in_progress": - self._pending_tool_events = [ # mutable-ok: replaces speculative function events + self._pending_tool_events = [ event for event in self._pending_tool_events if getattr(event, "output_index", None) != output_index @@ -403,7 +403,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): type=ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED, output_index=output_index, item=BaseLiteLLMOpenAIResponseObject( - **{ # mutable-ok: BaseLiteLLM object accepts dynamic item fields + **{ "id": item.id, "type": item.type, "status": "in_progress", diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 64324c6cad8..183fe0838f0 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -173,7 +173,7 @@ class _ToolFunctionDefinition(TypedDict, total=False): def _attribute_fields(value: object) -> dict[str, object]: if not hasattr(value, "__dict__"): - return {} # mutable-ok: provider_specific_fields payload + return {} return dict(cast("Iterable[tuple[str, object]]", value)) # cast-ok: dict() raises on non-pair values, as before @@ -735,9 +735,7 @@ class LiteLLMCompletionResponsesConfig: if reasoning_text: message["reasoning_content"] = reasoning_text if thinking_blocks: - message["thinking_blocks"] = list( # mutable-ok: thinking_blocks is a list on the message contract - thinking_blocks - ) + message["thinking_blocks"] = list(thinking_blocks) return message @staticmethod @@ -820,9 +818,7 @@ class LiteLLMCompletionResponsesConfig: else: setattr(msg, "reasoning_content", combined) # noqa: B010 # attribute name is fixed, not dynamic if pending_blocks: - replayed: Final = list( # mutable-ok: thinking_blocks is a list on the message contract - pending_blocks + (_thinking_blocks(msg) or ()) - ) + replayed: Final = list(pending_blocks + (_thinking_blocks(msg) or ())) if isinstance(msg, dict): cast(dict[str, object], msg)["thinking_blocks"] = replayed # cast-ok: mutable reasoning carrier else: @@ -835,13 +831,13 @@ class LiteLLMCompletionResponsesConfig: | GenericChatCompletionMessage | ChatCompletionMessageToolCall | ChatCompletionResponseMessage - ] = [] # mutable-ok: accumulator + ] = [] pending: list[ # mutable-ok: accumulator # rebind-ok: accumulator tuple[ str | None, tuple[ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock, ...] | None, ] - ] = [] # mutable-ok: accumulator + ] = [] for msg in messages: if ( @@ -855,20 +851,16 @@ class LiteLLMCompletionResponsesConfig: if pending and _role(msg) == "assistant": _apply_pending(msg, pending) - pending = [] # mutable-ok: reset accumulator + pending = [] elif pending: # Not followed by an assistant message — keep the reasoning # standalone instead of dropping it. - merged.extend( # mutable-ok: append reasoning messages - [_standalone(text, blocks) for text, blocks in pending] # mutable-ok: append reasoning messages - ) - pending = [] # mutable-ok: reset accumulator + merged.extend([_standalone(text, blocks) for text, blocks in pending]) + pending = [] merged.append(msg) - merged.extend( # mutable-ok: append trailing reasoning - [_standalone(text, blocks) for text, blocks in pending] # mutable-ok: append trailing reasoning - ) + merged.extend([_standalone(text, blocks) for text, blocks in pending]) return merged @@ -904,7 +896,7 @@ class LiteLLMCompletionResponsesConfig: content: Final = ( new_content if not previous_content - else [ # mutable-ok: outbound chat content uses JSON arrays + else [ block for value in (previous_content, new_content) for block in ( @@ -914,7 +906,7 @@ class LiteLLMCompletionResponsesConfig: ) ] ) - merged: Final = { # mutable-ok: json.dumps rejects MappingProxyType in outbound chat messages + merged: Final = { **last_message, "content": content, } @@ -1345,7 +1337,7 @@ class LiteLLMCompletionResponsesConfig: """ if input_item.get("type") == "web_search_call": search: Final = ResponseFunctionWebSearch.model_validate(input_item) - return [ # mutable-ok: input conversion returns chat message lists + return [ GenericChatCompletionMessage( role="assistant", content="Hosted web search: " + search.model_dump_json(exclude_none=True), @@ -1385,8 +1377,8 @@ class LiteLLMCompletionResponsesConfig: or input_item.get("content") ) if inspectable is None: - return [] # mutable-ok: empty drop result - return [ # mutable-ok: single message result + return [] + return [ GenericChatCompletionMessage( role=_input_item_role(input_item), content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content( @@ -1401,8 +1393,8 @@ class LiteLLMCompletionResponsesConfig: input_item ) if not reasoning_text and not thinking_blocks: - return [] # mutable-ok: empty drop result - return [ # mutable-ok: single message result + return [] + return [ LiteLLMCompletionResponsesConfig._reasoning_only_assistant_message( reasoning_text=reasoning_text, thinking_blocks=thinking_blocks, @@ -1914,9 +1906,7 @@ class LiteLLMCompletionResponsesConfig: function: Final = ChatCompletionToolParamFunctionChunk( name=chat_tool_name, description=description, - parameters=dict( # mutable-ok: json.dumps rejects MappingProxyType in the outbound payload - normalized_parameters - ), + parameters=dict(normalized_parameters), strict=bool(namespace_tool.get("strict", False)), ) allowed_callers: Final = validated_allowed_callers(namespace_tool.get("allowed_callers")) @@ -1993,11 +1983,7 @@ class LiteLLMCompletionResponsesConfig: if tool_type == "function": typed_tool: Final = cast(FunctionToolParam, tool) raw_parameters: Final = typed_tool.get("parameters", {}) or {} - parameters: Final = ( - {**raw_parameters} # mutable-ok: json.dumps rejects MappingProxyType - if "type" in raw_parameters - else {**raw_parameters, "type": "object"} # mutable-ok: json.dumps rejects MappingProxyType - ) + parameters: Final = {**raw_parameters} if "type" in raw_parameters else {**raw_parameters, "type": "object"} chat_completion_tool: Final[dict[str, object]] = { "type": "function", "function": { @@ -2167,7 +2153,7 @@ class LiteLLMCompletionResponsesConfig: ) responses_tools: Final[ list[ResponseFunctionToolCall | ResponseFunctionWebSearch | CustomToolCallOutputItem] - ] = [] # mutable-ok: preserves provider tool-call order + ] = [] for tool in all_chat_completion_tools: if tool.type == "function": function_definition = tool.function @@ -2237,7 +2223,7 @@ class LiteLLMCompletionResponsesConfig: def _web_search_calls_by_call_id( chat_completion_response: ModelResponse, ) -> Mapping[str, ResponseFunctionWebSearch]: - calls: Final[dict[str, ResponseFunctionWebSearch]] = {} # mutable-ok: indexes provider-built calls + calls: Final[dict[str, ResponseFunctionWebSearch]] = {} for choice in chat_completion_response.choices: provider_fields = getattr(choice.message, "provider_specific_fields", None) if not isinstance(provider_fields, Mapping): diff --git a/litellm/responses/main.py b/litellm/responses/main.py index a68cd02e61b..cb10762051e 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -493,7 +493,7 @@ def _will_bridge_to_chat_completions( @contextmanager def _prompt_management_sees_a_provisional_message_list( - kwargs: dict[str, Any], # mutable-ok: the signal is read and popped out of the caller's own kwargs + kwargs: dict[str, Any], bridged: bool, ) -> Generator[None, None]: """Tell the cache-control hook that this layer's messages are not the ones sent upstream. diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 40ff88fc557..c2d77ba3d6a 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -164,7 +164,7 @@ def _log_background_task_failure(task: asyncio.Task[object], *, task_name: str) _ERROR_CODE_HTTP_STATUS: Final[Mapping[str, int]] = MappingProxyType( - { # mutable-ok: immediately frozen by MappingProxyType + { "server_error": 500, "rate_limit_exceeded": 429, "insufficient_quota": 429, @@ -275,9 +275,7 @@ class BaseResponsesAPIStreamingIterator: self._hidden_params["additional_headers"] = process_response_headers( self.response.headers or {} ) # GUARANTEE OPENAI HEADERS IN RESPONSE - self._raw_response_headers: Mapping[str, str] = MappingProxyType( - dict(self.response.headers or {}) # mutable-ok: immediately frozen by MappingProxyType - ) + self._raw_response_headers: Mapping[str, str] = MappingProxyType(dict(self.response.headers or {})) def _check_max_streaming_duration(self) -> None: """Raise litellm.Timeout if the stream has exceeded LITELLM_MAX_STREAMING_DURATION_SECONDS.""" @@ -499,9 +497,9 @@ class BaseResponsesAPIStreamingIterator: raw_headers: Final[Mapping[str, object]] = raw if isinstance(raw, Mapping) else EMPTY_MAPPING # rebuild by value and let existing keys win: sharing the source dicts would alias what the proxy # splats into the client's HTTP headers, and copying non-header keys would carry response_cost - target._hidden_params = { # mutable-ok: the cost calculator writes optional_params into _hidden_params - "additional_headers": {**headers}, # mutable-ok: fresh copy, logging callbacks may mutate it - "headers": {**raw_headers}, # mutable-ok: fresh copy, logging callbacks may mutate it + target._hidden_params = { + "additional_headers": {**headers}, + "headers": {**raw_headers}, **existing, } @@ -1494,9 +1492,7 @@ def _extract_frame_quota_estimate_inputs(msg_obj: Mapping[str, object]) -> tuple params: Final[Mapping[str, object]] = ( nested if _is_json_object(nested) and nested - else MappingProxyType( # mutable-ok: immediately frozen filtered frame - {k: v for k, v in msg_obj.items() if k != "type"} - ) + else MappingProxyType({k: v for k, v in msg_obj.items() if k != "type"}) ) text_parts: Final[list[str]] = [] # mutable-ok: local accumulator built in one pass, not shared pending: Final[list[object]] = [ # mutable-ok: explicit worklist avoids recursion @@ -2023,10 +2019,10 @@ class ResponsesWebSocketStreaming: except RateLimitError as e: try: await self.websocket.send_text( - json.dumps( # mutable-ok: WebSocket wire payload requires JSON objects - { # mutable-ok: WebSocket wire payload requires JSON objects + json.dumps( + { "type": "error", - "error": { # mutable-ok: nested WebSocket error object + "error": { "type": "rate_limit_exceeded", "message": str(e), }, diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index d63e3ddf0aa..bf8f5fbb716 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -40,7 +40,7 @@ def _is_object_sequence(value: object) -> TypeIs[Sequence[object]]: # guard-ok: def _is_object_dict( value: object, -) -> TypeIs[dict[str, object]]: # guard-ok: wire dicts have str keys # mutable-ok: callers rewrite ids in place +) -> TypeIs[dict[str, object]]: # guard-ok: wire dicts have str keys return isinstance(value, dict) @@ -61,7 +61,7 @@ def _is_chat_text_part(part: object) -> bool: def _as_input_text_part(part: object) -> object: if isinstance(part, dict) and part.get("type") == "text": - return {**part, "type": "input_text"} # mutable-ok: fresh part so the caller's block keeps its chat type + return {**part, "type": "input_text"} return part @@ -78,8 +78,8 @@ class ResponsesAPIRequestUtils: content: object = message.get("content") if not isinstance(content, list) or not any(_is_chat_text_part(part) for part in content): return message - shaped_content: Final = [_as_input_text_part(part) for part in content] # mutable-ok: Responses-shaped copy - return {**message, "content": shaped_content} # mutable-ok: copy, the hook's message stays untouched + shaped_content: Final = [_as_input_text_part(part) for part in content] + return {**message, "content": shaped_content} @staticmethod def responses_input_to_chat_messages( @@ -568,7 +568,7 @@ class ResponsesAPIRequestUtils: ) if not readable: return None - kept: Final[dict[str, object]] = { # mutable-ok: request item rebuilt without the undecryptable keys + kept: Final[dict[str, object]] = { key: value for key, value in reasoning.items() if key not in ("encrypted_content", "id") } return kept diff --git a/litellm/router.py b/litellm/router.py index 8865543badd..d845ca7c866 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -440,9 +440,7 @@ def _with_router_resolved_session_model(session: object, model_name: str) -> Map return _NO_SESSION_KWARGS if "model" not in typed_session: return _NO_SESSION_KWARGS - return MappingProxyType( - {"session": {**typed_session, "model": model_name}} # mutable-ok: callees deepcopy and JSON-dump session - ) + return MappingProxyType({"session": {**typed_session, "model": model_name}}) # Router._aanthropic_messages_streaming_iterator buffers lifecycle chunks @@ -558,9 +556,7 @@ class FallbackAwareAnthropicMessagesStream: self._async_generator = async_generator self._source_iterator = source_iterator self.fallback_headers_adopted = False - self._hidden_params = dict( # mutable-ok: mutated in place by merge_fallback_hidden_params - getattr(source_iterator, "_hidden_params", None) or {} - ) + self._hidden_params = dict(getattr(source_iterator, "_hidden_params", None) or {}) @property def has_buffered_provider_output(self) -> bool: @@ -594,12 +590,10 @@ class FallbackAwareAnthropicMessagesStream: existing_headers: Final = cast( # cast-ok: additional_headers is always a dict[str, object] when present "dict[str, object]", self._hidden_params.get("additional_headers") or {} ) - self._hidden_params = { # mutable-ok: matches _hidden_params' existing dict[str, object] shape + self._hidden_params = { **self._hidden_params, **fallback_hidden_params, - "additional_headers": dict( # mutable-ok: hidden params expect a writable header bag - replace_complexity_router_headers(existing_headers, fallback_headers) - ), + "additional_headers": dict(replace_complexity_router_headers(existing_headers, fallback_headers)), } @@ -611,7 +605,7 @@ class RoutingArgs(enum.Enum): # entries their deployments own. Weak so a router nothing references any more, such # as the per-request one built from a caller-supplied user_config, drops out on its # own rather than leaving entries behind that nothing can withdraw. -_live_routers: Final["weakref.WeakSet[Router]"] = weakref.WeakSet() # mutable-ok: identity set of live routers +_live_routers: Final["weakref.WeakSet[Router]"] = weakref.WeakSet() def _replay_live_router_model_cost() -> None: @@ -660,12 +654,12 @@ class FallbackAwareStreamWrapper(CustomStreamWrapper): self._response_headers = getattr(fallback_response, "_response_headers", None) fallback_hidden_params, fallback_headers = prepared_fallback_hidden_params if fallback_hidden_params: - self._hidden_params = { # mutable-ok: the rest of litellm writes into _hidden_params + self._hidden_params = { **fallback_hidden_params, # dict() because add_retry_fallback_headers mutates additional_headers in place - "additional_headers": dict(fallback_headers), # mutable-ok: see above + "additional_headers": dict(fallback_headers), } - self._base_hidden_params = { # mutable-ok: CustomStreamWrapper keeps this snapshot as a dict + self._base_hidden_params = { **self._hidden_params, "response_cost": None, } @@ -1515,7 +1509,7 @@ class Router: routing_group: Final = self.get_routing_group(model) if routing_group is None: return None - return [ # mutable-ok: matches _get_all_deployments' list contract expected by downstream filters + return [ deployment for member in routing_group.models for deployment in self._get_all_deployments(model_name=member, team_id=team_id) @@ -2319,7 +2313,7 @@ class Router: @staticmethod def _deployment_params_with_request_reasoning_override( deployment_params: Mapping[str, object], request_kwargs: Mapping[str, object] - ) -> dict[str, object]: # mutable-ok: litellm's request pipeline consumes a mutable kwargs mapping + ) -> dict[str, object]: """Return deployment params whose equivalent effort controls cannot outrank a request override. Providers expose the same setting through several native carriers. A request-level @@ -2327,7 +2321,7 @@ class Router: ``*.effort`` must not remain beside it and either win or trigger a conflicting-params 400. Every changed mapping is copied so the Router's shared deployment config stays immutable. """ - sanitized: Final = dict(deployment_params) # mutable-ok: request-local copy protects shared Router state + sanitized: Final = dict(deployment_params) if request_kwargs.get("reasoning_effort") is None: return sanitized @@ -2337,7 +2331,7 @@ class Router: extra_body: Final = sanitized.get("extra_body") if isinstance(extra_body, Mapping): - sanitized_extra_body: Final = dict(extra_body) # mutable-ok: request-local nested copy + sanitized_extra_body: Final = dict(extra_body) sanitized_extra_body.pop("reasoning_effort", None) sanitized_extra_body.pop("thinking", None) Router._pop_effort_from_nested_carrier(sanitized_extra_body, "output_config") @@ -2362,7 +2356,7 @@ class Router: self, deployment: DeploymentTypedDict, model: str, - kwargs: dict[str, object], # mutable-ok: fallback must update the active request and its log body together + kwargs: dict[str, object], ) -> None: """Let a classifier fallback without reasoning support remain a usable fallback. @@ -3189,7 +3183,7 @@ class Router: def adopt_fallback_headers(self, fallback_response: object) -> tuple[dict[str, object], dict[str, object]]: prepared: Final = Router._prepare_fallback_hidden_params(fallback_response) - self._hidden_params = {**prepared[0], "additional_headers": prepared[1]} # mutable-ok: stream metadata + self._hidden_params = {**prepared[0], "additional_headers": prepared[1]} self.fallback_headers_adopted = True return prepared @@ -5356,7 +5350,7 @@ class Router: # The pre-routing hook stamps its tier selection into this bucket during the primary # attempt; seeding it before the snapshot gives both the live kwargs and the copy a # bucket, so the post-call carry-over below always has somewhere to read and write. - kwargs.setdefault("litellm_metadata", {}) # mutable-ok: shared bucket # rebind-ok: stamp must be readable here + kwargs.setdefault("litellm_metadata", {}) # rebind-ok: stamp must be readable here fallback_kwargs: Final[dict[str, object]] = kwargs.copy() if isinstance(fallback_kwargs.get("litellm_metadata"), dict): @@ -5385,7 +5379,7 @@ class Router: async def _aanthropic_messages_streaming_iterator( self, response: AsyncIterator[bytes], - initial_kwargs: dict[str, Any], # mutable-ok: mutated in-place before re-entering the fallback chain + initial_kwargs: dict[str, Any], ) -> AsyncIterator[bytes]: """ Wrap an anthropic_messages (/v1/messages) streaming response so a @@ -5553,7 +5547,7 @@ class Router: has_generated_content: bool, buffered_lifecycle_chunks: tuple[bytes, ...], model: str, - initial_kwargs: dict[str, Any], # mutable-ok: handed to _aanthropic_messages_fallback_attempt, which mutates it + initial_kwargs: dict[str, Any], wrapper: "FallbackAwareAnthropicMessagesStream", ) -> AsyncGenerator[bytes, None]: """Turns a source-iterator failure into a fallback attempt or the error reaching the caller.""" @@ -5580,7 +5574,7 @@ class Router: async def _aanthropic_messages_fallback_attempt( self, e: "MidStreamFallbackError", - initial_kwargs: dict[str, Any], # mutable-ok: mutated in-place before re-entering the fallback chain + initial_kwargs: dict[str, Any], wrapper: "FallbackAwareAnthropicMessagesStream", ) -> AsyncGenerator[bytes, None]: """ @@ -5679,9 +5673,9 @@ class Router: # The pre-routing hook stamps its tier selection into this bucket during the primary # attempt; seeding it before the snapshot gives both the live kwargs and the copy a # bucket, so the post-call carry-over below always has somewhere to read and write. - kwargs.setdefault("litellm_metadata", {}) # mutable-ok: shared bucket # rebind-ok: stamp must be readable here + kwargs.setdefault("litellm_metadata", {}) # rebind-ok: stamp must be readable here - fallback_kwargs: Final[dict[str, object]] = kwargs.copy() # mutable-ok: mutated below before re-entry + fallback_kwargs: Final[dict[str, object]] = kwargs.copy() if isinstance(fallback_kwargs.get("litellm_metadata"), dict): fallback_kwargs["litellm_metadata"] = safe_deep_copy(fallback_kwargs["litellm_metadata"]) if isinstance(fallback_kwargs.get("metadata"), dict): @@ -6974,7 +6968,7 @@ class Router: "avector_store_delete", ): vector_store_kwargs: Final = ( - { # mutable-ok: the async routed request requires dynamic keyword arguments + { **kwargs, "_direct_vector_store_embedding_executor": RouterVectorStoreEmbeddingExecutor( router=self, @@ -8681,7 +8675,7 @@ class Router: return f"{type(value).__module__}.{type(value).__qualname__}" @staticmethod - def generate_model_id(model_group: str, litellm_params: dict) -> str: # mutable-ok: hashed read-only + def generate_model_id(model_group: str, litellm_params: dict) -> str: """ Helper function to consistently generate the same id for a deployment @@ -8743,7 +8737,7 @@ class Router: @staticmethod def _inherit_builtin_base_rates_for_off_peak( - model_info: dict, # mutable-ok: cost-map entry filled in place + model_info: dict, backend_model: str, custom_llm_provider: str | None, ) -> None: @@ -9931,7 +9925,7 @@ class Router: return (backend_key,) @staticmethod - def _deployment_model_cost_payload(deployment: Deployment) -> dict: # mutable-ok: cost-map entry + def _deployment_model_cost_payload(deployment: Deployment) -> dict: """The ``model_info`` a deployment contributes to ``litellm.model_cost``. Custom pricing lives on ``litellm_params`` rather than ``model_info``, and @@ -9939,7 +9933,7 @@ class Router: both are folded back in here. That keeps this reproducible from a deployment alone, which is what lets a refresh rebuild the same entries. """ - model_info: Final[dict] = deployment.model_info.model_dump(exclude_none=True) # mutable-ok: built in place + model_info: Final[dict] = deployment.model_info.model_dump(exclude_none=True) for field in CustomPricingLiteLLMParams.model_fields: field_value = deployment.litellm_params.get(field) if field_value is not None: @@ -9966,7 +9960,7 @@ class Router: def _register_deployment_in_model_cost( *, model_id: str | None, - model_info: dict, # mutable-ok: cost-map entry + model_info: dict, model: str, custom_llm_provider: str | None, ) -> None: @@ -9985,9 +9979,7 @@ class Router: requests that route to (and bill as) a real deployment. """ if classify_strategy_router_model(model) is not None: - model_info = { # mutable-ok: filtered copy of the caller's entry, handed straight to register_model - k: v for k, v in model_info.items() if k not in CustomPricingLiteLLMParams.model_fields - } + model_info = {k: v for k, v in model_info.items() if k not in CustomPricingLiteLLMParams.model_fields} if model_id is not None: litellm.register_model( @@ -11680,10 +11672,8 @@ class Router: the group, so inheriting them here would let a key holding a member's access group list and call the whole group. """ - model_info: Final = { # mutable-ok: DeploymentTypedDict rows are plain dicts - k: v for k, v in (deployment.get("model_info") or {}).items() if k != "access_groups" - } - return {**deployment, "model_info": model_info} # mutable-ok: DeploymentTypedDict rows are plain dicts + model_info: Final = {k: v for k, v in (deployment.get("model_info") or {}).items() if k != "access_groups"} + return {**deployment, "model_info": model_info} TIER_PARAMS_NEVER_DROPPED: Final = frozenset(all_litellm_params) | frozenset( { @@ -12612,9 +12602,7 @@ class Router: self, model: str, deployments: Sequence[DeploymentTypedDict] ) -> list[DeploymentTypedDict]: """A strategy marker is never a callable deployment, whichever resolution arm produced it.""" - selectable: Final = [ # mutable-ok: matches _common_checks_available_deployment's list contract - d for d in deployments if not self._is_strategy_marker_deployment(d) - ] + selectable: Final = [d for d in deployments if not self._is_strategy_marker_deployment(d)] if deployments and not selectable: raise litellm.BadRequestError( message=f"You passed in model={model}. {RouterErrors.only_strategy_marker_deployments.value}", @@ -13552,7 +13540,7 @@ class Router: # deployment-context filtering key off this field. Compared by value, since # pydantic rebuilds the list rather than keeping the object passed in. pre_routing_hook_response: Final = ( - routed.model_copy(update={"messages": messages}) # mutable-ok: pydantic's model_copy takes a dict + routed.model_copy(update={"messages": messages}) if routed is not None and routing_messages is not None and routed.messages == routing_messages else routed ) @@ -14155,7 +14143,7 @@ class Router: ] if not filtered: - return [] if health_check_probe else healthy_deployments # mutable-ok: empty list signals unavailable probe + return [] if health_check_probe else healthy_deployments return filtered diff --git a/litellm/router_strategy/auto_router/litellm_encoder.py b/litellm/router_strategy/auto_router/litellm_encoder.py index 1b34785b6fe..caabbb3a342 100644 --- a/litellm/router_strategy/auto_router/litellm_encoder.py +++ b/litellm/router_strategy/auto_router/litellm_encoder.py @@ -87,7 +87,7 @@ class LiteLLMRouterEncoder(CustomDenseEncoder, AsymmetricDenseMixin): limit: Final = self.max_input_chars if limit <= 0: return docs - clamped: Final = [doc[:limit] for doc in docs] # mutable-ok: embedding() takes `input: str | list` + clamped: Final = [doc[:limit] for doc in docs] if clamped != docs: verbose_router_logger.debug( "LiteLLMRouterEncoder: cut input to %s chars for embedding model %s", limit, self.model_name diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index 9deccc9a468..e3a6732a96c 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -1143,7 +1143,7 @@ def _parse_session_affinity_pin(value: object) -> _SessionAffinityPin | None: def _session_affinity_cache_value(model: str, tier: ComplexityTier | str | None) -> Mapping[str, str | None]: tier_value: Final = _tier_name(tier) if tier is not None else None - return {"model": model, "tier": tier_value} # mutable-ok: cache requires JSON mapping + return {"model": model, "tier": tier_value} class ComplexityRouter(CustomLogger): @@ -1736,7 +1736,7 @@ class ComplexityRouter(CustomLogger): self, prompt: str, system_prompt: str | None, - request_kwargs: dict[str, Any] | None, # mutable-ok: handed to _classify_with_llm as-is + request_kwargs: dict[str, Any] | None, messages: Sequence[Mapping[str, object]] | None, ) -> ClassificationOutcome: """Score locally, and only pay for the classifier call when the scorer did not confidently @@ -1769,7 +1769,7 @@ class ComplexityRouter(CustomLogger): self, prompt: str, system_prompt: str | None, - request_kwargs: dict[str, Any] | None, # mutable-ok: handed to _classify_with_llm as-is + request_kwargs: dict[str, Any] | None, messages: Sequence[Mapping[str, object]] | None, ) -> ClassificationOutcome: """Score locally, and only pay for the classifier when the score sits near a tier boundary. @@ -1824,7 +1824,7 @@ class ComplexityRouter(CustomLogger): self, prompt: str, system_prompt: str | None, - request_kwargs: dict[str, Any] | None, # mutable-ok: handed to _classify_with_llm as-is + request_kwargs: dict[str, Any] | None, messages: Sequence[Mapping[str, object]] | None, scored: ClassificationOutcome | None = None, ) -> ClassificationOutcome: @@ -1902,7 +1902,7 @@ class ComplexityRouter(CustomLogger): self, prompt: str, system_prompt: str | None, - request_kwargs: dict[str, Any] | None, # mutable-ok: handed to resolve_structured_messages as-is + request_kwargs: dict[str, Any] | None, raw_messages: list[dict[str, Any]] | None, # mutable-ok: same shape _run_routing_plugins receives ) -> ClassificationOutcome: from litellm.litellm_core_utils.prompt_templates.factory import resolve_structured_messages @@ -2056,7 +2056,7 @@ class ComplexityRouter(CustomLogger): ) request_metadata = (request_kwargs or {}).get("litellm_metadata") or (request_kwargs or {}).get("metadata") - metadata: Final = { # mutable-ok: SDK metadata kwarg is enriched by the request pipeline + metadata: Final = { **forwarded_internal_call_metadata(request_metadata, AUTOROUTER_CLASSIFIER_CALL_ORIGIN), INTERNAL_CALL_ORIGIN_METADATA_KEY: AUTOROUTER_CLASSIFIER_CALL_ORIGIN, } @@ -2064,7 +2064,7 @@ class ComplexityRouter(CustomLogger): image_parts: Final = self._classifier_image_parts(messages) user_content: Final[str | Sequence[ChatCompletionTextObject | ChatCompletionImageObject]] = ( - [ # mutable-ok: SDK request payload content list is built once + [ {"type": "text", "text": user_payload}, *image_parts, ] @@ -2905,7 +2905,7 @@ class ComplexityRouter(CustomLogger): response: PreRoutingHookResponse, messages: list[dict[str, Any]] | None, # mutable-ok: forwarded verbatim to the list-typed re-pick resolved_messages: Sequence[Mapping[str, object]] | None, - request_kwargs: dict, # mutable-ok: same shape the hook receives + request_kwargs: dict, context_fit: _RequestContextFit | None = None, ) -> PreRoutingHookResponse: """Replace a routed model that cannot accept this request's image input. @@ -2948,7 +2948,7 @@ class ComplexityRouter(CustomLogger): ) if capable is not None: new_tier: ComplexityTier | str | None = capable if self.config.has_custom_tiers else ComplexityTier(capable) - repick_messages: Final = list(resolved_messages) # mutable-ok: the pick's param is list-typed + repick_messages: Final = list(resolved_messages) new_model = await self._pick_model_for_tier( new_tier, messages, @@ -3038,7 +3038,7 @@ class ComplexityRouter(CustomLogger): model_name: str, messages: list[dict[str, Any]] | None, # mutable-ok: forwarded verbatim to the router's own probe input: str | list | None, # mutable-ok: mirrors the owner's own input parameter, which this forwards verbatim - request_kwargs: dict, # mutable-ok: same shape the hook receives + request_kwargs: dict, ) -> bool: """Whether the router would find a deployment for this group ON THIS REQUEST. @@ -3067,7 +3067,7 @@ class ComplexityRouter(CustomLogger): from litellm.exceptions import BadRequestError from litellm.types.router import RouterErrors, RouterRateLimitError, RouterRateLimitErrorBasic - probe_kwargs: Final = dict(request_kwargs) # mutable-ok: the owner pops routing keys off the dict it is handed + probe_kwargs: Final = dict(request_kwargs) try: deployments: Final = await self.litellm_router_instance.async_get_healthy_deployments( model=model_name, @@ -3096,7 +3096,7 @@ class ComplexityRouter(CustomLogger): messages: list[dict[str, Any]] | None, # mutable-ok: forwarded verbatim to the list-typed re-pick input: str | list | None, # mutable-ok: mirrors the owner's own input parameter, which this forwards verbatim resolved_messages: Sequence[Mapping[str, object]] | None, - request_kwargs: dict, # mutable-ok: same shape the hook receives + request_kwargs: dict, context_fit: _RequestContextFit | None = None, ) -> PreRoutingHookResponse: """Try compatible tier recovery before the default, preserving request policy and fit.""" @@ -3145,9 +3145,7 @@ class ComplexityRouter(CustomLogger): ) live: Final = tuple(peer for peer, can_serve in zip(candidates, servable) if can_serve) if live: - repick_messages: Final = ( - list(resolved_messages) if resolved_messages else None # mutable-ok: the pick's param is list-typed - ) + repick_messages: Final = list(resolved_messages) if resolved_messages else None try: new_model: Final = await self._pick_model_for_tier( candidate_tier if self.config.has_custom_tiers else ComplexityTier(candidate_tier), @@ -3183,7 +3181,7 @@ class ComplexityRouter(CustomLogger): context_escalation_original_tier=decision.get("context_escalation_original_tier"), ) return response.model_copy( - update={ # mutable-ok: model_copy types update as a plain dict + update={ "model": new_model, "litellm_params": self._litellm_params_for_model(candidate_tier, new_model), "routing_decision": new_decision, @@ -3227,7 +3225,7 @@ class ComplexityRouter(CustomLogger): context_escalation_original_tier=decision.get("context_escalation_original_tier"), ) return response.model_copy( - update={ # mutable-ok: model_copy types update as a plain dict + update={ "model": default_model, "litellm_params": self._litellm_params_for_model(None, default_model), "routing_decision": default_decision, @@ -3502,11 +3500,7 @@ class ComplexityRouter(CustomLogger): ) -> PreRoutingHookResponse | None: if response is None or not self._uses_deployment_pin: return response - return response.model_copy( - update={ # mutable-ok: model_copy types update as a plain dict - "session_affinity_ttl_seconds": self.config.session_affinity_ttl_seconds - } - ) + return response.model_copy(update={"session_affinity_ttl_seconds": self.config.session_affinity_ttl_seconds}) async def async_pre_routing_hook( self, diff --git a/litellm/router_strategy/complexity_router/config.py b/litellm/router_strategy/complexity_router/config.py index 1f1b5a5cc4b..bfb74aab769 100644 --- a/litellm/router_strategy/complexity_router/config.py +++ b/litellm/router_strategy/complexity_router/config.py @@ -239,7 +239,7 @@ class ComplexityTierModel(BaseModel): @field_serializer("litellm_params") def _serialize_litellm_params(self, value: Mapping[str, object]) -> Mapping[str, object]: - return dict(value) # mutable-ok: Pydantic JSON serialization requires a concrete mapping + return dict(value) def _normalize_tier_entries( @@ -254,11 +254,7 @@ def _normalize_tier_entries( model_names: Final = tuple(entry.model_name for entry in entries) if len(model_names) != len(frozenset(model_names)): raise ValueError(f"tier {tier} contains duplicate model_name values; each pool entry needs distinct parameters") - normalized: Final = ( - entries[0].model_name - if not isinstance(raw_value, (list, tuple)) - else list(model_names) # mutable-ok: config.tiers must preserve its existing list contract - ) + normalized: Final = entries[0].model_name if not isinstance(raw_value, (list, tuple)) else list(model_names) return normalized, entries @@ -1360,7 +1356,7 @@ class ComplexityRouterConfig(BaseModel): or (isinstance(existing_configs, dict) and tier in existing_configs) } ) - return { # mutable-ok: Pydantic before-validator requires a concrete mapping + return { **value, "tiers": normalized_tiers, "tier_model_configs": tier_model_configs, diff --git a/litellm/router_strategy/tag_based_routing.py b/litellm/router_strategy/tag_based_routing.py index d4f46e94579..50dce250920 100644 --- a/litellm/router_strategy/tag_based_routing.py +++ b/litellm/router_strategy/tag_based_routing.py @@ -217,9 +217,7 @@ def _strip_routing_prefix(tags: Sequence[str], prefix: str) -> tuple[tuple[str, def _split_tags(tags: Sequence[str]) -> tuple[tuple[str, ...], list[str], tuple[str, ...]]: required: Final = tuple(tag[1:] for tag in tags if tag.startswith("&") and len(tag) > 1) - positive: Final = [ - t for t in tags if not t.startswith("!") and not t.startswith("&") - ] # mutable-ok: feeds _match_deployment's existing list[str]-typed request_tags param + positive: Final = [t for t in tags if not t.startswith("!") and not t.startswith("&")] excluded: Final = tuple(tag[1:] for tag in tags if tag.startswith("!") and len(tag) > 1) return required, positive, excluded diff --git a/litellm/router_utils/auto_router_tuning_baseline.py b/litellm/router_utils/auto_router_tuning_baseline.py index 9699ab886b9..9cf6d28183f 100644 --- a/litellm/router_utils/auto_router_tuning_baseline.py +++ b/litellm/router_utils/auto_router_tuning_baseline.py @@ -120,7 +120,7 @@ def snapshot_tuning_baselines(deployments: Iterable[Mapping[str, object]]) -> Ma if (pair := heuristic_v1_router_fingerprint(deployment)) is not None for identity, fingerprint in (pair,) } - ) # mutable-ok: MappingProxyType owns the completed immutable snapshot + ) def is_mutable_tuned_candidate(candidate: Mapping[str, object], baselines: Mapping[str, str]) -> bool: diff --git a/litellm/rust_bridge/chat_completions.py b/litellm/rust_bridge/chat_completions.py index 674bd8847f7..470eb597f23 100644 --- a/litellm/rust_bridge/chat_completions.py +++ b/litellm/rust_bridge/chat_completions.py @@ -330,9 +330,9 @@ def _build_model_response( model_response: ModelResponse, ) -> ModelResponse: built: Final = convert_to_model_response_object( - response_object=dict(rust_response), # mutable-ok: the converter takes a real dict and rewrites it + response_object=dict(rust_response), model_response_object=model_response, - hidden_params={"additional_headers": {RUST_RESPONSE_HEADER: "true"}}, # mutable-ok: rewritten by the converter + hidden_params={"additional_headers": {RUST_RESPONSE_HEADER: "true"}}, ) if not isinstance(built, ModelResponse): raise TypeError(f"expected a ModelResponse from the rust path, got {type(built).__name__}") diff --git a/litellm/rust_bridge/lifecycle.py b/litellm/rust_bridge/lifecycle.py index f5e0c1b0fc6..513dde27508 100644 --- a/litellm/rust_bridge/lifecycle.py +++ b/litellm/rust_bridge/lifecycle.py @@ -81,7 +81,7 @@ def setup( from litellm import utils from litellm.litellm_core_utils.litellm_logging import Logging - arguments: Final = { # mutable-ok: function_setup consumes an owned kwargs dict + arguments: Final = { "litellm_call_id": str(uuid.uuid4()), **kwargs, } diff --git a/litellm/rust_bridge/ocr.py b/litellm/rust_bridge/ocr.py index de8a93dd8b1..f751a050ec8 100644 --- a/litellm/rust_bridge/ocr.py +++ b/litellm/rust_bridge/ocr.py @@ -112,7 +112,7 @@ def ocr( custom_llm_provider=custom_llm_provider, extra_headers=extra_headers, optional_params=optional_params, - input_sources=dict(input_sources or {}), # mutable-ok: native boundary requires a concrete dict + input_sources=dict(input_sources or {}), timeout_seconds=_timeout_to_seconds(timeout), ) @@ -140,6 +140,6 @@ async def aocr( custom_llm_provider=custom_llm_provider, extra_headers=extra_headers, optional_params=optional_params, - input_sources=dict(input_sources or {}), # mutable-ok: native boundary requires a concrete dict + input_sources=dict(input_sources or {}), timeout_seconds=_timeout_to_seconds(timeout), ) diff --git a/litellm/rust_bridge/ocr_lifecycle.py b/litellm/rust_bridge/ocr_lifecycle.py index 5ca584e1c11..04949c55aa5 100644 --- a/litellm/rust_bridge/ocr_lifecycle.py +++ b/litellm/rust_bridge/ocr_lifecycle.py @@ -59,8 +59,8 @@ def map_failure(error: Exception, request: LiteLLMOcrRequest, request_provider: model=request.model.removeprefix(f"{request_provider}/"), custom_llm_provider=request_provider, original_exception=error, - completion_kwargs=dict(arguments(request)), # mutable-ok: exception mapper requires owned kwargs - extra_kwargs=dict(request.kwargs), # mutable-ok: exception mapper requires owned kwargs + completion_kwargs=dict(arguments(request)), + extra_kwargs=dict(request.kwargs), ) except Exception as public_error: public_error.__context__ = error diff --git a/litellm/types/integrations/newrelic.py b/litellm/types/integrations/newrelic.py index b5905ad0b93..e662c260065 100644 --- a/litellm/types/integrations/newrelic.py +++ b/litellm/types/integrations/newrelic.py @@ -88,7 +88,7 @@ NewRelicMetric = NewRelicCountMetric | NewRelicGaugeMetric | NewRelicSummaryMetr #: ``interval.ms`` has a dot in it, so the functional TypedDict form is required. NewRelicMetricCommon = TypedDict( "NewRelicMetricCommon", - { # mutable-ok: functional TypedDict requires a dict-literal fields argument ("interval.ms" key) + { "timestamp": ReadOnly[int], "interval.ms": ReadOnly[int], }, diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index e3eac9b9205..f55d70517f1 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -115,7 +115,7 @@ class HttpxBinaryResponseContent(_HttpxBinaryResponseContent): def __init__(self, response: httpx.Response) -> None: super().__init__(response) - self._hidden_params = {} # mutable-ok: mutable-dict contract shared with ModelResponse logging consumers + self._hidden_params = {} def set_response_cost(self, response_cost: float | None) -> None: if response_cost is None: @@ -393,9 +393,7 @@ class OpenAIFileObject(BaseModel): serialized: Final[Mapping[str, object]] = handler(self) if self.litellm_batch_guardrail is not None: return serialized - return { # mutable-ok: pydantic's json serializer rejects a mapping that is not a dict - key: value for key, value in serialized.items() if key != BATCH_GUARDRAIL_RESPONSE_FIELD - } + return {key: value for key, value in serialized.items() if key != BATCH_GUARDRAIL_RESPONSE_FIELD} def __contains__(self, key) -> bool: # Define custom behavior for the 'in' operator diff --git a/litellm/types/management_endpoints/auto_router_endpoints.py b/litellm/types/management_endpoints/auto_router_endpoints.py index 9f29f27e41d..345d7eca6a6 100644 --- a/litellm/types/management_endpoints/auto_router_endpoints.py +++ b/litellm/types/management_endpoints/auto_router_endpoints.py @@ -116,13 +116,7 @@ class AutoRouterRoutingTestRequest(BaseModel): raise ValueError("provide exactly one of prompt or messages") if self.messages is not None: return self - return self.model_copy( - update={ # mutable-ok: model_copy types update as a plain dict - "messages": [ # mutable-ok: the routing hook's signature takes a list of message dicts - {"role": "user", "content": self.prompt} # mutable-ok: a message is dict-shaped - ] - } - ) + return self.model_copy(update={"messages": [{"role": "user", "content": self.prompt}]}) def wire_body(self) -> Mapping[str, object]: """The request kwargs a serving-path request would carry for this body. @@ -132,7 +126,7 @@ class AutoRouterRoutingTestRequest(BaseModel): the serving path. """ return MappingProxyType( - { # mutable-ok: MappingProxyType needs a dict to wrap + { key: value for key, value in (("messages", self.messages), ("system", self.system), ("tools", self.tools)) if value is not None diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/aim.py b/litellm/types/proxy/guardrails/guardrail_hooks/aim.py index 291740613ef..18d98441065 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/aim.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/aim.py @@ -20,7 +20,7 @@ class AimGuardrailConfigModel(GuardrailConfigModel): "Send /embeddings `input` to Aim as user messages. Off by default because embedding input is " "documents being indexed, not a conversation." ), - json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL}, # mutable-ok: pydantic accepts only a dict here + json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL}, ) @staticmethod diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/cato_networks.py b/litellm/types/proxy/guardrails/guardrail_hooks/cato_networks.py index 69b4d5bec37..dc69bd137ec 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/cato_networks.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/cato_networks.py @@ -20,7 +20,7 @@ class CatoNetworksGuardrailConfigModel(GuardrailConfigModel): "Send /embeddings `input` to Cato Networks as user messages. Off by default because embedding " "input is documents being indexed, not a conversation." ), - json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL}, # mutable-ok: pydantic accepts only a dict here + json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL}, ) @staticmethod diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/hide_secrets.py b/litellm/types/proxy/guardrails/guardrail_hooks/hide_secrets.py index 3100968e8b1..4bc2015e639 100644 --- a/litellm/types/proxy/guardrails/guardrail_hooks/hide_secrets.py +++ b/litellm/types/proxy/guardrails/guardrail_hooks/hide_secrets.py @@ -10,7 +10,7 @@ class HideSecretsGuardrailConfigModel(GuardrailConfigModel): on the detect-secrets library; ``detect_secrets_config`` overrides the bundled plugin set.""" - detect_secrets_config: dict | None = Field( # mutable-ok: UI type derivation maps dict to "object" + detect_secrets_config: dict | None = Field( default=None, description="Optional detect-secrets configuration (plugins_used, filters_used) overriding the bundled plugin set", ) diff --git a/litellm/types/responses/main.py b/litellm/types/responses/main.py index 26cc5c4c6cc..2381c7ff3a1 100644 --- a/litellm/types/responses/main.py +++ b/litellm/types/responses/main.py @@ -50,7 +50,7 @@ def build_web_search_call( query: Final = tool_input.get("query", "") if isinstance(tool_input, Mapping) else "" content: Final = result.get("content") if isinstance(result, Mapping) else None result_items: Final = content if isinstance(content, Sequence) and not isinstance(content, (str, bytes)) else () - sources: Final = [ # mutable-ok: official SDK expects a source list + sources: Final = [ ActionSearchSource(type="url", url=url) for item in result_items if isinstance(item, Mapping) @@ -62,10 +62,10 @@ def build_web_search_call( id=f"ws_{tool_id}", type="web_search_call", status=status or ("failed" if failed else "completed"), - action={ # mutable-ok: official SDK expects an action mapping + action={ "type": "search", "query": query if isinstance(query, str) else "", - "queries": [query] if isinstance(query, str) and query else [], # mutable-ok: SDK list field + "queries": [query] if isinstance(query, str) and query else [], "sources": sources, }, ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 1d73542c9bb..0a663ed18ea 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1306,9 +1306,7 @@ def add_provider_specific_fields(object: BaseModel, provider_specific_fields: di class Message(SafeAttributeModel, OpenAIObject): content: str | None role: Literal["assistant", "user", "system", "tool", "function"] - tool_calls: ( - list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | None - ) # mutable-ok: public pydantic response field; only the union member is new + tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | None function_call: FunctionCall | None audio: ChatCompletionAudioResponse | None = None images: list[ImageURLListItem] | None = None @@ -1431,9 +1429,7 @@ class Delta(SafeAttributeModel, OpenAIObject): content: str | None role: str | None function_call: FunctionCall | None - tool_calls: ( - list[ChatCompletionDeltaToolCall | ChatCompletionDeltaCustomToolCall] | None - ) # mutable-ok: public pydantic response field; only the union member is new + tool_calls: list[ChatCompletionDeltaToolCall | ChatCompletionDeltaCustomToolCall] | None audio: ChatCompletionAudioResponse | None images: list[ImageURLListItem] | None annotations: list[ChatCompletionAnnotation] | None @@ -1471,9 +1467,7 @@ class Delta(SafeAttributeModel, OpenAIObject): function_call = FunctionCall(**function_call) if tool_calls is not None and isinstance(tool_calls, (list, tuple)): - coerced_tool_calls: list[ - ChatCompletionDeltaToolCall | ChatCompletionDeltaCustomToolCall - ] = [] # mutable-ok: public Delta.tool_calls contract is a list + coerced_tool_calls: list[ChatCompletionDeltaToolCall | ChatCompletionDeltaCustomToolCall] = [] current_index = 0 for tool_call in tool_calls: if isinstance(tool_call, dict): diff --git a/litellm/utils.py b/litellm/utils.py index 04139a124b6..1787f6e52d4 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1133,7 +1133,7 @@ def function_setup( verbose_logger.debug("Error extracting messages from Google contents: %s", e) messages = "default-message-value" elif call_type in NON_INFERENCE_CALL_TYPES: - messages = [] # mutable-ok: loggers require a list here and Logging copies it + messages = [] else: messages = "default-message-value" stream = False @@ -2939,9 +2939,9 @@ def _update_dictionary(existing_dict: dict, new_dict: dict) -> dict: elif isinstance(v, dict): existing_nested_dict = existing_dict.get(k) if isinstance(existing_nested_dict, dict): - existing_dict[k] = {**existing_nested_dict, **v} # mutable-ok: copy-on-write merge + existing_dict[k] = {**existing_nested_dict, **v} else: - existing_dict[k] = dict(v) # mutable-ok: detached copy, never the caller's dict by reference + existing_dict[k] = dict(v) else: existing_dict[k] = v @@ -3047,7 +3047,7 @@ def _get_builtin_model_info_for_registration(model: str) -> ModelInfo | None: return None if is_generalized_model_info(info) else info -_runtime_registered_model_cost: Final[dict[str, dict[str, object]]] = {} # mutable-ok: replayed on reload +_runtime_registered_model_cost: Final[dict[str, dict[str, object]]] = {} class _LiveDeploymentReplay: @@ -3092,7 +3092,7 @@ def reapply_runtime_model_cost_registrations() -> None: if _LiveDeploymentReplay.callback is not None: _LiveDeploymentReplay.callback() if _runtime_registered_model_cost: - register_model(model_cost=dict(_runtime_registered_model_cost)) # mutable-ok: snapshot, replay rewrites it + register_model(model_cost=dict(_runtime_registered_model_cost)) def register_model( @@ -3136,7 +3136,7 @@ def register_model( if persist_across_reloads: _registrations: Final[Mapping[str, Mapping[str, object]]] = loaded_model_cost for _registered_key, _registered_value in _registrations.items(): - _runtime_registered_model_cost[_registered_key] = dict(_registered_value) # mutable-ok: caller-owned + _runtime_registered_model_cost[_registered_key] = dict(_registered_value) _skip_get_model_info_providers: Final = PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO @@ -3158,7 +3158,7 @@ def register_model( # An exact entry ends the lookup ladder before the capability rules are # consulted, so seed from them: otherwise registering an unmapped model # shadows the very defaults it would have resolved to unregistered. - existing_model = dict(match_capability_generalizations(_key_str) or {}) # mutable-ok: merge target + existing_model = dict(match_capability_generalizations(_key_str) or {}) model_cost_key = key builtin_entry = _resolve_builtin_model_cost_entry(key=_key_str, provider=provider) if builtin_entry is not None: @@ -4885,7 +4885,7 @@ def provider_rejectable_params(passed_params: Mapping[str, object]) -> frozenset params at all, so a caller filtering on "is this an OpenAI param" would discard configuration the request needs while never touching what the provider would have rejected. """ - params: Final = dict(passed_params) # mutable-ok: get_non_default_params takes a dict + params: Final = dict(passed_params) return frozenset(get_non_default_params(params)) - PROVIDER_UNVALIDATED_PARAMS @@ -7059,7 +7059,7 @@ class TextCompletionStreamWrapper: def mock_stream_usage_chunk(model_response: ModelResponseStream, model: str, prompt_tokens: int) -> ModelResponseStream: return ModelResponseStream( id=model_response.id, - choices=[], # mutable-ok: ModelResponseStream only treats a list as explicit choices, a tuple gets a default choice + choices=[], model=model, usage=Usage( prompt_tokens=prompt_tokens, diff --git a/litellm/vector_stores/main.py b/litellm/vector_stores/main.py index 976e6dead76..4d945310293 100644 --- a/litellm/vector_stores/main.py +++ b/litellm/vector_stores/main.py @@ -307,9 +307,7 @@ async def asearch( embedding_executor: Final = _direct_vector_store_embedding_executor( kwargs.pop("_direct_vector_store_embedding_executor", None), router, kwargs ) - local_vars: Final = { # mutable-ok: exception logging requires a sanitized mutable snapshot - key: value for key, value in locals().items() if key != "embedding_executor" - } + local_vars: Final = {key: value for key, value in locals().items() if key != "embedding_executor"} try: loop: Final = asyncio.get_event_loop() @@ -393,9 +391,7 @@ def search( embedding_executor: Final = _direct_vector_store_embedding_executor( kwargs.pop("_direct_vector_store_embedding_executor", None), router, kwargs ) - local_vars: Final = { # mutable-ok: exception logging requires a sanitized mutable snapshot - key: value for key, value in locals().items() if key != "embedding_executor" - } + local_vars: Final = {key: value for key, value in locals().items() if key != "embedding_executor"} try: litellm_logging_obj: Final[LiteLLMLoggingObj] = kwargs.get("litellm_logging_obj") litellm_call_id: Final[str | None] = kwargs.get("litellm_call_id", None) diff --git a/scripts/check_type_discipline.py b/scripts/check_type_discipline.py index a2ab4760c4f..9ced0b817c0 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -3,38 +3,21 @@ Rules ----- -LIT001 Mutable collection in a type annotation, anywhere it appears: function +LIT001 Mutable sequence or set in a type annotation, anywhere it appears: function parameters, return types, class attributes, locals, and module globals. - Covers the builtins (dict/list/set, bare or parameterized), their typing - aliases (Dict/List/...), the collections concretes (deque/defaultdict/...), - and the mutable ABCs (MutableMapping/MutableSequence/MutableSet). A mutable - collection lets whoever holds it grow or rewrite it after the fact; annotate - a read-only view instead (Mapping/Sequence/AbstractSet/tuple[X, ...]/ - frozenset[X], or a frozen dataclass / NamedTuple / ReadOnly TypedDict) and - build it functionally (comprehension / map, not append-in-a-loop). + Covers the builtins (list/set, bare or parameterized), their typing aliases + (List/Deque), the collections concretes (deque), and the mutable ABCs + (MutableSequence/MutableSet). A mutable collection lets whoever holds it grow + or rewrite it after the fact; annotate a read-only view instead (Sequence/ + AbstractSet/tuple[X, ...]/frozenset[X]) and build it functionally + (comprehension / map, not append-in-a-loop). Mappings are out of scope: dict + is the interchange type of the Python ecosystem, and a rule against it only + bought MappingProxyType round-trips at every library boundary. Suppress with `# mutable-ok: ` on the offending line. -LIT002 Mutable-collection *construction*: a list/dict/set literal or comprehension, or - a call to a mutable constructor (list/dict/set/deque/defaultdict/Counter/...). - Catches the unannotated seed-then-mutate pattern LIT001 cannot see (`acc = []`). - Build the value in one shot and freeze it: a `tuple`/`frozenset` wrapping a - generator (`tuple(f(x) for x in xs)`), a tuple literal, a frozen dataclass / - NamedTuple, a TypedDict-annotated dict literal, or (if it really must be - dynamic) a MappingProxyType wrapping a dict literal or comprehension. Generator - expressions and freezing-wrapper calls (`tuple(...)`, `frozenset(...)`, - `MappingProxyType(...)`) are not construction and pass, as does the value passed - directly to a wrapper: it is frozen before it can escape, though anything - mutable nested inside it still counts. Annotation-internal lists - (`Callable[[int], str]`) are exempt. A dict literal whose assignment is - annotated with a TypedDict (`x: Final[MyTD] = {...}`; bare `x: Final = {...}` - does not qualify) is a fixed-shape build basedpyright checks key-by-key against - fields LIT012 keeps ReadOnly, not a growable accumulator, so it is exempt along - with the dict literals nested in it (nested TypedDict fields); any other - construction inside still counts. Detection is name-based: Final/ClassVar/ - Optional (and Annotated's first argument) unwrap, a PEP 604 union - (`MyTD | None`) qualifies through either arm, and any remaining named head - outside the mutable collections and Mapping/Any/object is taken to be a - TypedDict, since a dict literal assigned to any other named type would not - survive basedpyright. Suppress with `# mutable-ok: `. +LIT002 Retired. It banned every list/dict/set literal, comprehension, and constructor + call, and in practice produced `# mutable-ok` on most lines that touched a + library, plus defensive deep copies that were worse than the mutation they + guarded against. The code is gone; the number is not reused. LIT003 noqa suppression without rule codes or without a reason. Required shape: `# noqa: TID251 # ` LIT004 pyright/mypy ignore without bracketed codes or without a reason. @@ -88,8 +71,8 @@ LIT011 Function-argument mutation: a parameter that is re-bound (`param = ...`, annotations are evaluated in the enclosing scope and are attributed there. `self`/`cls` are exempt from the in-place-store check (methods own their instance), not from re-binding. Method-call mutation (`param.append(x)`) is - out of reach without type information; LIT001/LIT002 keep mutable collections - off signatures instead. Suppress with `# rebind-ok: `. + out of reach without type information; LIT001 keeps mutable sequences and + sets off signatures instead. Suppress with `# rebind-ok: `. LIT012 TypedDict field without a `ReadOnly[...]` qualifier. A writable key lets any holder of the payload rewrite it after construction; qualify every field with `ReadOnly[...]` (PEP 705), which nests freely with Required/NotRequired/ @@ -124,40 +107,19 @@ from pathlib import Path from collections.abc import Iterable, Iterator, Mapping, Sequence from typing import NamedTuple -# Mutable collection types, banned in *every* annotation. Name-based, so `dict`, -# `typing.Dict`, `collections.deque`, and `collections.abc.MutableMapping` all match -# however they were imported. The read-only interfaces (Mapping, Sequence, the -# immutable AbstractSet / `abc.Set`, Collection) and the immutable concretes (tuple, -# frozenset) are the escape hatch and are deliberately absent -- as is the bare name -# `Set`, which collides with the read-only `collections.abc.Set`. +# Mutable sequence and set types, banned in *every* annotation. Name-based, so `list`, +# `typing.List`, `collections.deque`, and `collections.abc.MutableSequence` all match +# however they were imported. The read-only interfaces (Sequence, the immutable +# AbstractSet / `abc.Set`, Collection) and the immutable concretes (tuple, frozenset) +# are the escape hatch and are deliberately absent -- as is the bare name `Set`, which +# collides with the read-only `collections.abc.Set`. Mappings (dict, Dict, defaultdict, +# MutableMapping, ...) are deliberately allowed. MUTABLE_COLLECTIONS = frozenset(( - "dict", "list", "set", - "Dict", "List", "DefaultDict", "OrderedDict", "Counter", "Deque", "ChainMap", - "deque", "defaultdict", - "MutableMapping", "MutableSequence", "MutableSet", + "list", "set", + "List", "Deque", + "deque", + "MutableSequence", "MutableSet", )) - -# Callables whose result is a fresh *mutable* collection (LIT002). `tuple` and -# `frozenset` are deliberately absent -- they are the wrappers you reach for, and -# a generator expression fed to them is the blessed one-shot build. -MUTABLE_CONSTRUCTORS = frozenset(( - "dict", "list", "set", - "deque", "defaultdict", "OrderedDict", "Counter", "ChainMap", -)) -# A *qualified* call (`x.deque()`) counts as construction only for names that are rarely -# method names; `dict`/`list`/`set` are dropped here because `.dict()` / `.set()` / `.list()` -# are common methods (e.g. pydantic's `model.dict()`), not collection construction. A -# qualified `collections.deque(...)` still counts. -QUALIFIED_CONSTRUCTORS = MUTABLE_CONSTRUCTORS - frozenset(("dict", "list", "set")) -FREEZING_WRAPPERS = frozenset(("tuple", "frozenset", "MappingProxyType")) -# Wrappers unwrapped when deciding whether an assignment's annotation names a -# TypedDict (the LIT002 dict-literal exemption); bare, they name no type. Annotated -# is handled separately: only its first argument is type syntax. -TYPEDDICT_ANNOTATION_WRAPPERS = frozenset(("Final", "ClassVar", "Optional")) -# Heads that can type a dict literal without being a TypedDict. Every other named -# head counts as one: a dict literal assigned to any other named type would not -# survive basedpyright, which is the second gate behind this name-based check. -NON_TYPEDDICT_HEADS = MUTABLE_COLLECTIONS | frozenset(("Mapping", "Any", "object")) UNSAFE_GUARDS = frozenset(("TypeGuard", "TypeIs")) READONLY_QUALIFIER = "ReadOnly" # Qualifiers ReadOnly may nest under, in any order (PEP 705); for Annotated only the @@ -338,10 +300,9 @@ def _mutable_ann(path: Path, line: int, name: str, where: str) -> Violation: return Violation( path, line, "LIT001", f"mutable `{name}` in {where}: a mutable collection can be grown or rewritten " - f"by whoever holds it. Annotate a read-only view -- Mapping[...], Sequence[...], " - f"AbstractSet[...], tuple[X, ...], frozenset[X], or a frozen dataclass / " - f"NamedTuple / ReadOnly TypedDict -- and build it functionally, not by " - f"append-in-a-loop (suppress: `# mutable-ok: `)", + f"by whoever holds it. Annotate a read-only view -- Sequence[...], " + f"AbstractSet[...], tuple[X, ...], or frozenset[X] -- and build it functionally, " + f"not by append-in-a-loop (suppress: `# mutable-ok: `)", ) @@ -454,169 +415,6 @@ def iter_guard_violations(path: Path, tree: ast.AST, comments: Comments) -> Iter ) -# --------------------------------------------------------------------------- # -# Mutable-collection construction (LIT002) -# --------------------------------------------------------------------------- # - - -def _annotations_of(node: ast.AST) -> tuple[ast.expr | None, ...]: - """The annotation expressions a node carries (signatures and `x: T`).""" - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - a = node.args - params = (*a.posonlyargs, *a.args, *a.kwonlyargs, a.vararg, a.kwarg) - return (*(p.annotation for p in params if p is not None), node.returns) - if isinstance(node, ast.AnnAssign): - return (node.annotation,) - return () - - -def _annotation_node_ids(tree: ast.AST) -> frozenset[int]: - """ids() of every node living inside an annotation. - - A list display inside an annotation (`Callable[[int], str]`) is type syntax, - not construction, so the LIT002 walk must skip those subtrees. - """ - return frozenset( - id(sub) - for node in ast.walk(tree) - for ann in _annotations_of(node) - if ann is not None - for sub in ast.walk(ann) - ) - - -def _is_freezing_wrapper(func: ast.expr) -> bool: - if isinstance(func, ast.Name): - return func.id in FREEZING_WRAPPERS - return ( - isinstance(func, ast.Attribute) - and func.attr == "MappingProxyType" - and isinstance(func.value, ast.Name) - and func.value.id == "types" - ) - - -def _frozen_argument_ids(tree: ast.AST) -> frozenset[int]: - """ids() of every expression passed directly to a freezing wrapper. - - `MappingProxyType({...})`, `frozenset({...})`, and `tuple([...])` freeze their - argument before it can escape, so the literal inside is a one-shot build, not a - mutable value anyone can grow later. Only the argument itself is exempt; a - mutable collection nested inside it still trips LIT002. Only bare names (plus - `types.MappingProxyType`) qualify, so an unrelated method that happens to share - a wrapper's name cannot exempt its argument. - """ - return frozenset( - id(node.args[0]) - for node in ast.walk(tree) - if isinstance(node, ast.Call) and len(node.args) == 1 and _is_freezing_wrapper(node.func) - ) - - -def _is_typeddict_annotation(annotation: ast.expr) -> bool: - """True iff the annotation names a TypedDict, by the name-based heuristic. - - Final/ClassVar/Optional unwrap (as does Annotated's first argument, the only - one that is type syntax), a PEP 604 union qualifies through either arm, string - forward references are parsed, and whatever named head remains counts as a - TypedDict unless it is a mutable collection or Mapping/Any/object -- the heads - that can type a dict literal without being one. Bare wrappers - (`x: Final = ...`) name no type and never qualify. - """ - if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str): - try: - inner = ast.parse(annotation.value, mode="eval").body - except SyntaxError: - return False - return _is_typeddict_annotation(inner) - if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr): - return _is_typeddict_annotation(annotation.left) or _is_typeddict_annotation(annotation.right) - if isinstance(annotation, ast.Subscript): - head = _head_name(annotation.value) - if head in TYPEDDICT_ANNOTATION_WRAPPERS: - return _is_typeddict_annotation(annotation.slice) - if head == "Annotated": - first = annotation.slice.elts[0] if isinstance(annotation.slice, ast.Tuple) and annotation.slice.elts else None - return first is not None and _is_typeddict_annotation(first) - return head is not None and head not in NON_TYPEDDICT_HEADS - name = _head_name(annotation) - return ( - name is not None - and name not in NON_TYPEDDICT_HEADS - and name not in TYPEDDICT_ANNOTATION_WRAPPERS - and name != "Annotated" - ) - - -def _typeddict_build_ids(tree: ast.AST) -> frozenset[int]: - """ids() of every dict literal built under a TypedDict-annotated assignment. - - `x: Final[MyTD] = {...}` is a fixed-shape build: basedpyright checks each key - against the declared fields, which LIT012 keeps ReadOnly, so nothing here is - the seed-then-mutate accumulator LIT002 hunts. Dict literals nested in the - value (nested TypedDict fields) share the exemption; any other construction - inside it still counts, and a bare `x: Final = {...}` stays flagged. - """ - return frozenset( - id(sub) - for node in ast.walk(tree) - if isinstance(node, ast.AnnAssign) - and isinstance(node.value, ast.Dict) - and _is_typeddict_annotation(node.annotation) - for sub in ast.walk(node.value) - if isinstance(sub, ast.Dict) - ) - - -def _construction_kind(node: ast.expr) -> str | None: - """Human label if `node` builds a mutable collection, else None.""" - if isinstance(node, ast.List): - return "list literal" - if isinstance(node, ast.ListComp): - return "list comprehension" - if isinstance(node, ast.Set): - return "set literal" - if isinstance(node, ast.SetComp): - return "set comprehension" - if isinstance(node, ast.Dict): - return "dict literal" - if isinstance(node, ast.DictComp): - return "dict comprehension" - if isinstance(node, ast.Call): - func = node.func - if isinstance(func, ast.Name) and func.id in MUTABLE_CONSTRUCTORS: - return f"`{func.id}()` constructor" - if isinstance(func, ast.Attribute) and func.attr in QUALIFIED_CONSTRUCTORS: - return f"`{func.attr}()` constructor" - return None - - -def iter_construction_violations(path: Path, tree: ast.AST, comments: Comments) -> Iterator[Violation]: - in_annotation = _annotation_node_ids(tree) - frozen_arguments = _frozen_argument_ids(tree) - typeddict_builds = _typeddict_build_ids(tree) - for node in ast.walk(tree): - if ( - not isinstance(node, ast.expr) - or id(node) in in_annotation - or id(node) in frozen_arguments - or id(node) in typeddict_builds - ): - continue - kind = _construction_kind(node) - if kind is None or node.lineno in comments.mutable_ok_lines: - continue - yield Violation( - path, node.lineno, "LIT002", - f"mutable {kind}: this builds a collection that can be grown or rewritten. " - f"Build it in one shot and freeze it -- a tuple/frozenset wrapping a generator " - f"(`tuple(f(x) for x in xs)`), a tuple literal, a frozen dataclass / NamedTuple, " - f"a TypedDict-annotated dict literal (`x: Final[MyTD] = {{...}}`), or (if it " - f"really must be dynamic) a MappingProxyType wrapping a dict literal or " - f"comprehension (suppress: `# mutable-ok: `)", - ) - - # --------------------------------------------------------------------------- # # Final-annotation discipline (LIT010) and argument immutability (LIT011) # --------------------------------------------------------------------------- # @@ -1056,7 +854,6 @@ def check_file(path: Path) -> tuple[Violation, ...]: *iter_annotation_violations(path, tree, comments), *iter_cast_violations(path, tree, comments), *iter_guard_violations(path, tree, comments), - *iter_construction_violations(path, tree, comments), *iter_final_violations(path, tree, comments), *iter_param_violations(path, tree, comments), *iter_typeddict_violations(path, tree, comments), diff --git a/scripts/type_discipline_gate.py b/scripts/type_discipline_gate.py index 40e61cf7265..ee1ee5c4b3c 100644 --- a/scripts/type_discipline_gate.py +++ b/scripts/type_discipline_gate.py @@ -8,9 +8,8 @@ higher than the base it merges into, so a change is blamed for the violations it adds, never for drift that already exists in the base. Rules not present in the budget are ignored, but today every rule the checker -emits is gated: LIT001 (mutable collection in any annotation), LIT002 -(mutable-collection construction), LIT003/LIT004 (noqa / pyright-mypy ignore -without codes or reason), LIT006 (cast), LIT008 (`**kwargs`), LIT009 (inert +emits is gated: LIT001 (mutable sequence or set in any annotation), LIT003/LIT004 +(noqa / pyright-mypy ignore without codes or reason), LIT006 (cast), LIT008 (`**kwargs`), LIT009 (inert `# type: ignore`, dead syntax while enableTypeIgnoreComments is false), LIT010 (assignment without a Final declaration; suppress deliberate rebinding with `# rebind-ok: `), LIT011 (parameter rebinding or in-place mutation), and diff --git a/tests/e2e/lifecycle.py b/tests/e2e/lifecycle.py index eb9704d4dcb..6841740cc48 100644 --- a/tests/e2e/lifecycle.py +++ b/tests/e2e/lifecycle.py @@ -56,7 +56,7 @@ class ResourceManager: strict_cleanup: bool = False _cleanups: List[Callable[[], object]] = field( default_factory=list - ) # mutable-ok: append-only teardown registry + ) def init(self) -> None: """No global setup needed today; present for lifecycle symmetry.""" diff --git a/tests/e2e/mcp/oauth_chat_client.py b/tests/e2e/mcp/oauth_chat_client.py index 2eaf512cfa5..91f2b8b9e83 100644 --- a/tests/e2e/mcp/oauth_chat_client.py +++ b/tests/e2e/mcp/oauth_chat_client.py @@ -80,7 +80,7 @@ async def _browser_follow_authorize(start_url: str, storage_state_path: str) -> code/state are read off the query string.""" from playwright.async_api import async_playwright - captured: dict[str, str] = {} # mutable-ok: hand-off from the request listener + captured: dict[str, str] = {} trail: list[str] = [] # mutable-ok: navigation diagnostics for a failed dance def _note_request(request: object) -> None: @@ -132,7 +132,7 @@ def _oauth_provider(url: str, storage: InMemoryTokenStorage, storage_state_path: """The SDK's real OAuth machinery (RFC 9728/8414 discovery, RFC 7591 DCR, PKCE, token exchange) with the browser leg driven by Playwright against the upstream's consent screen.""" - code_holder: dict[str, str | None] = {} # mutable-ok: hand-off between the two SDK callbacks + code_holder: dict[str, str | None] = {} async def redirect_handler(authorize_url: str) -> None: code, state = await _browser_follow_authorize(authorize_url, storage_state_path) diff --git a/tests/test_litellm/integrations/pointfive/test_upload_client.py b/tests/test_litellm/integrations/pointfive/test_upload_client.py index 50ef085386d..f1196bb6d3c 100644 --- a/tests/test_litellm/integrations/pointfive/test_upload_client.py +++ b/tests/test_litellm/integrations/pointfive/test_upload_client.py @@ -51,8 +51,8 @@ class FakeHTTPClient: presign: Sequence[httpx.Response | Exception] | None = None, put: Sequence[httpx.Response | Exception] | None = None, ) -> None: - self.presign = list(presign) if presign else [_presigned()] # mutable-ok: results are consumed by popping - self.put_results = list(put) if put else [_accepted()] # mutable-ok: results are consumed by popping + self.presign = list(presign) if presign else [_presigned()] + self.put_results = list(put) if put else [_accepted()] self.presign_calls: list[dict] = [] self.put_calls: list[dict] = [] diff --git a/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py b/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py index d0697ca9b0e..2274072afbc 100644 --- a/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py +++ b/tests/test_litellm/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py @@ -119,7 +119,7 @@ def test_responses_call_hits_native_endpoint_with_mcp_tool_untouched() -> None: response: Final = litellm.responses( model="fireworks_ai/accounts/fireworks/models/kimi-k3", input="What is litellm?", - tools=[mcp_tool], # mutable-ok: the Responses API takes tools as a JSON list + tools=[mcp_tool], api_key="fw-test-key", ) url, headers, body = _sent_request(client) @@ -151,7 +151,7 @@ def test_responses_call_forwards_previous_response_id_and_store() -> None: with patch(HTTPX_CLIENT_FACTORY, return_value=client): litellm.responses( model="fireworks_ai/kimi-k3", - input=[tool_output], # mutable-ok: the Responses API takes input items as a JSON list + input=[tool_output], previous_response_id="resp_0e946f2d46bf4b49bf8b29ff78083583", store=True, api_key="fw-test-key", @@ -167,7 +167,7 @@ def test_responses_call_folds_developer_items_into_instructions() -> None: with patch(HTTPX_CLIENT_FACTORY, return_value=client): litellm.responses( model="fireworks_ai/accounts/fireworks/models/kimi-k3", - input=[ # mutable-ok: the Responses API takes input as a JSON list + input=[ {"role": "user", "content": "Hi there"}, {"role": "developer", "content": "Answer with exactly one word."}, {"role": "user", "content": [{"type": "input_text", "text": "What is the capital of France?"}]}, @@ -188,7 +188,7 @@ def test_responses_call_folds_instructions_and_developer_item_into_instructions_ litellm.responses( model="fireworks_ai/accounts/fireworks/models/qwen3p8-2p4t-a95b", instructions="You are a coding agent running in the Codex CLI.", - input=[ # mutable-ok: the Responses API takes input as a JSON list + input=[ { "role": "developer", "content": [{"type": "input_text", "text": "read-only"}], @@ -231,7 +231,7 @@ def test_responses_call_folds_instructions_and_developer_item_with_previous_resp litellm.responses( model="fireworks_ai/accounts/fireworks/models/qwen3p8-2p4t-a95b", instructions="You are a terse assistant.", - input=[ # mutable-ok: the Responses API takes input as a JSON list + input=[ {"role": "developer", "content": "Answer with exactly one word."}, {"role": "user", "content": "And of Spain?"}, ], @@ -258,7 +258,7 @@ def test_responses_call_keeps_a_closing_developer_item_after_an_assistant_turn_i litellm.responses( model="fireworks_ai/accounts/fireworks/models/qwen3p8-2p4t-a95b", instructions="Be terse.", - input=[ # mutable-ok: the Responses API takes input as a JSON list + input=[ {"role": "developer", "content": "Answer with exactly one word."}, {"role": "user", "content": "What is the capital of France?"}, assistant_turn, @@ -280,7 +280,7 @@ def test_responses_call_keeps_a_mid_conversation_system_item_in_place() -> None: with patch(HTTPX_CLIENT_FACTORY, return_value=client): litellm.responses( model="fireworks_ai/accounts/fireworks/models/kimi-k3", - input=[ # mutable-ok: the Responses API takes input as a JSON list + input=[ {"role": "user", "content": "Hi there"}, {"role": "system", "content": "Switch to French."}, {"role": "user", "content": "What is the capital of France?"}, @@ -309,7 +309,7 @@ def test_responses_call_keeps_a_developer_item_with_non_text_parts_in_place_as_a litellm.responses( model="fireworks_ai/accounts/fireworks/models/qwen3p8-2p4t-a95b", instructions="Answer with one word.", - input=[developer_item, {"role": "user", "content": "What is the capital of France?"}], # mutable-ok: JSON list + input=[developer_item, {"role": "user", "content": "What is the capital of France?"}], store=False, api_key="fw-test-key", ) @@ -340,10 +340,10 @@ def test_transform_request_forwards_non_string_instructions_and_input_untouched( user_item: Final = {"role": "user", "content": "What is the capital of France?"} request: Final = FireworksAIResponsesAPIConfig().transform_responses_api_request( model="accounts/fireworks/models/kimi-k3", - input=cast(ResponseInputParam, [developer_item, user_item]), # mutable-ok: JSON list - response_api_optional_request_params={"instructions": ["not", "a", "string"]}, # mutable-ok: base takes a dict + input=cast(ResponseInputParam, [developer_item, user_item]), + response_api_optional_request_params={"instructions": ["not", "a", "string"]}, litellm_params=GenericLiteLLMParams(), - headers={}, # mutable-ok: base takes a dict + headers={}, ) assert request["instructions"] == ["not", "a", "string"] assert tuple(request["input"]) == ( @@ -356,7 +356,7 @@ def test_responses_call_maps_pydantic_developer_items_and_replays_pydantic_outpu client: Final = _mock_http_client(_fireworks_response("accounts/fireworks/models/kimi-k3")) pydantic_input: Final = cast( ResponseInputParam, - [ # mutable-ok: the Responses API takes input as a JSON list + [ EasyInputMessage(role="developer", content="Answer with exactly one word.", type="message"), ResponseReasoningItem(id="rs_1", summary=(), type="reasoning"), ResponseFunctionToolCall( diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index 7b285674145..f8284c5f2f0 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -1853,7 +1853,7 @@ class TestBedrockAgentRuntimePassthroughToggle: request: Final = Mock() request.method = "POST" request.state = SimpleNamespace() - request.json = AsyncMock(return_value={"retrievalQuery": {"text": "hi"}}) # mutable-ok: must be json.dumps-able + request.json = AsyncMock(return_value={"retrievalQuery": {"text": "hi"}}) return request @contextlib.contextmanager diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index d57bed430c1..b83a7f374e4 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -5565,7 +5565,7 @@ async def _drive_passthrough_request_and_capture_logging( mock_request.query_params = QueryParams({}) mock_request.body = AsyncMock(return_value=b'{"model": "gemini-2.0-flash"}') - captured_data: dict = {} # mutable-ok: the pre-call hook records the request data into it + captured_data: dict = {} async def capture_pre_call_hook(user_api_key_dict, data, call_type): captured_data.update(data) @@ -5730,7 +5730,7 @@ async def test_pass_through_request_leaves_guardrail_readable_metadata(): }, ) - observed: dict[str, dict[str, str] | BaseException] = {} # mutable-ok: the pre-call hook records into it + observed: dict[str, dict[str, str] | BaseException] = {} def read_headers_the_way_a_guardrail_does(logging_obj: LiteLLMLoggingObj | None) -> None: assert logging_obj is not None diff --git a/tests/test_litellm/router_utils/test_cooldown_cache.py b/tests/test_litellm/router_utils/test_cooldown_cache.py index 6f90fa8465f..36097852b4b 100644 --- a/tests/test_litellm/router_utils/test_cooldown_cache.py +++ b/tests/test_litellm/router_utils/test_cooldown_cache.py @@ -441,7 +441,7 @@ class SharedRedisDouble: """ def __init__(self) -> None: - self.store: dict = {} # mutable-ok: stands in for Redis' own mutable keyspace + self.store: dict = {} def set_cache(self, key, value, **kwargs): self.store[key] = value diff --git a/tests/test_litellm/test_check_type_discipline.py b/tests/test_litellm/test_check_type_discipline.py index 2d49332e687..52e974b8d80 100644 --- a/tests/test_litellm/test_check_type_discipline.py +++ b/tests/test_litellm/test_check_type_discipline.py @@ -102,23 +102,46 @@ def test_mypy_ignore_shape_is_lit004_not_lit009(tmp_path): def test_ok_suppression_without_reason_is_flagged(tmp_path): - codes = _codes(tmp_path, "y = [] # mutable-ok\n") + codes = _codes(tmp_path, "y: list[int] = [] # mutable-ok\n") assert "LIT005" in codes # reasonless suppression - assert "LIT002" in codes # and it does not suppress, so the construction still trips + assert "LIT001" in codes # and it does not suppress, so the annotation still trips # --------------------------------------------------------------------------- # -# Mutable annotations (LIT001) and construction (LIT002) +# Mutable annotations (LIT001) # --------------------------------------------------------------------------- # def test_mutable_annotation_is_flagged(tmp_path): - assert "LIT001" in _codes(tmp_path, "x: dict[str, int]\n") + assert "LIT001" in _codes(tmp_path, "x: list[int]\n") + assert "LIT001" in _codes(tmp_path, "x: set[int]\n") def test_typing_alias_and_forward_ref_annotations_are_flagged(tmp_path): assert "LIT001" in _codes(tmp_path, "from typing import List\nx: List[int]\n") - assert "LIT001" in _codes(tmp_path, 'x: "dict[str, int]"\n') + assert "LIT001" in _codes(tmp_path, 'x: "list[int]"\n') + assert "LIT001" in _codes(tmp_path, "import collections\nx: collections.deque[int]\n") + assert "LIT001" in _codes(tmp_path, "from collections.abc import MutableSequence\nx: MutableSequence[int]\n") + + +def test_mapping_annotations_are_allowed(tmp_path): + for ann in ( + "dict[str, int]", + "Dict[str, int]", + "DefaultDict[str, int]", + "MutableMapping[str, int]", + "defaultdict[str, int]", + "OrderedDict[str, int]", + ): + source = ( + "from collections import OrderedDict, defaultdict\n" + "from collections.abc import MutableMapping\n" + "from typing import DefaultDict, Dict\n" + f"x: {ann}\n" + ) + assert "LIT001" not in _codes(tmp_path, source), ann + assert "LIT001" not in _codes(tmp_path, 'x: "dict[str, int]"\n') + assert "LIT001" in _codes(tmp_path, "x: dict[str, list[int]]\n") def test_literal_string_args_are_values_not_forward_refs(tmp_path): @@ -127,8 +150,8 @@ def test_literal_string_args_are_values_not_forward_refs(tmp_path): tmp_path, 'from typing import Literal\ndef f(op: Literal["create", "list"] = "create") -> None:\n return None\n', ) - assert "LIT001" not in _codes(tmp_path, 'import typing\nx: typing.Literal["dict"] = "dict"\n') - assert "LIT001" in _codes(tmp_path, 'from typing import Literal\nx: dict[str, Literal["a"]]\n') + assert "LIT001" not in _codes(tmp_path, 'import typing\nx: typing.Literal["set"] = "set"\n') + assert "LIT001" in _codes(tmp_path, 'from typing import Literal\nx: list[Literal["a"]]\n') assert "LIT001" in _codes(tmp_path, "x: \"Literal['x'] | list[int]\"\n") @@ -137,119 +160,19 @@ def test_readonly_annotations_are_clean(tmp_path): assert "LIT001" not in _codes(tmp_path, f"from typing import Mapping, Sequence\nx: {ann}\n") -def test_mutable_construction_is_flagged(tmp_path): - assert "LIT002" in _codes(tmp_path, "y = []\n") - assert "LIT002" in _codes(tmp_path, "z = dict(a=1)\n") +def test_construction_is_not_flagged(tmp_path): + for source in ( + "y = []\n", + "z = dict(a=1)\n", + "s = {1, 2}\n", + "c = [i for i in range(3)]\n", + "import collections\nq = collections.deque()\n", + ): + assert not [c for c in _codes(tmp_path, source) if c.startswith("LIT00")], source -def test_construction_inside_annotation_is_exempt(tmp_path): - # `Callable[[int], str]` carries a list display that is type syntax, not construction. - assert "LIT002" not in _codes( - tmp_path, "from typing import Callable\ndef f(cb: Callable[[int], str]) -> None:\n return None\n" - ) - - -def test_generator_and_tuple_are_not_construction(tmp_path): - assert "LIT002" not in _codes(tmp_path, "g = tuple(i for i in range(3))\n") - assert "LIT002" not in _codes(tmp_path, "t = (1, 2, 3)\n") - - -def test_dict_list_set_method_calls_are_not_construction(tmp_path): - # `.dict()` / `.list()` / `.set()` are common method names (e.g. pydantic model.dict()), - # not collection construction; only the unqualified builtins count. - assert "LIT002" not in _codes(tmp_path, "d = model.dict()\n") - assert "LIT002" not in _codes(tmp_path, "s = obj.set()\n") - assert "LIT002" in _codes(tmp_path, "d = dict(a=1)\n") # unqualified still counts - - -def test_qualified_collections_constructors_still_count(tmp_path): - # collections concretes are rarely method names, so a qualified call still flags. - assert "LIT002" in _codes(tmp_path, "import collections\nq = collections.deque()\n") - assert "LIT002" in _codes(tmp_path, "import collections\nm = collections.defaultdict(list)\n") - - -def test_value_frozen_by_wrapper_is_exempt(tmp_path): - assert "LIT002" not in _codes(tmp_path, "from types import MappingProxyType\nm = MappingProxyType({'a': 1})\n") - assert "LIT002" not in _codes(tmp_path, "import types\nm = types.MappingProxyType({'a': 1})\n") - assert "LIT002" not in _codes(tmp_path, "from types import MappingProxyType\nm = MappingProxyType(dict(a=1))\n") - assert "LIT002" not in _codes(tmp_path, "f = frozenset({1, 2})\n") - assert "LIT002" not in _codes(tmp_path, "t = tuple([1, 2])\n") - - -def test_same_named_method_does_not_exempt_its_argument(tmp_path): - assert "LIT002" in _codes(tmp_path, "t = obj.tuple([1, 2])\n") - assert "LIT002" in _codes(tmp_path, "f = obj.frozenset({1, 2})\n") - assert "LIT002" in _codes(tmp_path, "m = obj.MappingProxyType({'a': 1})\n") - - -def test_mutable_nested_inside_frozen_wrapper_still_counts(tmp_path): - assert "LIT002" in _codes(tmp_path, "from types import MappingProxyType\nm = MappingProxyType({'a': []})\n") - - -def test_unfrozen_literal_still_counts(tmp_path): - assert "LIT002" in _codes(tmp_path, "from types import MappingProxyType\nd = {'a': 1}\nm = MappingProxyType(d)\n") - - -def test_lit002_fix_message_names_mappingproxytype(tmp_path): - f = tmp_path / "snippet.py" - f.write_text("x = {'a': 1}\n", encoding="utf-8") - messages = [v.message for v in checker.check_file(f) if v.code == "LIT002"] - assert "MappingProxyType" in messages[0] - - -def test_mutable_ok_with_reason_suppresses_both_rules(tmp_path): - codes = _codes(tmp_path, "x: dict[str, int] = {} # mutable-ok: in-place buffer mutated hot path\n") - assert "LIT001" not in codes - assert "LIT002" not in codes - - -def test_typeddict_annotated_dict_literal_is_exempt(tmp_path): - assert "LIT002" not in _codes( - tmp_path, "from typing import Final\nfrom foo import MyTD\nx: Final[MyTD] = {'a': 1}\n" - ) - assert "LIT002" not in _codes(tmp_path, "from foo import MyTD\nx: MyTD = {'a': 1}\n") - assert "LIT002" not in _codes(tmp_path, "from typing import Final\nx: Final['MyTD'] = {'a': 1}\n") - assert "LIT002" not in _codes(tmp_path, "import foo\nfrom typing import Final\nx: Final[foo.MyTD] = {'a': 1}\n") - - -def test_wrapped_typeddict_annotations_share_the_exemption(tmp_path): - assert "LIT002" not in _codes( - tmp_path, "from typing import Final, Optional\nx: Final[Optional[MyTD]] = {'a': 1}\n" - ) - assert "LIT002" not in _codes( - tmp_path, "from typing import Annotated, Final\nx: Final[Annotated[MyTD, 'meta']] = {'a': 1}\n" - ) - assert "LIT002" not in _codes( - tmp_path, "from typing import ClassVar\nclass C:\n x: ClassVar[MyTD] = {'a': 1}\n" - ) - assert "LIT002" not in _codes(tmp_path, "from typing import Final\nx: Final[MyTD | None] = {'a': 1}\n") - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final[dict[str, int] | None] = {'a': 1}\n") - - -def test_bare_final_dict_literal_still_counts(tmp_path): - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final = {'a': 1}\n") - assert "LIT002" in _codes(tmp_path, "from typing import ClassVar\nclass C:\n x: ClassVar = {'a': 1}\n") - - -def test_non_typeddict_annotations_do_not_exempt(tmp_path): - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final[dict[str, int]] = {'a': 1}\n") - assert "LIT002" in _codes( - tmp_path, "from collections.abc import Mapping\nfrom typing import Final\nx: Final[Mapping[str, int]] = {'a': 1}\n" - ) - assert "LIT002" in _codes(tmp_path, "from typing import Any, Final\nx: Final[Any] = {'a': 1}\n") - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final[object] = {'a': 1}\n") - - -def test_typeddict_exemption_covers_only_dict_literals(tmp_path): - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final[MyTD] = dict(a=1)\n") - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final[MyTD] = {k: 1 for k in ('a',)}\n") - - -def test_nested_dict_literals_share_the_typeddict_exemption(tmp_path): - assert "LIT002" not in _codes( - tmp_path, "from typing import Final\nx: Final[Outer] = {'inner': {'a': 1}, 'steps': ({'b': 2},)}\n" - ) - assert "LIT002" in _codes(tmp_path, "from typing import Final\nx: Final[Outer] = {'tags': ['a']}\n") +def test_mutable_ok_with_reason_suppresses_annotation(tmp_path): + assert "LIT001" not in _codes(tmp_path, "x: list[int] = [] # mutable-ok: in-place buffer mutated hot path\n") # --------------------------------------------------------------------------- # diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index 4f7a51eb531..eb1e6ccca41 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -2434,7 +2434,7 @@ def test_mock_completion_usage_falls_back_to_default_without_admission_count(): _ADMISSION_INPUT_TOKENS: Final = 51234 -def _admission_metadata(input_tokens: int) -> dict[str, object]: # mutable-ok: logging writes into metadata +def _admission_metadata(input_tokens: int) -> dict[str, object]: return {"user_api_key_budget_reservation": {"reserved_cost": 1.0, "input_tokens": input_tokens}} diff --git a/tests/test_litellm_rust/conftest.py b/tests/test_litellm_rust/conftest.py index 4387ea2e2fd..5bb25f3e6af 100644 --- a/tests/test_litellm_rust/conftest.py +++ b/tests/test_litellm_rust/conftest.py @@ -42,7 +42,7 @@ def _list_attribute(container: ModuleType, attribute: str) -> list[object]: def _isolated_list(container: ModuleType, attribute: str) -> Iterator[None]: source: Final = _list_attribute(container, attribute) original: Final = list(source) - source.clear() # mutable-ok: test isolation mutates global registries by design + source.clear() try: yield finally: