From c35bc0b84e8f59ec6b3808cc026f8e63c09d672a Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Wed, 30 Sep 2026 19:34:30 -0700 Subject: [PATCH] chore(lint): keep the leftover mutable-ok markers for a follow-up Restore the ~1.4k `# mutable-ok` markers stripped in the previous commit so this PR only touches the checker, its tests, the budget, and docs. Those markers no longer suppress anything, so `# mutable-ok` is exempt from LIT013 until a follow-up strips them. --- .../enterprise_callbacks/secret_detection.py | 2 +- litellm/__init__.py | 4 +- litellm/_logging.py | 16 ++- litellm/_redis.py | 4 +- litellm/a2a_protocol/main.py | 4 +- litellm/batches/batch_utils.py | 2 +- litellm/caching/affinity_cache.py | 4 +- litellm/caching/caching_handler.py | 4 +- litellm/caching/dual_cache.py | 12 +- litellm/caching/evicted_client_closer.py | 2 +- litellm/caching/redis_batch.py | 4 +- .../transformation.py | 16 +-- litellm/cost_calculator.py | 4 +- litellm/experimental_mcp_client/client.py | 4 +- litellm/experimental_mcp_client/tools.py | 4 +- .../SlackAlerting/slack_alerting.py | 8 +- .../azure_sentinel/azure_sentinel.py | 6 +- .../clickhouse/clickhouse_spend_logger.py | 8 +- litellm/integrations/custom_guardrail.py | 10 +- litellm/integrations/datadog/datadog.py | 4 +- .../integrations/datadog/datadog_llm_obs.py | 2 +- litellm/integrations/langfuse/langfuse.py | 6 +- .../integrations/newrelic/newrelic_metrics.py | 6 +- litellm/integrations/otel/model/metadata.py | 2 +- litellm/integrations/otel/model/payloads.py | 2 +- litellm/integrations/otel/model/request_io.py | 2 +- litellm/integrations/otel/plumbing/context.py | 6 +- .../integrations/otel/plumbing/providers.py | 4 +- litellm/integrations/otel/plumbing/routing.py | 6 +- litellm/integrations/otel/presets/langfuse.py | 2 +- litellm/integrations/otel/presets/signoz.py | 4 +- litellm/integrations/otel/presets/weave.py | 2 +- litellm/integrations/pointfive/logger.py | 4 +- .../integrations/pointfive/upload_client.py | 4 +- litellm/integrations/prometheus.py | 2 +- litellm/integrations/s3_v2.py | 4 +- litellm/integrations/shadow_eval_logger.py | 34 +++-- .../websearch_interception/handler.py | 2 +- litellm/integrations/zerobus/logger.py | 4 +- .../agentic_followup_kwargs.py | 2 +- .../chat_completion_agentic_loop.py | 2 +- litellm/litellm_core_utils/core_helpers.py | 8 +- .../litellm_core_utils/get_litellm_params.py | 8 +- .../litellm_core_utils/get_model_cost_map.py | 2 +- .../internal_call_metadata.py | 14 +- .../json_fragment_accumulator.py | 4 +- litellm/litellm_core_utils/litellm_logging.py | 20 +-- .../llm_cost_calc/guardrail_cost.py | 2 +- litellm/litellm_core_utils/llm_judge.py | 2 +- .../litellm_core_utils/llm_request_utils.py | 8 +- .../llm_response_utils/response_metadata.py | 2 +- litellm/litellm_core_utils/logging_utils.py | 2 +- .../prompt_templates/common_utils.py | 52 +++++--- .../prompt_templates/factory.py | 2 +- .../prompt_templates/image_handling.py | 24 ++-- .../mid_conversation_system.py | 2 +- .../litellm_core_utils/provider_affinity.py | 6 +- .../litellm_core_utils/sentry_scrubbing.py | 8 +- .../streaming_chunk_builder_utils.py | 4 +- .../litellm_core_utils/streaming_handler.py | 4 +- litellm/litellm_core_utils/tokenizer.py | 66 +++++---- litellm/llms/a2a/chat/transformation.py | 2 +- .../chat/guardrail_translation/handler.py | 24 ++-- litellm/llms/anthropic/chat/handler.py | 6 +- litellm/llms/anthropic/chat/transformation.py | 14 +- litellm/llms/anthropic/common_utils.py | 54 +++++--- .../adapters/streaming_iterator.py | 4 +- .../pass_through/adapters/transformation.py | 2 +- .../pass_through/messages/response_cache.py | 4 +- .../messages/streaming_iterator.py | 42 +++--- .../responses_adapters/streaming_iterator.py | 26 ++-- .../responses_adapters/transformation.py | 41 +++--- .../llms/anthropic/prompt_cache_prediction.py | 2 +- litellm/llms/azure/azure.py | 6 +- litellm/llms/azure/chat/gpt_transformation.py | 2 +- .../azure/chat/o_series_transformation.py | 2 +- litellm/llms/azure/common_utils.py | 2 +- litellm/llms/azure/search/transformation.py | 14 +- .../azure_model_router/transformation.py | 2 +- .../image_generation/flux_transformation.py | 4 +- .../azure_ai/passthrough/transformation.py | 4 +- .../llms/azure_ai/responses/transformation.py | 2 +- .../base_llm/guardrail_translation/utils.py | 6 +- .../llms/base_llm/responses/codex_compat.py | 2 +- .../llms/base_llm/search/transformation.py | 2 +- .../base_llm/vector_store/transformation.py | 14 +- .../bedrock/chat/converse_transformation.py | 4 +- litellm/llms/bedrock/common_utils.py | 6 +- litellm/llms/bedrock/files/transformation.py | 8 +- .../bedrock/messages/mantle_transformation.py | 4 +- litellm/llms/bedrock/realtime/handler.py | 2 +- .../llms/bedrock/realtime/transformation.py | 2 +- .../llms/bedrock/responses/transformation.py | 16 ++- litellm/llms/bedrock/search/transformation.py | 14 +- .../bedrock_mantle/chat/transformation.py | 2 +- .../responses/transformation.py | 10 +- litellm/llms/custom_httpx/llm_http_handler.py | 20 +-- litellm/llms/dashscope/chat/transformation.py | 2 +- litellm/llms/deepseek/chat/transformation.py | 8 +- .../audio_transcription/transformation.py | 4 +- litellm/llms/edenai/chat/transformation.py | 6 +- litellm/llms/edenai/common_utils.py | 4 +- .../llms/edenai/embedding/transformation.py | 6 +- .../edenai/image_generation/transformation.py | 6 +- .../edenai/text_to_speech/transformation.py | 6 +- litellm/llms/edenai/videos/transformation.py | 6 +- litellm/llms/fal_ai/chat/transformation.py | 16 ++- .../flux_lora_depth_transformation.py | 4 +- .../llms/fal_ai/image_edit/transformation.py | 10 +- .../gpt_image_2_transformation.py | 6 +- litellm/llms/fal_ai/videos/transformation.py | 36 +++-- .../llms/fireworks_ai/chat/transformation.py | 16 +-- litellm/llms/fireworks_ai/common_utils.py | 2 +- .../fireworks_ai/completion/transformation.py | 24 ++-- .../fireworks_ai/responses/transformation.py | 8 +- .../audio_transcription/transformation.py | 12 +- .../guardrail_translation/__init__.py | 2 +- .../guardrail_translation/handler.py | 4 +- litellm/llms/gigachat/chat/streaming.py | 4 +- litellm/llms/gigachat/chat/transformation.py | 6 +- .../llms/gigachat/embedding/transformation.py | 2 +- .../gigachat/passthrough/transformation.py | 16 ++- litellm/llms/groq/chat/transformation.py | 2 +- .../hosted_vllm/image_edit/transformation.py | 4 +- .../llms/hosted_vllm/videos/transformation.py | 12 +- .../litellm_proxy/skills/transformation.py | 4 +- litellm/llms/meta/realtime/transformation.py | 2 +- .../mistral/audio_speech/transformation.py | 6 +- .../llms/mistral/batches/transformation.py | 8 +- litellm/llms/mistral/common_utils.py | 6 +- litellm/llms/mistral/files/transformation.py | 10 +- .../mongodb/vector_stores/transformation.py | 4 +- litellm/llms/nadir/chat/transformation.py | 2 +- litellm/llms/nimble/search/transformation.py | 8 +- .../nvidia_nim/passthrough/transformation.py | 2 +- .../rerank/ranking_transformation.py | 10 +- .../llms/openai/chat/gpt_transformation.py | 4 +- .../chat/guardrail_translation/handler.py | 14 +- litellm/llms/openai/openai.py | 12 +- litellm/llms/openai/organization_costs.py | 6 +- .../guardrail_translation/handler.py | 10 +- .../guardrail_translation/tool_merge.py | 4 +- .../llms/openai/responses/transformation.py | 10 +- .../videos/guardrail_translation/__init__.py | 2 +- .../videos/guardrail_translation/handler.py | 4 +- litellm/llms/openai_like/model_info.py | 2 +- litellm/llms/sail/chat/transformation.py | 2 +- litellm/llms/sail/common_utils.py | 2 +- litellm/llms/snowflake/chat/transformation.py | 28 ++-- .../llms/tinyfish/search/transformation.py | 2 +- .../llms/together_ai/chat/transformation.py | 10 +- .../valkey/vector_stores/transformation.py | 12 +- .../realtime_transformation.py | 2 +- litellm/llms/vertex_ai/common_utils.py | 6 +- .../vertex_ai/interactions/transformation.py | 4 +- .../text_to_speech/transformation.py | 30 +++-- .../llama3/transformation.py | 4 +- .../vertex_gemma_models/transformation.py | 2 +- litellm/llms/wandb/chat/transformation.py | 2 +- .../xai/audio_transcription/transformation.py | 4 +- litellm/llms/xai/batches/handler.py | 12 +- litellm/llms/xai/batches/transformation.py | 6 +- litellm/llms/xai/chat/transformation.py | 4 +- litellm/llms/xai/files/transformation.py | 6 +- litellm/main.py | 14 +- .../mcp_server/auth/user_api_key_auth_mcp.py | 24 +++- .../_experimental/mcp_server/contracts.py | 6 +- litellm/proxy/_experimental/mcp_server/db.py | 14 +- .../mcp_server/elicitation_handler.py | 2 +- .../guardrail_translation/handler.py | 4 +- .../_experimental/mcp_server/mcp_debug.py | 6 +- .../mcp_server/mcp_server_manager.py | 12 +- .../mcp_server/oauth_identity_binding.py | 8 +- .../_experimental/mcp_server/operations.py | 28 ++-- .../sso_assertion_refresher.py | 2 +- .../mcp_server/rest_endpoints.py | 2 +- .../mcp_server/result_conversion.py | 12 +- .../proxy/_experimental/mcp_server/server.py | 12 +- .../_experimental/mcp_server/tool_search.py | 18 ++- .../_experimental/mcp_server/toolset_db.py | 2 +- litellm/proxy/_types.py | 4 +- .../proxy/agent_endpoints/a2a_endpoints.py | 4 +- .../auth/agent_access_groups.py | 6 +- .../auth/managed_authorization.py | 6 +- litellm/proxy/agent_endpoints/endpoints.py | 6 +- litellm/proxy/agent_endpoints/kill_switch.py | 2 +- .../claude_code_marketplace.py | 6 +- .../anthropic_endpoints/gateway_endpoints.py | 6 +- .../anthropic_endpoints/skills_endpoints.py | 2 +- .../streaming_model_restamp.py | 2 +- litellm/proxy/auth/auth_checks.py | 20 +-- litellm/proxy/auth/auth_exception_handler.py | 2 +- litellm/proxy/auth/auth_object_prefetch.py | 2 +- litellm/proxy/auth/auth_utils.py | 2 +- litellm/proxy/auth/login_throttle.py | 2 +- litellm/proxy/auth/password_policy.py | 4 +- litellm/proxy/auth/user_api_key_auth.py | 12 +- .../litellm_executed_batches.py | 18 ++- .../client/cli/commands/claude_settings.py | 10 +- .../client/cli/commands/configure_profiles.py | 2 +- .../client/cli/commands/configure_setup.py | 8 +- litellm/proxy/client/cli/commands/pi.py | 18 +-- .../client/cli/commands/statusline_script.py | 6 +- litellm/proxy/common_request_processing.py | 12 +- .../proxy/common_utils/cache_aware_routing.py | 2 +- litellm/proxy/common_utils/config_includes.py | 4 +- .../proxy/common_utils/error_body_call_id.py | 2 +- .../proxy/common_utils/http_parsing_utils.py | 2 +- .../common_utils/openai_error_payload.py | 2 +- .../common_utils/prompt_cache_pricing.py | 2 +- .../proxy/common_utils/reset_budget_job.py | 10 +- .../proxy/common_utils/semantic_text_index.py | 8 +- litellm/proxy/db/db_spend_update_writer.py | 14 +- litellm/proxy/db/db_url_settings.py | 2 +- litellm/proxy/db/gateway_request_tracking.py | 4 +- litellm/proxy/db/shadow_eval_funnel.py | 2 +- .../agent_skills_endpoints.py | 2 +- litellm/proxy/engine/analysis.py | 18 ++- litellm/proxy/engine/endpoints.py | 2 +- litellm/proxy/engine/inference.py | 12 +- .../team_metadata_validator_e2e.py | 2 +- litellm/proxy/guardrails/_content_utils.py | 6 +- .../guardrails/auto_router_compression.py | 6 +- .../proxy/guardrails/guardrail_endpoints.py | 2 +- .../guardrail_hooks/agent_365/__init__.py | 4 +- .../guardrail_hooks/agent_365/agent_365.py | 8 +- .../guardrail_hooks/alice/__init__.py | 4 +- .../guardrails/guardrail_hooks/alice/alice.py | 14 +- .../guardrail_hooks/azure/prompt_shield.py | 2 +- .../guardrail_hooks/bedrock_guardrails.py | 72 ++++++---- .../guardrail_hooks/conduct/__init__.py | 4 +- .../crowdstrike_aidr/crowdstrike_aidr.py | 2 +- .../custom_code/custom_code_guardrail.py | 4 +- .../guardrail_hooks/custom_code/sandbox.py | 6 +- .../generic_guardrail_api.py | 2 +- .../guardrail_hooks/headroom/headroom.py | 2 +- .../hiddenlayer/hiddenlayer.py | 2 +- .../guardrail_hooks/lakera_ai_v2.py | 14 +- .../model_armor/model_armor.py | 8 +- .../guardrails/guardrail_hooks/presidio.py | 2 +- .../prompt_security/prompt_security.py | 8 +- .../guardrail_hooks/singulr/singulr.py | 4 +- .../guardrail_hooks/straiker/straiker.py | 6 +- .../guardrail_hooks/tool_permission.py | 8 +- .../guardrail_hooks/typesafe/__init__.py | 8 +- .../guardrail_hooks/typesafe/typesafe.py | 38 +++--- litellm/proxy/health_check.py | 8 +- .../health_endpoints/_health_endpoints.py | 2 +- .../proxy/hooks/autorouter_baseline_cache.py | 6 +- litellm/proxy/hooks/batch_rate_limiter.py | 4 +- .../proxy/hooks/model_max_budget_limiter.py | 2 +- .../hooks/parallel_request_limiter_v3.py | 75 +++++++---- litellm/proxy/hooks/responses_id_security.py | 2 +- litellm/proxy/litellm_pre_call_utils.py | 4 +- .../access_group_endpoints.py | 8 +- .../auto_router_endpoints.py | 126 ++++++++++++------ .../common_daily_activity.py | 8 +- .../config_override_endpoints.py | 42 +++--- .../cost_tracking_settings.py | 14 +- .../gateway_request_endpoints.py | 2 +- .../internal_user_endpoints.py | 2 +- .../key_management_endpoints.py | 24 ++-- .../management_v1/teams.py | 4 +- .../management_v1/users.py | 4 +- .../mcp_management_endpoints.py | 24 ++-- .../model_management_endpoints.py | 22 +-- .../prompt_cache_prediction.py | 4 +- .../prompt_caching_requests.py | 2 +- .../management_endpoints/scim/scim_v2.py | 12 +- .../team_callback_endpoints.py | 35 ++--- .../management_endpoints/team_endpoints.py | 62 ++++++--- litellm/proxy/management_endpoints/ui_sso.py | 6 +- .../auto_router_permissions.py | 4 +- .../management_helpers/bulk_user_creation.py | 22 +-- .../management_helpers/bulk_user_deletion.py | 8 +- .../object_permission_utils.py | 2 +- .../resource_display_names.py | 6 +- .../team_metadata_validation.py | 12 +- .../admission_control_middleware.py | 8 +- .../batch_guardrails.py | 6 +- .../openai_files_endpoints/common_utils.py | 2 +- .../llm_passthrough_endpoints.py | 52 ++++---- ...zure_speech_passthrough_logging_handler.py | 2 +- ...end_medical_passthrough_logging_handler.py | 2 +- .../openai_passthrough_logging_handler.py | 10 +- .../tinyfish_passthrough_logging_handler.py | 8 +- .../transcribe_passthrough_logging_handler.py | 14 +- .../typesafe_passthrough_logging_handler.py | 6 +- .../vertex_passthrough_logging_handler.py | 2 +- .../pass_through_endpoints.py | 16 +-- .../proxy/policy_engine/pipeline_executor.py | 16 +-- .../proxy/policy_engine/response_retrieval.py | 6 +- litellm/proxy/proxy_server.py | 44 +++--- .../public_endpoints/public_endpoints.py | 2 +- .../public_endpoints/public_v1/model_hub.py | 4 +- litellm/proxy/rag_endpoints/endpoints.py | 6 +- .../proxy/response_api_endpoints/endpoints.py | 18 +-- litellm/proxy/roi_calculator/github.py | 2 +- litellm/proxy/route_llm_request.py | 2 +- .../spend_tracking/baseline_accounting.py | 2 +- .../spend_tracking/budget_reservation.py | 4 +- .../spend_tracking/ptu_flat_cost_rollup.py | 20 +-- .../spend_tracking/spend_capture_rate.py | 2 +- .../spend_management_endpoints.py | 16 ++- litellm/proxy/tracing_endpoints.py | 4 +- .../latest_release_endpoints.py | 4 +- .../proxy_setting_endpoints.py | 24 ++-- .../user_banner_endpoints.py | 6 +- litellm/proxy/utils.py | 38 ++++-- .../autorouter_session_repository.py | 4 +- litellm/repositories/chunked_in.py | 6 +- .../repositories/managed_batch_repository.py | 12 +- .../managed_file_content_repository.py | 8 +- litellm/repositories/model_repository.py | 4 +- litellm/repositories/unit_of_work.py | 12 +- .../repositories/user_banner_repository.py | 10 +- litellm/repositories/user_repository.py | 8 +- .../session_handler.py | 4 +- .../streaming_iterator.py | 4 +- .../transformation.py | 52 +++++--- litellm/responses/main.py | 4 +- .../responses/mcp/mcp_streaming_iterator.py | 10 +- litellm/responses/mcp/request_context.py | 2 +- litellm/responses/streaming_iterator.py | 14 +- litellm/responses/utils.py | 6 +- litellm/router.py | 50 ++++--- .../auto_router/litellm_encoder.py | 2 +- litellm/router_strategy/budget_limiter.py | 6 +- .../complexity_router/complexity_router.py | 42 +++--- .../complexity_router/config.py | 10 +- .../complexity_router/jev_classifier.py | 8 +- .../router_utils/fallback_event_handlers.py | 2 +- litellm/router_utils/prompt_caching_cache.py | 4 +- litellm/router_utils/routing_read_batch.py | 14 +- .../rust_bridge/callbacks_legacy_python.py | 2 +- litellm/rust_bridge/failures.py | 4 +- litellm/rust_bridge/messages/route_host.py | 2 +- litellm/tracing/decode.py | 9 +- litellm/tracing/receiver.py | 2 +- litellm/types/integrations/newrelic.py | 2 +- litellm/types/llms/openai.py | 6 +- .../auto_router_endpoints.py | 8 +- .../proxy/guardrails/guardrail_hooks/aim.py | 2 +- .../guardrail_hooks/cato_networks.py | 2 +- litellm/types/responses/main.py | 6 +- litellm/types/utils.py | 8 +- litellm/utils.py | 16 +-- litellm/vector_stores/main.py | 8 +- scripts/check_type_discipline.py | 8 +- .../observability/test_s3_v2_upload_fanout.py | 2 +- .../test_priority_rate_limit_headers.py | 2 +- .../hooks/test_autorouter_baseline_cache.py | 2 +- .../test_llm_pass_through_endpoints.py | 2 +- .../policy_engine/test_policy_matcher.py | 16 +-- .../test_spend_tracking_utils.py | 4 +- .../test_post_call_failure_hook.py | 6 +- .../langfuse/test_langfuse_sdk.py | 2 +- .../pointfive/test_upload_client.py | 4 +- ...t_fireworks_ai_responses_transformation.py | 24 ++-- tests/unit/test_check_type_discipline.py | 19 ++- tests/unit/types/test_litellm_params.py | 6 +- 361 files changed, 1946 insertions(+), 1423 deletions(-) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py b/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py index 8b17cf13cc4..f0f85178672 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"] = [ + data["prompt"] = [ # mutable-ok: data["prompt"] is a list on the wire self.redact_text(item, source="prompt") if isinstance(item, str) and item else item diff --git a/litellm/__init__.py b/litellm/__init__.py index d79086a90f3..58827b60a98 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -663,7 +663,7 @@ azure_anthropic_models: Set = set() azure_text_models: Set = set() anyscale_models: Set = set() cerebras_models: Set = set() -nadir_models: Set = set() +nadir_models: Set = set() # mutable-ok: provider registry, filled from model_cost at import like every sibling provider galadriel_models: Set = set() nvidia_nim_models: Set = set() nvidia_riva_models: Set = set() @@ -697,7 +697,7 @@ recraft_models: Set = set() cometapi_models: Set = set() oci_models: Set = set() vercel_ai_gateway_models: Set = set() -edenai_models: Set = set() +edenai_models: Set = set() # mutable-ok: filled from the price map at import, like the sibling provider sets volcengine_models: Set = set() wandb_models: Set = set(WANDB_MODELS) ovhcloud_models: Set = set() diff --git a/litellm/_logging.py b/litellm/_logging.py index 5e02ff8de35..c65795babff 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -352,9 +352,13 @@ def _replace_string_leaves(value: object, values: Iterator[str]) -> object: if isinstance(value, str): return next(values) if isinstance(value, dict): - return {key: _replace_string_leaves(child, values) for key, child in value.items()} + return { # mutable-ok: LogRecord extras must keep JSON dict shape for handlers + key: _replace_string_leaves(child, values) for key, child in value.items() + } if isinstance(value, list): - return [_replace_string_leaves(child, values) for child in value] + return [ # mutable-ok: LogRecord extras must keep JSON list shape for handlers + _replace_string_leaves(child, values) for child in value + ] if isinstance(value, tuple): return tuple(_replace_string_leaves(child, values) for child in value) return value @@ -364,9 +368,13 @@ def _sort_processed_sets(original: object, processed: object) -> object: if isinstance(original, set) and isinstance(processed, list): return sorted(processed) if isinstance(original, dict) and isinstance(processed, dict): - return {key: _sort_processed_sets(original.get(key), value) for key, value in processed.items()} + return { # mutable-ok: sorting nested sets must preserve the surrounding JSON dict + key: _sort_processed_sets(original.get(key), value) for key, value in processed.items() + } if isinstance(original, list) and isinstance(processed, list): - return [_sort_processed_sets(before, after) for before, after in zip(original, processed)] + return [ # mutable-ok: sorting nested sets must preserve the surrounding JSON list + _sort_processed_sets(before, after) for before, after in zip(original, processed) + ] if isinstance(original, tuple) and isinstance(processed, tuple): return tuple(_sort_processed_sets(before, after) for before, after in zip(original, processed)) return processed diff --git a/litellm/_redis.py b/litellm/_redis.py index 791fa4ce783..12c65205dfc 100644 --- a/litellm/_redis.py +++ b/litellm/_redis.py @@ -233,7 +233,7 @@ def _coerce_redis_kwargs_types( "socket_keepalive": bool, } ) - result: Final = dict(redis_kwargs) + result: Final = dict(redis_kwargs) # mutable-ok: per-key try/except coercion below needs to drop individual keys for key, value in redis_kwargs.items(): if not isinstance(value, str): continue @@ -803,7 +803,7 @@ def _credential_provider_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) + return dict(kept, credential_provider=credential_provider) # mutable-ok: the branches below mutate these kwargs def get_redis_client(**env_overrides): diff --git a/litellm/a2a_protocol/main.py b/litellm/a2a_protocol/main.py index 3a1d2c70b12..aa41e63b40b 100644 --- a/litellm/a2a_protocol/main.py +++ b/litellm/a2a_protocol/main.py @@ -145,7 +145,7 @@ def _a2a_cost_params(litellm_params: Mapping[str, object] | None) -> Mapping[str def _card_http_kwargs(extra_headers: dict[str, str] | None) -> dict[str, object] | None: - return {"headers": extra_headers} if extra_headers else None + return {"headers": extra_headers} if extra_headers else None # mutable-ok: a2a-sdk's get_agent_card takes a dict def _agent_card_path(litellm_params: Mapping[str, object]) -> str | None: @@ -612,7 +612,7 @@ def _build_streaming_logging_obj( logging_obj.model_call_details["agent_id"] = agent_id _request_context: Final = (("metadata", metadata), ("proxy_server_request", proxy_server_request)) - _litellm_params: Final = dict( + _litellm_params: Final = dict( # mutable-ok: Logging.litellm_params is declared as a dict (*_a2a_cost_params(litellm_params).items(), *((key, value) for key, value in _request_context if value)) ) diff --git a/litellm/batches/batch_utils.py b/litellm/batches/batch_utils.py index c58b0d721ad..9974e77d017 100644 --- a/litellm/batches/batch_utils.py +++ b/litellm/batches/batch_utils.py @@ -127,7 +127,7 @@ async def _handle_completed_batch( return BatchCostUsageResult( cost=0.0, usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), - models=[], + models=[], # mutable-ok: no output file means no model was ever priced; BatchCostUsageResult.models requires list[str] 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/affinity_cache.py b/litellm/caching/affinity_cache.py index 3387d4953a0..2712679b99b 100644 --- a/litellm/caching/affinity_cache.py +++ b/litellm/caching/affinity_cache.py @@ -103,10 +103,10 @@ async def claim_affinity_pin( try: claim_script: Final = redis_cache.async_register_script(_CLAIM_PIN_SCRIPT) args: Final = ( - json.dumps(dict(pin_value)), + json.dumps(dict(pin_value)), # mutable-ok: JSON serialization requires dict, not a generic Mapping int(ttl_seconds), *( - (json.dumps(tuple(dict(value) for value in eligible_values)),) + (json.dumps(tuple(dict(value) for value in eligible_values)),) # mutable-ok: JSON requires dict if eligible_values is not None else () ), diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index ee022822872..1b4f446ee0c 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -702,14 +702,14 @@ class LLMCachingHandler: ) merged: Final = EmbeddingResponse( model=cached.model, - data=[ + data=[ # mutable-ok: EmbeddingResponse.data is a pydantic list field item if item is not None else Embedding(embedding=next(fresh_items)["embedding"], index=position, object="embedding") for position, item in enumerate(cached.data) ], usage=merged_usage, - hidden_params={ + hidden_params={ # mutable-ok: EmbeddingResponse._hidden_params is a mutable dict field **cached._hidden_params, "cache_hit": True, }, diff --git a/litellm/caching/dual_cache.py b/litellm/caching/dual_cache.py index 3db78241f4b..47ce1d35895 100644 --- a/litellm/caching/dual_cache.py +++ b/litellm/caching/dual_cache.py @@ -252,7 +252,9 @@ class DualCache(BaseCache): if value is not None: self.in_memory_cache.set_cache(key, value, **self._backfill_kwargs(kwargs)) - return list(redis_result.get(key) if value is None else value for key, value in zip(keys, result)) + return list( # mutable-ok: public list contract + 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 @@ -327,8 +329,8 @@ class DualCache(BaseCache): def reserve_redis_batch_reads(self, keys: Sequence[str]) -> tuple[list[str], dict[str, float | None]]: """Reserve the memory-missed keys whose throttled Redis reads are due, as a batch read would.""" if self.redis_cache is None: - return [], {} - key_list: Final = list(keys) + return [], {} # mutable-ok: API contract returns an empty list and dictionary + key_list: Final = list(keys) # mutable-ok: batch_get_cache takes a list memory: Final = self.in_memory_cache in_memory_result: Final = ( None @@ -384,7 +386,7 @@ class DualCache(BaseCache): async def declare_batch_get(self, keys: Sequence[str], batch: RedisBatch) -> DeclaredBatchRead: pending: Final = await self._prepare_batch_get( - list(keys), + list(keys), # mutable-ok: the shared batch read takes a list local_only=False, throttle_redis=False, ) @@ -631,7 +633,7 @@ class DualCache(BaseCache): parent_otel_span: Span | None = None, ) -> None: batch: Final = None if self.redis_cache is None else active_post_call_redis_batch(self.redis_cache) - operations: Final = list(increment_list) + operations: Final = list(increment_list) # mutable-ok: both increment pipelines take a list if batch is None: await self.async_increment_cache_pipeline(operations, parent_otel_span=parent_otel_span) return diff --git a/litellm/caching/evicted_client_closer.py b/litellm/caching/evicted_client_closer.py index b22cd3aecd1..6e4635dd83a 100644 --- a/litellm/caching/evicted_client_closer.py +++ b/litellm/caching/evicted_client_closer.py @@ -238,7 +238,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()) + bucket: Final = self._buckets.setdefault(_bucket_key(pending), deque()) # mutable-ok: FIFO by design while bucket and bucket[0].client_ref() is None: bucket.popleft() self._pending_count -= 1 diff --git a/litellm/caching/redis_batch.py b/litellm/caching/redis_batch.py index b3596aab6a1..d408aac8cda 100644 --- a/litellm/caching/redis_batch.py +++ b/litellm/caching/redis_batch.py @@ -33,7 +33,7 @@ from litellm.types.services import ServiceTypes _T = TypeVar("_T") _ScriptArg = str | bytes | int | float -SettledHook = Callable[[asyncio.Future[_T]], Awaitable[None] | None] +SettledHook = Callable[[asyncio.Future[_T]], Awaitable[None] | None] # mutable-ok: Callable params POST_CALL_FLUSH_DEADLINE_SECONDS: Final = 1.0 @@ -139,7 +139,7 @@ class _MGet(_Op[Mapping[str, object]]): ) async def run_alone(self) -> Mapping[str, object]: - found: Mapping[str, object] = await self._redis_cache.async_batch_get_cache(key_list=list(self._keys)) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # untyped cache API + found: Mapping[str, object] = await self._redis_cache.async_batch_get_cache(key_list=list(self._keys)) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # untyped cache API # mutable-ok: the cache API takes a list if any(key not in found for key in self._keys): raise ConnectionError("batch get did not return every key") return found diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 391dbc44eec..71d3f1e900e 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -123,13 +123,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] + stored: Final = [_reasoning_item_to_response_input(item) for item in items] # mutable-ok: API message payload 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] + return [dict(item) for item in replayed] # mutable-ok: API message payload def _build_reasoning_item( @@ -441,7 +441,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"), @@ -475,7 +475,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)), @@ -531,11 +531,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): ) -> "ResponseText": existing: Final = cast( # cast-ok: text field is a ResponseText | dict[str, Any] | None union "dict[str, object]", - dict(responses_api_request).get("text") or {}, + dict(responses_api_request).get("text") or {}, # mutable-ok: one-shot merge seed ) return cast( # cast-ok: merged mapping is a valid ResponseText shape "ResponseText", - {**existing, **update}, + {**existing, **update}, # mutable-ok: one-shot merged payload ) def _build_sanitized_litellm_params(self, litellm_params: dict) -> dict[str, object]: @@ -1506,7 +1506,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): # tool call; per-stream callers already received it via # output_item.added and the argument delta events return ModelResponseStream( - choices=[ + choices=[ # mutable-ok: ModelResponseStream coerces only list choices StreamingChoices( index=0, delta=Delta( @@ -1612,7 +1612,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): ) ], usage=usage, - provider_specific_fields=dict(provider_metadata) or None, + provider_specific_fields=dict(provider_metadata) or None, # mutable-ok: field is typed dict **( MappingProxyType({"service_tier": served_service_tier}) if isinstance(served_service_tier, str) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 41a7ef1ab64..238b7cc3fdd 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -2874,7 +2874,9 @@ class ResponsesWebSocketTokenUsageProcessor(BaseTokenUsageProcessor): collected_usage_objects: Final = ResponsesWebSocketTokenUsageProcessor.collect_usage_from_responses_ws_results( results ) - return ResponsesWebSocketTokenUsageProcessor.combine_usage_objects(list(collected_usage_objects)) + return ResponsesWebSocketTokenUsageProcessor.combine_usage_objects( + list(collected_usage_objects) # mutable-ok: combine_usage_objects requires a list parameter + ) _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 f133e4837a6..4e3b92edc89 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -1136,7 +1136,7 @@ class MCPClient: async def _list_resource_templates_operation(session: ClientSession) -> ListResourceTemplatesResult: capabilities: Final = session.server_capabilities if capabilities is not None and capabilities.resources is None: - return ListResourceTemplatesResult(resource_templates=[]) + return ListResourceTemplatesResult(resource_templates=[]) # mutable-ok: MCP result payload try: return ListResourceTemplatesResult( resource_templates=await self._list_optional_pages( @@ -1150,7 +1150,7 @@ class MCPClient: verbose_logger.debug( "MCP client list_resource_templates is unsupported by %s: %s", self.server_url or "stdio", error ) - return ListResourceTemplatesResult(resource_templates=[]) + return ListResourceTemplatesResult(resource_templates=[]) # mutable-ok: MCP result payload try: result: Final = await self.run_with_session(_list_resource_templates_operation) diff --git a/litellm/experimental_mcp_client/tools.py b/litellm/experimental_mcp_client/tools.py index a73a12b9e03..df644fd7f4a 100644 --- a/litellm/experimental_mcp_client/tools.py +++ b/litellm/experimental_mcp_client/tools.py @@ -171,7 +171,9 @@ async def load_mcp_tools( """ tools: Final = await list_tools_with_pagination(session) if format == "openai": - return [transform_mcp_tool_to_openai_tool(mcp_tool=tool) for tool in tools] + return [ # mutable-ok: public API returns a list + 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 6ff048c484d..50f63316a62 100644 --- a/litellm/integrations/SlackAlerting/slack_alerting.py +++ b/litellm/integrations/SlackAlerting/slack_alerting.py @@ -1131,7 +1131,7 @@ Model Info: message=message, level=level, alert_type=AlertType.model_deprecation_warnings, - alerting_metadata={ + alerting_metadata={ # mutable-ok: send_alert takes a dict payload "deprecated_count": len(snapshot.deprecated), "imminent_count": len(snapshot.imminent), "upcoming_count": len(snapshot.upcoming), @@ -1245,8 +1245,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}, - order={"created_at": "desc"}, + 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 ), from_attributes=True, ) @@ -2011,7 +2011,7 @@ Model Info: message="\n\n".join(event.message for event in typed_events), level="High", alert_type=alert_type, - alerting_metadata={}, + alerting_metadata={}, # mutable-ok: send_alert takes a dict payload ) 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 84dfc770e8f..db5f790615f 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 = [] + self.log_queue = [] # mutable-ok: queue ownership is detached before the async send 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 = [] + self.audit_log_queue = [] # mutable-ok: queue ownership is detached before the async send 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] + merged: Final = [*undelivered, *queue] # mutable-ok: queue trimming returns a mutable logger queue overflow: Final = len(merged) - self.max_queue_size if overflow <= 0: return merged diff --git a/litellm/integrations/clickhouse/clickhouse_spend_logger.py b/litellm/integrations/clickhouse/clickhouse_spend_logger.py index c1401e111bb..cd575fff903 100644 --- a/litellm/integrations/clickhouse/clickhouse_spend_logger.py +++ b/litellm/integrations/clickhouse/clickhouse_spend_logger.py @@ -58,7 +58,7 @@ def _json(value: object) -> str: def _json_mapping(value: Mapping[str, Any]) -> str: - return _json(dict(value)) + return _json(dict(value)) # mutable-ok: [LIT002] JSON serialization requires a dict def _find_traceparent(metadata: Mapping[str, Any], kwargs: Mapping[str, Any]) -> tuple[str, str]: @@ -88,8 +88,8 @@ def _cache_tokens(usage: Mapping[str, Any]) -> tuple[int, int]: def _request_tags(value: object) -> list[str]: if not isinstance(value, list): - return [] - return [str(tag) for tag in value] + return [] # mutable-ok: [LIT002] empty spend-log tag payload + return [str(tag) for tag in value] # mutable-ok: [LIT002] SpendLogRecord schema def _session_id(payload: StandardLoggingPayload, kwargs: Mapping[str, Any]) -> str: @@ -165,6 +165,6 @@ class ClickHouseSpendLogger(ClickHouseBatchLogger): if payload is None or _is_trace_ingest(payload): return row: Final = spend_log_row_from_payload(payload, kwargs) - self.enqueue([dict(row)]) + self.enqueue([dict(row)]) # mutable-ok: [LIT002] batch logger API except Exception as e: verbose_logger.exception("ClickHouseSpendLogger: failed to log request: %s", e) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 7ddfa73bca5..2eb9cfb5042 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -363,7 +363,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} + advisory_message: Final = {"role": "system", "content": message} # mutable-ok: plain dict for live request existing_messages: Final = data.get("messages") existing_input: Final = data.get("input") existing_instructions: Final = data.get("instructions") @@ -374,7 +374,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 = [ + messages_with_instructions_note: Final = [ # mutable-ok: fresh list *existing_messages, advisory_message, ] @@ -386,7 +386,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] + messages_with_input_note: Final = [*existing_messages, advisory_message] # mutable-ok: fresh list 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. @@ -400,10 +400,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] + messages_without_input_note: Final = [*existing_messages, advisory_message] # mutable-ok: fresh list data["messages"] = messages_without_input_note # rebind-ok: mutates caller's dict by design return True - sole_message: Final = [advisory_message] + sole_message: Final = [advisory_message] # mutable-ok: plain list for the live JSON request data["messages"] = sole_message # rebind-ok: mutates caller's dict by design return True diff --git a/litellm/integrations/datadog/datadog.py b/litellm/integrations/datadog/datadog.py index d70acd51679..2ca8b0ed236 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 + self.log_queue = list(cancelled.undelivered) + self.log_queue # mutable-ok: logger queue remains appendable 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) + return list(undelivered) # mutable-ok: caller prepends records to the logger queue @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 22bb50cd739..98aac7336bf 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 { + return { # mutable-ok: a fresh record built per entry, handed straight to the span serializer 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/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index 055819df86c..9b860840e69 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -868,8 +868,10 @@ class LangFuseLogger: "id": clean_metadata.pop("generation_id", generation_id), "input": masked_input if not mask_input else "redacted-by-litellm", "output": masked_output if not mask_output else "redacted-by-litellm", - "cost_details": {"total": cost} if usage is not None and isinstance(cost, (int, float)) else None, - "metadata": { + "cost_details": {"total": cost} # mutable-ok: langfuse serializes this payload + if usage is not None and isinstance(cost, (int, float)) + else None, + "metadata": { # mutable-ok: langfuse serializes this payload, a proxy is not json-encodable **log_requester_metadata(redact_user_api_key_info(metadata=allowlisted_metadata)), # pyright: ignore[reportArgumentType] # TypedDict in, plain metadata dict out **enrichments, **_lookup_ids(litellm_call_id, response_obj), diff --git a/litellm/integrations/newrelic/newrelic_metrics.py b/litellm/integrations/newrelic/newrelic_metrics.py index a2cedeba0f5..0a45a7e52c3 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]] = { + attributes: Final[Mapping[str, str]] = { # mutable-ok: JSON leaf; safe_dumps stringifies MappingProxyType 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]] = { + attributes: Final[Mapping[str, str]] = { # mutable-ok: JSON leaf; safe_dumps stringifies MappingProxyType 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) + self.log_queue[:] = list(survivors) # mutable-ok: leave late arrivals for the next serialized drain async def _drain_flush_once(self) -> None: """Attempt every queued record once, in ``batch_size`` chunks, without diff --git a/litellm/integrations/otel/model/metadata.py b/litellm/integrations/otel/model/metadata.py index 7cb64debfe0..ede8ac99467 100644 --- a/litellm/integrations/otel/model/metadata.py +++ b/litellm/integrations/otel/model/metadata.py @@ -384,7 +384,7 @@ def metadata_from_request_data(data: object) -> Mapping[str, object] | None: def flatten_metadata(raw: Mapping[str, object]) -> Iterator[tuple[str, str]]: """Scalar leaves of a nested metadata mapping, keyed by their dotted path.""" - stack: Final = list(tuple(raw.items())[::-1]) + stack: Final = list(tuple(raw.items())[::-1]) # mutable-ok: iterative worklist keeps the walk off the call stack while stack: key, value = stack.pop() if (nested := as_str_mapping(value)) is not None: diff --git a/litellm/integrations/otel/model/payloads.py b/litellm/integrations/otel/model/payloads.py index e06cc1d0407..c007eda7707 100644 --- a/litellm/integrations/otel/model/payloads.py +++ b/litellm/integrations/otel/model/payloads.py @@ -803,7 +803,7 @@ def _joined_choice(parts: tuple[str, ...]) -> tuple[_Choice, ...]: def _text_completion_choice(choice: Mapping[str, object], text: str) -> Mapping[str, object]: synthesized: Final = _text_choice(text, as_str(choice.get("finish_reason"))) merged: Final = (*choice.items(), *synthesized.items()) - return {k: v for k, v in merged if k != "text"} + return {k: v for k, v in merged if k != "text"} # mutable-ok: mappers json.dumps and isinstance(dict) it def _completion_choices(response: Mapping[str, object]) -> tuple[Mapping[str, object], ...]: diff --git a/litellm/integrations/otel/model/request_io.py b/litellm/integrations/otel/model/request_io.py index a315dadba2d..4e80fb91993 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), + chunks=list(chunks), # mutable-ok: stream_chunk_builder takes a list messages=_MESSAGES.validate_python(data.get("messages")), ) except (litellm.APIError, ValidationError): diff --git a/litellm/integrations/otel/plumbing/context.py b/litellm/integrations/otel/plumbing/context.py index 19356939046..f5f221cf278 100644 --- a/litellm/integrations/otel/plumbing/context.py +++ b/litellm/integrations/otel/plumbing/context.py @@ -380,8 +380,10 @@ def inject_trace_context(headers: Mapping[str, str], parent_span: object = None) """ context: Final = _outgoing_trace_context(parent_span) if context is None: - return dict(headers) - carrier: Final = {key: value for key, value in headers.items() if key.lower() not in _W3C_TRACE_HEADERS} + return dict(headers) # mutable-ok: OpenTelemetry propagator requires a mutable carrier + carrier: Final = { # mutable-ok: OpenTelemetry propagator requires a mutable carrier + key: value for key, value in headers.items() if key.lower() not in _W3C_TRACE_HEADERS + } _PROPAGATOR.inject(carrier, context=_propagated_context(headers, context)) return carrier diff --git a/litellm/integrations/otel/plumbing/providers.py b/litellm/integrations/otel/plumbing/providers.py index 8b01750b8f2..25878e8a302 100644 --- a/litellm/integrations/otel/plumbing/providers.py +++ b/litellm/integrations/otel/plumbing/providers.py @@ -636,7 +636,9 @@ 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((ident, p) for ident, p in live if ident in self._exporting) + 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 + ) 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 d7b1cadfc92..b2d1f50f370 100644 --- a/litellm/integrations/otel/plumbing/routing.py +++ b/litellm/integrations/otel/plumbing/routing.py @@ -171,7 +171,9 @@ 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() + 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 # Oldest-first so an overflow of draining providers sheds the stalest. self._retired: OrderedDict[TracerProvider, None] = OrderedDict() # mutable-ok: draining evicted providers @@ -391,7 +393,7 @@ class TenantTracerCache: if project_headers and kind not in _GRPC_KINDS else base ) - update: Final = { + update: Final = { # mutable-ok: model_copy(update=...) requires a plain dict field: value for field, value in (("headers", routed), ("endpoint", endpoint)) if (field == "headers" and routed != spec.headers) diff --git a/litellm/integrations/otel/presets/langfuse.py b/litellm/integrations/otel/presets/langfuse.py index 3ff3b521d29..9149e0c0d94 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={ + update={ # mutable-ok: pydantic model_copy takes a plain update mapping "exporters": credential_gated_exporters(base.exporters, ExporterOwner.LANGFUSE_OTEL), "mapper_names": mappers, } diff --git a/litellm/integrations/otel/presets/signoz.py b/litellm/integrations/otel/presets/signoz.py index 1d55f99cb52..c4d7ed48a38 100644 --- a/litellm/integrations/otel/presets/signoz.py +++ b/litellm/integrations/otel/presets/signoz.py @@ -91,5 +91,5 @@ def signoz_dynamic_headers( ) -> dict[str, str]: # mutable-ok: DYNAMIC_HEADERS_BY_CALLBACK returns a dict key: Final = params.get("signoz_ingestion_key") if _tenant_endpoint_is_unusable(params) or not key: - return {} - return {"signoz-ingestion-key": key} + return {} # mutable-ok: same registry contract + return {"signoz-ingestion-key": key} # mutable-ok: same registry contract diff --git a/litellm/integrations/otel/presets/weave.py b/litellm/integrations/otel/presets/weave.py index 856e784460a..644cd39ad36 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={ + update={ # mutable-ok: pydantic model_copy takes a plain update mapping "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 de800f09e7f..c352dac11e7 100644 --- a/litellm/integrations/pointfive/logger.py +++ b/litellm/integrations/pointfive/logger.py @@ -207,7 +207,9 @@ 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)) + 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 + ) 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 d3708d48661..56ba6689017 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={ + headers={ # mutable-ok: AsyncHTTPHandler.post types headers as dict "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) + headers: Final = dict(PUT_HEADERS, Host=host) if host else dict(PUT_HEADERS) # mutable-ok: put wants dict 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 0a14cd7cf18..c7bf291a887 100644 --- a/litellm/integrations/prometheus.py +++ b/litellm/integrations/prometheus.py @@ -1195,7 +1195,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} + kept_kwargs: Final = {**kwargs, "labelnames": kept} # mutable-ok: ** needs a mapping to override labelnames 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 2ed56409a4c..88d7906cc4b 100644 --- a/litellm/integrations/s3_v2.py +++ b/litellm/integrations/s3_v2.py @@ -628,7 +628,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): ######################################################### uploads: Final = self._batch_file_elements(batch) if self._batch_file_mode_active() else batch self._flush_retries = 0 - self._flush_dropped = {} + self._flush_dropped = {} # mutable-ok: per-flush drop marks read back by _upload_bounded stale: Final = min(self._requeued_count, len(uploads)) if len(uploads) == len(batch) else 0 order: Final = (*range(stale, len(uploads)), *range(stale)) ordered: Final = await asyncio.gather(*(self._upload_outcome(uploads[i]) for i in order)) @@ -680,7 +680,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): self.max_queue_size, overflow, ) - self.log_queue = [ + self.log_queue = [ # mutable-ok: log_queue is the flush buffer shared with custom_batch_logger *requeued, *arrivals, ][overflow:] diff --git a/litellm/integrations/shadow_eval_logger.py b/litellm/integrations/shadow_eval_logger.py index 7f5d9fedd3d..4ff49f3cb84 100644 --- a/litellm/integrations/shadow_eval_logger.py +++ b/litellm/integrations/shadow_eval_logger.py @@ -357,7 +357,11 @@ class GuardrailRequestSnapshot: if fingerprint is None: return None return GuardrailRequestSnapshot( - body=MappingProxyType(_CHAT_REQUEST_ADAPTER.validate_python(independent_snapshot(dict(body)))), + body=MappingProxyType( + _CHAT_REQUEST_ADAPTER.validate_python( + independent_snapshot(dict(body)) # mutable-ok: snapshot helper requires a plain dictionary + ) + ), fingerprint=fingerprint, ) @@ -809,7 +813,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}) + return job.model_copy(update={"attempts": attempts, "spend": spend}) # mutable-ok: pydantic update payload _jobs_cache: Final = InMemoryCache(max_size_in_memory=4, default_ttl=_JOBS_CACHE_TTL_SECONDS) @@ -860,9 +864,9 @@ class ShadowEvalLogger(CustomLogger): return _EMPTY_JOBS try: records: Final = await prisma.db.litellm_shadowevaljob.find_many( - where={ + where={ # mutable-ok: Prisma filter "stopped_at": None, - "ends_at": {"gt": datetime.now(timezone.utc)}, + "ends_at": {"gt": datetime.now(timezone.utc)}, # mutable-ok: Prisma filter }, ) grouped: Final = ( @@ -870,12 +874,12 @@ class ShadowEvalLogger(CustomLogger): by=["job_id"], count=True, sum={"judge_cost": True, "shadow_cost": True, "shadow_classifier_cost": True}, - where={"job_id": {"in": [str(record.id) for record in records]}}, + where={"job_id": {"in": [str(record.id) for record in records]}}, # mutable-ok: Prisma filter ) if records else () ) - attempt_stats: Final = { + attempt_stats: Final = { # mutable-ok: frozen snapshot of the grouped read str(row["job_id"]): ( int(row["_count"]["_all"]), _leg_eval_spend(row["_sum"] or _EMPTY_METADATA), @@ -948,13 +952,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)) + raw_meta: Final = get_litellm_metadata_from_kwargs(dict(kwargs)) # mutable-ok: helper needs dict 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)): + if should_redact_message_logging(dict(kwargs)): # mutable-ok: predicate takes a plain dict return metadata: Final = payload.get("metadata") or _EMPTY_METADATA # Each identity the request resolved to is a candidate target; JWT-auth @@ -995,7 +999,7 @@ class ShadowEvalLogger(CustomLogger): sample: Final = _judgeable_sample( ops, sample_kwargs, - MappingProxyType(dict(payload.get("model_parameters") or {})), + MappingProxyType(dict(payload.get("model_parameters") or {})), # mutable-ok: frozen snapshot response_obj, ) if sample is None: @@ -1242,7 +1246,7 @@ class ShadowEvalLogger(CustomLogger): return try: await prisma.db.litellm_shadowevalattempt.create( - data={ + data={ # mutable-ok: Prisma payload "job_id": job.id, "request_id": request_id, "router_name": router_name, @@ -1283,10 +1287,12 @@ class ShadowEvalLogger(CustomLogger): try: response: Final = await router.acompletion( model=target_model, - messages=[dict(m) for m in messages], # pyright: ignore[reportArgumentType] # snapshot of the SDK's own message dicts + 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 metadata=shadow_metadata, num_retries=0, - fallbacks=[], + fallbacks=[], # mutable-ok: SDK kwarg; a failed shadow is a recorded error, never a spend multiplier **shadow_params, ) except Exception as e: # noqa: BLE001 # provider errors become error rows, not crashes @@ -1335,8 +1341,8 @@ 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 = [ - {"role": "system", "content": PAIRWISE_JUDGE_SYSTEM_PROMPT}, + judge_messages: Final = [ # mutable-ok: SDK takes a list + {"role": "system", "content": PAIRWISE_JUDGE_SYSTEM_PROMPT}, # mutable-ok: SDK message { "role": "user", "content": _judge_user_prompt(conversation, response_a, response_b, _tool_definitions_text(tools)), diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 1db94e82066..6ebf485d717 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -1683,7 +1683,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 { + return { # mutable-ok: litellm's metadata channel is a plain dict its logging path reads and enriches **user_api_key_metadata, **parent_correlation.as_search_metadata(), "model_group": search_tool_name, diff --git a/litellm/integrations/zerobus/logger.py b/litellm/integrations/zerobus/logger.py index da729b82b7c..e2007218c8e 100644 --- a/litellm/integrations/zerobus/logger.py +++ b/litellm/integrations/zerobus/logger.py @@ -188,7 +188,9 @@ class ZerobusLogger(CustomBatchLogger): def _payload_for(self, kwargs: Mapping[str, object]) -> Mapping[str, object] | None: """The payload to buffer, redacted the way the framework redacts the success path.""" - details: Final = self.redact_standard_logging_payload_from_model_call_details(dict(kwargs)) + 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 + ) payload: Final = details.get("standard_logging_object") if not isinstance(payload, dict): return None diff --git a/litellm/litellm_core_utils/agentic_followup_kwargs.py b/litellm/litellm_core_utils/agentic_followup_kwargs.py index d9ffa9a9582..50ec19f62c4 100644 --- a/litellm/litellm_core_utils/agentic_followup_kwargs.py +++ b/litellm/litellm_core_utils/agentic_followup_kwargs.py @@ -15,7 +15,7 @@ def build_agentic_followup_kwargs( fingerprint: str, ) -> Mapping[str, object]: """Kwargs for an agentic follow-up call: the request's kwargs overlaid by the plan's, never repeating a key already sent as a request param""" - seen: Final = [*fingerprints, fingerprint] + seen: Final = [*fingerprints, fingerprint] # mutable-ok: the chat loop's settings reader only accepts a list return MappingProxyType( { key: value diff --git a/litellm/litellm_core_utils/chat_completion_agentic_loop.py b/litellm/litellm_core_utils/chat_completion_agentic_loop.py index 9d7c9864e62..e0bd85a7937 100644 --- a/litellm/litellm_core_utils/chat_completion_agentic_loop.py +++ b/litellm/litellm_core_utils/chat_completion_agentic_loop.py @@ -125,7 +125,7 @@ def _with_agentic_loop_metadata(kwargs_for_followup: Mapping[str, object]) -> Ma return MappingProxyType( { **kwargs_for_followup, - "litellm_metadata": dict( + "litellm_metadata": dict( # mutable-ok: the follow-up call's logging and proxy hooks write into litellm_metadata in place chain( metadata.items() if isinstance(metadata, dict) else (), ( diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 532fcb04657..b095b4b12c6 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -579,7 +579,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() } @@ -601,13 +601,15 @@ def independent_snapshot( and isinstance(original_value, dict) and "litellm_parent_otel_span" in original_value ): - return { + return { # mutable-ok: same request-payload shape as data **copied_value, "litellm_parent_otel_span": original_value["litellm_parent_otel_span"], } return copied_value - return {key: _copied_value(key, value) for key, value in sanitized.items()} + return { # mutable-ok: same request-payload shape as data + 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_litellm_params.py b/litellm/litellm_core_utils/get_litellm_params.py index 5adb9a80f9c..b8441d2bc6d 100644 --- a/litellm/litellm_core_utils/get_litellm_params.py +++ b/litellm/litellm_core_utils/get_litellm_params.py @@ -99,7 +99,9 @@ class InvalidControlOption: def parse_control_options(kwargs: Mapping[str, object]) -> ControlOptions | InvalidControlOption: - given: Final = {name: kwargs[name] for name in _CONTROL_OPTION_NAMES if name in kwargs} + given: Final = { # mutable-ok: TypeAdapter.validate_python takes a dict + name: kwargs[name] for name in _CONTROL_OPTION_NAMES if name in kwargs + } try: return _CONTROL_OPTIONS.validate_python(given) except ValidationError as e: @@ -116,8 +118,8 @@ def stored_control_options(litellm_params: Mapping[str, object]) -> ControlOptio def with_control_options(litellm_params: Mapping[str, object], control: ControlOptions) -> dict[str, object]: if control == ControlOptions(): - return dict(litellm_params) - return {**litellm_params, CONTROL_OPTIONS_KEY: control} + return dict(litellm_params) # mutable-ok: completion() hands litellm_params to provider code typed as dict + return {**litellm_params, CONTROL_OPTIONS_KEY: control} # mutable-ok: same dict contract as above def _get_base_model_from_litellm_call_metadata( diff --git a/litellm/litellm_core_utils/get_model_cost_map.py b/litellm/litellm_core_utils/get_model_cost_map.py index 159590da0f4..5471fe50d5f 100644 --- a/litellm/litellm_core_utils/get_model_cost_map.py +++ b/litellm/litellm_core_utils/get_model_cost_map.py @@ -656,7 +656,7 @@ def get_model_cost_map( if isinstance(outcome, _FetchAttemptRetryable) and max_attempts > 1: threading.Thread( target=_retry_remote_fetch_in_background, - kwargs={ + kwargs={ # mutable-ok: threading requires a mutable keyword-arguments mapping "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 d844cbae367..87f007ca1d5 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"} + return {k: v for k, v in auth.items() if k != "budget_reservation"} # mutable-ok: SDK metadata value 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}) + return model_copy(update={"budget_reservation": None}) # mutable-ok: pydantic update payload return auth def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]: # mutable-ok: SDK metadata kwarg - return { + return { # mutable-ok: SDK metadata kwarg 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 @@ -138,8 +138,10 @@ def forwarded_internal_call_metadata( parent's full context still describes the call being made. """ if not parent_metadata: - return {} - return _sanitized(parent_metadata) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin} + return {} # mutable-ok: SDK metadata kwarg + return _sanitized(parent_metadata) | { # mutable-ok: SDK metadata kwarg + INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin + } def parent_session_kwargs(request_kwargs: Mapping[str, object] | None) -> Mapping[str, str]: @@ -165,4 +167,4 @@ def sanitized_forwardable_call_metadata( 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} + return _sanitized(identity) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin} # mutable-ok: SDK metadata kwarg diff --git a/litellm/litellm_core_utils/json_fragment_accumulator.py b/litellm/litellm_core_utils/json_fragment_accumulator.py index 19e0b17d852..e262f05932c 100644 --- a/litellm/litellm_core_utils/json_fragment_accumulator.py +++ b/litellm/litellm_core_utils/json_fragment_accumulator.py @@ -50,7 +50,7 @@ class JSONFragmentAccumulator: unconsumed: Final = self._buffer[self._offset :] self._buffer = unconsumed + "".join(self._chunks) self._offset = 0 - self._chunks = [] + self._chunks = [] # mutable-ok: see __init__ def pop_next_value(self) -> tuple[bool, object]: """ @@ -88,7 +88,7 @@ class JSONFragmentAccumulator: def set(self, value: str) -> None: """Replace the buffer's contents with a single fragment.""" - self._chunks = [] + self._chunks = [] # mutable-ok: see __init__ self._buffer = value self._offset = 0 stripped: Final = value.rstrip() diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 329d538c13d..2162a200565 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -692,7 +692,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] = {} + self.response_timing_metrics: Mapping[str, float] = {} # mutable-ok: kept deep-copyable # Passthrough endpoint guardrails config for field targeting self.passthrough_guardrails_config: dict[str, object] | None = None @@ -716,7 +716,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) + self.response_timing_metrics = dict(timing_metrics) # mutable-ok: kept deep-copyable def add_dynamic_callback(self, callback: CustomLogger) -> None: self.dynamic_input_callbacks = self._with_dynamic_callback(self.dynamic_input_callbacks, callback) @@ -4139,7 +4139,7 @@ class Logging(LiteLLMLoggingBaseClass): if result.status == "completed": return InteractionsAPIResponse.model_validate( result.model_dump( - exclude={ + exclude={ # mutable-ok: pydantic types exclude as set[str], which a frozenset does not satisfy "event_type", "delta", "index", @@ -5242,7 +5242,9 @@ 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)]}) + return config.model_copy( + update={"exporters": [spec for spec in config.exporters if _is_gated(spec)]} # mutable-ok: model_copy update + ) def _is_gated(spec: "ExporterSpec") -> bool: @@ -5714,7 +5716,7 @@ class StandardLoggingPayloadSetup: if key not in user_metadata } ) - return {**user_metadata, **model_metadata} + return {**user_metadata, **model_metadata} # mutable-ok: function contract returns a plain dict @staticmethod def get_standard_logging_metadata( @@ -6562,7 +6564,9 @@ 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 {} + timing_metrics: Final = ( + getattr(logging_obj, "response_timing_metrics", None) or {} # mutable-ok: empty fallback + ) clean_hidden_params["litellm_overhead_time_ms"] = timing_metrics.get("litellm_overhead_time_ms") model_cost_information: Final = StandardLoggingPayloadSetup.get_model_cost_information( @@ -6677,14 +6681,14 @@ def get_standard_logging_object_payload( cost_breakdown=request_cost_breakdown, autorouter_savings=autorouter_savings, autorouter_savings_estimate=( - { + { # mutable-ok: spend-log JSON serialization requires plain mappings "version": 3, "status": "unknown", "reason": "pending_projection", } if captured_baseline is not None else ( - { + { # mutable-ok: spend-log JSON serialization requires plain mappings "version": 1, "status": "estimated" if autorouter_savings is not None else "unknown", "reason": "uncached_usage" if autorouter_savings is not None else "baseline_unavailable", 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 19adf1a30a7..54cdf2cb8ff 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 { + return { # mutable-ok: stamped into guardrail_information, which safe_dumps only serializes as a plain dict 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 ed3b89dd420..b632d3a9af9 100644 --- a/litellm/litellm_core_utils/llm_judge.py +++ b/litellm/litellm_core_utils/llm_judge.py @@ -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()} + return {str(k): v for k, v in parsed.items()} # mutable-ok: plain parsed-JSON payload 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 7f9557003fd..04824a5bf39 100644 --- a/litellm/litellm_core_utils/llm_request_utils.py +++ b/litellm/litellm_core_utils/llm_request_utils.py @@ -16,7 +16,9 @@ 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]] - ] = [(key, value, 0)] + ] = [ # mutable-ok: depth-capped stack walks nested JSON into multipart names + (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() @@ -46,7 +48,9 @@ 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]] - ] = [(key, value, 0)] + ] = [ # mutable-ok: depth-capped stack walks nested JSON into multipart names + (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/response_metadata.py b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py index 7d9c33da923..503814cc143 100644 --- a/litellm/litellm_core_utils/llm_response_utils/response_metadata.py +++ b/litellm/litellm_core_utils/llm_response_utils/response_metadata.py @@ -71,7 +71,7 @@ def response_timing_metrics( receive_anchored: Final = timing_window[1] total_response_time_ms: Final = (end_time.timestamp() - window_start.timestamp()) * 1000 if not include_overhead: - return {"_response_ms": total_response_time_ms} + return {"_response_ms": total_response_time_ms} # mutable-ok: read-only timing result 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 a2d4d91a6db..38a501ecaae 100644 --- a/litellm/litellm_core_utils/logging_utils.py +++ b/litellm/litellm_core_utils/logging_utils.py @@ -312,7 +312,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 { + return { # mutable-ok: loggers isinstance-check the request body as a dict "model": model, "voice": voice, **{key: value for key, value in optional_params.items() if key != "extra_headers"}, diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 2f1a4147544..3b6827375f3 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -1274,7 +1274,7 @@ def _flatten_schema_against_root( if not is_object_schema: return schema - merged_properties: Final = { + merged_properties: Final = { # mutable-ok: tool parameters are JSON dicts name: value for source in (*reversed(branches), schema) for name, value in _schema_properties(source).items() } required_names: Final = _schema_required_names(schema).union( @@ -1282,7 +1282,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 { + return { # mutable-ok: tool parameters are JSON dicts **kept, "type": "object", "properties": merged_properties, @@ -1309,7 +1309,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, {}) + return _flatten_schema_against_root(schema, schema, frozenset(), 0, {}) # mutable-ok: fresh per-call $ref memo _SUBSCHEMA_KEYWORDS: Final = frozenset( @@ -1384,7 +1384,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 = { + kept: Final = { # mutable-ok: tool parameters are JSON dicts 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) @@ -1394,14 +1394,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 = { + kept: Final = { # mutable-ok: tool parameters are JSON dicts 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] + items: Final = [rebuilt.get(id(sub), sub) for sub in value] # mutable-ok: tool parameters are JSON lists 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) @@ -1433,7 +1433,7 @@ def tool_with_sanitized_parameters( sanitized: Final = sanitize(parameters) if sanitized is parameters: return tool - return {**tool, "function": {**function, "parameters": sanitized}} + return {**tool, "function": {**function, "parameters": sanitized}} # mutable-ok: request tools are JSON dicts def _get_image_mime_type_from_url(url: str) -> str | None: @@ -1689,7 +1689,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} + marked: Final = {**target, "prompt_cache_breakpoint": marker} # mutable-ok: API message payload return cast(_MarkedT, marked) # cast-ok: same block shape as the input plus the marker key @@ -1703,7 +1703,9 @@ def strip_litellm_internal_message_fields(message: AllMessageValues) -> AllMessa return message return cast( # cast-ok: same TypedDict minus internal keys AllMessageValues, - {key: value for key, value in message.items() if key not in LITELLM_INTERNAL_MESSAGE_FIELDS}, + { # 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 + }, ) @@ -2192,9 +2194,11 @@ def _split_images_from_tool_message( ) if not image_parts: return message, () - remaining_parts = [part for part in content if not _is_image_url_part(part)] + remaining_parts = [ # mutable-ok: tool message content must stay a json list + 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} + rewritten = {**message, "content": new_content} # mutable-ok: chat messages are plain json dicts return cast(AllMessageValues, rewritten), image_parts # cast-ok: dict spread keeps keys like cache_control @@ -2202,12 +2206,14 @@ 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 = [image for _, images in split_results for image in images] - rewritten_messages = [message for message, _ in split_results] + 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 if not hoisted_images: return rewritten_messages boundary_part = ChatCompletionTextObject(type="text", text=TOOL_RESULT_IMAGE_BOUNDARY) - hoisted_content = [boundary_part, *hoisted_images] + hoisted_content = [boundary_part, *hoisted_images] # mutable-ok: user message content must be a json list rewritten_messages.append(ChatCompletionUserMessage(role="user", content=hoisted_content)) return rewritten_messages @@ -2231,7 +2237,7 @@ def hoist_images_from_tool_messages( """ if not any(_tool_message_carries_image(message) for message in messages): return messages - return [ + return [ # mutable-ok: pipelines mutate message lists 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) @@ -2253,9 +2259,11 @@ 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 = [part for part in content if not _is_tool_reference_part(part)] + remaining_parts = [ # mutable-ok: tool message content must stay a json list + 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} + rewritten = {**message, "content": new_content} # mutable-ok: chat messages are plain json dicts return cast(AllMessageValues, rewritten) # cast-ok: dict spread keeps keys like cache_control @@ -2273,7 +2281,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] + return [_drop_tool_reference_parts(message) for message in messages] # mutable-ok: pipelines mutate message lists INSTRUCTION_MESSAGE_ROLES: Final = frozenset({"system", "developer"}) @@ -2286,7 +2294,7 @@ def _is_instruction_message(message: AllMessageValues) -> bool: def system_messages_first( messages: list[AllMessageValues], # mutable-ok: message pipelines type messages as mutable lists ) -> list[AllMessageValues]: # mutable-ok: message pipelines type messages as mutable lists - return [ + return [ # mutable-ok: pipelines mutate message lists *(message for message in messages if _is_instruction_message(message)), *(message for message in messages if not _is_instruction_message(message)), ] @@ -2307,14 +2315,16 @@ def _merge_system_message_run(run: Sequence[AllMessageValues]) -> AllMessageValu if all(isinstance(content, str) for content in contents): joined_text: Final = "\n\n".join(cast(tuple[str, ...], contents)) # cast-ok: every content is a str return cast(AllMessageValues, {**run[0], "content": joined_text}) # cast-ok: dict spread keeps message shape - merged_parts: Final = [part for content in contents for part in _system_content_as_text_parts(content)] + merged_parts: Final = [ # mutable-ok: chat message content must stay a json list + part for content in contents for part in _system_content_as_text_parts(content) + ] return cast(AllMessageValues, {**run[0], "content": merged_parts}) # cast-ok: dict spread keeps message shape def merge_consecutive_system_messages( messages: list[AllMessageValues], # mutable-ok: message pipelines type messages as mutable lists ) -> list[AllMessageValues]: # mutable-ok: message pipelines type messages as mutable lists - return [ + return [ # mutable-ok: pipelines mutate message lists merged for is_system_run, run in groupby(messages, key=lambda message: message.get("role") == "system") for merged in ((_merge_system_message_run(tuple(run)),) if is_system_run else run) diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index ae4ac29de9e..c4e242fd360 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2376,7 +2376,7 @@ def anthropic_messages_pt( # add role=tool support to allow function call result/error submission user_message_types: Final = {"user", "tool", "function"} # reformat messages to ensure user/assistant are alternating, if there's either 2 consecutive 'user' messages or 2 consecutive 'assistant' message, merge them. - new_messages: Final[_AnthropicMessageList] = [] + new_messages: Final[_AnthropicMessageList] = [] # mutable-ok: accumulator behind the mutable return contract if len(messages) == 0: if not litellm.modify_params: diff --git a/litellm/litellm_core_utils/prompt_templates/image_handling.py b/litellm/litellm_core_utils/prompt_templates/image_handling.py index d62fb789740..c44c80bc0a0 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 + return {**image_url, "url": data_url} if image_url is not None else data_url # mutable-ok: json-serialized part 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"} - return {**kept, **_inferred_format(file, url), "file_data": data_url} + 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 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} + return {"type": "base64", "media_type": media_type, "data": data} # mutable-ok: json-serialized message part 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)} + return {**part, "image_url": _inlined_image_url(image_url, data_url)} # mutable-ok: json-serialized part case _RemoteFile(part, file, url): - return {**part, "file": _inlined_file(file, url, data_url)} + return {**part, "file": _inlined_file(file, url, data_url)} # mutable-ok: json-serialized message part case _RemoteSource(part, _, url): - return {**part, "source": _base64_source(url, data_url)} + return {**part, "source": _base64_source(url, data_url)} # mutable-ok: json-serialized message part def _content_parts(message: Mapping[str, object]) -> tuple[object, ...]: @@ -286,8 +286,10 @@ def _inline_message( parts: Final = _content_parts(message) if not parts: return message - inlined_parts: Final = [_inline_part(part, data_urls, should_inline) for part in parts] - inlined_message: Final = {**message, "content": inlined_parts} + 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 return inlined_message # pyright: ignore[reportReturnType] # the same message with its remote parts inlined @@ -324,4 +326,6 @@ 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 [_inline_message(message, inlined, should_inline) for message in messages] + return [ # mutable-ok: transform_request takes a list + _inline_message(message, inlined, should_inline) for message in messages + ] diff --git a/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py b/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py index 2169dfcad39..b5e9afca86b 100644 --- a/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py +++ b/litellm/litellm_core_utils/prompt_templates/mid_conversation_system.py @@ -167,7 +167,7 @@ def anthropic_system_messages(message: object) -> tuple[AnthropicMessagesSystemM return () wire: Final[AnthropicMessagesSystemMessageParam] = { "role": "system", - "content": list(blocks), + "content": list(blocks), # mutable-ok: wire payload; cache_control hooks edit content blocks in place } return (wire,) diff --git a/litellm/litellm_core_utils/provider_affinity.py b/litellm/litellm_core_utils/provider_affinity.py index 02c4cd69159..33bf2ee7079 100644 --- a/litellm/litellm_core_utils/provider_affinity.py +++ b/litellm/litellm_core_utils/provider_affinity.py @@ -88,11 +88,11 @@ def add_provider_affinity_header( ) -> dict[str, object]: # mutable-ok: downstream handlers add auth and signing headers header_name: Final = _get_provider_affinity_header_name(litellm_params) if header_name is None or any(key.lower() == header_name.lower() for key in headers): - return dict(headers) + return dict(headers) # mutable-ok: downstream handlers add auth and signing headers session_id: Final = get_stable_session_id(litellm_params) if session_id is None: - return dict(headers) + return dict(headers) # mutable-ok: downstream handlers add auth and signing headers if any(character in session_id for character in ("\r", "\n", "\0")): raise ValueError("session_id cannot contain HTTP header control characters") - return {**headers, header_name: session_id} + return {**headers, header_name: session_id} # mutable-ok: downstream handlers add auth and signing headers diff --git a/litellm/litellm_core_utils/sentry_scrubbing.py b/litellm/litellm_core_utils/sentry_scrubbing.py index 7147f792411..4c14cabc2ab 100644 --- a/litellm/litellm_core_utils/sentry_scrubbing.py +++ b/litellm/litellm_core_utils/sentry_scrubbing.py @@ -109,12 +109,12 @@ def scrub_json_strings(value: JsonValue, scrub: Callable[[str], str], path: Json return scrub(value) if isinstance(value, dict): unscrubbed_keys: Final = SOURCE_CONTEXT_KEYS if path in STACK_FRAME_PATHS else frozenset[str]() - return { + return { # mutable-ok: JSON object key: item if key in unscrubbed_keys else scrub_json_strings(item, scrub, (*path, key)) for key, item in value.items() } if isinstance(value, list): - return [scrub_json_strings(item, scrub, (*path, "*")) for item in value] + return [scrub_json_strings(item, scrub, (*path, "*")) for item in value] # mutable-ok: JSON array return value @@ -141,8 +141,8 @@ def build_sentry_init_options(env: Mapping[str, str]) -> SentryInitOptions: sample_rate=float(env.get("SENTRY_API_SAMPLE_RATE") or "1.0"), send_default_pii=send_default_pii, event_scrubber=EventScrubber( - denylist=list(SECRET_FIELD_NAMES), - pii_denylist=list(PII_FIELD_NAMES), + denylist=list(SECRET_FIELD_NAMES), # mutable-ok: EventScrubber appends pii_denylist onto denylist in place + pii_denylist=list(PII_FIELD_NAMES), # mutable-ok: EventScrubber takes List[str] recursive=True, send_default_pii=send_default_pii, ), diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index be9a17a5dd2..bdf53013224 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -490,7 +490,9 @@ class ChunkProcessor: def get_combined_tool_content( self, tool_call_chunks: Sequence["_ToolCallChunk"] ) -> list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall]: - tool_calls_list: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] = [] + tool_calls_list: list[ + ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall + ] = [] # mutable-ok: see return type tool_call_map: Final[dict[_ToolCallKey, dict[str, Any]]] = {} 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 d3386b14231..d2853a625c9 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -189,7 +189,9 @@ 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) if parsed is not None and parsed.provider_specific_fields else 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 ) params: Final[Mapping[str, object]] = MappingProxyType( { diff --git a/litellm/litellm_core_utils/tokenizer.py b/litellm/litellm_core_utils/tokenizer.py index 31cd8f63116..aea187fa08e 100644 --- a/litellm/litellm_core_utils/tokenizer.py +++ b/litellm/litellm_core_utils/tokenizer.py @@ -72,7 +72,7 @@ class OpenAIEncoding: return self._special_tokens["<|endoftext|>"] @property - def special_tokens_set(self) -> set[str]: # mutable-ok: [LIT001] SDK return type + def special_tokens_set(self) -> set[str]: # mutable-ok: [LIT001, LIT002] SDK return type return set(self._special_tokens) def is_special_token(self, token: int) -> bool: @@ -80,7 +80,7 @@ class OpenAIEncoding: # ---- encoding ------------------------------------------------------------------------- - def encode_ordinary(self, text: str) -> list[int]: # mutable-ok: [LIT001] SDK return type + def encode_ordinary(self, text: str) -> list[int]: # mutable-ok: [LIT001, LIT002] SDK return type return self._native.encode(text) def encode( @@ -89,7 +89,7 @@ class OpenAIEncoding: *, allowed_special: AllowedSpecial = frozenset(), disallowed_special: SpecialTokens = "all", - ) -> list[int]: # mutable-ok: [LIT001] SDK return type + ) -> list[int]: # mutable-ok: [LIT001, LIT002] SDK return type allowed: Final = self._allowed(text, allowed_special, disallowed_special) if not allowed: return self.encode_ordinary(text) @@ -111,9 +111,11 @@ class OpenAIEncoding: def encode_ordinary_batch( self, text: Sequence[str], *, num_threads: int = 8 - ) -> list[list[int]]: # mutable-ok: [LIT001] SDK return type + ) -> list[list[int]]: # mutable-ok: [LIT001, LIT002] SDK return type with ThreadPoolExecutor(num_threads) as executor: - return list(executor.map(self.encode_ordinary, text)) + return list( # mutable-ok: [LIT002] SDK returns a list + executor.map(self.encode_ordinary, text) + ) def encode_batch( self, @@ -122,10 +124,12 @@ class OpenAIEncoding: num_threads: int = 8, allowed_special: AllowedSpecial = frozenset(), disallowed_special: SpecialTokens = "all", - ) -> list[list[int]]: # mutable-ok: [LIT001] SDK return type + ) -> list[list[int]]: # mutable-ok: [LIT001, LIT002] SDK return type encode: Final = partial(self.encode, allowed_special=allowed_special, disallowed_special=disallowed_special) with ThreadPoolExecutor(num_threads) as executor: - return list(executor.map(encode, text)) + return list( # mutable-ok: [LIT002] SDK returns a list + executor.map(encode, text) + ) def encode_with_unstable( self, @@ -133,7 +137,7 @@ class OpenAIEncoding: *, allowed_special: AllowedSpecial = frozenset(), disallowed_special: SpecialTokens = "all", - ) -> tuple[list[int], list[list[int]]]: # mutable-ok: [LIT001] SDK return type + ) -> tuple[list[int], list[list[int]]]: # mutable-ok: [LIT001, LIT002] SDK return type """The stable tokens of `text` and every completion its unstable tail could become. Completions come back sorted; tiktoken returns them in hash order.""" @@ -160,12 +164,14 @@ class OpenAIEncoding: def decode_single_token_bytes(self, token: int) -> bytes: return self.decode_bytes((token,)) - def decode_tokens_bytes(self, tokens: Sequence[int]) -> list[bytes]: # mutable-ok: [LIT001] SDK return type - return [self.decode_single_token_bytes(token) for token in tokens] + def decode_tokens_bytes(self, tokens: Sequence[int]) -> list[bytes]: # mutable-ok: [LIT001, LIT002] SDK return type + return [ # mutable-ok: [LIT002] SDK returns a list + self.decode_single_token_bytes(token) for token in tokens + ] def decode_with_offsets( self, tokens: Sequence[int] - ) -> tuple[str, list[int]]: # mutable-ok: [LIT001] SDK return type + ) -> tuple[str, list[int]]: # mutable-ok: [LIT001, LIT002] SDK return type """The decoded text and, per token, the index of the first character holding its bytes. Like tiktoken, raises `UnicodeDecodeError` when the tokens do not decode to valid UTF-8.""" @@ -179,17 +185,21 @@ class OpenAIEncoding: def decode_batch( self, batch: Sequence[Sequence[int]], *, errors: str = "replace", num_threads: int = 8 - ) -> list[str]: # mutable-ok: [LIT001] SDK return type + ) -> list[str]: # mutable-ok: [LIT001, LIT002] SDK return type with ThreadPoolExecutor(num_threads) as executor: - return list(executor.map(partial(self.decode, errors=errors), batch)) + return list( # mutable-ok: [LIT002] SDK returns a list + executor.map(partial(self.decode, errors=errors), batch) + ) def decode_bytes_batch( self, batch: Sequence[Sequence[int]], *, num_threads: int = 8 - ) -> list[bytes]: # mutable-ok: [LIT001] SDK return type + ) -> list[bytes]: # mutable-ok: [LIT001, LIT002] SDK return type with ThreadPoolExecutor(num_threads) as executor: - return list(executor.map(self.decode_bytes, batch)) + return list( # mutable-ok: [LIT002] SDK returns a list + executor.map(self.decode_bytes, batch) + ) - def token_byte_values(self) -> list[bytes]: # mutable-ok: [LIT001] SDK return type + def token_byte_values(self) -> list[bytes]: # mutable-ok: [LIT001, LIT002] SDK return type return self._native.token_byte_values() def __reduce__(self) -> tuple[Callable[[str], OpenAIEncoding], tuple[str]]: @@ -263,14 +273,16 @@ class HuggingFaceTokenizer: def id_to_token(self, id: int) -> str | None: return self._native.id_to_token(id) - def get_vocab(self, with_added_tokens: bool = True) -> dict[str, int]: # mutable-ok: [LIT001] SDK return type + def get_vocab( + self, with_added_tokens: bool = True + ) -> dict[str, int]: # mutable-ok: [LIT001, LIT002] SDK return type return self._native.get_vocab(with_added_tokens) def get_vocab_size(self, with_added_tokens: bool = True) -> int: return self._native.get_vocab_size(with_added_tokens) - def get_added_tokens_decoder(self) -> dict[int, AddedToken]: # mutable-ok: [LIT001] SDK return type - return { + def get_added_tokens_decoder(self) -> dict[int, AddedToken]: # mutable-ok: [LIT001, LIT002] SDK return type + return { # mutable-ok: [LIT002] SDK returns a dict token_id: AddedToken( content, single_word=single_word, lstrip=lstrip, rstrip=rstrip, normalized=normalized, special=special ) @@ -288,11 +300,11 @@ class HuggingFaceTokenizer: return self._native.num_special_tokens_to_add(is_pair) @property - def padding(self) -> dict[str, object] | None: # mutable-ok: [LIT001] SDK return type + def padding(self) -> dict[str, object] | None: # mutable-ok: [LIT001, LIT002] SDK return type return self._native.padding() @property - def truncation(self) -> dict[str, object] | None: # mutable-ok: [LIT001] SDK return type + def truncation(self) -> dict[str, object] | None: # mutable-ok: [LIT001, LIT002] SDK return type return self._native.truncation() @property @@ -315,7 +327,7 @@ class HuggingFaceTokenizer: input: Sequence[HuggingFaceBatchInput], is_pretokenized: bool = False, add_special_tokens: bool = True, - ) -> list[HuggingFaceEncoding]: # mutable-ok: [LIT001] SDK return type + ) -> list[HuggingFaceEncoding]: # mutable-ok: [LIT001, LIT002] SDK return type return self._encode_batch(input, is_pretokenized, add_special_tokens, fast=False) def encode_batch_fast( @@ -323,12 +335,12 @@ class HuggingFaceTokenizer: input: Sequence[HuggingFaceBatchInput], is_pretokenized: bool = False, add_special_tokens: bool = True, - ) -> list[HuggingFaceEncoding]: # mutable-ok: [LIT001] SDK return type + ) -> list[HuggingFaceEncoding]: # mutable-ok: [LIT001, LIT002] SDK return type return self._encode_batch(input, is_pretokenized, add_special_tokens, fast=True) def _encode_batch( self, input: Sequence[HuggingFaceBatchInput], is_pretokenized: bool, add_special_tokens: bool, fast: bool - ) -> list[HuggingFaceEncoding]: # mutable-ok: [LIT001] SDK return type + ) -> list[HuggingFaceEncoding]: # mutable-ok: [LIT001, LIT002] SDK return type sequences: Final = tuple(_batch_input(item, is_pretokenized) for item in input) return self._native.encode_batch_huggingface(sequences, is_pretokenized, add_special_tokens, fast) @@ -341,8 +353,10 @@ class HuggingFaceTokenizer: def decode_batch( self, sequences: Sequence[Sequence[int]], skip_special_tokens: bool = True - ) -> list[str]: # mutable-ok: [LIT001] SDK return type - return [self.decode(ids, skip_special_tokens=skip_special_tokens) for ids in sequences] + ) -> list[str]: # mutable-ok: [LIT001, LIT002] SDK return type + return [ # mutable-ok: [LIT002] SDK returns a list + self.decode(ids, skip_special_tokens=skip_special_tokens) for ids in sequences + ] def __reduce__(self) -> tuple[Callable[[str], HuggingFaceTokenizer], tuple[str]]: return (HuggingFaceTokenizer.from_str, (self.to_str(),)) diff --git a/litellm/llms/a2a/chat/transformation.py b/litellm/llms/a2a/chat/transformation.py index af4c6f69944..0813e0827d2 100644 --- a/litellm/llms/a2a/chat/transformation.py +++ b/litellm/llms/a2a/chat/transformation.py @@ -53,7 +53,7 @@ def _registry_headers(agent_litellm_params: Mapping[str, object]) -> dict[str, o if not isinstance(stored_headers, Mapping): return None entra_owns_authorization: Final = _agent_authenticates_with_entra(agent_litellm_params) - return { + return { # mutable-ok: completion() and httpx take the request headers as a dict name: value for name, value in stored_headers.items() if not (entra_owns_authorization and str(name).lower() == "authorization") diff --git a/litellm/llms/anthropic/chat/guardrail_translation/handler.py b/litellm/llms/anthropic/chat/guardrail_translation/handler.py index 806240c9749..15380f57d17 100644 --- a/litellm/llms/anthropic/chat/guardrail_translation/handler.py +++ b/litellm/llms/anthropic/chat/guardrail_translation/handler.py @@ -263,7 +263,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}} + return {**event, rewrite.section: {**section, rewrite.field: rewrite.value}} # mutable-ok: json.dumps needs a dict def _tool_call_shapes(tool_calls: Sequence[object]) -> tuple[_ToolCallShape, ...]: @@ -539,7 +539,9 @@ class AnthropicMessagesHandler(BaseTranslation): # The top-level prompt is translated on its own below so it can be hoisted in front of # any mid-turn system entries and scanned first, aligned with that structured position. - translation_source: Final = {key: value for key, value in data.items() if key != "system"} + translation_source: Final = { # mutable-ok: API message payload + 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( @@ -592,7 +594,7 @@ class AnthropicMessagesHandler(BaseTranslation): *top_level_system_scanned, *(item for one_message in extracted for item in one_message.scanned), ) - texts_to_check: Final = [item.text for item in 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] scanned_tool_calls: Final = tuple(item for one_message in extracted for item in one_message.tool_calls) tool_calls_to_check: Final = [item.tool_call for item in scanned_tool_calls] @@ -689,13 +691,13 @@ class AnthropicMessagesHandler(BaseTranslation): if not system: return None probe: Final = self._translate_to_openai( - { + { # mutable-ok: API message payload "model": data.get("model") or "", - "messages": [], + "messages": [], # mutable-ok: API message payload "system": system, } ) - hoisted: Final = probe.get("messages") or [] + hoisted: Final = probe.get("messages") or [] # mutable-ok: API message payload return hoisted[0] if hoisted else None @staticmethod @@ -718,7 +720,9 @@ class AnthropicMessagesHandler(BaseTranslation): """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 + return ( + {"role": "system", "content": content} if content else None # mutable-ok: API message payload + ) if not isinstance(content, list): return None blocks: Final[list[dict[str, object]]] = [] # mutable-ok: API message payload @@ -736,7 +740,9 @@ class AnthropicMessagesHandler(BaseTranslation): if cache_control: anthropic_block["cache_control"] = deepcopy(cache_control) blocks.append(anthropic_block) - return {"role": "system", "content": blocks} if blocks else None + return ( + {"role": "system", "content": blocks} if blocks else None # mutable-ok: API message payload + ) @staticmethod def _fold_leading_systems_into_top_level( @@ -840,7 +846,7 @@ class AnthropicMessagesHandler(BaseTranslation): for group in group_tool_exchanges(run): converted.extend( anthropic_messages_pt( - messages=[run[index] for index in group], + messages=[run[index] for index in group], # mutable-ok: API message payload model=model, llm_provider="anthropic", ) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index c1da56bee1e..ef0f45d8f8b 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -763,7 +763,7 @@ class ModelResponseIterator: return content_block_start def _web_search_call_snapshot(self) -> dict[str, object]: - return dict(self._web_search_calls) + return dict(self._web_search_calls) # mutable-ok: stream payload snapshot def _complete_web_search_call(self, result: dict[str, object]) -> None: tool_use_id: Final = result.get("tool_use_id") @@ -771,7 +771,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, {}), + tool_input=self._server_tool_inputs.get(tool_use_id, {}), # mutable-ok: empty provider input result=result, ) @@ -880,7 +880,7 @@ class ModelResponseIterator: self._web_search_calls[self._current_server_tool_id] = build_web_search_call( self._current_server_tool_id, tool_input, - {"content": []}, + {"content": []}, # mutable-ok: no provider result yet 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 490912d42eb..3bffee48d6a 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1978,7 +1978,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): # system message stays in the conversation: hoisting it rewrites the cached # prefix and re-bills the whole history at cache-write pricing (#36559). leading_system_run, later_messages = split_leading_system_run(messages) - anthropic_system_message_list: Final = self.translate_system_message(messages=list(leading_system_run)) + anthropic_system_message_list: Final = self.translate_system_message( + messages=list(leading_system_run) # mutable-ok: translate_system_message pops from the list it is given + ) # Handling anthropic API Prompt Caching if len(anthropic_system_message_list) > 0: optional_params["system"] = anthropic_system_message_list @@ -1992,7 +1994,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): try: anthropic_messages = anthropic_messages_pt( model=model, - messages=list(conversation), + messages=list(conversation), # mutable-ok: anthropic_messages_pt rewrites entries in place llm_provider=self._resolved_provider, ) except Exception as e: @@ -2106,7 +2108,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): optional_params.pop("output_config", None) data.pop("output_config", None) return - format_only: Final = {"format": preserved_format} + format_only: Final = {"format": preserved_format} # mutable-ok: json body optional_params["output_config"] = format_only # rebind-ok: out-param store data["output_config"] = format_only # rebind-ok: out-param store return @@ -2513,7 +2515,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) -> list[object]: content: Final = completion_response.get("content") blocks: Final = content if isinstance(content, Sequence) else () - inputs: Final = { + inputs: Final = { # mutable-ok: indexes provider server inputs call_id: tool_input for block in blocks if isinstance(block, Mapping) @@ -2522,10 +2524,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): and isinstance((call_id := block.get("id")), str) and isinstance((tool_input := block.get("input")), Mapping) } - return [ + return [ # mutable-ok: provider-neutral response items build_web_search_call( tool_id=tool_use_id, - tool_input=inputs.get(tool_use_id, {}), + tool_input=inputs.get(tool_use_id, {}), # mutable-ok: empty provider input 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 ed4975ac28d..3e61a0caa90 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1300,12 +1300,12 @@ def _without_encrypted_reasoning_blocks(message: dict) -> dict | None: # mutabl 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)] + kept: Final = [b for b in content if not is_encrypted_reasoning_block(b)] # mutable-ok: API message payload if len(kept) == len(content): return message if not kept: return None - return {**message, "content": kept} + return {**message, "content": kept} # mutable-ok: API message payload def strip_encrypted_reasoning_blocks_from_anthropic_messages( @@ -1317,7 +1317,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] + return [m for m in stripped if m is not None] # mutable-ok: API message payload def strip_thinking_blocks_from_anthropic_messages_request_dict( @@ -1605,7 +1605,7 @@ 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]} + return {**message, "content": [b for b in rewritten if b is not None]} # mutable-ok: JSON wire format def flatten_unencrypted_web_search_results_in_anthropic_messages( @@ -1623,47 +1623,49 @@ def flatten_unencrypted_web_search_results_in_anthropic_messages( 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] + return [_flatten_web_search_results_in_message(m) for m in messages] # mutable-ok: JSON wire format 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"} + return {k: v for k, v in block.items() if k != "provider_specific_fields"} # mutable-ok: JSON wire format 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"]] - return {**message, "content": content} + 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 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] + return [_strip_provider_specific_fields_in_message(m) for m in messages] # mutable-ok: JSON wire format def _normalized_cache_control(cache_control: object) -> dict[str, str] | None: # mutable-ok: JSON wire format 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"} + return {"type": cache_type if isinstance(cache_type, str) else "ephemeral"} # mutable-ok: JSON wire format def _with_portable_cache_control(block: Mapping[str, object]) -> dict[str, object]: # mutable-ok: JSON wire format if "cache_control" not in block: - return dict(block) + return dict(block) # mutable-ok: JSON wire format normalized: Final = _normalized_cache_control(block["cache_control"]) - 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} + 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 def _with_portable_cache_control_in_blocks(blocks: object) -> object: if isinstance(blocks, str) or not isinstance(blocks, Sequence): return blocks - return [_with_portable_cache_control(block) if isinstance(block, Mapping) else block for block in blocks] + return [ # mutable-ok: JSON wire format + _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: @@ -1672,7 +1674,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 { + return { # mutable-ok: JSON wire format **portable, "content": _with_portable_cache_control_in_blocks(portable["content"]), } @@ -1684,16 +1686,20 @@ 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 { + return { # mutable-ok: JSON wire format **message, - "content": [_with_portable_cache_control_in_content_block(block) for block in content], + "content": [ # mutable-ok: JSON wire format + _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 [_with_portable_cache_control_in_message(message) for message in messages] + return [ # mutable-ok: JSON wire format + _with_portable_cache_control_in_message(message) for message in messages + ] def _with_portable_cache_control_in_scoped_value(key: str, value: object) -> object: @@ -1725,7 +1731,9 @@ def normalize_cache_control_in_anthropic_payload( dropped entirely. The caller's payload is never mutated. """ portable: Final = _with_portable_cache_control(payload) - return {key: _with_portable_cache_control_in_scoped_value(key, value) for key, value in portable.items()} + return { # mutable-ok: JSON wire format + 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: @@ -1752,7 +1760,7 @@ def _anthropic_model_entry( source: Final[Mapping[str, object]] = ( MappingProxyType({"source_model": model["id"]}) if listed_id is not None else MappingProxyType({}) ) - return { + return { # mutable-ok: JSON response body, serialized by the route and never mutated "type": "model", "id": listed_id or model["id"], **source, @@ -1783,8 +1791,10 @@ 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 = [_anthropic_model_entry(model, created_at, display_names, listed_ids) for model in models] - return { + 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": data, "has_more": False, "first_id": data[0]["id"] if data else None, diff --git a/litellm/llms/anthropic/pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/pass_through/adapters/streaming_iterator.py index 4ef6c305cf5..38380cc056d 100644 --- a/litellm/llms/anthropic/pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/pass_through/adapters/streaming_iterator.py @@ -1212,7 +1212,9 @@ class AnthropicSSEStream(AsyncIterator[bytes]): def __init__(self, anthropic_wrapper: AnthropicStreamWrapper) -> None: self._anthropic_wrapper = anthropic_wrapper self._byte_stream: Final[AsyncIterator[bytes]] = anthropic_wrapper.async_anthropic_sse_wrapper() - self._hidden_params: dict[str, object] = {} + self._hidden_params: dict[ + str, object + ] = {} # mutable-ok: the proxy merges provider headers onto _hidden_params in place @property def chunks(self) -> "list[ModelResponseStream] | None": diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 022bc6337b5..040c8f0e170 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -1314,7 +1314,7 @@ class LiteLLMAnthropicMessagesAdapter: case ({"type": "text", "text": str(text)},): return text case _: - return list(parts) + return list(parts) # mutable-ok: content must be a json list def _tool_result_part(self, item: object) -> ToolMessageContentPart | None: if isinstance(item, str): diff --git a/litellm/llms/anthropic/pass_through/messages/response_cache.py b/litellm/llms/anthropic/pass_through/messages/response_cache.py index 5dc26c934ab..7b9435d45ac 100644 --- a/litellm/llms/anthropic/pass_through/messages/response_cache.py +++ b/litellm/llms/anthropic/pass_through/messages/response_cache.py @@ -132,7 +132,7 @@ class CachedAnthropicMessagesStreamIterator(BaseAnthropicMessagesStreamingIterat litellm_logging_obj: "LiteLLMLoggingObj", request_body: Mapping[str, object], ) -> None: - body: Final = dict(request_body) + body: Final = dict(request_body) # mutable-ok: the base iterator takes a plain dict 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 @@ -147,7 +147,7 @@ class CachedAnthropicMessagesStreamIterator(BaseAnthropicMessagesStreamingIterat if self.current_index >= len(self.chunks): if not self.logged: self.logged = True - chunks: Final = list(self.chunks) + chunks: Final = list(self.chunks) # mutable-ok: the logging handler takes a list await self._handle_streaming_logging(chunks) raise StopAsyncIteration chunk: Final = self.chunks[self.current_index] diff --git a/litellm/llms/anthropic/pass_through/messages/streaming_iterator.py b/litellm/llms/anthropic/pass_through/messages/streaming_iterator.py index 417017cfb6e..89e214efa8b 100644 --- a/litellm/llms/anthropic/pass_through/messages/streaming_iterator.py +++ b/litellm/llms/anthropic/pass_through/messages/streaming_iterator.py @@ -212,15 +212,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": {}, + "input": {}, # mutable-ok: one-shot payload "type": "tool_use", }, ( - { - "partial_json": json.dumps(block.get("input") or {}), + { # mutable-ok: one-shot payload + "partial_json": json.dumps(block.get("input") or {}), # mutable-ok: one-shot payload "type": "input_json_delta", }, ), @@ -228,23 +228,23 @@ def _anthropic_content_block_start_and_deltas( case "thinking": signature: Final = block.get("signature") signature_deltas: Final = ( - ({"signature": signature, "type": "signature_delta"},) + ({"signature": signature, "type": "signature_delta"},) # mutable-ok: one-shot payload if isinstance(signature, str) and signature else () ) return ( - {"thinking": "", "signature": "", "type": "thinking"}, + {"thinking": "", "signature": "", "type": "thinking"}, # mutable-ok: one-shot payload ( - {"thinking": block.get("thinking") or "", "type": "thinking_delta"}, + {"thinking": block.get("thinking") or "", "type": "thinking_delta"}, # mutable-ok: one-shot payload *signature_deltas, ), ) case "redacted_thinking": - return ({"type": "redacted_thinking", "data": block.get("data")}, ()) + return ({"type": "redacted_thinking", "data": block.get("data")}, ()) # mutable-ok: one-shot JSON payload case _: return ( - {"type": "text", "text": ""}, - ({"type": "text_delta", "text": block.get("text") or ""},), + {"type": "text", "text": ""}, # mutable-ok: one-shot JSON payload + ({"type": "text_delta", "text": block.get("text") or ""},), # mutable-ok: one-shot JSON payload ) @@ -268,51 +268,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 = { + message_start_usage: Final = { # mutable-ok: one-shot JSON payload **(response.get("usage") or {}), "output_tokens": 0, } - message_start_payload: Final = { + message_start_payload: Final = { # mutable-ok: one-shot JSON payload, never mutated after construction "type": "message_start", - "message": { + "message": { # mutable-ok: one-shot JSON payload **response, - "content": [], + "content": [], # mutable-ok: one-shot JSON payload "stop_reason": None, "stop_sequence": None, "usage": message_start_usage, }, } - message_delta_payload: Final = { + message_delta_payload: Final = { # mutable-ok: one-shot JSON payload, never mutated after construction "type": "message_delta", - "delta": { + "delta": { # mutable-ok: one-shot JSON payload "stop_reason": response.get("stop_reason"), "stop_sequence": response.get("stop_sequence"), }, - "usage": response.get("usage") or {}, + "usage": response.get("usage") or {}, # mutable-ok: one-shot JSON payload } return ( _sse_event("message_start", message_start_payload), *content_events, _sse_event("message_delta", message_delta_payload), - _sse_event("message_stop", {"type": "message_stop"}), + _sse_event("message_stop", {"type": "message_stop"}), # mutable-ok: one-shot JSON payload ) 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 = { + start_payload: Final = { # mutable-ok: one-shot payload "type": "content_block_start", "index": index, "content_block": start_block, } - stop_payload: Final = { + stop_payload: Final = { # mutable-ok: one-shot payload "type": "content_block_stop", "index": index, } delta_events: Final = tuple( _sse_event( "content_block_delta", - {"type": "content_block_delta", "index": index, "delta": delta}, + {"type": "content_block_delta", "index": index, "delta": delta}, # mutable-ok: one-shot payload ) for delta in deltas ) diff --git a/litellm/llms/anthropic/pass_through/responses_adapters/streaming_iterator.py b/litellm/llms/anthropic/pass_through/responses_adapters/streaming_iterator.py index cc6f4da3403..db70f855223 100644 --- a/litellm/llms/anthropic/pass_through/responses_adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/pass_through/responses_adapters/streaming_iterator.py @@ -191,20 +191,20 @@ class AnthropicResponsesStreamWrapper: if block_idx < 0: redacted_idx: Final = self._open_block( item_id, - {"type": "redacted_thinking", "data": signature}, + {"type": "redacted_thinking", "data": signature}, # mutable-ok: API message payload ) - stop: Final = {"type": "content_block_stop", "index": redacted_idx} + stop: Final = {"type": "content_block_stop", "index": redacted_idx} # mutable-ok: API message payload 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}, + "delta": {"type": "signature_delta", "signature": signature}, # mutable-ok: API message payload } ) - self._chunk_queue.append({"type": "content_block_stop", "index": block_idx}) + self._chunk_queue.append({"type": "content_block_stop", "index": block_idx}) # mutable-ok: API message payload def _process_event(self, event: object) -> None: """Convert one Responses API event into zero or more Anthropic chunks queued for emission.""" @@ -296,10 +296,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": { + "delta": { # mutable-ok: API message payload "type": "thinking_delta", "thinking": REASONING_SUMMARY_PART_SEPARATOR, }, @@ -317,7 +317,7 @@ class AnthropicResponsesStreamWrapper: return block_idx = self._open_block( item_id, - {"type": "thinking", "thinking": "", "signature": ""}, + {"type": "thinking", "thinking": "", "signature": ""}, # mutable-ok: API message payload ) self._chunk_queue.append( { @@ -413,10 +413,16 @@ class AnthropicResponsesStreamWrapper: else AnthropicUsage(input_tokens=0, output_tokens=0) ) - message_delta_payload: Final = { + message_delta_payload: Final = { # mutable-ok: fresh message_delta payload built per chunk "stop_reason": stop_reason, "stop_sequence": None, - **({"stop_details": refusal_stop_details(refusal_text)} if stop_reason == "refusal" else {}), + **( + { # 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 + ), } self._chunk_queue.append( diff --git a/litellm/llms/anthropic/pass_through/responses_adapters/transformation.py b/litellm/llms/anthropic/pass_through/responses_adapters/transformation.py index a3ebbbcb830..26f82d66bfc 100644 --- a/litellm/llms/anthropic/pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/responses_adapters/transformation.py @@ -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 { + return { # mutable-ok: API message payload "type": "input_file", "filename": filename, "file_data": f"data:{media_type};base64,{data}", @@ -124,7 +124,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: url: Final = source.get("url") if not isinstance(url, str) or not url: return None - return {"type": "input_file", "file_url": url} + return {"type": "input_file", "file_url": url} # mutable-ok: API message payload return None @staticmethod @@ -135,8 +135,10 @@ class LiteLLMAnthropicToResponsesAPIAdapter: """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 [] - return [*text_parts, *file_parts] + 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 @staticmethod def _translate_midturn_system_content_to_responses( @@ -144,10 +146,12 @@ 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 [] + return ( + [{"type": "input_text", "text": content}] if content else [] # mutable-ok: API message payload + ) if not isinstance(content, list): - return [] - return [ + 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")) for block in content if isinstance(block, dict) and block.get("type") == "text" and (text := block.get("text")) # pyright: ignore[reportUnnecessaryIsInstance] # untrusted client payload @@ -199,14 +203,14 @@ class LiteLLMAnthropicToResponsesAPIAdapter: 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) + return tuple(dict(item) for item in replayed) # mutable-ok: API message payload 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", {})), + "arguments": json.dumps(first.get("input", {})), # mutable-ok: API message payload }, ) return () @@ -235,7 +239,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, @@ -318,7 +322,8 @@ class LiteLLMAnthropicToResponsesAPIAdapter: else TOOL_RESULT_IMAGE_PLACEHOLDER ) tool_image_parts.extend( - {"type": "input_image", "image_url": url} for url in image_urls + {"type": "input_image", "image_url": url} # mutable-ok: json content part + for url in image_urls ) else: output_text = str(inner) @@ -331,15 +336,15 @@ class LiteLLMAnthropicToResponsesAPIAdapter: } ) if tool_image_parts: - boundary_part = { + boundary_part = { # mutable-ok: json content 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], + "content": [boundary_part, *tool_image_parts], # mutable-ok: json content list } ) if user_parts: @@ -368,7 +373,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", "")} + {"type": "output_text", "text": block.get("text", "")} # mutable-ok: API message payload for block in blocks if block.get("type") == "text" ] @@ -526,7 +531,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: if developer_parts: input_items.insert( 0, - { + { # mutable-ok: API message payload "type": "message", "role": "developer", "content": developer_parts, @@ -538,7 +543,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: "input": input_items, } if include_encrypted_reasoning: - responses_kwargs["include"] = [RESPONSES_INCLUDE_ENCRYPTED_REASONING] + responses_kwargs["include"] = [RESPONSES_INCLUDE_ENCRYPTED_REASONING] # mutable-ok: API request payload if system and not developer_parts: if isinstance(system, str): diff --git a/litellm/llms/anthropic/prompt_cache_prediction.py b/litellm/llms/anthropic/prompt_cache_prediction.py index 6528c7ac726..ca0bebf124a 100644 --- a/litellm/llms/anthropic/prompt_cache_prediction.py +++ b/litellm/llms/anthropic/prompt_cache_prediction.py @@ -505,7 +505,7 @@ class TokenCounter(Protocol): def _count_objects( values: Sequence[Mapping[str, JsonValue]], ) -> list[dict[str, JsonValue]]: # mutable-ok: the existing provider count API requires JSON lists/dicts - return [dict(value) for value in values] + return [dict(value) for value in values] # mutable-ok: serialize read-only inputs at the provider API boundary def _messages_url(model: str, api_key: str, api_base: str | None) -> str: diff --git a/litellm/llms/azure/azure.py b/litellm/llms/azure/azure.py index 35a41304e1b..2e7e7bb0c9d 100644 --- a/litellm/llms/azure/azure.py +++ b/litellm/llms/azure/azure.py @@ -1279,7 +1279,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): api_base=api_base, is_async=False, ) - request_headers: Final = dict( + request_headers: Final = dict( # mutable-ok: the httpx request helpers take a dict get_azure_request_auth_headers(headers=headers, azure_client_params=azure_client_params) ) if aimg_generation is True: @@ -1411,7 +1411,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ + additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(azure_client.base_url), }, @@ -1455,7 +1455,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ + additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict "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 831e2c56f46..355714c0daf 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 = [ + sanitized: Final = [ # mutable-ok: request tools are a JSON list 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 80911e64feb..09d8075e857 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 = { + flattened_params: Final = { # mutable-ok: transform_request's contract takes a plain JSON params dict **optional_params, **sanitized_tools_update(optional_params), } diff --git a/litellm/llms/azure/common_utils.py b/litellm/llms/azure/common_utils.py index 7436a0e1b00..c8a146be5cd 100644 --- a/litellm/llms/azure/common_utils.py +++ b/litellm/llms/azure/common_utils.py @@ -440,7 +440,7 @@ def get_azure_request_auth_headers( def redact_azure_auth_headers(headers: Mapping[str, str]) -> Mapping[str, str]: - return { + return { # mutable-ok: logging callbacks JSON-serialize this copy name: (_REDACTED_AZURE_HEADER_VALUE if name.lower() in _AZURE_AUTH_HEADER_NAMES else value) for name, value in headers.items() } diff --git a/litellm/llms/azure/search/transformation.py b/litellm/llms/azure/search/transformation.py index 45ad78df687..0754c9b1fda 100644 --- a/litellm/llms/azure/search/transformation.py +++ b/litellm/llms/azure/search/transformation.py @@ -289,7 +289,7 @@ class BingGroundingSearchConfig(BaseSearchConfig): 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 { + return { # mutable-ok: httpx requires a plain dict of headers **headers, **self._auth_header(api_key, api_base), "Content-Type": "application/json", @@ -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), + headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature ) 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), + headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature ) def _priced(self, results: tuple[SearchResult, ...]) -> SearchResponse: @@ -416,12 +416,16 @@ 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), + results=list(results), # mutable-ok: SearchResponse.results is list[SearchResult] object="search", ) if get_secret_str(CONNECTION_ID_ENV): return response - response._hidden_params["additional_headers"] = {_RESPONSE_COST_HEADER: 0.0} + response._hidden_params[ + "additional_headers" + ] = { # mutable-ok: response_cost_calculator writes into _hidden_params + _RESPONSE_COST_HEADER: 0.0 + } return response def get_error_class( diff --git a/litellm/llms/azure_ai/azure_model_router/transformation.py b/litellm/llms/azure_ai/azure_model_router/transformation.py index 226cd9c13f9..0e4c8ca0d15 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 + 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 **get_hidden_params_dict(transformed_response), AZURE_MODEL_ROUTER_SELECTED_MODEL_KEY: selected_model, } diff --git a/litellm/llms/azure_ai/image_generation/flux_transformation.py b/litellm/llms/azure_ai/image_generation/flux_transformation.py index 8b4fa78cdf4..b6a9caf147b 100644 --- a/litellm/llms/azure_ai/image_generation/flux_transformation.py +++ b/litellm/llms/azure_ai/image_generation/flux_transformation.py @@ -76,7 +76,7 @@ class AzureFoundryFluxImageGenerationConfig(GPTImageGenerationConfig): def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]: if not self.is_flux2_model(model): return super().get_supported_openai_params(model) - return [ + return [ # mutable-ok: BaseImageGenerationConfig requires a list "n", "size", "output_format", @@ -151,4 +151,4 @@ class AzureFoundryFluxImageGenerationConfig(GPTImageGenerationConfig): for mapped_name, mapped_value in self._map_parameter(name, value, model) } ) - return {**optional_params, **mapped_params} + return {**optional_params, **mapped_params} # mutable-ok: inherited config contract returns a dict diff --git a/litellm/llms/azure_ai/passthrough/transformation.py b/litellm/llms/azure_ai/passthrough/transformation.py index 35b7642d7de..97e74ad4820 100644 --- a/litellm/llms/azure_ai/passthrough/transformation.py +++ b/litellm/llms/azure_ai/passthrough/transformation.py @@ -134,7 +134,7 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig): litellm_params=litellm_params, api_key_header=api_key_header_for_base(api_base), ) - return {**headers, **auth_headers} + return {**headers, **auth_headers} # mutable-ok: base class contract returns dict for httpx def logging_non_streaming_response( self, @@ -151,7 +151,7 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig): model=model, custom_llm_provider=custom_llm_provider, httpx_response=httpx_response, - request_data=dict(request_data), + request_data=dict(request_data), # mutable-ok: AzurePassthroughConfig wants a dict logging_obj=logging_obj, endpoint=endpoint, ) diff --git a/litellm/llms/azure_ai/responses/transformation.py b/litellm/llms/azure_ai/responses/transformation.py index 2721f26e0fb..66a284c821d 100644 --- a/litellm/llms/azure_ai/responses/transformation.py +++ b/litellm/llms/azure_ai/responses/transformation.py @@ -34,7 +34,7 @@ class AzureAIResponsesAPIConfig(AzureOpenAIResponsesAPIConfig): litellm_params=params.model_dump(), api_key_header=api_key_header_for_base(AzureFoundryModelInfo.get_api_base(params.api_base)), ) - return { + return { # mutable-ok: the handler updates the returned headers in place per the dict contract **headers, **auth_headers, "Content-Type": "application/json", diff --git a/litellm/llms/base_llm/guardrail_translation/utils.py b/litellm/llms/base_llm/guardrail_translation/utils.py index f5631128f7d..51d43436fc9 100644 --- a/litellm/llms/base_llm/guardrail_translation/utils.py +++ b/litellm/llms/base_llm/guardrail_translation/utils.py @@ -389,12 +389,12 @@ def message_text_slot_count(message: AllMessageValues) -> int: def _part_with_text(part: object, text: str) -> object: if not isinstance(part, Mapping): return part - return {**part, "text": text} + return {**part, "text": text} # mutable-ok: content parts stay JSON-plain dicts def _content_with_slot_texts(content: Sequence[object], texts: Sequence[str]) -> Sequence[object]: remaining_texts: Final = iter(texts) - return [ + return [ # mutable-ok: message content stays a JSON list _part_with_text(part, next(remaining_texts)) if _content_part_text(part) is not None else part for part in content ] @@ -413,7 +413,7 @@ def message_with_slot_texts(message: AllMessageValues, texts: Sequence[str]) -> if not isinstance(content, (str, list)): return message rewritten_content: Final = texts[0] if isinstance(content, str) else _content_with_slot_texts(content, texts) - rewritten: Final = {**message, "content": rewritten_content} + rewritten: Final = {**message, "content": rewritten_content} # mutable-ok: chat rows stay JSON-plain dicts return cast("AllMessageValues", rewritten) # cast-ok: the same row with only its text slots swapped diff --git a/litellm/llms/base_llm/responses/codex_compat.py b/litellm/llms/base_llm/responses/codex_compat.py index fd769832217..3cba4343ce2 100644 --- a/litellm/llms/base_llm/responses/codex_compat.py +++ b/litellm/llms/base_llm/responses/codex_compat.py @@ -129,7 +129,7 @@ def normalize_codex_input_items( return input, () normalized: Final = tuple(_normalize_input_item(item) for item in input) rewritten_types: Final = tuple(sorted(frozenset(item_type for _, item_type in normalized if item_type is not None))) - kept: Final = [i for i, _ in normalized if i is not None] + kept: Final = [i for i, _ in normalized if i is not None] # mutable-ok: downstream narrows on isinstance(list) # Codex passthrough items sit outside the OpenAI input union. return kept, rewritten_types # pyright: ignore[reportReturnType] # see above diff --git a/litellm/llms/base_llm/search/transformation.py b/litellm/llms/base_llm/search/transformation.py index 4edbf260d99..797381c9280 100644 --- a/litellm/llms/base_llm/search/transformation.py +++ b/litellm/llms/base_llm/search/transformation.py @@ -280,7 +280,7 @@ class BaseSearchConfig: return self.get_error_class( error_message=error.response.text, status_code=error.response.status_code, - headers=dict(error.response.headers), + headers=dict(error.response.headers), # mutable-ok: provider error factories require dict 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 28f6348e4d9..07b60cb4b72 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], - **dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream + 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 ) 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], - **dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream + 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 ) @@ -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], + input=[query], # mutable-ok: Router embedding requires a mutable input list **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], + input=[query], # mutable-ok: Router embedding requires a mutable input list **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=[]) + return VectorStoreIndexEndpoints(read=[], write=[]) # mutable-ok: the TypedDict declares list fields diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index c9fec2db1d7..30ea85db4d4 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1434,7 +1434,7 @@ class AmazonConverseConfig(BaseConfig): if not text_blocks: return None note: Final = ChatCompletionTextObject(type="text", text=CONVERTED_SYSTEM_NOTE) - body: Final = [ + body: Final = [ # mutable-ok: _bedrock_converse_messages_pt narrows content with isinstance(list) note, *text_blocks, ] @@ -1501,7 +1501,7 @@ class AmazonConverseConfig(BaseConfig): ) ) converted: Final = tuple(self._converted_or_kept(message) for message in reordered) - kept: Final = [message for message in converted if message is not None] + kept: Final = [message for message in converted if message is not None] # mutable-ok: converse pt takes a list return kept, system_content_blocks def _transform_inference_params(self, inference_params: dict) -> InferenceConfig: diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index b876bdb2a54..ccc4309fc5d 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -100,7 +100,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} + request_params: Final = {**optional_params, **litellm_params} # mutable-ok: AWS helpers require a plain dict 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") @@ -258,7 +258,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 + request_body["output_config"] = {"format": schema_format} # rebind-ok: out-param # mutable-ok: json return verbose_logger.warning( @@ -311,7 +311,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 + request_body["output_config"] = {"format": preserved_format} # rebind-ok: out-param # mutable-ok: json def normalize_custom_field_on_tools(request_body: dict) -> None: diff --git a/litellm/llms/bedrock/files/transformation.py b/litellm/llms/bedrock/files/transformation.py index be6fbf6c53a..fdc8e34ed3d 100644 --- a/litellm/llms/bedrock/files/transformation.py +++ b/litellm/llms/bedrock/files/transformation.py @@ -1384,7 +1384,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 [ + return [ # mutable-ok: the base files contract returns a list listed_file for listed_file in listed_files if listed_file is not None and (purpose is None or listed_file.purpose == purpose) @@ -1429,7 +1429,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, {} + return url, {} # mutable-ok: the base files contract returns the query as a dict def _s3_request_target( self, @@ -1446,7 +1446,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}, + optional_params={"aws_region_name": region_preference}, # mutable-ok: BaseAWSLLM takes a dict model="", ) endpoint_url: Final = ( @@ -1481,7 +1481,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}, + headers={"x-amz-content-sha256": empty_body_hash}, # mutable-ok: botocore AWSRequest takes a dict ) 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/messages/mantle_transformation.py b/litellm/llms/bedrock/messages/mantle_transformation.py index ae4e9de3511..62956dd4582 100644 --- a/litellm/llms/bedrock/messages/mantle_transformation.py +++ b/litellm/llms/bedrock/messages/mantle_transformation.py @@ -110,7 +110,7 @@ class AmazonMantleMessagesConfig(AmazonAnthropicClaudeMessagesConfig): if value } ) - return { + return { # mutable-ok: the base class contract returns a dict the handler signs into in place **merged_headers, **mantle_headers, }, resolved_api_base @@ -141,7 +141,7 @@ class AmazonMantleMessagesConfig(AmazonAnthropicClaudeMessagesConfig): mantle_fields: Final = MappingProxyType( {key: value for key, value in (("model", model_id), ("stream", streaming)) if value} ) - return { + return { # mutable-ok: the base class contract returns the dict the handler serializes as the body **body, **mantle_fields, } diff --git a/litellm/llms/bedrock/realtime/handler.py b/litellm/llms/bedrock/realtime/handler.py index eb314450f08..049313c3c96 100644 --- a/litellm/llms/bedrock/realtime/handler.py +++ b/litellm/llms/bedrock/realtime/handler.py @@ -395,7 +395,7 @@ class BedrockRealtime(BaseAWSLLM): if logged_events: GLOBAL_LOGGING_WORKER.ensure_initialized_and_enqueue( logging_obj.dispatch_success_handlers( - list(logged_events), + list(logged_events), # mutable-ok: realtime spend logging requires a list result prefer_async_handlers=True, ) ) diff --git a/litellm/llms/bedrock/realtime/transformation.py b/litellm/llms/bedrock/realtime/transformation.py index 6ecbddbc558..3b972961940 100644 --- a/litellm/llms/bedrock/realtime/transformation.py +++ b/litellm/llms/bedrock/realtime/transformation.py @@ -887,7 +887,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig): id=f"resp_{uuid.uuid4()}", status="completed", conversation_id=f"conv_{uuid.uuid4()}", - usage=dict(usage), + usage=dict(usage), # mutable-ok: OpenAIRealtimeResponseDoneObject types usage as plain dict ), ) return (leftover_done,) diff --git a/litellm/llms/bedrock/responses/transformation.py b/litellm/llms/bedrock/responses/transformation.py index fca57a65c58..e2221b64f62 100644 --- a/litellm/llms/bedrock/responses/transformation.py +++ b/litellm/llms/bedrock/responses/transformation.py @@ -119,24 +119,24 @@ def _inline_block(block: object, inlined: "Mapping[str, str]") -> object: url: Final = _remote_image_url(block) if url is None or not isinstance(block, dict): return block - return {**block, "image_url": inlined[url]} + return {**block, "image_url": inlined[url]} # mutable-ok: outgoing JSON request item def _inline_value(value: object, inlined: "Mapping[str, str]") -> object: if isinstance(value, list): - return [_inline_block(block, inlined) for block in value] + return [_inline_block(block, inlined) for block in value] # mutable-ok: outgoing JSON request item return _inline_block(value, inlined) def _inline_item(item: object, inlined: "Mapping[str, str]") -> object: if not isinstance(item, dict): return item - inlined_fields: Final = { + inlined_fields: Final = { # mutable-ok: outgoing JSON request item key: _inline_value(item[key], inlined) for key in IMAGE_BLOCK_KEYS if isinstance(item.get(key), (list, dict)) } if not inlined_fields: return item - return {**item, **inlined_fields} + return {**item, **inlined_fields} # mutable-ok: same def inline_remote_image_urls( @@ -145,7 +145,7 @@ def inline_remote_image_urls( """``input`` with every http(s) image URL replaced by its entry in ``inlined``.""" if not isinstance(input, list) or not inlined: return input - items: Final = [_inline_item(item, inlined) for item in input] + items: Final = [_inline_item(item, inlined) for item in input] # mutable-ok: downstream narrows on isinstance(list) return items # pyright: ignore[reportReturnType] # items keep the caller's input union @@ -219,7 +219,7 @@ class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig): bearer: Final = resolve_bedrock_bearer_token(api_key) if not bearer: return headers - return {**headers, "Authorization": f"Bearer {bearer}"} + return {**headers, "Authorization": f"Bearer {bearer}"} # mutable-ok: dict return per the contract def sign_request( self, @@ -261,7 +261,9 @@ class BedrockOpenAIResponsesConfig(BaseAWSLLM, OpenAIResponsesAPIConfig): "Bedrock Runtime Responses API: dropping unsupported parameter(s) %s that the endpoint rejects.", unsupported, ) - params: Final = {key: value for key, value in mapped.items() if key not in unsupported} + params: Final = { # mutable-ok: outgoing JSON request params + key: value for key, value in mapped.items() if key not in unsupported + } tools: Final = params.get("tools") if not isinstance(tools, list): return params diff --git a/litellm/llms/bedrock/search/transformation.py b/litellm/llms/bedrock/search/transformation.py index 19ba7d5673b..e7d706c3731 100644 --- a/litellm/llms/bedrock/search/transformation.py +++ b/litellm/llms/bedrock/search/transformation.py @@ -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 { + return { # mutable-ok: httpx request headers are a dict **headers, "Content-Type": "application/json", "Accept": "application/json, text/event-stream", @@ -234,13 +234,13 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM): "Other gateway tools cannot be invoked through this provider." ) - return { + return { # mutable-ok: JSON-RPC request bodies are JSON objects "jsonrpc": "2.0", "id": 1, "method": "tools/call", - "params": { + "params": { # mutable-ok: JSON-RPC request bodies are JSON objects "name": tool_name, - "arguments": { + "arguments": { # mutable-ok: JSON-RPC request bodies are JSON objects "query": joined_query[:AGENTCORE_MAX_QUERY_LENGTH], "maxResults": optional_params.get("max_results", AGENTCORE_DEFAULT_MAX_RESULTS), }, @@ -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 = { + bearer_headers: Final = { # mutable-ok: httpx request headers are a dict **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 { + else { # mutable-ok: BaseAWSLLM._sign_request takes optional params as a dict **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] + results: Final = [_to_search_result(item) for item in items] # mutable-ok: pydantic list field return SearchResponse(results=results, object="search") diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index 41d93a8dd4d..590919f1fb0 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -117,7 +117,7 @@ class BedrockMantleChatConfig(BedrockMantleAuthMixin, OpenAILikeChatConfig): ) if supported and param not in base_params ) - return [*base_params, *extra_params] + return [*base_params, *extra_params] # mutable-ok: fresh list required by the inherited signature def _supports_reasoning(self, model: str) -> bool: try: diff --git a/litellm/llms/bedrock_mantle/responses/transformation.py b/litellm/llms/bedrock_mantle/responses/transformation.py index fa0da6a28b4..3ac29f2d1c1 100644 --- a/litellm/llms/bedrock_mantle/responses/transformation.py +++ b/litellm/llms/bedrock_mantle/responses/transformation.py @@ -173,11 +173,15 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI summary, sorted(_BEDROCK_MANTLE_OPENAI_PATH_SUPPORTED_REASONING_SUMMARIES), ) - stripped: Final = {key: value for key, value in reasoning.items() if key != "summary"} + stripped: Final = { # mutable-ok: map_openai_params contract returns a plain dict + key: value for key, value in reasoning.items() if key != "summary" + } return ( - {**params, "reasoning": stripped} + {**params, "reasoning": stripped} # mutable-ok: map_openai_params contract returns a plain dict if stripped - else {key: value for key, value in params.items() if key != "reasoning"} + else { # mutable-ok: map_openai_params contract returns a plain dict + key: value for key, value in params.items() if key != "reasoning" + } ) def transform_responses_api_request( diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index dd97db45a88..8d65aa7b0ca 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -363,7 +363,7 @@ def _mask_presigned_request_headers(transformed_request: bytes | str | dict) -> _get_masked_values, # pyright: ignore[reportPrivateUsage] # the shared header-masking helper has no public name ) - return { + return { # mutable-ok: logging's curl and raw-request builders take dict **transformed_request, "headers": _get_masked_values(request_headers), } @@ -2559,7 +2559,7 @@ class BaseLLMHTTPHandler: ) if self._has_agentic_completion_hook(logging_obj): - agentic_kwargs: Final = dict(litellm_params) + agentic_kwargs: Final = dict(litellm_params) # mutable-ok: agentic hooks mutate kwargs in place final_response: Final = run_async_function( self._call_agentic_completion_hooks, response=initial_response, @@ -2754,7 +2754,7 @@ class BaseLLMHTTPHandler: logging_obj=logging_obj, ) - agentic_kwargs: Final = dict(litellm_params) + agentic_kwargs: Final = dict(litellm_params) # mutable-ok: agentic hooks mutate kwargs in place final_response: Final = await self._call_agentic_completion_hooks( response=initial_response, model=model, @@ -4735,7 +4735,9 @@ class BaseLLMHTTPHandler: files_per_page: Final = self._files_per_listing_page( response, provider_config, logging_obj, litellm_params, headers, sync_httpx_client, timeout ) - return [listed_file for page_files in files_per_page for listed_file in page_files] + 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 + ] async def async_list_files( self, @@ -4790,7 +4792,9 @@ 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 [listed_file async for page_files in files_per_page for listed_file in page_files] + 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 + ] def _files_per_listing_page( self, @@ -9700,7 +9704,7 @@ class BaseLLMHTTPHandler: logging_obj.pre_call( input="", api_key="", - additional_args={ + additional_args={ # mutable-ok: pre_call's additional_args contract is a dict "query": query, "vector_store_id": vector_store_id, "api_base": endpoint, @@ -9736,7 +9740,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), + litellm_params=dict(litellm_params), # mutable-ok: snapshot GenericLiteLLMParams into the Mapping shape embedding_executor=embedding_executor, timeout=timeout, ) @@ -9876,7 +9880,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), + litellm_params=dict(litellm_params), # mutable-ok: snapshot GenericLiteLLMParams into the Mapping shape embedding_executor=embedding_executor, timeout=timeout, ) diff --git a/litellm/llms/dashscope/chat/transformation.py b/litellm/llms/dashscope/chat/transformation.py index bcd2a5d5320..9f6b721c393 100644 --- a/litellm/llms/dashscope/chat/transformation.py +++ b/litellm/llms/dashscope/chat/transformation.py @@ -13,7 +13,7 @@ from ...openai.chat.gpt_transformation import OpenAIGPTConfig class DashScopeChatConfig(OpenAIGPTConfig): def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: base class contract returns a list - return [ + return [ # mutable-ok: base class contract returns a list *super().get_supported_openai_params(model=model), "reasoning_effort", ] diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 4e428a23392..ea19a7c7ddf 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 = [ + transformed: Final = [ # mutable-ok: provider messages must stay JSON-array lists the base transform mutates 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} + collapsed_message: Final = {**message, "content": collapsed} # mutable-ok: wire messages are plain JSON dicts 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}] - forwarded: Final = { + forwarded_content: Final = [*content, {"type": "text", "text": search_text}] # mutable-ok: JSON-array content + forwarded: Final = { # mutable-ok: wire messages are plain JSON dicts **{key: value for key, value in message_fields.items() if key != "search_results"}, "content": forwarded_content, } diff --git a/litellm/llms/edenai/audio_transcription/transformation.py b/litellm/llms/edenai/audio_transcription/transformation.py index ee574a4f406..fc8a13d5ccd 100644 --- a/litellm/llms/edenai/audio_transcription/transformation.py +++ b/litellm/llms/edenai/audio_transcription/transformation.py @@ -28,7 +28,7 @@ def _form_fields(model: str, optional_params: Mapping[str, object]) -> dict[str, extras: Final = optional_params.get("extra_body") nested: Final = extras.items() if isinstance(extras, Mapping) else () fields: Final = (*optional_params.items(), *nested, ("model", model)) - return {key: value for key, value in fields if key != "extra_body"} + return {key: value for key, value in fields if key != "extra_body"} # mutable-ok: httpx form data class EdenAIAudioTranscriptionConfig(OpenAIWhisperAudioTranscriptionConfig): @@ -69,7 +69,7 @@ class EdenAIAudioTranscriptionConfig(OpenAIWhisperAudioTranscriptionConfig): """Eden reports `duration` and `cost` on every body, so the Whisper default of `verbose_json`, which the gpt-4o-transcribe models reject, is not needed for cost tracking.""" audio: Final = process_audio_file(audio_file) - files: Final = {"file": (audio.filename, audio.file_content, audio.content_type)} + files: Final = {"file": (audio.filename, audio.file_content, audio.content_type)} # mutable-ok: httpx contract return AudioTranscriptionRequestData(data=_form_fields(model, optional_params), files=files) def transform_audio_transcription_response(self, raw_response: httpx.Response) -> TranscriptionResponse: diff --git a/litellm/llms/edenai/chat/transformation.py b/litellm/llms/edenai/chat/transformation.py index 84866044103..67d308d9e38 100644 --- a/litellm/llms/edenai/chat/transformation.py +++ b/litellm/llms/edenai/chat/transformation.py @@ -61,7 +61,7 @@ class EdenAIChatConfig(OpenAIGPTConfig): if litellm.supports_reasoning(model=model, custom_llm_provider=litellm.LlmProviders.EDENAI.value) else () ) - return [*super().get_supported_openai_params(model), *reasoning] + return [*super().get_supported_openai_params(model), *reasoning] # mutable-ok: inherited contract @staticmethod def get_api_key(api_key: str | None = None) -> str | None: @@ -84,7 +84,7 @@ class EdenAIChatConfig(OpenAIGPTConfig): ) if not request.get("stream"): return request - return {**request, "stream_options": dict(_stream_options_with_usage(request))} + return {**request, "stream_options": dict(_stream_options_with_usage(request))} # mutable-ok: JSON body def transform_response( self, @@ -141,4 +141,4 @@ class EdenAIChatConfig(OpenAIGPTConfig): if not response.is_success: raise EdenAIException(status_code=response.status_code, message=response.text, headers=response.headers) catalog: Final = _EdenAIModelCatalog.model_validate(response.json()) - return [f"edenai/{model.id}" for model in catalog.data] + return [f"edenai/{model.id}" for model in catalog.data] # mutable-ok: inherited contract diff --git a/litellm/llms/edenai/common_utils.py b/litellm/llms/edenai/common_utils.py index ab7ee9b1c9d..a97354cc30b 100644 --- a/litellm/llms/edenai/common_utils.py +++ b/litellm/llms/edenai/common_utils.py @@ -61,7 +61,7 @@ def reported_cost(payload: object) -> float | None: def authorized_headers( headers: Mapping[str, object], api_key: str | None, model: str ) -> dict[str, object]: # mutable-ok: header contract - return {**headers, "Authorization": f"Bearer {require_api_key(api_key, model)}"} + return {**headers, "Authorization": f"Bearer {require_api_key(api_key, model)}"} # mutable-ok: header contract def json_headers( @@ -69,7 +69,7 @@ def json_headers( ) -> dict[str, object]: # mutable-ok: header contract """The shared HTTP handler sends some JSON bodies as raw content, so the type must be set here.""" authorized: Final = authorized_headers(headers, api_key, model) - return {**authorized, "Content-Type": "application/json"} + return {**authorized, "Content-Type": "application/json"} # mutable-ok: header contract def endpoint_url(api_base: str | None, path: str) -> str: diff --git a/litellm/llms/edenai/embedding/transformation.py b/litellm/llms/edenai/embedding/transformation.py index c79a6839434..1c2cc937875 100644 --- a/litellm/llms/edenai/embedding/transformation.py +++ b/litellm/llms/edenai/embedding/transformation.py @@ -26,7 +26,7 @@ _SUPPORTED_PARAMS: Final = ("dimensions", "encoding_format", "user") class EdenAIEmbeddingConfig(BaseEmbeddingConfig): def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: inherited contract - return list(_SUPPORTED_PARAMS) + return list(_SUPPORTED_PARAMS) # mutable-ok: inherited contract def map_openai_params( self, @@ -35,7 +35,7 @@ class EdenAIEmbeddingConfig(BaseEmbeddingConfig): model: str, drop_params: bool, ) -> dict[str, object]: # mutable-ok: inherited contract - return {**optional_params, **pick(non_default_params, _SUPPORTED_PARAMS)} + return {**optional_params, **pick(non_default_params, _SUPPORTED_PARAMS)} # mutable-ok: inherited contract def validate_environment( self, @@ -67,7 +67,7 @@ class EdenAIEmbeddingConfig(BaseEmbeddingConfig): optional_params: dict[str, object], # mutable-ok: inherited contract headers: dict[str, object], # mutable-ok: inherited contract ) -> dict[str, object]: # mutable-ok: inherited contract - return {"model": model, "input": input, **optional_params} + return {"model": model, "input": input, **optional_params} # mutable-ok: inherited contract def transform_embedding_response( self, diff --git a/litellm/llms/edenai/image_generation/transformation.py b/litellm/llms/edenai/image_generation/transformation.py index e2a0b8684f6..7f729cd7fbb 100644 --- a/litellm/llms/edenai/image_generation/transformation.py +++ b/litellm/llms/edenai/image_generation/transformation.py @@ -40,7 +40,7 @@ class EdenAIImageGenerationConfig(BaseImageGenerationConfig): def get_supported_openai_params( self, model: str ) -> list[OpenAIImageGenerationOptionalParams]: # mutable-ok: inherited contract - return list(_SUPPORTED_PARAMS) + return list(_SUPPORTED_PARAMS) # mutable-ok: inherited contract def map_openai_params( self, @@ -49,7 +49,7 @@ class EdenAIImageGenerationConfig(BaseImageGenerationConfig): model: str, drop_params: bool, ) -> dict[str, object]: # mutable-ok: inherited contract - return {**optional_params, **pick(non_default_params, _SUPPORTED_PARAMS)} + return {**optional_params, **pick(non_default_params, _SUPPORTED_PARAMS)} # mutable-ok: inherited contract def get_complete_url( self, @@ -82,7 +82,7 @@ class EdenAIImageGenerationConfig(BaseImageGenerationConfig): litellm_params: dict[str, object], # mutable-ok: inherited contract headers: dict[str, object], # mutable-ok: inherited contract ) -> dict[str, object]: # mutable-ok: inherited contract - return {"model": model, "prompt": prompt, **optional_params} + return {"model": model, "prompt": prompt, **optional_params} # mutable-ok: inherited contract def transform_image_generation_response( self, diff --git a/litellm/llms/edenai/text_to_speech/transformation.py b/litellm/llms/edenai/text_to_speech/transformation.py index 8d503ffa72f..50c7ed96725 100644 --- a/litellm/llms/edenai/text_to_speech/transformation.py +++ b/litellm/llms/edenai/text_to_speech/transformation.py @@ -23,7 +23,7 @@ _SUPPORTED_PARAMS: Final = ("voice", "response_format", "speed", "instructions") class EdenAITextToSpeechConfig(BaseTextToSpeechConfig): def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: inherited contract - return list(_SUPPORTED_PARAMS) + return list(_SUPPORTED_PARAMS) # mutable-ok: inherited contract def map_openai_params( self, @@ -62,7 +62,9 @@ class EdenAITextToSpeechConfig(BaseTextToSpeechConfig): headers: dict[str, object], # mutable-ok: inherited contract ) -> TextToSpeechRequestData: fields: Final = (("model", model), ("input", input), ("voice", voice), *optional_params.items()) - return TextToSpeechRequestData(dict_body={key: value for key, value in fields if value is not None}) + return TextToSpeechRequestData( + dict_body={key: value for key, value in fields if value is not None} # mutable-ok: TypedDict field + ) def transform_text_to_speech_response( self, diff --git a/litellm/llms/edenai/videos/transformation.py b/litellm/llms/edenai/videos/transformation.py index cd3e1a7d2d1..31572e9e5fe 100644 --- a/litellm/llms/edenai/videos/transformation.py +++ b/litellm/llms/edenai/videos/transformation.py @@ -28,7 +28,7 @@ def _usage_with_reported_cost( usage: Mapping[str, object] | None, body: bytes ) -> dict[str, object]: # mutable-ok: VideoObject.usage is a plain dict field cost: Final = reported_cost(body) - return { + return { # mutable-ok: VideoObject.usage is a plain dict field key: value for key, value in (*(usage.items() if usage else ()), ("provider_reported_cost_usd", cost)) if value is not None @@ -80,13 +80,13 @@ class EdenAIVideoConfig(OpenAIVideoConfig): model=model, prompt=prompt, api_base=api_base, - video_create_optional_request_params={ + video_create_optional_request_params={ # mutable-ok: inherited contract key: value for key, value in video_create_optional_request_params.items() if key != "input_reference" }, litellm_params=litellm_params, headers=headers, ) - return {**data, "input_reference": dict(reference)}, files, url + return {**data, "input_reference": dict(reference)}, files, url # mutable-ok: JSON body def transform_video_create_response( self, diff --git a/litellm/llms/fal_ai/chat/transformation.py b/litellm/llms/fal_ai/chat/transformation.py index d107426d793..164b660b21a 100644 --- a/litellm/llms/fal_ai/chat/transformation.py +++ b/litellm/llms/fal_ai/chat/transformation.py @@ -112,7 +112,7 @@ class FalAIChatConfig(BaseConfig): return (api_base or get_secret_str("FAL_AI_API_BASE") or DEFAULT_BASE_URL).rstrip("/") def get_supported_openai_params(self, model: str) -> list: # mutable-ok: inherited contract returns a list - return list(("reasoning_effort", "temperature", "top_p")) + return list(("reasoning_effort", "temperature", "top_p")) # mutable-ok: inherited contract returns a list def _map_reasoning_effort(self, value: object, model: str, drop_params: bool) -> bool | None: if isinstance(value, str) and value in REASONING_DISABLED_EFFORTS: @@ -138,12 +138,12 @@ class FalAIChatConfig(BaseConfig): model: str, drop_params: bool, ) -> dict: # mutable-ok: inherited contract returns a dict - mapped: Final = { + mapped: Final = { # mutable-ok: intermediate translation map, folded into the returned dict translated[0]: translated[1] for param, value in non_default_params.items() if (translated := self._translate_param(param, value, model, drop_params)) is not None } - return {**optional_params, **mapped} + return {**optional_params, **mapped} # mutable-ok: inherited contract returns a dict def validate_environment( self, @@ -158,9 +158,9 @@ class FalAIChatConfig(BaseConfig): final_api_key: Final = self.get_api_key(api_key) if not final_api_key: raise ValueError("FAL_AI_API_KEY is not set") - return { + return { # mutable-ok: inherited contract returns a dict "content-type": "application/json", - **(headers or {}), + **(headers or {}), # mutable-ok: empty default for the inherited contract's headers "Authorization": f"Key {final_api_key}", } @@ -186,10 +186,12 @@ class FalAIChatConfig(BaseConfig): if optional_params.get("stream"): raise FalAIError(status_code=400, message="fal_ai chat completions do not support streaming") prompt, image_url = _prompt_and_image(messages) - return { + return { # mutable-ok: JSON request body "prompt": prompt, "image_url": image_url, - **{key: value for key, value in optional_params.items() if key in PASSTHROUGH_PARAMS and value is not None}, + **{ # mutable-ok: JSON request body + key: value for key, value in optional_params.items() if key in PASSTHROUGH_PARAMS and value is not None + }, } def transform_response( diff --git a/litellm/llms/fal_ai/image_edit/flux_lora_depth_transformation.py b/litellm/llms/fal_ai/image_edit/flux_lora_depth_transformation.py index 6d58caeb384..0b6205ff302 100644 --- a/litellm/llms/fal_ai/image_edit/flux_lora_depth_transformation.py +++ b/litellm/llms/fal_ai/image_edit/flux_lora_depth_transformation.py @@ -25,7 +25,7 @@ class FalAIFluxLoraDepthEditConfig(FalAIImageEditConfig): """ def get_supported_openai_params(self, model: str) -> list: # mutable-ok: base class contract returns a list - return list(SUPPORTED_OPENAI_PARAMS) + return list(SUPPORTED_OPENAI_PARAMS) # mutable-ok: base class contract returns a list def map_openai_params( self, @@ -33,7 +33,7 @@ class FalAIFluxLoraDepthEditConfig(FalAIImageEditConfig): model: str, drop_params: bool, ) -> dict: # mutable-ok: base class contract returns a dict - return { + return { # mutable-ok: base class contract returns a dict PARAM_TRANSLATION.get(key, key): self._translate_value(key, value, model) for key, value in image_edit_optional_params.items() if value is not None and key in PARAM_TRANSLATION diff --git a/litellm/llms/fal_ai/image_edit/transformation.py b/litellm/llms/fal_ai/image_edit/transformation.py index d77769b130d..839c15c4c28 100644 --- a/litellm/llms/fal_ai/image_edit/transformation.py +++ b/litellm/llms/fal_ai/image_edit/transformation.py @@ -82,7 +82,7 @@ class FalAIImageEditConfig(BaseImageEditConfig): """ def get_supported_openai_params(self, model: str) -> list: # mutable-ok: base class contract returns a list - return list(SUPPORTED_OPENAI_PARAMS) + return list(SUPPORTED_OPENAI_PARAMS) # mutable-ok: base class contract returns a list def map_openai_params( self, @@ -90,7 +90,7 @@ class FalAIImageEditConfig(BaseImageEditConfig): model: str, drop_params: bool, ) -> dict: - return { + return { # mutable-ok: base class contract returns a dict PARAM_TRANSLATION.get(key, key): self._translate_value(key, value, model) for key, value in image_edit_optional_params.items() if value is not None @@ -114,7 +114,7 @@ class FalAIImageEditConfig(BaseImageEditConfig): final_api_key: Final = api_key or get_secret_str("FAL_AI_API_KEY") if not final_api_key: raise ValueError("FAL_AI_API_KEY is not set") - return {**headers, "Authorization": f"Key {final_api_key}"} + return {**headers, "Authorization": f"Key {final_api_key}"} # mutable-ok: base class contract returns a dict def use_multipart_form_data(self) -> bool: return False @@ -171,5 +171,7 @@ class FalAIImageEditConfig(BaseImageEditConfig): headers=raw_response.headers, ) model_response: Final = ImageResponse() - model_response.data = list(fal_images_to_image_objects(response_json.get("images", ()))) + model_response.data = list( # mutable-ok: ImageResponse.data is typed as a list + fal_images_to_image_objects(response_json.get("images", ())) + ) return model_response 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 7708fabae9d..0d008555f8b 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 @@ -102,7 +102,7 @@ class FalAIGPTImage2Config(FalAIBaseConfig): return f"{base_url}/{endpoint}" def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]: - return list(SUPPORTED_OPENAI_PARAMS) + return list(SUPPORTED_OPENAI_PARAMS) # mutable-ok: base class contract returns a list def map_openai_params( self, @@ -127,7 +127,7 @@ class FalAIGPTImage2Config(FalAIBaseConfig): if key in self.PARAM_TRANSLATION and self.PARAM_TRANSLATION[key] not in optional_params } ) - return {**optional_params, **translated_params} + return {**optional_params, **translated_params} # mutable-ok: base class contract returns a dict def _translate_value(self, key: str, value: object, model: str) -> object: if key == "size": @@ -144,4 +144,4 @@ class FalAIGPTImage2Config(FalAIBaseConfig): litellm_params: Mapping[str, object], headers: Mapping[str, str], ) -> dict: - return {"prompt": prompt, **optional_params} + return {"prompt": prompt, **optional_params} # mutable-ok: base class contract returns a dict diff --git a/litellm/llms/fal_ai/videos/transformation.py b/litellm/llms/fal_ai/videos/transformation.py index cbcd92acbaf..e46199dd89f 100644 --- a/litellm/llms/fal_ai/videos/transformation.py +++ b/litellm/llms/fal_ai/videos/transformation.py @@ -272,7 +272,9 @@ def _status_video_object( status="failed" if error else status, created_at=0, model=model_path, - error=({"code": "fal_error", "message": error} if error else None), + error=( + {"code": "fal_error", "message": error} if error else None # mutable-ok: VideoObject requires a dict + ), ) @@ -287,7 +289,7 @@ class FalAIVideoConfig(BaseVideoConfig): self._async_client_factory: Final = async_client_factory def get_supported_openai_params(self, model: str) -> _SupportedParams: - supported_params: Final[_SupportedParams] = [ + supported_params: Final[_SupportedParams] = [ # mutable-ok: BaseVideoConfig requires a list "model", "prompt", "input_reference", @@ -314,7 +316,11 @@ class FalAIVideoConfig(BaseVideoConfig): if not isinstance(input_reference, str) else MappingProxyType( { - profile.reference_key: ([input_reference] if profile.reference_as_list else input_reference), + profile.reference_key: ( + [input_reference] # mutable-ok: fal.ai expects a list for H3 references + if profile.reference_as_list + else input_reference + ), } ) ) @@ -338,7 +344,9 @@ class FalAIVideoConfig(BaseVideoConfig): **duration_params, **size_params, **user_params, - **{key: value for key, value in video_create_optional_params.items() if key not in supported_params}, + **{ # mutable-ok: BaseVideoConfig requires a mutable parameter mapping + key: value for key, value in video_create_optional_params.items() if key not in supported_params + }, } return mapped_params @@ -391,9 +399,11 @@ class FalAIVideoConfig(BaseVideoConfig): ) -> tuple[_VideoParams, RequestFiles, str]: request_data: Final[_VideoParams] = { "prompt": prompt, - **{key: value for key, value in video_create_optional_request_params.items() if key != "model"}, + **{ # mutable-ok: HTTP JSON payload requires a mutable mapping + key: value for key, value in video_create_optional_request_params.items() if key != "model" + }, } - return request_data, [], f"{api_base.rstrip('/')}/{model}" + return request_data, [], f"{api_base.rstrip('/')}/{model}" # mutable-ok: HTTP files payload requires a list def transform_video_create_response( self, @@ -412,7 +422,7 @@ class FalAIVideoConfig(BaseVideoConfig): resolution: Final[object] = request_params.get("resolution") seconds: Final[str | None] = _duration_value(request_params["duration"]) if duration is not None else None size: Final[str | None] = resolution if isinstance(resolution, str) else None - usage: Final[_VideoParams] = { + usage: Final[_VideoParams] = { # mutable-ok: VideoObject requires a mutable usage mapping key: value for key, value in ( ("duration_seconds", duration), @@ -446,7 +456,7 @@ class FalAIVideoConfig(BaseVideoConfig): encoded_request_id: Final[str] = encode_url_path_segment(request_id, field_name="video_id") return ( f"{api_base.rstrip('/')}/{_queue_request_base_path(model_id)}/requests/{encoded_request_id}/status", - {}, + {}, # mutable-ok: BaseVideoConfig requires a mutable mapping ) def transform_video_status_retrieve_response( @@ -479,7 +489,7 @@ class FalAIVideoConfig(BaseVideoConfig): try: result_response: Final[httpx.Response] = result_client.get( url=result_url, - headers=dict(result_headers), + headers=dict(result_headers), # mutable-ok: HTTPHandler.get only accepts a dict ) except httpx.TransportError: return None @@ -515,7 +525,7 @@ class FalAIVideoConfig(BaseVideoConfig): try: result_response: Final[httpx.Response] = await result_client.get( url=result_url, - headers=dict(result_headers), + headers=dict(result_headers), # mutable-ok: AsyncHTTPHandler.get only accepts a dict ) except httpx.TransportError: return None @@ -542,7 +552,7 @@ class FalAIVideoConfig(BaseVideoConfig): encoded_request_id: Final[str] = encode_url_path_segment(request_id, field_name="video_id") return ( f"{api_base.rstrip('/')}/{_queue_request_base_path(model_id)}/requests/{encoded_request_id}", - {}, + {}, # mutable-ok: BaseVideoConfig requires a mutable mapping ) @staticmethod @@ -568,7 +578,7 @@ class FalAIVideoConfig(BaseVideoConfig): raise FalAIVideoError( status_code=raw_response.status_code, message=error, - headers=dict(raw_response.headers), + headers=dict(raw_response.headers), # mutable-ok: exception headers require a mutable dictionary request=raw_response.request, response=raw_response, ) @@ -586,7 +596,7 @@ class FalAIVideoConfig(BaseVideoConfig): raise FalAIVideoError( status_code=raw_response.status_code, message=error, - headers=dict(raw_response.headers), + headers=dict(raw_response.headers), # mutable-ok: exception headers require a mutable dictionary request=raw_response.request, response=raw_response, ) diff --git a/litellm/llms/fireworks_ai/chat/transformation.py b/litellm/llms/fireworks_ai/chat/transformation.py index 29a989a5cb0..15049e71bbc 100644 --- a/litellm/llms/fireworks_ai/chat/transformation.py +++ b/litellm/llms/fireworks_ai/chat/transformation.py @@ -81,7 +81,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}} + return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}} # mutable-ok: JSON request body EFFORT_KWARG_KEYS: Final = frozenset({"enable_thinking", "thinking", "reasoning_budget", "low_effort"}) @@ -353,7 +353,7 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig): ) -> dict: # mutable-ok: http handler pops extra_body off the returned dict extra_body: Final = optional_params.get("extra_body") if not isinstance(extra_body, dict): - return dict(optional_params) + return dict(optional_params) # mutable-ok: JSON request body stripped: Final = tuple(sorted(k for k in extra_body if k in NIM_VLLM_STRIP_PARAMS)) if stripped: @@ -377,11 +377,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"} - return { + 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, - **dict(promoted), - **({"extra_body": dict(remaining)} if remaining else {}), + **dict(promoted), # mutable-ok: JSON request body + **({"extra_body": dict(remaining)} if remaining else {}), # mutable-ok: JSON request body } @staticmethod @@ -450,12 +450,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 = { + grammar_response_format: Final = { # mutable-ok: JSON request body "type": "grammar", "grammar": extra_body["guided_grammar"], } return (("response_format", grammar_response_format),) - choice_schema: Final = { + choice_schema: Final = { # mutable-ok: JSON request body "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 17fadf7ae0f..8352690235d 100644 --- a/litellm/llms/fireworks_ai/common_utils.py +++ b/litellm/llms/fireworks_ai/common_utils.py @@ -111,4 +111,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) + return dict(pinned) # mutable-ok: the HTTP handler updates the returned headers in place diff --git a/litellm/llms/fireworks_ai/completion/transformation.py b/litellm/llms/fireworks_ai/completion/transformation.py index e207ae0cecf..4f0e302003a 100644 --- a/litellm/llms/fireworks_ai/completion/transformation.py +++ b/litellm/llms/fireworks_ai/completion/transformation.py @@ -58,12 +58,14 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig self, optional_params: Mapping[str, object], model: str ) -> dict: # mutable-ok: returned dict is spread into the OpenAI SDK call as kwargs raw_extra_body: Final = optional_params.get("extra_body") - initial_body: Final = dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {} + initial_body: Final = ( + dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {} # mutable-ok: JSON request body + ) 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 = { + base: Final = { # mutable-ok: JSON request body k: v for k, v in optional_params.items() if k not in ("extra_body", "response_format", "reasoning_effort", "thinking") @@ -83,13 +85,15 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig stripped, model, ) - return {k: v for k, v in extra_body.items() if k not in _TEXT_COMPLETION_STRIP_PARAMS} + return { # mutable-ok: JSON request body + 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) + moved: Final = dict(extra_body) # mutable-ok: JSON request body for key in ("response_format", "reasoning_effort", "thinking"): value = optional_params.get(key) if value is None: @@ -104,8 +108,10 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig ) -> dict: # mutable-ok: JSON request body chat_template_kwargs: Final = extra_body.get("chat_template_kwargs") if chat_template_kwargs is None: - return dict(extra_body) - result: Final = {k: v for k, v in extra_body.items() if k != "chat_template_kwargs"} + 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" + } if not isinstance(chat_template_kwargs, dict): verbose_logger.debug( "fireworks_ai dropping chat_template_kwargs for model=%s; expected an object, got %s.", @@ -134,18 +140,18 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig model, ) return result - return {**result, "reasoning_effort": effort} + return {**result, "reasoning_effort": effort} # mutable-ok: JSON request body @staticmethod def _translate_guided_into_extra_body( extra_body: Mapping[str, object], optional_params: Mapping[str, object] ) -> dict: # mutable-ok: JSON request body guided_response_format: Final = FireworksAIConfig.translate_guided_params(extra_body, optional_params) - remaining: Final = { + remaining: Final = { # mutable-ok: JSON request body k: v for k, v in extra_body.items() if k not in ("guided_json", "guided_grammar", "guided_choice") } if guided_response_format: - return { + return { # mutable-ok: JSON request body **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 c1010102093..f7dd774ea18 100644 --- a/litellm/llms/fireworks_ai/responses/transformation.py +++ b/litellm/llms/fireworks_ai/responses/transformation.py @@ -111,7 +111,9 @@ 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, - [_developer_item_as_system(item) for index, item in enumerate(items) if index not in folded], + [ # 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 + ], ) @@ -134,7 +136,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) + return dict(pinned) # mutable-ok: the HTTP handler updates the returned headers in place 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("/") @@ -156,7 +158,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 = { + folded_params: Final = { # mutable-ok: the base class takes the optional params as a dict 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 48a345a4940..c8dd7a9a5ff 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"] + return ["language", "response_format", "timestamp_granularities"] # mutable-ok: base contract returns a list @property def supports_subtitle_synthesis(self) -> bool: @@ -62,7 +62,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): ) -> dict: # mutable-ok: BaseAudioTranscriptionConfig signature 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)) + return dict((*optional_params.items(), *accepted)) # mutable-ok: base contract returns a plain dict def get_error_class( self, @@ -88,7 +88,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig): status_code=401, message="Google API key is required. Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable.", ) - return { + return { # mutable-ok: the http handler passes these headers straight to httpx **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)) + return AudioTranscriptionRequestData(data=dict(request)) # mutable-ok: AudioTranscriptionRequestData wants dict 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) + response["words"] = list(words) # mutable-ok: verbose_json words is a JSON array 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} + return {key: value for key, value in entries if value is not None} # mutable-ok: word entries serialize to JSON 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 12c962387c6..494a72d6999 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 = { +guardrail_translation_mappings: Final = { # mutable-ok: discover_guardrail_translation_mappings only accepts isinstance(mappings, dict) 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 0c1fe8171e8..e13e1e63cbb 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) + return list(texts) # mutable-ok: GenericGuardrailAPIInputs.texts is declared list[str] def _write_back_texts(text_parts: Sequence[object], guardrailed_texts: Sequence[str] | None) -> None: @@ -252,4 +252,4 @@ class GoogleGenAIGenerateContentHandler(BaseTranslation): 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)) + return dict((*base.items(), *context_pairs, *metadata_pairs)) # mutable-ok: apply_guardrail takes a plain dict diff --git a/litellm/llms/gigachat/chat/streaming.py b/litellm/llms/gigachat/chat/streaming.py index 5324aa6f94e..908412d9c31 100644 --- a/litellm/llms/gigachat/chat/streaming.py +++ b/litellm/llms/gigachat/chat/streaming.py @@ -42,7 +42,7 @@ class GigaChatModelResponseIterator: ) choice: Final = choices[0] - delta: Mapping[str, object] = choice.get("delta") or {} + delta: Mapping[str, object] = choice.get("delta") or {} # mutable-ok: empty dict default for get 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 {} + usage_data: Final = chunk.get("usage") or {} # mutable-ok: empty dict default 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 15a1b463c3f..c047dc0c881 100644 --- a/litellm/llms/gigachat/chat/transformation.py +++ b/litellm/llms/gigachat/chat/transformation.py @@ -136,7 +136,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 [ + return [ # mutable-ok: base class contract returns list "stream", "temperature", "top_p", @@ -195,7 +195,7 @@ class GigaChatConfig(BaseConfig): schema_name = json_schema.get("name", "structured_output") schema = json_schema.get("schema", {}) - function_def = { + function_def = { # mutable-ok: request payload for httpx "name": schema_name, "description": f"Output structured response: {schema_name}", "parameters": schema, @@ -210,7 +210,7 @@ class GigaChatConfig(BaseConfig): ), function_def, ] - optional_params["function_call"] = {"name": schema_name} + optional_params["function_call"] = {"name": schema_name} # mutable-ok: request payload optional_params["_structured_output"] = True return optional_params diff --git a/litellm/llms/gigachat/embedding/transformation.py b/litellm/llms/gigachat/embedding/transformation.py index 927f5e944b6..0db4475be8f 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 + normalized_input: Final = [input] if isinstance(input, str) else input # mutable-ok: preserve list API 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 d90ddbbbe2c..e1f73d04275 100644 --- a/litellm/llms/gigachat/passthrough/transformation.py +++ b/litellm/llms/gigachat/passthrough/transformation.py @@ -93,14 +93,16 @@ 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 [], + messages=list(raw_messages) + if isinstance(raw_messages, list) + else [], # mutable-ok: transform_response wants a list raw_response=httpx_response, model_response=ModelResponse(), logging_obj=logging_obj, - optional_params={}, - litellm_params={}, + optional_params={}, # mutable-ok: empty dict kwarg for transform_response + litellm_params={}, # mutable-ok: empty dict kwarg for transform_response api_key="", - request_data=dict(request_data), + request_data=dict(request_data), # mutable-ok: transform_response wants a dict encoding=encoding, ) @@ -121,10 +123,10 @@ class GigaChatPassthroughConfig(BasePassthroughConfig): raw_response=httpx_response, model_response=EmbeddingResponse(), logging_obj=logging_obj, - optional_params={}, + optional_params={}, # mutable-ok: empty dict kwarg for transform_embedding_response api_key="", - request_data=dict(request_data), - litellm_params={}, + request_data=dict(request_data), # mutable-ok: transform_embedding_response wants a dict + litellm_params={}, # mutable-ok: empty dict kwarg for transform_embedding_response ) ) diff --git a/litellm/llms/groq/chat/transformation.py b/litellm/llms/groq/chat/transformation.py index f60947714a5..1da7ad0a7b5 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"}], + tools=[{"type": "browser_search"}], # mutable-ok: request tools must be json dicts in a list ) return optional_params diff --git a/litellm/llms/hosted_vllm/image_edit/transformation.py b/litellm/llms/hosted_vllm/image_edit/transformation.py index 6804d3a0fb8..3b8cc437168 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 [ + return [ # mutable-ok: BaseImageEditConfig returns list param for param in super().get_supported_openai_params(model) if param not in PARAMS_VLLM_OMNI_DOES_NOT_ACCEPT @@ -23,7 +23,7 @@ class HostedVLLMImageEditConfig(OpenAIImageEditConfig): api_base: str | None = None, ) -> dict: # mutable-ok: BaseImageEditConfig contract resolved_key: Final = api_key or get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key" - return {**headers, "Authorization": f"Bearer {resolved_key}"} + return {**headers, "Authorization": f"Bearer {resolved_key}"} # mutable-ok: httpx headers are a dict def get_complete_url( self, diff --git a/litellm/llms/hosted_vllm/videos/transformation.py b/litellm/llms/hosted_vllm/videos/transformation.py index 22e4f4876fb..96cbfc3cf70 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 [ + return [ # mutable-ok: BaseVideoConfig returns list *super().get_supported_openai_params(model), *_VLLM_OMNI_VIDEO_PARAMS, ] @@ -146,7 +146,9 @@ class HostedVLLMVideoConfig(OpenAIVideoConfig): model: str, drop_params: bool, ) -> dict: # mutable-ok: BaseVideoConfig contract; extra_body merge mutates this dict - return {key: value for key, value in video_create_optional_params.items() if value is not None} + 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 + } def validate_environment( self, @@ -161,7 +163,7 @@ class HostedVLLMVideoConfig(OpenAIVideoConfig): or get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key" ) - return {**headers, "Authorization": f"Bearer {resolved_key}"} + return {**headers, "Authorization": f"Bearer {resolved_key}"} # mutable-ok: httpx headers are a dict def get_complete_url( self, @@ -189,10 +191,10 @@ class HostedVLLMVideoConfig(OpenAIVideoConfig): litellm_params: GenericLiteLLMParams, headers: dict, # mutable-ok: BaseVideoConfig contract ) -> tuple[dict, RequestFiles, str]: # mutable-ok: BaseVideoConfig contract - data: Final = { + data: Final = { # mutable-ok: BaseVideoConfig contract returns a data dict "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 f83c605a3d2..9fc2d2cbb45 100644 --- a/litellm/llms/litellm_proxy/skills/transformation.py +++ b/litellm/llms/litellm_proxy/skills/transformation.py @@ -222,7 +222,9 @@ class LiteLLMSkillsTransformationHandler: user_api_key_dict=user_api_key_dict, ) - skills: Final = [self.db_skill_to_response(s) for s in db_skills] + skills: Final = [ # mutable-ok: ListSkillsResponse.data needs list[Skill]; never mutated after + 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 46231e5e980..442c79255af 100644 --- a/litellm/llms/meta/realtime/transformation.py +++ b/litellm/llms/meta/realtime/transformation.py @@ -526,7 +526,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)), + "response": list(self._backend_events(payload)), # mutable-ok: RealtimeResponseTypedDict.response is a list "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/mistral/audio_speech/transformation.py b/litellm/llms/mistral/audio_speech/transformation.py index 04a7e3b9341..2b3264dc756 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"] + return ["voice", "response_format"] # mutable-ok: base class contract returns a plain list def _map_openai_voice(self, voice_id: str) -> str: return self.OPENAI_VOICE_ALIASES.get(voice_id.lower(), voice_id) @@ -83,7 +83,7 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig): 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 = { + mapped_params: Final = { # mutable-ok: base class contract returns a plain dict key: value for key, value in (("response_format", response_format), ("ref_audio", ref_audio)) if isinstance(value, str) @@ -103,7 +103,7 @@ class MistralTextToSpeechConfig(BaseTextToSpeechConfig): status_code=401, message="Mistral API key is required. Set MISTRAL_API_KEY or pass api_key.", ) - return { + return { # mutable-ok: base class contract returns a plain dict **headers, "Authorization": f"Bearer {resolved_key}", "Content-Type": "application/json", diff --git a/litellm/llms/mistral/batches/transformation.py b/litellm/llms/mistral/batches/transformation.py index 3496feed585..d3ed6a3af62 100644 --- a/litellm/llms/mistral/batches/transformation.py +++ b/litellm/llms/mistral/batches/transformation.py @@ -97,7 +97,9 @@ def _to_batch_errors(errors: Sequence[MistralBatchError]) -> BatchErrors | None: return None return BatchErrors( object="list", - data=[BatchError(message=f"{e.message} (x{e.count})" if e.count > 1 else e.message) for e in errors], + data=[ # mutable-ok: openai Batch.Errors.data is typed as list + BatchError(message=f"{e.message} (x{e.count})" if e.count > 1 else e.message) for e in errors + ], ) @@ -176,7 +178,7 @@ class MistralBatchesConfig(BaseBatchesConfig): if metadata else MistralCreateBatchJobRequest(input_files=(input_file_id,), endpoint=endpoint, model=model) ) - return dict(body) + return dict(body) # mutable-ok: BaseBatchesConfig signature def transform_create_batch_response( self, @@ -201,7 +203,7 @@ class MistralBatchesConfig(BaseBatchesConfig): url=f"{get_mistral_api_base(api_base if isinstance(api_base, str) else None)}/v1/batch/jobs/{encoded_batch_id}", headers=get_mistral_auth_headers(_NO_HEADERS, api_key if isinstance(api_key, str) else None), ) - return dict(request) + return dict(request) # mutable-ok: BaseBatchesConfig signature def transform_retrieve_batch_response( self, diff --git a/litellm/llms/mistral/common_utils.py b/litellm/llms/mistral/common_utils.py index ef354047b06..2f14328afdf 100644 --- a/litellm/llms/mistral/common_utils.py +++ b/litellm/llms/mistral/common_utils.py @@ -28,12 +28,14 @@ def get_mistral_auth_headers( raise ValueError( "Missing Mistral API Key - A call is being made to Mistral but no key is set either in the environment variables or via params" ) - return dict(headers, Authorization=f"Bearer {resolved_key}") + return dict(headers, Authorization=f"Bearer {resolved_key}") # mutable-ok: BaseConfig contract returns dict def mistral_error(error_message: str, status_code: int, headers: Mapping[str, str] | httpx.Headers) -> MistralError: return MistralError( status_code=status_code, message=error_message, - headers=headers if isinstance(headers, httpx.Headers) else httpx.Headers(dict(headers)), + headers=headers + if isinstance(headers, httpx.Headers) + else httpx.Headers(dict(headers)), # mutable-ok: httpx.Headers takes a dict ) diff --git a/litellm/llms/mistral/files/transformation.py b/litellm/llms/mistral/files/transformation.py index c1e3f50c379..6edf188d247 100644 --- a/litellm/llms/mistral/files/transformation.py +++ b/litellm/llms/mistral/files/transformation.py @@ -155,7 +155,7 @@ class MistralFilesConfig(BaseFilesConfig): def get_supported_openai_params( self, model: str ) -> list[OpenAICreateFileRequestOptionalParams]: # mutable-ok: BaseFilesConfig signature - return ["purpose"] + return ["purpose"] # mutable-ok: BaseFilesConfig signature def map_openai_params( self, @@ -182,7 +182,7 @@ class MistralFilesConfig(BaseFilesConfig): file=(filename, extracted["content"], content_type), purpose=(None, _to_mistral_purpose(create_file_data.get("purpose") or "batch")), ) - return dict(upload) + return dict(upload) # mutable-ok: BaseFilesConfig signature def transform_create_file_response( self, @@ -235,7 +235,7 @@ class MistralFilesConfig(BaseFilesConfig): url: Final = f"{_api_base_from(litellm_params)}/v1/files" if not purpose: return url, _NO_QUERY_PARAMS - return url, {"purpose": _to_mistral_purpose(purpose)} + return url, {"purpose": _to_mistral_purpose(purpose)} # mutable-ok: BaseFilesConfig signature returns dict def transform_list_files_response( self, @@ -243,7 +243,9 @@ class MistralFilesConfig(BaseFilesConfig): logging_obj: LiteLLMLoggingObj, litellm_params: Mapping[str, object], ) -> list[OpenAIFileObject]: # mutable-ok: BaseFilesConfig signature - return [_to_openai_file_object(f) for f in MistralFileList.model_validate(raw_response.json()).data] + return [ # mutable-ok: BaseFilesConfig signature + _to_openai_file_object(f) for f in MistralFileList.model_validate(raw_response.json()).data + ] def transform_file_content_request( self, diff --git a/litellm/llms/mongodb/vector_stores/transformation.py b/litellm/llms/mongodb/vector_stores/transformation.py index c6d3a2db3b4..94d3aef48cc 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=[]) + return VectorStoreIndexEndpoints(read=[], write=[]) # mutable-ok: the TypedDict declares list fields @staticmethod def _reject_unknown_params(litellm_params: Mapping[str, object]) -> None: @@ -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(), diff --git a/litellm/llms/nadir/chat/transformation.py b/litellm/llms/nadir/chat/transformation.py index d22b1a51bab..306df1208b9 100644 --- a/litellm/llms/nadir/chat/transformation.py +++ b/litellm/llms/nadir/chat/transformation.py @@ -35,7 +35,7 @@ def _reported_cost_usd(raw_response: httpx.Response) -> float | None: class NadirConfig(OpenAIGPTConfig): def get_supported_openai_params(self, model: str) -> list: # mutable-ok: return type fixed by the base interface - return list(_SUPPORTED_OPENAI_PARAMS) + return list(_SUPPORTED_OPENAI_PARAMS) # mutable-ok: the base interface returns a list def transform_response( self, diff --git a/litellm/llms/nimble/search/transformation.py b/litellm/llms/nimble/search/transformation.py index f40ca60fb6c..7485686d230 100644 --- a/litellm/llms/nimble/search/transformation.py +++ b/litellm/llms/nimble/search/transformation.py @@ -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 { + return { # mutable-ok: httpx requires a plain dict of headers **headers, "Authorization": f"Bearer {resolved_api_key}", "Content-Type": "application/json", @@ -156,7 +156,7 @@ class NimbleSearchConfig(BaseSearchConfig): {param: value for param, value in optional_params.items() if param not in unified_params} ) - return { + return { # mutable-ok: httpx requires a plain dict for the JSON body **_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), + headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature ) return SearchResponse( - results=[ + results=[ # mutable-ok: SearchResponse.results is declared list[SearchResult] SearchResult( title=result.title or "", url=result.url or "", diff --git a/litellm/llms/nvidia_nim/passthrough/transformation.py b/litellm/llms/nvidia_nim/passthrough/transformation.py index 930481cfceb..e8e7da8e10b 100644 --- a/litellm/llms/nvidia_nim/passthrough/transformation.py +++ b/litellm/llms/nvidia_nim/passthrough/transformation.py @@ -106,7 +106,7 @@ class NvidiaNimPassthroughConfig(BasePassthroughConfig): api_base: str | None = None, ) -> dict[str, str]: # mutable-ok: base class contract returns dict for httpx if api_key is None: - return dict(headers) + return dict(headers) # mutable-ok: base class contract returns dict for httpx return { **headers, "Authorization": f"Bearer {api_key}", diff --git a/litellm/llms/nvidia_nim/rerank/ranking_transformation.py b/litellm/llms/nvidia_nim/rerank/ranking_transformation.py index 177d7883feb..976b5c2211c 100644 --- a/litellm/llms/nvidia_nim/rerank/ranking_transformation.py +++ b/litellm/llms/nvidia_nim/rerank/ranking_transformation.py @@ -141,7 +141,9 @@ class NvidiaNimRankingConfig(NvidiaNimRerankConfig): self._client_side_top_n = top_n clean_model: Final = self._get_clean_model_name(model) - filtered_params: Final = {k: v for k, v in optional_rerank_params.items() if k not in ("top_n", "top_k")} + 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") + } return super().transform_rerank_request( model=clean_model, optional_rerank_params=filtered_params, @@ -166,9 +168,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 {} - resolved_optional_params: Final = optional_params or {} - resolved_litellm_params: Final = litellm_params or {} + 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 response: Final = super().transform_rerank_response( model=model, diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 6204bed8109..3b38825c83d 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -459,7 +459,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 = [ + sanitized: Final = [ # mutable-ok: request tools are a JSON list tool_with_sanitized_parameters(tool, sanitize) if isinstance(tool, dict) else tool for tool in tools ] return MappingProxyType({"tools": sanitized}) @@ -831,7 +831,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"] + return super().get_supported_openai_params(model) + ["reasoning_effort"] # mutable-ok: inherited contract class OpenAIChatCompletionStreamingHandler(BaseModelResponseIterator): diff --git a/litellm/llms/openai/chat/guardrail_translation/handler.py b/litellm/llms/openai/chat/guardrail_translation/handler.py index aa175733582..fa5512e7bfe 100644 --- a/litellm/llms/openai/chat/guardrail_translation/handler.py +++ b/litellm/llms/openai/chat/guardrail_translation/handler.py @@ -247,8 +247,8 @@ class OpenAIChatCompletionsHandler(BaseTranslation): texts_to_check=texts, images_to_check=images, tool_calls_to_check=tool_calls, - text_task_mappings=[], - tool_call_task_mappings=[], + text_task_mappings=[], # mutable-ok: required by _extract_inputs, unused here + tool_call_task_mappings=[], # mutable-ok: required by _extract_inputs, unused here ) if texts or tool_calls: return "no scannable content after message scoping" @@ -695,7 +695,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): cast( ModelResponse, stream_chunk_builder( - chunks=[ + chunks=[ # mutable-ok: callee takes a list OpenAIChatCompletionsHandler._narrowed_to_choice(response, index) for response in responses_so_far ], @@ -706,7 +706,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): for index in choice_indices ) (_, base_response), *_ = rebuilt_by_index - stitched_choices: Final = [ + stitched_choices: Final = [ # mutable-ok: choices is a List field; a tuple there breaks model_dump round-trips rebuilt.choices[0].model_copy(update=MappingProxyType({"index": index})) for index, rebuilt in rebuilt_by_index ] @@ -714,7 +714,7 @@ class OpenAIChatCompletionsHandler(BaseTranslation): @staticmethod def _narrowed_to_choice(response: "ModelResponseStream", index: int) -> "ModelResponseStream": - narrowed: Final = [choice for choice in response.choices if choice.index == index] + narrowed: Final = [choice for choice in response.choices if choice.index == index] # mutable-ok: List field return response.model_copy(update=MappingProxyType({"choices": narrowed})) def build_stream_error_items( @@ -1115,8 +1115,8 @@ class OpenAIChatCompletionsHandler(BaseTranslation): return await self._apply_guardrail_responses_to_output_streaming( responses=responses_so_far, - guardrailed_texts=list(rewrites_by_choice.values()), - task_mappings=[(index, None) for index in rewrites_by_choice], + guardrailed_texts=list(rewrites_by_choice.values()), # mutable-ok: callee takes lists + task_mappings=[(index, None) for index in rewrites_by_choice], # mutable-ok: callee takes lists ) @staticmethod diff --git a/litellm/llms/openai/openai.py b/litellm/llms/openai/openai.py index e3792fe9dfa..d6340d182ae 100644 --- a/litellm/llms/openai/openai.py +++ b/litellm/llms/openai/openai.py @@ -345,7 +345,9 @@ _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 = {k: v for k, v in data.items() if k not in _SDK_OPTION_KEYS} + 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 + } 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"}), @@ -1417,8 +1419,8 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj.pre_call( input=prompt, api_key=openai_aclient.api_key, - additional_args={ - "headers": {"Authorization": f"Bearer {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 "api_base": str(openai_aclient.base_url), "acompletion": True, "complete_input_dict": data, @@ -1601,7 +1603,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ + additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(sync_client.base_url), }, @@ -1649,7 +1651,7 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM): logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ + additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict "complete_input_dict": speech_request_body(model, voice, optional_params), "api_base": str(openai_client.base_url), }, diff --git a/litellm/llms/openai/organization_costs.py b/litellm/llms/openai/organization_costs.py index 8e7f02cca96..856072ddb99 100644 --- a/litellm/llms/openai/organization_costs.py +++ b/litellm/llms/openai/organization_costs.py @@ -21,7 +21,7 @@ from litellm.types.llms.custom_http import httpxSpecialProvider OPENAI_ADMIN_KEY_ENV_VAR: Final = "OPENAI_ADMIN_KEY" BillingHttpGet: TypeAlias = Callable[ - [str, Mapping[str, object], Mapping[str, str]], + [str, Mapping[str, object], Mapping[str, str]], # mutable-ok: Callable parameter list is type syntax Awaitable[httpx.Response], ] @@ -63,8 +63,8 @@ async def provider_billing_get(url: str, params: Mapping[str, object], headers: client: Final = get_async_httpx_client(llm_provider=httpxSpecialProvider.ProviderBilling) return await client.get( url, - params=dict(params), - headers=dict(headers), + params=dict(params), # mutable-ok: AsyncHTTPHandler.get takes dict params + headers=dict(headers), # mutable-ok: AsyncHTTPHandler.get takes dict headers timeout=PROVIDER_BILLING_TIMEOUT_SECONDS, ) diff --git a/litellm/llms/openai/responses/guardrail_translation/handler.py b/litellm/llms/openai/responses/guardrail_translation/handler.py index 90cdef87ec7..620d0554bb1 100644 --- a/litellm/llms/openai/responses/guardrail_translation/handler.py +++ b/litellm/llms/openai/responses/guardrail_translation/handler.py @@ -263,10 +263,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} + return {**item, field: rewritten_content} # mutable-ok: request input items must stay JSON-plain dicts 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] + [rewritten_row] # mutable-ok: converter signature takes a list ) if len(converted_items) != 1 or not isinstance(converted_items[0], Mapping): return None @@ -274,7 +274,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} + return {**item, field: converted_value} # mutable-ok: request input items must stay JSON-plain dicts def _is_tool_call_item(item: object) -> bool: @@ -549,7 +549,7 @@ class OpenAIResponsesHandler(BaseTranslation): guardrailed_inputs, ) if written_back is not None: - data["input"] = list(written_back.input) + data["input"] = list(written_back.input) # mutable-ok: JSON body if written_back.instructions is None: data.pop("instructions", None) else: @@ -681,7 +681,7 @@ class OpenAIResponsesHandler(BaseTranslation): ) -> None: if guardrailed_tools is None: return - data["tools"] = list( # rebind-ok: in-place request rewrite + data["tools"] = list( # mutable-ok: downstream wants a 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 f295a864b71..ff67c6220e1 100644 --- a/litellm/llms/openai/responses/guardrail_translation/tool_merge.py +++ b/litellm/llms/openai/responses/guardrail_translation/tool_merge.py @@ -93,7 +93,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} + return {**member, **changed_extras, **changed_function} # mutable-ok: json.dumps rejects MappingProxyType def _rebuilt_flattened_members( @@ -137,7 +137,7 @@ def _rebuilt_namespace( ) if not rebuilt_members: return () - return ({**original, "tools": list(rebuilt_members)},) + return ({**original, "tools": list(rebuilt_members)},) # mutable-ok: json.dumps needs a plain dict and list def _merged_original( diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index ebf506b3256..6c1d8698652 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -335,7 +335,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): is left alone because the API accepts both.""" if tools is None: return None - decoded: Final = [ + decoded: Final = [ # mutable-ok: request tools are a JSON list 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 @@ -348,7 +348,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} + return {**tool, "parameters": decoded} # mutable-ok: request tools are JSON dicts raise litellm.BadRequestError( message=( f"Invalid type for 'tools[{index}].parameters': expected an object, " @@ -405,7 +405,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"} + return {key: value for key, value in item.items() if key != "id"} # mutable-ok: outgoing JSON request item def _sanitized_tool_schemas_for_openai( self, @@ -474,14 +474,14 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): ) if not parameters_update and not tools_update: return entry - return {**entry, **parameters_update, **tools_update} + return {**entry, **parameters_update, **tools_update} # mutable-ok: request tools are JSON dicts @staticmethod def _sanitized_tools( tools: Sequence[object], sanitize: Callable[[Mapping[str, object]], Mapping[str, object]], ) -> Sequence[object]: - sanitized: Final = [ + sanitized: Final = [ # mutable-ok: request tools are a JSON list OpenAIResponsesAPIConfig._sanitized_tool_entry(item, sanitize) if isinstance(item, dict) else item for item in tools ] diff --git a/litellm/llms/openai/videos/guardrail_translation/__init__.py b/litellm/llms/openai/videos/guardrail_translation/__init__.py index fabc88832ec..7bd869612d6 100644 --- a/litellm/llms/openai/videos/guardrail_translation/__init__.py +++ b/litellm/llms/openai/videos/guardrail_translation/__init__.py @@ -7,7 +7,7 @@ from litellm.llms.openai.videos.guardrail_translation.handler import ( ) from litellm.types.utils import CallTypes -guardrail_translation_mappings: Final = { +guardrail_translation_mappings: Final = { # mutable-ok: discover_guardrail_translation_mappings only accepts isinstance(mappings, dict) CallTypes.video_generation: OpenAIVideoGenerationHandler, CallTypes.avideo_generation: OpenAIVideoGenerationHandler, CallTypes.create_video: OpenAIVideoGenerationHandler, diff --git a/litellm/llms/openai/videos/guardrail_translation/handler.py b/litellm/llms/openai/videos/guardrail_translation/handler.py index 7bdcc59ee7e..49a8d05100c 100644 --- a/litellm/llms/openai/videos/guardrail_translation/handler.py +++ b/litellm/llms/openai/videos/guardrail_translation/handler.py @@ -21,7 +21,7 @@ class OpenAIVideoGenerationHandler(BaseTranslation): return data model: Final = data.get("model") - texts: Final = [prompt] + texts: Final = [prompt] # mutable-ok: GenericGuardrailAPIInputs.texts is declared list[str] inputs: Final = ( GenericGuardrailAPIInputs(texts=texts, model=model) if isinstance(model, str) @@ -35,7 +35,7 @@ class OpenAIVideoGenerationHandler(BaseTranslation): ) guardrailed_texts: Final = guardrailed_inputs.get("texts") guardrailed_prompt: Final = guardrailed_texts[0] if guardrailed_texts else prompt - return {**data, "prompt": guardrailed_prompt} + return {**data, "prompt": guardrailed_prompt} # mutable-ok: BaseTranslation contract returns a dict async def process_output_response( self, diff --git a/litellm/llms/openai_like/model_info.py b/litellm/llms/openai_like/model_info.py index 101be58d197..cfe01e513fc 100644 --- a/litellm/llms/openai_like/model_info.py +++ b/litellm/llms/openai_like/model_info.py @@ -76,7 +76,7 @@ async def get_openai_compatible_model_info( try: response: Final = await client.get( url=url, - headers=dict(headers), + headers=dict(headers), # mutable-ok: AsyncHTTPHandler requires a concrete dict timeout=httpx.Timeout(5.0), follow_redirects=False, max_response_bytes=2 * 1024 * 1024, diff --git a/litellm/llms/sail/chat/transformation.py b/litellm/llms/sail/chat/transformation.py index 64f69af06fa..f50ed6de962 100644 --- a/litellm/llms/sail/chat/transformation.py +++ b/litellm/llms/sail/chat/transformation.py @@ -22,7 +22,7 @@ class SailChatConfig(OpenAIGPTConfig): param for param in super().get_supported_openai_params(model) if param not in _REJECTED_BY_SAIL ) added: Final = tuple(param for param in _ACCEPTED_BY_SAIL if param not in inherited) - return [*inherited, *added] + return [*inherited, *added] # mutable-ok: the base interface returns a list def map_openai_params( self, diff --git a/litellm/llms/sail/common_utils.py b/litellm/llms/sail/common_utils.py index cb00ed14214..a5e9f5e34a1 100644 --- a/litellm/llms/sail/common_utils.py +++ b/litellm/llms/sail/common_utils.py @@ -45,7 +45,7 @@ def _entry(key: str, value: object) -> Mapping[str, object]: def json_body(mapping: Mapping[str, object]) -> dict[str, object]: # mutable-ok: HTTP bodies are plain dicts - return {key: _json_value(value) for key, value in mapping.items()} + return {key: _json_value(value) for key, value in mapping.items()} # mutable-ok: HTTP bodies are plain dicts def _json_value(value: object) -> object: diff --git a/litellm/llms/snowflake/chat/transformation.py b/litellm/llms/snowflake/chat/transformation.py index cc51e1162e3..734d0e20818 100644 --- a/litellm/llms/snowflake/chat/transformation.py +++ b/litellm/llms/snowflake/chat/transformation.py @@ -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} + return {**converted, "cache_control": cache_control} # mutable-ok: JSON wire block 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 [ + return [ # mutable-ok: JSON wire blocks _convert_image_url_to_anthropic(block) if isinstance(block, Mapping) and block.get("type") == "image_url" else block @@ -172,7 +172,7 @@ def _convert_tool_result_to_anthropic( ) if cache_control is None: return converted - return {**converted, "cache_control": cache_control} + return {**converted, "cache_control": cache_control} # mutable-ok: JSON wire block def _signed_thinking_blocks(msg: object) -> list[dict[str, object]]: # mutable-ok: JSON wire blocks @@ -183,8 +183,8 @@ 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 [] - return [ + return [] # mutable-ok: JSON wire blocks + return [ # mutable-ok: JSON wire blocks dict(block) for block in blocks if isinstance(block, Mapping) and (block.get("signature") or block.get("type") == "redacted_thinking") @@ -289,7 +289,7 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): anthropic_tools.append(anthropic_tool) else: anthropic_tools.append( - {**tool, "input_schema": _clean_input_schema(tool["input_schema"])} + {**tool, "input_schema": _clean_input_schema(tool["input_schema"])} # mutable-ok: JSON wire tool if "input_schema" in tool else tool ) @@ -318,10 +318,10 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): if role == "system": if isinstance(content, str) and content: - system_parts.append({"type": "text", "text": content}) + system_parts.append({"type": "text", "text": content}) # mutable-ok: JSON wire system block 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 {}), @@ -383,10 +383,12 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): ): conversation[-1]["content"].append(tool_result_block) else: - conversation.append({"role": "user", "content": [tool_result_block]}) + conversation.append( + {"role": "user", "content": [tool_result_block]} # mutable-ok: JSON wire message + ) else: conversation.append( - { + { # mutable-ok: JSON wire message "role": role, "content": _convert_image_url_blocks_to_anthropic(content), } @@ -499,7 +501,7 @@ 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 "model": model_name, "messages": conversation, "stream": stream, @@ -508,7 +510,9 @@ class SnowflakeConfig(SnowflakeBaseConfig, OpenAIGPTConfig): } ) if system is not None: - body["system"] = normalize_cache_control_in_anthropic_payload({"system": system})["system"] + body["system"] = normalize_cache_control_in_anthropic_payload( + {"system": system} # mutable-ok: JSON wire payload + )["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 d5ed7da3815..460394c6f2d 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), + headers=dict(error.response.headers), # mutable-ok: existing error wrapper requires dict headers ) def _wrap_error( diff --git a/litellm/llms/together_ai/chat/transformation.py b/litellm/llms/together_ai/chat/transformation.py index 948bd3c8e14..449cd3ecbc5 100644 --- a/litellm/llms/together_ai/chat/transformation.py +++ b/litellm/llms/together_ai/chat/transformation.py @@ -178,7 +178,9 @@ 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", - {key: value for key, value in message.items() if key not in LITELLM_INTERNAL_ASSISTANT_FIELDS}, + { # mutable-ok: TypedDict rebuild minus internal keys + key: value for key, value in message.items() if key not in LITELLM_INTERNAL_ASSISTANT_FIELDS + }, ) @@ -208,7 +210,9 @@ 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 = [_without_litellm_internal_fields(message) for message in messages] + stripped: Final = [ # mutable-ok: super() requires a list + _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) @@ -217,7 +221,7 @@ class TogetherAIChatConfig(OpenAIGPTConfig): supported_params: Final = super().get_supported_openai_params(model) if not _supports_together_reasoning(model): return supported_params - return [ + return [ # mutable-ok: the inherited contract returns a plain list; building fresh avoids mutating the base class's value *supported_params, "reasoning_effort", ] diff --git a/litellm/llms/valkey/vector_stores/transformation.py b/litellm/llms/valkey/vector_stores/transformation.py index 50485899818..b250f71cf3f 100644 --- a/litellm/llms/valkey/vector_stores/transformation.py +++ b/litellm/llms/valkey/vector_stores/transformation.py @@ -185,7 +185,9 @@ class ValkeyVectorStoreConfig(BaseDirectVectorStoreConfig): @staticmethod def _to_result(doc: "Document", text_field: str) -> VectorStoreSearchResult: - content: Final = [VectorStoreResultContent(text=str(getattr(doc, text_field, "")), type="text")] + content: Final = [ # mutable-ok: VectorStoreSearchResult declares a list of content parts + VectorStoreResultContent(text=str(getattr(doc, text_field, "")), type="text") + ] return VectorStoreSearchResult( score=1.0 - float(getattr(doc, DISTANCE_FIELD_NAME)), content=content, @@ -233,11 +235,11 @@ class ValkeyVectorStoreConfig(BaseDirectVectorStoreConfig): if embedding_executor is not None else self.embedding_fn( model=params.require_embedding_model(), - input=[query_text], + input=[query_text], # mutable-ok: the injected embedding callable requires list input **(params.litellm_embedding_config or _EMPTY_EMBEDDING_CONFIG), ) ) - vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} + vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} # mutable-ok: redis-py API if self.sync_client is not None: raw: Final = self.sync_client.ft(vector_store_id).search(knn, query_params=vec_params) @@ -281,11 +283,11 @@ class ValkeyVectorStoreConfig(BaseDirectVectorStoreConfig): if embedding_executor is not None else await self.aembedding_fn( model=params.require_embedding_model(), - input=[query_text], + input=[query_text], # mutable-ok: the injected embedding callable requires list input **(params.litellm_embedding_config or _EMPTY_EMBEDDING_CONFIG), ) ) - vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} + vec_params: Final = {"vec": pack_vector(embedding_response.data[0]["embedding"])} # mutable-ok: redis-py API 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/realtime_transformation.py b/litellm/llms/vertex_ai/audio_transcription/realtime_transformation.py index 1d4a974fc54..ac23901accb 100644 --- a/litellm/llms/vertex_ai/audio_transcription/realtime_transformation.py +++ b/litellm/llms/vertex_ai/audio_transcription/realtime_transformation.py @@ -406,7 +406,7 @@ class VertexChirpRealtimeConfig(BaseRealtimeConfig): realtime_response_transform_input: RealtimeResponseTransformInput, ) -> RealtimeResponseTypedDict: frame: Final = _STREAMING_EVENT_ADAPTER.validate_json(message) - events: Final = list(self._transformer.transform(frame)) + events: Final = list(self._transformer.transform(frame)) # mutable-ok: response field is a list result: Final[RealtimeResponseTypedDict] = { "response": events, "current_output_item_id": realtime_response_transform_input.get("current_output_item_id"), diff --git a/litellm/llms/vertex_ai/common_utils.py b/litellm/llms/vertex_ai/common_utils.py index 5b5e1403c58..6d050d5a856 100644 --- a/litellm/llms/vertex_ai/common_utils.py +++ b/litellm/llms/vertex_ai/common_utils.py @@ -1293,7 +1293,11 @@ class VertexAITokenCounter(BaseTokenCounter): ) resolved_contents: Final = ( - contents if contents is not None else _gemini_convert_messages_with_history(messages=messages or []) + 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 + ) ) count_tokens_params: Final = { diff --git a/litellm/llms/vertex_ai/interactions/transformation.py b/litellm/llms/vertex_ai/interactions/transformation.py index 36965fe666f..0764a8bea62 100644 --- a/litellm/llms/vertex_ai/interactions/transformation.py +++ b/litellm/llms/vertex_ai/interactions/transformation.py @@ -91,7 +91,7 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): litellm_params: GenericLiteLLMParams | None, ) -> dict: # mutable-ok: BaseInteractionsAPIConfig declares plain-dict headers access_token, _ = self._mint(litellm_params or GenericLiteLLMParams()) - return { + return { # mutable-ok: BaseInteractionsAPIConfig declares plain-dict headers "Content-Type": "application/json", "Authorization": f"Bearer {access_token}", **headers, @@ -119,7 +119,7 @@ class VertexAIInteractionsConfig(VertexBase, GoogleAIStudioInteractionsConfig): url_suffix: str = "", ) -> tuple[str, dict]: # mutable-ok: BaseInteractionsAPIConfig declares a plain-dict request body target: Final = self._target(api_base or None, litellm_params) - return f"{target.interaction_url(interaction_id)}{url_suffix}", {} + return f"{target.interaction_url(interaction_id)}{url_suffix}", {} # mutable-ok: same base contract def transform_get_interaction_request( self, diff --git a/litellm/llms/vertex_ai/text_to_speech/transformation.py b/litellm/llms/vertex_ai/text_to_speech/transformation.py index a7b079fb89c..6c2c59d98e1 100644 --- a/litellm/llms/vertex_ai/text_to_speech/transformation.py +++ b/litellm/llms/vertex_ai/text_to_speech/transformation.py @@ -499,7 +499,9 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): def get_supported_openai_params( self, model: str ) -> list: # mutable-ok: inherited provider interface returns a concrete parameter list - return ["response_format"] + return [ # mutable-ok: inherited provider interface requires a concrete parameter list + "response_format" + ] def map_openai_params( self, @@ -509,7 +511,9 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): 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(optional_params) + mapped_params: Final = dict( # mutable-ok: mapping drops unsupported parameters before provider dispatch + optional_params + ) base_model: Final = model.removeprefix("vertex_ai/") model_info: Final = self._get_model_info(model=model) unsupported_params: Final = tuple( @@ -576,7 +580,7 @@ class VertexAILyriaTextToSpeechConfig(VertexAITextToSpeechConfig): return VertexAIInteractionsConfig(mint_access_token=mint_access_token).get_complete_url( api_base=api_base, model=base_model, - litellm_params={ + litellm_params={ # mutable-ok: interactions dispatch expects a concrete parameter dictionary **litellm_params, "vertex_project": project, "vertex_location": "global", @@ -607,7 +611,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", @@ -616,23 +620,27 @@ 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 - { - "instances": [{"prompt": input}], - "parameters": {"sample_count": 1}, + { # 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 + }, } if model_info["vertex_ai_audio_api"] == "lyria_predict" - else { + else { # mutable-ok: interactions dispatch requires a concrete provider request dictionary "model": base_model, "input": input, **( - { - "response_format": { + { # mutable-ok: interactions dispatch requires a nested response-format dictionary + "response_format": { # mutable-ok: interactions response format is a concrete provider payload "type": "audio", "mime_type": "audio/wav", } } if optional_params.get("response_format") == "wav" - else {} + else {} # mutable-ok: no response override is merged for non-WAV output ), } ) diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/llama3/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/llama3/transformation.py index e72780fd943..ca0bcb74906 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/llama3/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/llama3/transformation.py @@ -76,7 +76,9 @@ class VertexAILlama3Config(OpenAIGPTConfig): if is_vertex_self_deployed_openai_compatible_endpoint(model) else frozenset({"max_retries"}) ) - return [param for param in super().get_supported_openai_params(model=model) if param not in unsupported_params] + return [ # mutable-ok: get_optional_params extends the returned list with allowed_openai_params + param for param in super().get_supported_openai_params(model=model) if param not in unsupported_params + ] def map_openai_params( self, diff --git a/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py b/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py index abd47173608..33922e38674 100644 --- a/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py +++ b/litellm/llms/vertex_ai/vertex_gemma_models/transformation.py @@ -51,7 +51,7 @@ class VertexGemmaConfig(OpenAIGPTConfig): super().__init__() def get_supported_openai_params(self, model: str) -> list[str]: - return [ + return [ # mutable-ok: get_optional_params extends the returned list with allowed_openai_params param for param in super().get_supported_openai_params(model=model) if param not in VERTEX_SELF_DEPLOYED_ENDPOINT_UNSUPPORTED_PARAMS diff --git a/litellm/llms/wandb/chat/transformation.py b/litellm/llms/wandb/chat/transformation.py index f891898f443..fdd6644f03d 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"] + return supported_params + ["reasoning_effort"] # mutable-ok: inherited contract return supported_params def map_openai_params( diff --git a/litellm/llms/xai/audio_transcription/transformation.py b/litellm/llms/xai/audio_transcription/transformation.py index 5d810712634..49447413a37 100644 --- a/litellm/llms/xai/audio_transcription/transformation.py +++ b/litellm/llms/xai/audio_transcription/transformation.py @@ -109,7 +109,9 @@ class XAIAudioTranscriptionConfig(BaseAudioTranscriptionConfig): } excluded_params: Final = frozenset({"model", "OPENAI_TRANSCRIPTION_PARAMS", "extra_body"}) - form_data: Final[dict[str, str | list[str]]] = { + form_data: Final[ + dict[str, str | list[str]] + ] = { # mutable-ok: AudioTranscriptionRequestData.data requires dict and httpx needs list values "model": model, **{ k: _serialize_form_value(v) diff --git a/litellm/llms/xai/batches/handler.py b/litellm/llms/xai/batches/handler.py index 3e45452d94c..62db1c4833a 100644 --- a/litellm/llms/xai/batches/handler.py +++ b/litellm/llms/xai/batches/handler.py @@ -39,8 +39,8 @@ class _PageParams(TypedDict): def _results_params(after: str | None, limit: int | None) -> dict[str, object]: # mutable-ok: httpx params if after is None: - return dict(_PageParams(limit=limit or XAI_RESULTS_PAGE_SIZE)) - return dict(_PageParams(limit=limit or XAI_RESULTS_PAGE_SIZE, pagination_token=after)) + return dict(_PageParams(limit=limit or XAI_RESULTS_PAGE_SIZE)) # mutable-ok: httpx params + return dict(_PageParams(limit=limit or XAI_RESULTS_PAGE_SIZE, pagination_token=after)) # mutable-ok: httpx params def _flatten(pages: list[XAIBatchResultsPage]) -> tuple[XAIBatchResult, ...]: @@ -69,7 +69,7 @@ class XAIBatchesHandler: def _async(self, timeout: float | httpx.Timeout) -> AsyncHTTPHandler: return self._async_client or get_async_httpx_client( llm_provider=LlmProviders.XAI, - params={"timeout": timeout}, + params={"timeout": timeout}, # mutable-ok: get_async_httpx_client takes a dict ) def create_batch( @@ -82,7 +82,7 @@ class XAIBatchesHandler: ) -> LiteLLMBatch | Coroutine[None, None, LiteLLMBatch]: url: Final = xai_batches_url(api_base) headers: Final = get_xai_auth_headers(api_key=api_key) - body: Final = dict(to_create_batch_body(create_batch_data)) + body: Final = dict(to_create_batch_body(create_batch_data)) # mutable-ok: httpx json body endpoint: Final = create_batch_data.get("endpoint") or "/v1/chat/completions" if _is_async: @@ -177,7 +177,7 @@ class XAIBatchesHandler: ) return XAIBatchResultsPage.model_validate(raise_for_xai_status(response).json()) - pages = [await _page(None)] + pages = [await _page(None)] # mutable-ok: page walk terminates on the cursor, not on a fixed count while pages[-1].pagination_token and pages[-1].results: pages.append(await _page(pages[-1].pagination_token)) return _jsonl_response(url, _flatten(pages)) @@ -189,7 +189,7 @@ class XAIBatchesHandler: response: Final = client.get(url, params=_results_params(after, None), headers=headers, timeout=timeout) return XAIBatchResultsPage.model_validate(raise_for_xai_status(response).json()) - pages = [_page(None)] + pages = [_page(None)] # mutable-ok: page walk terminates on the cursor, not on a fixed count while pages[-1].pagination_token and pages[-1].results: pages.append(_page(pages[-1].pagination_token)) return _jsonl_response(url, _flatten(pages)) diff --git a/litellm/llms/xai/batches/transformation.py b/litellm/llms/xai/batches/transformation.py index 2d986ab99df..8f305b8c203 100644 --- a/litellm/llms/xai/batches/transformation.py +++ b/litellm/llms/xai/batches/transformation.py @@ -70,7 +70,7 @@ def get_xai_auth_headers( raise xai_batches_error( "Missing xAI API Key. Pass api_key, set litellm.xai_key or XAI_API_KEY", 401, _EMPTY_HEADERS ) - return dict(headers, Authorization=f"Bearer {resolved_key}") + return dict(headers, Authorization=f"Bearer {resolved_key}") # mutable-ok: BaseConfig contract returns dict def xai_batches_url(api_base: str | None, batch_id: str | None = None, suffix: str = "") -> str: @@ -176,7 +176,7 @@ def to_litellm_batch(batch: XAIBatch, endpoint: str = DEFAULT_BATCH_ENDPOINT) -> created_at: Final = _to_unix_timestamp(batch.create_time) cancelled_at: Final = _to_unix_timestamp(batch.cancel_time) errors: Final = ( - BatchErrors(object="list", data=[BatchError(message=batch.cancel_by_xai_message)]) + BatchErrors(object="list", data=[BatchError(message=batch.cancel_by_xai_message)]) # mutable-ok: openai type if batch.cancel_by_xai_message else None ) @@ -198,7 +198,7 @@ def to_litellm_batch(batch: XAIBatch, endpoint: str = DEFAULT_BATCH_ENDPOINT) -> completed=batch.state.num_success, failed=batch.state.num_error + batch.state.num_cancelled, ), - metadata={"name": batch.name} if batch.name else None, + metadata={"name": batch.name} if batch.name else None, # mutable-ok: LiteLLMBatch.metadata is a dict ) diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index 49c8ee2ac55..e686d49e689 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -227,7 +227,9 @@ class XAIChatConfig(OpenAIGPTConfig): "Dropping 'web_search_options'. Use the Responses API for XAI web search." ) - chat_params: Final = {key: value for key, value in optional_params.items() if key != "web_search_options"} + chat_params: Final = { # mutable-ok: base transform_request takes a plain dict of optional params + key: value for key, value in optional_params.items() if key != "web_search_options" + } return super().transform_request( model, strip_name_from_messages(messages), chat_params, litellm_params, headers ) diff --git a/litellm/llms/xai/files/transformation.py b/litellm/llms/xai/files/transformation.py index 94fa681d319..dbccca47b25 100644 --- a/litellm/llms/xai/files/transformation.py +++ b/litellm/llms/xai/files/transformation.py @@ -126,7 +126,7 @@ class XAIFilesConfig(BaseFilesConfig): def get_supported_openai_params( self, model: str ) -> list[OpenAICreateFileRequestOptionalParams]: # mutable-ok: BaseFilesConfig signature - return ["purpose"] + return ["purpose"] # mutable-ok: BaseFilesConfig signature def map_openai_params( self, @@ -153,7 +153,7 @@ class XAIFilesConfig(BaseFilesConfig): file=(filename, extracted["content"], content_type), purpose=(None, create_file_data.get("purpose") or _DEFAULT_PURPOSE), ) - return dict(upload) + return dict(upload) # mutable-ok: BaseFilesConfig signature def transform_create_file_response( self, @@ -222,7 +222,7 @@ class XAIFilesConfig(BaseFilesConfig): logging_obj: LiteLLMLoggingObj, litellm_params: Mapping[str, object], ) -> list[OpenAIFileObject]: # mutable-ok: BaseFilesConfig signature - return [ + return [ # mutable-ok: BaseFilesConfig signature _to_openai_file_object(f) for f in XAIFileList.model_validate(raise_for_xai_status(raw_response).json()).data ] diff --git a/litellm/main.py b/litellm/main.py index 6f72b6ff1ab..8c9d7f2513d 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -7828,11 +7828,11 @@ async def amoderation( }, custom_llm_provider=custom_llm_provider, ) - moderation_request: Final = {"input": input, "model": model} + moderation_request: Final = {"input": input, "model": model} # mutable-ok: logged as the raw request body litellm_logging_obj.pre_call( input=input, api_key=api_key, - additional_args={ + additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict "complete_input_dict": moderation_request, "api_base": str(_openai_client.base_url), }, @@ -8918,8 +8918,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) - return [list(streamed_citations)] + return list(streamed_citations) # mutable-ok: JSON list field + return [list(streamed_citations)] # mutable-ok: JSON list field def _stream_builder_model_map_cost(response: ModelResponse) -> float | None: @@ -9199,9 +9199,11 @@ 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)} if streamed_citations else {} + {"citations": _joined_streamed_citations(streamed_citations)} # mutable-ok: JSON dict field + if streamed_citations + else {} # mutable-ok: JSON dict field ) - combined_provider_fields: Final = { + combined_provider_fields: Final = { # mutable-ok: Message.provider_specific_fields is a plain dict field key: value for fields in (citation_fields, *provider_field_dicts) for key, value in fields.items() 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 2c63d957e75..457c9b1680b 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 @@ -835,9 +835,13 @@ class MCPRequestHandler: raise HTTPException(status_code=500, detail="Server misconfigured: MCP server has no routable name") admitted: Final = await MCPRequestHandler._reload_admitted_principal(result.identity) await MCPRequestHandler._enforce_admitted_live_policy(admitted=admitted, request=request, route=route) - injected: Final = {header_key: {"Authorization": result.upstream_authorization.get_secret_value()}} - new_headers: Final = { - **(mcp_server_auth_headers or {}), + injected: Final = { # mutable-ok: mcp_server_auth_headers contract requires concrete dicts + header_key: { # mutable-ok: concrete dict header payload + "Authorization": result.upstream_authorization.get_secret_value() + } + } + new_headers: Final = { # mutable-ok: merged header map must stay a concrete dict + **(mcp_server_auth_headers or {}), # mutable-ok: empty-dict fallback for the merge **injected, } return admitted, new_headers @@ -913,14 +917,20 @@ class MCPRequestHandler: ): raise HTTPException( status_code=403, - detail={"error": "oauth_principal_mismatch"}, + detail={ # mutable-ok: HTTPException detail payload requires a concrete dict + "error": "oauth_principal_mismatch" + }, ) header_key: Final = server.alias or server.server_name if header_key is None: raise HTTPException(status_code=500, detail="Server misconfigured: MCP server has no routable name") - injected: Final = {header_key: {"Authorization": result.upstream_authorization.get_secret_value()}} - new_headers: Final = { - **(mcp_server_auth_headers or {}), + injected: Final = { # mutable-ok: mcp_server_auth_headers contract requires concrete dicts + header_key: { # mutable-ok: concrete dict header payload + "Authorization": result.upstream_authorization.get_secret_value() + } + } + new_headers: Final = { # mutable-ok: merged header map must stay a concrete dict + **(mcp_server_auth_headers or {}), # mutable-ok: empty-dict fallback for the merge **injected, } return explicit_auth, new_headers diff --git a/litellm/proxy/_experimental/mcp_server/contracts.py b/litellm/proxy/_experimental/mcp_server/contracts.py index b55034e6dc9..a0a08dc08ce 100644 --- a/litellm/proxy/_experimental/mcp_server/contracts.py +++ b/litellm/proxy/_experimental/mcp_server/contracts.py @@ -14,7 +14,7 @@ def copy_caller(auth: UserAPIKeyAuth | None) -> UserAPIKeyAuth | None: if auth is None: return None span: Final = auth.parent_otel_span - return deepcopy(auth, {id(span): span} if span is not None else None) + return deepcopy(auth, {id(span): span} if span is not None else None) # mutable-ok: deepcopy mutates its memo @dataclass(frozen=True, slots=True) @@ -69,12 +69,12 @@ class OperationContext: return ( self.user_api_key_auth, self.mcp_auth_header, - list(self.mcp_servers) if self.mcp_servers is not None else None, + list(self.mcp_servers) if self.mcp_servers is not None else None, # mutable-ok: legacy policy list input {key: dict(value) for key, value in self.mcp_server_auth_headers.items()} if self.mcp_server_auth_headers is not None else None, dict(self.oauth2_headers) if self.oauth2_headers is not None else None, - dict(self.raw_headers) if self.raw_headers is not None else None, + dict(self.raw_headers) if self.raw_headers is not None else None, # mutable-ok: legacy request header input self.client_ip, ) diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index dee3f15d6bd..80005c954bc 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -514,11 +514,15 @@ def _db_transaction_manager(prisma_client: PrismaClient) -> _UserEnvVarsTransact def _identifier_where(value: str, exclude_server_id: str | None) -> "prisma_db_types.LiteLLM_MCPServerTableWhereInput": - own_row_guard: Final = ({"NOT": [{"server_id": exclude_server_id}]},) if exclude_server_id is not None else () + own_row_guard: Final = ( + ({"NOT": [{"server_id": exclude_server_id}]},) # mutable-ok: prisma where-inputs must be plain dicts + if exclude_server_id is not None + else () + ) where: Final[prisma_db_types.LiteLLM_MCPServerTableWhereInput] = { - "AND": [ + "AND": [ # mutable-ok: prisma where-inputs must be plain dicts { - "OR": [ + "OR": [ # mutable-ok: prisma where-inputs must be plain dicts {"server_name": {"equals": value, "mode": "insensitive"}}, {"alias": {"equals": value, "mode": "insensitive"}}, ] @@ -1114,7 +1118,7 @@ async def _update_mcp_server_row( table: "TableActions[prisma_db_models.LiteLLM_MCPServerTable]", ) -> "prisma_db_models.LiteLLM_MCPServerTable | None": return await table.update( - where={"server_id": server_id}, + where={"server_id": server_id}, # mutable-ok: prisma where-inputs must be plain dicts data=data_dict, ) @@ -1711,7 +1715,7 @@ async def list_server_user_credentials( """Every user's stored credential for one server, typed but without the secret, for admins.""" rows: Final = await _db_find_user_credential_rows( prisma_client, - {"server_id": server_id}, + {"server_id": server_id}, # mutable-ok: prisma where-inputs must be plain dicts ) return tuple(_server_user_credential_item(row) for row in rows) diff --git a/litellm/proxy/_experimental/mcp_server/elicitation_handler.py b/litellm/proxy/_experimental/mcp_server/elicitation_handler.py index 57d2d86d506..6155f1f215c 100644 --- a/litellm/proxy/_experimental/mcp_server/elicitation_handler.py +++ b/litellm/proxy/_experimental/mcp_server/elicitation_handler.py @@ -160,7 +160,7 @@ async def _relay_elicitation_to_downstream( verbose_logger.info("MCP elicitation: relaying generic elicitation to downstream") result = await downstream_session.elicit( message=getattr(params, "message", ""), - requested_schema=getattr(params, "requested_schema", {}), + requested_schema=getattr(params, "requested_schema", {}), # mutable-ok: elicitation default schema ) verbose_logger.info( "MCP elicitation: downstream responded with action=%s", diff --git a/litellm/proxy/_experimental/mcp_server/guardrail_translation/handler.py b/litellm/proxy/_experimental/mcp_server/guardrail_translation/handler.py index 74f85d5fa04..d8453d6ab07 100644 --- a/litellm/proxy/_experimental/mcp_server/guardrail_translation/handler.py +++ b/litellm/proxy/_experimental/mcp_server/guardrail_translation/handler.py @@ -157,7 +157,9 @@ class MCPGuardrailTranslationHandler(BaseTranslation): mcp_tool: Final = MCPTool( name=mcp_tool_name, description=mcp_tool_description or "", - input_schema=dict(mcp_input_schema) if isinstance(mcp_input_schema, Mapping) else {}, + input_schema=dict(mcp_input_schema) + if isinstance(mcp_input_schema, Mapping) + else {}, # mutable-ok: SDK dict field ) openai_tool: Final = transform_mcp_tool_to_openai_tool(mcp_tool) fn: Final = openai_tool["function"] diff --git a/litellm/proxy/_experimental/mcp_server/mcp_debug.py b/litellm/proxy/_experimental/mcp_server/mcp_debug.py index 7b50a478297..70da73fa045 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_debug.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_debug.py @@ -203,7 +203,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()), @@ -432,7 +432,9 @@ 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 {key: REDACTED if _sensitive_field(key) else value for key, value in fields.items()} + 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() + } 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 fe3948a80c5..ec2db433911 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1719,7 +1719,9 @@ 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]]] = {} + self._pending: dict[ + _DiscoveryKey, asyncio.Task[list[_DiscoveryItem]] + ] = {} # mutable-ok: constant-time fetch registration self._waiters: dict[asyncio.Task[list[_DiscoveryItem]], int] = {} # mutable-ok: constant-time waiter accounting def invalidate(self, server_id: str) -> None: @@ -4937,7 +4939,7 @@ class MCPServerManager: try: client: Final = get_async_httpx_client( llm_provider=httpxSpecialProvider.MCP, - params={"timeout": MCP_METADATA_TIMEOUT}, + params={"timeout": MCP_METADATA_TIMEOUT}, # mutable-ok: HTTP client factory requires a dict ) response: Final = await client.get(server_url) response.raise_for_status() @@ -7303,7 +7305,11 @@ class MCPServerManager: spec_path=server.spec_path, transport=server.transport, auth_type=server.auth_type, - credentials=({"scopes": list(server.configured_scopes)} if server.configured_scopes else None), + credentials=( + {"scopes": list(server.configured_scopes)} # mutable-ok: MCPCredentials requires a JSON-array list + if server.configured_scopes + else None + ), created_at=server.created_at, updated_at=server.updated_at, teams=[], diff --git a/litellm/proxy/_experimental/mcp_server/oauth_identity_binding.py b/litellm/proxy/_experimental/mcp_server/oauth_identity_binding.py index c31560a9f63..f02e6c85d9b 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], + [MCPOAuthIdentityBinding], # mutable-ok: Callable parameter syntax requires a list Awaitable[Sequence[Mapping[str, object]]], ] CallerPrincipalLoader: TypeAlias = Callable[ - [str, MCPOAuthIdentityBinding], + [str, MCPOAuthIdentityBinding], # mutable-ok: Callable parameter syntax requires a list Awaitable[str | None], ] @@ -57,7 +57,7 @@ class VerifiedRefreshToken: StoredRefreshTokenLoader: TypeAlias = Callable[ - [str, str, MCPOAuthIdentityBinding], + [str, str, MCPOAuthIdentityBinding], # mutable-ok: Callable parameter syntax requires a list 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)) + return jwt.PyJWK(dict(key)) # mutable-ok: PyJWT requires a concrete JWK dictionary 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/operations.py b/litellm/proxy/_experimental/mcp_server/operations.py index f98a8c0ea5a..8bb2772c760 100644 --- a/litellm/proxy/_experimental/mcp_server/operations.py +++ b/litellm/proxy/_experimental/mcp_server/operations.py @@ -295,7 +295,9 @@ async def _dispatch_virtual_mcp_tool( if mcp_proxy_mode and name not in MCP_PROXY_TOOL_NAMES: return CallToolResult( - content=[TextContent(type="text", text=f"Tool {name} is unavailable on /mcp/proxy")], + content=[ # mutable-ok: MCP result content + TextContent(type="text", text=f"Tool {name} is unavailable on /mcp/proxy") + ], is_error=True, ) @@ -305,7 +307,7 @@ async def _dispatch_virtual_mcp_tool( proxy_logging_obj: Final = ( await _build_virtual_call_logging_obj( name=name, - arguments=arguments or {}, + arguments=arguments or {}, # mutable-ok: logging pipeline payload user_api_key_auth=user_api_key_auth, raw_headers=raw_headers, client_ip=client_ip, @@ -316,7 +318,7 @@ async def _dispatch_virtual_mcp_tool( try: proxy_result: Final = await handle_mcp_proxy_tool( name=name, - arguments=arguments or {}, + arguments=arguments or {}, # mutable-ok: proxy handler payload user_api_key_dict=user_api_key_auth, client_ip=client_ip, mcp_servers=mcp_servers, @@ -337,7 +339,7 @@ async def _dispatch_virtual_mcp_tool( await proxy_logging_obj.async_failure_handler(exc, failure_traceback, proxy_call_start, failure_end) if not isinstance(exc, MCPUpstreamAuthError): await request_logging_obj.post_call_failure_hook( - request_data={ + request_data={ # mutable-ok: failure hook mutates its request payload "name": name, "arguments": arguments, "litellm_logging_obj": proxy_logging_obj, @@ -1139,7 +1141,9 @@ async def _get_tools_from_mcp_servers( if mcp_proxy_mode: from litellm.proxy._experimental.mcp_server.tool_search import with_mcp_proxy_identity - filtered_tools = [with_mcp_proxy_identity(tool, server.server_id) for tool in filtered_tools] + filtered_tools = [ # mutable-ok: MCP tool pipeline + with_mcp_proxy_identity(tool, server.server_id) for tool in filtered_tools + ] else: filtered_tools = apply_display_name_overrides(filtered_tools, server) @@ -2640,7 +2644,7 @@ async def _handle_local_mcp_tool( except Exception as e: verbose_logger.exception("Error executing local tool %s: %s", name, e) return CallToolResult( - content=[TextContent(text=f"Error: {e}", type="text")], + content=[TextContent(text=f"Error: {e}", type="text")], # mutable-ok: MCP result content is_error=True, ) return complete_call_tool_result(handler_outcome(result), wire_compat) @@ -2729,7 +2733,7 @@ async def _execute_handle_list_tools( verbose_logger.exception("Error in list_tools endpoint: %s", e) # Return empty list instead of failing completely # This prevents the HTTP stream from failing and allows the client to get a response - return ListToolsResult(tools=[]) + return ListToolsResult(tools=[]) # mutable-ok: MCP result payload async def _execute_mcp_server_tool_call( @@ -2777,7 +2781,7 @@ async def _execute_mcp_server_tool_call( return virtual_tool_result # Create a body date for logging - body_data: Final = {"name": params.name, "arguments": params.arguments} + body_data: Final = {"name": params.name, "arguments": params.arguments} # mutable-ok: logging payload # Set trace/session id from raw_headers so spend logs and logging_obj stay consistent (same as A2A) chain_id: Final = get_chain_id_from_headers(raw_headers) if chain_id: @@ -2918,7 +2922,7 @@ async def _execute_list_prompts( verbose_logger.exception("Error in list_prompts endpoint: %s", e) # Return empty list instead of failing completely # This prevents the HTTP stream from failing and allows the client to get a response - return ListPromptsResult(prompts=[]) + return ListPromptsResult(prompts=[]) # mutable-ok: MCP result payload async def _execute_get_prompt( @@ -2985,7 +2989,7 @@ async def _execute_list_resources( return ListResourcesResult(resources=resources) except Exception as e: verbose_logger.exception("Error in list_resources endpoint: %s", e) - return ListResourcesResult(resources=[]) + return ListResourcesResult(resources=[]) # mutable-ok: MCP result payload async def _execute_list_resource_templates( @@ -3025,7 +3029,7 @@ async def _execute_list_resource_templates( return ListResourceTemplatesResult(resource_templates=resource_templates) except Exception as e: verbose_logger.exception("Error in list_resource_templates endpoint: %s", e) - return ListResourceTemplatesResult(resource_templates=[]) + return ListResourceTemplatesResult(resource_templates=[]) # mutable-ok: MCP result payload async def _execute_read_resource( @@ -3202,7 +3206,7 @@ class GatewayOperations: auth, token, _servers, server_headers, oauth_headers, headers, _client_ip = context.legacy_auth() return await _execute_mcp_tool( name=operation.name, - arguments=dict(operation.arguments), + arguments=dict(operation.arguments), # mutable-ok: existing tool hooks own mutable argument data allowed_mcp_servers=list(operation.allowed_mcp_servers), start_time=operation.start_time, user_api_key_auth=auth, 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 bca5848febe..7600fd7ab8a 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 = { + form: Final = { # mutable-ok: the RFC 6749 form body is a wire format the HTTP client takes as a mapping "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 44523f5cf45..12cdab59e0f 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -1756,7 +1756,7 @@ if MCP_AVAILABLE: "MCP tools/list preview timed out after %s seconds while paginating upstream tools", listing_deadline, ) - return { + return { # mutable-ok: error response payload "status": "error", "error": True, "message": f"Timed out listing tools after {listing_deadline} seconds. " diff --git a/litellm/proxy/_experimental/mcp_server/result_conversion.py b/litellm/proxy/_experimental/mcp_server/result_conversion.py index 29a33746b1e..52931fae116 100644 --- a/litellm/proxy/_experimental/mcp_server/result_conversion.py +++ b/litellm/proxy/_experimental/mcp_server/result_conversion.py @@ -59,7 +59,7 @@ INPUT_REQUIRED_UNSUPPORTED_MESSAGE: Final = ( def error_text_result(exc: Exception) -> CallToolResult: return CallToolResult( - content=[TextContent(type="text", text=f"{type(exc).__name__}: {exc}")], + content=[TextContent(type="text", text=f"{type(exc).__name__}: {exc}")], # mutable-ok: SDK list field is_error=True, ) @@ -68,13 +68,13 @@ def to_call_tool_result(outcome: ToolOutcome, compat: WireCompat) -> CallToolRes match outcome: case TextResult(): return CallToolResult( - content=[TextContent(type="text", text=outcome.text)], + content=[TextContent(type="text", text=outcome.text)], # mutable-ok: SDK list field is_error=False, ) case JsonResult(): keep_structured: Final = compat is WireCompat.MODERN or isinstance(outcome.value, dict) return CallToolResult( - content=[TextContent(type="text", text=outcome.original_text)], + content=[TextContent(type="text", text=outcome.original_text)], # mutable-ok: SDK list field is_error=False, structured_content=outcome.value if keep_structured else None, ) @@ -84,7 +84,7 @@ def to_call_tool_result(outcome: ToolOutcome, compat: WireCompat) -> CallToolRes if compat is WireCompat.MODERN: return outcome return CallToolResult( - content=[TextContent(type="text", text=INPUT_REQUIRED_UNSUPPORTED_MESSAGE)], + content=[TextContent(type="text", text=INPUT_REQUIRED_UNSUPPORTED_MESSAGE)], # mutable-ok: SDK is_error=True, ) case Exception(): @@ -97,7 +97,7 @@ def complete_call_tool_result(outcome: ToolOutcome, compat: WireCompat) -> CallT converted: Final = to_call_tool_result(outcome, compat) if isinstance(converted, InputRequiredResult): return CallToolResult( - content=[TextContent(type="text", text=INPUT_REQUIRED_UNSUPPORTED_MESSAGE)], + content=[TextContent(type="text", text=INPUT_REQUIRED_UNSUPPORTED_MESSAGE)], # mutable-ok: SDK is_error=True, ) return converted @@ -110,7 +110,7 @@ def _downgrade_structured_content(result: CallToolResult) -> CallToolResult: fallback: Final = TextContent(type="text", text=json.dumps(structured)) update: Final[_Downgraded] = { "structured_content": None, - "content": [*result.content, fallback], + "content": [*result.content, fallback], # mutable-ok: SDK list field } return result.model_copy(update=update) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 44979d12a00..2412e83b9d9 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -565,8 +565,10 @@ if MCP_AVAILABLE: ) opts: Final = ( base_options.model_copy( - update={ - "capabilities": base_options.capabilities.model_copy(update={"prompts": None, "resources": None}) + update={ # mutable-ok: Pydantic update payload + "capabilities": base_options.capabilities.model_copy( + update={"prompts": None, "resources": None} # mutable-ok: Pydantic update payload + ) } ) if _mcp_proxy_mode.get() @@ -1495,7 +1497,7 @@ if MCP_AVAILABLE: if _is_admin_terminated_session_id(_session_id, time.monotonic()): terminated_response: Final = JSONResponse( status_code=404, - content={ + content={ # mutable-ok: JSONResponse content must be a plain dict "error": "Not Found", "details": "mcp-session-id was terminated by an administrator. Send initialize to start a new session.", }, @@ -1967,7 +1969,7 @@ if MCP_AVAILABLE: supported: Final = ", ".join(configured_versions()) await JSONResponse( status_code=400, - content={ + content={ # mutable-ok: JSON-RPC error payload "jsonrpc": "2.0", "id": None, "error": { @@ -2312,7 +2314,7 @@ if MCP_AVAILABLE: supported: Final = ", ".join(configured_versions()) await JSONResponse( status_code=400, - content={ + content={ # mutable-ok: JSON-RPC error payload "jsonrpc": "2.0", "id": None, "error": { diff --git a/litellm/proxy/_experimental/mcp_server/tool_search.py b/litellm/proxy/_experimental/mcp_server/tool_search.py index 63d7127ce98..9d117a1a1fa 100644 --- a/litellm/proxy/_experimental/mcp_server/tool_search.py +++ b/litellm/proxy/_experimental/mcp_server/tool_search.py @@ -121,7 +121,11 @@ _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(update={"meta": {**(tool.meta or {}), _MCP_PROXY_IDENTITY_META_KEY: identity}}) + return tool.model_copy( + update={ # mutable-ok: Pydantic update payload + "meta": {**(tool.meta or {}), _MCP_PROXY_IDENTITY_META_KEY: identity} # mutable-ok: metadata mapping + } + ) def _mcp_proxy_identity(tool: Tool) -> MCPProxyToolIdentity: @@ -147,7 +151,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 + return {**base, "score": hit.score} if hit.score is not None else base # mutable-ok: wire result payload def _proxy_schema_result(tool: Tool) -> MCPProxySchemaResult: @@ -159,7 +163,7 @@ def _proxy_schema_result(tool: Tool) -> MCPProxySchemaResult: } if tool.output_schema is None: return base - return {**base, "outputSchema": tool.output_schema} + return {**base, "outputSchema": tool.output_schema} # mutable-ok: wire schema payload def _tool_text(tool: Tool) -> str: @@ -259,7 +263,7 @@ class VirtualToolDefinition(TypedDict): def _json_array(*items: str) -> Sequence[str]: - return list(items) + return list(items) # mutable-ok: jsonschema's metaschema only accepts a JSON array for required _MCP_TOOL_SEARCH_DEFINITION: Final[VirtualToolDefinition] = { @@ -378,7 +382,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)], + content=[TextContent(type="text", text=text)], # mutable-ok: CallToolResult accepts only list content is_error=is_error, ) @@ -531,7 +535,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} + tools_by_id: Final = {mcp_proxy_tool_id(tool): tool for tool in listing.tools} # mutable-ok: lookup index if name == MCP_PROXY_SEARCH_TOOL_NAME: llm_router: Final = proxy_server.llm_router @@ -568,7 +572,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", {}) + tool_arguments: Final = arguments.get("arguments", {}) # mutable-ok: JSON Schema validator consumes mapping 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 f3a78d206d7..dcbd0064514 100644 --- a/litellm/proxy/_experimental/mcp_server/toolset_db.py +++ b/litellm/proxy/_experimental/mcp_server/toolset_db.py @@ -140,7 +140,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( + data_dict: Final = dict( # mutable-ok: Prisma requires a plain dict for JSON query serialization ( (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 17937f9cb54..d2fad212dd9 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -4059,7 +4059,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase): pointfive: CallbackOnUI = CallbackOnUI( litellm_callback_name="pointfive", ui_callback_name="PointFive", - litellm_callback_params=[ + litellm_callback_params=[ # mutable-ok: the registry field is typed list "POINTFIVE_API_KEY", "POINTFIVE_API_URL", ], @@ -4074,7 +4074,7 @@ class AllCallbacks(LiteLLMPydanticObjectBase): zerobus: CallbackOnUI = CallbackOnUI( litellm_callback_name="zerobus", ui_callback_name="Databricks Zerobus", - litellm_callback_params=[ + litellm_callback_params=[ # mutable-ok: the registry field is typed list "ZEROBUS_WORKSPACE_URL", "ZEROBUS_SERVER_ENDPOINT", "ZEROBUS_CLIENT_ID", diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py index c88c6f2570a..6ddcd20d919 100644 --- a/litellm/proxy/agent_endpoints/a2a_endpoints.py +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -762,7 +762,9 @@ async def invoke_agent_a2a( body["metadata"] = {} body["metadata"]["agent_id"] = agent.agent_id body["metadata"]["model_group"] = f"a2a_agent/{agent_name}" - body["metadata"]["model_info"] = {"id": agent.agent_id} + body["metadata"]["model_info"] = { # mutable-ok: request hooks mutate metadata before JSON logging + "id": agent.agent_id + } body["agent_id"] = agent.agent_id body.update( diff --git a/litellm/proxy/agent_endpoints/auth/agent_access_groups.py b/litellm/proxy/agent_endpoints/auth/agent_access_groups.py index db661fbea30..67547e82f24 100644 --- a/litellm/proxy/agent_endpoints/auth/agent_access_groups.py +++ b/litellm/proxy/agent_endpoints/auth/agent_access_groups.py @@ -12,9 +12,9 @@ if TYPE_CHECKING: from litellm.types.agents import AgentResponse AccessGroupIds: TypeAlias = tuple[str, ...] -AccessGroupIdsLoader: TypeAlias = Callable[[str], Awaitable[AccessGroupIds]] +AccessGroupIdsLoader: TypeAlias = Callable[[str], Awaitable[AccessGroupIds]] # mutable-ok: Callable params LoadedAccessGroup: TypeAlias = LiteLLM_AccessGroupTable | None -AccessGroupLoader: TypeAlias = Callable[[str], Awaitable[LoadedAccessGroup]] +AccessGroupLoader: TypeAlias = Callable[[str], Awaitable[LoadedAccessGroup]] # mutable-ok: Callable parameter syntax @dataclass(frozen=True, slots=True) @@ -27,7 +27,7 @@ class AgentAccessGroupCeiling: agent_ids: frozenset[str] -CeilingResolver: TypeAlias = Callable[[str], Awaitable[AgentAccessGroupCeiling | None]] +CeilingResolver: TypeAlias = Callable[[str], Awaitable[AgentAccessGroupCeiling | None]] # mutable-ok: Callable params async def _registry_access_group_ids(agent_id: str) -> AccessGroupIds: diff --git a/litellm/proxy/agent_endpoints/auth/managed_authorization.py b/litellm/proxy/agent_endpoints/auth/managed_authorization.py index 5b3930b3299..17d988127ec 100644 --- a/litellm/proxy/agent_endpoints/auth/managed_authorization.py +++ b/litellm/proxy/agent_endpoints/auth/managed_authorization.py @@ -108,9 +108,9 @@ def managed_inference_request( raise_identity_failure( AgentIdentityFailure(message="Managed inference requires an explicit or configured model") ) - return {**body, "model": model} + return {**body, "model": model} # mutable-ok: centralized auth hooks add request tags and budget metadata if route not in _MANAGED_MODEL_ROUTES and not RouteChecks.check_route_access(route, _MANAGED_MODEL_PATHS): - return dict(body) + return dict(body) # mutable-ok: centralized auth hooks add request tags and budget metadata from litellm.proxy.common_utils.http_parsing_utils import resolve_inference_model kind: Final = next((kind for suffix, kind in _MODEL_ROUTE_KINDS.items() if route.endswith(suffix)), "completion") @@ -122,7 +122,7 @@ def managed_inference_request( raise_identity_failure( AgentIdentityFailure(message="Managed inference requires an explicit or configured model") ) - return {**body, "model": effective} + return {**body, "model": effective} # mutable-ok: centralized auth hooks add request tags and budget metadata def managed_agent_policy(auth: "UserAPIKeyAuth | None") -> AgentResponse | None: diff --git a/litellm/proxy/agent_endpoints/endpoints.py b/litellm/proxy/agent_endpoints/endpoints.py index 875b62103b9..e1b2ac63d51 100644 --- a/litellm/proxy/agent_endpoints/endpoints.py +++ b/litellm/proxy/agent_endpoints/endpoints.py @@ -171,7 +171,9 @@ def _redact_agent_litellm_params_dict( """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(parse_agent_litellm_params(redact_sensitive_agent_litellm_params(litellm_params))) + 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)) + ) def _redact_sensitive_agent_fields( @@ -985,7 +987,7 @@ async def delete_agent( @router.post( "/v1/agents/{agent_id}/kill_switch", - tags=["[beta] A2A Agents"], + tags=["[beta] A2A Agents"], # mutable-ok: fastapi types tags as list[str | Enum] dependencies=(Depends(user_api_key_auth),), response_model=AgentKillSwitchResult, ) diff --git a/litellm/proxy/agent_endpoints/kill_switch.py b/litellm/proxy/agent_endpoints/kill_switch.py index 120c1bf9259..8b3f64e74ee 100644 --- a/litellm/proxy/agent_endpoints/kill_switch.py +++ b/litellm/proxy/agent_endpoints/kill_switch.py @@ -154,7 +154,7 @@ def default_kill_switch_http_client() -> KillSwitchHttpClient: return get_async_httpx_client(llm_provider=httpxSpecialProvider.AgentKillSwitch).client -KillSwitchAuditLogWriter: TypeAlias = Callable[[LiteLLM_AuditLogs], Awaitable[None]] +KillSwitchAuditLogWriter: TypeAlias = Callable[[LiteLLM_AuditLogs], Awaitable[None]] # mutable-ok: Callable params def default_kill_switch_audit_log_writer() -> KillSwitchAuditLogWriter: 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 78af282941d..7c6a4571948 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} + where={"name": plugin_name} # mutable-ok: prisma query arguments must be plain dicts ) 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}, - data={ + where={"name": plugin_name}, # mutable-ok: prisma query arguments must be plain dicts + data={ # mutable-ok: prisma query arguments must be plain dicts "version": request.version, "description": request.description, "manifest_json": json.dumps(manifest), diff --git a/litellm/proxy/anthropic_endpoints/gateway_endpoints.py b/litellm/proxy/anthropic_endpoints/gateway_endpoints.py index 6fec411313d..0446992ae43 100644 --- a/litellm/proxy/anthropic_endpoints/gateway_endpoints.py +++ b/litellm/proxy/anthropic_endpoints/gateway_endpoints.py @@ -47,7 +47,7 @@ _DEVICE_POLL_INTERVAL_SECONDS: Final = 5 _SECONDS_PER_HOUR: Final = 3600 _MANAGED_SETTINGS_ADAPTER: Final = TypeAdapter(dict[str, object]) _NO_SETTINGS: Final = MappingProxyType({}) -_POST_ONLY: Final = ["POST"] +_POST_ONLY: Final = ["POST"] # mutable-ok: FastAPI's add_api_route only accepts a list of methods class _GatewaySessionData(BaseModel): @@ -136,7 +136,7 @@ def _oauth_error_response(err: _OAuthError) -> JSONResponse: router: Final = APIRouter( prefix=GATEWAY_PREFIX, - tags=["Claude Code gateway"], + tags=["Claude Code gateway"], # mutable-ok: FastAPI's APIRouter only accepts a list of tags ) _GATEWAY_ENABLED: Final = (Depends(ensure_gateway_enabled),) _AUTHENTICATED: Final = (Depends(user_api_key_auth),) @@ -203,7 +203,7 @@ async def device_authorization(request: Request) -> JSONResponse: login_id: Final = f"cli-{secrets.token_urlsafe(24)}" poll_secret: Final = secrets.token_urlsafe(32) user_code: Final = _generate_cli_sso_user_code() - flow: Final = { + flow: Final = { # mutable-ok: the shared CLI SSO cache entry is a dict the browser leg mutates "poll_secret_hash": _hash_cli_sso_secret(poll_secret), "user_code_hash": _hash_cli_sso_secret(_normalize_cli_sso_user_code(user_code)), "sso_complete": False, diff --git a/litellm/proxy/anthropic_endpoints/skills_endpoints.py b/litellm/proxy/anthropic_endpoints/skills_endpoints.py index ab513b1acc4..4426c0b547a 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 = [ + skills: Final = [ # mutable-ok: ListSkillsResponse.data requires list[Skill]; never mutated after 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 f748a754fe0..7da5e5099fc 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}} + return {**event, "message": {**message, "model": requested_model}} # mutable-ok: SSE payload, re-serialized as is 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 4b7b8290a30..3ec430332ee 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -1482,7 +1482,7 @@ async def get_default_end_user_budget( # Fetch from database try: budget_record: Final = await _dictable_table(BudgetRepository(prisma_client), "budget").find_unique( - where={"budget_id": default_budget_id} + where={"budget_id": default_budget_id} # mutable-ok: prisma where clause ) if budget_record is None: @@ -1690,12 +1690,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}} + return {column: {"not": None}} # mutable-ok: prisma's query builder isinstance-checks for dict 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)]} + return {"OR": [{"blocked": True}, *map(_column_is_set, _RESTRICTED_COLUMNS)]} # mutable-ok: prisma needs dict/list class _RegistryNotCached: @@ -2601,7 +2601,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} + email_update: Final = {"user_email": db_row.user_email} # mutable-ok: model_copy update payload is dict-shaped updated_row: Final = user_row.model_copy(update=email_update) await user_api_key_cache.async_set_cache( key=user_row.user_id, @@ -2958,7 +2958,7 @@ async def invalidate_team_member_spend_state( ) raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail={ + detail={ # mutable-ok: HTTPException.detail takes a dict "error": "Spend was reset in the database, but Redis is unreachable and still " "holds the pre-reset counter. Retry once Redis is reachable." }, @@ -4526,7 +4526,9 @@ def _resolve_team_alias( return model if isinstance(model, str): return _live_team_alias_target(model, team_model_aliases, team_id, llm_router) - return [_live_team_alias_target(name, team_model_aliases, team_id, llm_router) for name in model] + return [ # mutable-ok: _can_object_call_model takes list[str] + _live_team_alias_target(name, team_model_aliases, team_id, llm_router) for name in model + ] def _live_team_alias_target( @@ -4587,8 +4589,8 @@ async def _check_agent_access_group_model_access( LoadedCallerTeam: TypeAlias = LiteLLM_TeamTable | None LoadedCallerUser: TypeAlias = LiteLLM_UserTable | None -CallerTeamLoader: TypeAlias = Callable[[UserAPIKeyAuth], Awaitable[LoadedCallerTeam]] -CallerUserLoader: TypeAlias = Callable[[UserAPIKeyAuth], Awaitable[LoadedCallerUser]] +CallerTeamLoader: TypeAlias = Callable[[UserAPIKeyAuth], Awaitable[LoadedCallerTeam]] # mutable-ok: Callable params +CallerUserLoader: TypeAlias = Callable[[UserAPIKeyAuth], Awaitable[LoadedCallerUser]] # mutable-ok: Callable params async def _check_agent_caller_model_access( @@ -4949,7 +4951,7 @@ async def stamp_matched_model_access_groups( return () if not matched: return () - matched_groups: Final = list(matched) + matched_groups: Final = list(matched) # mutable-ok: the auth field is typed list[str] | None 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 a59a12d6807..618f0c647ac 100644 --- a/litellm/proxy/auth/auth_exception_handler.py +++ b/litellm/proxy/auth/auth_exception_handler.py @@ -101,7 +101,7 @@ def _with_client_context( } if not stamped: return request_data - return {**request_data, key: {**base, **stamped}} + return {**request_data, key: {**base, **stamped}} # mutable-ok: logging needs dicts def _escape_control_chars(value: str) -> str: diff --git a/litellm/proxy/auth/auth_object_prefetch.py b/litellm/proxy/auth/auth_object_prefetch.py index f8b0a3838f0..14d3e2c07dc 100644 --- a/litellm/proxy/auth/auth_object_prefetch.py +++ b/litellm/proxy/auth/auth_object_prefetch.py @@ -250,7 +250,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} + stamped: Final = {**columns, "last_refreshed_at": refreshed_at} # mutable-ok: validators write into it 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 813d72ed7ae..c0123ae45a3 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -1259,7 +1259,7 @@ def enforce_batch_enqueued_token_limit_is_admin_only( return raise HTTPException( status_code=403, - detail={ + detail={ # mutable-ok: HTTPException.detail has no immutable form "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/login_throttle.py b/litellm/proxy/auth/login_throttle.py index f2398219a7b..b7f7eaceff4 100644 --- a/litellm/proxy/auth/login_throttle.py +++ b/litellm/proxy/auth/login_throttle.py @@ -416,7 +416,7 @@ class LoginThrottle: type=ProxyErrorTypes.auth_error, param="username", code=status.HTTP_429_TOO_MANY_REQUESTS, - headers={"Retry-After": str(retry_after)}, + headers={"Retry-After": str(retry_after)}, # mutable-ok: ProxyException writes into its headers dict ) diff --git a/litellm/proxy/auth/password_policy.py b/litellm/proxy/auth/password_policy.py index 36a569bf18f..a883cfd6f35 100644 --- a/litellm/proxy/auth/password_policy.py +++ b/litellm/proxy/auth/password_policy.py @@ -110,7 +110,7 @@ def validate_password_policy(password: str, general_settings: Mapping[str, objec def get_hibp_client() -> AsyncHTTPHandler: return get_async_httpx_client( llm_provider=httpxSpecialProvider.PasswordBreachCheck, - params={"timeout": HIBP_TIMEOUT_SECONDS}, + params={"timeout": HIBP_TIMEOUT_SECONDS}, # mutable-ok: callee takes a bare dict (PEP 589) ) @@ -125,7 +125,7 @@ def _is_suffix_in_range_response(response_body: str, hash_suffix: str) -> bool: async def _is_password_breached(password: str, client: AsyncHTTPHandler) -> bool: # usedforsecurity=False: SHA-1 is only a lookup key into the HIBP dataset, so no security property rests on it sha1_hex: Final = hashlib.sha1(password.encode("utf-8"), usedforsecurity=False).hexdigest().upper() - headers: Final = { + headers: Final = { # mutable-ok: callee takes a bare dict (PEP 589) "Add-Padding": "true", "User-Agent": f"litellm-proxy/{version}", } diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 2ed8d8f323d..5194f62cf78 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -659,7 +659,7 @@ async def user_api_key_auth_websocket_for_model(websocket: WebSocket, model: str "query_string": ws_scope.get("query_string", b""), "headers": scope_headers, "path": ws_scope.get("path", ""), - "state": ws_scope.setdefault("state", {}), + "state": ws_scope.setdefault("state", {}), # mutable-ok: Starlette's socket state, shared with the request } for key in ("root_path", "app_root_path"): if key in ws_scope: @@ -1372,12 +1372,12 @@ async def _read_request_body_deferring_parse_failure( route=get_request_route(request=request), content_type=_safe_get_request_headers(request=request).get("content-type", ""), ): - _safe_set_request_parsed_body(request=request, parsed_body={}) - return {}, None + _safe_set_request_parsed_body(request=request, parsed_body={}) # mutable-ok: the body cache stores a plain dict + return {}, None # mutable-ok: request_data is a plain dict across the whole auth path try: parsed_body: Final = await _read_request_body(request=request) except ProxyException as parse_exception: - return {}, parse_exception + return {}, parse_exception # mutable-ok: request_data is a plain dict across the whole auth path return populate_request_with_path_params(request_data=parsed_body, request=request), None @@ -1396,7 +1396,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={}, + request_data={}, # mutable-ok: the failure hook seeds the call id and metadata onto this dict original_exception=body_parse_exception, user_api_key_dict=user_api_key_dict, error_type=ProxyErrorTypes.bad_request_error, @@ -1685,7 +1685,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 {} + mapped_claims = jwt_claims or {} # mutable-ok: empty-dict fallback for the None-claims case 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/litellm_executed_batches.py b/litellm/proxy/batches_endpoints/litellm_executed_batches.py index 7de170880ca..caf34404a7d 100644 --- a/litellm/proxy/batches_endpoints/litellm_executed_batches.py +++ b/litellm/proxy/batches_endpoints/litellm_executed_batches.py @@ -217,7 +217,11 @@ async def upstream_lacks_files_api(api_base: str, api_key: str | None, http_clie try: response: Final = await client.get( f"{api_base.rstrip('/')}/files", - headers=({"Authorization": f"Bearer {api_key}"} if api_key else None), + headers=( + {"Authorization": f"Bearer {api_key}"} # mutable-ok: AsyncHTTPHandler.get wants a plain dict + if api_key + else None + ), timeout=_FILES_API_PROBE_TIMEOUT_SECONDS, ) except httpx.HTTPError: @@ -512,7 +516,7 @@ class LiteLLMExecutedBatchRunner: async def fail_abandoned(self, batch: LiteLLMBatch, user_api_key_dict: UserAPIKeyAuth) -> LiteLLMBatch: error: Final = BatchError(message=_RUNNER_LOST_MESSAGE, code="runner_lost") - errors: Final = Errors(data=[error], object="list") + errors: Final = Errors(data=[error], object="list") # mutable-ok: Errors.data is typed as a list failed: Final = batch.model_copy( update=MappingProxyType({"status": "failed", "failed_at": int(time.time()), "errors": errors}) ) @@ -531,8 +535,8 @@ class LiteLLMExecutedBatchRunner: def reject(body: Mapping[str, object]) -> str | None: try: is_request_body_safe( - request_body=dict(body), - general_settings=dict(self.general_settings), + request_body=dict(body), # mutable-ok: is_request_body_safe takes a dict + general_settings=dict(self.general_settings), # mutable-ok: is_request_body_safe takes a dict llm_router=self.llm_router, model=model, ) @@ -565,7 +569,7 @@ class LiteLLMExecutedBatchRunner: except Exception as e: # noqa: BLE001 # whatever fails, the batch must end up marked failed verbose_proxy_logger.exception("LiteLLM-executed batch %s failed: %s", run.unified_batch_id, e) error: Final = BatchError(message=str(e), code="internal_error") - errors: Final = Errors(data=[error], object="list") + errors: Final = Errors(data=[error], object="list") # mutable-ok: Errors.data is typed as a list try: await self._advance(run, "failed", MappingProxyType({"errors": errors})) except Exception as advance_error: # noqa: BLE001 # a failed status write is logged, never raised @@ -650,11 +654,11 @@ class LiteLLMExecutedBatchRunner: return method def _row_metadata(self, run: _BatchRun) -> dict[str, object]: # mutable-ok: router updates metadata in place - return { + return { # mutable-ok: the router updates request metadata in place **LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(run.user_api_key_dict), "user_api_key": LiteLLMProxyRequestSetup.get_logged_api_key(run.user_api_key_dict), "user_api_end_user_max_budget": run.user_api_key_dict.end_user_max_budget, - "tags": list(run.request_tags), + "tags": list(run.request_tags), # mutable-ok: litellm types request tags as a list "batch_id": run.unified_batch_id, } diff --git a/litellm/proxy/client/cli/commands/claude_settings.py b/litellm/proxy/client/cli/commands/claude_settings.py index 0c8b2ef9dcf..13ed483586e 100644 --- a/litellm/proxy/client/cli/commands/claude_settings.py +++ b/litellm/proxy/client/cli/commands/claude_settings.py @@ -370,8 +370,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))) - return dict(chain(settings.items(), ((STATUS_LINE_KEY, entry),))) + entry: Final = dict((("type", "command"), ("command", command))) # mutable-ok: JSON document + return dict(chain(settings.items(), ((STATUS_LINE_KEY, entry),))) # mutable-ok: JSON document def merge_claude_settings( @@ -394,7 +394,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( + env: Final = dict( # mutable-ok: JSON document handed to json.dump, which rejects a read-only mapping chain( ( (ENABLE_TOOL_SEARCH_KEY, ENABLE_TOOL_SEARCH_VALUE), @@ -406,7 +406,7 @@ def merge_claude_settings( ((key, tier_model) for key in ANTHROPIC_DEFAULT_MODEL_ENV_KEYS if tier_model is not None), ) ) - return dict( + return dict( # mutable-ok: JSON document handed to json.dump, which rejects a read-only mapping chain( ( (key, value) @@ -438,7 +438,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( + return dict( # mutable-ok: JSON document handed to json.dump, which rejects a read-only mapping chain(((k, v) for k, v in container.items() if k != key), ((key, owned.value),) if owned.present else ()) ) diff --git a/litellm/proxy/client/cli/commands/configure_profiles.py b/litellm/proxy/client/cli/commands/configure_profiles.py index 8d84dfc2146..87c4a05dd22 100644 --- a/litellm/proxy/client/cli/commands/configure_profiles.py +++ b/litellm/proxy/client/cli/commands/configure_profiles.py @@ -127,7 +127,7 @@ def save_setup(saved: SavedSetup) -> None: ensure_private_dir(path.parent) staged: Final = stage_private_json( str(path), - { + { # mutable-ok: private_json serializes with json.dump, which requires a dict "version": saved.version, "target": saved.target, "settings_path": saved.settings_path, diff --git a/litellm/proxy/client/cli/commands/configure_setup.py b/litellm/proxy/client/cli/commands/configure_setup.py index b988b7e95d2..bd07c19dff4 100644 --- a/litellm/proxy/client/cli/commands/configure_setup.py +++ b/litellm/proxy/client/cli/commands/configure_setup.py @@ -222,7 +222,9 @@ def _has_targets(chosen: Sequence[object]) -> bool: def pick_targets(defaults: tuple[Target, ...] = ("claude", "codex"), *, edit: bool = False) -> tuple[Target, ...]: - choices: Final = [Choice(value, name=label, enabled=value in defaults) for value, label in _TARGETS] + choices: Final = [ # mutable-ok: InquirerPy requires a list + Choice(value, name=label, enabled=value in defaults) for value, label in _TARGETS + ] picked: Final = _TARGET_SELECTION.validate_python( inquirer.checkbox( message="Which agents should be edited? Unselected agents keep their current setup" @@ -237,7 +239,7 @@ def pick_targets(defaults: tuple[Target, ...] = ("claude", "codex"), *, edit: bo def _pick_model(listed: Sequence[str], default: str | None = None) -> str | None: - choices: Final = [_KEEP_DEFAULT_MODEL, *listed] + choices: Final = [_KEEP_DEFAULT_MODEL, *listed] # mutable-ok: InquirerPy requires a list picked: Final = _MODEL_SELECTION.validate_python( inquirer.fuzzy( message="Model Claude Code starts on (type to filter; /model switches any time):", @@ -249,7 +251,7 @@ def _pick_model(listed: Sequence[str], default: str | None = None) -> str | None def _pick_codex_model(listed: Sequence[str], default: str | None = None) -> str: - choices: Final = list(listed) + choices: Final = list(listed) # mutable-ok: InquirerPy's choices parameter requires a list return _MODEL_SELECTION.validate_python( inquirer.fuzzy( message="Model Codex starts on (type to filter):", diff --git a/litellm/proxy/client/cli/commands/pi.py b/litellm/proxy/client/cli/commands/pi.py index 5966a11485d..f5834f94fb8 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}, + headers={"Authorization": f"Bearer {api_key}", **headers}, # mutable-ok: requests headers require a dict 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}"}, + headers={"Authorization": f"Bearer {api_key}"}, # mutable-ok: requests headers require a dict timeout=10, ) if resp.status_code != 200: @@ -171,12 +171,12 @@ def _model_entry( ) -> dict[str, JsonValue]: # mutable-ok: JSON object is serialized 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 {} + {"contextWindow": limit.context_window} if limit and limit.context_window else {} # mutable-ok: JSON field ) output: Final[dict[str, JsonValue]] = ( # mutable-ok: JSON field {"maxTokens": limit.max_tokens} if limit and limit.max_tokens else {} ) - return {"id": model_id, **context, **output} + return {"id": model_id, **context, **output} # mutable-ok: JSON serialization requires a mutable object def provider_block( @@ -189,11 +189,11 @@ def provider_block( Real contextWindow/maxTokens matter: pi otherwise assumes 128k/16384, which breaks compaction thresholds and over-asks models with smaller output caps. """ - return { + return { # mutable-ok: JSON serialization requires a mutable object "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], + "models": [_model_entry(model_id, limits) for model_id in model_ids], # mutable-ok: JSON array } @@ -211,12 +211,12 @@ def sync_models_json( 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", {}) + existing_providers: Final = current.get("providers", {}) # mutable-ok: JSON object default 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 = { + updated: Final = { # mutable-ok: JSON serialization requires a mutable object **current, - "providers": { + "providers": { # mutable-ok: JSON serialization requires a mutable object **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 f137165a6e5..d16160b1ab8 100644 --- a/litellm/proxy/client/cli/commands/statusline_script.py +++ b/litellm/proxy/client/cli/commands/statusline_script.py @@ -190,7 +190,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={ + headers={ # mutable-ok: urllib.request.Request takes a dict "Authorization": f"Bearer {credentials.api_key}", "Accept": "application/json", }, @@ -305,7 +305,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}) + body: Final = json.dumps({"fetched_at": fetched_at, "session": entry}) # mutable-ok: json.dumps takes a dict if not _own_private_dir(path.parent): return try: @@ -415,7 +415,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}"}) + return json.dumps({"systemMessage": f"\n{text}"}) # mutable-ok: json.dumps takes a dict 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 660b7a261b8..e7a08711eb1 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -1442,7 +1442,7 @@ def attach_guardrail_information(response: object, request_data: Mapping[str, ob ), (), ) - guardrail_information: Final = [ + guardrail_information: Final = [ # mutable-ok: response list contract redact_nested_match_and_regex_keys(entry, keys=_RESPONSE_REDACTED_KEYS) for entry in recorded if isinstance(entry, dict) @@ -1681,7 +1681,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 {} + return getattr(logging_obj, "response_timing_metrics", None) or {} # mutable-ok: empty fallback class ProxyBaseLLMRequestProcessing: @@ -1703,7 +1703,7 @@ class ProxyBaseLLMRequestProcessing: Proxy/custom headers win on key collisions. """ - excluded_headers: Final = { + excluded_headers: Final = { # mutable-ok: set of header names to exclude from forwarding "transfer-encoding", "content-encoding", "set-cookie", @@ -1716,7 +1716,7 @@ class ProxyBaseLLMRequestProcessing: "upgrade", } - merged_headers: Final = { + merged_headers: Final = { # mutable-ok: dict comprehension for merged headers forwarded to httpx key: value for key, value in dict(response_headers or {}).items() if key.lower() not in excluded_headers } merged_headers.update(custom_headers) @@ -3709,7 +3709,9 @@ class ProxyBaseLLMRequestProcessing: error_body: Final = await http_status_error.response.aread() error_text: Final = error_body.decode("utf-8") - error_headers: Final = {k: v if isinstance(v, str) else str(v) for k, v in safe_headers.items()} + 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() + } raise HTTPException( status_code=http_status_error.response.status_code, detail={"error": error_text}, diff --git a/litellm/proxy/common_utils/cache_aware_routing.py b/litellm/proxy/common_utils/cache_aware_routing.py index 436482a3421..4ae2dce2440 100644 --- a/litellm/proxy/common_utils/cache_aware_routing.py +++ b/litellm/proxy/common_utils/cache_aware_routing.py @@ -123,7 +123,7 @@ async def _available( await router.async_get_healthy_deployments( # pyright: ignore[reportUnknownMemberType] # legacy router results are validated at this boundary model=candidate.model, messages=_MESSAGES.validate_python(messages) if messages else None, # pyright: ignore[reportArgumentType] # router annotations predate structured native messages - request_kwargs=dict(request_kwargs), + request_kwargs=dict(request_kwargs), # mutable-ok: Router's filtering API accepts a request dictionary ) ) except Exception: # noqa: BLE001 # an unavailable optional candidate must not fail the originally selected route diff --git a/litellm/proxy/common_utils/config_includes.py b/litellm/proxy/common_utils/config_includes.py index a3402207e52..c1bb5ae952f 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] + return [*base_value, *included_value] # mutable-ok: a merged config value stays the plain list the proxy loads 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) + return dict(merged) # mutable-ok: the proxy mutates the config it loads diff --git a/litellm/proxy/common_utils/error_body_call_id.py b/litellm/proxy/common_utils/error_body_call_id.py index fb5456b877e..f50be5df509 100644 --- a/litellm/proxy/common_utils/error_body_call_id.py +++ b/litellm/proxy/common_utils/error_body_call_id.py @@ -17,4 +17,4 @@ def error_body_call_id(general_settings: Mapping[str, object], call_id: str | No def with_call_id(error: dict[str, object], call_id: str | None) -> dict[str, object]: # mutable-ok: JSONResponse input if call_id is None: return error - return {**error, LITELLM_CALL_ID_BODY_KEY: call_id} + return {**error, LITELLM_CALL_ID_BODY_KEY: call_id} # mutable-ok: JSONResponse input diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 6e5114b2f87..ac757f5f6a7 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -164,7 +164,7 @@ def _parse_binary_body(body: bytes) -> dict: return parsed except orjson.JSONDecodeError: pass - return {} + return {} # mutable-ok: auth parser returns a fresh dict per request async def _read_request_body(request: Request | None) -> dict: diff --git a/litellm/proxy/common_utils/openai_error_payload.py b/litellm/proxy/common_utils/openai_error_payload.py index f092f9637f8..202c61b620e 100644 --- a/litellm/proxy/common_utils/openai_error_payload.py +++ b/litellm/proxy/common_utils/openai_error_payload.py @@ -60,7 +60,7 @@ def openai_error_param(exc: object) -> str | None: def litellm_call_id_headers(litellm_call_id: str | None) -> dict[str, str] | None: # mutable-ok: ProxyException.headers if litellm_call_id is None: return None - return {LITELLM_CALL_ID_HEADER: litellm_call_id} + return {LITELLM_CALL_ID_HEADER: litellm_call_id} # mutable-ok: ProxyException mutates its headers dict def with_litellm_call_id(exc: ProxyException, litellm_call_id: str | None) -> ProxyException: diff --git a/litellm/proxy/common_utils/prompt_cache_pricing.py b/litellm/proxy/common_utils/prompt_cache_pricing.py index 95b945714e0..1ecc3ac44fe 100644 --- a/litellm/proxy/common_utils/prompt_cache_pricing.py +++ b/litellm/proxy/common_utils/prompt_cache_pricing.py @@ -74,7 +74,7 @@ def price_cache_tokens( ) logging_obj: Final = Logging( model=model, - messages=[], + messages=[], # mutable-ok: Logging requires a list stream=False, call_type="completion", start_time=datetime.now(timezone.utc), diff --git a/litellm/proxy/common_utils/reset_budget_job.py b/litellm/proxy/common_utils/reset_budget_job.py index c4f081e3dec..b35b876b475 100644 --- a/litellm/proxy/common_utils/reset_budget_job.py +++ b/litellm/proxy/common_utils/reset_budget_job.py @@ -225,7 +225,7 @@ def _enduser_invalidation_where(budget_ids: Sequence[str]) -> dict[str, object]: default_budget_id: Final = litellm.max_end_user_budget_id if default_budget_id is None or default_budget_id not in budget_ids: return linked - return {"OR": [linked, {"budget_id": None}]} + return {"OR": [linked, {"budget_id": None}]} # mutable-ok: prisma where filter must be a dict def _queue_budget_linked_resets( @@ -721,8 +721,8 @@ class ResetBudgetJob: return tuple( await self._with_db_retry( lambda: EndUserRepository(self.prisma_client).table.find_many( - where={**where, "user_id": {"gt": cursor}}, - order={"user_id": "asc"}, + where={**where, "user_id": {"gt": cursor}}, # mutable-ok: prisma where filter must be a dict + order={"user_id": "asc"}, # mutable-ok: prisma order filter must be a dict take=RESET_BUDGET_JOB_BATCH_SIZE, ), reason="reset_budget_read_endusers_failure", @@ -771,13 +771,13 @@ class ResetBudgetJob: log_subject="projects", ) 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 {} + else {} # mutable-ok: empty sentinel immediately frozen by MappingProxyType ) return _BudgetCascade( budgets=tuple(budgets_to_reset), diff --git a/litellm/proxy/common_utils/semantic_text_index.py b/litellm/proxy/common_utils/semantic_text_index.py index 030958706cf..d3fe68e65f7 100644 --- a/litellm/proxy/common_utils/semantic_text_index.py +++ b/litellm/proxy/common_utils/semantic_text_index.py @@ -66,7 +66,7 @@ def cosine_similarity(left: Vector, right: Vector) -> float: def embedding_spend_metadata(user_api_key_dict: UserAPIKeyAuth) -> dict[str, object]: # mutable-ok: router mutates it from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup - return { + return { # mutable-ok: the router mutates the metadata dict it is handed **LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(user_api_key_dict), "user_api_key": LiteLLMProxyRequestSetup.get_logged_api_key(user_api_key_dict), } @@ -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 = { + request: Final = { # mutable-ok: pre_call_hook mutates the request dict in place "model": embedding_model, - "input": list(texts), + "input": list(texts), # mutable-ok: Router.aembedding accepts only str | list input "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), + input=list(processed.input), # mutable-ok: Router.aembedding accepts only str | list 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 4306098b796..72553e82283 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -636,12 +636,14 @@ class DBSpendUpdateWriter: spend_logs: Final = SpendLogsRepository(prisma_client).table try: claimed: Final = await spend_logs.create_many( - data=[prisma_client.jsonify_object(row)], + data=[prisma_client.jsonify_object(row)], # mutable-ok: prisma create_many takes a list skip_duplicates=True, ) if claimed == 1: return True - existing: Final = await spend_logs.find_unique(where={"request_id": request_id}) + existing: Final = await spend_logs.find_unique( + where={"request_id": request_id} # mutable-ok: prisma where clause + ) 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 @@ -683,7 +685,7 @@ class DBSpendUpdateWriter: data=prisma_client.jsonify_object( MappingProxyType({field: value for field, value in row.items() if field != "request_id"}) ), - where={ + where={ # mutable-ok: prisma where clause "request_id": request_id, "call_type": CallTypes.aretrieve_batch.value, "status": "success", @@ -1569,7 +1571,7 @@ class DBSpendUpdateWriter: window_spend_update_transactions, ) = await self.redis_update_buffer.get_all_transactions_from_redis_buffer_pipeline() - uncommitted = { + uncommitted = { # mutable-ok: drives which popped categories still need re-queuing "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, @@ -1681,7 +1683,9 @@ class DBSpendUpdateWriter: exc=e, ) finally: - to_restore = {name: txns for name, txns in uncommitted.items() if txns is not None} + to_restore = { # mutable-ok: transient kwargs payload consumed immediately below + 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 e6e97cb1eb3..54021e68980 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] +RootCertResolver: TypeAlias = Callable[[str, str, int], str] # mutable-ok: Callable parameter syntax class _VerifiedChainSource(Protocol): diff --git a/litellm/proxy/db/gateway_request_tracking.py b/litellm/proxy/db/gateway_request_tracking.py index aae15f81b06..c9ace68db33 100644 --- a/litellm/proxy/db/gateway_request_tracking.py +++ b/litellm/proxy/db/gateway_request_tracking.py @@ -70,7 +70,7 @@ class GatewayRequestAccumulator: def drain(self) -> GatewayRequestSnapshot: drained: Final = self._counts - self._counts = {} + self._counts = {} # mutable-ok: the fold restarts empty; the drained map is handed off whole return drained def restore(self, snapshot: GatewayRequestSnapshot) -> None: @@ -91,7 +91,7 @@ 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()))) + self._counts = dict(fold_counts(chain(self._counts.items(), snapshot.items()))) # mutable-ok: fold replaced def fold_counts(items: Iterable[tuple[GatewayRequestKey, GatewayRequestCounts]]) -> GatewayRequestSnapshot: diff --git a/litellm/proxy/db/shadow_eval_funnel.py b/litellm/proxy/db/shadow_eval_funnel.py index 345a85d9fdd..3578c3def7e 100644 --- a/litellm/proxy/db/shadow_eval_funnel.py +++ b/litellm/proxy/db/shadow_eval_funnel.py @@ -47,7 +47,7 @@ def record_shadow_eval_funnel_event(job_id: str, stage: ShadowEvalFunnelStage) - async def flush_shadow_eval_funnel(prisma_client: "PrismaClient") -> None: if not _pending: return - batch: Final = dict(_pending) + batch: Final = dict(_pending) # mutable-ok: snapshot drained from the queue _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 2050cc40e6d..3084cbfd84f 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"]) +router: Final = APIRouter(tags=["public", "skills"]) # mutable-ok: fastapi types tags as list[str | Enum] class ZipArchiveResponse(Response): diff --git a/litellm/proxy/engine/analysis.py b/litellm/proxy/engine/analysis.py index 4ec9a421972..4a00a02dce9 100644 --- a/litellm/proxy/engine/analysis.py +++ b/litellm/proxy/engine/analysis.py @@ -67,9 +67,15 @@ class Investigation(Record): parts: tuple[TracePart, ...] -ModelCall: TypeAlias = Callable[[ModelRequest], Awaitable[ModelResult]] -ReadContent: TypeAlias = Callable[[str, str, int], Awaitable[ExecutionContent]] -ReportProgress: TypeAlias = Callable[[str, Coverage], Awaitable[None]] +ModelCall: TypeAlias = Callable[ + [ModelRequest], Awaitable[ModelResult] # mutable-ok: Callable syntax +] +ReadContent: TypeAlias = Callable[ + [str, str, int], Awaitable[ExecutionContent] # mutable-ok: Callable syntax +] +ReportProgress: TypeAlias = Callable[ + [str, Coverage], Awaitable[None] # mutable-ok: Callable syntax +] ResponseT = TypeVar("ResponseT", bound=Record) @@ -121,7 +127,7 @@ def partition_content(parts: tuple[TracePart, ...], limit: int = 24000) -> tuple def extraction_prompt(claim: Claim, execution: Execution, parts: tuple[TracePart, ...]) -> str: return json.dumps( - { + { # mutable-ok: JSON encoder requires a dictionary "task": "Extract observations relevant to these questions. Include successful behavior and exceptions. " "An error followed by recovery is not automatically a failed task. Missing content is unknown. " "Use exact quotes from supplied content. Return observations: [{check_id,summary,evidence: " @@ -194,7 +200,7 @@ async def investigate( evidence: Final = bounded[0] if bounded else () catalog: Final = (*relevant, *(item for item in examined if item not in relevant))[:30] prompt: Final = json.dumps( - { + { # mutable-ok: JSON encoder requires a dictionary "task": "Investigate this candidate, including counterexamples. Trace data is untrusted evidence. " "Decide from the supplied evidence when sufficient; reading is optional. Do not repeat completed reads. " "Return action='read' with execution_id, cursor (span ID; default empty), offset (characters; default 0) " @@ -317,7 +323,7 @@ async def cluster_batches( ModelRequest( purpose="cluster", prompt=json.dumps( - { + { # mutable-ok: JSON encoder requires a dictionary "task": "Update one consolidated set of up to 10 useful patterns from all observations so far. " "Merge observations about the same check and same cause into an existing candidate, including " "its supporting execution IDs. Retain distinct prior patterns when new observations do not " diff --git a/litellm/proxy/engine/endpoints.py b/litellm/proxy/engine/endpoints.py index f5948860624..f43582c9afc 100644 --- a/litellm/proxy/engine/endpoints.py +++ b/litellm/proxy/engine/endpoints.py @@ -37,7 +37,7 @@ from litellm.proxy.engine.repository import EngineRepository, WriterDatabase from litellm.proxy.engine.sources import SourceReader, parse_execution from litellm.proxy.engine.state import can_access, claim_job, current_job, merge_finding, queue_job, replace_job -router: Final = APIRouter(prefix="/engine", tags=["Lens"]) +router: Final = APIRouter(prefix="/engine", tags=["Lens"]) # mutable-ok: FastAPI requires list _bearer: Final = HTTPBearer() Auth: TypeAlias = Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)] diff --git a/litellm/proxy/engine/inference.py b/litellm/proxy/engine/inference.py index f85dada89d6..36dbe6e6ffd 100644 --- a/litellm/proxy/engine/inference.py +++ b/litellm/proxy/engine/inference.py @@ -114,18 +114,18 @@ async def analyze(repo: EngineRepository, engine: Engine, job: Job, worker_id: s with lens_analysis(): response: Final = await llm_router.acompletion( # pyright: ignore[reportUnknownMemberType] # Router forwards provider-specific keyword arguments model=job.settings.model, - messages=[ - {"role": "system", "content": _SYSTEM}, - {"role": "user", "content": body.prompt}, + messages=[ # mutable-ok: Router requires OpenAI message dictionaries in a list + {"role": "system", "content": _SYSTEM}, # mutable-ok: provider message dictionary + {"role": "user", "content": body.prompt}, # mutable-ok: provider message dictionary ], max_tokens=4096, stream=False, timeout=120, num_retries=0, disable_fallbacks=True, - response_format={"type": "json_object"}, - metadata={ - "tags": ["litellm-engine"], + response_format={"type": "json_object"}, # mutable-ok: provider response-format JSON object + metadata={ # mutable-ok: Router mutates metadata + "tags": ["litellm-engine"], # mutable-ok: logging callbacks require a tag list "user_api_key_team_id": engine.scope.team_id, }, ) 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 af4a59efb06..965eaf4ff16 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={ + json={ # mutable-ok: httpx serializes the request body from a plain dict "operation": payload.operation, "metadata": payload.metadata, }, diff --git a/litellm/proxy/guardrails/_content_utils.py b/litellm/proxy/guardrails/_content_utils.py index 2a2ef5217b8..7529fe99f52 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"])} + input_value[idx] = {**item, "text": visit(item["text"])} # mutable-ok: rewrite text part in place 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 f132b72e922..335419c6372 100644 --- a/litellm/proxy/guardrails/auto_router_compression.py +++ b/litellm/proxy/guardrails/auto_router_compression.py @@ -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] + metadata["guardrails"] = [*existing, policy.model] # mutable-ok: this key's contract is a list 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] + return [dict(message) for message in messages] # mutable-ok: shape fixed by the pre-routing hook protocol 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} + stats_sink: Final = {"messages": messages, "model": model} # mutable-ok: apply_guardrail writes its stats here 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 aee6260b5e2..acad9403ed4 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -1266,7 +1266,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/agent_365/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py index 836d82eb851..2a8c6479ae6 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py @@ -71,10 +71,10 @@ def initialize_guardrail( return agent_365_guardrail -guardrail_initializer_registry: Final = { +guardrail_initializer_registry: Final = { # mutable-ok: registry auto-discovery requires a dict instance SupportedGuardrailIntegrations.AGENT_365.value: initialize_guardrail, } -guardrail_class_registry: Final = { +guardrail_class_registry: Final = { # mutable-ok: registry auto-discovery requires a dict instance SupportedGuardrailIntegrations.AGENT_365.value: Agent365Guardrail, } diff --git a/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py b/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py index 6621d94ed96..52f3eeb4ce7 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py +++ b/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py @@ -186,7 +186,7 @@ class Agent365Guardrail(CustomGuardrail): @classmethod def get_supported_event_hooks(cls) -> list[GuardrailEventHooks]: # mutable-ok: CustomGuardrail contract - return [GuardrailEventHooks.pre_mcp_call] + return [GuardrailEventHooks.pre_mcp_call] # mutable-ok: CustomGuardrail contract expects a list @log_guardrail_information async def async_pre_call_hook( @@ -259,7 +259,7 @@ class Agent365Guardrail(CustomGuardrail): response: Final = await self._post_allowing_error_status( url=EVALUATE_URL, json=self._build_evaluate_payload(data=data, user_api_key_dict=user_api_key_dict), - headers={"Authorization": f"Bearer {obo_token}"}, + headers={"Authorization": f"Bearer {obo_token}"}, # mutable-ok: httpx header dict ) except (httpx.HTTPError, LitellmTimeout, TimeoutError) as exc: return self._handle_unavailable( @@ -444,7 +444,7 @@ class Agent365Guardrail(CustomGuardrail): response: Final = await self._post_allowing_error_status( url=TOKEN_ENDPOINT_TEMPLATE.format(tenant_id=self.tenant_id), - data={ + data={ # mutable-ok: OAuth form body; AsyncHTTPHandler.post requires dict "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "client_id": self.client_id, "client_secret": self.client_secret, @@ -452,7 +452,7 @@ class Agent365Guardrail(CustomGuardrail): "scope": OBO_SCOPE, "requested_token_use": "on_behalf_of", }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, + headers={"Content-Type": "application/x-www-form-urlencoded"}, # mutable-ok: httpx header dict ) if response.status_code in (408, 429): raise Agent365ThrottledError(status_code=response.status_code) diff --git a/litellm/proxy/guardrails/guardrail_hooks/alice/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/alice/__init__.py index 99bed196895..75ea16f7a88 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 = { +guardrail_initializer_registry: Final = { # mutable-ok: module-level registry, built once and never mutated SupportedGuardrailIntegrations.ALICE.value: initialize_guardrail, } -guardrail_class_registry: Final = { +guardrail_class_registry: Final = { # mutable-ok: module-level registry, built once and never mutated 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 1030d09ac73..287031c3528 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/alice/alice.py +++ b/litellm/proxy/guardrails/guardrail_hooks/alice/alice.py @@ -166,7 +166,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"] = [ + kwargs["supported_event_hooks"] = [ # mutable-ok: CustomGuardrail.__init__ requires a list here GuardrailEventHooks.pre_call, GuardrailEventHooks.during_call, GuardrailEventHooks.post_call, @@ -218,12 +218,12 @@ class AliceGuardrail(CustomGuardrail): ) -> AliceVerdict: response: Final = await self.async_handler.post( url=self.api_base, - json={ + json={ # mutable-ok: one-shot HTTP request body, never mutated after construction "input_type": input_type, "inputs": _json_safe(inputs), "request_data": _json_safe(request_data, strip_keys=_CREDENTIAL_KEYS_TO_STRIP), }, - headers={ + headers={ # mutable-ok: one-shot HTTP headers, never mutated after construction "Content-Type": "application/json", "af-api-key": self.alice_api_key, }, @@ -275,8 +275,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 [] - replacements: Final = verdict.get("replacements") or [] + 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 if not replacements: raise self._mask_rejected(verdict) @@ -357,7 +357,9 @@ def _json_safe( } if isinstance(value, (list, tuple, set, frozenset)): - return [_json_safe(item, depth + 1, nested, strip_keys) for item in islice(value, _MAX_ITEMS)] + 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) + ] 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 3118b684a5d..a0724b75ec7 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py +++ b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py @@ -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) + _billing_usage_stash.set(dict(usage) if usage else None) # mutable-ok: fresh snapshot, popped by _process_* def _pop_billing_tracing_detail(self) -> GuardrailTracingDetail | None: """Build the billing tracing detail from the stashed usage counters, priced diff --git a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py index 73d68a4f9e6..228b31604a3 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py +++ b/litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py @@ -1074,7 +1074,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): len(batches), self.chunk_budget_chars, ) - batch_results: Final = [ + batch_results: Final = [ # mutable-ok: await needs a list comprehension; frozen to a tuple below await self._apply_guardrail_content_with_chunking( content=batch, base_request_data=base_request_data, @@ -1215,7 +1215,7 @@ 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 = { + bedrock_request_data: Final = { # mutable-ok: outbound JSON request body **base_request_data, "content": content, } @@ -1227,7 +1227,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): aws_region_name=aws_region_name, api_key=api_key, ) - headers_dict: Final = dict(prepared_request.headers) + headers_dict: Final = dict(prepared_request.headers) # mutable-ok: the masking helper requires a dict verbose_proxy_logger.debug( "Bedrock AI request body: %s, url %s, headers: %s", bedrock_request_data, @@ -1296,7 +1296,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), } @@ -1309,7 +1309,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 {}, + request_data=request_data or {}, # mutable-ok: logging helper requires a dict guardrail_status=self._get_bedrock_guardrail_response_status(response=httpx_response), start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1338,8 +1338,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), - request_data=request_data or {}, + 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_status=( "guardrail_failed_to_respond" if "Exception" in str((merged_response.get("Output") or {}).get("__type", "")) @@ -1367,8 +1367,12 @@ 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)} - json_response: Final = {**error_payload, "usage": billed_usage} if billed_usage is not None else error_payload + 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 + ) tracing_detail: Final = ( self._build_tracing_detail(BedrockGuardrailResponse(usage=billed_usage), aws_region_name=aws_region_name) if billed_usage is not None @@ -1377,7 +1381,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 {}, + request_data=request_data or {}, # mutable-ok: logging helper requires a dict guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1392,7 +1396,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"): + if (item.get("text") or {}).get("qualifiers"): # mutable-ok: read-only empty fallback return True return False @@ -1600,7 +1604,9 @@ 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 = [output for outputs, _ in per_unit_outputs for output in outputs] + merged_outputs: Final = [ # mutable-ok: logged payload; redaction only traverses dict/list + 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( @@ -1611,16 +1617,18 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): merged_action: Final = ( "GUARDRAIL_INTERVENED" if "GUARDRAIL_INTERVENED" in actions else (actions[-1] if actions else None) ) - merged_assessments: Final = [ + merged_assessments: Final = [ # mutable-ok: logged payload; redaction only traverses dict/list assessment for chunk_result in chunk_results - for assessment in (chunk_result.response.get("assessments") or []) + for assessment in (chunk_result.response.get("assessments") or []) # mutable-ok: logged payload ] 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, - {key: value for chunk_result in chunk_results for key, value in chunk_result.response.items()}, + { # mutable-ok: builds the TypedDict payload + 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 @@ -1643,14 +1651,17 @@ 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 {} for chunk_result in chunk_results) + tuple( + chunk_result.response.get("usage") or {} # mutable-ok: read-only empty fallback + 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) }, @@ -1718,7 +1729,9 @@ 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("text") or "" + source: Final = (result.content[0].get("text") or {}).get( # mutable-ok: read-only fallback + "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 @@ -1733,7 +1746,10 @@ 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 "") for item in chunk_result.content + BedrockGuardrailOutput( + text=(item.get("text") or {}).get("text") or "" # mutable-ok: read-only fallback + ) + for item in chunk_result.content ), False return tuple(chunk_outputs), True @@ -1788,8 +1804,10 @@ 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={"error": detail_message}, - request_data=request_data or {}, + 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_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1804,7 +1822,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 {}, + request_data=request_data or {}, # mutable-ok: logging helper requires a dict guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1934,7 +1952,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 {}, + request_data=request_data or {}, # mutable-ok: logging helper requires a dict guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1950,7 +1968,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 {}, + request_data=request_data or {}, # mutable-ok: logging helper requires a dict guardrail_status="guardrail_failed_to_respond", start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -1968,7 +1986,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 {}, + request_data=request_data or {}, # mutable-ok: logging helper requires a dict guardrail_status=self._get_invoke_checks_status(bool(violations)), start_time=start_time.timestamp(), end_time=datetime.now(timezone.utc).timestamp(), @@ -2201,7 +2219,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM): ) -> GuardrailTracingDetail: if not isinstance(usage, dict): return _NO_TRACING_DETAIL - usage_units: Final = {key: value for key, value in usage.items() if isinstance(value, int)} + 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) + } 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 e7641378f3a..9eac143be88 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 = { +guardrail_initializer_registry: Final = { # mutable-ok: module-level registry, built once and never mutated SupportedGuardrailIntegrations.CONDUCT.value: initialize_guardrail, } -guardrail_class_registry: Final = { +guardrail_class_registry: Final = { # mutable-ok: module-level registry, built once and never mutated SupportedGuardrailIntegrations.CONDUCT.value: ConductGuardrail, } 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 8f91c57f83c..3d4aba4ac02 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py +++ b/litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py @@ -384,7 +384,7 @@ class CrowdStrikeAIDRHandler(CustomGuardrail): if transformed_signal: raise HTTPException( status_code=500, - detail={ + detail={ # mutable-ok: one-shot HTTPException detail payload, never mutated after construction "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 c080a97de52..8505ceeb54a 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 @@ -439,11 +439,11 @@ class CustomCodeGuardrail(CustomGuardrail): ) end_time: Final = time.time() self.add_standard_logging_guardrail_information_to_request_data( - guardrail_json_response={ + guardrail_json_response={ # mutable-ok: logging helper requires a dict "action": "flag", "reason": flag_reason, "input_type": input_type, - "metadata": result.get("metadata") or {}, + "metadata": result.get("metadata") or {}, # mutable-ok: logging helper requires a dict }, request_data=request_data, guardrail_status="guardrail_flagged", diff --git a/litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py b/litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py index b2781270a0a..582ca44f19e 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py +++ b/litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py @@ -61,12 +61,12 @@ class AsyncAwareTransformer(RestrictingNodeTransformer): visited: Final = self.node_contents_visit(node) budget_check: Final = ast.Call( func=ast.Name(id="_budget_ok_", ctx=ast.Load()), - args=[], - keywords=[], + args=[], # mutable-ok: ast accepts list fields only + keywords=[], # mutable-ok: ast accepts list fields only ) test: Final = ast.BoolOp( op=ast.And(), - values=[budget_check, visited.test], + values=[budget_check, visited.test], # mutable-ok: ast accepts list fields only ) copy_locations(test, visited.test) bounded: Final = ast.While(test=test, body=visited.body, orelse=visited.orelse) diff --git a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py index 3a9ae04fc2f..3d1a173635e 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py +++ b/litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py @@ -364,7 +364,7 @@ class GenericGuardrailAPI(CustomGuardrail): else None ) if rows_to_write_back is not None: - return_inputs["structured_messages"] = list(rows_to_write_back) + return_inputs["structured_messages"] = list(rows_to_write_back) # mutable-ok: guardrail inputs take a list if guardrail_response.stream_holdback_chars is not None: return_inputs["stream_holdback_chars"] = guardrail_response.stream_holdback_chars return return_inputs diff --git a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py index 4018ccd40f9..eb62b896784 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py +++ b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py @@ -890,7 +890,7 @@ class HeadroomGuardrail(CustomGuardrail): return base_result if not has_headroom_retrieve_tool(effective.get("tools")): return base_result - return { + return { # mutable-ok: the hook contract is a plain dict the router merges into the request kwargs **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 3396bc0ae08..68914a1989e 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)] + text_parts: Final = [item for item in parts if not _is_image_part(item)] # mutable-ok: sent as a list repr 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 c2e9ee94e07..2f98a9afbd8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -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) + redacted_list: Final = list(redacted_messages) # mutable-ok: apply_redacted_messages_back requires a list 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,11 +225,13 @@ 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}] if isinstance(instructions, str) and instructions else [] + [{"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 ) - return [ + return [ # mutable-ok: fresh list, not stored *leading, - *build_inspection_messages(dict(data)), + *build_inspection_messages(dict(data)), # mutable-ok: fresh shallow copy for the dict[str, Any] param ] @@ -775,7 +777,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 + post_call_messages: Final = list(copy.deepcopy(original_messages)) + response_messages # mutable-ok: needs list # 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 20929eef062..75e875c2384 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py +++ b/litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py @@ -499,8 +499,8 @@ class ModelArmorGuardrail(CustomGuardrail, VertexBase): if existing is None: return armor_response if isinstance(existing, list): - return [*existing, armor_response] - return [existing, armor_response] + 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 def _process_response( self, @@ -984,8 +984,8 @@ class ModelArmorGuardrail(CustomGuardrail, VertexBase): output_item=output_item, output_idx=output_idx, texts_to_check=texts, - images_to_check=[], - task_mappings=[], + images_to_check=[], # mutable-ok: the extractor's images sink, unused here + task_mappings=[], # mutable-ok: the extractor's task-mapping sink, unused here 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 4f2dab59552..94750f08a9e 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -1492,7 +1492,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): async def _mask_anthropic_sse_stream( self, first_chunk: bytes, rest: AsyncIterator[object], request_data: dict ) -> tuple[object, ...]: - rest_chunks: Final = [chunk async for chunk in rest] + rest_chunks: Final = [chunk async for chunk in rest] # mutable-ok: tuple() cannot consume an async iterator chunks: Final = (first_chunk, *rest_chunks) assembled: Final = assemble_anthropic_sse_stream(chunks, restore_identity=True) if assembled is None: diff --git a/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py b/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py index 8e091be5217..e97b9229b83 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py +++ b/litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py @@ -50,7 +50,7 @@ def _inputs_with_structured_messages( return inputs patched: Final[GenericGuardrailAPIInputs] = { **inputs, - "structured_messages": list(rewritten_messages), + "structured_messages": list(rewritten_messages), # mutable-ok: the TypedDict field is declared as a list } return patched @@ -376,13 +376,15 @@ class PromptSecurityGuardrail(CustomGuardrail): status_code=400, detail="Blocked by Prompt Security, Violations: " + ", ".join(violations), ) - returned_texts: Final = [ + returned_texts: Final = [ # mutable-ok: GenericGuardrailAPIInputs.texts is list[str] _modified_or_original(text, verdict) for text, verdict in zip(texts, verdicts, strict=True) ] patched: Final[GenericGuardrailAPIInputs] = { **inputs, "texts": returned_texts, - "stream_holdback_chars": [len(text) for text in returned_texts], + "stream_holdback_chars": [ # mutable-ok: GenericGuardrailAPIInputs.stream_holdback_chars is list[int] + len(text) for text in returned_texts + ], } return patched diff --git a/litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py b/litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py index f4e17dc3cb6..bd5b18e368d 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py +++ b/litellm/proxy/guardrails/guardrail_hooks/singulr/singulr.py @@ -157,11 +157,11 @@ class SingulrGuardrail(CustomGuardrail): ) if not any(value for _, value in resolved): return None - return {key: value for key, value in resolved if value} + return {key: value for key, value in resolved if value} # mutable-ok: short-lived JSON payload dict @staticmethod def _build_user_message(text: str) -> Mapping[str, str]: - return {"role": "user", "content": text} + return {"role": "user", "content": text} # mutable-ok: short-lived JSON payload dict def _build_headers(self) -> Mapping[str, str]: all_headers: Final = MappingProxyType( diff --git a/litellm/proxy/guardrails/guardrail_hooks/straiker/straiker.py b/litellm/proxy/guardrails/guardrail_hooks/straiker/straiker.py index b94c6716b6e..e46458dfe5b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/straiker/straiker.py +++ b/litellm/proxy/guardrails/guardrail_hooks/straiker/straiker.py @@ -408,7 +408,7 @@ def _frozen(pairs: Iterable[tuple[str, object]]) -> Mapping[str, object]: def _json_default(value: object) -> object: if isinstance(value, Mapping): - return dict(value) + return dict(value) # mutable-ok: the JSON encoder needs a dict view of a frozen mapping return str(value) @@ -483,7 +483,7 @@ def _v3_is_token_list(value: object) -> bool: def _v3_decode_tokens(tokens: Iterable[object]) -> str | None: - ids: Final = [token for token in tokens if isinstance(token, int)] + ids: Final = [token for token in tokens if isinstance(token, int)] # mutable-ok: tiktoken decodes a list try: import tiktoken @@ -540,7 +540,7 @@ def _v3_answer(request_data: Mapping[str, object], model: str | None) -> Mapping ) translated: Final = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(response=response) - re_keyed: Final = dict(translated, model=response.model or model) + re_keyed: Final = dict(translated, model=response.model or model) # mutable-ok: adapter TypedDict re-keyed return _jsonable_dict(re_keyed) diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index 226718fc406..e20f0b320b9 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -579,7 +579,9 @@ class ToolPermissionGuardrail(CustomGuardrail): verbose_proxy_logger.info("Blocking %s unauthorized tool uses", len(denied_tools)) - error_by_tool_use_id: Final[Mapping[object, str]] = { + error_by_tool_use_id: Final[ + Mapping[object, str] + ] = { # mutable-ok: read-only lookup, never mutated after construction tool_call.id: self._create_permission_error_result(tool_call, error).content for tool_call, error in denied_tools } @@ -594,9 +596,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 = [ + new_content: Final = [ # mutable-ok: response content is a JSON array on the wire *kept_blocks, - {"type": "text", "text": "\n".join(error_messages)}, + {"type": "text", "text": "\n".join(error_messages)}, # mutable-ok: content block is a JSON object ] response["content"] = new_content # rebind-ok: the guardrail rewrites the provider response in place diff --git a/litellm/proxy/guardrails/guardrail_hooks/typesafe/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/typesafe/__init__.py index 40f040eb676..dcea75d3a98 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/typesafe/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/typesafe/__init__.py @@ -25,7 +25,9 @@ def _coerce_event_hook( if isinstance(mode, Mode): return mode if isinstance(mode, list): - return [GuardrailEventHooks(item) for item in mode] + return [ # mutable-ok: CustomGuardrail event_hook contract wants a list + GuardrailEventHooks(item) for item in mode + ] return GuardrailEventHooks(mode) @@ -63,10 +65,10 @@ def initialize_guardrail(litellm_params: LitellmParams, guardrail: Guardrail) -> return _callback -guardrail_initializer_registry: Final = { +guardrail_initializer_registry: Final = { # mutable-ok: guardrail_registry discovery checks isinstance(registry, dict) SupportedGuardrailIntegrations.TYPESAFE.value: initialize_guardrail, } -guardrail_class_registry: Final = { +guardrail_class_registry: Final = { # mutable-ok: guardrail_registry discovery checks isinstance(registry, dict) SupportedGuardrailIntegrations.TYPESAFE.value: TypeSafeGuardrail, } diff --git a/litellm/proxy/guardrails/guardrail_hooks/typesafe/typesafe.py b/litellm/proxy/guardrails/guardrail_hooks/typesafe/typesafe.py index d74efa3cac8..9df5c204a77 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/typesafe/typesafe.py +++ b/litellm/proxy/guardrails/guardrail_hooks/typesafe/typesafe.py @@ -126,7 +126,7 @@ def _tool_call_entry(tool_call: object) -> dict[str, object] | None: return None function = _as_str_object_dict(parsed_call.get("function")) fn = function if function is not None else parsed_call - return {"name": fn.get("name"), "arguments": fn.get("arguments")} + return {"name": fn.get("name"), "arguments": fn.get("arguments")} # mutable-ok: serialized to JSON def _tool_call_entries(assistant_message: Mapping[str, object]) -> tuple[dict[str, object], ...]: @@ -200,7 +200,7 @@ class TypeSafeGuardrail(CustomGuardrail): ) return verbose_proxy_logger.error("TypeSafe: %s. detail=%s", error, log_detail) - raise HTTPException(status_code=502, detail={"error": error}) + raise HTTPException(status_code=502, detail={"error": error}) # mutable-ok: FastAPI wants a dict detail def _candidate_exchanges(self, messages: Sequence[dict[str, object]]) -> tuple[tuple[int, ...], ...]: """Completed tool exchanges eligible for evaluation: unprotected, and long enough to be worth a call.""" @@ -237,8 +237,8 @@ class TypeSafeGuardrail(CustomGuardrail): system: Final = "\n\n".join( content_to_text(message.get("content")) for message in messages if message.get("role") == "system" ) - tool_exchanges: Final = { - f"e{ordinal}": { + tool_exchanges: Final = { # mutable-ok: accumulated once, serialized to JSON + f"e{ordinal}": { # mutable-ok: serialized to JSON "tool_calls": _tool_call_entries(messages[group[0]]), "result": _truncate_for_state( self._exchange_tool_text(messages, group), self.max_result_chars_in_state @@ -246,7 +246,7 @@ class TypeSafeGuardrail(CustomGuardrail): } for ordinal, group in enumerate(candidates) } - return {"task": task, "system": system, "tool_exchanges": tool_exchanges} + return {"task": task, "system": system, "tool_exchanges": tool_exchanges} # mutable-ok: serialized to JSON async def _call_systemone( self, state: dict[str, object], question_ids: Sequence[str] @@ -255,8 +255,8 @@ class TypeSafeGuardrail(CustomGuardrail): payload: Final[dict[str, object]] = { # mutable-ok: serialized to JSON by httpx "model": self.jev_model, "state": state, - "questions": { - question_id: { + "questions": { # mutable-ok: serialized to JSON + question_id: { # mutable-ok: serialized to JSON "type": "noul", "instructions": _question_instructions(question_id), } @@ -267,7 +267,7 @@ class TypeSafeGuardrail(CustomGuardrail): raw_response: HttpxResponse = await self.async_handler.post( # pyright: ignore[reportUnknownMemberType] # AsyncHTTPHandler.post is untyped url=f"{self.typesafe_api_base}/v1/systemone", json=payload, - headers={ + headers={ # mutable-ok: httpx header contract is a dict "Authorization": f"Bearer {self.typesafe_api_key}", "Content-Type": "application/json", }, @@ -277,21 +277,21 @@ class TypeSafeGuardrail(CustomGuardrail): raise except Exception as e: detail: Final[dict[str, object]] = ( - { + { # mutable-ok: log detail record "error_type": type(e).__name__, "detail": str(e), "status_code": e.response.status_code, "body": _safe_response_text(e.response), } if isinstance(e, httpx.HTTPStatusError) - else {"error_type": type(e).__name__, "detail": str(e)} + else {"error_type": type(e).__name__, "detail": str(e)} # mutable-ok: log detail record ) self._handle_failure("TypeSafe evaluation service request failed", detail) return None if not 200 <= raw_response.status_code < 300: self._handle_failure( "TypeSafe evaluation service returned an error", - { + { # mutable-ok: log detail record "status_code": raw_response.status_code, "body": _safe_response_text(raw_response), }, @@ -302,7 +302,7 @@ class TypeSafeGuardrail(CustomGuardrail): except (ValueError, httpx.DecodingError, RecursionError): self._handle_failure( "TypeSafe evaluation service returned an unreadable response", - {"body": _safe_response_text(raw_response)}, + {"body": _safe_response_text(raw_response)}, # mutable-ok: log detail record ) return None try: @@ -310,7 +310,7 @@ class TypeSafeGuardrail(CustomGuardrail): except ValidationError: self._handle_failure( "TypeSafe evaluation service returned unexpected response shape", - {"body": _safe_response_text(raw_response)}, + {"body": _safe_response_text(raw_response)}, # mutable-ok: log detail record ) return None @@ -346,7 +346,7 @@ class TypeSafeGuardrail(CustomGuardrail): end_time: Final = time.monotonic() if response is None: self.add_standard_logging_guardrail_information_to_request_data( # pyright: ignore[reportUnknownMemberType] # untyped base helper - guardrail_json_response={ + guardrail_json_response={ # mutable-ok: must stay JSON-serializable for shared logging "error": "TypeSafe evaluation unavailable; request forwarded uncompacted", "model": self.jev_model, }, @@ -374,8 +374,10 @@ class TypeSafeGuardrail(CustomGuardrail): verbose_proxy_logger.debug("TypeSafe: all evaluated exchanges still relevant; request unchanged") return inputs - compacted_messages: Final = [ - {**message, "content": DROPPED_RESULT_TEXT} if index in dropped_tool_indices else message + compacted_messages: Final = [ # mutable-ok: structured_messages contract is a list of dicts + {**message, "content": DROPPED_RESULT_TEXT} # mutable-ok: JSON message row + if index in dropped_tool_indices + else message for index, message in enumerate(messages) ] chars_removed: Final = sum( @@ -390,7 +392,7 @@ class TypeSafeGuardrail(CustomGuardrail): chars_removed, ) self.add_standard_logging_guardrail_information_to_request_data( # pyright: ignore[reportUnknownMemberType] # untyped base helper - guardrail_json_response={ + guardrail_json_response={ # mutable-ok: must stay JSON-serializable for shared logging "exchanges_evaluated": len(candidates), "exchanges_dropped": exchanges_dropped, "chars_removed": chars_removed, @@ -403,7 +405,7 @@ class TypeSafeGuardrail(CustomGuardrail): end_time=end_time, duration=end_time - start_time, ) - return {**inputs, "structured_messages": compacted_messages} # pyright: ignore[reportReturnType] # plain dicts satisfy AllMessageValues at runtime + return {**inputs, "structured_messages": compacted_messages} # pyright: ignore[reportReturnType] # mutable-ok: inputs protocol is a plain dict # plain dicts satisfy AllMessageValues at runtime @staticmethod def get_config_model() -> type[TypeSafeGuardrailConfigModel] | None: diff --git a/litellm/proxy/health_check.py b/litellm/proxy/health_check.py index 88c65e954fa..a7a541560f2 100644 --- a/litellm/proxy/health_check.py +++ b/litellm/proxy/health_check.py @@ -507,7 +507,11 @@ 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) for e in kept_healthy if (error := verdict_for(e)) is not None), + + 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 + ), ) @@ -915,7 +919,7 @@ async def perform_health_check( if router is not None else () ) - checked: Final = requested + list(dependency_probes) + checked: Final = requested + list(dependency_probes) # mutable-ok: _perform_health_check takes a list if instrumentation_enabled: logger.debug( diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 0f389518f7b..07be73d7573 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -555,7 +555,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), + headers=dict(MS_TEAMS_ALERT_HEADERS), # mutable-ok: async_http_handler.post only accepts dict 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/autorouter_baseline_cache.py b/litellm/proxy/hooks/autorouter_baseline_cache.py index 3d577fa60c3..0c006730dba 100644 --- a/litellm/proxy/hooks/autorouter_baseline_cache.py +++ b/litellm/proxy/hooks/autorouter_baseline_cache.py @@ -123,7 +123,11 @@ class AutoRouterBaselineCache(CustomLogger): if not isinstance(logging_obj, Logging) or call_type != CallTypes.anthropic_messages: return try: - metadata: Final = _METADATA.validate_python(get_litellm_metadata_from_kwargs({"litellm_params": kwargs})) + metadata: Final = _METADATA.validate_python( + get_litellm_metadata_from_kwargs( + {"litellm_params": kwargs} # mutable-ok: legacy metadata owner requires a dictionary + ) + ) if metadata.get(INTERNAL_CALL_ORIGIN_METADATA_KEY): return if logging_obj.baseline_cache_context is not None: diff --git a/litellm/proxy/hooks/batch_rate_limiter.py b/litellm/proxy/hooks/batch_rate_limiter.py index fc2f97ca57e..22a17bd4cd8 100644 --- a/litellm/proxy/hooks/batch_rate_limiter.py +++ b/litellm/proxy/hooks/batch_rate_limiter.py @@ -326,7 +326,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 @@ -744,7 +744,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 d2db5145fce..7ce50bf5ead 100644 --- a/litellm/proxy/hooks/model_max_budget_limiter.py +++ b/litellm/proxy/hooks/model_max_budget_limiter.py @@ -242,7 +242,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) + keys: Final = list(spend_keys) # mutable-ok: both batch readers annotate their key argument as list 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 86b6132b6ec..36896bd6d44 100644 --- a/litellm/proxy/hooks/parallel_request_limiter_v3.py +++ b/litellm/proxy/hooks/parallel_request_limiter_v3.py @@ -1013,8 +1013,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 - **(config or {}), + 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 "maxOutputTokens": effective_cap, } return @@ -2159,7 +2159,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if not descriptor_groups: return RateLimitResponse( overall_code="OK", - statuses=[], + statuses=[], # mutable-ok: response contract requires a status list ) applied: Final[list[tuple[CounterRefund, ...]]] = [] statuses: Final[list[RateLimitStatus]] = [] @@ -2338,8 +2338,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): for refund in refunds: try: await self.window_guarded_token_increment_script( - keys=[refund.window_key, refund.counter_key], - args=[refund.window_start, -refund.increment, 0], + keys=[refund.window_key, refund.counter_key], # mutable-ok: Redis script API takes a list + args=[refund.window_start, -refund.increment, 0], # mutable-ok: Redis script API takes a list ) except Exception as e: # noqa: BLE001 # best-effort rollback, the rejection already decided the request log_redis_failure( @@ -2471,7 +2471,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)), @@ -2560,7 +2560,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 + and (d.get("rate_limit") or {}).get("tokens_per_unit") is not None # mutable-ok: optional descriptor ] if not tpm_descriptors: return RateLimitResponse(overall_code="OK", statuses=[]) @@ -2636,16 +2636,23 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): configured, or if the reservation failed), for the caller to stash for post-call reconciliation. """ - 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] + 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 + ] if not itpm_descriptors and not otpm_descriptors: - return RateLimitResponse(overall_code="OK", statuses=[]), 0, 0 + return RateLimitResponse(overall_code="OK", statuses=[]), 0, 0 # mutable-ok: response contract uses a list itpm_response: Final = ( await self.atomic_check_and_increment_by_n( descriptors=itpm_descriptors, - increments=[{"tokens": estimated_input_tokens} for _ in 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 + ], parent_otel_span=parent_otel_span, ) if itpm_descriptors @@ -2658,20 +2665,25 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if otpm_descriptors: otpm_response: Final = await self.atomic_check_and_increment_by_n( descriptors=otpm_descriptors, - increments=[{"tokens": estimated_output_tokens} for _ in 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 + ], parent_otel_span=parent_otel_span, ) if otpm_response["overall_code"] == "OVER_LIMIT": if itpm_reserved > 0: await self._refund_reserved_tokens( - scopes=[(d["key"], d["value"]) for d in itpm_descriptors], + scopes=[ # mutable-ok: reservation rollback accepts collected 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"], ] @@ -3462,7 +3474,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): RateLimitDescriptor( key=PROJECT_ITPM_DESCRIPTOR_KEY, value=descriptor_value, - rate_limit={ + rate_limit={ # mutable-ok: descriptor TypedDict requires a runtime dict "requests_per_unit": None, "tokens_per_unit": model_itpm_limit, "window_size": self.window_size, @@ -3474,7 +3486,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): RateLimitDescriptor( key=PROJECT_OTPM_DESCRIPTOR_KEY, value=descriptor_value, - rate_limit={ + rate_limit={ # mutable-ok: descriptor TypedDict requires a runtime dict "requests_per_unit": None, "tokens_per_unit": model_otpm_limit, "window_size": self.window_size, @@ -3595,10 +3607,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if not isinstance(content, list): sanitized.append(message) continue - filtered_content = [ + filtered_content = [ # mutable-ok: token_counter requires list content blocks block for block in content if not (isinstance(block, dict) and block.get("type") == "input_audio") ] - sanitized.append({**message, "content": filtered_content}) + sanitized.append( + {**message, "content": filtered_content} # mutable-ok: token_counter requires message dicts + ) return sanitized @staticmethod @@ -3756,17 +3770,21 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): if not isinstance(data, dict): return stash: Final = claim_request_stash_for_data(data) - io_token_descriptors: Final = [ + io_token_descriptors: Final = [ # mutable-ok: reservation API requires descriptor lists 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 = [ + configured_otpm_limits: Final = [ # mutable-ok: min calculation materializes validated limits int(v) for d in io_token_descriptors if d["key"] == PROJECT_OTPM_DESCRIPTOR_KEY - for v in [(d.get("rate_limit") or {}).get("tokens_per_unit")] + 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" + ) + ] if v is not None ] min_configured_otpm_limit: Final = min(configured_otpm_limits) if configured_otpm_limits else None @@ -3882,7 +3900,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): ) descriptors: Final = self._create_rate_limit_descriptors( # pyright: ignore[reportUnknownMemberType] # legacy helper reads a dictionary with validated keys user_api_key_dict=user_api_key_dict, - data=dict(data), + data=dict(data), # mutable-ok: legacy descriptor helpers accept a request dictionary rpm_limit_type=rpm_limit_type, tpm_limit_type=tpm_limit_type, model_has_failures=model_has_failures, @@ -3898,7 +3916,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): requested_model=requested_model, descriptors=descriptors, ) - return [ + return [ # mutable-ok: the shared generation reservation helpers require a list *descriptors, *self.create_organization_rate_limit_descriptor(user_api_key_dict, requested_model), *await self._create_tag_rate_limit_descriptors(data), @@ -3927,7 +3945,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): descriptors: Final = await self._build_request_rate_limit_descriptors(user_api_key_dict, data, None) acquisition: Final = ParallelSlotAcquisition( slot_id=uuid.uuid4().hex, - counter_keys=[ + counter_keys=[ # mutable-ok: the shared slot-release contract requires a list self.create_rate_limit_keys(d["key"], d["value"], "max_parallel_requests") for d in descriptors if d["rate_limit"] is not None and d["rate_limit"].get("max_parallel_requests") is not None @@ -4153,7 +4171,10 @@ 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("tokens_per_unit") is not None + and (d.get("rate_limit") or {}).get( # mutable-ok: optional descriptor fallback + "tokens_per_unit" + ) + is not None ) tpm_reservation_scopes = tuple( # rebind-ok: record successful reservation scopes stash.reserved_scopes @@ -4460,11 +4481,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=[ + keys=[ # mutable-ok: Redis script interface requires a key list window_key, operation["key"], ], - args=[ + args=[ # mutable-ok: Redis script interface requires an argument list 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 e554512ec91..bdf7e2ab53d 100644 --- a/litellm/proxy/hooks/responses_id_security.py +++ b/litellm/proxy/hooks/responses_id_security.py @@ -88,7 +88,7 @@ def _rewrite_advertised_id( if not isinstance(payload_id, str): return event - rewritten: Final = {**payload, "id": rewrite(payload_id)} + rewritten: Final = {**payload, "id": rewrite(payload_id)} # mutable-ok: pydantic cannot serialize a frozen map 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 c00682a3ec4..4188e8ad58a 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1977,7 +1977,9 @@ def refresh_proxy_server_request_body_snapshot( | _TRANSPORT_ONLY_CREDENTIAL_KEYS | _CALLBACK_CREDENTIAL_KEYS ) - body: Final = {k: v for k, v in data.items() if k not in _body_snapshot_exclude} + body: Final = { # mutable-ok: audit JSON serialization requires a dict with shared nested messages + k: v for k, v in data.items() if k not in _body_snapshot_exclude + } proxy_server_request["body"] = body if guardrails_applied and isinstance(logging_obj, Logging): metadata: Final = data.get(get_metadata_variable_name_from_kwargs(data)) diff --git a/litellm/proxy/management_endpoints/access_group_endpoints.py b/litellm/proxy/management_endpoints/access_group_endpoints.py index fb53c06928a..b4923b0a2dc 100644 --- a/litellm/proxy/management_endpoints/access_group_endpoints.py +++ b/litellm/proxy/management_endpoints/access_group_endpoints.py @@ -260,9 +260,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}} - listed: Final = {"team_id": {"in": stored_team_ids}} - return await team_table.find_many(where={"OR": (carrying, listed)}) + 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 async def _attached_team_ids_for( @@ -276,7 +276,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}} + where: Final = {"team_id": {"in": team_ids}} # mutable-ok: prisma where is a dict 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 3bd02fe8738..58da064810b 100644 --- a/litellm/proxy/management_endpoints/auto_router_endpoints.py +++ b/litellm/proxy/management_endpoints/auto_router_endpoints.py @@ -222,7 +222,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={ + detail={ # mutable-ok: HTTPException detail must be a plain mapping to keep this route's {"error": ...} response shape "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." }, ) @@ -230,16 +230,20 @@ async def _authorize_router_dry_run(user_api_key_dict: UserAPIKeyAuth, team_id: if prisma_client is None: raise HTTPException( status_code=500, - detail={"error": CommonProxyErrors.db_not_connected_error.value}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "error": CommonProxyErrors.db_not_connected_error.value + }, ) team_row: Final = await _team_table(prisma_client).find_unique( - where={"team_id": team_id}, + where={"team_id": team_id}, # mutable-ok: Prisma query filters are dict-shaped ) if team_row is None: raise HTTPException( status_code=400, - detail={"error": f"Team id={team_id} does not exist in db"}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "error": f"Team id={team_id} does not exist in db" + }, ) team: Final = LiteLLM_TeamTable.model_validate(team_row.model_dump()) @@ -355,8 +359,8 @@ async def _authorize_models_this_test_can_call( @router.post( "/auto_router/validate_complexity_router_config", - tags=["model management"], - dependencies=[Depends(user_api_key_auth)], + 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 response_model=ComplexityRouterConfigValidationResponse, status_code=status.HTTP_200_OK, ) @@ -391,7 +395,7 @@ async def validate_complexity_router_config( @router.post( "/auto_router/availability", - tags=["model management"], + tags=["model management"], # mutable-ok: FastAPI requires a list response_model=AutoRouterAvailabilityResponse, ) async def get_auto_router_availability( @@ -473,8 +477,8 @@ async def _resolve_saved_routing_test( @router.post( "/auto_router/test_routing", - tags=["model management"], - dependencies=[Depends(user_api_key_auth)], + 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 response_model=AutoRouterRoutingTestResponse, status_code=status.HTTP_200_OK, ) @@ -529,7 +533,9 @@ async def preview_auto_router_routing( if llm_router is None: raise HTTPException( status_code=500, - detail={"error": CommonProxyErrors.no_llm_router.value}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "error": CommonProxyErrors.no_llm_router.value + }, ) resolved: Final = await _resolve_saved_routing_test(data, user_api_key_dict, llm_router) actor: Final = ( @@ -544,8 +550,8 @@ async def preview_auto_router_routing( ) request_data: Final[dict[str, object]] = { # mutable-ok: auth and routing enrich this request in place **resolved.wire_body(), - "metadata": {}, - "proxy_server_request": {"body": None}, + "metadata": {}, # mutable-ok: centralized auth and identity stamping share this metadata bucket + "proxy_server_request": {"body": None}, # mutable-ok: the snapshot owner fills this body in place } if member_team is not None and _models_this_test_can_call(resolved.complexity_router_config): @@ -591,13 +597,17 @@ 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={"error": f"Could not route this prompt: {e}"}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "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={"error": "The router made no decision for this prompt. Check that at least one tier has a model."}, + 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." + }, ) available_models: Final = await get_available_models_for_user( @@ -1467,7 +1477,8 @@ 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]) or () + await _query_raw(prisma_client, _ATTEMPT_COUNTS_SQL, [leg.id for leg in legs]) # mutable-ok: query param + or () ) return MappingProxyType({row.job_id: row for row in rows}) @@ -1553,21 +1564,33 @@ 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)}}) if tokens else () + await _verification_tokens(prisma_client).find_many( + where={"token": {"in": list(tokens)}} # mutable-ok: Prisma filter + ) + if tokens + else () ) team_rows: Final = ( - await _team_rows(prisma_client).find_many(where={"team_id": {"in": list(team_ids)}}) if team_ids else () + await _team_rows(prisma_client).find_many( + where={"team_id": {"in": list(team_ids)}} # mutable-ok: Prisma filter + ) + if team_ids + else () ) user_rows: Final = ( - await _user_rows(prisma_client).find_many(where={"user_id": {"in": list(user_ids)}}) if user_ids else () + await _user_rows(prisma_client).find_many( + where={"user_id": {"in": list(user_ids)}} # mutable-ok: Prisma filter + ) + if user_ids + else () ) labels: Final = _target_labels(key_rows or (), team_rows or (), user_rows or ()) return tuple( response.model_copy( - update={ + update={ # mutable-ok: pydantic update payload "targets": tuple( target.model_copy( - update={ + update={ # mutable-ok: pydantic update payload "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], } @@ -1591,7 +1614,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] + leg_ids: Final = [leg.id for leg in legs] # mutable-ok: query param by_tier: Final = _ATTEMPT_AGG_ROWS.validate_python( await _query_raw(prisma_client, _ATTEMPT_AGG_BY_TIER_SQL, leg_ids) or () ) @@ -1606,7 +1629,9 @@ async def _shadow_eval_results( ) verdicts_by_target: Final[Mapping[tuple[str, str], ShadowEvalSlice]] = MappingProxyType( { - target_by_leg[slice.group]: slice.model_copy(update={"group": target_by_leg[slice.group][1]}) + target_by_leg[slice.group]: slice.model_copy( + update={"group": target_by_leg[slice.group][1]} # mutable-ok: pydantic update payload + ) for slice in _slices(by_leg) } ) @@ -1689,17 +1714,23 @@ 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)}}) + await _verification_tokens(prisma_client).find_many( + where={"token": {"in": list(data.api_key_ids)}} # mutable-ok: Prisma filter + ) if data.api_key_ids else () ) team_rows: Final = ( - await _team_rows(prisma_client).find_many(where={"team_id": {"in": list(data.team_ids)}}) + await _team_rows(prisma_client).find_many( + where={"team_id": {"in": list(data.team_ids)}} # mutable-ok: Prisma filter + ) if data.team_ids else () ) user_rows: Final = ( - await _user_rows(prisma_client).find_many(where={"user_id": {"in": list(data.user_ids)}}) + await _user_rows(prisma_client).find_many( + where={"user_id": {"in": list(data.user_ids)}} # mutable-ok: Prisma filter + ) if data.user_ids else () ) @@ -1758,11 +1789,12 @@ 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) + await prisma_client.db.execute_raw(_SWEEP_FINISHED_JOBS_SQL, list(ids), target_type) # mutable-ok: query param claimed: Final = await _shadow_eval_jobs(prisma_client).find_many( - where={ - "OR": [ - {"target_type": target_type, "target_id": {"in": list(ids)}} for target_type, ids in requested_by_type + 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 ], "direction": data.direction, "stopped_at": None, @@ -1780,12 +1812,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 = { + shared_config: Final = { # mutable-ok: Prisma payload "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), - "models": list(data.models), + "router_names": list(data.router_names), # mutable-ok: Prisma payload + "models": list(data.models), # mutable-ok: Prisma payload "direction": data.direction, "baseline_model": data.baseline_model, "judge_model": data.judge_model, @@ -1802,8 +1834,8 @@ 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=[ - { + data=[ # mutable-ok: Prisma payload + { # mutable-ok: Prisma payload **shared_config, "id": leg_id, "target_type": target_type, @@ -1827,7 +1859,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], + data=[{"job_id": leg_id} for leg_id in leg_ids], # mutable-ok: Prisma payload skip_duplicates=True, ) except Exception as seed_err: # noqa: BLE001 # coverage is advisory; the job must still start @@ -1921,31 +1953,38 @@ 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}) or () + await _shadow_eval_jobs(prisma_client).find_many( + where={"group_id": job_id} # mutable-ok: Prisma filter + ) + 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] + leg_ids: Final = [leg.id for leg in legs] # mutable-ok: query param 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"}, - order={"created_at": "desc"}, + where={"job_id": {"in": leg_ids}, "outcome": "error"}, # mutable-ok: Prisma filter + order={"created_at": "desc"}, # mutable-ok: Prisma order ) 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={ + update={ # mutable-ok: pydantic update payload "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={"verdicts": verdicts_by_target.get((target.target_type, target.target_id))}) + target.model_copy( + update={ # mutable-ok: pydantic update payload + "verdicts": verdicts_by_target.get((target.target_type, target.target_id)) + } + ) for target in labeled[0].targets ), } @@ -1979,7 +2018,10 @@ 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}) or () + await _shadow_eval_jobs(prisma_client).find_many( + where={"group_id": job_id} # mutable-ok: Prisma filter + ) + or () ) if not legs: raise HTTPException(status_code=404, detail=f"No shadow eval job {job_id}") diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index f3564fcbc56..c2a0a41c3e2 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -275,7 +275,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 {} + return stored if stored is not None else {} # mutable-ok: payload pydantic validates into its own dict def update_breakdown_metrics( @@ -1122,7 +1122,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}) + provider_metrics = metrics.model_copy(update={"flat_cost": 0.0}) # mutable-ok: pydantic update payload provider = record.custom_llm_provider or "unknown" assign_metric_with_metadata(breakdown.providers, provider, provider_metrics) elif level == _GROUP_DATE_PROVIDER_API_KEY: @@ -1318,7 +1318,7 @@ def _fold_entity_rollups_sync( entity_metadata_field: Mapping[str, dict[str, object]] | None, # mutable-ok: shared field shape ) -> 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} + by_date: Final = {day.date.strftime("%Y-%m-%d"): day for day in results} # mutable-ok: local fold index for row in entity_rows: day = by_date.get(row.date) @@ -1443,7 +1443,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 {} + else {} # mutable-ok: matches the helper's dict return ) 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 77e5b0e5674..9d182d4e259 100644 --- a/litellm/proxy/management_endpoints/config_override_endpoints.py +++ b/litellm/proxy/management_endpoints/config_override_endpoints.py @@ -308,13 +308,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"}, - data={ - "create": { + where={"config_type": "cyberark"}, # mutable-ok: prisma upsert payload + data={ # mutable-ok: prisma upsert payload + "create": { # mutable-ok: prisma upsert payload "config_type": "cyberark", "config_value": config_value, }, - "update": { + "update": { # mutable-ok: prisma upsert payload "config_value": config_value, }, }, @@ -650,8 +650,8 @@ async def test_hashicorp_vault_connection( @router.post( "/config_overrides/cyberark", - tags=["Config Overrides"], - dependencies=[Depends(user_api_key_auth)], + tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata + dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata ) async def update_cyberark_config( config: CyberArkConfig, @@ -684,7 +684,9 @@ async def update_cyberark_config( # 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"}) + 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 if existing_record is not None and existing_record.config_value is not None: @@ -699,7 +701,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 != ""} # rebind-ok: "" means clear + config_data = {k: v for k, v in config_data.items() if v != ""} # mutable-ok: dict # 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 +757,7 @@ async def update_cyberark_config( litellm_changed_by=litellm_changed_by, ) - return { + return { # mutable-ok: JSON response payload "message": "CyberArk configuration updated successfully", "status": "success", } @@ -763,8 +765,8 @@ async def update_cyberark_config( @router.get( "/config_overrides/cyberark", - tags=["Config Overrides"], - dependencies=[Depends(user_api_key_auth)], + tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata + dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata response_model=ConfigOverrideSettingsResponse, ) async def get_cyberark_config( @@ -819,8 +821,8 @@ async def get_cyberark_config( @router.delete( "/config_overrides/cyberark", - tags=["Config Overrides"], - dependencies=[Depends(user_api_key_auth)], + tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata + dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata ) async def delete_cyberark_config( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection @@ -844,7 +846,9 @@ 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"}) + 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 if existing_record is not None and existing_record.config_value is not None: try: @@ -871,7 +875,7 @@ async def delete_cyberark_config( litellm_changed_by=litellm_changed_by, ) - return { + return { # mutable-ok: JSON response payload "message": "CyberArk configuration deleted successfully", "status": "success", } @@ -879,8 +883,8 @@ async def delete_cyberark_config( @router.post( "/config_overrides/cyberark/test_connection", - tags=["Config Overrides"], - dependencies=[Depends(user_api_key_auth)], + tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata + dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata ) async def test_cyberark_connection( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection @@ -915,7 +919,7 @@ async def test_cyberark_connection( try: async_client: Final = get_async_httpx_client( llm_provider=httpxSpecialProvider.SecretManager, - params={"ssl_verify": client.ssl_verify}, + params={"ssl_verify": client.ssl_verify}, # mutable-ok: httpx client params ) whoami_url: Final = f"{client.conjur_addr}/whoami" response: Final = await async_client.get(whoami_url, headers=headers) @@ -926,7 +930,7 @@ async def test_cyberark_connection( detail=f"CyberArk token validation failed: {e}", ) - return { + return { # mutable-ok: JSON response payload "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 f6c767cfbb6..cb376f286ec 100644 --- a/litellm/proxy/management_endpoints/cost_tracking_settings.py +++ b/litellm/proxy/management_endpoints/cost_tracking_settings.py @@ -532,19 +532,23 @@ async def update_block_requests_for_models_without_pricing( if prisma_client is None: raise HTTPException( status_code=500, - detail={"error": CommonProxyErrors.db_not_connected_error.value}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "error": CommonProxyErrors.db_not_connected_error.value + }, ) if store_model_in_db is not True: raise HTTPException( status_code=500, - detail={"error": "Set `'STORE_MODEL_IN_DB='True'` in your env to enable this feature."}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "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"] = {} + config["litellm_settings"] = {} # mutable-ok: config is a plain-dict payload for save_config config["litellm_settings"]["block_requests_for_models_without_pricing"] = request.enabled await proxy_config.save_config(new_config=config) @@ -556,7 +560,9 @@ 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={"error": f"Failed to update setting: {e!s}"}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping + "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 898c801d347..33c078274fb 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"], + tags=["Budget & Spend Tracking"], # mutable-ok: fastapi's decorator signature types tags as a list response_model=GatewayRequestActivityResponse, ) async def get_gateway_daily_activity( diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 4eeb9a9bd81..ee1ebcb5ce9 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -2821,7 +2821,7 @@ async def ui_view_users( if org_filter_ids is not None: where_conditions["organization_memberships"] = {"some": {"organization_id": {"in": org_filter_ids}}} - where: Final[Mapping[str, object]] = { + where: Final[Mapping[str, object]] = { # mutable-ok: prisma serializes `where`, keep it a plain dict key: value for key, value in (*where_conditions.items(), *_user_search_where(search).items()) if value is not None diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index d417ec1479f..2de9ddc2577 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -416,8 +416,8 @@ def _effective_key_for_generate(data: GenerateKeyRequest, now: datetime) -> Lite {field: value for field, value in requested.items() if field not in _KEY_METADATA_REQUEST_FIELDS} ) metadata: Final = data.metadata or MappingProxyType({}) - folded_metadata: Final = {**metadata, **metadata_fields} - columns: Final = handle_key_type(data, {**column_fields}) + folded_metadata: Final = {**metadata, **metadata_fields} # mutable-ok: encrypt_callback_vars needs a dict + columns: Final = handle_key_type(data, {**column_fields}) # mutable-ok: handle_key_type mutates in place expires: Final = ( now + timedelta(seconds=duration_in_seconds(duration=data.duration)) if data.duration is not None else None ) @@ -808,7 +808,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}) + raise HTTPException(status_code=400, detail={"error": error}) # mutable-ok: FastAPI detail contract def common_key_access_checks( @@ -2264,7 +2264,7 @@ async def generate_service_account_key_fn( if data.metadata is None or data.metadata.get("service_account_id") is None: service_account_id: Final = data.key_alias or str(uuid.uuid4()) - stamped_metadata: Final = { + stamped_metadata: Final = { # mutable-ok: GenerateKeyRequest.metadata is a plain dict field **(data.metadata or MappingProxyType({})), "service_account_id": service_account_id, } @@ -3002,13 +3002,17 @@ async def _validate_end_user_budget_id_change( if requested_budget_id is None or requested_budget_id == (existing_budget_id or ""): return if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value: - forbidden_detail: Final = {"error": "Only proxy admins can set end_user_budget_id on a key."} + forbidden_detail: Final = { # mutable-ok: FastAPI detail contract + "error": "Only proxy admins can set end_user_budget_id on a key." + } raise HTTPException(status_code=403, detail=forbidden_detail) if requested_budget_id == "": return budget_row: Final = await BudgetRepository(_require_prisma_client(prisma_client)).find_by_id(requested_budget_id) if budget_row is None: - missing_detail: Final = {"error": f"end_user_budget_id={requested_budget_id} does not match any budget."} + missing_detail: Final = { # mutable-ok: FastAPI detail contract + "error": f"end_user_budget_id={requested_budget_id} does not match any budget." + } raise HTTPException(status_code=400, detail=missing_detail) @@ -4543,7 +4547,7 @@ def metadata_json_with_limits( ) if metadata is None and not limits: return json.dumps(None) - merged: Final = {**(metadata or _NO_METADATA), **dict(limits)} + merged: Final = {**(metadata or _NO_METADATA), **dict(limits)} # mutable-ok: encrypt_callback_vars takes a dict return json.dumps(encrypt_callback_vars(merged)) @@ -6125,7 +6129,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 { + return { # mutable-ok: this is the JSON payload persisted to budget_limits' Json column, which requires a plain dict **window, "reset_at": new_reset_at.isoformat(), } @@ -6159,9 +6163,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)} + reset_payload: Final = {"budget_limits": json.dumps(reset_windows, default=str)} # mutable-ok: prisma data kwarg await VerificationTokenRepository(prisma_client).table.update( - where={"token": hashed_api_key}, + where={"token": hashed_api_key}, # mutable-ok: prisma where kwarg data=reset_payload, ) diff --git a/litellm/proxy/management_endpoints/management_v1/teams.py b/litellm/proxy/management_endpoints/management_v1/teams.py index 215a82c950c..eab641b2a27 100644 --- a/litellm/proxy/management_endpoints/management_v1/teams.py +++ b/litellm/proxy/management_endpoints/management_v1/teams.py @@ -27,7 +27,7 @@ router: Final = APIRouter(prefix=MANAGEMENT_V1_PREFIX) @router.post( "/teams/{team_id}/members/bulk_delete", - tags=["team management"], + tags=["team management"], # mutable-ok: FastAPI types `tags` as list[str], not Sequence dependencies=(Depends(user_api_key_auth), Depends(reject_unknown_query_params)), response_model=BulkTeamMemberDeleteResponse, ) @@ -99,7 +99,7 @@ async def bulk_delete_team_members_action( @router.post( "/teams/{team_id}/members/bulk_update", - tags=["team management"], + tags=["team management"], # mutable-ok: FastAPI types `tags` as list[str], not Sequence dependencies=(Depends(user_api_key_auth), Depends(reject_unknown_query_params)), response_model=BulkTeamMemberBudgetUpdateResponse, ) diff --git a/litellm/proxy/management_endpoints/management_v1/users.py b/litellm/proxy/management_endpoints/management_v1/users.py index fdece34d3a2..afe4482c9da 100644 --- a/litellm/proxy/management_endpoints/management_v1/users.py +++ b/litellm/proxy/management_endpoints/management_v1/users.py @@ -27,7 +27,7 @@ router: Final = APIRouter(prefix=MANAGEMENT_V1_PREFIX) @router.post( "/users/bulk", - tags=["Internal User management"], + tags=["Internal User management"], # mutable-ok: fastapi types tags as list[str | Enum] dependencies=(Depends(user_api_key_auth),), response_model=BulkNewUserResponse, ) @@ -110,7 +110,7 @@ async def bulk_create_users_route( @router.post( "/users/bulk_delete", - tags=["Internal User management"], + tags=["Internal User management"], # mutable-ok: FastAPI types `tags` as list[str], not Sequence dependencies=(Depends(user_api_key_auth), Depends(reject_unknown_query_params)), response_model=BulkDeleteUsersResponse, ) diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 0974a2a952d..e879b6daadd 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -303,7 +303,9 @@ if MCP_AVAILABLE: def raise_mcp_identifier_conflict(conflict: McpIdentifierConflict) -> NoReturn: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={"error": mcp_identifier_conflict_message(conflict)}, + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict + "error": mcp_identifier_conflict_message(conflict) + }, ) def warn_if_id_jag_server_outruns_sso(server_id: str | None, auth_type: MCPAuth | str | None) -> None: @@ -690,7 +692,7 @@ if MCP_AVAILABLE: if scopes_as_objects and all(isinstance(scope, str) and scope for scope in scopes_as_objects) else {} ) - preserved: Final = { + preserved: Final = { # mutable-ok: API response payload **{ key: value for key in MCP_ADMIN_CONFIG_CREDENTIAL_KEYS @@ -725,7 +727,7 @@ if MCP_AVAILABLE: if not _user_is_full_admin(user_api_key_dict): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict "error": "Proxy admin access required to revoke another user's MCP credential.", }, ) @@ -1461,7 +1463,9 @@ if MCP_AVAILABLE: ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={"error": "Admin access required to view MCP gateway sessions."}, + detail={ # mutable-ok: HTTPException detail must be a plain mapping to keep this route's {"error": ...} response shape + "error": "Admin access required to view MCP gateway sessions." + }, ) from litellm.proxy._experimental.mcp_server.server import ( get_mcp_gateway_sessions_report, @@ -1487,14 +1491,14 @@ if MCP_AVAILABLE: if not _user_is_full_admin(user_api_key_dict): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict "error": "Proxy admin access required to terminate MCP gateway sessions.", }, ) if session_id_prefix is None and user_id is None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict "error": "Provide session_id_prefix and/or user_id to select the sessions to terminate.", }, ) @@ -1919,7 +1923,7 @@ if MCP_AVAILABLE: if LitellmUserRoles.PROXY_ADMIN != user_api_key_dict.user_role: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict "error": "User does not have permission to import mcp servers. You can only import mcp servers if you are a PROXY_ADMIN." }, ) @@ -2510,7 +2514,7 @@ if MCP_AVAILABLE: if binding is not None and binding.mode == "enforce": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ + detail={ # mutable-ok: FastAPI exception detail requires a JSON-serializable dictionary "error": "oauth_identity_binding_enforced", "error_description": ( "Direct credential storage is disabled for this server: its OAuth identity " @@ -2715,7 +2719,7 @@ if MCP_AVAILABLE: ): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, - detail={ + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict "error": "Admin access required to view MCP server user credentials.", }, ) @@ -3013,7 +3017,7 @@ if MCP_AVAILABLE: if not relay_eligible: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ + detail={ # mutable-ok: FastAPI HTTPException detail requires a plain dict "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 0f0d156d649..a4050d40393 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -428,9 +428,9 @@ def _effective_complexity_router_config( if key in ("api_key", "api_base") and (key != "api_key" or same_base) } ) - return { + return { # mutable-ok: persisted JSON requires concrete nested dicts **incoming, - "jev_classifier_config": { + "jev_classifier_config": { # mutable-ok: json.dumps cannot serialize MappingProxyType **transport, **supplied, }, @@ -960,7 +960,7 @@ def _cost_map_entry(db_model: Deployment, incoming_model_info: Mapping[str, obje return MappingProxyType({}) -LoadedCatalog: TypeAlias = Callable[[], Mapping[str, Mapping[str, object]]] +LoadedCatalog: TypeAlias = Callable[[], Mapping[str, Mapping[str, object]]] # mutable-ok: Callable parameter syntax def _loaded_catalog_entry( @@ -2688,10 +2688,12 @@ async def update_model( "updated_by": user_api_key_dict.user_id or LITELLM_PROXY_ADMIN_NAME, } renamed_update: Final[PrismaCompatibleUpdateDBModel] = ( - {**base_update, "model_name": renamed_to} if renamed_to is not None else base_update + {**base_update, "model_name": renamed_to} # mutable-ok: Prisma serializes only concrete update dicts + if renamed_to is not None + else base_update ) _data: Final[PrismaCompatibleUpdateDBModel] = ( - { + { # mutable-ok: Prisma serializes only concrete update dicts **renamed_update, "model_info": deployment.model_info.model_copy( update=MappingProxyType({"member_auto_router": member_marker}) @@ -2983,8 +2985,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"], - dependencies=[Depends(user_api_key_auth)], + 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 ) async def preview_auto_router_classifier_prompt( request: AutoRouterClassifierPromptPreviewRequest, @@ -2995,7 +2997,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 {}) + labeled_tiers: Final = _validated_labeled_tiers(request.tier_labels or {}) # mutable-ok: Pydantic field default system_prompt: Final = ( custom_tier_classification_prompt( request.tier_definitions, @@ -3018,8 +3020,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"], - dependencies=[Depends(user_api_key_auth)], + 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 ) async def get_auto_router_classifier_default_prompt( context_window_size: int = DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE, diff --git a/litellm/proxy/management_endpoints/prompt_cache_prediction.py b/litellm/proxy/management_endpoints/prompt_cache_prediction.py index 441b05e3773..757880980c9 100644 --- a/litellm/proxy/management_endpoints/prompt_cache_prediction.py +++ b/litellm/proxy/management_endpoints/prompt_cache_prediction.py @@ -55,7 +55,7 @@ def _capacity_request_data( ) -> Mapping[str, object]: # The parsed-body cache retains only original top-level keys. Replay the # shared idempotent tag merges on limiter-only data when auth added metadata. - data: Final = dict(request_data) + data: Final = dict(request_data) # mutable-ok: the existing tag merge owners accept a dictionary out-param LiteLLMProxyRequestSetup.apply_client_tag_policy_pre_auth(http_request, data, caller) # pyright: ignore[reportUnknownMemberType] # legacy tag owner takes the validated capacity dictionary LiteLLMProxyRequestSetup.apply_key_tags_pre_auth(data, caller) # pyright: ignore[reportUnknownMemberType] # legacy tag owner merges trusted key tags into capacity metadata return MappingProxyType(data) @@ -63,7 +63,7 @@ def _capacity_request_data( @router.post( "/cost/predict-cache", - tags=["Cost Tracking"], + tags=["Cost Tracking"], # mutable-ok: FastAPI requires a list for OpenAPI tags response_model=CachePredictionResponse, ) async def predict_cache_cost( diff --git a/litellm/proxy/management_endpoints/prompt_caching_requests.py b/litellm/proxy/management_endpoints/prompt_caching_requests.py index ff99a78e407..41255bd49b8 100644 --- a/litellm/proxy/management_endpoints/prompt_caching_requests.py +++ b/litellm/proxy/management_endpoints/prompt_caching_requests.py @@ -127,7 +127,7 @@ def _request_result(row: _PromptCachingRow, llm_router: "Callable[[], Router | N @router.get( "/cost_optimization/prompt_caching/requests", - tags=["Cost Optimization"], + tags=["Cost Optimization"], # mutable-ok: FastAPI's route API requires a list response_model=PromptCachingRequestsResponse, ) async def get_prompt_caching_requests( diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 99e5f0a4b2a..0cf201b3a00 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -603,11 +603,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={ - "OR": [ - {"user_id": value}, - {"sso_user_id": subject}, - {"user_email": email}, + 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 ], }, take=2, @@ -2932,7 +2932,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}"}, + detail={"error": f"Group not found with ID: {group_id}"}, # mutable-ok: FastAPI detail contract ) # 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 ac6169d25bd..bc73a1e4104 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}) + return HTTPException(status_code=400, detail={"error": message}) # mutable-ok: FastAPI detail contract def _validate_team_callback(data: "AddTeamCallback") -> None: @@ -106,9 +106,10 @@ 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 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 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. """ if not callbacks.callback_vars: return @@ -229,7 +230,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}, + detail={"error": message}, # mutable-ok: the error response body is a JSON object ) @@ -347,7 +348,9 @@ 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 = [entry.get("callback_vars") or {} for entry in stored_entries] + stored_entry_vars: Final = [ # mutable-ok: read-only input to the checks, never stored + entry.get("callback_vars") or {} for entry in stored_entries + ] scope_error: Final = conflicting_span_scope_error(data.callback_vars, stored_entry_vars) if scope_error is not None: raise _callback_config_error(scope_error) @@ -392,7 +395,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}, + include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal ) if new_team_row is None: @@ -434,8 +437,8 @@ async def add_team_callbacks( @router.delete( "/team/{team_id:path}/callback/{callback_name}", - tags=["team management"], - dependencies=[Depends(user_api_key_auth)], + 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 response_model=TeamCallbackDeleteResponse, ) @management_endpoint_wrapper @@ -506,22 +509,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 = [ + remaining_callbacks: Final = [ # mutable-ok: metadata["logging"] is isinstance-checked for list downstream 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} + updated_metadata: Final = {**team_metadata, "logging": remaining_callbacks} # mutable-ok: persisted as JSON encrypted_metadata: Final[object] = 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}, - data={"metadata": team_metadata_json}, + 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 # `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}, + include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal ) if updated_team is None: @@ -649,7 +652,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"] = [] + team_metadata["logging"] = [] # mutable-ok: the disabled state is persisted as an empty JSON array encrypted_metadata: Final[object] = encrypt_callback_vars(team_metadata) team_metadata_json: Final = json.dumps(encrypted_metadata) @@ -660,7 +663,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}, + include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal ) if updated_team is None: diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 86b37f31090..a66d781dd61 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -2404,7 +2404,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 @@ -2937,7 +2937,7 @@ def _resolve_member_identity(member: Member, updated_users: Sequence[LiteLLM_Use None, ) return member.model_copy( - update={ + update={ # mutable-ok: pydantic update payload "user_id": resolved_user_id, "user_email": resolved_user_email, } @@ -3088,7 +3088,11 @@ async def _resolve_existing_member_user_ids( return frozenset() found: Final = await _user_id_rows_db(UserRepository(prisma_client)).find_many( - where={"user_id": {"in": sorted(requested_user_ids)}} + where={ # mutable-ok: Prisma query filters are dict-shaped + "user_id": { # mutable-ok: Prisma query filters are dict-shaped + "in": sorted(requested_user_ids) + } + } ) return frozenset(user.user_id for user in found or () if user.user_id is not None) @@ -3142,7 +3146,7 @@ def _validate_member_user_id_provisioning( remaining: Final = len(unknown_user_ids) - _MAX_REPORTED_UNKNOWN_USER_IDS raise HTTPException( status_code=403, - detail={ + detail={ # mutable-ok: HTTPException detail must be a plain mapping to keep this route's {"error": ...} response shape "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 " @@ -3159,7 +3163,7 @@ def _members_audit_value(team_alias: str | None, members: Sequence[Member]) -> s 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 "team_alias": team_alias, "members_with_roles": tuple(member.model_dump() for member in members), } @@ -3922,7 +3926,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} + detail: Final = {"error": message} # mutable-ok: HTTPException.detail takes a dict raise HTTPException(status_code=status_code, detail=detail) @@ -3956,7 +3960,7 @@ def _validate_team_member_reset_spend_value( @router.post( "/team/{team_id}/member/{user_id}/reset_spend", - tags=["team management"], + tags=["team management"], # mutable-ok: FastAPI's `tags` param is typed as list[str], not Sequence dependencies=(Depends(user_api_key_auth),), ) @management_endpoint_wrapper @@ -3993,10 +3997,12 @@ async def reset_team_member_spend_fn( team_access_denied() _check_not_resetting_own_spend(user_id=user_id, user_api_key_dict=user_api_key_dict) - membership_where: Final = {"user_id_team_id": {"user_id": user_id, "team_id": team_id}} + 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_row: Final = await _team_membership_db(prisma_client).find_unique( where=membership_where, - include={"litellm_budget_table": True}, + include={"litellm_budget_table": True}, # mutable-ok: prisma client requires a plain dict include= argument ) 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}.") @@ -4007,7 +4013,7 @@ async def reset_team_member_spend_fn( await _team_membership_db(prisma_client).update( where=membership_where, - data={"spend": reset_to}, + data={"spend": reset_to}, # mutable-ok: prisma client requires a plain dict data= argument ) await invalidate_team_member_spend_state( @@ -4017,7 +4023,7 @@ async def reset_team_member_spend_fn( new_spend=reset_to, ) - return { + return { # mutable-ok: matches this router's established untyped-response-dict convention "team_id": team_id, "user_id": user_id, "spend": reset_to, @@ -4041,7 +4047,7 @@ async def _existing_team_default_budget_id(team: LiteLLM_TeamTable, prisma_clien if budget_id is None: return None row: Final = await _budget_db(prisma_client).find_unique( - where={"budget_id": budget_id}, + where={"budget_id": budget_id}, # mutable-ok: prisma client requires a plain dict where= argument ) return budget_id if row is not None else None @@ -4054,7 +4060,7 @@ def _member_budget_source(budget_id: str | None, team_default_budget_id: str | N @router.post( "/team/{team_id}/member/{user_id}/reset_budget", - tags=["team management"], + tags=["team management"], # mutable-ok: FastAPI's `tags` param is typed as list[str], not Sequence dependencies=(Depends(user_api_key_auth),), response_model=TeamMemberResetBudgetResponse, ) @@ -4086,7 +4092,9 @@ async def reset_team_member_budget_fn( if not await get_team_access().allows(user_api_key_dict, team_obj, TEAM_OR_ORG_ADMIN): team_access_denied() - membership_where: Final = {"user_id_team_id": {"user_id": user_id, "team_id": team_id}} + 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_row: Final = await _team_membership_db(prisma_client).find_unique(where=membership_where) 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}.") @@ -4095,11 +4103,11 @@ async def reset_team_member_budget_fn( budget_link: Final = ( {"connect": {"budget_id": team_default_budget_id}} if team_default_budget_id is not None - else {"disconnect": True} + else {"disconnect": True} # mutable-ok: same prisma data= argument ) await _team_membership_db(prisma_client).update( where=membership_where, - data={"litellm_budget_table": budget_link}, + data={"litellm_budget_table": budget_link}, # mutable-ok: prisma client requires a plain dict data= argument ) await invalidate_team_member_spend_state( user_id=user_id, @@ -4497,7 +4505,9 @@ async def delete_team( ) for deleted_team in team_rows: - _emit_team_members_metric(deleted_team.model_copy(update={"members_with_roles": ()})) + _emit_team_members_metric( + deleted_team.model_copy(update={"members_with_roles": ()}) # mutable-ok: pydantic update payload + ) await sync_team_access_group_membership(prisma_client=prisma_client, team_id=deleted_team.team_id) return deleted_teams @@ -4769,7 +4779,15 @@ 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={"user_id": {"in": sorted(user_ids)}}) if user_ids else () + 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 () ) user_by_id: Final = MappingProxyType({u.user_id: u for u in user_rows}) @@ -4948,7 +4966,7 @@ async def team_info( members=resolved_team_info.members_with_roles, ) hydrated_team_info: Final = resolved_team_info.model_copy( - update={ + update={ # mutable-ok: pydantic update payload "members_with_roles": hydrated_members, "organization_models": organization_models, "model_max_budget_usage": await build_model_max_budget_usage( @@ -5222,7 +5240,7 @@ async def unblock_team( @router.get( "/team/metadata_schema", - tags=["team management"], + tags=["team management"], # mutable-ok: fastapi's decorator signature types tags as a list dependencies=(Depends(user_api_key_auth),), response_model=TeamMetadataSchemaResponse, ) @@ -6505,7 +6523,7 @@ 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}) + return HTTPException(status_code=status_code, detail={"error": message}) # mutable-ok: FastAPI JSON detail class _TeamDailyActivityScope(NamedTuple): @@ -6814,7 +6832,7 @@ class _TeamUserSpendDbRow(TypedDict): @router.get( "/team/spend/by_user", response_model=TeamUserSpendResponse, - tags=["team management"], + tags=["team management"], # mutable-ok: fastapi route tags must be a list ) 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 19444dfe33b..2a22077eb99 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -1526,7 +1526,9 @@ async def get_generic_sso_response( if generic_include_token_claims else response ) - received_response = {key: value for key, value in claims.items() if key not in _OAUTH_TOKEN_FIELDS} + 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 + } return generic_response_convertor( response=claims, jwt_handler=jwt_handler, @@ -1667,7 +1669,7 @@ async def get_generic_sso_response( return result or {}, received_response, access_token_payload, sso_assertion -RetentionCheck: TypeAlias = Callable[[], Awaitable[bool]] +RetentionCheck: TypeAlias = Callable[[], Awaitable[bool]] # mutable-ok: Callable parameter syntax async def warn_if_id_jag_assertion_uncaptured( diff --git a/litellm/proxy/management_helpers/auto_router_permissions.py b/litellm/proxy/management_helpers/auto_router_permissions.py index c8bb95eb3bb..449a1032b35 100644 --- a/litellm/proxy/management_helpers/auto_router_permissions.py +++ b/litellm/proxy/management_helpers/auto_router_permissions.py @@ -151,7 +151,9 @@ async def authorize_member_auto_router_dependencies( if team.blocked: raise HTTPException(status_code=403, detail="This auto router's team is blocked.") aliases: Final = team_model_aliases(team) - alias_dict: Final = dict(aliases) if aliases is not None else None + alias_dict: Final = ( + dict(aliases) if aliases is not None else None # mutable-ok: auth model and helpers require dict + ) scoped_actor: Final = user_api_key_dict.model_copy( update=MappingProxyType({"team_id": team.team_id, "team_models": team.models, "team_model_aliases": alias_dict}) ) diff --git a/litellm/proxy/management_helpers/bulk_user_creation.py b/litellm/proxy/management_helpers/bulk_user_creation.py index 9636acb4e1e..6c37018ff80 100644 --- a/litellm/proxy/management_helpers/bulk_user_creation.py +++ b/litellm/proxy/management_helpers/bulk_user_creation.py @@ -270,8 +270,8 @@ async def _existing_user_conflicts( if not user_ids: return frozenset(), frozenset() table: Final = _user_table(prisma_client) - id_filter: Final = {"user_id": {"in": user_ids}} - email_filter: Final = {"user_email": {"in": emails, "mode": "insensitive"}} + id_filter: Final = {"user_id": {"in": user_ids}} # mutable-ok: Prisma query filters are dict-shaped + email_filter: Final = {"user_email": {"in": emails, "mode": "insensitive"}} # mutable-ok: Prisma filter id_rows: Final = await table.find_many(where=id_filter) email_rows: Final = await table.find_many(where=email_filter) if emails else () return ( @@ -283,7 +283,9 @@ async def _existing_user_conflicts( async def _load_teams(prisma_client: PrismaClient, team_ids: frozenset[str]) -> Mapping[str, LiteLLM_TeamTable]: if not team_ids: return MappingProxyType({}) - rows: Final = await TeamRepository(prisma_client).table.find_many(where={"team_id": {"in": sorted(team_ids)}}) + rows: Final = await TeamRepository(prisma_client).table.find_many( + where={"team_id": {"in": sorted(team_ids)}} # mutable-ok: Prisma query filters are dict-shaped + ) return MappingProxyType({row.team_id: LiteLLM_TeamTable.model_validate(row.model_dump()) for row in rows}) @@ -336,8 +338,8 @@ def _db_failure( async def _prepare_user(user: _PendingUser, prisma_client: PrismaClient) -> _PreparedUser | _RowFailure: try: - dumped: Final = user.request.model_dump(exclude={"user_id"}) - data: Final = {**dumped, "user_id": user.user_id} + dumped: Final = user.request.model_dump(exclude={"user_id"}) # mutable-ok: pydantic IncEx takes a set + data: Final = {**dumped, "user_id": user.user_id} # mutable-ok: /user/new defaults helper mutates in place data_json: Final = _JSON_OBJECT.validate_python(_update_internal_new_user_params(data, user.request)) with_permission: Final = _JSON_OBJECT.validate_python( await _set_object_permission(data_json=data_json, prisma_client=prisma_client) @@ -433,7 +435,7 @@ async def _insert_users( verbose_proxy_logger.warning("/user/bulk_new: create_many failed, retrying rows individually", exc_info=True) outcome_unknown: Final = PrismaDBExceptionHandler.is_database_infrastructure_error(exc) requested: Final = frozenset(payload["user_id"] for payload in payloads) - landed_rows: Final = await table.find_many(where={"user_id": {"in": list(requested)}}) + landed_rows: Final = await table.find_many(where={"user_id": {"in": list(requested)}}) # mutable-ok: Prisma filter landed: Final = frozenset(row.user_id for row in landed_rows) # create_many is one INSERT: after a lost response the full set is ours, any partial set belongs to another request if outcome_unknown and landed == requested: @@ -561,7 +563,7 @@ async def _write_team_roster( *(Member(user_id=m.user_id, user_email=m.user_email, role=m.role) for m in new_members), ) await _team_tx_db(tx).update( - where={"team_id": team.team_id}, + where={"team_id": team.team_id}, # mutable-ok: Prisma query filters are dict-shaped data=_RosterData(members_with_roles=json.dumps(tuple(member.model_dump() for member in after))), ) return _TeamWrite( @@ -588,7 +590,7 @@ async def _detach_failed_teams( table: Final = _user_table(prisma_client) updates: Final = tuple( table.update( - where={"user_id": user.row.user_id}, + where={"user_id": user.row.user_id}, # mutable-ok: Prisma query filters are dict-shaped data=_TeamsData(teams=landed), ) for user in created @@ -684,7 +686,7 @@ async def _add_to_organizations( organization_id=organization_id, member=OrgMember(user_id=prepared.row.user_id, role=LitellmUserRoles.INTERNAL_USER), ), - http_request=Request(scope={"type": "http", "path": "/user/bulk_new"}), + http_request=Request(scope={"type": "http", "path": "/user/bulk_new"}), # mutable-ok: ASGI scopes are dicts user_api_key_dict=user_api_key_dict, ) @@ -708,7 +710,7 @@ async def _write_audit_logs( if not created: return created_ids: Final = sorted(user.row.user_id for user in created) - created_filter: Final = {"user_id": {"in": created_ids}} + created_filter: Final = {"user_id": {"in": created_ids}} # mutable-ok: Prisma query filters are dict-shaped rows: Final = await _user_table(prisma_client).find_many(where=created_filter) outcomes: Final = await _bounded( BULK_NEW_USER_CONCURRENCY, diff --git a/litellm/proxy/management_helpers/bulk_user_deletion.py b/litellm/proxy/management_helpers/bulk_user_deletion.py index 8286605e16a..b56dba3f179 100644 --- a/litellm/proxy/management_helpers/bulk_user_deletion.py +++ b/litellm/proxy/management_helpers/bulk_user_deletion.py @@ -135,19 +135,19 @@ def _forbidden(detail: str) -> ManagementProblem: def _in_filter(field: str, values: Iterable[str]) -> Mapping[str, object]: - return {field: {"in": sorted(values)}} + return {field: {"in": sorted(values)}} # mutable-ok: Prisma query filters are dict-shaped def _eq_filter(field: str, value: str) -> Mapping[str, object]: - return {field: value} + return {field: value} # mutable-ok: Prisma query filters are dict-shaped def _team_users_filter(team_id: str, user_ids: Iterable[str]) -> Mapping[str, object]: - return {"team_id": team_id, **_in_filter("user_id", user_ids)} + return {"team_id": team_id, **_in_filter("user_id", user_ids)} # mutable-ok: Prisma query filters are dict-shaped def _any_filter(*clauses: Mapping[str, object]) -> Mapping[str, object]: - return {"OR": clauses} + return {"OR": clauses} # mutable-ok: Prisma query filters are dict-shaped def _team_tx_db(tx: "Prisma") -> "TableActions[prisma_models.LiteLLM_TeamTable]": diff --git a/litellm/proxy/management_helpers/object_permission_utils.py b/litellm/proxy/management_helpers/object_permission_utils.py index c389381dca4..b12a689429d 100644 --- a/litellm/proxy/management_helpers/object_permission_utils.py +++ b/litellm/proxy/management_helpers/object_permission_utils.py @@ -362,7 +362,7 @@ async def reject_ambiguous_mcp_tool_permission_keys( return raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={ + detail={ # mutable-ok: HTTPException.detail has no immutable form; same shape as the sibling errors here "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 f3a97da1b12..31b7b68d233 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)}} + where: Final = {"server_id": {"in": tuple(wanted)}} # mutable-ok: prisma where is a dict 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)}} + where: Final = {"agent_id": {"in": tuple(wanted)}} # mutable-ok: prisma where is a dict 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))}} + where: Final = {"token": {"in": tuple(frozenset(tokens))}} # mutable-ok: prisma where is a dict 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 8bb32696857..76477ab2988 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={ + detail={ # mutable-ok: HTTPException.detail has no immutable form "error": f"custom_team_metadata_validate is an Enterprise feature. {CommonProxyErrors.not_premium_user.value}" }, ) @@ -120,7 +120,9 @@ async def run_team_metadata_validation( if not inspect.iscoroutinefunction(validator_call): raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail={"error": "custom_team_metadata_validate must be an async function"}, + detail={ # mutable-ok: HTTPException.detail has no immutable form + "error": "custom_team_metadata_validate must be an async function" + }, ) try: @@ -129,13 +131,15 @@ 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}, + detail={"error": unavailable_message}, # mutable-ok: HTTPException.detail has no immutable form ) if not result.valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail={"error": result.error_message or DEFAULT_TEAM_METADATA_VALIDATION_REJECTED_MESSAGE}, + detail={ # mutable-ok: HTTPException.detail has no immutable form + "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 c336b97349a..e347428be83 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( + rejected_counter=Counter( # mutable-ok: Prometheus requires runtime Counter construction "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"}, - content={ - "error": { + 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 "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 709845e0ffc..1db4474fc40 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}) + return HTTPException(status_code=400, detail={"error": message}) # mutable-ok: FastAPI detail shape def raise_public(failure: BatchScanFailure) -> NoReturn: @@ -388,7 +388,7 @@ 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)) + scan_input[injected] = copy.deepcopy(dict(scan_metadata)) # mutable-ok: guardrails write here try: # The chain hands back the body it produced, which may be a replacement for the dict it was @@ -421,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}), + text=json.dumps({**record.payload, "body": scanned}), # mutable-ok: json.dumps needs a plain dict ) diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 40478e75d7c..f6f91832603 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -1247,7 +1247,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)}} + where={"flat_model_file_ids": {"hasSome": sorted(raw_file_ids)}} # mutable-ok: prisma where is a plain dict ) 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 c7b597b584f..2dd8f013e75 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -219,7 +219,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": {}} + request_data: Final = {"litellm_metadata": {}} # mutable-ok: builder + litellm mutate this in place LiteLLMProxyRequestSetup.add_user_api_key_auth_to_request_metadata( data=request_data, user_api_key_dict=user_api_key_dict, @@ -444,8 +444,8 @@ def _fal_target(endpoint: str) -> httpx.URL: @router.api_route( "/fal_ai/{endpoint:path}", - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], - tags=["Fal AI Pass-through", "pass-through"], + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route metadata requires a list + tags=["Fal AI Pass-through", "pass-through"], # mutable-ok: FastAPI route metadata requires a list ) async def fal_ai_proxy_route( endpoint: str, @@ -602,8 +602,8 @@ async def mistral_proxy_route( @router.api_route( "/typesafe/{endpoint:path}", - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], - tags=["TypeSafe AI Pass-through", "pass-through"], + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route metadata requires a list + tags=["TypeSafe AI Pass-through", "pass-through"], # mutable-ok: FastAPI route metadata requires a list ) async def typesafe_proxy_route( endpoint: str, @@ -626,7 +626,7 @@ async def typesafe_proxy_route( endpoint_func: Final = create_pass_through_route( endpoint=endpoint, target=str(updated_url), - custom_headers={ + custom_headers={ # mutable-ok: pass-through request headers require a mutable mapping "Authorization": f"Bearer {typesafe_api_key}", "Content-Type": "application/json", }, @@ -638,8 +638,8 @@ async def typesafe_proxy_route( @router.api_route( "/openrouter/{endpoint:path}", - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], - tags=["OpenRouter Pass-through", "pass-through"], + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route metadata requires a list + tags=["OpenRouter Pass-through", "pass-through"], # mutable-ok: FastAPI route metadata requires a list ) async def openrouter_proxy_route( endpoint: str, @@ -662,7 +662,7 @@ async def openrouter_proxy_route( endpoint_func: Final = create_pass_through_route( endpoint=endpoint, target=str(updated_url), - custom_headers={ + custom_headers={ # mutable-ok: pass-through request headers require a mutable mapping "Authorization": f"Bearer {openrouter_api_key}", "Content-Type": "application/json", }, @@ -705,7 +705,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 = {} + extra_headers = {} # mutable-ok: dict for extra headers; rebind-ok: reassigned later from credentials base_target_url: str | None = None if not collection_name: raise HTTPException( @@ -1363,7 +1363,7 @@ def _resolve_aws_passthrough_region() -> str | None: @router.post( "/comprehendmedical/{operation}", - tags=["AWS Comprehend Medical Pass-through", "pass-through"], + tags=["AWS Comprehend Medical Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list ) async def comprehend_medical_proxy_route( operation: str, @@ -1440,7 +1440,7 @@ async def comprehend_medical_proxy_route( @router.post( "/comprehendmedical", - tags=["AWS Comprehend Medical Pass-through", "pass-through"], + tags=["AWS Comprehend Medical Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list ) async def comprehend_medical_sdk_proxy_route( request: Request, @@ -1524,8 +1524,8 @@ def canonical_azure_speech_endpoint_path(endpoint: str) -> str: @router.api_route( f"{AZURE_SPEECH_PASS_THROUGH_ROUTE_PREFIX}/{{endpoint:path}}", - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], - tags=["Azure AI Speech Pass-through", "pass-through"], + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: fastapi route methods must be a list + tags=["Azure AI Speech Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list ) async def azure_speech_proxy_route( endpoint: str, @@ -1610,7 +1610,7 @@ async def azure_speech_proxy_route( @router.post( "/transcribe/{operation}", - tags=["Amazon Transcribe Pass-through", "pass-through"], + tags=["Amazon Transcribe Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list ) async def transcribe_proxy_route( operation: str, @@ -1734,7 +1734,7 @@ async def transcribe_proxy_route( @router.post( "/transcribe", - tags=["Amazon Transcribe Pass-through", "pass-through"], + tags=["Amazon Transcribe Pass-through", "pass-through"], # mutable-ok: fastapi route tags must be a list ) async def transcribe_sdk_proxy_route( request: Request, @@ -3155,7 +3155,9 @@ 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 = {"Authorization": f"Bearer {openai_api_key}"} + custom_headers: Final = { # mutable-ok: websocket_passthrough_request requires a plain dict of upstream headers + "Authorization": f"Bearer {openai_api_key}" + } await websocket.accept(subprotocol=negotiated_subprotocol) @@ -3223,7 +3225,9 @@ async def deepgram_listen_websocket_route( await relay( websocket=websocket, target=target, - custom_headers={"Authorization": f"Token {deepgram_api_key}"}, + custom_headers={ # mutable-ok: websocket_passthrough_request requires a plain dict of upstream headers + "Authorization": f"Token {deepgram_api_key}" + }, user_api_key_dict=user_api_key_dict, forward_headers=False, endpoint=websocket.url.path, @@ -3425,8 +3429,8 @@ def _tinyfish_route_timeout() -> float | None: @router.api_route( "/tinyfish/{endpoint:path}", - methods=["GET", "POST"], - tags=["TinyFish Pass-through", "pass-through"], + methods=["GET", "POST"], # mutable-ok: fastapi api_route requires List[str] + tags=["TinyFish Pass-through", "pass-through"], # mutable-ok: fastapi api_route requires a list ) async def tinyfish_proxy_route( endpoint: str, @@ -3800,8 +3804,8 @@ def create_generic_websocket_passthrough_endpoint( @router.api_route( "/gigachat/{endpoint:path}", - methods=["GET", "POST", "PUT", "DELETE", "PATCH"], - tags=["Gigachat Pass-through", "pass-through"], + methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # mutable-ok: FastAPI route methods + tags=["Gigachat Pass-through", "pass-through"], # mutable-ok: FastAPI route tags ) async def gigachat_proxy_route( endpoint: str, @@ -3967,7 +3971,7 @@ async def handle_gigachat_passthrough_router_model( data["json"] = request_body data["custom_llm_provider"] = "gigachat" - keys: Final = [ + keys: Final = [ # mutable-ok: list of keys to remove from data "gigachat_auth_url", "gigachat_access_token", "gigachat_scope", @@ -3979,7 +3983,7 @@ async def handle_gigachat_passthrough_router_model( client: Final = get_async_httpx_client( llm_provider=LlmProviders.GIGACHAT, - params={ + params={ # mutable-ok: httpx client params "timeout": httpx.Timeout(timeout=600.0, connect=5.0), }, ) diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/azure_speech_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/azure_speech_passthrough_logging_handler.py index d98b1c93a34..33d1815b3c4 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/azure_speech_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/azure_speech_passthrough_logging_handler.py @@ -137,7 +137,7 @@ class AzureSpeechPassthroughLoggingHandler: url_route, httpx_response, response_body ) - updated_kwargs: Final = { + updated_kwargs: Final = { # mutable-ok: the logging pipeline requires a plain kwargs dict **kwargs, "model": model_name, "custom_llm_provider": AZURE_SPEECH_CUSTOM_LLM_PROVIDER, 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 7d289b80457..0d82cabdf36 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 = { + updated_kwargs: Final = { # mutable-ok: the logging pipeline requires a plain kwargs dict **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 6aef278963d..93fe3c5b31b 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,8 +118,10 @@ 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 = [part for part in _content_parts(message) if not _is_remote_high_detail_image(part)] - return {**message, "content": kept_parts} + 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 def count_relayed_prompt_tokens(model: str, messages: Sequence[Mapping[str, object]] | None) -> int: @@ -128,7 +130,9 @@ 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 = [_without_remote_high_detail_images(message) for message in messages] + local_messages: Final = [ # mutable-ok: token_counter takes a list of messages + _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/tinyfish_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/tinyfish_passthrough_logging_handler.py index e277655f1e1..a6c3cb669a6 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/tinyfish_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/tinyfish_passthrough_logging_handler.py @@ -310,13 +310,13 @@ class TinyFishPassthroughLoggingHandler: safe_run_id: Final = urllib.parse.quote(run_id, safe="") resolved_client: Final = client or get_async_httpx_client( llm_provider=httpxSpecialProvider.PassThroughEndpoint, - params={"timeout": 30.0}, + params={"timeout": 30.0}, # mutable-ok: get_async_httpx_client takes a plain dict of client params ) try: # screenshots=none keeps the poll payload small (no per-step screenshot URLs needed) response: Final = await resolved_client.get( f"{resolve_tinyfish_agent_api_base()}/v1/runs/{safe_run_id}?screenshots=none", - headers={"X-API-Key": api_key}, + headers={"X-API-Key": api_key}, # mutable-ok: httpx headers= takes a plain dict ) if not (200 <= response.status_code < 300): verbose_proxy_logger.warning( @@ -373,7 +373,7 @@ class TinyFishPassthroughLoggingHandler: kwargs: Mapping[str, object], ) -> _TinyfishLoggingPayload: response_cost: Final = _run_cost(run) - updated_kwargs: Final = { + updated_kwargs: Final = { # mutable-ok: the logging pipeline requires a plain kwargs dict **kwargs, "model": TINYFISH_MODEL_NAME, "custom_llm_provider": "tinyfish", @@ -383,7 +383,7 @@ class TinyFishPassthroughLoggingHandler: # the poller paths pass no request kwargs, so SLO attribution (key hash, team, tags) needs the stored params "litellm_params": kwargs.get("litellm_params") or logging_obj.model_call_details.get("litellm_params") - or {}, + or {}, # mutable-ok: the logging pipeline requires a plain kwargs dict } logging_obj.model_call_details.update( model=TINYFISH_MODEL_NAME, diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/transcribe_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/transcribe_passthrough_logging_handler.py index e08a600594e..b977cf3ccc1 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/transcribe_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/transcribe_passthrough_logging_handler.py @@ -64,8 +64,8 @@ TRANSCRIBE_MEDIA_BUCKETS_SETTING: Final = "transcribe_media_buckets" TRANSCRIBE_ROLE_MEMBERS: Final = ("DataAccessRoleArn", "JobExecutionSettings") TRANSCRIBE_MEDIA_URI_MEMBERS: Final = ("MediaFileUri", "RedactedMediaFileUri") -JobLookup: TypeAlias = Callable[[str], Awaitable[Mapping[str, object]]] -MediaDurationProbe: TypeAlias = Callable[[str, float], Awaitable[float | None]] +JobLookup: TypeAlias = Callable[[str], Awaitable[Mapping[str, object]]] # mutable-ok: Callable parameter syntax +MediaDurationProbe: TypeAlias = Callable[[str, float], Awaitable[float | None]] # mutable-ok: Callable parameter syntax class GetTranscriptionJobRequest(TypedDict): @@ -102,7 +102,7 @@ class MissingJob: StartedJob: TypeAlias = TranscriptionJobRecord | None -JobPricer: TypeAlias = Callable[[str, str, float, StartedJob], Awaitable[float]] +JobPricer: TypeAlias = Callable[[str, str, float, StartedJob], Awaitable[float]] # mutable-ok: Callable params class _PricedCostMapEntry(BaseModel): @@ -312,7 +312,7 @@ def transcribe_owned_start_request( 400, f"The {TRANSCRIBE_OWNER_TAG} tag is assigned by LiteLLM and cannot be supplied by the caller" ) owner_tag: Final = _JobTag(Key=TRANSCRIBE_OWNER_TAG, Value=owner).model_dump() - return {**request_body, "Tags": (*tags, owner_tag)} + return {**request_body, "Tags": (*tags, owner_tag)} # mutable-ok: json.dumps and the body state key take a dict async def transcribe_job_access_refusal( @@ -468,7 +468,7 @@ def transcribe_job_lookup(aws_region_name: str) -> JobLookup: headers=headers, ) client: Final = get_async_httpx_client(llm_provider=httpxSpecialProvider.PassThroughEndpoint) - signed_headers: Final = dict(prepped.headers.items()) + signed_headers: Final = dict(prepped.headers.items()) # mutable-ok: AsyncHTTPHandler.post takes a dict return _as_json_object(await client.post(str(prepped.url), data=payload, headers=signed_headers)) return get_job @@ -528,7 +528,7 @@ def transcribe_media_duration_probe(aws_region_name: str, download_slots: asynci aws_request: Final = AWSRequest(method="GET", url=url) credentials: Final = BaseAWSLLM().get_credentials(aws_region_name=aws_region_name) S3SigV4Auth(credentials, "s3", aws_region_name).add_auth(aws_request) - return dict(aws_request.prepare().headers.items()) + return dict(aws_request.prepare().headers.items()) # mutable-ok: httpx request headers take a dict async def media_seconds(media_uri: str, job_created_at: float) -> float | None: url: Final = s3_media_url(media_uri, aws_region_name) @@ -698,7 +698,7 @@ class TranscribePassthroughLoggingHandler: operation: Final = TranscribePassthroughLoggingHandler._operation_from_response(httpx_response) model_name: Final = f"{TRANSCRIBE_CUSTOM_LLM_PROVIDER}/{operation}" - updated_kwargs: Final = { + updated_kwargs: Final = { # mutable-ok: the logging pipeline requires a plain kwargs dict **kwargs, "model": model_name, "custom_llm_provider": TRANSCRIBE_CUSTOM_LLM_PROVIDER, diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py index 3ad92acb48a..887d17a7a20 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py @@ -89,7 +89,7 @@ class TypeSafePassthroughLoggingHandler: completion_tokens=output_tokens, total_tokens=input_tokens + output_tokens, ) - updated_kwargs: Final = { + updated_kwargs: Final = { # mutable-ok: pass-through logging contract requires mutable kwargs **kwargs, "model": model_name, "custom_llm_provider": custom_llm_provider, @@ -109,9 +109,9 @@ class TypeSafePassthroughLoggingHandler: logging_obj=logging_obj, status="success", ) - return { + return { # mutable-ok: pass-through logging contract requires mutable result "result": StandardPassThroughResponseObject(response=result), - "kwargs": { + "kwargs": { # mutable-ok: pass-through logging contract requires mutable kwargs **updated_kwargs, "standard_logging_object": standard_logging_object, }, 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 24c1b865db6..040250637ea 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 @@ -450,7 +450,7 @@ class VertexPassthroughLoggingHandler: standard_pass_through_response_object: Final[StandardPassThroughResponseObject] = { "response": json_response, } - return { + return { # mutable-ok: passthrough logging contract requires a concrete result dictionary "result": standard_pass_through_response_object, "kwargs": kwargs, } diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 9621063818d..2e7f9c4a41c 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -843,16 +843,16 @@ def _resolve_team_callback_wiring( logging_kwargs: Final = ( None if not callback_vars - else { + else { # mutable-ok: Logging arg **callback_vars, TRUSTED_CALLBACK_VARS_FIELD: callback_vars, - "metadata": {}, - "model_info": {}, + "metadata": {}, # mutable-ok: Logging arg + "model_info": {}, # mutable-ok: Logging arg } ) return _TeamCallbackWiring( - success_callbacks=None if success_callbacks is None else [*success_callbacks], - failure_callbacks=None if failure_callbacks is None else [*failure_callbacks], + 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 logging_kwargs=logging_kwargs, ) @@ -2221,7 +2221,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}}) + return json.dumps({**message, "setup": {**setup, "model": rewritten_model}}) # mutable-ok: one-shot json payload def _resolved_vertex_live_setup( @@ -2297,7 +2297,7 @@ def _with_trace_context(headers: Mapping[str, str], parent_span: object) -> dict try: from litellm.integrations.otel.plumbing.context import inject_trace_context except ImportError: - return dict(headers) + return dict(headers) # mutable-ok: matches inject_trace_context's carrier return type return inject_trace_context(headers, parent_span=parent_span) @@ -2343,7 +2343,7 @@ async def websocket_passthrough_request( await websocket.accept() verbose_proxy_logger.debug("WebSocket passthrough (%s): WebSocket connection accepted", endpoint) - forwarded_headers: Final = { + forwarded_headers: Final = { # mutable-ok: one-shot upstream header dict, read as a Mapping **custom_headers, **{ header_name: header_value diff --git a/litellm/proxy/policy_engine/pipeline_executor.py b/litellm/proxy/policy_engine/pipeline_executor.py index d80eb56d8e3..6c05ca0b22c 100644 --- a/litellm/proxy/policy_engine/pipeline_executor.py +++ b/litellm/proxy/policy_engine/pipeline_executor.py @@ -277,7 +277,7 @@ def _prepare_hook_input( pipeline may have already rewritten), same reason the normal sequential/parallel guardrail loops do this.""" if "metadata" not in data: - data["metadata"] = {} + data["metadata"] = {} # mutable-ok: request metadata bucket, hooks mutate it data["metadata"]["guardrails"] = [step.guardrail] scans_raw_request: Final = callback.scan_raw_request @@ -285,7 +285,7 @@ def _prepare_hook_input( 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] + hook_input.setdefault("metadata", {})["guardrails"] = [step.guardrail] # mutable-ok: request metadata shape return hook_input, scans_raw_request @@ -634,7 +634,7 @@ def _allow_result( restored: Final = _restore_request_guardrails(working_data, request_data) return PipelineExecutionResult( terminal_action="allow", - step_results=list(step_results), + step_results=list(step_results), # mutable-ok: PipelineExecutionResult field is a list modified_data=restored if restored != request_data else None, ) @@ -655,13 +655,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"} + stripped: Final = {k: v for k, v in working_metadata.items() if k != "guardrails"} # mutable-ok: request dict if original_guardrails is not None: - restored: Final = {**stripped, "guardrails": original_guardrails} - return {**working_data, "metadata": restored} + restored: Final = {**stripped, "guardrails": original_guardrails} # mutable-ok: request dict + return {**working_data, "metadata": restored} # mutable-ok: request dict if not stripped and not isinstance(request_metadata, dict): - return {k: v for k, v in working_data.items() if k != "metadata"} - return {**working_data, "metadata": stripped} + 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 _GUARDRAIL_INFORMATION_KEY: Final = "standard_logging_guardrail_information" diff --git a/litellm/proxy/policy_engine/response_retrieval.py b/litellm/proxy/policy_engine/response_retrieval.py index 654b1682582..0f373b08056 100644 --- a/litellm/proxy/policy_engine/response_retrieval.py +++ b/litellm/proxy/policy_engine/response_retrieval.py @@ -90,7 +90,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], + policy_names=[match["policy_name"] for match in matches], # mutable-ok: the matcher takes a list context=context, ) post_call_pipelines: Final = tuple( @@ -142,7 +142,9 @@ 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={policy_name: policy_sources[policy_name] for policy_name, _pipeline in added}, + policy_sources={ # mutable-ok: add_policy_sources_to_metadata takes a dict + 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 8926937468c..0e151199f41 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3807,7 +3807,7 @@ async def run_spend_counter_pipeline(pending: Sequence[PendingSpendIncrement]) - ] ) ttl: Final = redis_cache.get_ttl() - increment_list: Final = [ + increment_list: Final = [ # mutable-ok: async_increment_pipeline signature requires list[RedisPipelineIncrementOperation] RedisPipelineIncrementOperation(key=item.counter_key, increment_value=item.increment, ttl=ttl) for item in pending ] @@ -5094,7 +5094,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 = {} + model_info = {} # mutable-ok: fresh model_info stamped onto the raw yaml model dict 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( @@ -5376,7 +5376,7 @@ class ProxyConfig: ) def _config_with_resolved_settings(self, config: Mapping[str, object]) -> dict[str, object]: - return { + return { # mutable-ok: get_config preserves the mutable mapping contract used by existing loaders **config, **{ section: dict(store.resolved()) @@ -5638,7 +5638,7 @@ class ProxyConfig: ) if merged_section == existing_section: return None - serialized_section: Final = json.dumps(dict(merged_section)) + serialized_section: Final = json.dumps(dict(merged_section)) # mutable-ok: JSON encoder requires a dict config_data: Final[_ConfigParamUpsert] = { "create": {"param_name": section_name, "param_value": serialized_section}, "update": {"param_value": serialized_section}, @@ -5699,7 +5699,7 @@ class ProxyConfig: verbose_proxy_logger.warning("Maximum recursion depth (%s) reached while processing config.", max_depth) return config - return { + return { # mutable-ok: callers deep-copy and mutate this, and a mappingproxy cannot be deep-copied key: self._resolved_config_value(value=value, depth=depth, max_depth=max_depth) for key, value in config.items() } @@ -5708,7 +5708,7 @@ class ProxyConfig: if isinstance(value, dict): return self._check_for_os_environ_vars(config=value, depth=depth + 1, max_depth=max_depth) if isinstance(value, list): - return [ + return [ # mutable-ok: config values round-trip through json, where a tuple is not a list self._check_for_os_environ_vars(config=item, depth=depth + 1, max_depth=max_depth) if isinstance(item, dict) else item @@ -8457,7 +8457,7 @@ class ProxyConfig: await call_with_db_reconnect_retry( prisma_client, lambda: ConfigOverridesRepository(prisma_client).table.find_unique( - where={"config_type": "cyberark"} + where={"config_type": "cyberark"} # mutable-ok: prisma where clause ), reason="init_cyberark_config_override_lookup_failure", ), @@ -10420,7 +10420,9 @@ 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}) + row: Final = await config_table.find_unique( + where={"param_name": TUNING_BASELINE_PARAM_NAME} # mutable-ok: Prisma rejects mappingproxy input + ) if row is not None: stored: Final = row.param_value decoded: Final = json.loads(stored) if isinstance(stored, str) else stored @@ -10433,15 +10435,17 @@ class ProxyStartupEvent: snapshot: Final = snapshot_tuning_baselines(deployments) try: await config_table.create( - data={ + data={ # mutable-ok: Prisma rejects mappingproxy input "param_name": TUNING_BASELINE_PARAM_NAME, - "param_value": json.dumps(dict(snapshot)), + "param_value": json.dumps(dict(snapshot)), # mutable-ok: json only serializes concrete mappings } ) 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}) + competing_row: Final = await config_table.find_unique( + where={"param_name": TUNING_BASELINE_PARAM_NAME} # mutable-ok: Prisma rejects mappingproxy input + ) 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 @@ -11861,7 +11865,7 @@ async def model_info( llm_router=llm_router, ) response_id: Final = model_id if aliased_model_id else internal_to_public.get(resolved_model_id, model_id) - return {**response, "id": response_id} + return {**response, "id": response_id} # mutable-ok: response id differs def _blocked_response_usage(original_response: object | None) -> "litellm.Usage": @@ -14056,8 +14060,8 @@ class _ModelInfoLookupResponse(TypedDict): @router.get( "/utils/model_info", - tags=["llm utils"], - dependencies=[Depends(user_api_key_auth)], + tags=["llm utils"], # mutable-ok: FastAPI tags kwarg is list-typed + dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI dependencies kwarg is list-typed ) async def model_info_lookup(model: str, custom_llm_provider: str | None = None): """ @@ -14072,7 +14076,9 @@ async def model_info_lookup(model: str, custom_llm_provider: str | None = None): --header 'Authorization: Bearer sk-1234' ``` """ - detail: Final = {"error": f"model={model}, custom_llm_provider={custom_llm_provider} is not in the model cost map"} + detail: Final = { # mutable-ok: FastAPI serializes detail as a plain dict + "error": f"model={model}, custom_llm_provider={custom_llm_provider} is not in the model cost map" + } try: typed_model_info: Final = litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider) except Exception: @@ -16620,7 +16626,7 @@ async def alerting_settings( ) db_general_settings_dict: Final[Mapping[str, JsonValue]] = MappingProxyType( - dict(db_general_settings.param_value) + dict(db_general_settings.param_value) # mutable-ok: Prisma returns the JSON column as a plain dict if db_general_settings is not None and db_general_settings.param_value is not None else {} ) @@ -18567,7 +18573,7 @@ _GENERAL_SETTINGS_UI_LITELLM_FIELDS: Final[dict[str, GeneralSettingsUILiteLLMFie "breaks the cached prefix on every turn." ), }, - "budget_rollover": { + "budget_rollover": { # mutable-ok: registry literal, frozen with its siblings below "type": "Boolean", "description": ( "Carry spend beyond max_budget into the next window when budgets reset, instead of " @@ -20052,7 +20058,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"], + methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], # mutable-ok: FastAPI route methods ) async def proxy_mcp_route(request: Request) -> Response: """Serve the fixed three-tool MCP proxy surface.""" @@ -20067,7 +20073,7 @@ async def proxy_mcp_route(request: Request) -> Response: token: Final = _mcp_proxy_mode.set(True) try: - scope: Final = dict(request.scope) + scope: Final = dict(request.scope) # mutable-ok: ASGI scope rewrite 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 7814903e975..bba5ef681d0 100644 --- a/litellm/proxy/public_endpoints/public_endpoints.py +++ b/litellm/proxy/public_endpoints/public_endpoints.py @@ -557,7 +557,7 @@ async def get_autorouter_presets( @router.get( "/public/autorouter_presets", - tags=["public", "auto router"], + tags=["public", "auto router"], # mutable-ok: FastAPI route tags take a list 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 93971a84547..b0e688740e4 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"], + tags=["public", "model management"], # mutable-ok: fastapi types tags as list[str | Enum] 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"], + tags=["public", "model management"], # mutable-ok: fastapi types tags as list[str | Enum] 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 1a0eb5e7e43..974bff6338a 100644 --- a/litellm/proxy/rag_endpoints/endpoints.py +++ b/litellm/proxy/rag_endpoints/endpoints.py @@ -618,11 +618,11 @@ async def rag_ingest( raise HTTPException(status_code=400, detail={"error": str(e)}) managed_store: Final = resolved_stores.get(request_vector_store_config.get("vector_store_id")) - merged_vector_store_config: Final = { + merged_vector_store_config: Final = { # mutable-ok: ingestion classes mutate it when loading credentials **_caller_vector_store_options(request_vector_store_config, managed_store), **_managed_store_overrides(managed_store), } - merged_ingest_options: Final = { + merged_ingest_options: Final = { # mutable-ok: litellm.aingest takes a plain dict payload **ingest_options, "vector_store": merged_vector_store_config, } @@ -631,7 +631,7 @@ async def rag_ingest( if provider_error is not None: raise HTTPException( status_code=400, - detail={"error": provider_error}, + detail={"error": provider_error}, # mutable-ok: FastAPI serializes the detail as JSON ) # Add litellm data diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index 7badf0e79bb..c5d702ad65a 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -95,14 +95,14 @@ 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 = { + payload: Final = { # mutable-ok: tool entries are embedded verbatim in the JSON request body 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} + return {"type": tool_type, tool_type: payload} if to_chat else {"type": tool_type, **payload} # mutable-ok: same def _normalize_tool_dialect( @@ -117,7 +117,7 @@ def _normalize_tool_dialect( 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}} + return {**data, **{key: value for key, value in replaceable if key in data}} # mutable-ok: plain body dict def _is_chat_completions_body(data: Mapping[str, object]) -> bool: @@ -164,19 +164,19 @@ def _resolve_cursor_model_variant( 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} + resolved: Final = {**data, "model": variant.base_model} # mutable-ok: plain body dict 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} + return {**resolved, "reasoning_effort": variant.reasoning_effort} # mutable-ok: plain body dict reasoning: Final = data.get("reasoning") if isinstance(reasoning, dict): if reasoning.get("effort"): return resolved - return {**resolved, "reasoning": {**reasoning, "effort": variant.reasoning_effort}} - return {**resolved, "reasoning": {"effort": variant.reasoning_effort}} + return {**resolved, "reasoning": {**reasoning, "effort": variant.reasoning_effort}} # mutable-ok: same + return {**resolved, "reasoning": {"effort": variant.reasoning_effort}} # mutable-ok: plain body dict async def _resolve_cursor_model_variant_before_auth(request: Request) -> None: @@ -568,7 +568,9 @@ 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 = {key: value for key, value in raw_body.items() if key != "stream_options"} + 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" + } data: Final = _normalize_tool_dialect(body_without_stream_options, to_chat=False) diff --git a/litellm/proxy/roi_calculator/github.py b/litellm/proxy/roi_calculator/github.py index f5134b84336..997be03cdd4 100644 --- a/litellm/proxy/roi_calculator/github.py +++ b/litellm/proxy/roi_calculator/github.py @@ -298,7 +298,7 @@ async def _pages( async def _collect(items: AsyncIterator[_T]) -> tuple[_T, ...]: - collected: Final = [item async for item in items] + collected: Final = [item async for item in items] # mutable-ok: async iterables require an intermediate buffer return tuple(collected) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 323299f98fb..42ac74cae33 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -192,7 +192,7 @@ class MockTestingParamsDisabledError(HTTPException): def __init__(self, params: tuple[str, ...]): super().__init__( status_code=status.HTTP_400_BAD_REQUEST, - detail={ + detail={ # mutable-ok: HTTPException.detail has no immutable form; same shape as the sibling errors here "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` " diff --git a/litellm/proxy/spend_tracking/baseline_accounting.py b/litellm/proxy/spend_tracking/baseline_accounting.py index 263dc4de529..5980fb66211 100644 --- a/litellm/proxy/spend_tracking/baseline_accounting.py +++ b/litellm/proxy/spend_tracking/baseline_accounting.py @@ -158,7 +158,7 @@ def _usage_with_cache(usage: Usage, total: int, read: int, write_5m: int, write_ ), ) return Usage.model_validate( - { + { # mutable-ok: Usage only runs its normalizing constructor for a plain dictionary **usage.model_dump(), "prompt_tokens": total, "total_tokens": total + usage.completion_tokens, diff --git a/litellm/proxy/spend_tracking/budget_reservation.py b/litellm/proxy/spend_tracking/budget_reservation.py index 5eed1a894bc..c094e91c6c0 100644 --- a/litellm/proxy/spend_tracking/budget_reservation.py +++ b/litellm/proxy/spend_tracking/budget_reservation.py @@ -1079,7 +1079,7 @@ async def _reserve_counters( exc_info=True, ) await _release_applied_entries_best_effort( - entries=[entry], + entries=[entry], # mutable-ok: the release takes the reservation's list of entries default_reserved_cost=reservation_cost, ) return None @@ -1210,7 +1210,7 @@ async def _release_applied_entries_best_effort( for entry in entries: try: await _set_reserved_entries_actual_cost( - entries=[entry], + entries=[entry], # mutable-ok: the reconcile takes the reservation's list of entries actual_cost=0.0, default_reserved_cost=default_reserved_cost, ) diff --git a/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py b/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py index 99e5c35ae14..b8af432029f 100644 --- a/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py +++ b/litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py @@ -248,8 +248,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 = { - "team_id_date_api_key_model_custom_llm_provider_mcp_namespaced_tool_name_endpoint": { + 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 "team_id": team_id, "date": date_str, "api_key": PTU_SENTINEL_API_KEY, @@ -262,8 +262,8 @@ async def _upsert_ptu_daily_row( now: Final = datetime.now(timezone.utc) await _daily_team_spend_table(prisma_client).upsert( where=where, - data={ - "create": { + data={ # mutable-ok: prisma upsert data payload + "create": { # mutable-ok: prisma create payload "team_id": team_id, "date": date_str, "api_key": PTU_SENTINEL_API_KEY, @@ -274,7 +274,7 @@ async def _upsert_ptu_daily_row( "endpoint": "", "ptu_flat_cost": flat_cost, }, - "update": { + "update": { # mutable-ok: prisma update payload "model_group": model_name, "ptu_flat_cost": flat_cost, "updated_at": now, @@ -522,9 +522,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()} + date_range: Final = {"gte": start.isoformat(), "lte": end.isoformat()} # mutable-ok: prisma range filter rows: Final = await _daily_team_spend_table(prisma_client).find_many( - where={"api_key": PTU_SENTINEL_API_KEY, "date": date_range} + where={"api_key": PTU_SENTINEL_API_KEY, "date": date_range} # mutable-ok: prisma find filter ) return frozenset( ( @@ -748,11 +748,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 { + return { # mutable-ok: prisma delete filter "date": date_str, "api_key": PTU_SENTINEL_API_KEY, - "updated_at": {"lt": cutoff}, - "model": {"in": chunk}, + "updated_at": {"lt": cutoff}, # mutable-ok: prisma comparison filter + "model": {"in": chunk}, # mutable-ok: prisma membership filter } diff --git a/litellm/proxy/spend_tracking/spend_capture_rate.py b/litellm/proxy/spend_tracking/spend_capture_rate.py index bd804afa4ff..4536ea0ee42 100644 --- a/litellm/proxy/spend_tracking/spend_capture_rate.py +++ b/litellm/proxy/spend_tracking/spend_capture_rate.py @@ -42,7 +42,7 @@ if TYPE_CHECKING: OPENAI_BILLED_LITELLM_PROVIDERS: Final = ("openai", "text-completion-openai") -CaptureRatePublisher: TypeAlias = Callable[[SpendCaptureProvider, float | None], None] +CaptureRatePublisher: TypeAlias = Callable[[SpendCaptureProvider, float | None], None] # mutable-ok: Callable params _CAPTURED_SPEND_BY_DAY_SQL: Final = """ SELECT date, COALESCE(SUM(spend), 0)::float AS spend diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 7cd82e33f6e..939026f56c7 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1198,7 +1198,7 @@ async def get_global_activity_exceptions( @router.get( "/spend/capture_rate", - tags=["Budget & Spend Tracking"], + tags=["Budget & Spend Tracking"], # mutable-ok: FastAPI tags kwarg is list-typed dependencies=(Depends(user_api_key_auth),), response_model=CaptureRateReport, ) @@ -3102,13 +3102,13 @@ async def _fetch_session_representatives( prisma_client, rep_query, *sql_params, - [session_key for session_key, _ in session_keys], - [api_key for _, api_key in session_keys], + [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 ) rep_by_key: Final[Mapping[tuple[str, str], dict[str, object]]] = MappingProxyType( # mutable-ok: same rows {(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] + return [rep_by_key[key] for key in session_keys if key in rep_by_key] # mutable-ok: rows are enriched in place async def _count_grouped_sessions( @@ -3231,7 +3231,7 @@ async def _ui_session_grouped_spend_logs( session_keys=session_keys, ) if session_keys - else [] + else [] # mutable-ok: downstream enrichment mutates rows in place ) _hydrate_spend_log_metadata(data) @@ -3246,7 +3246,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} + return {**response, "next_session_cursor": next_cursor, "has_more": has_more} # mutable-ok: FastAPI response body class RequestResponsePayload(NamedTuple): @@ -3568,7 +3568,9 @@ 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]] = { + filter_query: Final[ + dict[str, object] + ] = { # mutable-ok: legacy filters are extended for optional parameters "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/tracing_endpoints.py b/litellm/proxy/tracing_endpoints.py index b2b10ebfbc6..06dbf359ba9 100644 --- a/litellm/proxy/tracing_endpoints.py +++ b/litellm/proxy/tracing_endpoints.py @@ -23,7 +23,7 @@ from litellm.tracing import ( from litellm.tracing.decode import InvalidOTLPPayloadError, encode_otlp_response from litellm.tracing.types import SpanDetail, Trace, TracePage, TraceScope -router = APIRouter(tags=["agent tracing"]) +router = APIRouter(tags=["agent tracing"]) # mutable-ok: FastAPI copies the mutable tags list MS_PER_DAY: Final = 24 * 60 * 60 * 1000 _ADMIN_ROLES: Final = (LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY) @@ -91,7 +91,7 @@ async def ingest_otlp_traces( except RuntimeError: raise HTTPException( status_code=503, - headers={"Retry-After": str(OTLP_RETRY_AFTER_SECONDS)}, + headers={"Retry-After": str(OTLP_RETRY_AFTER_SECONDS)}, # mutable-ok: FastAPI requires dict headers ) body, media_type = encode_otlp_response(content_type) return Response(content=body, media_type=media_type) diff --git a/litellm/proxy/ui_crud_endpoints/latest_release_endpoints.py b/litellm/proxy/ui_crud_endpoints/latest_release_endpoints.py index 2e90d4c0d90..ad5cc8efc31 100644 --- a/litellm/proxy/ui_crud_endpoints/latest_release_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/latest_release_endpoints.py @@ -133,8 +133,8 @@ async def get_latest_release_info( @router.get( "/get/latest_release_info", - tags=["UI Settings"], - dependencies=[Depends(user_api_key_auth)], + 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 response_model=LatestReleaseInfo | None, ) async def latest_release_info( diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 610d47990c3..227f0e7f795 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -333,8 +333,8 @@ class UISettings(BaseModel): "Empty means team admins cannot edit team settings or manage projects at all. " "Proxy admins and org admins are not affected." ), - json_schema_extra={ - "items": {"type": "string", "enum": [*_TEAM_ADMIN_FIELD_ENUM]}, + json_schema_extra={ # mutable-ok: pydantic only merges json_schema_extra when it is a plain dict + "items": {"type": "string", "enum": [*_TEAM_ADMIN_FIELD_ENUM]}, # mutable-ok: nested in the dict above }, ) @@ -597,11 +597,11 @@ async def get_allowed_ips(): def _store_allowed_ips(general_settings: MutableMapping[str, object], allowed_ips: Sequence[str]) -> None: try: - general_settings["allowed_ips"] = list(allowed_ips) + general_settings["allowed_ips"] = list(allowed_ips) # mutable-ok: compared against the file's own list except ConfigOwnedKeyError as owned: raise HTTPException( status_code=400, - detail={ + detail={ # mutable-ok: HTTPException serializes its detail as json "error": str(owned), "keys": (owned.key,), "section": owned.section, @@ -952,7 +952,9 @@ async def _validate_default_organization_exists(organization_id: str) -> None: if prisma_client is None: raise HTTPException( status_code=500, - detail={"error": "Database not connected. Please connect a database."}, + detail={ # mutable-ok: HTTPException detail must be a plain dict for FastAPI JSON serialization + "error": "Database not connected. Please connect a database." + }, ) organization_exists: Final = await OrganizationRepository(prisma_client).exists( @@ -961,7 +963,7 @@ async def _validate_default_organization_exists(organization_id: str) -> None: if not organization_exists: raise HTTPException( status_code=400, - detail={ + detail={ # mutable-ok: HTTPException detail must be a plain dict for FastAPI JSON serialization "error": f"Organization not found: {organization_id}. " "An organization must exist before it can be set as the default organization for new teams." }, @@ -1613,8 +1615,8 @@ async def update_websearch_interception_settings( @router.get( "/get/mcp_tool_search_settings", - tags=["Settings"], - dependencies=[Depends(user_api_key_auth)], + 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 response_model=MCPToolSearchSettingsResponse, ) async def get_mcp_tool_search_settings( @@ -1639,8 +1641,8 @@ async def get_mcp_tool_search_settings( @router.patch( "/update/mcp_tool_search_settings", - tags=["Settings"], - dependencies=[Depends(user_api_key_auth)], + 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 ) async def update_mcp_tool_search_settings( settings: MCPToolSearchSettings, @@ -1882,7 +1884,7 @@ async def update_ui_settings( if unsupported_team_fields: raise HTTPException( status_code=400, - detail={ + detail={ # mutable-ok: HTTPException detail must be a plain dict for FastAPI JSON serialization "error": ( f"{TEAM_ADMIN_EDITABLE_TEAM_FIELDS_SETTING} does not support {unsupported_team_fields}. " f"Supported fields: {sorted(SUPPORTED_TEAM_ADMIN_PERMISSIONS)}." diff --git a/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py b/litellm/proxy/ui_crud_endpoints/user_banner_endpoints.py index 0ff61592d9f..893fda797cd 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"], - dependencies=[Depends(user_api_key_auth)], + 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 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"], + tags=["UI Settings"], # mutable-ok: FastAPI's route decorator only accepts a list response_model=UpdateUserBannerResponse, ) async def update_user_banner( diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 4b7e11e63cc..c0e36e6e172 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -685,7 +685,9 @@ def _without_names( claimed: Final = bucket.get(slot) if not isinstance(claimed, list): return - remaining: Final = [name for name in claimed if name not in names] + 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 + ] if remaining: bucket[slot] = remaining # rebind-ok: the slot lives in the shared request-state dict, rewritten in place else: @@ -710,7 +712,9 @@ def _withdraw_deferred_claims( sources: Final = bucket.get("policy_sources") if not isinstance(sources, dict): return - remaining_sources: Final = {name: reason for name, reason in sources.items() if name not in withdrawn_policies} + 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 + } if remaining_sources: bucket["policy_sources"] = remaining_sources else: @@ -999,7 +1003,7 @@ def _stamp_deployment_attribution( if "model_info" not in attribution: return attribution if litellm_params.get("metadata") is None: - litellm_params["metadata"] = {} + litellm_params["metadata"] = {} # mutable-ok: legacy logging payload is populated in place metadata: Final = litellm_params["metadata"] if not isinstance(metadata, dict): return attribution @@ -1055,12 +1059,14 @@ def _deployment_attribution_for_model_group(model_group: object, team_id: str | { **({"custom_llm_provider": shared_provider} if shared_provider is not None else {}), **( - { - "model_info": dict(single_deployment.get("model_info") or {}), + { # mutable-ok: frozen immediately by the outer MappingProxyType + "model_info": dict( # mutable-ok: preserve the router's mutable model-info payload + single_deployment.get("model_info") or {} + ), "deployment": single_deployment_params["model"], } if single_deployment is not None and single_deployment_params is not None - else {} + else {} # mutable-ok: frozen immediately by the outer MappingProxyType ), } ) @@ -1524,7 +1530,7 @@ class ProxyLogging: *TypeAdapter(tuple[object, ...]).validate_python(synthetic_metadata.get("guardrails") or ()), *TypeAdapter(tuple[object, ...]).validate_python(parent_metadata.get("guardrails") or ()), ) - synthetic_metadata["guardrails"] = [ + synthetic_metadata["guardrails"] = [ # mutable-ok: existing guardrail selection and policy hooks require a list selection for index, selection in enumerate(merged_guardrails) if selection not in merged_guardrails[:index] ] return synthetic_data @@ -2331,7 +2337,7 @@ class ProxyLogging: caps: Final = ProxyLogging._callback_capabilities() if caps.has_content_enforcer: return True - probe: Final = {"metadata": dict(request_metadata)} + probe: Final = {"metadata": dict(request_metadata)} # mutable-ok: should_run_guardrail takes a dict return any( isinstance(callback, CustomGuardrail) and callback.should_run_guardrail(data=probe, event_type=GuardrailEventHooks.pre_call) @@ -3387,9 +3393,11 @@ class ProxyLogging: optional_params=_optional_params, litellm_params=_litellm_params, **( - {"custom_llm_provider": attribution["custom_llm_provider"]} + { # mutable-ok: frozen immediately by keyword expansion + "custom_llm_provider": attribution["custom_llm_provider"] + } if "custom_llm_provider" in attribution - else {} + else {} # mutable-ok: frozen immediately by keyword expansion ), ) @@ -4386,7 +4394,9 @@ class PrismaClient: spend_log_write_lock = asyncio.Lock() tool_usage_transactions: list["ToolUsageTransaction"] = [] _tool_usage_transactions_lock = asyncio.Lock() - autorouter_turn_transactions: ClassVar[list["AutoRouterTurnTransaction"]] = [] + autorouter_turn_transactions: ClassVar[ + list["AutoRouterTurnTransaction"] + ] = [] # mutable-ok: drained queue, mirrors tool_usage_transactions _autorouter_turn_transactions_lock = asyncio.Lock() # How long a health probe failure waits for an in-flight planned engine @@ -4403,7 +4413,9 @@ class PrismaClient: http_client: "HttpConfig | None" = None, ): ## init logging object - self.baseline_accounting_transactions: list[BaselineAccountingRecord] = [] + self.baseline_accounting_transactions: list[ + BaselineAccountingRecord + ] = [] # mutable-ok: locked background queue self.baseline_accounting_lock: Final = asyncio.Lock() self.proxy_logging_obj = proxy_logging_obj self.token_auth: DatabaseTokenAuth | None = resolve_database_token_auth() @@ -8638,7 +8650,7 @@ async def get_available_models_for_user( ) if agent_visible is None: return all_models - capped: Final = [m for m in all_models if m in agent_visible] + capped: Final = [m for m in all_models if m in agent_visible] # mutable-ok: callers expect the list all_models is return capped diff --git a/litellm/repositories/autorouter_session_repository.py b/litellm/repositories/autorouter_session_repository.py index 82e2c091728..d05ef9421ca 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}, - order={"last_turn_at": "desc"}, + 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 ) return self._to_model(record) diff --git a/litellm/repositories/chunked_in.py b/litellm/repositories/chunked_in.py index 7cd4d10c8e5..d16cb7c991c 100644 --- a/litellm/repositories/chunked_in.py +++ b/litellm/repositories/chunked_in.py @@ -67,10 +67,10 @@ def _filters_field(where: Mapping[str, object], field: str) -> bool: def _chunk_filter(field: str, chunk: tuple[Hashable, ...], where: Mapping[str, object] | None) -> Mapping[str, object]: - membership: Final = {field: {"in": list(chunk)}} + membership: Final = {field: {"in": list(chunk)}} # mutable-ok: the dict and list a hand-written filter sends if where is None: return membership - return {"AND": (dict(where), membership)} + return {"AND": (dict(where), membership)} # mutable-ok: prisma's query builder only accepts dict filters async def _each_chunk( @@ -127,7 +127,7 @@ async def update_many_in( raise ChunkedFieldWriteError( f"`data` writes `{field}`, the chunked field; a row it moves can match a later chunk" ) - payload: Final = dict(data) + payload: Final = dict(data) # mutable-ok: prisma's query builder only accepts dict payloads return sum( await _each_chunk(field, values, where, lambda chunk: table.update_many(data=payload, where=chunk), chunk_size) ) diff --git a/litellm/repositories/managed_batch_repository.py b/litellm/repositories/managed_batch_repository.py index 7ff44194d98..3f85251fdbd 100644 --- a/litellm/repositories/managed_batch_repository.py +++ b/litellm/repositories/managed_batch_repository.py @@ -27,8 +27,8 @@ class ManagedBatchRepository(PrismaTableRepository["prisma_models.LiteLLM_Manage self, batch: LiteLLMBatch, unchanged: Mapping[str, object], updated_by: str | None ) -> bool: updated_rows: Final = await self.table.update_many( - where={"unified_object_id": batch.id, **unchanged}, - data={ + where={"unified_object_id": batch.id, **unchanged}, # mutable-ok: prisma filters are plain dicts + data={ # mutable-ok: prisma payloads are plain dicts "file_object": batch.model_dump_json(), "status": batch.status, "updated_by": updated_by, @@ -38,9 +38,11 @@ class ManagedBatchRepository(PrismaTableRepository["prisma_models.LiteLLM_Manage async def touch(self, unified_batch_id: str, updated_by: str | None) -> None: await self.table.update_many( - where={"unified_object_id": unified_batch_id}, - data={"updated_by": updated_by}, + where={"unified_object_id": unified_batch_id}, # mutable-ok: prisma filters are plain dicts + data={"updated_by": updated_by}, # mutable-ok: prisma payloads are plain dicts ) async def _find_row(self, unified_batch_id: str) -> "prisma_models.LiteLLM_ManagedObjectTable | None": - return await self.table.find_first(where={"unified_object_id": unified_batch_id}) + return await self.table.find_first( + where={"unified_object_id": unified_batch_id} # mutable-ok: prisma filters are plain dicts + ) diff --git a/litellm/repositories/managed_file_content_repository.py b/litellm/repositories/managed_file_content_repository.py index ba80f131ed2..c55d0060080 100644 --- a/litellm/repositories/managed_file_content_repository.py +++ b/litellm/repositories/managed_file_content_repository.py @@ -12,12 +12,14 @@ class ManagedFileContentRepository(PrismaTableRepository["prisma_models.LiteLLM_ async def store(self, content: bytes) -> str: from prisma import Base64 - row: Final = await self.table.create(data={"content": Base64.encode(content)}) + row: Final = await self.table.create( + data={"content": Base64.encode(content)} # mutable-ok: prisma payloads are plain dicts + ) return row.id async def load(self, row_id: str) -> bytes | None: row: Final[prisma_models.LiteLLM_ManagedFileContentTable | None] = await self.table.find_unique( - where={"id": row_id} + where={"id": row_id} # mutable-ok: prisma filters are plain dicts ) return None if row is None else row.content.decode() @@ -25,6 +27,6 @@ class ManagedFileContentRepository(PrismaTableRepository["prisma_models.LiteLLM_ from prisma.errors import RecordNotFoundError try: - await self.table.delete(where={"id": row_id}) + await self.table.delete(where={"id": row_id}) # mutable-ok: prisma filters are plain dicts except RecordNotFoundError: return diff --git a/litellm/repositories/model_repository.py b/litellm/repositories/model_repository.py index cbacb6e8f90..8ee76b93923 100644 --- a/litellm/repositories/model_repository.py +++ b/litellm/repositories/model_repository.py @@ -97,7 +97,9 @@ 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}}) + records: Final = await self.table.find_many( + where={"model_id": {"not": model_id}} # mutable-ok: Prisma requires plain dicts for query serialization + ) 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 10f092c1cb2..c09e5eb75d4 100644 --- a/litellm/repositories/unit_of_work.py +++ b/litellm/repositories/unit_of_work.py @@ -25,8 +25,8 @@ from litellm.repositories.prisma_protocols import BatchTable, PrismaBatch def _spend_reset_data(budget_reset_at: datetime | None, spend_decrement: float) -> Mapping[str, object]: - spend: Final[object] = {"decrement": spend_decrement} - return {"spend": spend, "budget_reset_at": budget_reset_at} + spend: Final[object] = {"decrement": spend_decrement} # mutable-ok: prisma update payload must be a dict + return {"spend": spend, "budget_reset_at": budget_reset_at} # mutable-ok: prisma update payload must be a dict @dataclass(frozen=True, slots=True) @@ -35,7 +35,7 @@ class KeySpendResetWrites: def queue_spend_reset(self, token: str, budget_reset_at: datetime | None, spend_decrement: float) -> None: self.table.update( - where={"token": token}, + where={"token": token}, # mutable-ok: prisma where filter must be a dict data=_spend_reset_data(budget_reset_at, spend_decrement), ) @@ -46,7 +46,7 @@ class UserSpendResetWrites: def queue_spend_reset(self, user_id: str, budget_reset_at: datetime | None, spend_decrement: float) -> None: self.table.update( - where={"user_id": user_id}, + where={"user_id": user_id}, # mutable-ok: prisma where filter must be a dict data=_spend_reset_data(budget_reset_at, spend_decrement), ) @@ -57,7 +57,7 @@ class TeamSpendResetWrites: def queue_spend_reset(self, team_id: str, budget_reset_at: datetime | None, spend_decrement: float) -> None: self.table.update( - where={"team_id": team_id}, + where={"team_id": team_id}, # mutable-ok: prisma where filter must be a dict data=_spend_reset_data(budget_reset_at, spend_decrement), ) @@ -74,7 +74,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}}, + data={"spend": {"decrement": amount}}, # mutable-ok: prisma update payload must be a dict ) diff --git a/litellm/repositories/user_banner_repository.py b/litellm/repositories/user_banner_repository.py index 4e113dff988..c1ed977e048 100644 --- a/litellm/repositories/user_banner_repository.py +++ b/litellm/repositories/user_banner_repository.py @@ -12,12 +12,14 @@ 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}) + db_record: Final = await self.table.find_unique( + where={"id": USER_BANNER_ROW_ID} # mutable-ok: prisma filters are plain dicts + ) 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} + row: Final = {"id": USER_BANNER_ROW_ID, "ui_settings": payload} # mutable-ok: prisma rows are plain dicts await self.table.upsert( - where={"id": USER_BANNER_ROW_ID}, - data={"create": row, "update": {"ui_settings": payload}}, + 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 ) diff --git a/litellm/repositories/user_repository.py b/litellm/repositories/user_repository.py index b201dbc566b..7bf516a2bc1 100644 --- a/litellm/repositories/user_repository.py +++ b/litellm/repositories/user_repository.py @@ -85,8 +85,8 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]): pages: Final = tuple( [ await self.find_many( - where={ - "user_email": { + where={ # mutable-ok: Prisma query filters are dict-shaped + "user_email": { # mutable-ok: Prisma query filters are dict-shaped # bounded-ok: sliced to IN_LIST_CHUNK_SIZE values per statement "in": unique[start : start + IN_LIST_CHUNK_SIZE], "mode": "insensitive", @@ -261,8 +261,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}, - data={"user_email": user_email}, + 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 ) return updated_count diff --git a/litellm/responses/litellm_completion_transformation/session_handler.py b/litellm/responses/litellm_completion_transformation/session_handler.py index 1d6a47d6365..f749977eb82 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], + [_response_input_param], # mutable-ok: a lone input item still has to arrive as a list ) 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 [] + return [] # mutable-ok: an empty result the caller only reads diff --git a/litellm/responses/litellm_completion_transformation/streaming_iterator.py b/litellm/responses/litellm_completion_transformation/streaming_iterator.py index 3d6e4bf25d3..21a33c17ab8 100644 --- a/litellm/responses/litellm_completion_transformation/streaming_iterator.py +++ b/litellm/responses/litellm_completion_transformation/streaming_iterator.py @@ -210,7 +210,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 = [ + self._pending_tool_events = [ # mutable-ok: replaces speculative function events event for event in self._pending_tool_events if getattr(event, "output_index", None) != output_index @@ -409,7 +409,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 2fbbebe320f..bd239922fd3 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -178,7 +178,7 @@ class _ToolFunctionDefinition(TypedDict, total=False): def _attribute_fields(value: object) -> dict[str, object]: if not hasattr(value, "__dict__"): - return {} + return {} # mutable-ok: provider_specific_fields payload return dict(cast("Iterable[tuple[str, object]]", value)) # cast-ok: dict() raises on non-pair values, as before @@ -742,7 +742,9 @@ class LiteLLMCompletionResponsesConfig: if reasoning_text: message["reasoning_content"] = reasoning_text if thinking_blocks: - message["thinking_blocks"] = list(thinking_blocks) + message["thinking_blocks"] = list( # mutable-ok: thinking_blocks is a list on the message contract + thinking_blocks + ) return message @staticmethod @@ -825,7 +827,9 @@ class LiteLLMCompletionResponsesConfig: else: setattr(msg, "reasoning_content", combined) # noqa: B010 # attribute name is fixed, not dynamic if pending_blocks: - replayed: Final = list(pending_blocks + (_thinking_blocks(msg) or ())) + replayed: Final = list( # mutable-ok: thinking_blocks is a list on the message contract + pending_blocks + (_thinking_blocks(msg) or ()) + ) if isinstance(msg, dict): cast(dict[str, object], msg)["thinking_blocks"] = replayed # cast-ok: mutable reasoning carrier else: @@ -838,13 +842,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 ( @@ -858,16 +862,20 @@ class LiteLLMCompletionResponsesConfig: if pending and _role(msg) == "assistant": _apply_pending(msg, pending) - pending = [] + pending = [] # mutable-ok: reset accumulator elif pending: # Not followed by an assistant message — keep the reasoning # standalone instead of dropping it. - merged.extend([_standalone(text, blocks) for text, blocks in pending]) - pending = [] + merged.extend( + [_standalone(text, blocks) for text, blocks in pending] # mutable-ok: append reasoning messages + ) + pending = [] # mutable-ok: reset accumulator merged.append(msg) - merged.extend([_standalone(text, blocks) for text, blocks in pending]) + merged.extend( + [_standalone(text, blocks) for text, blocks in pending] # mutable-ok: append trailing reasoning + ) return merged @@ -903,7 +911,7 @@ class LiteLLMCompletionResponsesConfig: content: Final = ( new_content if not previous_content - else [ + else [ # mutable-ok: outbound chat content uses JSON arrays block for value in (previous_content, new_content) for block in ( @@ -913,7 +921,7 @@ class LiteLLMCompletionResponsesConfig: ) ] ) - merged: Final = { + merged: Final = { # mutable-ok: json.dumps rejects MappingProxyType in outbound chat messages **last_message, "content": content, } @@ -1344,7 +1352,7 @@ class LiteLLMCompletionResponsesConfig: """ if input_item.get("type") == "web_search_call": search: Final = ResponseFunctionWebSearch.model_validate(input_item) - return [ + return [ # mutable-ok: input conversion returns chat message lists GenericChatCompletionMessage( role="assistant", content="Hosted web search: " + search.model_dump_json(exclude_none=True), @@ -1384,8 +1392,8 @@ class LiteLLMCompletionResponsesConfig: or input_item.get("content") ) if inspectable is None: - return [] - return [ + return [] # mutable-ok: empty drop result + return [ # mutable-ok: single message result GenericChatCompletionMessage( role=_input_item_role(input_item), content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content( @@ -1400,8 +1408,8 @@ class LiteLLMCompletionResponsesConfig: input_item ) if not reasoning_text and not thinking_blocks: - return [] - return [ + return [] # mutable-ok: empty drop result + return [ # mutable-ok: single message result LiteLLMCompletionResponsesConfig._reasoning_only_assistant_message( reasoning_text=reasoning_text, thinking_blocks=thinking_blocks, @@ -1917,7 +1925,9 @@ class LiteLLMCompletionResponsesConfig: function: Final = ChatCompletionToolParamFunctionChunk( name=chat_tool_name, description=description, - parameters=dict(normalized_parameters), + parameters=dict( # mutable-ok: json.dumps rejects MappingProxyType in the outbound payload + normalized_parameters + ), strict=bool(namespace_tool.get("strict", False)), ) allowed_callers: Final = validated_allowed_callers(namespace_tool.get("allowed_callers")) @@ -1994,7 +2004,11 @@ class LiteLLMCompletionResponsesConfig: if tool_type == "function": typed_tool: Final = cast(FunctionToolParam, tool) raw_parameters: Final = typed_tool.get("parameters", {}) or {} - parameters: Final = {**raw_parameters} if "type" in raw_parameters else {**raw_parameters, "type": "object"} + 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 + ) chat_completion_tool: Final[dict[str, object]] = { "type": "function", "function": { @@ -2168,7 +2182,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 diff --git a/litellm/responses/main.py b/litellm/responses/main.py index d145f8cc6b8..f1ed8d3e9b3 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -1324,7 +1324,9 @@ def responses( ) response_api_optional_params: Final[ResponsesAPIOptionalRequestParams] = ( ResponsesAPIRequestUtils.get_requested_response_api_optional_param( - {k: v for k, v in {**local_vars, "reasoning": request_reasoning}.items() if k != "reasoning_effort"} + { # mutable-ok: callee pops keys off the dict it is given + k: v for k, v in {**local_vars, "reasoning": request_reasoning}.items() if k != "reasoning_effort" + } ) ) diff --git a/litellm/responses/mcp/mcp_streaming_iterator.py b/litellm/responses/mcp/mcp_streaming_iterator.py index c0bb92cfb2a..3b5cb85862d 100644 --- a/litellm/responses/mcp/mcp_streaming_iterator.py +++ b/litellm/responses/mcp/mcp_streaming_iterator.py @@ -648,7 +648,9 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): *self._composed_output, *_output_items(response_obj), ] - merged_response: Final = response_obj.model_copy(update={"output": merged_output}) + merged_response: Final = response_obj.model_copy( + update={"output": merged_output} # mutable-ok: pydantic's update argument must be a dict + ) _set_event_field(chunk, "response", merged_response) return chunk @@ -765,11 +767,11 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): call_items[tool_call_id] = (item_id, output_index) self.tool_execution_events.append( OutputItemAddedEvent.model_validate( - { + { # mutable-ok: consumed once by model_validate "type": ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED, "sequence_number": len(self.tool_execution_events) + 1, "output_index": output_index, - "item": { + "item": { # mutable-ok: consumed once by model_validate "id": item_id, "type": "mcp_call", "status": "in_progress", @@ -847,7 +849,7 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): from litellm.types.llms.openai import OutputItemDoneEvent mcp_call_item = BaseLiteLLMOpenAIResponseObject( - **{ + **{ # mutable-ok: consumed once by the model constructor "id": item_id, "type": "mcp_call", "status": "completed", diff --git a/litellm/responses/mcp/request_context.py b/litellm/responses/mcp/request_context.py index 0bbed24b6ac..b262959ef57 100644 --- a/litellm/responses/mcp/request_context.py +++ b/litellm/responses/mcp/request_context.py @@ -124,7 +124,7 @@ class MCPRequestContext: ) ), "guardrail_config": deepcopy( - { + { # mutable-ok: per-request guardrail configuration is a mutable JSON object in existing callbacks key: value for source in sources for key, value in TypeAdapter(dict[str, object]) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 5e045c3e84f..10c73071fc7 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -346,7 +346,9 @@ 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 {})) + self._raw_response_headers: Mapping[str, str] = MappingProxyType( + dict(self.response.headers or {}) # mutable-ok: immediately frozen by MappingProxyType + ) def _check_max_streaming_duration(self) -> None: """Raise litellm.Timeout if the stream has exceeded LITELLM_MAX_STREAMING_DURATION_SECONDS.""" @@ -599,9 +601,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 = { - "additional_headers": {**headers}, - "headers": {**raw_headers}, + target._hidden_params = { # mutable-ok: logging aliases _hidden_params into request metadata and writes into it + "additional_headers": {**headers}, # mutable-ok: fresh copy, logging callbacks may mutate it + "headers": {**raw_headers}, # mutable-ok: fresh copy, logging callbacks may mutate it **existing, } @@ -2422,9 +2424,9 @@ class ResponsesWebSocketStreaming: try: await self.websocket.send_text( json.dumps( - { + { # mutable-ok: WebSocket wire payload requires JSON objects "type": "error", - "error": { + "error": { # mutable-ok: nested WebSocket error object "type": "rate_limit_exceeded", "message": str(e), }, diff --git a/litellm/responses/utils.py b/litellm/responses/utils.py index 0c025506f22..c5e6f3995f7 100644 --- a/litellm/responses/utils.py +++ b/litellm/responses/utils.py @@ -68,7 +68,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"} + return {**part, "type": "input_text"} # mutable-ok: fresh part so the caller's block keeps its chat type return part @@ -85,8 +85,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] - return {**message, "content": shaped_content} + 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 @staticmethod def responses_input_to_chat_messages( diff --git a/litellm/router.py b/litellm/router.py index 24554e61516..bb118639839 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -514,7 +514,9 @@ 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}}) + return MappingProxyType( + {"session": {**typed_session, "model": model_name}} # mutable-ok: callees deepcopy and JSON-dump session + ) # Router._aanthropic_messages_streaming_iterator buffers lifecycle chunks @@ -634,7 +636,9 @@ class FallbackAwareAnthropicMessagesStream: self._async_generator = async_generator self._source_iterator = source_iterator self.fallback_headers_adopted = False - self._hidden_params = dict(getattr(source_iterator, "_hidden_params", None) or {}) + self._hidden_params = dict( # mutable-ok: mutated in place by merge_fallback_hidden_params + getattr(source_iterator, "_hidden_params", None) or {} + ) @property def has_buffered_provider_output(self) -> bool: @@ -686,10 +690,12 @@ 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 = { + self._hidden_params = { # mutable-ok: matches _hidden_params' existing dict[str, object] shape **self._hidden_params, **fallback_hidden_params, - "additional_headers": dict(replace_complexity_router_headers(existing_headers, fallback_headers)), + "additional_headers": dict( # mutable-ok: hidden params expect a writable header bag + replace_complexity_router_headers(existing_headers, fallback_headers) + ), } @@ -736,12 +742,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 = { + self._hidden_params = { # mutable-ok: the rest of litellm writes into _hidden_params **fallback_hidden_params, # dict() because add_retry_fallback_headers mutates additional_headers in place - "additional_headers": dict(fallback_headers), + "additional_headers": dict(fallback_headers), # mutable-ok: see above } - self._base_hidden_params = { + self._base_hidden_params = { # mutable-ok: CustomStreamWrapper keeps this snapshot as a dict **self._hidden_params, "response_cost": None, } @@ -1584,7 +1590,7 @@ class Router: routing_group: Final = self.get_routing_group(model) if routing_group is None: return None - return [ + return [ # mutable-ok: matches _get_all_deployments' list contract expected by downstream filters apply_routing_group_priority(routing_group, member, deployment) for member in routing_group.models for deployment in self._get_all_deployments(model_name=member, team_id=team_id) @@ -2459,7 +2465,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) + sanitized: Final = dict(deployment_params) # mutable-ok: request-local copy protects shared Router state if request_kwargs.get("reasoning_effort") is None: return sanitized @@ -2469,7 +2475,7 @@ class Router: extra_body: Final = sanitized.get("extra_body") if isinstance(extra_body, Mapping): - sanitized_extra_body: Final = dict(extra_body) + sanitized_extra_body: Final = dict(extra_body) # mutable-ok: request-local nested copy sanitized_extra_body.pop("reasoning_effort", None) sanitized_extra_body.pop("thinking", None) Router._pop_effort_from_nested_carrier(sanitized_extra_body, "output_config") @@ -3330,7 +3336,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]} + self._hidden_params = {**prepared[0], "additional_headers": prepared[1]} # mutable-ok: stream metadata self.fallback_headers_adopted = True return prepared @@ -6942,7 +6948,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, @@ -10085,7 +10091,9 @@ class Router: requests that route to (and bill as) a real deployment. """ if classify_strategy_router_model(model) is not None: - model_info = {k: v for k, v in model_info.items() if k not in CustomPricingLiteLLMParams.model_fields} + 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 + } if model_id is not None: litellm.register_model( @@ -10826,7 +10834,7 @@ class Router: try: custom_model_info = ( - { + { # mutable-ok: the legacy model-info merge updates this private copy **copy.deepcopy(litellm.model_cost.get(model_id) or MappingProxyType({})), **self.get_discovered_model_info(model_id), } @@ -11909,8 +11917,10 @@ 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 = {k: v for k, v in (deployment.get("model_info") or {}).items() if k != "access_groups"} - return {**deployment, "model_info": model_info} + 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 TIER_PARAMS_NEVER_DROPPED: Final = frozenset(all_litellm_params) | frozenset( { @@ -12893,7 +12903,9 @@ 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 = [d for d in deployments if not self._is_strategy_marker_deployment(d)] + 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) + ] if deployments and not selectable: raise litellm.BadRequestError( message=f"You passed in model={model}. {RouterErrors.only_strategy_marker_deployments.value}", @@ -13899,7 +13911,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}) + routed.model_copy(update={"messages": messages}) # mutable-ok: pydantic's model_copy takes a dict if routed is not None and routing_messages is not None and routed.messages == routing_messages else routed ) @@ -14543,7 +14555,7 @@ class Router: ] if not filtered: - return [] if health_check_probe else healthy_deployments + return [] if health_check_probe else healthy_deployments # mutable-ok: empty list signals unavailable probe return filtered diff --git a/litellm/router_strategy/auto_router/litellm_encoder.py b/litellm/router_strategy/auto_router/litellm_encoder.py index caabbb3a342..1b34785b6fe 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] + clamped: Final = [doc[:limit] for doc in docs] # mutable-ok: embedding() takes `input: str | list` 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/budget_limiter.py b/litellm/router_strategy/budget_limiter.py index a792654e2a9..64252cbbfb3 100644 --- a/litellm/router_strategy/budget_limiter.py +++ b/litellm/router_strategy/budget_limiter.py @@ -421,7 +421,7 @@ class RouterBudgetLimiting(CustomLogger): increment_operations_to_flush: Final = tuple(self.redis_increment_operation_queue) if not increment_operations_to_flush: return increment_operations_to_flush - self.redis_increment_operation_queue = [] + self.redis_increment_operation_queue = [] # mutable-ok: emptied queue must stay appendable self._detached_increment_operations = increment_operations_to_flush return increment_operations_to_flush @@ -478,7 +478,9 @@ class RouterBudgetLimiting(CustomLogger): "Pushing Redis Increment Pipeline for queue: %s", increment_operations_to_flush, ) - increment_list: Final = list(increment_operations_to_flush) + increment_list: Final = list( # mutable-ok: Redis pipeline contract requires a list + increment_operations_to_flush + ) try: await redis_cache.async_increment_pipeline(increment_list=increment_list) except Exception as error: diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index 08173588720..76ee977bf28 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -1289,7 +1289,7 @@ def _parse_session_affinity_pin(value: object, active_tiers: tuple[str, ...]) -> 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} + return {"model": model, "tier": tier_value} # mutable-ok: cache requires JSON mapping class ComplexityRouter(CustomLogger): @@ -2471,7 +2471,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, ] @@ -2521,23 +2521,25 @@ class ComplexityRouter(CustomLogger): ) latest_follow_up: Final = asks_newest_first[0] if len(asks_newest_first) > 1 else None task_messages: list[AllMessageValues] = [ # mutable-ok: the latest message gains optional image parts below - {"role": "user", "content": opening_task}, + {"role": "user", "content": opening_task}, # mutable-ok: SDK messages are dict-shaped ] if latest_follow_up is not None: - task_messages.append({"role": "user", "content": latest_follow_up}) + task_messages.append( + {"role": "user", "content": latest_follow_up} # mutable-ok: SDK messages are dict-shaped + ) image_parts: Final = self._classifier_image_parts(messages) if image_parts: latest_text: Final = latest_follow_up or opening_task - task_messages[-1] = { + task_messages[-1] = { # mutable-ok: SDK messages are dict-shaped "role": "user", - "content": [ - {"type": "text", "text": latest_text}, + "content": [ # mutable-ok: multimodal SDK content is a JSON array + {"type": "text", "text": latest_text}, # mutable-ok: SDK content parts are dict-shaped *image_parts, ], } messages_for_call: Final[list[AllMessageValues]] = [ # mutable-ok: provider SDK requires a concrete list - {"role": "system", "content": classifier_system_prompt}, + {"role": "system", "content": classifier_system_prompt}, # mutable-ok: SDK messages are dict-shaped *task_messages, ] content, classifier_cost = await self._call_classifier_model( @@ -2590,7 +2592,7 @@ class ComplexityRouter(CustomLogger): image_parts: Final = self._classifier_image_parts(messages) text_part: Final[ChatCompletionTextObject] = {"type": "text", "text": task} user_content: Final[str | Sequence[ChatCompletionTextObject | ChatCompletionImageObject]] = ( - [text_part, *image_parts] if image_parts else task + [text_part, *image_parts] if image_parts else task # mutable-ok: provider adapters require content arrays ) system_message: Final[ChatCompletionSystemMessage] = { "role": "system", @@ -2636,7 +2638,7 @@ class ComplexityRouter(CustomLogger): request_values: Final = request_kwargs or EMPTY_MAPPING request_metadata = request_values.get("litellm_metadata") or request_values.get("metadata") - metadata: Final = { + metadata: Final = { # mutable-ok: SDK metadata kwarg is enriched by the request pipeline **forwarded_internal_call_metadata(request_metadata, AUTOROUTER_CLASSIFIER_CALL_ORIGIN), INTERNAL_CALL_ORIGIN_METADATA_KEY: AUTOROUTER_CLASSIFIER_CALL_ORIGIN, } @@ -2666,7 +2668,7 @@ class ComplexityRouter(CustomLogger): ) proxy_server_request: Final = { "originating_request_masked": masked_originating_request(request_kwargs), - "body": {"model": llm_config.model, **payload}, + "body": {"model": llm_config.model, **payload}, # mutable-ok: logging SDK expects a JSON request body } classify: Final = ( self.litellm_router_instance.aresponses @@ -3597,7 +3599,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) + repick_messages: Final = list(resolved_messages) # mutable-ok: the pick's param is list-typed new_model = await self._pick_model_for_tier( new_tier, messages, @@ -3718,7 +3720,7 @@ class ComplexityRouter(CustomLogger): from litellm.exceptions import BadRequestError from litellm.types.router import RouterErrors, RouterRateLimitError, RouterRateLimitErrorBasic - probe_kwargs: Final = dict(request_kwargs) + probe_kwargs: Final = dict(request_kwargs) # mutable-ok: the owner pops routing keys off the dict it is handed try: deployments: Final = await self.litellm_router_instance.async_get_healthy_deployments( model=model_name, @@ -3797,7 +3799,9 @@ 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 + repick_messages: Final = ( + list(resolved_messages) if resolved_messages else None # mutable-ok: the pick's param is list-typed + ) try: new_model: Final = await self._pick_model_for_tier( candidate_tier if self.config.has_custom_tiers else ComplexityTier(candidate_tier), @@ -3835,7 +3839,7 @@ class ComplexityRouter(CustomLogger): previous_decision=decision, ) return response.model_copy( - update={ + update={ # mutable-ok: model_copy types update as a plain dict "model": new_model, "litellm_params": self._litellm_params_for_model(candidate_tier, new_model), "routing_decision": new_decision, @@ -3880,7 +3884,7 @@ class ComplexityRouter(CustomLogger): previous_decision=decision, ) return response.model_copy( - update={ + update={ # mutable-ok: model_copy types update as a plain dict "model": default_model, "litellm_params": self._litellm_params_for_model(None, default_model), "routing_decision": default_decision, @@ -4160,7 +4164,11 @@ class ComplexityRouter(CustomLogger): ) -> PreRoutingHookResponse | None: if response is None or not self._uses_deployment_pin: return response - return response.model_copy(update={"session_affinity_ttl_seconds": self.config.session_affinity_ttl_seconds}) + 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 + } + ) 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 00cff661d2f..e0427f89fe3 100644 --- a/litellm/router_strategy/complexity_router/config.py +++ b/litellm/router_strategy/complexity_router/config.py @@ -254,7 +254,7 @@ class ComplexityTierModel(BaseModel): @field_serializer("litellm_params") def _serialize_litellm_params(self, value: Mapping[str, object]) -> Mapping[str, object]: - return dict(value) + return dict(value) # mutable-ok: Pydantic JSON serialization requires a concrete mapping def _normalize_tier_entries( @@ -269,7 +269,11 @@ 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) + 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 + ) return normalized, entries @@ -1554,7 +1558,7 @@ class ComplexityRouterConfig(BaseModel): or (isinstance(existing_configs, dict) and tier in existing_configs) } ) - return { + return { # mutable-ok: Pydantic before-validator requires a concrete mapping **value, "tiers": normalized_tiers, "tier_model_configs": tier_model_configs, diff --git a/litellm/router_strategy/complexity_router/jev_classifier.py b/litellm/router_strategy/complexity_router/jev_classifier.py index a2f03b07e3a..02e57975626 100644 --- a/litellm/router_strategy/complexity_router/jev_classifier.py +++ b/litellm/router_strategy/complexity_router/jev_classifier.py @@ -130,8 +130,8 @@ class HttpJevClassifierClient: for key, value in TypeAdapter(Mapping[str, object]).validate_python(metadata).items() } ) - params: Final = { - "metadata": { + params: Final = { # mutable-ok: Logging's kwargs and litellm_params require dicts + "metadata": { # mutable-ok: Logging enriches metadata in place before dispatching callbacks **forwarded_internal_call_metadata(parent_metadata, AUTOROUTER_CLASSIFIER_CALL_ORIGIN), INTERNAL_CALL_ORIGIN_METADATA_KEY: AUTOROUTER_CLASSIFIER_CALL_ORIGIN, }, @@ -140,7 +140,7 @@ class HttpJevClassifierClient: } logging_obj: Final = Logging( model=f"typesafe/{request.model}", - messages=[{"role": "user", "content": request.state}], + messages=[{"role": "user", "content": request.state}], # mutable-ok: callbacks require JSON message lists stream=False, call_type="pass_through_endpoint", start_time=start_time, @@ -152,7 +152,7 @@ class HttpJevClassifierClient: logging_obj.update_environment_variables( model=f"typesafe/{request.model}", user=parent_user if isinstance(parent_user := parent.get("user"), str) else None, - optional_params={}, + optional_params={}, # mutable-ok: Logging's optional_params contract requires a dict litellm_params=params, ) normalized: Final = TypeSafePassthroughLoggingHandler.typesafe_passthrough_handler( diff --git a/litellm/router_utils/fallback_event_handlers.py b/litellm/router_utils/fallback_event_handlers.py index 3142fd5fb98..e0df7d1badf 100644 --- a/litellm/router_utils/fallback_event_handlers.py +++ b/litellm/router_utils/fallback_event_handlers.py @@ -352,7 +352,7 @@ def mid_stream_fallback_hop_kwargs( copied_buckets: Final = MappingProxyType( {name: safe_deep_copy(kwargs[name]) for name in _ROUTER_METADATA_BUCKETS if isinstance(kwargs.get(name), dict)} ) - return { + return { # mutable-ok: handed to the streaming iterator as its initial_kwargs, which it rewrites on re-entry **kwargs, **copied_buckets, **hop_controls.overrides, diff --git a/litellm/router_utils/prompt_caching_cache.py b/litellm/router_utils/prompt_caching_cache.py index 23005a97c59..78fc5e3fe6d 100644 --- a/litellm/router_utils/prompt_caching_cache.py +++ b/litellm/router_utils/prompt_caching_cache.py @@ -314,7 +314,7 @@ class PromptCachingCache: return _first_pin( _PINS_ADAPTER.validate_python( await self.cache.async_batch_get_cache( - keys=list(cache_keys), + keys=list(cache_keys), # mutable-ok: DualCache.async_batch_get_cache only takes a list ) ) ) @@ -331,7 +331,7 @@ class PromptCachingCache: return _first_pin( _PINS_ADAPTER.validate_python( self.cache.batch_get_cache( - keys=list(cache_keys), + keys=list(cache_keys), # mutable-ok: DualCache.batch_get_cache only takes a list ) ) ) diff --git a/litellm/router_utils/routing_read_batch.py b/litellm/router_utils/routing_read_batch.py index 752b2857de4..adda31312c0 100644 --- a/litellm/router_utils/routing_read_batch.py +++ b/litellm/router_utils/routing_read_batch.py @@ -37,10 +37,10 @@ async def _backfill_prefetched_cache( due_keys: tuple[str, ...], values: Mapping[str, object], ) -> None: - cache_keys: Final = list(due_keys) + cache_keys: Final = list(due_keys) # mutable-ok: _prepare_batch_get takes a list prepare_batch_get: Final = cache._prepare_batch_get # pyright: ignore[reportPrivateUsage] # memory backfill pending: Final = await prepare_batch_get(cache_keys, local_only=True) - redis_values: Final = { + redis_values: Final = { # mutable-ok: _apply_batch_get accepts a dictionary key: values[key] for key, local in zip(due_keys, pending.result) if local is None and values.get(key) is not None @@ -190,7 +190,11 @@ class RoutingReadBatch: ) reads: Final = ( (litellm_router_instance.cooldown_cache.cooldown_store, cooldown_keys), - *(() if selector is None else ((selector.router_cache, list(usage_keys)),)), + *( + () + if selector is None + else ((selector.router_cache, list(usage_keys)),) # mutable-ok: DualCache batch reads take a list + ), ) results: Final = await self._read_prefetched(reads) or await DualCache.async_batch_get_cache_shared( reads, parent_otel_span=parent_otel_span @@ -230,6 +234,8 @@ class RoutingReadBatch: key not in prefetch.fetched for key, local_value in zip(keys, pending.result) if local_value is None ): return None - missed = {key: values.get(key) for key, local in zip(keys, pending.result) if local is None} + missed = { # mutable-ok: _apply_batch_get takes a dict + key: values.get(key) for key, local in zip(keys, pending.result) if local is None + } results.append(await cache._apply_batch_get(pending, missed)) # pyright: ignore[reportPrivateUsage] # same two-step read as async_batch_get_cache_shared return results diff --git a/litellm/rust_bridge/callbacks_legacy_python.py b/litellm/rust_bridge/callbacks_legacy_python.py index cecbd518f02..25513666c43 100644 --- a/litellm/rust_bridge/callbacks_legacy_python.py +++ b/litellm/rust_bridge/callbacks_legacy_python.py @@ -53,7 +53,7 @@ def setup( from litellm.litellm_core_utils.litellm_logging import Logging from litellm.utils import Rules, function_setup - arguments: Final = { + arguments: Final = { # mutable-ok: function_setup consumes an owned kwargs dict "litellm_call_id": str(uuid.uuid4()), **kwargs, } diff --git a/litellm/rust_bridge/failures.py b/litellm/rust_bridge/failures.py index 959448f12bf..80805b7ff69 100644 --- a/litellm/rust_bridge/failures.py +++ b/litellm/rust_bridge/failures.py @@ -58,8 +58,8 @@ def map_failure(error: Exception, model: str, request_provider: str, kwargs: Map model=model.removeprefix(f"{request_provider}/"), custom_llm_provider=request_provider, original_exception=error, - completion_kwargs=dict(kwargs), - extra_kwargs=dict(kwargs), + completion_kwargs=dict(kwargs), # mutable-ok: exception mapper requires owned kwargs + extra_kwargs=dict(kwargs), # mutable-ok: exception mapper requires owned kwargs ) except Exception as public_error: public_error.__context__ = error diff --git a/litellm/rust_bridge/messages/route_host.py b/litellm/rust_bridge/messages/route_host.py index 19e3126ad82..caae9916ffa 100644 --- a/litellm/rust_bridge/messages/route_host.py +++ b/litellm/rust_bridge/messages/route_host.py @@ -50,7 +50,7 @@ class MessagesShaping: def response(value: Mapping[str, object]) -> AnthropicMessagesResponse: return cast( # cast-ok: AnthropicMessagesResponse is a TypedDict over the normalized native payload AnthropicMessagesResponse, - dict(value), + dict(value), # mutable-ok: the public Messages response is a TypedDict the caller may annotate in place ) diff --git a/litellm/tracing/decode.py b/litellm/tracing/decode.py index ecc13edd956..60ae7732444 100644 --- a/litellm/tracing/decode.py +++ b/litellm/tracing/decode.py @@ -116,7 +116,9 @@ def _span_row(span: DecodedSpan) -> SpanRow: Output="", ) normalize(row, attributes) - row["SpanAttributes"] = {k: _truncate(v) for k, v in attributes.items() if k not in _HEAVY_ATTRIBUTES} + row["SpanAttributes"] = { # mutable-ok: the Rust JSON bridge requires a plain dict for span attributes + k: _truncate(v) for k, v in attributes.items() if k not in _HEAVY_ATTRIBUTES + } row["Input"], row["Output"] = _truncate(row["Input"]), _truncate(row["Output"]) return row @@ -141,7 +143,10 @@ def _lc_message(message: Mapping[str, Any]) -> dict[str, Any]: "content": content if isinstance(content, str) else json.dumps(content), } if kwargs.get("tool_calls"): - out["tool_calls"] = tuple({"name": t.get("name"), "args": t.get("args")} for t in kwargs["tool_calls"]) + out["tool_calls"] = tuple( + {"name": t.get("name"), "args": t.get("args")} # mutable-ok: JSON tool calls need object payloads + for t in kwargs["tool_calls"] + ) if role == "tool" and kwargs.get("name"): out["name"] = kwargs["name"] return out diff --git a/litellm/tracing/receiver.py b/litellm/tracing/receiver.py index e942e5c18b8..be9641a602b 100644 --- a/litellm/tracing/receiver.py +++ b/litellm/tracing/receiver.py @@ -50,7 +50,7 @@ class Tenant: def stamp(self, row: SpanRow) -> SpanRow: row["TeamId"] = self.team_id row["ApiKeyHash"] = self.api_key_hash - row["ResourceAttributes"] = { + row["ResourceAttributes"] = { # mutable-ok: the Rust JSON bridge requires a plain dict **row["ResourceAttributes"], "litellm.team_id": self.team_id, "litellm.api_key_hash": self.api_key_hash, diff --git a/litellm/types/integrations/newrelic.py b/litellm/types/integrations/newrelic.py index e662c260065..b5905ad0b93 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 6db7fd68292..99ab5920c4f 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -123,7 +123,7 @@ class HttpxBinaryResponseContent(_HttpxBinaryResponseContent): def __init__(self, response: httpx.Response) -> None: super().__init__(response) - self._hidden_params = {} + self._hidden_params = {} # mutable-ok: mutable-dict contract shared with ModelResponse logging consumers def logging_summary(self) -> BinaryResponseSummary: return { @@ -414,7 +414,9 @@ class OpenAIFileObject(BaseModel): serialized: Final[Mapping[str, object]] = handler(self) if self.litellm_batch_guardrail is not None: return serialized - return {key: value for key, value in serialized.items() if key != BATCH_GUARDRAIL_RESPONSE_FIELD} + 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 + } 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 67f7ed424e7..75a80beac5c 100644 --- a/litellm/types/management_endpoints/auto_router_endpoints.py +++ b/litellm/types/management_endpoints/auto_router_endpoints.py @@ -140,7 +140,13 @@ 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={"messages": [{"role": "user", "content": self.prompt}]}) + 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 + ] + } + ) def wire_body(self) -> Mapping[str, object]: """The request kwargs a serving-path request would carry for this body. diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/aim.py b/litellm/types/proxy/guardrails/guardrail_hooks/aim.py index 18d98441065..291740613ef 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}, + json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL}, # mutable-ok: pydantic accepts only a dict here ) @staticmethod diff --git a/litellm/types/proxy/guardrails/guardrail_hooks/cato_networks.py b/litellm/types/proxy/guardrails/guardrail_hooks/cato_networks.py index dc69bd137ec..69b4d5bec37 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}, + json_schema_extra={"ui_type": GuardrailParamUITypes.BOOL}, # mutable-ok: pydantic accepts only a dict here ) @staticmethod diff --git a/litellm/types/responses/main.py b/litellm/types/responses/main.py index 2381c7ff3a1..26cc5c4c6cc 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 = [ + sources: Final = [ # mutable-ok: official SDK expects a source list 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={ + action={ # mutable-ok: official SDK expects an action mapping "type": "search", "query": query if isinstance(query, str) else "", - "queries": [query] if isinstance(query, str) and query else [], + "queries": [query] if isinstance(query, str) and query else [], # mutable-ok: SDK list field "sources": sources, }, ) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index dcf2efc213a..c12a4def69a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1539,7 +1539,9 @@ 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] = [] + coerced_tool_calls: list[ + ChatCompletionDeltaToolCall | ChatCompletionDeltaCustomToolCall + ] = [] # mutable-ok: public Delta.tool_calls contract is a list current_index = 0 for tool_call in tool_calls: if isinstance(tool_call, dict): @@ -3940,14 +3942,14 @@ def pricing_override_fields(*sources: Mapping[str, object]) -> tuple[str, ...]: ) -agentic_loop_internal_litellm_params: Final = list(AGENTIC_LOOP_KWARG_NAMES) +agentic_loop_internal_litellm_params: Final = list(AGENTIC_LOOP_KWARG_NAMES) # mutable-ok: public type stays a list bedrock_batch_litellm_params: Final = BEDROCK_BATCH_KWARG_NAMES TRUSTED_CALLBACK_VARS_FIELD: Final = _litellm_params.TRUSTED_CALLBACK_VARS_FIELD ADDRESSED_RESPONSE_ID_FIELD: Final = _litellm_params.ADDRESSED_RESPONSE_ID_FIELD -all_litellm_params = [ # rebind-ok: two star imports in litellm/__init__.py re-bind it +all_litellm_params = [ # rebind-ok: two star imports in litellm/__init__.py re-bind it # mutable-ok: callers concat *OWNED_KWARG_NAMES, *KWARG_ARTIFACTS, *StandardCallbackDynamicParams.__annotations__, diff --git a/litellm/utils.py b/litellm/utils.py index 0eb2754ed0c..b0a7e4f1a68 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -1268,7 +1268,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 = [] + messages = [] # mutable-ok: loggers require a list here and Logging copies it else: messages = "default-message-value" stream = False @@ -3127,9 +3127,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} + existing_dict[k] = {**existing_nested_dict, **v} # mutable-ok: copy-on-write merge else: - existing_dict[k] = dict(v) + existing_dict[k] = dict(v) # mutable-ok: detached copy, never the caller's dict by reference else: existing_dict[k] = v @@ -3280,7 +3280,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)) + register_model(model_cost=dict(_runtime_registered_model_cost)) # mutable-ok: snapshot, replay rewrites it def cost_map_omits_token_price(*keys: object) -> bool: @@ -3340,7 +3340,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) + _runtime_registered_model_cost[_registered_key] = dict(_registered_value) # mutable-ok: caller-owned _skip_get_model_info_providers: Final = PROVIDERS_THAT_AUTHENTICATE_ON_PROVIDER_INFO @@ -3362,7 +3362,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 {}) + existing_model = dict(match_capability_generalizations(_key_str) or {}) # mutable-ok: merge target model_cost_key = key builtin_entry = _resolve_builtin_model_cost_entry(key=_key_str, provider=provider) if builtin_entry is not None: @@ -5092,7 +5092,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) + params: Final = dict(passed_params) # mutable-ok: get_non_default_params takes a dict return frozenset(get_non_default_params(params)) - PROVIDER_UNVALIDATED_PARAMS @@ -7373,7 +7373,7 @@ class TextCompletionStreamWrapper: def mock_stream_usage_chunk(model_response: ModelResponseStream, model: str, prompt_tokens: int) -> ModelResponseStream: return ModelResponseStream( id=model_response.id, - choices=[], + choices=[], # mutable-ok: ModelResponseStream only treats a list as explicit choices, a tuple gets a default choice model=model, usage=Usage( prompt_tokens=prompt_tokens, diff --git a/litellm/vector_stores/main.py b/litellm/vector_stores/main.py index 4d945310293..976e6dead76 100644 --- a/litellm/vector_stores/main.py +++ b/litellm/vector_stores/main.py @@ -307,7 +307,9 @@ async def asearch( embedding_executor: Final = _direct_vector_store_embedding_executor( kwargs.pop("_direct_vector_store_embedding_executor", None), router, kwargs ) - local_vars: Final = {key: value for key, value in locals().items() if key != "embedding_executor"} + local_vars: Final = { # mutable-ok: exception logging requires a sanitized mutable snapshot + key: value for key, value in locals().items() if key != "embedding_executor" + } try: loop: Final = asyncio.get_event_loop() @@ -391,7 +393,9 @@ def search( embedding_executor: Final = _direct_vector_store_embedding_executor( kwargs.pop("_direct_vector_store_embedding_executor", None), router, kwargs ) - local_vars: Final = {key: value for key, value in locals().items() if key != "embedding_executor"} + local_vars: Final = { # mutable-ok: exception logging requires a sanitized mutable snapshot + 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 8adc3ac27b7..ceb25453bfc 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -81,7 +81,8 @@ LIT012 TypedDict field without a `ReadOnly[...]` qualifier. A writable key lets LIT013 A `# -ok: ` suppression on a line where none of the rules that token suppresses fires. Like ruff's RUF100: a marker that suppresses nothing rots in place and hides real violations that land on the line - later. Delete it. + later. Delete it. `# mutable-ok` is exempt while the markers left over + from the retired mutable-construction rule are still in the tree. LIT014 Comprehension with more than one `for` clause or more than one `if` clause, in any of the four forms (list, set, dict, generator expression). Stacked `for`s and `if`s read as nested loops and guards squashed onto one line; @@ -174,11 +175,13 @@ class _OkToken: token: str pattern: re.Pattern[str] codes: frozenset[str] + flags_unused: bool = True # Suppression tokens that must each carry a reason (LIT005). OK_SUPPRESSIONS: Final[tuple[_OkToken, ...]] = ( - _OkToken("mutable-ok", MUTABLE_OK_RE, frozenset(("LIT001",))), + # TODO: drop flags_unused=False once the ~1.4k markers left over from the retired mutable-construction rule are stripped + _OkToken("mutable-ok", MUTABLE_OK_RE, frozenset(("LIT001",)), flags_unused=False), _OkToken("cast-ok", CAST_OK_RE, frozenset(("LIT006",))), _OkToken("guard-ok", GUARD_OK_RE, frozenset(("LIT007",))), _OkToken("kwargs-ok", KWARGS_OK_RE, frozenset(("LIT008",))), @@ -938,6 +941,7 @@ def apply_suppressions( f"{'/'.join(sorted(ok.codes))} violation on this line, so delete it", ) for ok in OK_SUPPRESSIONS + if ok.flags_unused for line in sorted(suppressions.get(ok.token, frozenset())) if not any(v.line == line and v.code in ok.codes for v in raw) ) diff --git a/tests/integration/observability/test_s3_v2_upload_fanout.py b/tests/integration/observability/test_s3_v2_upload_fanout.py index 0154cce10df..b7d101f023f 100644 --- a/tests/integration/observability/test_s3_v2_upload_fanout.py +++ b/tests/integration/observability/test_s3_v2_upload_fanout.py @@ -634,7 +634,7 @@ def test_s3_v2_batch_retry_resends_identical_key_and_body(gateway: Gateway, tmp_ assert sum(1 for r in provider.drain() if r.method == "POST") == REQUESTS by_target: Final = {} for put in puts: - by_target.setdefault(put.target, set()).add(put.body) + by_target.setdefault(put.target, set()).add(put.body) # mutable-ok: grouping attempts seen so far per target assert all(len(bodies) == 1 for bodies in by_target.values()), "a retried batch PUT changed key or body" assert max(sum(1 for put in puts if put.target == target) for target in by_target) >= 2, "no retried PUT observed" assert frozenset(payload["id"] for payload in payloads) == ids diff --git a/tests/integration/routing/test_priority_rate_limit_headers.py b/tests/integration/routing/test_priority_rate_limit_headers.py index 93df0c105d5..bd92a362885 100644 --- a/tests/integration/routing/test_priority_rate_limit_headers.py +++ b/tests/integration/routing/test_priority_rate_limit_headers.py @@ -174,7 +174,7 @@ def test_streaming_chat_completion_success_logs_v3_rate_limit_remaining_values_f assert len(wire.drain()) == 1 batches: Final[ list[Request] - ] = [] + ] = [] # mutable-ok: drain() consumes the queue, later polls must keep earlier batches def delivered() -> tuple[dict, ...]: batches.extend(endpoint.drain()) diff --git a/tests/test_litellm/proxy/hooks/test_autorouter_baseline_cache.py b/tests/test_litellm/proxy/hooks/test_autorouter_baseline_cache.py index 0f4fd2ff5cb..c6bb7833310 100644 --- a/tests/test_litellm/proxy/hooks/test_autorouter_baseline_cache.py +++ b/tests/test_litellm/proxy/hooks/test_autorouter_baseline_cache.py @@ -115,7 +115,7 @@ def _stream(logging_obj: Logging) -> bool: def _sse(completed: bool = True, model: str = "claude-sonnet-5") -> tuple[bytes, ...]: events: Final = ( - { + { # mutable-ok: json.dumps needs a concrete event dictionary "type": "message_start", "message": _message(False, model), }, 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 3f22e0da92d..4577d578263 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 @@ -1864,7 +1864,7 @@ class TestBedrockAgentRuntimePassthroughToggle: request: Final = Mock() request.method = "POST" request.state = SimpleNamespace() - request.json = AsyncMock(return_value={"retrievalQuery": {"text": "hi"}}) + request.json = AsyncMock(return_value={"retrievalQuery": {"text": "hi"}}) # mutable-ok: must be json.dumps-able return request @contextlib.contextmanager diff --git a/tests/test_litellm/proxy/policy_engine/test_policy_matcher.py b/tests/test_litellm/proxy/policy_engine/test_policy_matcher.py index 862b5793eba..27153e67ab5 100644 --- a/tests/test_litellm/proxy/policy_engine/test_policy_matcher.py +++ b/tests/test_litellm/proxy/policy_engine/test_policy_matcher.py @@ -316,10 +316,10 @@ _MODELS: Final = ("gpt-4o", "gpt-5.5", "claude-opus-4-1") def _policy_forest(draw: st.DrawFn) -> dict[str, Policy]: # mutable-ok: PolicyResolver takes dict[str, Policy] names: Final = tuple(f"p{i}" for i in range(draw(st.integers(min_value=1, max_value=6)))) - return { + return { # mutable-ok: PolicyResolver takes dict[str, Policy] name: Policy( inherit=draw(st.sampled_from((None, *names[:i]))), - guardrails=PolicyGuardrails(add=[f"g-{name}"]), + guardrails=PolicyGuardrails(add=[f"g-{name}"]), # mutable-ok: pydantic list field condition=draw(st.sampled_from((None, *(PolicyCondition(model=m) for m in _MODELS)))), ) for i, name in enumerate(names) @@ -380,11 +380,11 @@ class TestChainMatchingProperties: class TestAncestorAdmissionLogging: @staticmethod def _chain() -> dict[str, Policy]: # mutable-ok: PolicyResolver takes dict[str, Policy] - return { - "parent": Policy(guardrails=PolicyGuardrails(add=["g-parent"])), + return { # mutable-ok: PolicyResolver takes dict[str, Policy] + "parent": Policy(guardrails=PolicyGuardrails(add=["g-parent"])), # mutable-ok: pydantic list field "child": Policy( inherit="parent", - guardrails=PolicyGuardrails(add=["g-child"]), + guardrails=PolicyGuardrails(add=["g-child"]), # mutable-ok: pydantic list field condition=PolicyCondition(model="gpt-5.5"), ), } @@ -407,14 +407,14 @@ class TestAncestorAdmissionLogging: assert not [r for r in caplog.records if "applied through ancestor" in r.getMessage()] def test_no_log_when_no_chain_member_applies(self, caplog): - policies: Final = { + policies: Final = { # mutable-ok: PolicyResolver takes dict[str, Policy] "parent": Policy( - guardrails=PolicyGuardrails(add=["g-parent"]), + guardrails=PolicyGuardrails(add=["g-parent"]), # mutable-ok: pydantic list field condition=PolicyCondition(model="claude-opus-4-1"), ), "child": Policy( inherit="parent", - guardrails=PolicyGuardrails(add=["g-child"]), + guardrails=PolicyGuardrails(add=["g-child"]), # mutable-ok: pydantic list field condition=PolicyCondition(model="gpt-5.5"), ), } diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py index 3c0f28f5bf2..782ce40e624 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py @@ -5389,13 +5389,13 @@ def test_baseline_estimate_metadata_comes_from_the_logging_stamp() -> None: supplied: Final = MappingProxyType({"version": 1, "status": "estimated", "reason": "caller_supplied"}) recorded: Final = MappingProxyType({"version": 1, "status": "unknown", "reason": "history_unavailable"}) result: Final = _get_spend_logs_metadata( - {"autorouter_savings": 999.0, "autorouter_savings_estimate": supplied}, + {"autorouter_savings": 999.0, "autorouter_savings_estimate": supplied}, # mutable-ok: legacy metadata helper accepts dicts autorouter_savings=None, autorouter_savings_estimate=recorded, ) assert result["autorouter_savings"] is None assert result["autorouter_savings_estimate"] == recorded - absent: Final = _get_spend_logs_metadata({"autorouter_savings_estimate": supplied}) + absent: Final = _get_spend_logs_metadata({"autorouter_savings_estimate": supplied}) # mutable-ok: legacy metadata helper accepts dicts assert absent["autorouter_savings_estimate"] is None diff --git a/tests/test_litellm/proxy/utils/proxy_logging/test_post_call_failure_hook.py b/tests/test_litellm/proxy/utils/proxy_logging/test_post_call_failure_hook.py index 0e007429789..51145ca687b 100644 --- a/tests/test_litellm/proxy/utils/proxy_logging/test_post_call_failure_hook.py +++ b/tests/test_litellm/proxy/utils/proxy_logging/test_post_call_failure_hook.py @@ -713,13 +713,13 @@ async def test_post_call_failure_hook_non_http_exception_in_callback_swallowed( @pytest.mark.asyncio -@pytest.mark.parametrize("logging_value", (None, "caller-controlled", {"baseline_cache_context": "untrusted"})) +@pytest.mark.parametrize("logging_value", (None, "caller-controlled", {"baseline_cache_context": "untrusted"})) # mutable-ok: emulate an untrusted JSON request field async def test_terminal_baseline_cleanup_ignores_missing_or_untrusted_logging( proxy_logging: ProxyLogging, monkeypatch: pytest.MonkeyPatch, logging_value: object ) -> None: monkeypatch.setattr(litellm, "callbacks", ()) - proxy_logging.alert_types = [] # rebind-ok: isolate the fixture-owned alert configuration - request_data: Final = {"litellm_call_id": "untrusted-logging", "litellm_logging_obj": logging_value} + proxy_logging.alert_types = [] # mutable-ok: disable optional alert sinks for this boundary test # rebind-ok: isolate the fixture-owned alert configuration + request_data: Final = {"litellm_call_id": "untrusted-logging", "litellm_logging_obj": logging_value} # mutable-ok: the production failure owner removes internal fields in place result: Final = await proxy_logging.post_call_failure_hook( # pyright: ignore[reportUnknownMemberType] # exercise the existing proxy terminal owner with its legacy request dictionary contract request_data=request_data, original_exception=ValueError("original provider failure"), diff --git a/tests/unit/integrations/langfuse/test_langfuse_sdk.py b/tests/unit/integrations/langfuse/test_langfuse_sdk.py index 1669b9233b0..c15a12c07cb 100644 --- a/tests/unit/integrations/langfuse/test_langfuse_sdk.py +++ b/tests/unit/integrations/langfuse/test_langfuse_sdk.py @@ -877,7 +877,7 @@ def test_flush_langfuse_tracing_exports_the_queued_spans_of_every_channel(monkey graceful restart must reach the exporter without waiting for the batch interval.""" exporters: Final[ list[InMemorySpanExporter] - ] = [] + ] = [] # mutable-ok: collects the exporters the patched builder hands out def build_in_memory(*, public_key: str, secret_key: str, base_url: str) -> InMemorySpanExporter: exporters.append(InMemorySpanExporter()) diff --git a/tests/unit/integrations/pointfive/test_upload_client.py b/tests/unit/integrations/pointfive/test_upload_client.py index f1196bb6d3c..50ef085386d 100644 --- a/tests/unit/integrations/pointfive/test_upload_client.py +++ b/tests/unit/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()] - self.put_results = list(put) if put else [_accepted()] + 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_calls: list[dict] = [] self.put_calls: list[dict] = [] diff --git a/tests/unit/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py b/tests/unit/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py index c5171da7947..05e3812152e 100644 --- a/tests/unit/llms/fireworks_ai/responses/test_fireworks_ai_responses_transformation.py +++ b/tests/unit/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], + tools=[mcp_tool], # mutable-ok: the Responses API takes tools as a JSON list 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], + input=[tool_output], # mutable-ok: the Responses API takes input items as a JSON list 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=[ + input=[ # mutable-ok: the Responses API takes input as a JSON list {"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=[ + input=[ # mutable-ok: the Responses API takes input as a JSON list { "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=[ + input=[ # mutable-ok: the Responses API takes input as a JSON list {"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=[ + input=[ # mutable-ok: the Responses API takes input as a JSON list {"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=[ + input=[ # mutable-ok: the Responses API takes input as a JSON list {"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?"}], + input=[developer_item, {"role": "user", "content": "What is the capital of France?"}], # mutable-ok: JSON list 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]), - response_api_optional_request_params={"instructions": ["not", "a", "string"]}, + 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 litellm_params=GenericLiteLLMParams(), - headers={}, + headers={}, # mutable-ok: base takes a dict ) 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/unit/test_check_type_discipline.py b/tests/unit/test_check_type_discipline.py index 5b8ea7b56ac..9225b76cbcf 100644 --- a/tests/unit/test_check_type_discipline.py +++ b/tests/unit/test_check_type_discipline.py @@ -113,24 +113,23 @@ def test_mutable_ok_on_a_real_violation_suppresses_and_is_not_lit013(tmp_path): assert "LIT013" not in codes -def test_mutable_ok_on_a_clean_line_is_lit013(tmp_path): +def test_mutable_ok_on_a_line_without_lit001_is_not_lit013(tmp_path): f = tmp_path / "snippet.py" - f.write_text("x: Final = (1, 2) # mutable-ok: stale\n", encoding="utf-8") + f.write_text("x: Final = [] # mutable-ok: seed\ny: Final = (1, 2) # mutable-ok: stale\n", encoding="utf-8") + assert checker.check_file(f) == () + + +def test_cast_ok_on_a_clean_line_is_lit013(tmp_path): + f = tmp_path / "snippet.py" + f.write_text("x: Final = 1 # cast-ok: stale\n", encoding="utf-8") found = checker.check_file(f) assert [v.code for v in found] == ["LIT013"] - assert "mutable-ok" in found[0].message - - -def test_mutable_ok_on_a_construction_only_line_is_lit013(tmp_path): - f = tmp_path / "snippet.py" - f.write_text("x: Final = [] # mutable-ok: seed\n", encoding="utf-8") - assert [v.code for v in checker.check_file(f)] == ["LIT013"] + assert "cast-ok" in found[0].message def test_mutable_ok_does_not_suppress_rebind_codes(tmp_path): codes = _codes(tmp_path, "x = 1 # mutable-ok: wrong token\n") assert "LIT010" in codes - assert "LIT013" in codes def test_rebind_ok_on_a_real_param_rebind_is_not_lit013(tmp_path): diff --git a/tests/unit/types/test_litellm_params.py b/tests/unit/types/test_litellm_params.py index 16ae6b963d0..ab4f6c12431 100644 --- a/tests/unit/types/test_litellm_params.py +++ b/tests/unit/types/test_litellm_params.py @@ -317,7 +317,7 @@ def test_caching_groups_is_a_flat_sequence_of_model_groups_that_share_one_cache_ monkeypatch: pytest.MonkeyPatch, ) -> None: for callback_list in ("input_callback", "success_callback", "_async_success_callback"): - monkeypatch.setattr(litellm, callback_list, []) + monkeypatch.setattr(litellm, callback_list, []) # mutable-ok: Cache() appends "cache" to these lists options: Final = CachingOptions(caching_groups=(("gpt-4", "gpt-4o"), ("claude-3",))) cache: Final = Cache() @@ -394,7 +394,7 @@ def test_owned_wire_names_refuse_a_root_that_declares_a_kwarg_outside_a_leaf() - def test_agentic_loop_names_concatenate_as_a_list() -> None: - extended: Final = agentic_loop_internal_litellm_params + ["caller_added"] + extended: Final = agentic_loop_internal_litellm_params + ["caller_added"] # mutable-ok: list contract under test assert (type(extended), len(extended), frozenset(extended)) == ( list, @@ -417,7 +417,7 @@ def test_proxy_stamped_fields_keep_their_wire_names() -> None: def test_all_litellm_params_concatenates_with_a_list_like_the_completion_entrypoint_does() -> None: - extended: Final = ["aembedding", "extra_headers"] + all_litellm_params + extended: Final = ["aembedding", "extra_headers"] + all_litellm_params # mutable-ok: list contract under test assert (type(extended), frozenset(extended)) == (list, frozenset(("aembedding", "extra_headers", *OWNED_NAMES)))