From 4af58e1f97404659c4f8a5e74b858d474ad6d4b7 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Tue, 5 May 2026 20:21:15 +0000 Subject: [PATCH 1/7] [Security] Clear AWS Inspector CVE findings on Docker image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Narrow /root/.cache COPY in Dockerfile to /root/.cache/prisma{,-python} only — drops ~660MB of uv build cache including a setuptools wheel that surfaced as CVE-2024-6345 / CVE-2025-47273 even though it was never on the runtime sys.path. - DiskCache: switch to dc.JSONDisk to neutralize the pickle code path (CVE-2025-69872, no upstream fix). Values must be JSON-serializable; cleanup get_cache to skip the now-dead json.loads(dict) branch by guarding on isinstance(str). - pyproject.toml: drop diskcache pin from [caching] extra (no fixed version exists). Stub kept so `pip install litellm[caching]` doesn't warn; users who want disk caching install diskcache themselves. - Bump black 24.10.0 → 26.3.1 (CVE-2026-32274) + apply 296-file mechanical reformat. Black is dev-only (not in the runtime image), but bumping clears the manifest-scan finding. - Refresh ui/litellm-dashboard/package-lock.json to pick up next 16.2.4 (was 16.1.7, GHSA-q4gf-8mx6-v5v3), uuid 14.0.0, postcss 8.5.13. - Refresh litellm-js/spend-logs/package-lock.json to pick up hono 4.12.16 (GHSA-458j-xx4x-4375). - uv lock: gitpython 3.1.46 → 3.1.49 (clears two High GHSAs), langchain-text-splitters 1.1.1 → 1.1.2. - Add tests/test_litellm/caching/test_disk_cache.py covering JSONDisk enforcement, dict/string round-trip, TTL, increment, delete/flush. Net delta on combined trivy + grype scans: 17 findings → 4 (all remaining 4 are Wolfi system python-3.13 CVEs marked WONTFIX upstream in CPython 3.14; CVE-2026-3298 is Windows-unreachable on Linux). Existing on-disk caches written by the previous pickle-format Disk will silently miss after upgrade — diskcache is intended to be ephemeral so impact is recreate-on-next-write. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...to_update_price_and_context_window_file.py | 90 +- .../workflows/run_llm_translation_tests.py | 512 ++--- Dockerfile | 10 +- ci_cd/run_migration.py | 1 - cookbook/benchmark/benchmark.py | 1 - .../braintrust_prompt_wrapper_server.py | 1 - cookbook/livekit_agent_sdk/main.py | 2 +- cookbook/misc/migrate_proxy_config.py | 4 +- .../mock_bedrock_guardrail_server.py | 6 +- db_scripts/create_views.py | 6 +- .../enterprise_callbacks/callback_controls.py | 141 +- .../send_emails/base_email.py | 14 +- .../send_emails/sendgrid_email.py | 3 +- .../send_emails/smtp_email.py | 1 + .../litellm_core_utils/litellm_logging.py | 1 + .../proxy/audit_logging_endpoints.py | 10 +- .../litellm_enterprise/proxy/auth/__init__.py | 2 +- .../proxy/common_utils/check_batch_cost.py | 70 +- .../common_utils/check_responses_cost.py | 33 +- .../proxy/hooks/managed_vector_stores.py | 92 +- .../internal_user_endpoints.py | 1 - .../proxy/vector_stores/endpoints.py | 6 +- .../types/enterprise_callbacks/send_emails.py | 12 +- litellm-js/spend-logs/package-lock.json | 6 +- litellm/_uuid.py | 1 - .../exceptions/exception_mapping_utils.py | 1 - .../exceptions/exceptions.py | 1 - litellm/caching/disk_cache.py | 25 +- .../transformation.py | 2 +- litellm/compression/content_detection.py | 1 - litellm/files/types.py | 1 - litellm/google_genai/adapters/__init__.py | 4 +- .../SlackAlerting/batching_handler.py | 6 +- litellm/integrations/SlackAlerting/utils.py | 2 +- .../integrations/additional_logging_utils.py | 2 +- litellm/integrations/custom_batch_logger.py | 2 +- litellm/integrations/focus/transformer.py | 1 - litellm/integrations/opik/utils.py | 2 +- litellm/integrations/s3_v2.py | 4 +- litellm/interactions/__init__.py | 12 +- litellm/interactions/main.py | 10 +- litellm/litellm_core_utils/litellm_logging.py | 12 +- .../prompt_templates/factory.py | 4 +- .../specialty_caches/dynamic_logging_cache.py | 4 +- .../messages/agentic_streaming_iterator.py | 1 - .../azure/chat/o_series_transformation.py | 8 +- .../azure_ai/embed/cohere_transformation.py | 2 +- .../llms/azure_ai/rerank/transformation.py | 2 +- .../embed/amazon_titan_g1_transformation.py | 2 +- .../bedrock/embed/cohere_transformation.py | 2 +- .../bedrock_mantle/chat/transformation.py | 1 - litellm/llms/cohere/embed/handler.py | 2 +- litellm/llms/custom_httpx/mock_transport.py | 1 - litellm/llms/dashscope/cost_calculator.py | 2 +- litellm/llms/datarobot/chat/transformation.py | 2 +- .../llms/deepinfra/rerank/transformation.py | 2 +- litellm/llms/deepseek/cost_calculator.py | 2 +- .../text_to_speech/transformation.py | 1 - litellm/llms/gemini/videos/transformation.py | 2 +- .../llms/infinity/rerank/transformation.py | 2 +- litellm/llms/jina_ai/rerank/transformation.py | 2 +- .../llms/lm_studio/embed/transformation.py | 2 +- litellm/llms/novita/chat/transformation.py | 2 +- .../llms/nvidia_nim/chat/transformation.py | 4 +- litellm/llms/nvidia_nim/embed.py | 2 +- .../openai/chat/o_series_transformation.py | 12 +- litellm/llms/openai/common_utils.py | 2 +- .../image_generation/transformation.py | 1 - .../sagemaker/completion/transformation.py | 2 +- .../sagemaker/embedding/transformation.py | 2 +- litellm/llms/sap/credentials.py | 2 +- litellm/llms/snowflake/chat/transformation.py | 1 - litellm/llms/together_ai/chat.py | 2 +- litellm/llms/together_ai/embed.py | 2 +- .../llms/together_ai/rerank/transformation.py | 2 +- .../context_caching/transformation.py | 4 +- .../llms/vertex_ai/gemini/transformation.py | 6 +- .../batch_embed_content_transformation.py | 2 +- .../text_to_speech/text_to_speech_handler.py | 2 +- .../llms/vllm/completion/transformation.py | 2 +- .../embedding/transformation_contextual.py | 4 +- .../proxy/common_utils/custom_openapi_spec.py | 6 +- .../proxy/common_utils/http_parsing_utils.py | 2 +- .../common_utils/openai_endpoint_utils.py | 2 +- .../pass_through_endpoints.py | 2 +- litellm/proxy/db/create_views.py | 6 +- litellm/proxy/guardrails/_content_utils.py | 1 - .../guardrail_hooks/akto/__init__.py | 1 - .../proxy/hooks/litellm_skills/__init__.py | 2 +- .../budget_management_endpoints.py | 4 +- .../customer_endpoints.py | 4 +- .../model_management_endpoints.py | 2 +- .../sso/custom_microsoft_sso.py | 2 +- .../management_endpoints/team_endpoints.py | 4 +- .../user_agent_analytics_endpoints.py | 2 +- .../cursor_passthrough_logging_handler.py | 1 - litellm/proxy/proxy_cli.py | 11 +- litellm/proxy/proxy_server.py | 6 +- .../spend_management_endpoints.py | 6 +- litellm/proxy/utils.py | 12 +- .../vertex_ai_endpoints/langfuse_endpoints.py | 2 +- litellm/router.py | 2 +- .../router_strategy/adaptive_router/hooks.py | 2 +- .../adaptive_router/signals.py | 1 - litellm/router_strategy/budget_limiter.py | 8 +- litellm/router_utils/get_retry_from_policy.py | 2 +- .../router_utils/pattern_match_deployments.py | 2 +- .../track_deployment_metrics.py | 2 +- litellm/secret_managers/aws_secret_manager.py | 2 +- .../secret_managers/aws_secret_manager_v2.py | 2 +- litellm/vector_store_files/utils.py | 4 +- pyproject.toml | 8 +- scripts/adaptive_router_demo/eval.py | 54 +- scripts/adaptive_router_demo/traffic.py | 59 +- scripts/benchmark_mock.py | 1 - scripts/benchmark_proxy_vs_provider.py | 2 +- scripts/eval_compression.py | 180 +- .../benchmark_get_all_latest_health_checks.py | 2 +- .../test_batch_custom_pricing.py | 1 - .../test_hosted_vllm_batches_and_files.py | 1 - tests/benchmarks/test_benchmarks.py | 1 - .../check_fastuuid_usage.py | 1 - tests/enterprise/conftest.py | 2 - .../integrations/test_prometheus.py | 86 +- .../proxy/auth/test_user_api_key_auth.py | 11 +- .../test_apply_guardrail_endpoint.py | 4 +- .../proxy/hooks/test_managed_files.py | 674 +++---- .../test_internal_user_endpoints.py | 63 +- .../test_project_endpoints_prisma.py | 4 +- .../guardrails_tests/test_akto_guardrails.py | 1 - .../guardrails_tests/test_custom_guardrail.py | 1 - .../test_eu_ai_act_article5.py | 1 - .../test_sg_mas_ai_guardrails.py | 1 - .../test_sg_pdpa_guardrails.py | 1 - .../image_gen_tests/test_image_generation.py | 1 - tests/image_gen_tests/test_image_variation.py | 1 - .../llms/bedrock/embed/test_embedding.py | 1 - .../llms/bedrock/test_nova_imported_models.py | 1 - .../vertex_ai/test_gemini_batch_embeddings.py | 16 +- tests/litellm/test_bedrock_nemotron_super.py | 1 - .../test_bedrock_converse_dedup_factory.py | 19 +- .../test_aws_secret_manager.py | 1 - .../test_litellm_overhead.py | 1 - tests/llm_responses_api_testing/conftest.py | 1 - .../test_anthropic_responses_api.py | 1 - ...est_anthropic_tool_result_empty_call_id.py | 2 +- ...t_base_responses_api_streaming_iterator.py | 4 +- .../test_responses_hooks.py | 4 +- tests/llm_translation/conftest.py | 1 - .../test_bedrock_anthropic_regression.py | 8 +- .../test_bedrock_invoke_tests.py | 1 - tests/llm_translation/test_crusoe.py | 7 +- tests/llm_translation/test_gemini.py | 21 +- .../test_gemini_image_usage.py | 2 +- .../create_mock_standard_logging_payload.py | 1 - .../test_amazing_vertex_completion.py | 9 +- tests/local_testing/test_cache_preset_key.py | 2 +- tests/local_testing/test_caching.py | 7 +- tests/local_testing/test_completion.py | 6 +- .../test_configs/custom_callbacks.py | 6 +- tests/local_testing/test_get_llm_provider.py | 1 - tests/local_testing/test_langsmith.py | 1 - tests/local_testing/test_router_caching.py | 1 - tests/logging_callback_tests/conftest.py | 1 - .../create_mock_standard_logging_payload.py | 1 - .../test_bedrock_knowledgebase_hook.py | 1 - .../test_datadog_llm_obs.py | 1 - .../test_gcs_pub_sub.py | 1 - .../test_generic_api_callback.py | 1 - .../test_langsmith_unit_test.py | 1 - .../test_log_db_redis_services.py | 1 - .../test_view_request_resp_logs.py | 1 - tests/mcp_tests/test_mcp_logging.py | 1 - tests/mcp_tests/test_mcp_server.py | 1 - tests/mcp_tests/test_proxy_mcp_e2e.py | 1 - tests/ocr_tests/base_ocr_unit_tests.py | 1 - .../test_ocr_azure_document_intelligence.py | 5 +- .../tests/bursty_load_test_completion.py | 1 - .../tests/load_test_embedding_100.py | 1 - .../test_openai_request_with_traceparent.py | 1 - .../test_openai_batches_endpoint.py | 1 - .../test_openai_files_endpoints.py | 1 - tests/otel_tests/test_e2e_model_access.py | 1 - .../test_team_member_permissions.py | 12 +- .../test_openai_assistants_passthrough.py | 1 - tests/pass_through_tests/test_vertex_ai.py | 1 - ..._anthropic_messages_prompt_caching_test.py | 8 +- .../test_route_check_unit_tests.py | 1 - .../test_usage_endpoints.py | 4 +- .../test_claude_agent_sdk.py | 1 - tests/proxy_unit_tests/conftest.py | 1 - .../test_configs/custom_callbacks.py | 6 +- .../proxy_unit_tests/test_jwt_key_mapping.py | 1 - .../test_search_api_logging.py | 2 +- .../test_user_api_key_auth.py | 4 +- .../create_mock_standard_logging_payload.py | 1 - .../test_router_handle_error.py | 1 - tests/search_tests/test_google_pse_search.py | 1 - .../test_callbacks_in_db.py | 4 +- .../test_bedrock_agentcore_a2a.py | 1 - tests/test_litellm/caching/test_disk_cache.py | 70 + .../send_emails/test_base_email.py | 179 +- .../send_emails/test_resend_email.py | 11 +- .../send_emails/test_sendgrid_email.py | 6 +- .../test_callback_controls.py | 332 +++- .../test_batch_retrieve_input_file_id.py | 23 +- ..._retrieve_returns_unified_input_file_id.py | 4 +- .../proxy/test_enterprise_routes.py | 48 +- .../proxy/test_file_deletion_blocking.py | 188 +- .../proxy/test_managed_files_hook.py | 18 +- .../google_genai/test_google_genai_adapter.py | 1 + .../test_google_genai_adapter_fixes.py | 1 + .../google_genai/test_google_genai_handler.py | 1 + .../google_genai/test_google_genai_main.py | 1 + .../test_google_genai_transformation.py | 1 + .../arize/test_arize_otel_coexistence.py | 1 - .../dotprompt/test_prompt_manager.py | 18 +- .../integrations/test_custom_guardrail.py | 9 +- .../test_websearch_thinking_constraint.py | 1 - .../test_azure_assistant_cost_tracking.py | 2 +- .../litellm_core_utils/test_core_helpers.py | 24 +- .../test_streaming_handler.py | 14 +- .../litellm_core_utils/test_token_counter.py | 1 - ...al_pass_through_adapters_transformation.py | 4 +- .../test_agentic_streaming_iterator.py | 1 - .../messages/test_parallel_tool_calls.py | 1 - .../test_reasoning_auto_summary_messages.py | 22 +- .../llms/azure/test_azure_cost_calculation.py | 1 - .../azure_ai/test_azure_ai_cost_calculator.py | 25 +- .../test_managed_resource_isolation.py | 1 - .../llms/bedrock/chat/test_invoke_handler.py | 1 - tests/test_litellm/llms/crusoe/test_crusoe.py | 13 +- .../custom_httpx/test_aiohttp_so_keepalive.py | 5 +- .../llms/custom_httpx/test_mock_transport.py | 1 - .../llms/databricks/test_databricks_e2e.py | 6 +- ...gram_audio_transcription_transformation.py | 2 +- .../test_github_copilot_authenticator.py | 64 +- .../test_huggingface_embedding_handler.py | 1 - .../test_moonshot_chat_transformation.py | 4 +- .../test_perplexity_cost_calculator.py | 2 +- .../perplexity/test_perplexity_integration.py | 2 +- ...eway_audio_transcription_transformation.py | 1 - .../llms/test_polling_url_origin_match.py | 1 - .../llms/test_predibase_transformation.py | 25 +- .../gemini/test_context_circulation.py | 1 - ...test_vertex_and_google_ai_studio_gemini.py | 29 +- ...test_batch_embed_content_transformation.py | 14 +- .../vertex_ai/test_vertex_ai_common_utils.py | 6 +- .../test_vertex_model_garden_openapi.py | 5 +- .../auth/test_user_api_key_auth_mcp.py | 4 +- .../mcp_server/test_db_credentials.py | 1 - .../mcp_server/test_is_tool_name_prefixed.py | 1 - .../mcp_server/test_mcp_server.py | 17 +- .../mcp_server/test_mcp_stale_session.py | 116 +- .../mcp_server/test_semantic_tool_filter.py | 28 +- .../test_agent_header_isolation.py | 1 - .../agent_endpoints/test_agent_headers.py | 1 - .../proxy/auth/test_onboarding.py | 1 - .../proxy/common_utils/test_cache_codec.py | 8 +- tests/test_litellm/proxy/conftest.py | 1 - .../test_google_api_endpoints.py | 1 + .../content_filter/test_content_filter.py | 6 +- .../openai/test_moderations.py | 1 + .../proxy/guardrails/test_content_utils.py | 1 - .../guardrails/test_custom_code_security.py | 1 - .../proxy/guardrails/test_llm_as_a_judge.py | 61 +- .../test_qostodian_nexus_guardrail.py | 6 +- .../proxy/hooks/test_batch_file_validation.py | 1 - .../test_activity_tenant_scoping.py | 1 - .../test_budget_endpoints.py | 1 - .../test_delete_verification_tokens_failed.py | 1 - .../test_project_org_authz.py | 1 - .../test_team_default_params.py | 1 - .../test_workflow_management_endpoints.py | 1 - .../usage_endpoints/test_ai_usage_chat.py | 1 - .../test_llm_pass_through_endpoints.py | 6 +- .../proxy/test_fastapi_offline_routes.py | 2 +- ...test_filter_models_by_team_access_group.py | 4 +- .../proxy/test_model_level_guardrails.py | 1 - .../proxy/test_redis_auth_cache_flag.py | 1 - .../test_litellm/proxy/test_team_org_move.py | 31 +- .../test_responses_api_bridge_flag.py | 16 +- .../adaptive_router/test_router_dispatch.py | 4 +- .../test_acompletion_session_reuse_e2e.py | 1 - .../test_anthropic_skills_transformation.py | 1 - .../test_dashscope_image_generation.py | 1 - .../test_deepseek_model_metadata.py | 1 - tests/test_litellm/test_model_cost_aliases.py | 1 - tests/test_litellm/test_nested_drop_params.py | 1 - ...penai_embedding_encoding_format_default.py | 4 +- .../test_litellm/test_router_google_genai.py | 1 + .../test_streaming_connection_cleanup.py | 1 - tests/test_litellm/test_utils.py | 24 +- tests/test_litellm/types/test_completion.py | 2 +- .../test_prometheus_label_value_sanitize.py | 3 +- tests/test_passthrough_endpoints.py | 1 - .../vector_store_tests/rag/base_rag_tests.py | 4 +- .../rag/test_rag_vertex_ai.py | 8 +- .../test_milvus_vector_store.py | 1 - .../test_vertex_ai_search_api_vector_store.py | 1 - .../fixtures/mock_llm_server/server.py | 1 - ui/litellm-dashboard/package-lock.json | 1642 +++++++++-------- uv.lock | 89 +- 303 files changed, 3342 insertions(+), 2649 deletions(-) create mode 100644 tests/test_litellm/caching/test_disk_cache.py diff --git a/.github/workflows/auto_update_price_and_context_window_file.py b/.github/workflows/auto_update_price_and_context_window_file.py index 461d8d347d9..b92a0568e37 100644 --- a/.github/workflows/auto_update_price_and_context_window_file.py +++ b/.github/workflows/auto_update_price_and_context_window_file.py @@ -2,6 +2,7 @@ import asyncio import aiohttp import json + # Asynchronously fetch data from a given URL async def fetch_data(url): try: @@ -15,22 +16,24 @@ async def fetch_data(url): resp_json = await resp.json() print("Fetch the data from URL.") # Return the 'data' field from the JSON response - return resp_json['data'] + return resp_json["data"] except Exception as e: # Print an error message if fetching data fails print("Error fetching data from URL:", e) return None + # Synchronize local data with remote data def sync_local_data_with_remote(local_data, remote_data): # Update existing keys in local_data with values from remote_data - for key in (set(local_data) & set(remote_data)): + for key in set(local_data) & set(remote_data): local_data[key].update(remote_data[key]) # Add new keys from remote_data to local_data - for key in (set(remote_data) - set(local_data)): + for key in set(remote_data) - set(local_data): local_data[key] = remote_data[key] + # Write data to the json file def write_to_file(file_path, data): try: @@ -43,6 +46,7 @@ def write_to_file(file_path, data): # Print an error message if writing to file fails print("Error updating JSON file:", e) + # Update the existing models and add the missing models for OpenRouter def transform_openrouter_data(data): transformed = {} @@ -54,33 +58,41 @@ def transform_openrouter_data(data): } # Add 'max_output_tokens' as a field if it is not None - if "top_provider" in row and "max_completion_tokens" in row["top_provider"] and row["top_provider"]["max_completion_tokens"] is not None: - obj['max_output_tokens'] = int(row["top_provider"]["max_completion_tokens"]) + if ( + "top_provider" in row + and "max_completion_tokens" in row["top_provider"] + and row["top_provider"]["max_completion_tokens"] is not None + ): + obj["max_output_tokens"] = int(row["top_provider"]["max_completion_tokens"]) # Add the field 'output_cost_per_token' - obj.update({ - "output_cost_per_token": float(row["pricing"]["completion"]), - }) + obj.update( + { + "output_cost_per_token": float(row["pricing"]["completion"]), + } + ) # Add field 'input_cost_per_image' if it exists and is non-zero - if "pricing" in row and "image" in row["pricing"] and float(row["pricing"]["image"]) != 0.0: - obj['input_cost_per_image'] = float(row["pricing"]["image"]) + if ( + "pricing" in row + and "image" in row["pricing"] + and float(row["pricing"]["image"]) != 0.0 + ): + obj["input_cost_per_image"] = float(row["pricing"]["image"]) # Add the fields 'litellm_provider' and 'mode' - obj.update({ - "litellm_provider": "openrouter", - "mode": "chat" - }) + obj.update({"litellm_provider": "openrouter", "mode": "chat"}) # Add the 'supports_vision' field if the modality is 'multimodal' - if row.get('architecture', {}).get('modality') == 'multimodal': - obj['supports_vision'] = True + if row.get("architecture", {}).get("modality") == "multimodal": + obj["supports_vision"] = True # Use a composite key to store the transformed object transformed[f'openrouter/{row["id"]}'] = obj return transformed + # Update the existing models and add the missing models for Vercel AI Gateway def transform_vercel_ai_gateway_data(data): transformed = {} @@ -89,20 +101,30 @@ def transform_vercel_ai_gateway_data(data): "max_tokens": row["context_window"], "input_cost_per_token": float(row["pricing"]["input"]), "output_cost_per_token": float(row["pricing"]["output"]), - 'max_output_tokens': row['max_tokens'], - 'max_input_tokens': row["context_window"], + "max_output_tokens": row["max_tokens"], + "max_input_tokens": row["context_window"], } # Handle cache pricing if available if "pricing" in row: - if "input_cache_read" in row["pricing"] and row["pricing"]["input_cache_read"] is not None: - obj['cache_read_input_token_cost'] = float(f"{float(row['pricing']['input_cache_read']):e}") - - if "input_cache_write" in row["pricing"] and row["pricing"]["input_cache_write"] is not None: - obj['cache_creation_input_token_cost'] = float(f"{float(row['pricing']['input_cache_write']):e}") + if ( + "input_cache_read" in row["pricing"] + and row["pricing"]["input_cache_read"] is not None + ): + obj["cache_read_input_token_cost"] = float( + f"{float(row['pricing']['input_cache_read']):e}" + ) + + if ( + "input_cache_write" in row["pricing"] + and row["pricing"]["input_cache_write"] is not None + ): + obj["cache_creation_input_token_cost"] = float( + f"{float(row['pricing']['input_cache_write']):e}" + ) mode = "embedding" if "embedding" in row["id"].lower() else "chat" - + obj.update({"litellm_provider": "vercel_ai_gateway", "mode": mode}) transformed[f'vercel_ai_gateway/{row["id"]}'] = obj @@ -126,24 +148,31 @@ def load_local_data(file_path): print("Error decoding JSON:", e) return None + def main(): - local_file_path = "model_prices_and_context_window.json" # Path to the local data file - openrouter_url = "https://openrouter.ai/api/v1/models" # URL to fetch OpenRouter data - vercel_ai_gateway_url = "https://ai-gateway.vercel.sh/v1/models" # URL to fetch Vercel AI Gateway data + local_file_path = ( + "model_prices_and_context_window.json" # Path to the local data file + ) + openrouter_url = ( + "https://openrouter.ai/api/v1/models" # URL to fetch OpenRouter data + ) + vercel_ai_gateway_url = ( + "https://ai-gateway.vercel.sh/v1/models" # URL to fetch Vercel AI Gateway data + ) # Load local data from file local_data = load_local_data(local_file_path) - + # Fetch OpenRouter data openrouter_data = asyncio.run(fetch_data(openrouter_url)) # Transform the fetched OpenRouter data openrouter_data = transform_openrouter_data(openrouter_data) - + # Fetch Vercel AI Gateway data vercel_data = asyncio.run(fetch_data(vercel_ai_gateway_url)) # Transform the fetched Vercel AI Gateway data vercel_data = transform_vercel_ai_gateway_data(vercel_data) - + # Combine both datasets all_remote_data = {**openrouter_data, **vercel_data} @@ -154,6 +183,7 @@ def main(): else: print("Failed to fetch model data from either local file or URL.") + # Entry point of the script if __name__ == "__main__": main() diff --git a/.github/workflows/run_llm_translation_tests.py b/.github/workflows/run_llm_translation_tests.py index 3f3a70efe92..22be769a739 100644 --- a/.github/workflows/run_llm_translation_tests.py +++ b/.github/workflows/run_llm_translation_tests.py @@ -16,64 +16,75 @@ from pathlib import Path import json from typing import Dict, List, Tuple, Optional + # ANSI color codes for terminal output class Colors: - GREEN = '\033[92m' - RED = '\033[91m' - YELLOW = '\033[93m' - BLUE = '\033[94m' - PURPLE = '\033[95m' - CYAN = '\033[96m' - RESET = '\033[0m' - BOLD = '\033[1m' + GREEN = "\033[92m" + RED = "\033[91m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + PURPLE = "\033[95m" + CYAN = "\033[96m" + RESET = "\033[0m" + BOLD = "\033[1m" + def print_colored(message: str, color: str = Colors.RESET): """Print colored message to terminal""" print(f"{color}{message}{Colors.RESET}") + def get_provider_from_test_file(test_file: str) -> str: """Map test file names to provider names""" provider_mapping = { - 'test_anthropic': 'Anthropic', - 'test_azure': 'Azure', - 'test_bedrock': 'AWS Bedrock', - 'test_openai': 'OpenAI', - 'test_vertex': 'Google Vertex AI', - 'test_gemini': 'Google Vertex AI', - 'test_cohere': 'Cohere', - 'test_databricks': 'Databricks', - 'test_groq': 'Groq', - 'test_together': 'Together AI', - 'test_mistral': 'Mistral', - 'test_deepseek': 'DeepSeek', - 'test_replicate': 'Replicate', - 'test_huggingface': 'HuggingFace', - 'test_fireworks': 'Fireworks AI', - 'test_perplexity': 'Perplexity', - 'test_cloudflare': 'Cloudflare', - 'test_voyage': 'Voyage AI', - 'test_xai': 'xAI', - 'test_nvidia': 'NVIDIA', - 'test_watsonx': 'IBM watsonx', - 'test_azure_ai': 'Azure AI', - 'test_snowflake': 'Snowflake', - 'test_infinity': 'Infinity', - 'test_jina': 'Jina AI', - 'test_deepgram': 'Deepgram', - 'test_clarifai': 'Clarifai', - 'test_triton': 'Triton', + "test_anthropic": "Anthropic", + "test_azure": "Azure", + "test_bedrock": "AWS Bedrock", + "test_openai": "OpenAI", + "test_vertex": "Google Vertex AI", + "test_gemini": "Google Vertex AI", + "test_cohere": "Cohere", + "test_databricks": "Databricks", + "test_groq": "Groq", + "test_together": "Together AI", + "test_mistral": "Mistral", + "test_deepseek": "DeepSeek", + "test_replicate": "Replicate", + "test_huggingface": "HuggingFace", + "test_fireworks": "Fireworks AI", + "test_perplexity": "Perplexity", + "test_cloudflare": "Cloudflare", + "test_voyage": "Voyage AI", + "test_xai": "xAI", + "test_nvidia": "NVIDIA", + "test_watsonx": "IBM watsonx", + "test_azure_ai": "Azure AI", + "test_snowflake": "Snowflake", + "test_infinity": "Infinity", + "test_jina": "Jina AI", + "test_deepgram": "Deepgram", + "test_clarifai": "Clarifai", + "test_triton": "Triton", } - + for key, provider in provider_mapping.items(): if key in test_file: return provider - + # For cross-provider test files - if any(name in test_file for name in ['test_optional_params', 'test_prompt_factory', - 'test_router', 'test_text_completion']): - return f'Cross-Provider Tests ({test_file})' - - return 'Other Tests' + if any( + name in test_file + for name in [ + "test_optional_params", + "test_prompt_factory", + "test_router", + "test_text_completion", + ] + ): + return f"Cross-Provider Tests ({test_file})" + + return "Other Tests" + def format_duration(seconds: float) -> str: """Format duration in human-readable format""" @@ -89,290 +100,355 @@ def format_duration(seconds: float) -> str: return f"{hours}h {minutes}m" -def generate_markdown_report(junit_xml_path: str, output_path: str, tag: str = None, commit: str = None): +def generate_markdown_report( + junit_xml_path: str, output_path: str, tag: str = None, commit: str = None +): """Generate a beautiful markdown report from JUnit XML""" try: tree = ET.parse(junit_xml_path) root = tree.getroot() - + # Handle both testsuite and testsuites root - if root.tag == 'testsuites': - suites = root.findall('testsuite') + if root.tag == "testsuites": + suites = root.findall("testsuite") else: suites = [root] - + # Overall statistics total_tests = 0 total_failures = 0 total_errors = 0 total_skipped = 0 total_time = 0.0 - + # Provider breakdown - provider_stats = defaultdict(lambda: {'passed': 0, 'failed': 0, 'skipped': 0, 'errors': 0, 'time': 0.0}) + provider_stats = defaultdict( + lambda: {"passed": 0, "failed": 0, "skipped": 0, "errors": 0, "time": 0.0} + ) provider_tests = defaultdict(list) - + for suite in suites: - total_tests += int(suite.get('tests', 0)) - total_failures += int(suite.get('failures', 0)) - total_errors += int(suite.get('errors', 0)) - total_skipped += int(suite.get('skipped', 0)) - total_time += float(suite.get('time', 0)) - - for testcase in suite.findall('testcase'): - classname = testcase.get('classname', '') - test_name = testcase.get('name', '') - test_time = float(testcase.get('time', 0)) - + total_tests += int(suite.get("tests", 0)) + total_failures += int(suite.get("failures", 0)) + total_errors += int(suite.get("errors", 0)) + total_skipped += int(suite.get("skipped", 0)) + total_time += float(suite.get("time", 0)) + + for testcase in suite.findall("testcase"): + classname = testcase.get("classname", "") + test_name = testcase.get("name", "") + test_time = float(testcase.get("time", 0)) + # Extract test file name from classname - if '.' in classname: - parts = classname.split('.') - test_file = parts[-2] if len(parts) > 1 else 'unknown' + if "." in classname: + parts = classname.split(".") + test_file = parts[-2] if len(parts) > 1 else "unknown" else: - test_file = 'unknown' - + test_file = "unknown" + provider = get_provider_from_test_file(test_file) - provider_stats[provider]['time'] += test_time - + provider_stats[provider]["time"] += test_time + # Check test status - if testcase.find('failure') is not None: - provider_stats[provider]['failed'] += 1 - failure = testcase.find('failure') - failure_msg = failure.get('message', '') if failure is not None else '' - provider_tests[provider].append({ - 'name': test_name, - 'status': 'FAILED', - 'time': test_time, - 'message': failure_msg - }) - elif testcase.find('error') is not None: - provider_stats[provider]['errors'] += 1 - error = testcase.find('error') - error_msg = error.get('message', '') if error is not None else '' - provider_tests[provider].append({ - 'name': test_name, - 'status': 'ERROR', - 'time': test_time, - 'message': error_msg - }) - elif testcase.find('skipped') is not None: - provider_stats[provider]['skipped'] += 1 - skip = testcase.find('skipped') - skip_msg = skip.get('message', '') if skip is not None else '' - provider_tests[provider].append({ - 'name': test_name, - 'status': 'SKIPPED', - 'time': test_time, - 'message': skip_msg - }) + if testcase.find("failure") is not None: + provider_stats[provider]["failed"] += 1 + failure = testcase.find("failure") + failure_msg = ( + failure.get("message", "") if failure is not None else "" + ) + provider_tests[provider].append( + { + "name": test_name, + "status": "FAILED", + "time": test_time, + "message": failure_msg, + } + ) + elif testcase.find("error") is not None: + provider_stats[provider]["errors"] += 1 + error = testcase.find("error") + error_msg = error.get("message", "") if error is not None else "" + provider_tests[provider].append( + { + "name": test_name, + "status": "ERROR", + "time": test_time, + "message": error_msg, + } + ) + elif testcase.find("skipped") is not None: + provider_stats[provider]["skipped"] += 1 + skip = testcase.find("skipped") + skip_msg = skip.get("message", "") if skip is not None else "" + provider_tests[provider].append( + { + "name": test_name, + "status": "SKIPPED", + "time": test_time, + "message": skip_msg, + } + ) else: - provider_stats[provider]['passed'] += 1 - provider_tests[provider].append({ - 'name': test_name, - 'status': 'PASSED', - 'time': test_time, - 'message': '' - }) - + provider_stats[provider]["passed"] += 1 + provider_tests[provider].append( + { + "name": test_name, + "status": "PASSED", + "time": test_time, + "message": "", + } + ) + passed = total_tests - total_failures - total_errors - total_skipped - + # Generate the markdown report - with open(output_path, 'w') as f: + with open(output_path, "w") as f: # Header f.write("# LLM Translation Test Results\n\n") - + # Metadata table f.write("## Test Run Information\n\n") f.write("| Field | Value |\n") f.write("|-------|-------|\n") f.write(f"| **Tag** | `{tag or 'N/A'}` |\n") - f.write(f"| **Date** | {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} |\n") + f.write( + f"| **Date** | {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} |\n" + ) f.write(f"| **Commit** | `{commit or 'N/A'}` |\n") f.write(f"| **Duration** | {format_duration(total_time)} |\n") f.write("\n") - + # Overall statistics with visual elements f.write("## Overall Statistics\n\n") - + # Summary box f.write("```\n") f.write(f"Total Tests: {total_tests}\n") - f.write(f"├── Passed: {passed:>4} ({(passed/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") - f.write(f"├── Failed: {total_failures:>4} ({(total_failures/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") - f.write(f"├── Errors: {total_errors:>4} ({(total_errors/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") - f.write(f"└── Skipped: {total_skipped:>4} ({(total_skipped/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") + f.write( + f"├── Passed: {passed:>4} ({(passed/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" + ) + f.write( + f"├── Failed: {total_failures:>4} ({(total_failures/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" + ) + f.write( + f"├── Errors: {total_errors:>4} ({(total_errors/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" + ) + f.write( + f"└── Skipped: {total_skipped:>4} ({(total_skipped/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" + ) f.write("```\n\n") - - + # Provider summary table f.write("## Results by Provider\n\n") - f.write("| Provider | Total | Pass | Fail | Error | Skip | Pass Rate | Duration |\n") - f.write("|----------|-------|------|------|-------|------|-----------|----------|") - + f.write( + "| Provider | Total | Pass | Fail | Error | Skip | Pass Rate | Duration |\n" + ) + f.write( + "|----------|-------|------|------|-------|------|-----------|----------|" + ) + # Sort providers: specific providers first, then cross-provider tests sorted_providers = [] cross_provider = [] for p in sorted(provider_stats.keys()): - if 'Cross-Provider' in p or p == 'Other Tests': + if "Cross-Provider" in p or p == "Other Tests": cross_provider.append(p) else: sorted_providers.append(p) - + all_providers = sorted_providers + cross_provider - + for provider in all_providers: stats = provider_stats[provider] - total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped'] - pass_rate = (stats['passed'] / total * 100) if total > 0 else 0 - - f.write(f"\n| {provider} | {total} | {stats['passed']} | {stats['failed']} | ") + total = ( + stats["passed"] + + stats["failed"] + + stats["errors"] + + stats["skipped"] + ) + pass_rate = (stats["passed"] / total * 100) if total > 0 else 0 + + f.write( + f"\n| {provider} | {total} | {stats['passed']} | {stats['failed']} | " + ) f.write(f"{stats['errors']} | {stats['skipped']} | {pass_rate:.1f}% | ") f.write(f"{format_duration(stats['time'])} |") - + # Detailed test results by provider f.write("\n\n## Detailed Test Results\n\n") - + for provider in sorted_providers: if provider_tests[provider]: stats = provider_stats[provider] - total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped'] - + total = ( + stats["passed"] + + stats["failed"] + + stats["errors"] + + stats["skipped"] + ) + f.write(f"### {provider}\n\n") f.write(f"**Summary:** {stats['passed']}/{total} passed ") - f.write(f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%) ") + f.write( + f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%) " + ) f.write(f"in {format_duration(stats['time'])}\n\n") - + # Group tests by status tests_by_status = defaultdict(list) for test in provider_tests[provider]: - tests_by_status[test['status']].append(test) - + tests_by_status[test["status"]].append(test) + # Show failed tests first (if any) - if tests_by_status['FAILED']: + if tests_by_status["FAILED"]: f.write("
\nFailed Tests\n\n") - for test in tests_by_status['FAILED']: + for test in tests_by_status["FAILED"]: f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n") - if test['message']: + if test["message"]: # Truncate long error messages - msg = test['message'][:200] + '...' if len(test['message']) > 200 else test['message'] + msg = ( + test["message"][:200] + "..." + if len(test["message"]) > 200 + else test["message"] + ) f.write(f" > {msg}\n") f.write("\n
\n\n") - + # Show errors (if any) - if tests_by_status['ERROR']: + if tests_by_status["ERROR"]: f.write("
\nError Tests\n\n") - for test in tests_by_status['ERROR']: + for test in tests_by_status["ERROR"]: f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n") f.write("\n
\n\n") - + # Show passed tests in collapsible section - if tests_by_status['PASSED']: + if tests_by_status["PASSED"]: f.write("
\nPassed Tests\n\n") - for test in tests_by_status['PASSED']: + for test in tests_by_status["PASSED"]: f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n") f.write("\n
\n\n") - + # Show skipped tests (if any) - if tests_by_status['SKIPPED']: + if tests_by_status["SKIPPED"]: f.write("
\nSkipped Tests\n\n") - for test in tests_by_status['SKIPPED']: + for test in tests_by_status["SKIPPED"]: f.write(f"- `{test['name']}`\n") f.write("\n
\n\n") - + # Cross-provider tests in a separate section if cross_provider: f.write("### Cross-Provider Tests\n\n") for provider in cross_provider: if provider_tests[provider]: stats = provider_stats[provider] - total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped'] - + total = ( + stats["passed"] + + stats["failed"] + + stats["errors"] + + stats["skipped"] + ) + f.write(f"#### {provider}\n\n") f.write(f"**Summary:** {stats['passed']}/{total} passed ") - f.write(f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%)\n\n") - + f.write( + f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%)\n\n" + ) + # For cross-provider tests, just show counts f.write(f"- Passed: {stats['passed']}\n") - if stats['failed'] > 0: + if stats["failed"] > 0: f.write(f"- Failed: {stats['failed']}\n") - if stats['errors'] > 0: + if stats["errors"] > 0: f.write(f"- Errors: {stats['errors']}\n") - if stats['skipped'] > 0: + if stats["skipped"] > 0: f.write(f"- Skipped: {stats['skipped']}\n") f.write("\n") - - + print_colored(f"Report generated: {output_path}", Colors.GREEN) - + except Exception as e: print_colored(f"Error generating report: {e}", Colors.RED) raise -def run_tests(test_path: str = "tests/llm_translation/", - junit_xml: str = "test-results/junit.xml", - report_path: str = "test-results/llm_translation_report.md", - tag: str = None, - commit: str = None) -> int: + +def run_tests( + test_path: str = "tests/llm_translation/", + junit_xml: str = "test-results/junit.xml", + report_path: str = "test-results/llm_translation_report.md", + tag: str = None, + commit: str = None, +) -> int: """Run the LLM translation tests and generate report""" - + # Create test results directory os.makedirs(os.path.dirname(junit_xml), exist_ok=True) - + print_colored("Starting LLM Translation Tests", Colors.BOLD + Colors.BLUE) print_colored(f"Test directory: {test_path}", Colors.CYAN) print_colored(f"Output: {junit_xml}", Colors.CYAN) print() - + # Run pytest cmd = [ - "uv", "run", "--no-sync", "pytest", test_path, + "uv", + "run", + "--no-sync", + "pytest", + test_path, f"--junitxml={junit_xml}", "-v", "--tb=short", "--maxfail=500", - "-n", "auto" + "-n", + "auto", ] - + # Add timeout if pytest-timeout is installed try: - subprocess.run(["uv", "run", "--no-sync", "python", "-c", "import pytest_timeout"], - capture_output=True, check=True) + subprocess.run( + ["uv", "run", "--no-sync", "python", "-c", "import pytest_timeout"], + capture_output=True, + check=True, + ) cmd.extend(["--timeout=300"]) except: - print_colored("Warning: pytest-timeout not installed, skipping timeout option", Colors.YELLOW) - + print_colored( + "Warning: pytest-timeout not installed, skipping timeout option", + Colors.YELLOW, + ) + print_colored("Running pytest with command:", Colors.YELLOW) print(f" {' '.join(cmd)}") print() - + # Run the tests result = subprocess.run(cmd, capture_output=False) - + # Generate the report regardless of test outcome if os.path.exists(junit_xml): print() print_colored("Generating test report...", Colors.BLUE) generate_markdown_report(junit_xml, report_path, tag, commit) - + # Print summary to console print() print_colored("Test Summary:", Colors.BOLD + Colors.PURPLE) - + # Parse XML for quick summary tree = ET.parse(junit_xml) root = tree.getroot() - - if root.tag == 'testsuites': - suites = root.findall('testsuite') + + if root.tag == "testsuites": + suites = root.findall("testsuite") else: suites = [root] - - total = sum(int(s.get('tests', 0)) for s in suites) - failures = sum(int(s.get('failures', 0)) for s in suites) - errors = sum(int(s.get('errors', 0)) for s in suites) - skipped = sum(int(s.get('skipped', 0)) for s in suites) + + total = sum(int(s.get("tests", 0)) for s in suites) + failures = sum(int(s.get("failures", 0)) for s in suites) + errors = sum(int(s.get("errors", 0)) for s in suites) + skipped = sum(int(s.get("skipped", 0)) for s in suites) passed = total - failures - errors - skipped - + print(f" Total: {total}") print_colored(f" Passed: {passed}", Colors.GREEN) if failures > 0: @@ -381,59 +457,75 @@ def run_tests(test_path: str = "tests/llm_translation/", print_colored(f" Errors: {errors}", Colors.RED) if skipped > 0: print_colored(f" Skipped: {skipped}", Colors.YELLOW) - + if total > 0: pass_rate = (passed / total) * 100 - color = Colors.GREEN if pass_rate >= 80 else Colors.YELLOW if pass_rate >= 60 else Colors.RED + color = ( + Colors.GREEN + if pass_rate >= 80 + else Colors.YELLOW if pass_rate >= 60 else Colors.RED + ) print_colored(f" Pass Rate: {pass_rate:.1f}%", color) else: print_colored("No test results found!", Colors.RED) - + print() print_colored("Test run complete!", Colors.BOLD + Colors.GREEN) - + return result.returncode + if __name__ == "__main__": import argparse - + parser = argparse.ArgumentParser(description="Run LLM Translation Tests") - parser.add_argument("--test-path", default="tests/llm_translation/", - help="Path to test directory") - parser.add_argument("--junit-xml", default="test-results/junit.xml", - help="Path for JUnit XML output") - parser.add_argument("--report", default="test-results/llm_translation_report.md", - help="Path for markdown report") + parser.add_argument( + "--test-path", default="tests/llm_translation/", help="Path to test directory" + ) + parser.add_argument( + "--junit-xml", + default="test-results/junit.xml", + help="Path for JUnit XML output", + ) + parser.add_argument( + "--report", + default="test-results/llm_translation_report.md", + help="Path for markdown report", + ) parser.add_argument("--tag", help="Git tag or version") parser.add_argument("--commit", help="Git commit SHA") - + args = parser.parse_args() - + # Get git info if not provided if not args.commit: try: - result = subprocess.run(["git", "rev-parse", "HEAD"], - capture_output=True, text=True) + result = subprocess.run( + ["git", "rev-parse", "HEAD"], capture_output=True, text=True + ) if result.returncode == 0: args.commit = result.stdout.strip() except: pass - + if not args.tag: try: - result = subprocess.run(["git", "describe", "--tags", "--abbrev=0"], - capture_output=True, text=True) + result = subprocess.run( + ["git", "describe", "--tags", "--abbrev=0"], + capture_output=True, + text=True, + ) if result.returncode == 0: args.tag = result.stdout.strip() except: pass - + exit_code = run_tests( test_path=args.test_path, junit_xml=args.junit_xml, report_path=args.report, tag=args.tag, - commit=args.commit + commit=args.commit, ) - + sys.exit(exit_code) diff --git a/Dockerfile b/Dockerfile index 915daff999c..846351839af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -85,10 +85,12 @@ ENV PATH="/app/.venv/bin:${PATH}" COPY --from=builder /app /app # Prisma binaries live in $HOME/.cache (default prisma-python location), -# which is /root/.cache here. Copy them from the builder so they survive -# deployments that volume-mount /app/.cache (e.g. readOnlyRootFilesystem -# + emptyDir) — otherwise the mount would shadow the baked-in query engine. -COPY --from=builder /root/.cache /root/.cache +# which is /root/.cache here. Copy only the Prisma subdirs — copying the +# whole /root/.cache drags in the uv build cache (~660 MB, includes a +# setuptools wheel that surfaces as a CVE finding even though it's not +# on the runtime sys.path). +COPY --from=builder /root/.cache/prisma /root/.cache/prisma +COPY --from=builder /root/.cache/prisma-python /root/.cache/prisma-python RUN find /app/.venv -type f -path "*/tornado/test/*" -delete && \ find /app/.venv -type d -path "*/tornado/test" -delete diff --git a/ci_cd/run_migration.py b/ci_cd/run_migration.py index feec4046ee1..760aee66da8 100644 --- a/ci_cd/run_migration.py +++ b/ci_cd/run_migration.py @@ -9,7 +9,6 @@ from pathlib import Path import testing.postgresql - DESTRUCTIVE_PATTERN = re.compile(r"\bDROP\s+(COLUMN|TABLE|INDEX)\b", re.IGNORECASE) DEFAULT_BASE_BRANCH = "litellm_internal_staging" diff --git a/cookbook/benchmark/benchmark.py b/cookbook/benchmark/benchmark.py index b38d185a166..2b3ddacad80 100644 --- a/cookbook/benchmark/benchmark.py +++ b/cookbook/benchmark/benchmark.py @@ -6,7 +6,6 @@ from tabulate import tabulate from termcolor import colored import os - # Define the list of models to benchmark # select any LLM listed here: https://docs.litellm.ai/docs/providers models = ["gpt-3.5-turbo", "claude-2"] diff --git a/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py b/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py index 6379314c5b6..a7049c68ced 100644 --- a/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py +++ b/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py @@ -22,7 +22,6 @@ from fastapi import FastAPI, HTTPException, Header, Query from fastapi.responses import JSONResponse import uvicorn - app = FastAPI( title="Braintrust Prompt Wrapper", description="Wrapper server for Braintrust prompts to work with LiteLLM", diff --git a/cookbook/livekit_agent_sdk/main.py b/cookbook/livekit_agent_sdk/main.py index c68e5534ea8..26a42b40d07 100644 --- a/cookbook/livekit_agent_sdk/main.py +++ b/cookbook/livekit_agent_sdk/main.py @@ -2,7 +2,7 @@ Simple xAI Voice Agent using LiveKit SDK with LiteLLM Gateway This example shows how to use LiveKit's xAI realtime plugin through LiteLLM proxy. -LiteLLM acts as a unified interface, allowing you to switch between xAI, OpenAI, +LiteLLM acts as a unified interface, allowing you to switch between xAI, OpenAI, and Azure realtime APIs without changing your agent code. """ diff --git a/cookbook/misc/migrate_proxy_config.py b/cookbook/misc/migrate_proxy_config.py index 31c3f32c08a..dc039070a5d 100644 --- a/cookbook/misc/migrate_proxy_config.py +++ b/cookbook/misc/migrate_proxy_config.py @@ -1,14 +1,14 @@ """ LiteLLM Migration Script! -Takes a config.yaml and calls /model/new +Takes a config.yaml and calls /model/new Inputs: - File path to config.yaml - Proxy base url to your hosted proxy Step 1: Reads your config.yaml -Step 2: reads `model_list` and loops through all models +Step 2: reads `model_list` and loops through all models Step 3: calls `/model/new` for each model """ diff --git a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py index 7bf9cc32484..9f70bd20c2b 100644 --- a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py +++ b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py @@ -518,8 +518,7 @@ if __name__ == "__main__": print(f"Endpoint: POST /guardrail/{{id}}/version/{{version}}/apply") print("=" * 80) print("\nExample curl command:") - print( - f""" + print(f""" curl -X POST "http://{host}:{port}/guardrail/test-guardrail/version/1/apply" \\ -H "Authorization: Bearer {bearer_token}" \\ -H "Content-Type: application/json" \\ @@ -533,8 +532,7 @@ curl -X POST "http://{host}:{port}/guardrail/test-guardrail/version/1/apply" \\ }} ] }}' - """ - ) + """) print("=" * 80) uvicorn.run(app, host=host, port=port) diff --git a/db_scripts/create_views.py b/db_scripts/create_views.py index 3027b38958d..82f5b451db9 100644 --- a/db_scripts/create_views.py +++ b/db_scripts/create_views.py @@ -34,8 +34,7 @@ async def check_view_exists(): # noqa: PLR0915 print("LiteLLM_VerificationTokenView Exists!") # noqa except Exception: # If an error occurs, the view does not exist, so create it - await db.execute_raw( - """ + await db.execute_raw(""" CREATE VIEW "LiteLLM_VerificationTokenView" AS SELECT v.*, @@ -45,8 +44,7 @@ async def check_view_exists(): # noqa: PLR0915 t.rpm_limit AS team_rpm_limit FROM "LiteLLM_VerificationToken" v LEFT JOIN "LiteLLM_TeamTable" t ON v.team_id = t.team_id; - """ - ) + """) print("LiteLLM_VerificationTokenView Created!") # noqa diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py b/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py index 8824f4c02de..7353b995d2a 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py @@ -14,53 +14,74 @@ from litellm.types.utils import StandardCallbackDynamicParams class EnterpriseCallbackControls: @staticmethod def is_callback_disabled_dynamically( - callback: litellm.CALLBACK_TYPES, - litellm_params: dict, - standard_callback_dynamic_params: StandardCallbackDynamicParams - ) -> bool: - """ - Check if a callback is disabled via the x-litellm-disable-callbacks header or via `litellm_disabled_callbacks` in standard_callback_dynamic_params. - - Args: - callback: The callback to check (can be string, CustomLogger instance, or callable) - litellm_params: Parameters containing proxy server request info - - Returns: - bool: True if the callback should be disabled, False otherwise - """ - from litellm.litellm_core_utils.custom_logger_registry import ( - CustomLoggerRegistry, - ) + callback: litellm.CALLBACK_TYPES, + litellm_params: dict, + standard_callback_dynamic_params: StandardCallbackDynamicParams, + ) -> bool: + """ + Check if a callback is disabled via the x-litellm-disable-callbacks header or via `litellm_disabled_callbacks` in standard_callback_dynamic_params. + + Args: + callback: The callback to check (can be string, CustomLogger instance, or callable) + litellm_params: Parameters containing proxy server request info + + Returns: + bool: True if the callback should be disabled, False otherwise + """ + from litellm.litellm_core_utils.custom_logger_registry import ( + CustomLoggerRegistry, + ) + + try: + disabled_callbacks = EnterpriseCallbackControls.get_disabled_callbacks( + litellm_params, standard_callback_dynamic_params + ) + verbose_logger.debug( + f"Dynamically disabled callbacks from {X_LITELLM_DISABLE_CALLBACKS}: {disabled_callbacks}" + ) + verbose_logger.debug( + f"Checking if {callback} is disabled via headers. Disable callbacks from headers: {disabled_callbacks}" + ) + if disabled_callbacks is not None: + ######################################################### + # premium user check + ######################################################### + if ( + not EnterpriseCallbackControls._should_allow_dynamic_callback_disabling() + ): + return False + ######################################################### + if isinstance(callback, str): + if callback.lower() in disabled_callbacks: + verbose_logger.debug( + f"Not logging to {callback} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}" + ) + return True + elif isinstance(callback, CustomLogger): + # get the string name of the callback + callback_str = ( + CustomLoggerRegistry.get_callback_str_from_class_type( + callback.__class__ + ) + ) + if ( + callback_str is not None + and callback_str.lower() in disabled_callbacks + ): + verbose_logger.debug( + f"Not logging to {callback_str} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}" + ) + return True + return False + except Exception as e: + verbose_logger.debug(f"Error checking disabled callbacks header: {str(e)}") + return False - try: - disabled_callbacks = EnterpriseCallbackControls.get_disabled_callbacks(litellm_params, standard_callback_dynamic_params) - verbose_logger.debug(f"Dynamically disabled callbacks from {X_LITELLM_DISABLE_CALLBACKS}: {disabled_callbacks}") - verbose_logger.debug(f"Checking if {callback} is disabled via headers. Disable callbacks from headers: {disabled_callbacks}") - if disabled_callbacks is not None: - ######################################################### - # premium user check - ######################################################### - if not EnterpriseCallbackControls._should_allow_dynamic_callback_disabling(): - return False - ######################################################### - if isinstance(callback, str): - if callback.lower() in disabled_callbacks: - verbose_logger.debug(f"Not logging to {callback} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}") - return True - elif isinstance(callback, CustomLogger): - # get the string name of the callback - callback_str = CustomLoggerRegistry.get_callback_str_from_class_type(callback.__class__) - if callback_str is not None and callback_str.lower() in disabled_callbacks: - verbose_logger.debug(f"Not logging to {callback_str} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}") - return True - return False - except Exception as e: - verbose_logger.debug( - f"Error checking disabled callbacks header: {str(e)}" - ) - return False @staticmethod - def get_disabled_callbacks(litellm_params: dict, standard_callback_dynamic_params: StandardCallbackDynamicParams) -> Optional[List[str]]: + def get_disabled_callbacks( + litellm_params: dict, + standard_callback_dynamic_params: StandardCallbackDynamicParams, + ) -> Optional[List[str]]: """ Get the disabled callbacks from the standard callback dynamic params. """ @@ -71,18 +92,24 @@ class EnterpriseCallbackControls: request_headers = get_proxy_server_request_headers(litellm_params) disabled_callbacks = request_headers.get(X_LITELLM_DISABLE_CALLBACKS, None) if disabled_callbacks is not None: - disabled_callbacks = set([cb.strip().lower() for cb in disabled_callbacks.split(",")]) + disabled_callbacks = set( + [cb.strip().lower() for cb in disabled_callbacks.split(",")] + ) return list(disabled_callbacks) - ######################################################### # check if disabled via request body ######################################################### - if standard_callback_dynamic_params.get("litellm_disabled_callbacks", None) is not None: - return standard_callback_dynamic_params.get("litellm_disabled_callbacks", None) - + if ( + standard_callback_dynamic_params.get("litellm_disabled_callbacks", None) + is not None + ): + return standard_callback_dynamic_params.get( + "litellm_disabled_callbacks", None + ) + return None - + @staticmethod def _should_allow_dynamic_callback_disabling(): import litellm @@ -90,10 +117,14 @@ class EnterpriseCallbackControls: # Check if admin has disabled this feature if litellm.allow_dynamic_callback_disabling is not True: - verbose_logger.debug("Dynamic callback disabling is disabled by admin via litellm.allow_dynamic_callback_disabling") + verbose_logger.debug( + "Dynamic callback disabling is disabled by admin via litellm.allow_dynamic_callback_disabling" + ) return False - + if premium_user: return True - verbose_logger.warning(f"Disabling callbacks using request headers is an enterprise feature. {CommonProxyErrors.not_premium_user.value}") - return False \ No newline at end of file + verbose_logger.warning( + f"Disabling callbacks using request headers is an enterprise feature. {CommonProxyErrors.not_premium_user.value}" + ) + return False diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index 89c3b854686..4fb6679a6eb 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -349,8 +349,10 @@ class BaseEmailLogger(CustomLogger): ) # Calculate percentage and alert threshold - percentage = threshold_pct if threshold_pct is not None else int( - EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE * 100 + percentage = ( + threshold_pct + if threshold_pct is not None + else int(EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE * 100) ) threshold_fraction = percentage / 100.0 alert_threshold_str = ( @@ -609,9 +611,7 @@ class BaseEmailLogger(CustomLogger): continue _id = user_info.token or user_info.user_id or "default_id" - _cache_key = ( - f"email_budget_alerts:max_budget_alert:{threshold_pct}:{_id}" - ) + _cache_key = f"email_budget_alerts:max_budget_alert:{threshold_pct}:{_id}" result = await _cache.async_get_cache(key=_cache_key) if result is not None: @@ -630,7 +630,9 @@ class BaseEmailLogger(CustomLogger): continue recipient_emails = list(set(emails)) - event_message = f"Max Budget Alert - {threshold_pct}% of Maximum Budget Reached" + event_message = ( + f"Max Budget Alert - {threshold_pct}% of Maximum Budget Reached" + ) webhook_event = WebhookEvent( event="max_budget_alert", event_message=event_message, diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py index 8fc2d66d531..a1e8def2bb2 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py @@ -15,7 +15,6 @@ from litellm.llms.custom_httpx.http_handler import ( from .base_email import BaseEmailLogger - SENDGRID_API_ENDPOINT = "https://api.sendgrid.com/v3/mail/send" @@ -79,4 +78,4 @@ class SendGridEmailLogger(BaseEmailLogger): verbose_logger.debug( f"SendGrid response status={response.status_code}, body={response.text}" ) - return \ No newline at end of file + return diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py index 8efdaf231b7..8e4dbde437b 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py @@ -1,6 +1,7 @@ """ This is the litellm SMTP email integration """ + import asyncio from typing import List diff --git a/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py b/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py index 44ba0063ffe..24941e90ab8 100644 --- a/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py +++ b/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py @@ -1,6 +1,7 @@ """ Enterprise specific logging utils """ + from litellm.litellm_core_utils.litellm_logging import StandardLoggingMetadata diff --git a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py index 18ac29b9781..4f2eaa3c468 100644 --- a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py @@ -153,11 +153,11 @@ async def get_audit_logs( # Return paginated response return PaginatedAuditLogResponse( - audit_logs=[ - AuditLogResponse(**audit_log.model_dump()) for audit_log in audit_logs - ] - if audit_logs - else [], + audit_logs=( + [AuditLogResponse(**audit_log.model_dump()) for audit_log in audit_logs] + if audit_logs + else [] + ), total=total_count, page=page, page_size=page_size, diff --git a/enterprise/litellm_enterprise/proxy/auth/__init__.py b/enterprise/litellm_enterprise/proxy/auth/__init__.py index f67826ca7fa..dc70b57ab55 100644 --- a/enterprise/litellm_enterprise/proxy/auth/__init__.py +++ b/enterprise/litellm_enterprise/proxy/auth/__init__.py @@ -7,4 +7,4 @@ including custom SSO handlers and advanced authentication features. from .custom_sso_handler import EnterpriseCustomSSOHandler -__all__ = ["EnterpriseCustomSSOHandler"] \ No newline at end of file +__all__ = ["EnterpriseCustomSSOHandler"] diff --git a/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py b/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py index 356f6ecd4b5..3aef2bd77cb 100644 --- a/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py +++ b/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py @@ -53,7 +53,9 @@ class CheckBatchCost: "user_api_key_alias": getattr(user_row, "user_alias", None), } except Exception as e: - verbose_proxy_logger.error(f"CheckBatchCost: could not look up user {user_id} for batch {batch_id}: {e}") + verbose_proxy_logger.error( + f"CheckBatchCost: could not look up user {user_id} for batch {batch_id}: {e}" + ) return {} async def _cleanup_stale_managed_objects(self) -> None: @@ -62,11 +64,22 @@ class CheckBatchCost: in non-terminal states as 'stale_expired'. These will never complete and should not be polled. """ - cutoff = datetime.now(timezone.utc) - timedelta(days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS) + cutoff = datetime.now(timezone.utc) - timedelta( + days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS + ) result = await self.prisma_client.db.litellm_managedobjecttable.update_many( where={ "file_purpose": "batch", - "status": {"not_in": ["completed", "complete", "failed", "expired", "cancelled", "stale_expired"]}, + "status": { + "not_in": [ + "completed", + "complete", + "failed", + "expired", + "cancelled", + "stale_expired", + ] + }, "created_at": {"lt": cutoff}, }, data={"status": "stale_expired"}, @@ -120,9 +133,12 @@ class CheckBatchCost: try: from litellm.integrations.prometheus import PrometheusLogger + prom_logger = PrometheusLogger.get_instance() except Exception as e: - verbose_proxy_logger.error(f"CheckBatchCost: could not get Prometheus logger: {e}") + verbose_proxy_logger.error( + f"CheckBatchCost: could not get Prometheus logger: {e}" + ) prom_logger = None processed_models: List[Tuple[Optional[str], Optional[str]]] = [] @@ -161,7 +177,11 @@ class CheckBatchCost: order={"created_at": "asc"}, ) except Exception as query_err: - if "batch_processed" not in str(query_err).lower() and "unknown column" not in str(query_err).lower() and "does not exist" not in str(query_err).lower(): + if ( + "batch_processed" not in str(query_err).lower() + and "unknown column" not in str(query_err).lower() + and "does not exist" not in str(query_err).lower() + ): raise # Permanent schema gap — cache the result so future cycles skip straight to fallback self._has_batch_processed_column = False @@ -216,14 +236,13 @@ class CheckBatchCost: f"Skipping job {unified_object_id} because of error querying model ID: {model_id} for cost and usage of batch ID: {batch_id}: {e}" ) if prom_logger: - prom_logger.record_check_batch_cost_error("provider_retrieval_error") + prom_logger.record_check_batch_cost_error( + "provider_retrieval_error" + ) continue ## RETRIEVE THE BATCH JOB OUTPUT FILE - if ( - response.status == "completed" - and response.output_file_id is not None - ): + if response.status == "completed" and response.output_file_id is not None: verbose_proxy_logger.info( f"Batch ID: {batch_id} is complete, tracking cost and usage" ) @@ -250,20 +269,25 @@ class CheckBatchCost: decoded = _is_base64_encoded_unified_file_id(raw_output_file_id) if decoded: try: - raw_output_file_id = decoded.split("llm_output_file_id,")[1].split(";")[0] + raw_output_file_id = decoded.split("llm_output_file_id,")[ + 1 + ].split(";")[0] except (IndexError, AttributeError): pass - credentials = self.llm_router.get_deployment_credentials_with_provider(model_id) or {} + credentials = ( + self.llm_router.get_deployment_credentials_with_provider(model_id) + or {} + ) _file_content = await afile_content( file_id=raw_output_file_id, **credentials, ) # Access content - handle both direct attribute and method call - if hasattr(_file_content, 'content'): + if hasattr(_file_content, "content"): content_bytes = _file_content.content # type: ignore[union-attr] - elif hasattr(_file_content, 'read'): + elif hasattr(_file_content, "read"): content_bytes = await _file_content.read() # type: ignore[misc] else: content_bytes = _file_content # type: ignore[assignment] @@ -290,7 +314,9 @@ class CheckBatchCost: f"Skipping job {unified_object_id} because it is not a valid deployment info" ) if prom_logger: - prom_logger.record_check_batch_cost_error("deployment_not_found") + prom_logger.record_check_batch_cost_error( + "deployment_not_found" + ) continue custom_llm_provider = deployment_info.litellm_params.custom_llm_provider litellm_model_name = deployment_info.litellm_params.model @@ -302,7 +328,11 @@ class CheckBatchCost: # Pass deployment model_info so custom batch pricing # (input_cost_per_token_batches etc.) is used for cost calc - deployment_model_info = deployment_info.model_info.model_dump() if deployment_info.model_info else {} + deployment_model_info = ( + deployment_info.model_info.model_dump() + if deployment_info.model_info + else {} + ) batch_cost, batch_usage, batch_models = ( await calculate_batch_cost_and_usage( file_content_dictionary=file_content_as_dict, @@ -349,7 +379,9 @@ class CheckBatchCost: # Record batch duration (completed_at - created_at) if prom_logger and response.completed_at and response.created_at: - duration_seconds = float(response.completed_at - response.created_at) + duration_seconds = float( + response.completed_at - response.created_at + ) if duration_seconds >= 0: prom_logger.record_managed_batch_duration( duration_seconds=duration_seconds, @@ -358,7 +390,9 @@ class CheckBatchCost: ) # Track this job for the final metrics summary - processed_models.append((model_name, str(llm_provider) if llm_provider else None)) + processed_models.append( + (model_name, str(llm_provider) if llm_provider else None) + ) # mark the job as complete try: diff --git a/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py b/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py index dc0168683c8..8b2d15c1578 100644 --- a/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py +++ b/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py @@ -33,9 +33,7 @@ class CheckResponsesCost: self.prisma_client: PrismaClient = prisma_client self.llm_router: Router = llm_router - async def _expire_stale_rows( - self, cutoff: datetime, batch_size: int - ) -> int: + async def _expire_stale_rows(self, cutoff: datetime, batch_size: int) -> int: """Execute the bounded UPDATE that marks stale rows as 'stale_expired'. Isolated so it can be swapped / mocked in tests without touching the @@ -74,7 +72,9 @@ class CheckResponsesCost: rows per invocation to avoid overwhelming the DB when there is a large backlog. """ - cutoff = datetime.now(timezone.utc) - timedelta(days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS) + cutoff = datetime.now(timezone.utc) - timedelta( + days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS + ) result = await self._expire_stale_rows(cutoff, STALE_OBJECT_CLEANUP_BATCH_SIZE) if result > 0: verbose_proxy_logger.warning( @@ -105,7 +105,7 @@ class CheckResponsesCost: take=MAX_OBJECTS_PER_POLL_CYCLE, order={"created_at": "asc"}, ) - + verbose_proxy_logger.debug(f"Found {len(jobs)} response jobs to check") completed_jobs = [] @@ -120,29 +120,33 @@ class CheckResponsesCost: # Get the stored response object to extract model information stored_response = job.file_object model_name = stored_response.get("model", None) - + # Decrypt the response ID - responses_id_security, _, _ = ResponsesIDSecurity()._decrypt_response_id(unified_object_id) - + responses_id_security, _, _ = ( + ResponsesIDSecurity()._decrypt_response_id(unified_object_id) + ) + # Prepare metadata with model information for cost tracking litellm_metadata = { "user_api_key_user_id": job.created_by or "default-user-id", } - + # Add model information if available if model_name: litellm_metadata["model"] = model_name - litellm_metadata["model_group"] = model_name # Use same value for model_group - + litellm_metadata["model_group"] = ( + model_name # Use same value for model_group + ) + response = await litellm.aget_responses( response_id=responses_id_security, litellm_metadata=litellm_metadata, ) - + verbose_proxy_logger.debug( f"Response {unified_object_id} status: {response.status}, model: {model_name}" ) - + except Exception as e: verbose_proxy_logger.info( f"Skipping job {unified_object_id} due to error: {e}" @@ -155,7 +159,7 @@ class CheckResponsesCost: f"Response {unified_object_id} is complete. Cost automatically tracked by aget_responses." ) completed_jobs.append(job) - + elif response.status in ["failed", "cancelled"]: verbose_proxy_logger.info( f"Response {unified_object_id} has status {response.status}, marking as complete" @@ -171,4 +175,3 @@ class CheckResponsesCost: verbose_proxy_logger.info( f"Marked {len(completed_jobs)} response jobs as completed" ) - diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py b/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py index 254d816039c..70634537c55 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py @@ -41,7 +41,7 @@ class _PROXY_LiteLLMManagedVectorStores( ): """ Managed vector stores with target_model_names support. - + This class provides functionality to: - Create vector stores across multiple models - Retrieve vector stores by unified ID @@ -77,14 +77,14 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> str: """ Generate the format string for the unified vector store ID. - + Format: litellm_proxy:vector_store;unified_id,;target_model_names,;resource_id,;model_id, """ # VectorStoreCreateResponse is a TypedDict, so resource_object is a dictionary # Extract provider resource ID from the response provider_resource_id = resource_object.get("id", "") - + # Model ID is stored in hidden params if the response object supports it # For TypedDict responses, we need to check if _hidden_params was added hidden_params: Dict[str, Any] = {} @@ -109,20 +109,18 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> VectorStoreCreateResponse: """ Create a vector store for a specific model. - + Args: llm_router: LiteLLM router instance model: Model name to create vector store for request_data: Request data for vector store creation litellm_parent_otel_span: OpenTelemetry span for tracing - + Returns: VectorStoreCreateResponse from the provider """ # Use the router to create the vector store - response = await llm_router.avector_store_create( - model=model, **request_data - ) + response = await llm_router.avector_store_create(model=model, **request_data) return response # ============================================================================ @@ -139,14 +137,14 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> VectorStoreCreateResponse: """ Create a vector store across multiple models. - + Args: create_request: Vector store creation request parameters llm_router: LiteLLM router instance target_model_names_list: List of target model names litellm_parent_otel_span: OpenTelemetry span for tracing user_api_key_dict: User API key authentication details - + Returns: VectorStoreCreateResponse with unified ID """ @@ -196,7 +194,7 @@ class _PROXY_LiteLLMManagedVectorStores( # VectorStoreCreateResponse is a TypedDict, so we need to create a new dict with the unified ID response = responses[0].copy() response["id"] = unified_id - + verbose_logger.info( f"Successfully created managed vector store with unified ID: {unified_id}" ) @@ -212,13 +210,13 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> Dict[str, Any]: """ List vector stores created by a user. - + Args: user_api_key_dict: User API key authentication details limit: Maximum number of vector stores to return after: Cursor for pagination order: Sort order ('asc' or 'desc') - + Returns: Dictionary with list of vector stores and pagination info """ @@ -238,23 +236,23 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> bool: """ Check if user has access to a vector store. - + Args: vector_store_id: The unified vector store ID user_api_key_dict: User API key authentication details - + Returns: True if user has access, False otherwise """ is_unified_id = is_base64_encoded_unified_id(vector_store_id) - + if is_unified_id: # Check access for managed vector store return await self.can_user_access_unified_resource_id( vector_store_id, user_api_key_dict, ) - + # Not a managed vector store, allow access return True @@ -263,24 +261,22 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> bool: """ Check if user has access to a managed vector store in request data. - + Args: data: Request data containing vector_store_id user_api_key_dict: User API key authentication details - + Returns: True if this is a managed vector store and user has access - + Raises: HTTPException: If user doesn't have access """ vector_store_id = cast(Optional[str], data.get("vector_store_id")) is_unified_id = ( - is_base64_encoded_unified_id(vector_store_id) - if vector_store_id - else False + is_base64_encoded_unified_id(vector_store_id) if vector_store_id else False ) - + if is_unified_id and vector_store_id: if await self.can_user_access_unified_resource_id( vector_store_id, user_api_key_dict @@ -291,7 +287,7 @@ class _PROXY_LiteLLMManagedVectorStores( status_code=403, detail=f"User {user_api_key_dict.user_id} does not have access to vector store {vector_store_id}", ) - + return False # ============================================================================ @@ -307,18 +303,18 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> Union[Exception, str, Dict, None]: """ Pre-call hook to handle vector store operations. - + This hook intercepts vector store requests and: - Validates access for managed vector stores - Transforms unified IDs to provider-specific IDs - Adds model routing information - + Args: user_api_key_dict: User API key authentication details cache: Cache instance data: Request data call_type: Type of call being made - + Returns: Modified request data or None """ @@ -330,40 +326,40 @@ class _PROXY_LiteLLMManagedVectorStores( # Handle vector store search operations if call_type == "avector_store_search": vector_store_id = data.get("vector_store_id") - + if vector_store_id: # Check if it's a managed vector store ID decoded_id = is_base64_encoded_unified_id(vector_store_id) - + if decoded_id: verbose_logger.debug( f"Processing managed vector store search: {vector_store_id}" ) - + # Check access has_access = await self.can_user_access_unified_resource_id( vector_store_id, user_api_key_dict ) - + if not has_access: raise HTTPException( status_code=403, detail=f"User {user_api_key_dict.user_id} does not have access to vector store {vector_store_id}", ) - + # Parse the unified ID to extract components parsed_id = parse_unified_id(vector_store_id) - + if parsed_id: # Extract the model ID and provider resource ID model_id = parsed_id.get("model_id") provider_resource_id = parsed_id.get("provider_resource_id") target_model_names = parsed_id.get("target_model_names", []) - + verbose_logger.debug( f"Decoded vector store - model_id: {model_id}, provider_resource_id: {provider_resource_id}, target_model_names: {target_model_names}" ) - + # Determine which model to use for routing # Priority: model_id (deployment ID) > first target_model_name routing_model = None @@ -371,28 +367,28 @@ class _PROXY_LiteLLMManagedVectorStores( routing_model = model_id elif target_model_names and len(target_model_names) > 0: routing_model = target_model_names[0] - + # Set the model for routing if routing_model: data["model"] = routing_model verbose_logger.info( f"Routing vector store search to model: {routing_model}" ) - + # Replace the unified ID with the provider-specific ID if provider_resource_id: data["vector_store_id"] = provider_resource_id verbose_logger.debug( f"Replaced unified ID with provider resource ID: {provider_resource_id}" ) - + # Handle vector store retrieve/delete operations elif call_type in ("avector_store_retrieve", "avector_store_delete"): await self.check_managed_vector_store_access(data, user_api_key_dict) - + # If it's a managed vector store, we'll handle it in the endpoint # No need to transform here as the endpoint will route to the hook - + return data # ============================================================================ @@ -407,15 +403,15 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> Any: """ Post-call hook to transform responses. - + This hook can be used to transform responses if needed. For now, it just passes through the response. - + Args: data: Request data user_api_key_dict: User API key authentication details response: Response from the provider - + Returns: Potentially modified response """ @@ -436,21 +432,21 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> List[Dict]: """ Filter deployments based on vector store availability. - + This is used by the router to select only deployments that have the vector store available. - + Note: This method signature is a compromise between CustomLogger and BaseManagedResource parent classes which have incompatible signatures. The type: ignore[override] is necessary due to this multiple inheritance conflict. - + Args: model: Model name healthy_deployments: List of healthy deployments messages: Messages (unused for vector stores, required by CustomLogger interface) request_kwargs: Request kwargs containing vector_store_id and mappings parent_otel_span: OpenTelemetry span for tracing - + Returns: Filtered list of deployments """ diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py index 2f53f9e9281..48b6dd76348 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py @@ -2,7 +2,6 @@ Enterprise internal user management endpoints """ - from fastapi import APIRouter, Depends, HTTPException from litellm.proxy._types import UserAPIKeyAuth diff --git a/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py b/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py index 5e799599862..cf8c38719d7 100644 --- a/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py +++ b/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py @@ -147,12 +147,12 @@ async def list_vector_stores( vector_stores_from_db = await VectorStoreRegistry._get_vector_stores_from_db( prisma_client=prisma_client ) - + # Also clean up in-memory registry to remove any deleted vector stores if litellm.vector_store_registry is not None: db_vector_store_ids = { - vs.get("vector_store_id") - for vs in vector_stores_from_db + vs.get("vector_store_id") + for vs in vector_stores_from_db if vs.get("vector_store_id") } # Remove any in-memory vector stores that no longer exist in database diff --git a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py index 380b0a6facb..d9d5a989abb 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -39,15 +39,23 @@ class EmailEvent(str, enum.Enum): soft_budget_crossed = "Soft Budget Crossed" max_budget_alert = "Max Budget Alert" + class EmailEventSettings(BaseModel): event: EmailEvent enabled: bool + + class EmailEventSettingsUpdateRequest(BaseModel): settings: List[EmailEventSettings] + + class EmailEventSettingsResponse(BaseModel): settings: List[EmailEventSettings] + + class DefaultEmailSettings(BaseModel): """Default settings for email events""" + settings: Dict[EmailEvent, bool] = Field( default_factory=lambda: { EmailEvent.virtual_key_created: True, # On by default @@ -57,10 +65,12 @@ class DefaultEmailSettings(BaseModel): EmailEvent.max_budget_alert: True, # On by default } ) + def to_dict(self) -> Dict[str, bool]: """Convert to dictionary with string keys for storage""" return {event.value: enabled for event, enabled in self.settings.items()} + @classmethod def get_defaults(cls) -> Dict[str, bool]: """Get the default settings as a dictionary with string keys""" - return cls().to_dict() \ No newline at end of file + return cls().to_dict() diff --git a/litellm-js/spend-logs/package-lock.json b/litellm-js/spend-logs/package-lock.json index e9fc4b00f6a..e33079766c9 100644 --- a/litellm-js/spend-logs/package-lock.json +++ b/litellm-js/spend-logs/package-lock.json @@ -535,9 +535,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/litellm/_uuid.py b/litellm/_uuid.py index 52acf647dd8..2b7c3b82d35 100644 --- a/litellm/_uuid.py +++ b/litellm/_uuid.py @@ -6,7 +6,6 @@ Always uses fastuuid for performance. import fastuuid as _uuid # type: ignore - # Expose a module-like alias so callers can use: uuid.uuid4() uuid = _uuid diff --git a/litellm/anthropic_interface/exceptions/exception_mapping_utils.py b/litellm/anthropic_interface/exceptions/exception_mapping_utils.py index 28020e763f4..4548185bbdc 100644 --- a/litellm/anthropic_interface/exceptions/exception_mapping_utils.py +++ b/litellm/anthropic_interface/exceptions/exception_mapping_utils.py @@ -9,7 +9,6 @@ from typing import Dict, Optional from .exceptions import AnthropicErrorResponse, AnthropicErrorType - # HTTP status code -> Anthropic error type # Source: https://docs.anthropic.com/en/api/errors ANTHROPIC_ERROR_TYPE_MAP: Dict[int, AnthropicErrorType] = { diff --git a/litellm/anthropic_interface/exceptions/exceptions.py b/litellm/anthropic_interface/exceptions/exceptions.py index 984390fa702..b289e493e6b 100644 --- a/litellm/anthropic_interface/exceptions/exceptions.py +++ b/litellm/anthropic_interface/exceptions/exceptions.py @@ -2,7 +2,6 @@ from typing_extensions import Literal, Required, TypedDict - # Known Anthropic error types # Source: https://docs.anthropic.com/en/api/errors AnthropicErrorType = Literal[ diff --git a/litellm/caching/disk_cache.py b/litellm/caching/disk_cache.py index e32c29b3bc6..a02327f7784 100644 --- a/litellm/caching/disk_cache.py +++ b/litellm/caching/disk_cache.py @@ -17,14 +17,14 @@ class DiskCache(BaseCache): import diskcache as dc except ModuleNotFoundError as e: raise ModuleNotFoundError( - "Please install litellm with `litellm[caching]` to use disk caching." + "DiskCache requires the `diskcache` package. Install it with " + "`pip install diskcache`." ) from e - # if users don't provider one, use the default litellm cache - if disk_cache_dir is None: - self.disk_cache = dc.Cache(".litellm_cache") - else: - self.disk_cache = dc.Cache(disk_cache_dir) + # JSONDisk avoids pickle deserialization (CVE-2025-69872). Values + # passed to set_cache must be JSON-serializable. + cache_dir = disk_cache_dir if disk_cache_dir is not None else ".litellm_cache" + self.disk_cache = dc.Cache(cache_dir, disk=dc.JSONDisk) def set_cache(self, key, value, **kwargs): if "ttl" in kwargs: @@ -43,14 +43,15 @@ class DiskCache(BaseCache): self.set_cache(key=cache_key, value=cache_value) def get_cache(self, key, **kwargs): - original_cached_response = self.disk_cache.get(key) - if original_cached_response: + cached = self.disk_cache.get(key) + if cached is None: + return None + if isinstance(cached, str): try: - cached_response = json.loads(original_cached_response) # type: ignore + return json.loads(cached) except Exception: - cached_response = original_cached_response - return cached_response - return None + return cached + return cached def batch_get_cache(self, keys: list, **kwargs): return_val = [] diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index da3b9184edb..25860e72f2f 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -97,7 +97,7 @@ def _build_reasoning_item( def _reasoning_item_to_response_input( - r_item: Union[ChatCompletionReasoningItem, Dict[str, Any]] + r_item: Union[ChatCompletionReasoningItem, Dict[str, Any]], ) -> Dict[str, Any]: """Convert a stored ChatCompletionReasoningItem back to a Responses API input item.""" r_input: Dict[str, Any] = { diff --git a/litellm/compression/content_detection.py b/litellm/compression/content_detection.py index 0655a42daf5..975117eb608 100644 --- a/litellm/compression/content_detection.py +++ b/litellm/compression/content_detection.py @@ -5,7 +5,6 @@ Auto-detect content type per message: code, JSON, or text. import json import re - _CODE_KEYWORDS = re.compile( r"\b(?:def |function |class |import |from |require\(|#include|fn |func |const |let |var |public |private |static )\b" ) diff --git a/litellm/files/types.py b/litellm/files/types.py index 688bc86f0cf..ba42a39f666 100644 --- a/litellm/files/types.py +++ b/litellm/files/types.py @@ -1,6 +1,5 @@ from typing import AsyncIterator, Dict, Iterator, Literal, NamedTuple, Union - FileContentProvider = Literal[ "openai", "azure", "vertex_ai", "bedrock", "hosted_vllm", "anthropic", "manus" ] diff --git a/litellm/google_genai/adapters/__init__.py b/litellm/google_genai/adapters/__init__.py index bfa9e712678..6fbe7d95a55 100644 --- a/litellm/google_genai/adapters/__init__.py +++ b/litellm/google_genai/adapters/__init__.py @@ -1,10 +1,10 @@ """ Google GenAI Adapters for LiteLLM -This module provides adapters for transforming Google GenAI generate_content requests +This module provides adapters for transforming Google GenAI generate_content requests to/from LiteLLM completion format with full support for: - Text content transformation -- Tool calling (function declarations, function calls, function responses) +- Tool calling (function declarations, function calls, function responses) - Streaming (both regular and tool calling) - Mixed content (text + tool calls) """ diff --git a/litellm/integrations/SlackAlerting/batching_handler.py b/litellm/integrations/SlackAlerting/batching_handler.py index fdce2e04793..828f3eb4175 100644 --- a/litellm/integrations/SlackAlerting/batching_handler.py +++ b/litellm/integrations/SlackAlerting/batching_handler.py @@ -1,9 +1,9 @@ """ -Handles Batching + sending Httpx Post requests to slack +Handles Batching + sending Httpx Post requests to slack -Slack alerts are sent every 10s or when events are greater than X events +Slack alerts are sent every 10s or when events are greater than X events -see custom_batch_logger.py for more details / defaults +see custom_batch_logger.py for more details / defaults """ from typing import TYPE_CHECKING, Any diff --git a/litellm/integrations/SlackAlerting/utils.py b/litellm/integrations/SlackAlerting/utils.py index e695266c88b..e2580768178 100644 --- a/litellm/integrations/SlackAlerting/utils.py +++ b/litellm/integrations/SlackAlerting/utils.py @@ -18,7 +18,7 @@ else: def process_slack_alerting_variables( - alert_to_webhook_url: Optional[Dict[AlertType, Union[List[str], str]]] + alert_to_webhook_url: Optional[Dict[AlertType, Union[List[str], str]]], ) -> Optional[Dict[AlertType, Union[List[str], str]]]: """ process alert_to_webhook_url diff --git a/litellm/integrations/additional_logging_utils.py b/litellm/integrations/additional_logging_utils.py index 795afd81d41..59319140a18 100644 --- a/litellm/integrations/additional_logging_utils.py +++ b/litellm/integrations/additional_logging_utils.py @@ -1,5 +1,5 @@ """ -Base class for Additional Logging Utils for CustomLoggers +Base class for Additional Logging Utils for CustomLoggers - Health Check for the logging util - Get Request / Response Payload for the logging util diff --git a/litellm/integrations/custom_batch_logger.py b/litellm/integrations/custom_batch_logger.py index f9d4496c21f..6da7aec5559 100644 --- a/litellm/integrations/custom_batch_logger.py +++ b/litellm/integrations/custom_batch_logger.py @@ -1,5 +1,5 @@ """ -Custom Logger that handles batching logic +Custom Logger that handles batching logic Use this if you want your logs to be stored in memory and flushed periodically. """ diff --git a/litellm/integrations/focus/transformer.py b/litellm/integrations/focus/transformer.py index b7d28e3dbb9..6f4433b4a05 100644 --- a/litellm/integrations/focus/transformer.py +++ b/litellm/integrations/focus/transformer.py @@ -9,7 +9,6 @@ import polars as pl from .schema import FOCUS_NORMALIZED_SCHEMA - _TAG_KEYS = ( "team_id", "team_alias", diff --git a/litellm/integrations/opik/utils.py b/litellm/integrations/opik/utils.py index b0ab5991c91..43577505c11 100644 --- a/litellm/integrations/opik/utils.py +++ b/litellm/integrations/opik/utils.py @@ -105,7 +105,7 @@ def _remove_nulls(x: Dict[str, Any]) -> Dict[str, Any]: def get_traces_and_spans_from_payload( - payload: List[Dict[str, Any]] + payload: List[Dict[str, Any]], ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: """ Separate traces and spans from payload. diff --git a/litellm/integrations/s3_v2.py b/litellm/integrations/s3_v2.py index 08ce7ed8947..08650d26a7a 100644 --- a/litellm/integrations/s3_v2.py +++ b/litellm/integrations/s3_v2.py @@ -1,8 +1,8 @@ """ s3 Bucket Logging Integration -async_log_success_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 -async_log_failure_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 +async_log_success_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 +async_log_failure_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 NOTE 1: S3 does not provide a BATCH PUT API endpoint, so we create tasks to upload each element individually """ diff --git a/litellm/interactions/__init__.py b/litellm/interactions/__init__.py index e1125b649a6..0077ac82076 100644 --- a/litellm/interactions/__init__.py +++ b/litellm/interactions/__init__.py @@ -5,28 +5,28 @@ This module provides SDK methods for Google's Interactions API. Usage: import litellm - + # Create an interaction with a model response = litellm.interactions.create( model="gemini-2.5-flash", input="Hello, how are you?" ) - + # Create an interaction with an agent response = litellm.interactions.create( agent="deep-research-pro-preview-12-2025", input="Research the current state of cancer research" ) - + # Async version response = await litellm.interactions.acreate(...) - + # Get an interaction response = litellm.interactions.get(interaction_id="...") - + # Delete an interaction result = litellm.interactions.delete(interaction_id="...") - + # Cancel an interaction result = litellm.interactions.cancel(interaction_id="...") diff --git a/litellm/interactions/main.py b/litellm/interactions/main.py index ab429ef6db5..be9f2b99e0e 100644 --- a/litellm/interactions/main.py +++ b/litellm/interactions/main.py @@ -8,25 +8,25 @@ Per OpenAPI spec (https://ai.google.dev/static/api/interactions.openapi.json): Usage: import litellm - + # Create an interaction with a model response = litellm.interactions.create( model="gemini-2.5-flash", input="Hello, how are you?" ) - + # Create an interaction with an agent response = litellm.interactions.create( agent="deep-research-pro-preview-12-2025", input="Research the current state of cancer research" ) - + # Async version response = await litellm.interactions.acreate(...) - + # Get an interaction response = litellm.interactions.get(interaction_id="...") - + # Delete an interaction result = litellm.interactions.delete(interaction_id="...") """ diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index a815442c2f9..cf20dbba70b 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -994,10 +994,8 @@ class Logging(LiteLLMLoggingBaseClass): try: # [Non-blocking Extra Debug Information in metadata] if turn_off_message_logging is True: - _metadata["raw_request"] = ( - "redacted by litellm. \ + _metadata["raw_request"] = "redacted by litellm. \ 'litellm.turn_off_message_logging=True'" - ) else: curl_command = self._get_request_curl_command( api_base=additional_args.get("api_base", ""), @@ -1031,12 +1029,8 @@ class Logging(LiteLLMLoggingBaseClass): error=str(e), ) ) - _metadata["raw_request"] = ( - "Unable to Log \ - raw request: {}".format( - str(e) - ) - ) + _metadata["raw_request"] = "Unable to Log \ + raw request: {}".format(str(e)) if getattr(self, "logger_fn", None) and callable(self.logger_fn): try: self.logger_fn( diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index ba840bc3d89..5773bbca8e6 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -5486,9 +5486,7 @@ def default_response_schema_prompt(response_schema: dict) -> str: prompt_str = """Use this JSON schema: ```json {} - ```""".format( - response_schema - ) + ```""".format(response_schema) return prompt_str diff --git a/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py b/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py index 13341f27a61..0a6a4e82c72 100644 --- a/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py +++ b/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py @@ -1,9 +1,9 @@ """ This is a cache for LangfuseLoggers. -Langfuse Python SDK initializes a thread for each client. +Langfuse Python SDK initializes a thread for each client. -This ensures we do +This ensures we do 1. Proper cleanup of Langfuse initialized clients. 2. Re-use created langfuse clients. """ diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py index d0780c82d06..d693d50b8e5 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py @@ -13,7 +13,6 @@ from typing import Any, AsyncIterator, Dict, List, Optional, cast from litellm._logging import verbose_logger - # --------------------------------------------------------------------------- # SSE parsing helpers (module-level to keep the class lean) # --------------------------------------------------------------------------- diff --git a/litellm/llms/azure/chat/o_series_transformation.py b/litellm/llms/azure/chat/o_series_transformation.py index cae7513245c..0a73597a4e4 100644 --- a/litellm/llms/azure/chat/o_series_transformation.py +++ b/litellm/llms/azure/chat/o_series_transformation.py @@ -4,10 +4,10 @@ Support for o1 and o3 model families https://platform.openai.com/docs/guides/reasoning Translations handled by LiteLLM: -- modalities: image => drop param (if user opts in to dropping param) -- role: system ==> translate to role 'user' -- streaming => faked by LiteLLM -- Tools, response_format => drop param (if user opts in to dropping param) +- modalities: image => drop param (if user opts in to dropping param) +- role: system ==> translate to role 'user' +- streaming => faked by LiteLLM +- Tools, response_format => drop param (if user opts in to dropping param) - Logprobs => drop param (if user opts in to dropping param) - Temperature => drop param (if user opts in to dropping param) """ diff --git a/litellm/llms/azure_ai/embed/cohere_transformation.py b/litellm/llms/azure_ai/embed/cohere_transformation.py index 64433c21b61..bbbfb60fbde 100644 --- a/litellm/llms/azure_ai/embed/cohere_transformation.py +++ b/litellm/llms/azure_ai/embed/cohere_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Azure AI Cohere's /v1/embed. +Transformation logic from OpenAI /v1/embeddings format to Azure AI Cohere's /v1/embed. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/azure_ai/rerank/transformation.py b/litellm/llms/azure_ai/rerank/transformation.py index b5993040ea0..f64133afa8b 100644 --- a/litellm/llms/azure_ai/rerank/transformation.py +++ b/litellm/llms/azure_ai/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Translate between Cohere's `/rerank` format and Azure AI's `/rerank` format. +Translate between Cohere's `/rerank` format and Azure AI's `/rerank` format. """ from typing import Optional diff --git a/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py b/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py index 2747551af81..64a79b73273 100644 --- a/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py +++ b/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Bedrock Amazon Titan G1 /invoke format. +Transformation logic from OpenAI /v1/embeddings format to Bedrock Amazon Titan G1 /invoke format. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/bedrock/embed/cohere_transformation.py b/litellm/llms/bedrock/embed/cohere_transformation.py index d00cb74aae0..885c91f9754 100644 --- a/litellm/llms/bedrock/embed/cohere_transformation.py +++ b/litellm/llms/bedrock/embed/cohere_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Bedrock Cohere /invoke format. +Transformation logic from OpenAI /v1/embeddings format to Bedrock Cohere /invoke format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index e413bb22b2d..81a56030a5c 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -16,7 +16,6 @@ from litellm.secret_managers.main import get_secret_str from ...openai_like.chat.transformation import OpenAILikeChatConfig - BEDROCK_MANTLE_DEFAULT_REGION = "us-east-1" diff --git a/litellm/llms/cohere/embed/handler.py b/litellm/llms/cohere/embed/handler.py index 3ab8baf7ba8..81b6a1c7aec 100644 --- a/litellm/llms/cohere/embed/handler.py +++ b/litellm/llms/cohere/embed/handler.py @@ -1,5 +1,5 @@ """ -Legacy /v1/embedding handler for Bedrock Cohere. +Legacy /v1/embedding handler for Bedrock Cohere. """ import json diff --git a/litellm/llms/custom_httpx/mock_transport.py b/litellm/llms/custom_httpx/mock_transport.py index c9844753e0e..ad93cc134ee 100644 --- a/litellm/llms/custom_httpx/mock_transport.py +++ b/litellm/llms/custom_httpx/mock_transport.py @@ -13,7 +13,6 @@ from typing import Tuple import httpx - # --------------------------------------------------------------------------- # Pre-built response templates # --------------------------------------------------------------------------- diff --git a/litellm/llms/dashscope/cost_calculator.py b/litellm/llms/dashscope/cost_calculator.py index 9b3e3851162..8bb7f605b82 100644 --- a/litellm/llms/dashscope/cost_calculator.py +++ b/litellm/llms/dashscope/cost_calculator.py @@ -1,5 +1,5 @@ """ -Cost calculator for Dashscope Chat models. +Cost calculator for Dashscope Chat models. Handles tiered pricing and prompt caching scenarios. """ diff --git a/litellm/llms/datarobot/chat/transformation.py b/litellm/llms/datarobot/chat/transformation.py index 23ce63c25b2..f81e2420930 100644 --- a/litellm/llms/datarobot/chat/transformation.py +++ b/litellm/llms/datarobot/chat/transformation.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/chat/completions` endpoint. +Support for OpenAI's `/v1/chat/completions` endpoint. Calls done in OpenAI/openai.py as DataRobot is openai-compatible. """ diff --git a/litellm/llms/deepinfra/rerank/transformation.py b/litellm/llms/deepinfra/rerank/transformation.py index 276735f4758..e4bfbcb2513 100644 --- a/litellm/llms/deepinfra/rerank/transformation.py +++ b/litellm/llms/deepinfra/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Translate between Cohere's `/rerank` format and Deepinfra's `/rerank` format. +Translate between Cohere's `/rerank` format and Deepinfra's `/rerank` format. """ from typing import Any, Dict, List, Optional, Union diff --git a/litellm/llms/deepseek/cost_calculator.py b/litellm/llms/deepseek/cost_calculator.py index 0f4490cb3df..e652ebeac54 100644 --- a/litellm/llms/deepseek/cost_calculator.py +++ b/litellm/llms/deepseek/cost_calculator.py @@ -1,5 +1,5 @@ """ -Cost calculator for DeepSeek Chat models. +Cost calculator for DeepSeek Chat models. Handles prompt caching scenario. """ diff --git a/litellm/llms/elevenlabs/text_to_speech/transformation.py b/litellm/llms/elevenlabs/text_to_speech/transformation.py index 6a59911701b..612fc687ef9 100644 --- a/litellm/llms/elevenlabs/text_to_speech/transformation.py +++ b/litellm/llms/elevenlabs/text_to_speech/transformation.py @@ -22,7 +22,6 @@ from litellm.types.utils import all_litellm_params from ..common_utils import ElevenLabsException - if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.types.llms.openai import HttpxBinaryResponseContent diff --git a/litellm/llms/gemini/videos/transformation.py b/litellm/llms/gemini/videos/transformation.py index c7116940b22..9714c8a3923 100644 --- a/litellm/llms/gemini/videos/transformation.py +++ b/litellm/llms/gemini/videos/transformation.py @@ -55,7 +55,7 @@ def _convert_image_to_gemini_format(image_file) -> Dict[str, str]: def _usage_video_resolution_from_parameters( - parameters: Dict[str, Any] + parameters: Dict[str, Any], ) -> Optional[str]: """Normalize Veo ``parameters.resolution`` for usage and cost tracking.""" res = parameters.get("resolution") diff --git a/litellm/llms/infinity/rerank/transformation.py b/litellm/llms/infinity/rerank/transformation.py index 314bf2f8a36..b9804605454 100644 --- a/litellm/llms/infinity/rerank/transformation.py +++ b/litellm/llms/infinity/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from Cohere's /v1/rerank format to Infinity's `/v1/rerank` format. +Transformation logic from Cohere's /v1/rerank format to Infinity's `/v1/rerank` format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/jina_ai/rerank/transformation.py b/litellm/llms/jina_ai/rerank/transformation.py index ad4416925a6..56be754fc34 100644 --- a/litellm/llms/jina_ai/rerank/transformation.py +++ b/litellm/llms/jina_ai/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from Cohere's /v1/rerank format to Jina AI's `/v1/rerank` format. +Transformation logic from Cohere's /v1/rerank format to Jina AI's `/v1/rerank` format. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/lm_studio/embed/transformation.py b/litellm/llms/lm_studio/embed/transformation.py index 1285550c30f..87f4f6e73d5 100644 --- a/litellm/llms/lm_studio/embed/transformation.py +++ b/litellm/llms/lm_studio/embed/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to LM Studio's `/v1/embeddings` format. +Transformation logic from OpenAI /v1/embeddings format to LM Studio's `/v1/embeddings` format. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/novita/chat/transformation.py b/litellm/llms/novita/chat/transformation.py index c05d2d7b2c5..5a64a124ade 100644 --- a/litellm/llms/novita/chat/transformation.py +++ b/litellm/llms/novita/chat/transformation.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/chat/completions` endpoint. +Support for OpenAI's `/v1/chat/completions` endpoint. Calls done in OpenAI/openai.py as Novita AI is openai-compatible. diff --git a/litellm/llms/nvidia_nim/chat/transformation.py b/litellm/llms/nvidia_nim/chat/transformation.py index b8f8b04eb53..2ef92a90626 100644 --- a/litellm/llms/nvidia_nim/chat/transformation.py +++ b/litellm/llms/nvidia_nim/chat/transformation.py @@ -1,7 +1,7 @@ """ -Nvidia NIM endpoint: https://docs.api.nvidia.com/nim/reference/databricks-dbrx-instruct-infer +Nvidia NIM endpoint: https://docs.api.nvidia.com/nim/reference/databricks-dbrx-instruct-infer -This is OpenAI compatible +This is OpenAI compatible This file only contains param mapping logic diff --git a/litellm/llms/nvidia_nim/embed.py b/litellm/llms/nvidia_nim/embed.py index 24c6cc34e4d..61c8e8244e4 100644 --- a/litellm/llms/nvidia_nim/embed.py +++ b/litellm/llms/nvidia_nim/embed.py @@ -1,7 +1,7 @@ """ Nvidia NIM embeddings endpoint: https://docs.api.nvidia.com/nim/reference/nvidia-nv-embedqa-e5-v5-infer -This is OpenAI compatible +This is OpenAI compatible This file only contains param mapping logic diff --git a/litellm/llms/openai/chat/o_series_transformation.py b/litellm/llms/openai/chat/o_series_transformation.py index 02ae2cc9750..8db7ecf7b3a 100644 --- a/litellm/llms/openai/chat/o_series_transformation.py +++ b/litellm/llms/openai/chat/o_series_transformation.py @@ -1,14 +1,14 @@ """ -Support for o1/o3 model family +Support for o1/o3 model family https://platform.openai.com/docs/guides/reasoning Translations handled by LiteLLM: -- modalities: image => drop param (if user opts in to dropping param) -- role: system ==> translate to role 'user' -- streaming => faked by LiteLLM -- Tools, response_format => drop param (if user opts in to dropping param) -- Logprobs => drop param (if user opts in to dropping param) +- modalities: image => drop param (if user opts in to dropping param) +- role: system ==> translate to role 'user' +- streaming => faked by LiteLLM +- Tools, response_format => drop param (if user opts in to dropping param) +- Logprobs => drop param (if user opts in to dropping param) """ from typing import Any, Coroutine, List, Literal, Optional, Union, cast, overload diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index c13a976c1b9..381f215a13f 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -201,7 +201,7 @@ class BaseOpenAILLM: @staticmethod def get_openai_client_initialization_param_fields( - client_type: Literal["openai", "azure"] + client_type: Literal["openai", "azure"], ) -> Tuple[str, ...]: """Returns a tuple of fields that are used to initialize the OpenAI client""" if client_type == "openai": diff --git a/litellm/llms/openrouter/image_generation/transformation.py b/litellm/llms/openrouter/image_generation/transformation.py index a55716a5e50..9c2293eb3f1 100644 --- a/litellm/llms/openrouter/image_generation/transformation.py +++ b/litellm/llms/openrouter/image_generation/transformation.py @@ -49,7 +49,6 @@ from litellm.types.utils import ( ) from litellm.llms.openrouter.common_utils import OpenRouterException - if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj else: diff --git a/litellm/llms/sagemaker/completion/transformation.py b/litellm/llms/sagemaker/completion/transformation.py index 3e4e2460cdb..8fd32bc4460 100644 --- a/litellm/llms/sagemaker/completion/transformation.py +++ b/litellm/llms/sagemaker/completion/transformation.py @@ -1,7 +1,7 @@ """ Translate from OpenAI's `/v1/chat/completions` to Sagemaker's `/invoke` -In the Huggingface TGI format. +In the Huggingface TGI format. """ import json diff --git a/litellm/llms/sagemaker/embedding/transformation.py b/litellm/llms/sagemaker/embedding/transformation.py index 04430171187..09bdb9295e7 100644 --- a/litellm/llms/sagemaker/embedding/transformation.py +++ b/litellm/llms/sagemaker/embedding/transformation.py @@ -1,7 +1,7 @@ """ Translate from OpenAI's `/v1/embeddings` to Sagemaker's `/invoke` -In the Huggingface TGI format. +In the Huggingface TGI format. """ from typing import TYPE_CHECKING, Any, List, Optional, Union diff --git a/litellm/llms/sap/credentials.py b/litellm/llms/sap/credentials.py index 0ae351783e8..dd307ddf496 100644 --- a/litellm/llms/sap/credentials.py +++ b/litellm/llms/sap/credentials.py @@ -207,7 +207,7 @@ def resolve_resource_group(sources: List[Source]) -> Optional[str]: def _parse_service_key_once( - service_key: Optional[Union[str, dict]] + service_key: Optional[Union[str, dict]], ) -> Optional[Dict[str, Any]]: """ Pre-parse service_key if it's a string to avoid repeated JSON parsing. diff --git a/litellm/llms/snowflake/chat/transformation.py b/litellm/llms/snowflake/chat/transformation.py index 3e590680a75..23bb6f44757 100644 --- a/litellm/llms/snowflake/chat/transformation.py +++ b/litellm/llms/snowflake/chat/transformation.py @@ -14,7 +14,6 @@ from ...openai_like.chat.transformation import OpenAIGPTConfig from ..utils import SnowflakeBaseConfig - if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj diff --git a/litellm/llms/together_ai/chat.py b/litellm/llms/together_ai/chat.py index 7efb12fc1b2..238849cc1ec 100644 --- a/litellm/llms/together_ai/chat.py +++ b/litellm/llms/together_ai/chat.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/chat/completions` endpoint. +Support for OpenAI's `/v1/chat/completions` endpoint. Calls done in OpenAI/openai.py as TogetherAI is openai-compatible. diff --git a/litellm/llms/together_ai/embed.py b/litellm/llms/together_ai/embed.py index 577df0256cc..6a39b94acfc 100644 --- a/litellm/llms/together_ai/embed.py +++ b/litellm/llms/together_ai/embed.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/embeddings` endpoint. +Support for OpenAI's `/v1/embeddings` endpoint. Calls done in OpenAI/openai.py as TogetherAI is openai-compatible. diff --git a/litellm/llms/together_ai/rerank/transformation.py b/litellm/llms/together_ai/rerank/transformation.py index 63b593dfe42..f4d642bd25a 100644 --- a/litellm/llms/together_ai/rerank/transformation.py +++ b/litellm/llms/together_ai/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from Cohere's /v1/rerank format to Together AI's `/v1/rerank` format. +Transformation logic from Cohere's /v1/rerank format to Together AI's `/v1/rerank` format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/vertex_ai/context_caching/transformation.py b/litellm/llms/vertex_ai/context_caching/transformation.py index 950edbeb478..ef71357d1d2 100644 --- a/litellm/llms/vertex_ai/context_caching/transformation.py +++ b/litellm/llms/vertex_ai/context_caching/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic for context caching. +Transformation logic for context caching. Why separate file? Make it easy to see how transformation works """ @@ -19,7 +19,7 @@ from ..gemini.transformation import ( def get_first_continuous_block_idx( - filtered_messages: List[Tuple[int, AllMessageValues]] # (idx, message) + filtered_messages: List[Tuple[int, AllMessageValues]], # (idx, message) ) -> int: """ Find the array index that ends the first continuous sequence of message blocks. diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 9afa5dec465..d20c26b8a26 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -632,16 +632,14 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 contents.append(ContentType(role="user", parts=tool_call_responses)) if len(contents) == 0: - verbose_logger.warning( - """ + verbose_logger.warning(""" No contents in messages. Contents are required. See https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/generateContent#request-body. If the original request did not comply to OpenAI API requirements it should have failed by now, but LiteLLM does not check for missing messages. Setting an empty content to prevent an 400 error. Relevant Issue - https://github.com/BerriAI/litellm/issues/9733 - """ - ) + """) contents.append(ContentType(role="user", parts=[PartType(text=" ")])) return contents except Exception as e: diff --git a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py index e1b365c9f42..ba6e6f0c056 100644 --- a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py +++ b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Google AI Studio /batchEmbedContents format. +Transformation logic from OpenAI /v1/embeddings format to Google AI Studio /batchEmbedContents format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py b/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py index 9d9015c2b91..b835ad7d8fa 100644 --- a/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py +++ b/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py @@ -139,7 +139,7 @@ class VertexTextToSpeechAPI(VertexLLM): ########## End of logging ############ ####### Send the request ################### if _is_async is True: - return self.async_audio_speech( # type:ignore + return self.async_audio_speech( # type: ignore logging_obj=logging_obj, url=url, headers=headers, request=request ) sync_handler = _get_httpx_client() diff --git a/litellm/llms/vllm/completion/transformation.py b/litellm/llms/vllm/completion/transformation.py index ec4c07e95d8..e03b07f9897 100644 --- a/litellm/llms/vllm/completion/transformation.py +++ b/litellm/llms/vllm/completion/transformation.py @@ -1,5 +1,5 @@ """ -Translates from OpenAI's `/v1/chat/completions` to the VLLM sdk `llm.generate`. +Translates from OpenAI's `/v1/chat/completions` to the VLLM sdk `llm.generate`. NOT RECOMMENDED FOR PRODUCTION USE. Use `hosted_vllm/` instead. """ diff --git a/litellm/llms/voyage/embedding/transformation_contextual.py b/litellm/llms/voyage/embedding/transformation_contextual.py index 40328062e09..1f5ca99f47d 100644 --- a/litellm/llms/voyage/embedding/transformation_contextual.py +++ b/litellm/llms/voyage/embedding/transformation_contextual.py @@ -1,6 +1,6 @@ """ -This module is used to transform the request and response for the Voyage contextualized embeddings API. -This would be used for all the contextualized embeddings models in Voyage. +This module is used to transform the request and response for the Voyage contextualized embeddings API. +This would be used for all the contextualized embeddings models in Voyage. """ from typing import List, Optional, Union diff --git a/litellm/proxy/common_utils/custom_openapi_spec.py b/litellm/proxy/common_utils/custom_openapi_spec.py index a93749c3952..fa3cb02195b 100644 --- a/litellm/proxy/common_utils/custom_openapi_spec.py +++ b/litellm/proxy/common_utils/custom_openapi_spec.py @@ -324,7 +324,7 @@ class CustomOpenAPISpec: @staticmethod def add_chat_completion_request_schema( - openapi_schema: Dict[str, Any] + openapi_schema: Dict[str, Any], ) -> Dict[str, Any]: """ Add ProxyChatCompletionRequest schema to chat completion endpoints for documentation. @@ -380,7 +380,7 @@ class CustomOpenAPISpec: @staticmethod def add_responses_api_request_schema( - openapi_schema: Dict[str, Any] + openapi_schema: Dict[str, Any], ) -> Dict[str, Any]: """ Add ResponsesAPIRequestParams schema to responses API endpoints for documentation. @@ -410,7 +410,7 @@ class CustomOpenAPISpec: @staticmethod def add_llm_api_request_schema_body( - openapi_schema: Dict[str, Any] + openapi_schema: Dict[str, Any], ) -> Dict[str, Any]: """ Add LLM API request schema bodies to OpenAPI specification for documentation. diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 71abdfa5e9e..7b5a068edfe 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -257,7 +257,7 @@ async def get_form_data(request: Request) -> Dict[str, Any]: async def convert_upload_files_to_file_data( - form_data: Dict[str, Any] + form_data: Dict[str, Any], ) -> Dict[str, Any]: """ Convert FastAPI UploadFile objects to file data tuples for litellm. diff --git a/litellm/proxy/common_utils/openai_endpoint_utils.py b/litellm/proxy/common_utils/openai_endpoint_utils.py index c4bfe11aec1..905967fa465 100644 --- a/litellm/proxy/common_utils/openai_endpoint_utils.py +++ b/litellm/proxy/common_utils/openai_endpoint_utils.py @@ -1,5 +1,5 @@ """ -Contains utils used by OpenAI compatible endpoints +Contains utils used by OpenAI compatible endpoints """ from typing import Optional, Set diff --git a/litellm/proxy/config_management_endpoints/pass_through_endpoints.py b/litellm/proxy/config_management_endpoints/pass_through_endpoints.py index 5ff02b8bce0..4ebd989dc53 100644 --- a/litellm/proxy/config_management_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/config_management_endpoints/pass_through_endpoints.py @@ -1,5 +1,5 @@ """ -What is this? +What is this? CRUD endpoints for managing pass-through endpoints """ diff --git a/litellm/proxy/db/create_views.py b/litellm/proxy/db/create_views.py index d84cebcf05a..97525a528d0 100644 --- a/litellm/proxy/db/create_views.py +++ b/litellm/proxy/db/create_views.py @@ -34,8 +34,7 @@ async def create_missing_views(db: _db): # noqa: PLR0915 if not any(marker in error_msg for marker in _VIEW_NOT_FOUND_MARKERS): raise # If an error occurs, the view does not exist, so create it - await db.execute_raw( - """ + await db.execute_raw(""" CREATE VIEW "LiteLLM_VerificationTokenView" AS SELECT v.*, @@ -47,8 +46,7 @@ async def create_missing_views(db: _db): # noqa: PLR0915 FROM "LiteLLM_VerificationToken" v LEFT JOIN "LiteLLM_TeamTable" t ON v.team_id = t.team_id LEFT JOIN "LiteLLM_ProjectTable" p ON v.project_id = p.project_id; - """ - ) + """) verbose_logger.debug("LiteLLM_VerificationTokenView Created!") diff --git a/litellm/proxy/guardrails/_content_utils.py b/litellm/proxy/guardrails/_content_utils.py index 7cad1352a79..766ef0cf9f6 100644 --- a/litellm/proxy/guardrails/_content_utils.py +++ b/litellm/proxy/guardrails/_content_utils.py @@ -10,7 +10,6 @@ every text fragment. from typing import Any, Callable, Dict, FrozenSet, Iterator, List - # Call types whose body carries free-form chat / prompt text that # text-content guardrails (banned keywords, content moderation, secret # detection, …) should inspect. The proxy ingress passes ``route_type`` diff --git a/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py index c4aaea709ba..1e3dd906b9f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py @@ -4,7 +4,6 @@ from litellm.types.guardrails import SupportedGuardrailIntegrations from .akto import AktoGuardrail - if TYPE_CHECKING: from litellm.types.guardrails import Guardrail, LitellmParams diff --git a/litellm/proxy/hooks/litellm_skills/__init__.py b/litellm/proxy/hooks/litellm_skills/__init__.py index 057cf3d8b38..1507b652ab4 100644 --- a/litellm/proxy/hooks/litellm_skills/__init__.py +++ b/litellm/proxy/hooks/litellm_skills/__init__.py @@ -6,7 +6,7 @@ The actual skill logic is in litellm/llms/litellm_proxy/skills/. Usage: from litellm.proxy.hooks.litellm_skills import SkillsInjectionHook - + # Register hook in proxy litellm.callbacks.append(SkillsInjectionHook()) """ diff --git a/litellm/proxy/management_endpoints/budget_management_endpoints.py b/litellm/proxy/management_endpoints/budget_management_endpoints.py index 81b133e6c81..20735b35528 100644 --- a/litellm/proxy/management_endpoints/budget_management_endpoints.py +++ b/litellm/proxy/management_endpoints/budget_management_endpoints.py @@ -1,9 +1,9 @@ """ BUDGET MANAGEMENT -All /budget management endpoints +All /budget management endpoints -/budget/new +/budget/new /budget/info /budget/update /budget/delete diff --git a/litellm/proxy/management_endpoints/customer_endpoints.py b/litellm/proxy/management_endpoints/customer_endpoints.py index 4889f0b7f80..1fd8320db20 100644 --- a/litellm/proxy/management_endpoints/customer_endpoints.py +++ b/litellm/proxy/management_endpoints/customer_endpoints.py @@ -1,9 +1,9 @@ """ CUSTOMER MANAGEMENT -All /customer management endpoints +All /customer management endpoints -/customer/new +/customer/new /customer/info /customer/update /customer/delete diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index af84bc123ff..b14d58e8337 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -529,7 +529,7 @@ async def _update_existing_team_model_assignment( """ def _get_team_public_model_name( - model_info: Optional[Union[dict, str]] + model_info: Optional[Union[dict, str]], ) -> Optional[str]: if isinstance(model_info, dict): value = model_info.get("team_public_model_name") diff --git a/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py b/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py index 191212d6f0b..04e44c623d1 100644 --- a/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py +++ b/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py @@ -7,7 +7,7 @@ variables. Environment Variables: - MICROSOFT_AUTHORIZATION_ENDPOINT: Custom authorization endpoint URL -- MICROSOFT_TOKEN_ENDPOINT: Custom token endpoint URL +- MICROSOFT_TOKEN_ENDPOINT: Custom token endpoint URL - MICROSOFT_USERINFO_ENDPOINT: Custom userinfo endpoint URL If these are not set, the default Microsoft endpoints are used. diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 259624f1e18..564ea85200c 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -4347,9 +4347,7 @@ async def list_team( except Exception as e: team_exception = """Invalid team object for team_id: {}. team_object={}. Error: {} - """.format( - team.team_id, team.model_dump(), str(e) - ) + """.format(team.team_id, team.model_dump(), str(e)) verbose_proxy_logger.exception(team_exception) continue # Sort the responses by team_alias diff --git a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py index 872b6fa2250..ebd276fbee5 100644 --- a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py +++ b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py @@ -3,7 +3,7 @@ User Agent Analytics Endpoints This module provides optimized endpoints for tracking user agent activity metrics including: - Daily Active Users (DAU) by tags for configurable number of days -- Weekly Active Users (WAU) by tags for configurable number of weeks +- Weekly Active Users (WAU) by tags for configurable number of weeks - Monthly Active Users (MAU) by tags for configurable number of months - Summary analytics by tags diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py index a104f962630..e7696e5a18a 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py @@ -18,7 +18,6 @@ from litellm.litellm_core_utils.litellm_logging import ( from litellm.proxy._types import PassThroughEndpointLoggingTypedDict from litellm.types.utils import StandardPassThroughResponseObject - CURSOR_AGENT_ENDPOINTS: Dict[str, str] = { "POST /v0/agents": "cursor:agent:create", "GET /v0/agents": "cursor:agent:list", diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 71aeea67884..10f2181dd3e 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -232,9 +232,7 @@ class ProxyInitializationHelpers: _endpoint_str = ( f"curl --location 'http://0.0.0.0:{port}/chat/completions' \\" ) - curl_command = ( - _endpoint_str - + """ + curl_command = _endpoint_str + """ --header 'Content-Type: application/json' \\ --data ' { "model": "gpt-3.5-turbo", @@ -247,7 +245,6 @@ class ProxyInitializationHelpers: }' \n """ - ) print() # noqa print( # noqa '\033[1;34mLiteLLM: Test your local proxy with: "litellm --test" This runs an openai.ChatCompletion request to your proxy [In a new terminal tab]\033[0m\n' @@ -323,11 +320,9 @@ class ProxyInitializationHelpers: with open(os.devnull, "w") as devnull: subprocess.Popen(command, stdout=devnull, stderr=devnull) except Exception as e: - print( # noqa - f""" + print(f""" LiteLLM Warning: proxy started with `ollama` model\n`ollama serve` failed with Exception{e}. \nEnsure you run `ollama serve` - """ - ) # noqa + """) # noqa # noqa @staticmethod def _is_port_in_use(port): diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a5905765c6e..331fc77179b 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2685,11 +2685,9 @@ def run_ollama_serve(): with open(os.devnull, "w") as devnull: subprocess.Popen(command, stdout=devnull, stderr=devnull) except Exception as e: - verbose_proxy_logger.debug( - f""" + verbose_proxy_logger.debug(f""" LiteLLM Warning: proxy started with `ollama` model\n`ollama serve` failed with Exception{e}. \nEnsure you run `ollama serve` - """ - ) + """) def _get_process_rss_mb() -> Optional[float]: diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index d030fabe8b5..e3019801aae 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -3184,16 +3184,14 @@ async def provider_budgets() -> ProviderBudgetResponse: async def get_spend_by_tags( prisma_client: PrismaClient, start_date=None, end_date=None ): - response = await prisma_client.db.query_raw( - """ + response = await prisma_client.db.query_raw(""" SELECT jsonb_array_elements_text(request_tags) AS individual_request_tag, COUNT(*) AS log_count, SUM(spend) AS total_spend FROM "LiteLLM_SpendLogs" GROUP BY individual_request_tag; - """ - ) + """) return response diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index e7f5f4ee396..78ea7552e03 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -2705,8 +2705,7 @@ class PrismaClient: required_view = "LiteLLM_VerificationTokenView" expected_views_str = ", ".join(f"'{view}'" for view in expected_views) pg_schema = os.getenv("DATABASE_SCHEMA", "public") - ret = await self.db.query_raw( - f""" + ret = await self.db.query_raw(f""" WITH existing_views AS ( SELECT viewname FROM pg_views @@ -2718,8 +2717,7 @@ class PrismaClient: (SELECT COUNT(*) FROM existing_views) AS view_count, ARRAY_AGG(viewname) AS view_names FROM existing_views - """ - ) + """) expected_total_views = len(expected_views) if ret[0]["view_count"] == expected_total_views: verbose_proxy_logger.info("All necessary views exist!") @@ -2728,8 +2726,7 @@ class PrismaClient: ## check if required view exists ## if ret[0]["view_names"] and required_view not in ret[0]["view_names"]: await self.health_check() # make sure we can connect to db - await self.db.execute_raw( - """ + await self.db.execute_raw(""" CREATE VIEW "LiteLLM_VerificationTokenView" AS SELECT v.*, @@ -2739,8 +2736,7 @@ class PrismaClient: t.rpm_limit AS team_rpm_limit FROM "LiteLLM_VerificationToken" v LEFT JOIN "LiteLLM_TeamTable" t ON v.team_id = t.team_id; - """ - ) + """) verbose_proxy_logger.info( "LiteLLM_VerificationTokenView Created in DB!" diff --git a/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py b/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py index 8ce1bedcf90..b47f6a747db 100644 --- a/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py +++ b/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py @@ -1,5 +1,5 @@ """ -What is this? +What is this? Logging Pass-Through Endpoints """ diff --git a/litellm/router.py b/litellm/router.py index 7512ee387dc..c3537ffd64b 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -826,7 +826,7 @@ class Router: @staticmethod def _normalize_strategy( - strategy: Union[RoutingStrategy, str, None] + strategy: Union[RoutingStrategy, str, None], ) -> Optional[str]: if strategy is None: return None diff --git a/litellm/router_strategy/adaptive_router/hooks.py b/litellm/router_strategy/adaptive_router/hooks.py index 9e346006ac1..99fe5e26f7f 100644 --- a/litellm/router_strategy/adaptive_router/hooks.py +++ b/litellm/router_strategy/adaptive_router/hooks.py @@ -103,7 +103,7 @@ def _last_user_content(messages: Optional[List[Dict[str, Any]]]) -> Optional[str def _recent_tool_results( - messages: Optional[List[Dict[str, Any]]] + messages: Optional[List[Dict[str, Any]]], ) -> List[Dict[str, Any]]: """Extract the current turn's tool result payloads from the request messages. diff --git a/litellm/router_strategy/adaptive_router/signals.py b/litellm/router_strategy/adaptive_router/signals.py index a48bdea1eb6..5e33a64d27f 100644 --- a/litellm/router_strategy/adaptive_router/signals.py +++ b/litellm/router_strategy/adaptive_router/signals.py @@ -24,7 +24,6 @@ from litellm.router_strategy.adaptive_router.config import ( TOOL_CALL_HISTORY_MAX, ) - # ---- Public types --------------------------------------------------------- diff --git a/litellm/router_strategy/budget_limiter.py b/litellm/router_strategy/budget_limiter.py index be27b852478..da41577e99a 100644 --- a/litellm/router_strategy/budget_limiter.py +++ b/litellm/router_strategy/budget_limiter.py @@ -10,11 +10,11 @@ This means you can use this with weighted-pick, lowest-latency, simple-shuffle, Example: ``` openai: - budget_limit: 0.000000000001 - time_period: 1d + budget_limit: 0.000000000001 + time_period: 1d anthropic: - budget_limit: 100 - time_period: 7d + budget_limit: 100 + time_period: 7d ``` """ diff --git a/litellm/router_utils/get_retry_from_policy.py b/litellm/router_utils/get_retry_from_policy.py index ec326ebb50d..162d6428f85 100644 --- a/litellm/router_utils/get_retry_from_policy.py +++ b/litellm/router_utils/get_retry_from_policy.py @@ -1,5 +1,5 @@ """ -Get num retries for an exception. +Get num retries for an exception. - Account for retry policy by exception type. """ diff --git a/litellm/router_utils/pattern_match_deployments.py b/litellm/router_utils/pattern_match_deployments.py index 17b453d6031..48f85a83411 100644 --- a/litellm/router_utils/pattern_match_deployments.py +++ b/litellm/router_utils/pattern_match_deployments.py @@ -34,7 +34,7 @@ class PatternUtils: @staticmethod def sorted_patterns( - patterns: Dict[str, List[Dict]] + patterns: Dict[str, List[Dict]], ) -> List[Tuple[str, List[Dict]]]: """ Cached property for patterns sorted by specificity. diff --git a/litellm/router_utils/router_callbacks/track_deployment_metrics.py b/litellm/router_utils/router_callbacks/track_deployment_metrics.py index 1f226879d03..9039b0df8e6 100644 --- a/litellm/router_utils/router_callbacks/track_deployment_metrics.py +++ b/litellm/router_utils/router_callbacks/track_deployment_metrics.py @@ -1,5 +1,5 @@ """ -Helper functions to get/set num success and num failures per deployment +Helper functions to get/set num success and num failures per deployment set_deployment_failures_for_current_minute diff --git a/litellm/secret_managers/aws_secret_manager.py b/litellm/secret_managers/aws_secret_manager.py index fbe951e6492..60d0a713eff 100644 --- a/litellm/secret_managers/aws_secret_manager.py +++ b/litellm/secret_managers/aws_secret_manager.py @@ -4,7 +4,7 @@ This is a file for the AWS Secret Manager Integration Relevant issue: https://github.com/BerriAI/litellm/issues/1883 Requires: -* `os.environ["AWS_REGION_NAME"], +* `os.environ["AWS_REGION_NAME"], * `pip install boto3>=1.28.57` """ diff --git a/litellm/secret_managers/aws_secret_manager_v2.py b/litellm/secret_managers/aws_secret_manager_v2.py index c1b4d019dcf..4461e34396e 100644 --- a/litellm/secret_managers/aws_secret_manager_v2.py +++ b/litellm/secret_managers/aws_secret_manager_v2.py @@ -10,7 +10,7 @@ Handles Async Operations for: Relevant issue: https://github.com/BerriAI/litellm/issues/1883 Requires: -* `os.environ["AWS_REGION_NAME"], +* `os.environ["AWS_REGION_NAME"], * `pip install boto3>=1.28.57` """ diff --git a/litellm/vector_store_files/utils.py b/litellm/vector_store_files/utils.py index ffe73516bda..1ee5b47e306 100644 --- a/litellm/vector_store_files/utils.py +++ b/litellm/vector_store_files/utils.py @@ -21,7 +21,7 @@ class VectorStoreFileRequestUtils: @staticmethod def get_create_request_params( - params: Dict[str, Any] + params: Dict[str, Any], ) -> VectorStoreFileCreateRequest: filtered = VectorStoreFileRequestUtils._filter_params( params=params, model=VectorStoreFileCreateRequest @@ -37,7 +37,7 @@ class VectorStoreFileRequestUtils: @staticmethod def get_update_request_params( - params: Dict[str, Any] + params: Dict[str, Any], ) -> VectorStoreFileUpdateRequest: filtered = VectorStoreFileRequestUtils._filter_params( params=params, model=VectorStoreFileUpdateRequest diff --git a/pyproject.toml b/pyproject.toml index 48acf8d8712..4b4b3a06b03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,11 @@ utils = [ # Not in Docker or PyPI proxy extra. "numpydoc==1.8.0", ] -caching = ["diskcache==5.6.3"] +# diskcache intentionally unpinned: CVE-2025-69872 (pickle RCE) has no +# upstream fix. Stub kept so `pip install litellm[caching]` doesn't warn; +# DiskCache loads diskcache lazily and forces JSONDisk for safety. See +# litellm/caching/disk_cache.py. +caching = [] semantic-router = [ "semantic-router==0.1.12; python_version < '3.14'", "aurelio-sdk==0.0.19; python_version < '3.14'", @@ -118,7 +122,7 @@ litellm-proxy = "litellm.proxy.client.cli:cli" dev = [ "diff-cover==9.7.2", "flake8==7.3.0", - "black==24.10.0", + "black==26.3.1", "mypy==1.19.0", "pytest==9.0.3", "pytest-mock==3.15.1", diff --git a/scripts/adaptive_router_demo/eval.py b/scripts/adaptive_router_demo/eval.py index b02e4a37d31..66b91c71932 100644 --- a/scripts/adaptive_router_demo/eval.py +++ b/scripts/adaptive_router_demo/eval.py @@ -35,7 +35,7 @@ import httpx class EvalCase: category: str prompt: str - ideal: str # criteria the judge checks the response against + ideal: str # criteria the judge checks the response against EVAL_CASES: List[EvalCase] = [ @@ -177,14 +177,19 @@ async def evaluate( async with httpx.AsyncClient() as client: for i, case in enumerate(EVAL_CASES, 1): print(f"\n[{i}/{len(EVAL_CASES)}] category={case.category}") - print(f" prompt : {case.prompt[:80]}{'…' if len(case.prompt) > 80 else ''}") + print( + f" prompt : {case.prompt[:80]}{'…' if len(case.prompt) > 80 else ''}" + ) session_id = f"eval-{uuid.uuid4()}" # Round 1: single-turn real request — get the actual LLM response to judge. try: response, chosen = await _chat( - client, proxy_url, api_key, router, + client, + proxy_url, + api_key, + router, [{"role": "user", "content": case.prompt}], session_id=session_id, ) @@ -194,16 +199,25 @@ async def evaluate( continue print(f" model : {chosen or router}") - print(f" response : {response[:120].replace(chr(10), ' ')}{'…' if len(response) > 120 else ''}") + print( + f" response : {response[:120].replace(chr(10), ' ')}{'…' if len(response) > 120 else ''}" + ) # Judge the real response. judge_msgs = [ {"role": "system", "content": JUDGE_SYSTEM}, - {"role": "user", "content": _judge_user(case.prompt, case.ideal, response)}, + { + "role": "user", + "content": _judge_user(case.prompt, case.ideal, response), + }, ] try: verdict, _ = await _chat( - client, proxy_url, api_key, judge_model, judge_msgs, + client, + proxy_url, + api_key, + judge_model, + judge_msgs, ) except Exception as exc: # noqa: BLE001 print(f" ERROR calling judge: {exc}", file=sys.stderr) @@ -227,15 +241,19 @@ async def evaluate( # On PASS → satisfaction follow-up (+alpha). On FAIL → neutral (no signal). follow_up = SATISFY_FOLLOWUP if is_pass else NEUTRAL_FOLLOWUP bandit_msgs = [ - {"role": "user", "content": case.prompt}, + {"role": "user", "content": case.prompt}, {"role": "assistant", "content": response}, - {"role": "user", "content": "ok continue"}, + {"role": "user", "content": "ok continue"}, {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": follow_up}, + {"role": "user", "content": follow_up}, ] try: await _chat( - client, proxy_url, api_key, router, bandit_msgs, + client, + proxy_url, + api_key, + router, + bandit_msgs, session_id=session_id, ) except Exception as exc: # noqa: BLE001 @@ -257,11 +275,17 @@ async def evaluate( # Entry point # --------------------------------------------------------------------------- def main() -> None: - ap = argparse.ArgumentParser(description="Evaluate the adaptive router with LLM-as-judge.") - ap.add_argument("--proxy-url", default="http://localhost:4000") - ap.add_argument("--api-key", required=True, help="proxy API key") - ap.add_argument("--router", default="smart-cheap-router", help="adaptive router model name") - ap.add_argument("--judge-model", default="smart", help="model name for the judge (via proxy)") + ap = argparse.ArgumentParser( + description="Evaluate the adaptive router with LLM-as-judge." + ) + ap.add_argument("--proxy-url", default="http://localhost:4000") + ap.add_argument("--api-key", required=True, help="proxy API key") + ap.add_argument( + "--router", default="smart-cheap-router", help="adaptive router model name" + ) + ap.add_argument( + "--judge-model", default="smart", help="model name for the judge (via proxy)" + ) args = ap.parse_args() asyncio.run(evaluate(args.proxy_url, args.api_key, args.router, args.judge_model)) diff --git a/scripts/adaptive_router_demo/traffic.py b/scripts/adaptive_router_demo/traffic.py index eae5506eaee..dd64d95c90c 100644 --- a/scripts/adaptive_router_demo/traffic.py +++ b/scripts/adaptive_router_demo/traffic.py @@ -72,8 +72,8 @@ PROMPTS: Dict[str, List[str]] = { # so that signals attribute to the right (type, model) bandit cell. SATISFY: Dict[str, str] = { "code_generation": "thanks, that works! now write me a python function that does the inverse", - "factual_lookup": "perfect, thanks! who is the current prime minister?", - "writing": "great, thanks! now write a follow-up email confirming attendance", + "factual_lookup": "perfect, thanks! who is the current prime minister?", + "writing": "great, thanks! now write a follow-up email confirming attendance", } # Neutral follow-up — does not match any signal regex, does not move the bandit. @@ -83,8 +83,8 @@ NEUTRAL_FOLLOWUP = "ok, noted" # Defaults: smart dominates code/writing; both are fine for factual_lookup. ORACLE: Dict[str, Dict[str, float]] = { "code_generation": {"smart": 0.92, "fast": 0.35}, - "factual_lookup": {"smart": 0.90, "fast": 0.85}, - "writing": {"smart": 0.85, "fast": 0.55}, + "factual_lookup": {"smart": 0.90, "fast": 0.85}, + "writing": {"smart": 0.85, "fast": 0.55}, } # Fabricated assistant turn — content doesn't matter for the hook, only the role. @@ -94,11 +94,11 @@ FAB_ASSISTANT = "Got it. Working on that now." def _build_messages(prompt: str, last_user: str) -> List[Dict[str, str]]: """5-message conversation that passes the SIGNAL_GATE_MIN_MESSAGES=4 gate.""" return [ - {"role": "user", "content": prompt}, + {"role": "user", "content": prompt}, {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": "ok continue"}, + {"role": "user", "content": "ok continue"}, {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": last_user}, + {"role": "user", "content": last_user}, ] @@ -155,7 +155,11 @@ async def _drive_one_session( # # Round 1: neutral follow-up → no signal fires, but we learn the pick. ok, chosen = await _send( - client, proxy_url, api_key, router, session_id, + client, + proxy_url, + api_key, + router, + session_id, _build_messages(prompt, NEUTRAL_FOLLOWUP), mock_response=FAB_ASSISTANT, ) @@ -171,10 +175,15 @@ async def _drive_one_session( # follow-up matches satisfaction → +alpha for (request_type, chosen). history = _build_messages(prompt, NEUTRAL_FOLLOWUP) + [ {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": follow_up}, + {"role": "user", "content": follow_up}, ] await _send( - client, proxy_url, api_key, router, session_id, history, + client, + proxy_url, + api_key, + router, + session_id, + history, mock_response=FAB_ASSISTANT, ) return chosen @@ -183,13 +192,22 @@ async def _drive_one_session( async def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--proxy-url", default="http://localhost:4000") - ap.add_argument("--api-key", required=True, help="proxy key with /v1/chat/completions perms") - ap.add_argument("--router", default="smart-cheap-router") - ap.add_argument("--rounds", type=int, default=100) - ap.add_argument("--rate", type=float, default=0.5, - help="seconds between sessions; lower = faster") - ap.add_argument("--types", default="code_generation,factual_lookup,writing", - help="comma-separated subset of request types to drive") + ap.add_argument( + "--api-key", required=True, help="proxy key with /v1/chat/completions perms" + ) + ap.add_argument("--router", default="smart-cheap-router") + ap.add_argument("--rounds", type=int, default=100) + ap.add_argument( + "--rate", + type=float, + default=0.5, + help="seconds between sessions; lower = faster", + ) + ap.add_argument( + "--types", + default="code_generation,factual_lookup,writing", + help="comma-separated subset of request types to drive", + ) args = ap.parse_args() types = [t.strip() for t in args.types.split(",") if t.strip() in PROMPTS] @@ -207,7 +225,12 @@ async def main() -> None: rt = random.choice(types) prompt = random.choice(PROMPTS[rt]) chosen = await _drive_one_session( - client, args.proxy_url, args.api_key, args.router, rt, prompt, + client, + args.proxy_url, + args.api_key, + args.router, + rt, + prompt, ) if chosen: counts[(rt, chosen)] = counts.get((rt, chosen), 0) + 1 diff --git a/scripts/benchmark_mock.py b/scripts/benchmark_mock.py index 55dbb1d4134..6c3a6ad8944 100644 --- a/scripts/benchmark_mock.py +++ b/scripts/benchmark_mock.py @@ -8,7 +8,6 @@ import statistics import aiohttp - REQUEST_BODY = { "model": "db-openai-endpoint", "messages": [{"role": "user", "content": "hi"}], diff --git a/scripts/benchmark_proxy_vs_provider.py b/scripts/benchmark_proxy_vs_provider.py index 6196580b230..8e1289e3c29 100755 --- a/scripts/benchmark_proxy_vs_provider.py +++ b/scripts/benchmark_proxy_vs_provider.py @@ -11,7 +11,7 @@ USAGE EXAMPLES: export PROVIDER_URL='https://api.openai.com/v1/chat/completions' export LITELLM_PROXY_API_KEY='sk-1234' export PROVIDER_API_KEY='sk-openai-key' - + # Run from scripts directory cd scripts python benchmark_proxy_vs_provider.py diff --git a/scripts/eval_compression.py b/scripts/eval_compression.py index a169cc02d74..44265eb1518 100644 --- a/scripts/eval_compression.py +++ b/scripts/eval_compression.py @@ -42,8 +42,7 @@ from litellm.types.utils import CallTypes PROBLEMS = [ { "id": "has_close_elements", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def has_close_elements(numbers: List[float], threshold: float) -> bool: @@ -54,10 +53,8 @@ PROBLEMS = [ >>> has_close_elements([1.0, 2.8, 3.0, 4.0, 5.0, 2.0], 0.3) True \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert has_close_elements([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.3) == True assert has_close_elements([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.05) == False assert has_close_elements([1.0, 2.0, 5.9, 4.0, 5.0], 0.95) == True @@ -65,13 +62,11 @@ PROBLEMS = [ assert has_close_elements([1.0, 2.0, 3.0, 4.0, 5.0], 2.0) == True assert has_close_elements([], 0.5) == False print("PASSED") - """ - ), + """), }, { "id": "separate_paren_groups", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def separate_paren_groups(paren_string: str) -> List[str]: @@ -82,22 +77,18 @@ PROBLEMS = [ >>> separate_paren_groups('( ) (( )) (( )( ))') ['()', '(())', '(()())'] \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert separate_paren_groups('(()()) ((())) () ((())()())') == ['(()())', '((()))', '()', '((())()())'] assert separate_paren_groups('() (()) ((())) (((())))') == ['()', '(())', '((()))', '(((())))'] assert separate_paren_groups('(()(()))') == ['(()(()))'] assert separate_paren_groups('( ) (( )) (( )( ))') == ['()', '(())', '(()())'] print("PASSED") - """ - ), + """), }, { "id": "truncate_number", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ def truncate_number(number: float) -> float: \"\"\"Given a positive floating point number, it can be decomposed into an integer part (largest integer smaller than given number) and decimals @@ -106,21 +97,17 @@ PROBLEMS = [ >>> truncate_number(3.5) 0.5 \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert truncate_number(3.5) == 0.5 assert abs(truncate_number(1.33) - 0.33) < 1e-6 assert abs(truncate_number(123.456) - 0.456) < 1e-6 print("PASSED") - """ - ), + """), }, { "id": "below_zero", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def below_zero(operations: List[int]) -> bool: @@ -132,10 +119,8 @@ PROBLEMS = [ >>> below_zero([1, 2, -4, 5]) True \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert below_zero([]) == False assert below_zero([1, 2, -3, 1, 2, -3]) == False assert below_zero([1, 2, -4, 5, 6]) == True @@ -143,13 +128,11 @@ PROBLEMS = [ assert below_zero([1, -1, 2, -2, 5, -5, 4, -5]) == True assert below_zero([1, -2]) == True print("PASSED") - """ - ), + """), }, { "id": "mean_absolute_deviation", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def mean_absolute_deviation(numbers: List[float]) -> float: @@ -161,21 +144,17 @@ PROBLEMS = [ >>> mean_absolute_deviation([1.0, 2.0, 3.0, 4.0]) 1.0 \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert abs(mean_absolute_deviation([1.0, 2.0, 3.0, 4.0]) - 1.0) < 1e-6 assert abs(mean_absolute_deviation([1.0, 2.0, 3.0, 4.0, 5.0]) - 1.2) < 1e-6 assert abs(mean_absolute_deviation([1.0, 1.0, 1.0, 1.0]) - 0.0) < 1e-6 print("PASSED") - """ - ), + """), }, { "id": "intersperse", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def intersperse(numbers: List[int], delimiter: int) -> List[int]: @@ -185,21 +164,17 @@ PROBLEMS = [ >>> intersperse([1, 2, 3], 4) [1, 4, 2, 4, 3] \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert intersperse([], 7) == [] assert intersperse([5, 6, 3, 2], 8) == [5, 8, 6, 8, 3, 8, 2] assert intersperse([2, 2, 2], 2) == [2, 2, 2, 2, 2] print("PASSED") - """ - ), + """), }, { "id": "parse_nested_parens", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def parse_nested_parens(paren_string: str) -> List[int]: @@ -209,21 +184,17 @@ PROBLEMS = [ >>> parse_nested_parens('(()()) ((())) () ((())())') [2, 3, 1, 3] \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert parse_nested_parens('(()()) ((())) () ((())())') == [2, 3, 1, 3] assert parse_nested_parens('() (()) ((())) (((())))') == [1, 2, 3, 4] assert parse_nested_parens('(()(())((())))') == [4] print("PASSED") - """ - ), + """), }, { "id": "filter_by_substring", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def filter_by_substring(strings: List[str], substring: str) -> List[str]: @@ -233,22 +204,18 @@ PROBLEMS = [ >>> filter_by_substring(['abc', 'bacd', 'cde', 'array'], 'a') ['abc', 'bacd', 'array'] \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert filter_by_substring([], 'john') == [] assert filter_by_substring(['xxx', 'asd', 'xxy', 'john doe', 'xxxuj', 'xxx'], 'xxx') == ['xxx', 'xxxuj', 'xxx'] assert filter_by_substring(['xxx', 'asd', 'aaber', 'john doe', 'xxxuj', 'xxx'], 'xx') == ['xxx', 'xxxuj', 'xxx'] assert filter_by_substring(['grunt', 'hierarchial', 'abc', 'hierarchial'], 'hi') == ['hierarchial', 'hierarchial'] print("PASSED") - """ - ), + """), }, { "id": "sum_product", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List, Tuple def sum_product(numbers: List[int]) -> Tuple[int, int]: @@ -259,23 +226,19 @@ PROBLEMS = [ >>> sum_product([1, 2, 3, 4]) (10, 24) \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert sum_product([]) == (0, 1) assert sum_product([1, 1, 1]) == (3, 1) assert sum_product([100, 0]) == (100, 0) assert sum_product([3, 5, 7]) == (15, 105) assert sum_product([10]) == (10, 10) print("PASSED") - """ - ), + """), }, { "id": "max_element", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def max_element(l: List[int]) -> int: @@ -285,21 +248,17 @@ PROBLEMS = [ >>> max_element([5, 3, -5, 2, -3, 3, 9, 0, 123, 1, -10]) 123 \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert max_element([1, 2, 3]) == 3 assert max_element([5, 3, -5, 2, -3, 3, 9, 0, 124, 1, -10]) == 124 assert max_element([-1, -2, -3]) == -1 print("PASSED") - """ - ), + """), }, { "id": "fizz_buzz", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ def fizz_buzz(n: int) -> int: \"\"\"Return the number of times the digit 7 appears in integers less than n which are divisible by 11 or 13. >>> fizz_buzz(50) @@ -309,10 +268,8 @@ PROBLEMS = [ >>> fizz_buzz(79) 3 \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert fizz_buzz(50) == 0 assert fizz_buzz(78) == 2 assert fizz_buzz(79) == 3 @@ -320,13 +277,11 @@ PROBLEMS = [ assert fizz_buzz(200) == 6 assert fizz_buzz(4000) == 192 print("PASSED") - """ - ), + """), }, { "id": "sort_by_binary_len", - "prompt": textwrap.dedent( - """\ + "prompt": textwrap.dedent("""\ from typing import List def sort_array(arr: List[int]) -> List[int]: @@ -339,10 +294,8 @@ PROBLEMS = [ >>> sort_array([1, 0, 2, 3, 4]) [0, 1, 2, 4, 3] \"\"\" - """ - ), - "tests": textwrap.dedent( - """\ + """), + "tests": textwrap.dedent("""\ assert sort_array([1, 5, 2, 3, 4]) == [1, 2, 4, 3, 5] assert sort_array([-2, -3, -4, -5, -6]) == [-6, -5, -4, -3, -2] assert sort_array([1, 0, 2, 3, 4]) == [0, 1, 2, 4, 3] @@ -350,8 +303,7 @@ PROBLEMS = [ assert sort_array([2, 5, 77, 4, 5, 3, 5, 7, 2, 3, 4]) == [2, 2, 4, 4, 3, 3, 5, 5, 5, 7, 77] assert sort_array([3, 6, 44, 12, 32, 5]) == [32, 3, 5, 6, 12, 44] print("PASSED") - """ - ), + """), }, ] @@ -360,8 +312,7 @@ PROBLEMS = [ # compressor to identify and drop them. DISTRACTOR_SNIPPETS = [ # distractor 0 — database connection pool - textwrap.dedent( - """\ + textwrap.dedent("""\ # db_pool.py import threading from contextlib import contextmanager @@ -408,11 +359,9 @@ DISTRACTOR_SNIPPETS = [ for conn in self._pool: conn.close() self._pool.clear() - """ - ), + """), # distractor 1 — HTTP retry logic - textwrap.dedent( - """\ + textwrap.dedent("""\ # http_retry.py import time import random @@ -456,11 +405,9 @@ DISTRACTOR_SNIPPETS = [ resp = requests.get(url, params=params, timeout=30) resp.raise_for_status() return resp.json() - """ - ), + """), # distractor 2 — LRU cache implementation - textwrap.dedent( - """\ + textwrap.dedent("""\ # lru_cache.py from collections import OrderedDict from threading import RLock @@ -509,11 +456,9 @@ DISTRACTOR_SNIPPETS = [ def __contains__(self, key): return key in self._cache - """ - ), + """), # distractor 3 — CSV report generator - textwrap.dedent( - """\ + textwrap.dedent("""\ # report_gen.py import csv import io @@ -566,11 +511,9 @@ DISTRACTOR_SNIPPETS = [ except (ValueError, KeyError): return False return self.filter_rows(in_range) - """ - ), + """), # distractor 4 — async task queue - textwrap.dedent( - """\ + textwrap.dedent("""\ # task_queue.py import asyncio import logging @@ -640,11 +583,9 @@ DISTRACTOR_SNIPPETS = [ async def shutdown(self): for w in self._workers: w.cancel() - """ - ), + """), # distractor 5 — config parser with env var interpolation - textwrap.dedent( - """\ + textwrap.dedent("""\ # config_parser.py import os import re @@ -707,8 +648,7 @@ DISTRACTOR_SNIPPETS = [ if val is None: raise ConfigError(f"Required config key missing: {key}") return val - """ - ), + """), ] diff --git a/scripts/health_check/benchmark_get_all_latest_health_checks.py b/scripts/health_check/benchmark_get_all_latest_health_checks.py index 45845554c86..5d0957d4f7d 100644 --- a/scripts/health_check/benchmark_get_all_latest_health_checks.py +++ b/scripts/health_check/benchmark_get_all_latest_health_checks.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Bench LiteLLM_HealthCheckTable + PrismaClient +Bench LiteLLM_HealthCheckTable + PrismaClient - set DATABASE_URL to your Postgres - Run ```prisma generate``` to install prisma client before running test ) - This test writes to the default "public" database. Make sure to run cleanup after testing diff --git a/tests/batches_tests/test_batch_custom_pricing.py b/tests/batches_tests/test_batch_custom_pricing.py index f4e84b46bee..911fd8e3113 100644 --- a/tests/batches_tests/test_batch_custom_pricing.py +++ b/tests/batches_tests/test_batch_custom_pricing.py @@ -18,7 +18,6 @@ from litellm.batches.batch_utils import ( from litellm.cost_calculator import batch_cost_calculator from litellm.types.utils import Usage - # --- helpers --- diff --git a/tests/batches_tests/test_hosted_vllm_batches_and_files.py b/tests/batches_tests/test_hosted_vllm_batches_and_files.py index c7a25c71c53..b7f21f6cab2 100644 --- a/tests/batches_tests/test_hosted_vllm_batches_and_files.py +++ b/tests/batches_tests/test_hosted_vllm_batches_and_files.py @@ -20,7 +20,6 @@ sys.path.insert(0, os.path.abspath("../..")) import litellm - SERVER_URL = "https://exampleopenaiendpoint-production-0ee2.up.railway.app/v1" diff --git a/tests/benchmarks/test_benchmarks.py b/tests/benchmarks/test_benchmarks.py index 123dad93e11..d5ade58980a 100644 --- a/tests/benchmarks/test_benchmarks.py +++ b/tests/benchmarks/test_benchmarks.py @@ -12,7 +12,6 @@ import litellm from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.litellm_core_utils.token_counter import token_counter - # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- diff --git a/tests/code_coverage_tests/check_fastuuid_usage.py b/tests/code_coverage_tests/check_fastuuid_usage.py index e0433454371..bb3176b57a3 100644 --- a/tests/code_coverage_tests/check_fastuuid_usage.py +++ b/tests/code_coverage_tests/check_fastuuid_usage.py @@ -2,7 +2,6 @@ import ast import os from typing import List, Dict, Any - ALLOWED_FILE = os.path.normpath("litellm/_uuid.py") diff --git a/tests/enterprise/conftest.py b/tests/enterprise/conftest.py index 0365bbbcfa0..524ab85b938 100644 --- a/tests/enterprise/conftest.py +++ b/tests/enterprise/conftest.py @@ -23,8 +23,6 @@ def event_loop(): loop.close() - - @pytest.fixture(scope="function", autouse=True) def setup_and_teardown(): """ diff --git a/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py b/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py index 212c5d4a322..6bdc808c4f3 100644 --- a/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py +++ b/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py @@ -944,8 +944,8 @@ def test_callback_failure_metric_different_callbacks(prometheus_logger): async def test_langfuse_callback_failure_metric(prometheus_logger): """ Test that Langfuse callback failures are properly tracked in Prometheus metrics. - - This test verifies that when Langfuse logging fails, the + + This test verifies that when Langfuse logging fails, the litellm_callback_logging_failures_metric is incremented with callback_name="langfuse". """ from unittest.mock import MagicMock, patch @@ -957,16 +957,20 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): # Get initial value initial_value = 0 try: - initial_value = prometheus_logger.litellm_callback_logging_failures_metric.labels( - callback_name="langfuse" - )._value.get() + initial_value = ( + prometheus_logger.litellm_callback_logging_failures_metric.labels( + callback_name="langfuse" + )._value.get() + ) except Exception: initial_value = 0 - + # Create Langfuse logger with mocked initialization - with patch("litellm.integrations.langfuse.langfuse_prompt_management.langfuse_client_init"): + with patch( + "litellm.integrations.langfuse.langfuse_prompt_management.langfuse_client_init" + ): langfuse_logger = LangfusePromptManagement() - + # Mock the log_event_on_langfuse to raise an exception with patch( "litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler.get_langfuse_logger_for_request" @@ -974,14 +978,16 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): mock_logger = MagicMock() mock_logger.log_event_on_langfuse.side_effect = Exception("Langfuse API error") mock_get_logger.return_value = mock_logger - + # Mock handle_callback_failure to track calls - with patch.object(prometheus_logger, "increment_callback_logging_failure") as mock_increment: + with patch.object( + prometheus_logger, "increment_callback_logging_failure" + ) as mock_increment: # Inject prometheus logger into the langfuse logger - langfuse_logger.handle_callback_failure = lambda callback_name: mock_increment( - callback_name=callback_name + langfuse_logger.handle_callback_failure = ( + lambda callback_name: mock_increment(callback_name=callback_name) ) - + # Call async_log_success_event - should catch exception and increment metric await langfuse_logger.async_log_success_event( kwargs={}, @@ -989,10 +995,10 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): start_time=None, end_time=None, ) - + # Verify that increment was called with correct callback name mock_increment.assert_called_once_with(callback_name="langfuse") - + print("✓ Langfuse callback failure metric test passed") @@ -1000,8 +1006,8 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): async def test_langfuse_otel_callback_failure_metric(prometheus_logger): """ Test that Langfuse OTEL callback failures are properly tracked in Prometheus metrics. - - This test verifies that when Langfuse OTEL logging fails, the + + This test verifies that when Langfuse OTEL logging fails, the litellm_callback_logging_failures_metric is incremented with callback_name="langfuse_otel". """ from unittest.mock import MagicMock, patch @@ -1011,50 +1017,58 @@ async def test_langfuse_otel_callback_failure_metric(prometheus_logger): # Get initial value initial_value = 0 try: - initial_value = prometheus_logger.litellm_callback_logging_failures_metric.labels( - callback_name="langfuse_otel" - )._value.get() + initial_value = ( + prometheus_logger.litellm_callback_logging_failures_metric.labels( + callback_name="langfuse_otel" + )._value.get() + ) except Exception: initial_value = 0 - + # Create Langfuse OTEL logger with mocked initialization - with patch("litellm.integrations.opentelemetry.OpenTelemetry.__init__", return_value=None): + with patch( + "litellm.integrations.opentelemetry.OpenTelemetry.__init__", return_value=None + ): langfuse_otel_logger = LangfuseOtelLogger(callback_name="langfuse_otel") langfuse_otel_logger.callback_name = "langfuse_otel" - + # Mock handle_callback_failure to track calls - with patch.object(prometheus_logger, "increment_callback_logging_failure") as mock_increment: + with patch.object( + prometheus_logger, "increment_callback_logging_failure" + ) as mock_increment: # Inject prometheus logger into the langfuse otel logger - langfuse_otel_logger.handle_callback_failure = lambda callback_name: mock_increment( - callback_name=callback_name + langfuse_otel_logger.handle_callback_failure = ( + lambda callback_name: mock_increment(callback_name=callback_name) ) - + # Test that the OpenTelemetry base class set_attributes exception handler works # This is where langfuse_otel failures are caught and tracked - with patch.object(langfuse_otel_logger, "set_attributes") as mock_set_attributes: + with patch.object( + langfuse_otel_logger, "set_attributes" + ) as mock_set_attributes: # Simulate the exception handling in set_attributes def set_attributes_with_error(*args, **kwargs): # This simulates what happens in the real set_attributes method try: raise Exception("Attribute error") except Exception as e: - langfuse_otel_logger.handle_callback_failure(callback_name=langfuse_otel_logger.callback_name) - + langfuse_otel_logger.handle_callback_failure( + callback_name=langfuse_otel_logger.callback_name + ) + mock_set_attributes.side_effect = set_attributes_with_error - + # Call set_attributes try: langfuse_otel_logger.set_attributes( - span=MagicMock(), - kwargs={}, - response_obj={} + span=MagicMock(), kwargs={}, response_obj={} ) except Exception: pass - + # Verify that increment was called with correct callback name mock_increment.assert_called_with(callback_name="langfuse_otel") - + print("✓ Langfuse OTEL callback failure metric test passed") diff --git a/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py b/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py index a45df5df008..ab5c5576625 100644 --- a/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py +++ b/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py @@ -49,10 +49,13 @@ async def test_enterprise_custom_auth_returns_string(): mock_user_auth = AsyncMock(return_value="sk-test-key") request = MagicMock(spec=Request) - with patch( - "litellm.proxy.auth.user_api_key_auth.enterprise_custom_auth", mock_user_auth - ), patch("litellm.proxy.proxy_server.master_key", "sk-1234"), patch( - "litellm.proxy.proxy_server.prisma_client", MagicMock() + with ( + patch( + "litellm.proxy.auth.user_api_key_auth.enterprise_custom_auth", + mock_user_auth, + ), + patch("litellm.proxy.proxy_server.master_key", "sk-1234"), + patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), ): # Verify the key is correctly handled in _user_api_key_auth_builder with patch( diff --git a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py index 0d27df50d15..b7fd665c410 100644 --- a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py +++ b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py @@ -105,7 +105,9 @@ async def test_apply_guardrail_endpoint_with_presidio_guardrail(): mock_guardrail = Mock(spec=CustomGuardrail) # Simulate masking PII entities - returns GenericGuardrailAPIInputs (dict with texts key) mock_guardrail.apply_guardrail = AsyncMock( - return_value={"texts": ["My name is [PERSON] and my email is [EMAIL_ADDRESS]"]} + return_value={ + "texts": ["My name is [PERSON] and my email is [EMAIL_ADDRESS]"] + } ) # Configure the registry to return our mock guardrail diff --git a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py index 9f4ca4ed108..3f9c2486d9c 100644 --- a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py +++ b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py @@ -95,9 +95,9 @@ async def test_async_pre_call_deployment_hook_resolves_model_id_from_litellm_met kwargs=kwargs, call_type=CallTypes.acreate_batch ) - assert result["input_file_id"] == provider_file_id, ( - f"Expected provider file ID '{provider_file_id}', got '{result['input_file_id']}'" - ) + assert ( + result["input_file_id"] == provider_file_id + ), f"Expected provider file ID '{provider_file_id}', got '{result['input_file_id']}'" @pytest.mark.asyncio @@ -134,9 +134,9 @@ async def test_async_pre_call_deployment_hook_prefers_top_level_model_info(): kwargs=kwargs, call_type=CallTypes.acreate_batch ) - assert result["input_file_id"] == top_level_provider_file, ( - "Should prefer top-level model_info over litellm_metadata" - ) + assert ( + result["input_file_id"] == top_level_provider_file + ), "Should prefer top-level model_info over litellm_metadata" @pytest.mark.asyncio @@ -162,9 +162,9 @@ async def test_async_pre_call_deployment_hook_no_model_info_leaves_file_id_uncha kwargs=kwargs, call_type=CallTypes.acreate_batch ) - assert result["input_file_id"] == managed_file_id, ( - "File ID should remain unchanged when model_info is not available" - ) + assert ( + result["input_file_id"] == managed_file_id + ), "File ID should remain unchanged when model_info is not available" # def test_list_managed_files(): @@ -341,7 +341,9 @@ async def test_async_pre_call_hook_for_unified_finetuning_job(): @pytest.mark.asyncio -@pytest.mark.parametrize("call_type", ["afile_content", "afile_delete", "afile_retrieve"]) +@pytest.mark.parametrize( + "call_type", ["afile_content", "afile_delete", "afile_retrieve"] +) async def test_can_user_call_unified_file_id(call_type): """ Test that on file retrieve, delete, and content we check if the user has access to the file @@ -601,7 +603,7 @@ async def test_error_file_id_for_failed_batch(): "litellm_model_name": "gpt-4o", "unified_batch_id": "litellm_proxy;model_id:test-model-id;llm_batch_id:batch_abc123", } - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=AsyncMock() ) @@ -620,12 +622,11 @@ async def test_error_file_id_for_failed_batch(): # Mock the afile_retrieve to simulate retrieving error file metadata with patch("litellm.afile_retrieve", new_callable=AsyncMock) as mock_retrieve: mock_retrieve.return_value = error_file_object - + user_api_key_dict = UserAPIKeyAuth( - user_id="test-user-123", - parent_otel_span=MagicMock() + user_id="test-user-123", parent_otel_span=MagicMock() ) - + response = await proxy_managed_files.async_post_call_success_hook( data={}, user_api_key_dict=user_api_key_dict, @@ -636,7 +637,9 @@ async def test_error_file_id_for_failed_batch(): assert cast(LiteLLMBatch, response).error_file_id is not None assert not cast(LiteLLMBatch, response).error_file_id.startswith("error-") # Verify it's a base64 encoded managed file ID - assert _is_base64_encoded_unified_file_id(cast(LiteLLMBatch, response).error_file_id) + assert _is_base64_encoded_unified_file_id( + cast(LiteLLMBatch, response).error_file_id + ) @pytest.mark.asyncio @@ -650,7 +653,7 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation(): # Use AsyncMock instead of real database connection prisma_client = AsyncMock() - + batch = LiteLLMBatch( id="bGl0ZWxsbV9wcm94eTttb2RlbF9pZDoxMjM0NTY3OTtsbG1fYmF0Y2hfaWQ6YmF0Y2hfNjg1YzVlNWQ2Mzk4ODE5MGI4NWJkYjIxNDdiYTEzMWQ", completion_window="24h", @@ -678,8 +681,10 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation(): # first retrieve batch tasks = [] first_create_task = asyncio.create_task - with patch('asyncio.create_task') as mock_create_task: - mock_create_task.side_effect = lambda coro: tasks.append(first_create_task(coro)) or tasks[-1] + with patch("asyncio.create_task") as mock_create_task: + mock_create_task.side_effect = ( + lambda coro: tasks.append(first_create_task(coro)) or tasks[-1] + ) response = await proxy_managed_files.async_post_call_success_hook( data={}, @@ -700,8 +705,10 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation(): # second retrieve batch tasks = [] second_create_task = asyncio.create_task - with patch('asyncio.create_task') as mock_create_task: - mock_create_task.side_effect = lambda coro: tasks.append(second_create_task(coro)) or tasks[-1] + with patch("asyncio.create_task") as mock_create_task: + mock_create_task.side_effect = ( + lambda coro: tasks.append(second_create_task(coro)) or tasks[-1] + ) await proxy_managed_files.async_post_call_success_hook( data={}, @@ -728,7 +735,7 @@ def test_update_responses_input_with_unified_file_id(): # Create a base64-encoded unified file ID # This decodes to: litellm_proxy:application/pdf;unified_id,6c0b5890-8914-48e0-b8f4-0ae5ed3c14a5;target_model_names,gpt-4o;llm_output_file_id,file-ECBPW7ML9g7XHdwGgUPZaM;llm_output_file_model_id,e26453f9e76e7993680d0068d98c1f4cc205bbad0967a33c664893568ca743c2 unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" - + # Test input with unified file ID in content array input_data = [ { @@ -745,15 +752,18 @@ def test_update_responses_input_with_unified_file_id(): ], } ] - + # Update the input updated_input = update_responses_input_with_model_file_ids(input=input_data) - + # Verify the file_id was updated to the provider-specific file ID assert updated_input[0]["content"][0]["type"] == "input_file" assert updated_input[0]["content"][0]["file_id"] == "file-ECBPW7ML9g7XHdwGgUPZaM" assert updated_input[0]["content"][1]["type"] == "input_text" - assert updated_input[0]["content"][1]["text"] == "What is the first dragon in the book?" + assert ( + updated_input[0]["content"][1]["text"] + == "What is the first dragon in the book?" + ) def test_update_responses_input_with_regular_file_id(): @@ -767,7 +777,7 @@ def test_update_responses_input_with_regular_file_id(): # Regular OpenAI file ID (not a unified file ID) regular_file_id = "file-abc123xyz" - + input_data = [ { "role": "user", @@ -783,10 +793,10 @@ def test_update_responses_input_with_regular_file_id(): ], } ] - + # Update the input updated_input = update_responses_input_with_model_file_ids(input=input_data) - + # Verify the file_id was kept unchanged (regular OpenAI file ID) assert updated_input[0]["content"][0]["type"] == "input_file" assert updated_input[0]["content"][0]["file_id"] == regular_file_id @@ -800,11 +810,11 @@ def test_update_responses_input_with_string_input(): from litellm.litellm_core_utils.prompt_templates.common_utils import ( update_responses_input_with_model_file_ids, ) - + input_data = "What is AI?" - + updated_input = update_responses_input_with_model_file_ids(input=input_data) - + assert updated_input == input_data assert isinstance(updated_input, str) @@ -822,7 +832,7 @@ def test_update_responses_input_with_multiple_file_ids(): unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" # Regular OpenAI file ID regular_file_id = "file-regular123" - + input_data = [ { "role": "user", @@ -842,9 +852,9 @@ def test_update_responses_input_with_multiple_file_ids(): ], } ] - + updated_input = update_responses_input_with_model_file_ids(input=input_data) - + # Verify unified file ID was updated assert updated_input[0]["content"][0]["file_id"] == "file-ECBPW7ML9g7XHdwGgUPZaM" # Verify regular file ID was kept unchanged @@ -864,7 +874,7 @@ def test_update_responses_input_with_model_file_id_mapping(): # Managed file ID (unified) managed_file_id = "litellm_proxy_file_123" - + # Model file ID mapping model_file_id_mapping = { managed_file_id: { @@ -872,7 +882,7 @@ def test_update_responses_input_with_model_file_id_mapping(): "model_id_2": "azure_file_xyz", } } - + input_data = [ { "role": "user", @@ -888,24 +898,24 @@ def test_update_responses_input_with_model_file_id_mapping(): ], } ] - + # Update input with model_id_1 mapping updated_input = update_responses_input_with_model_file_ids( input=input_data, model_id="model_id_1", model_file_id_mapping=model_file_id_mapping, ) - + # Verify the file_id was mapped to the correct provider-specific file ID assert updated_input[0]["content"][0]["file_id"] == "openai_file_abc" - + # Test with different model_id updated_input_2 = update_responses_input_with_model_file_ids( input=input_data, model_id="model_id_2", model_file_id_mapping=model_file_id_mapping, ) - + assert updated_input_2[0]["content"][0]["file_id"] == "azure_file_xyz" @@ -913,7 +923,7 @@ def test_update_responses_tools_with_model_file_id_mapping(): """ Test that update_responses_tools_with_model_file_ids correctly maps file IDs in code_interpreter tools with container.file_ids. - + This is a regression test for the issue where managed file IDs in tools.container.file_ids were not being replaced with provider-specific file IDs, causing "string too long" errors from OpenAI. @@ -925,7 +935,7 @@ def test_update_responses_tools_with_model_file_id_mapping(): # Managed file IDs managed_file_id_1 = "litellm_proxy_file_123" managed_file_id_2 = "litellm_proxy_file_456" - + # Model file ID mapping model_file_id_mapping = { managed_file_id_1: { @@ -935,7 +945,7 @@ def test_update_responses_tools_with_model_file_id_mapping(): "model_id_1": "openai_file_def", }, } - + tools = [ { "type": "code_interpreter", @@ -945,17 +955,20 @@ def test_update_responses_tools_with_model_file_id_mapping(): }, } ] - + # Update tools with model mapping updated_tools = update_responses_tools_with_model_file_ids( tools=tools, model_id="model_id_1", model_file_id_mapping=model_file_id_mapping, ) - + # Verify the file IDs were mapped to provider-specific file IDs assert updated_tools[0]["type"] == "code_interpreter" - assert updated_tools[0]["container"]["file_ids"] == ["openai_file_abc", "openai_file_def"] + assert updated_tools[0]["container"]["file_ids"] == [ + "openai_file_abc", + "openai_file_def", + ] def test_update_responses_tools_without_mapping(): @@ -968,7 +981,7 @@ def test_update_responses_tools_without_mapping(): ) regular_file_id = "file-abc123" - + tools = [ { "type": "code_interpreter", @@ -978,14 +991,14 @@ def test_update_responses_tools_without_mapping(): }, } ] - + # Update tools without mapping updated_tools = update_responses_tools_with_model_file_ids( tools=tools, model_id=None, model_file_id_mapping=None, ) - + # Verify the file ID was kept unchanged assert updated_tools[0]["container"]["file_ids"] == [regular_file_id] @@ -1001,13 +1014,13 @@ def test_update_responses_tools_with_mixed_file_ids(): managed_file_id = "litellm_proxy_file_123" regular_file_id = "file-abc123" - + model_file_id_mapping = { managed_file_id: { "model_id_1": "openai_file_abc", }, } - + tools = [ { "type": "code_interpreter", @@ -1017,16 +1030,19 @@ def test_update_responses_tools_with_mixed_file_ids(): }, } ] - + # Update tools updated_tools = update_responses_tools_with_model_file_ids( tools=tools, model_id="model_id_1", model_file_id_mapping=model_file_id_mapping, ) - + # Verify managed file ID was mapped and regular file ID was kept - assert updated_tools[0]["container"]["file_ids"] == ["openai_file_abc", regular_file_id] + assert updated_tools[0]["container"]["file_ids"] == [ + "openai_file_abc", + regular_file_id, + ] def test_get_file_ids_from_responses_tools(): @@ -1037,7 +1053,7 @@ def test_get_file_ids_from_responses_tools(): proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=MagicMock() ) - + tools = [ { "type": "code_interpreter", @@ -1047,9 +1063,9 @@ def test_get_file_ids_from_responses_tools(): }, } ] - + file_ids = proxy_managed_files.get_file_ids_from_responses_tools(tools) - + assert file_ids == ["file-123", "file-456"] @@ -1060,7 +1076,7 @@ def test_get_file_ids_from_responses_tools_multiple_tools(): proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=MagicMock() ) - + tools = [ { "type": "code_interpreter", @@ -1080,9 +1096,9 @@ def test_get_file_ids_from_responses_tools_multiple_tools(): }, }, ] - + file_ids = proxy_managed_files.get_file_ids_from_responses_tools(tools) - + # Should extract file IDs only from code_interpreter tools assert file_ids == ["file-123", "file-456", "file-789"] @@ -1094,15 +1110,15 @@ def test_get_file_ids_from_responses_tools_empty(): proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=MagicMock() ) - + # Test with None file_ids = proxy_managed_files.get_file_ids_from_responses_tools(None) assert file_ids == [] - + # Test with empty list file_ids = proxy_managed_files.get_file_ids_from_responses_tools([]) assert file_ids == [] - + # Test with tools without file_ids tools = [{"type": "file_search"}] file_ids = proxy_managed_files.get_file_ids_from_responses_tools(tools) @@ -1119,30 +1135,30 @@ async def test_check_file_ids_access_with_unified_file_ids(): # Create a unified file ID unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" regular_file_id = "file-abc123" - + # Mock the access check to return True prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock can_user_call_unified_file_id to return True proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=True) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + # Should not raise an exception for accessible files await proxy_managed_files.check_file_ids_access( [unified_file_id, regular_file_id], user_api_key_dict, ) - + # Verify can_user_call_unified_file_id was called for the unified file ID proxy_managed_files.can_user_call_unified_file_id.assert_called_once_with( unified_file_id, user_api_key_dict @@ -1155,32 +1171,32 @@ async def test_check_file_ids_access_denied(): Test that check_file_ids_access raises HTTPException when user doesn't have access. """ from litellm.proxy._types import UserAPIKeyAuth - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock can_user_call_unified_file_id to return False (access denied) proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=False) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + # Should raise HTTPException with 403 status code with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.check_file_ids_access( [unified_file_id], user_api_key_dict, ) - + assert exc_info.value.status_code == 403 assert "does not have access to the file" in exc_info.value.detail @@ -1191,32 +1207,32 @@ async def test_check_file_ids_access_with_regular_files_only(): Test that check_file_ids_access doesn't check access for regular (non-unified) file IDs. """ from litellm.proxy._types import UserAPIKeyAuth - + regular_file_id_1 = "file-abc123" regular_file_id_2 = "file-xyz789" - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock can_user_call_unified_file_id (should not be called for regular files) proxy_managed_files.can_user_call_unified_file_id = AsyncMock() - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + # Should not raise exception and should not call can_user_call_unified_file_id await proxy_managed_files.check_file_ids_access( [regular_file_id_1, regular_file_id_2], user_api_key_dict, ) - + # Verify can_user_call_unified_file_id was NOT called proxy_managed_files.can_user_call_unified_file_id.assert_not_called() @@ -1227,31 +1243,31 @@ async def test_completion_with_file_access_check(): Test that completion call type checks file access before processing. """ from litellm.proxy._types import UserAPIKeyAuth - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" - + prisma_client = AsyncMock() prisma_client.db.litellm_managedfiletable.find_first = AsyncMock(return_value=None) - + internal_usage_cache = MagicMock() internal_usage_cache.async_get_cache = AsyncMock(return_value=None) - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock the get_model_file_id_mapping to return empty dict proxy_managed_files.get_model_file_id_mapping = AsyncMock(return_value={}) - + # Mock access check to allow access proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=True) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + data = { "messages": [ { @@ -1267,7 +1283,7 @@ async def test_completion_with_file_access_check(): ], "model": "gpt-4", } - + # Should not raise exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=user_api_key_dict, @@ -1275,7 +1291,7 @@ async def test_completion_with_file_access_check(): data=data, call_type="acompletion", ) - + # Verify access check was called proxy_managed_files.can_user_call_unified_file_id.assert_called_once() @@ -1286,32 +1302,32 @@ async def test_responses_with_file_access_check(): Test that responses API checks file access for files in both input and tools. """ from litellm.proxy._types import UserAPIKeyAuth - + unified_file_id_1 = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" unified_file_id_2 = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsNzc3Nzc3Nzc7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1YWVo7bGxtX291dHB1dF9maWxlX21vZGVsX2lkLG1vZGVsXzEyMw" - + prisma_client = AsyncMock() prisma_client.db.litellm_managedfiletable.find_first = AsyncMock(return_value=None) - + internal_usage_cache = MagicMock() internal_usage_cache.async_get_cache = AsyncMock(return_value=None) - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock the get_model_file_id_mapping to return empty dict proxy_managed_files.get_model_file_id_mapping = AsyncMock(return_value={}) - + # Mock access check to allow access proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=True) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + data = { "input": [ { @@ -1333,7 +1349,7 @@ async def test_responses_with_file_access_check(): ], "model": "gpt-4", } - + # Should not raise exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=user_api_key_dict, @@ -1341,7 +1357,7 @@ async def test_responses_with_file_access_check(): data=data, call_type="aresponses", ) - + # Verify access check was called for both file IDs assert proxy_managed_files.can_user_call_unified_file_id.call_count == 2 @@ -1353,17 +1369,19 @@ async def test_store_unified_file_id_with_none_file_object(): (e.g., for batch output files that are stored before file metadata is available). """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - prisma_client.db.litellm_managedfiletable.create = AsyncMock(return_value=MagicMock()) + prisma_client.db.litellm_managedfiletable.create = AsyncMock( + return_value=MagicMock() + ) internal_usage_cache = MagicMock() internal_usage_cache.async_set_cache = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Store with file_object=None (simulating batch output file storage) await proxy_managed_files.store_unified_file_id( file_id="test-unified-file-id", @@ -1372,7 +1390,7 @@ async def test_store_unified_file_id_with_none_file_object(): model_mappings={"model-123": "file-provider-xyz"}, user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), ) - + # Verify DB create was called with expected data (without file_object) prisma_client.db.litellm_managedfiletable.create.assert_called_once() call_args = prisma_client.db.litellm_managedfiletable.create.call_args @@ -1387,34 +1405,38 @@ async def test_afile_delete_returns_provider_response_when_stored_file_object_no stored file_object is None (e.g., for batch output files). """ from litellm.types.llms.openai import OpenAIFileObject - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsdGVzdC1pZDt0YXJnZXRfbW9kZWxfbmFtZXMsZ3B0LTRvO2xsbV9vdXRwdXRfZmlsZV9pZCxmaWxlLXByb3ZpZGVyLXh5ejtsbG1fb3V0cHV0X2ZpbGVfbW9kZWxfaWQsbW9kZWwtMTIz" - + prisma_client = AsyncMock() db_record = MagicMock() db_record.model_mappings = '{"model-123": "file-provider-xyz"}' - prisma_client.db.litellm_managedfiletable.find_first = AsyncMock(return_value=db_record) + prisma_client.db.litellm_managedfiletable.find_first = AsyncMock( + return_value=db_record + ) prisma_client.db.litellm_managedfiletable.delete = AsyncMock() - + internal_usage_cache = MagicMock() - internal_usage_cache.async_get_cache = AsyncMock(return_value={ - "unified_file_id": unified_file_id, - "model_mappings": {"model-123": "file-provider-xyz"}, - "flat_model_file_ids": ["file-provider-xyz"], - "file_object": None, - "created_by": "test-user", - "updated_by": "test-user", - }) + internal_usage_cache.async_get_cache = AsyncMock( + return_value={ + "unified_file_id": unified_file_id, + "model_mappings": {"model-123": "file-provider-xyz"}, + "flat_model_file_ids": ["file-provider-xyz"], + "file_object": None, + "created_by": "test-user", + "updated_by": "test-user", + } + ) internal_usage_cache.async_set_cache = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock the delete_unified_file_id to return None (simulating file_object=None) proxy_managed_files.delete_unified_file_id = AsyncMock(return_value=None) - + # Mock router response provider_delete_response = OpenAIFileObject( id="file-provider-xyz", @@ -1424,16 +1446,16 @@ async def test_afile_delete_returns_provider_response_when_stored_file_object_no filename="test.jsonl", purpose="batch", ) - + mock_router = MagicMock() mock_router.afile_delete = AsyncMock(return_value=provider_delete_response) - + result = await proxy_managed_files.afile_delete( file_id=unified_file_id, litellm_parent_otel_span=None, llm_router=mock_router, ) - + # Should return the provider response with the unified file ID assert result is not None assert result.id == unified_file_id @@ -1446,21 +1468,21 @@ async def test_afile_retrieve_fetches_from_provider_when_file_object_none(): file_object is None (e.g., for batch output files). """ from litellm.types.llms.openai import OpenAIFileObject - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return a stored object with file_object=None stored_file = MagicMock() stored_file.file_object = None stored_file.model_mappings = {"model-123": "file-provider-xyz"} proxy_managed_files.get_unified_file_id = AsyncMock(return_value=stored_file) - + # Mock the router and provider response provider_file_response = OpenAIFileObject( id="file-provider-xyz", @@ -1470,23 +1492,25 @@ async def test_afile_retrieve_fetches_from_provider_when_file_object_none(): filename="output.jsonl", purpose="batch_output", ) - + mock_router = MagicMock() - mock_router.get_deployment_credentials_with_provider = MagicMock(return_value={ - "api_key": "test-key", - "api_base": "https://api.openai.com", - }) - + mock_router.get_deployment_credentials_with_provider = MagicMock( + return_value={ + "api_key": "test-key", + "api_base": "https://api.openai.com", + } + ) + with patch("litellm.afile_retrieve", new_callable=AsyncMock) as mock_afile_retrieve: mock_afile_retrieve.return_value = provider_file_response - + unified_file_id = "test-unified-file-id" result = await proxy_managed_files.afile_retrieve( file_id=unified_file_id, litellm_parent_otel_span=None, llm_router=mock_router, ) - + # Should return the provider response with the unified file ID assert result is not None assert result.id == unified_file_id @@ -1501,27 +1525,27 @@ async def test_afile_retrieve_raises_error_when_no_router_and_file_object_none() """ prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return a stored object with file_object=None stored_file = MagicMock() stored_file.file_object = None stored_file.model_mappings = {"model-123": "file-provider-xyz"} proxy_managed_files.get_unified_file_id = AsyncMock(return_value=stored_file) - + unified_file_id = "test-unified-file-id" - + with pytest.raises(Exception) as exc_info: await proxy_managed_files.afile_retrieve( file_id=unified_file_id, litellm_parent_otel_span=None, llm_router=None, ) - + assert "llm_router is required" in str(exc_info.value) @@ -1532,15 +1556,15 @@ async def test_afile_retrieve_returns_stored_file_object_when_exists(): (the normal case for user-uploaded files). """ from litellm.types.llms.openai import OpenAIFileObject - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return a stored object WITH file_object stored_file_object = OpenAIFileObject( id="test-unified-file-id", @@ -1553,13 +1577,13 @@ async def test_afile_retrieve_returns_stored_file_object_when_exists(): stored_file = MagicMock() stored_file.file_object = stored_file_object proxy_managed_files.get_unified_file_id = AsyncMock(return_value=stored_file) - + result = await proxy_managed_files.afile_retrieve( file_id="test-unified-file-id", litellm_parent_otel_span=None, llm_router=None, ) - + # Should return the stored file object directly assert result == stored_file_object @@ -1572,21 +1596,21 @@ async def test_afile_retrieve_raises_error_for_non_managed_file(): """ prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return None (file not found) proxy_managed_files.get_unified_file_id = AsyncMock(return_value=None) - + with pytest.raises(Exception) as exc_info: await proxy_managed_files.afile_retrieve( file_id="non-existent-file-id", litellm_parent_otel_span=None, ) - + assert "not found" in str(exc_info.value) @@ -1597,54 +1621,58 @@ async def test_list_batches_from_managed_objects_table(): from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + batch_record_1 = MagicMock() batch_record_1.unified_object_id = "unified-batch-id-1" - batch_record_1.file_object = json.dumps({ - "id": "batch_abc123", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567890, - "input_file_id": "file-input-1", - "request_counts": {"total": 1, "completed": 1, "failed": 0}, - }) - + batch_record_1.file_object = json.dumps( + { + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567890, + "input_file_id": "file-input-1", + "request_counts": {"total": 1, "completed": 1, "failed": 0}, + } + ) + batch_record_2 = MagicMock() batch_record_2.unified_object_id = "unified-batch-id-2" - batch_record_2.file_object = json.dumps({ - "id": "batch_xyz789", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "in_progress", - "created_at": 1234567891, - "input_file_id": "file-input-2", - "request_counts": {"total": 5, "completed": 2, "failed": 0}, - }) - + batch_record_2.file_object = json.dumps( + { + "id": "batch_xyz789", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "in_progress", + "created_at": 1234567891, + "input_file_id": "file-input-2", + "request_counts": {"total": 5, "completed": 2, "failed": 0}, + } + ) + prisma_client.db.litellm_managedobjecttable.find_many.return_value = [ batch_record_1, batch_record_2, ] - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + result = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), limit=10, ) - + assert result["object"] == "list" assert len(result["data"]) == 2 assert result["data"][0].id == "unified-batch-id-1" assert result["data"][1].id == "unified-batch-id-2" assert result["first_id"] == "unified-batch-id-1" assert result["last_id"] == "unified-batch-id-2" - + # Should filter by user_id (created_by) prisma_client.db.litellm_managedobjecttable.find_many.assert_called_once_with( where={"file_purpose": "batch", "created_by": "test-user"}, @@ -1659,21 +1687,21 @@ async def test_list_batches_from_managed_objects_table_empty_list(): prisma_client = AsyncMock() prisma_client.db.litellm_managedobjecttable.find_many.return_value = [] - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + result = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), ) - + assert result["object"] == "list" assert len(result["data"]) == 0 assert result["first_id"] is None assert result["last_id"] is None assert result["has_more"] is False - + # Verify where clause includes created_by filter # Default take is 20 when no limit is provided prisma_client.db.litellm_managedobjecttable.find_many.assert_called_once_with( @@ -1685,6 +1713,7 @@ async def test_list_batches_from_managed_objects_table_empty_list(): def _create_unified_batch_id(model_id: str, batch_id: str) -> str: import base64 + unified_str = f"litellm_proxy;model_id:{model_id};llm_batch_id:{batch_id}" return base64.urlsafe_b64encode(unified_str.encode()).decode().rstrip("=") @@ -1694,11 +1723,11 @@ async def test_list_batches_from_managed_objects_table_provider_filter_raises_ex from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # Filtering by provider should raise Exception with pytest.raises(Exception) as exc_info: await proxy_managed_files.list_user_batches( @@ -1706,11 +1735,11 @@ async def test_list_batches_from_managed_objects_table_provider_filter_raises_ex limit=10, provider="openai", ) - + assert str(exc_info.value) == ( "Filtering by 'provider' is not supported when using managed batches." ) - + # Verify find_many was NOT called since exception is raised before database query prisma_client.db.litellm_managedobjecttable.find_many.assert_not_called() @@ -1720,7 +1749,7 @@ async def test_list_batches_from_managed_objects_table_target_model_name_filter_ from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) @@ -1732,59 +1761,64 @@ async def test_list_batches_from_managed_objects_table_target_model_name_filter_ limit=10, target_model_names="gpt-4o,gpt-3.5", ) - + assert str(exc_info.value) == ( "Filtering by 'target_model_names' is not supported when using managed batches." ) - + # Verify find_many was NOT called since exception is raised before database query prisma_client.db.litellm_managedobjecttable.find_many.assert_not_called() + @pytest.mark.asyncio async def test_list_batches_from_managed_objects_table_filters_by_created_by(): from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + # Create batch for user1 batch_user1 = MagicMock() batch_user1.unified_object_id = "unified-batch-user1" - batch_user1.file_object = json.dumps({ - "id": "batch_user1_abc", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567890, - "input_file_id": "file-input-user1", - "request_counts": {"total": 1, "completed": 1, "failed": 0}, - }) - + batch_user1.file_object = json.dumps( + { + "id": "batch_user1_abc", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567890, + "input_file_id": "file-input-user1", + "request_counts": {"total": 1, "completed": 1, "failed": 0}, + } + ) + # Create batch for user2 batch_user2 = MagicMock() batch_user2.unified_object_id = "unified-batch-user2" - batch_user2.file_object = json.dumps({ - "id": "batch_user2_xyz", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567891, - "input_file_id": "file-input-user2", - "request_counts": {"total": 2, "completed": 2, "failed": 0}, - }) - + batch_user2.file_object = json.dumps( + { + "id": "batch_user2_xyz", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567891, + "input_file_id": "file-input-user2", + "request_counts": {"total": 2, "completed": 2, "failed": 0}, + } + ) + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # Query with user1's API key - should only return user1's batch prisma_client.db.litellm_managedobjecttable.find_many.return_value = [batch_user1] result_user1 = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="user1"), limit=10, ) - + assert len(result_user1["data"]) == 1 assert result_user1["data"][0].id == "unified-batch-user1" prisma_client.db.litellm_managedobjecttable.find_many.assert_called_with( @@ -1792,14 +1826,14 @@ async def test_list_batches_from_managed_objects_table_filters_by_created_by(): take=10, order={"created_at": "desc"}, ) - + # Query with user2's API key - should only return user2's batch prisma_client.db.litellm_managedobjecttable.find_many.return_value = [batch_user2] result_user2 = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="user2"), limit=10, ) - + assert len(result_user2["data"]) == 1 assert result_user2["data"][0].id == "unified-batch-user2" prisma_client.db.litellm_managedobjecttable.find_many.assert_called_with( @@ -1822,7 +1856,7 @@ async def test_return_unified_file_id_includes_expires_at(): filename="test.jsonl", purpose="batch", status="uploaded", - expires_at=1234657890, + expires_at=1234657890, ) file_object._hidden_params = {"model_id": "test-model-id"} @@ -1862,25 +1896,27 @@ async def test_return_unified_file_id_includes_expires_at(): async def test_user_b_cannot_retrieve_user_a_batch(): """ Test that User B cannot retrieve a batch created by User A. - + This verifies batch isolation between users at the database/hook level. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User B tries to retrieve User A's batch - unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - + unified_batch_id = ( + "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + ) + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -1890,7 +1926,7 @@ async def test_user_b_cannot_retrieve_user_a_batch(): data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -1901,21 +1937,23 @@ async def test_user_b_cannot_cancel_user_a_batch(): Test that User B cannot cancel a batch created by User A. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User B tries to cancel User A's batch - unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - + unified_batch_id = ( + "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + ) + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -1925,7 +1963,7 @@ async def test_user_b_cannot_cancel_user_a_batch(): data={"batch_id": unified_batch_id}, call_type="acancel_batch", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -1934,26 +1972,28 @@ async def test_user_b_cannot_cancel_user_a_batch(): async def test_user_a_can_retrieve_own_batch(): """ Test that User A can successfully retrieve their own batch. - + This is a positive test case to ensure permission checks don't block legitimate access. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User A retrieves their own batch - unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - + unified_batch_id = ( + "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + ) + # Should not raise an exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -1963,7 +2003,7 @@ async def test_user_a_can_retrieve_own_batch(): data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + # Should successfully return the decoded batch_id assert "batch_id" in result assert result["model"] == "my-model" @@ -1975,21 +2015,23 @@ async def test_user_b_cannot_retrieve_user_a_file(): Test that User B cannot retrieve a file created by User A. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User B tries to retrieve User A's file - unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - + unified_file_id = ( + "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + ) + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -1999,7 +2041,7 @@ async def test_user_b_cannot_retrieve_user_a_file(): data={"file_id": unified_file_id}, call_type="afile_retrieve", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -2010,21 +2052,23 @@ async def test_user_b_cannot_download_user_a_file_content(): Test that User B cannot download file content for User A's file. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User B tries to download User A's file content - unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - + unified_file_id = ( + "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + ) + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2034,7 +2078,7 @@ async def test_user_b_cannot_download_user_a_file_content(): data={"file_id": unified_file_id}, call_type="afile_content", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -2045,21 +2089,23 @@ async def test_user_b_cannot_delete_user_a_file(): Test that User B cannot delete a file created by User A. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User B tries to delete User A's file - unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - + unified_file_id = ( + "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + ) + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2069,7 +2115,7 @@ async def test_user_b_cannot_delete_user_a_file(): data={"file_id": unified_file_id}, call_type="afile_delete", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -2078,34 +2124,38 @@ async def test_user_b_cannot_delete_user_a_file(): async def test_user_a_can_retrieve_own_file(): """ Test that User A can successfully retrieve their own file. - + Positive test case to ensure permission checks work correctly for the owner. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" file_record.model_mappings = '{"model-123": "file-abc123"}' - file_record.file_object = json.dumps({ - "id": "file-abc123", - "object": "file", - "bytes": 1234, - "created_at": 1234567890, - "filename": "test.jsonl", - "purpose": "batch", - }) + file_record.file_object = json.dumps( + { + "id": "file-abc123", + "object": "file", + "bytes": 1234, + "created_at": 1234567890, + "filename": "test.jsonl", + "purpose": "batch", + } + ) prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User A retrieves their own file - unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - + unified_file_id = ( + "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + ) + # Should not raise an exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2115,7 +2165,7 @@ async def test_user_a_can_retrieve_own_file(): data={"file_id": unified_file_id}, call_type="afile_retrieve", ) - + # Should successfully return the decoded file_id assert "file_id" in result @@ -2124,44 +2174,46 @@ async def test_user_a_can_retrieve_own_file(): async def test_list_batches_only_returns_user_own_batches(): """ Test that list_user_batches only returns batches created by the requesting user. - + This ensures users cannot see other users' batches in list operations. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Create batches for User A batch_user_a = MagicMock() batch_user_a.unified_object_id = "batch-user-a" - batch_user_a.file_object = json.dumps({ - "id": "batch_a", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567890, - "input_file_id": "file-a", - "request_counts": {"total": 1, "completed": 1, "failed": 0}, - }) - + batch_user_a.file_object = json.dumps( + { + "id": "batch_a", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567890, + "input_file_id": "file-a", + "request_counts": {"total": 1, "completed": 1, "failed": 0}, + } + ) + # Mock database to only return User A's batches prisma_client.db.litellm_managedobjecttable.find_many.return_value = [batch_user_a] - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User A requests their batches result = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="user_a_id"), limit=10, ) - + # Should only return User A's batches assert len(result["data"]) == 1 assert result["data"][0].id == "batch-user-a" - + # Verify the database query filtered by user_id prisma_client.db.litellm_managedobjecttable.find_many.assert_called_once_with( where={"file_purpose": "batch", "created_by": "user_a_id"}, @@ -2174,51 +2226,49 @@ async def test_list_batches_only_returns_user_own_batches(): async def test_same_user_different_keys_can_access_batch(): """ Test that different API keys for the same user can access the same batch. - + This verifies that permission checks are based on user_id, not API key, allowing users to have multiple keys that can all access their resources. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return the user_id as creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - - unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - + + unified_batch_id = ( + "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + ) + # First API key for User A retrieves the batch result1 = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( - user_id="user_a_id", - api_key="key-1", - parent_otel_span=MagicMock() + user_id="user_a_id", api_key="key-1", parent_otel_span=MagicMock() ), cache=MagicMock(), data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + assert "batch_id" in result1 - + # Second API key for the same User A retrieves the batch result2 = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( - user_id="user_a_id", - api_key="key-2", - parent_otel_span=MagicMock() + user_id="user_a_id", api_key="key-2", parent_otel_span=MagicMock() ), cache=MagicMock(), data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + assert "batch_id" in result2 # Both keys should get the same result assert result1["batch_id"] == result2["batch_id"] diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py index 69c3b4cb59a..699ab8d1c4c 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py @@ -31,12 +31,16 @@ class TestAvailableEnterpriseUsers: self, client, mock_user_api_key_auth ): """Test when max_users is set and user count is within limit""" - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), patch( - "litellm.proxy.proxy_server.premium_user_data", - {"max_users": 10}, + with ( + patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, + patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), + patch( + "litellm.proxy.proxy_server.premium_user_data", + {"max_users": 10}, + ), ): # Mock database count mock_prisma.db.litellm_usertable.count = AsyncMock(return_value=5) @@ -66,12 +70,16 @@ class TestAvailableEnterpriseUsers: self, client, mock_user_api_key_auth ): """Test when max_users is not set (premium_user_data is None or doesn't contain max_users)""" - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), patch( - "litellm.proxy.proxy_server.premium_user_data", - None, + with ( + patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, + patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), + patch( + "litellm.proxy.proxy_server.premium_user_data", + None, + ), ): # Mock database count mock_prisma.db.litellm_usertable.count = AsyncMock(return_value=3) @@ -99,12 +107,16 @@ class TestAvailableEnterpriseUsers: self, client, mock_user_api_key_auth ): """Test the current bug where total_users_remaining can be negative""" - with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), patch( - "litellm.proxy.proxy_server.premium_user_data", - {"key": "value"}, + with ( + patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, + patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), + patch( + "litellm.proxy.proxy_server.premium_user_data", + {"key": "value"}, + ), ): # Mock database count higher than max_users to trigger the bug mock_prisma.db.litellm_usertable.count = AsyncMock(return_value=8) @@ -140,12 +152,15 @@ class TestAvailableEnterpriseUsers: """Test when prisma_client is None (no database connection)""" from litellm.proxy._types import CommonProxyErrors - with patch( - "litellm.proxy.proxy_server.prisma_client", - None, - ), patch( - "litellm.proxy.proxy_server.premium_user", - True, + with ( + patch( + "litellm.proxy.proxy_server.prisma_client", + None, + ), + patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), ): # Override the dependency client.app.dependency_overrides[mock_user_api_key_auth] = lambda: { diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index 52cb94ff346..8e0a3655b57 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -801,7 +801,9 @@ async def test_list_projects_returns_timestamps(): from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch - from litellm_enterprise.proxy.management_endpoints.project_endpoints import list_projects + from litellm_enterprise.proxy.management_endpoints.project_endpoints import ( + list_projects, + ) from litellm.proxy._types import LiteLLM_ProjectTable now = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc) diff --git a/tests/guardrails_tests/test_akto_guardrails.py b/tests/guardrails_tests/test_akto_guardrails.py index 83421f13136..488899d8317 100644 --- a/tests/guardrails_tests/test_akto_guardrails.py +++ b/tests/guardrails_tests/test_akto_guardrails.py @@ -14,7 +14,6 @@ from litellm.proxy.guardrails.guardrail_registry import ( ) from litellm.proxy.guardrails.guardrail_hooks.akto.akto import AktoGuardrail - # --------------------------------------------------------------------------- # Registry tests # --------------------------------------------------------------------------- diff --git a/tests/guardrails_tests/test_custom_guardrail.py b/tests/guardrails_tests/test_custom_guardrail.py index af1270756f2..95ac82b8d0f 100644 --- a/tests/guardrails_tests/test_custom_guardrail.py +++ b/tests/guardrails_tests/test_custom_guardrail.py @@ -6,7 +6,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/guardrails_tests/test_eu_ai_act_article5.py b/tests/guardrails_tests/test_eu_ai_act_article5.py index bda7bf6f517..d602b206b53 100644 --- a/tests/guardrails_tests/test_eu_ai_act_article5.py +++ b/tests/guardrails_tests/test_eu_ai_act_article5.py @@ -21,7 +21,6 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor ContentFilterCategoryConfig, ) - # Test cases: (sentence, expected_result, reason) TEST_CASES = [ # ALWAYS BLOCK - Explicit prohibited practices (1-10) diff --git a/tests/guardrails_tests/test_sg_mas_ai_guardrails.py b/tests/guardrails_tests/test_sg_mas_ai_guardrails.py index 668ee704692..47bfd1b8a10 100644 --- a/tests/guardrails_tests/test_sg_mas_ai_guardrails.py +++ b/tests/guardrails_tests/test_sg_mas_ai_guardrails.py @@ -23,7 +23,6 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor ContentFilterCategoryConfig, ) - # ── helpers ────────────────────────────────────────────────────────────── POLICY_DIR = os.path.abspath( diff --git a/tests/guardrails_tests/test_sg_pdpa_guardrails.py b/tests/guardrails_tests/test_sg_pdpa_guardrails.py index fd7133bc745..03f55777f21 100644 --- a/tests/guardrails_tests/test_sg_pdpa_guardrails.py +++ b/tests/guardrails_tests/test_sg_pdpa_guardrails.py @@ -28,7 +28,6 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor ContentFilterCategoryConfig, ) - # ── helpers ────────────────────────────────────────────────────────────── POLICY_DIR = os.path.abspath( diff --git a/tests/image_gen_tests/test_image_generation.py b/tests/image_gen_tests/test_image_generation.py index 5152e3e0129..bd39206fd02 100644 --- a/tests/image_gen_tests/test_image_generation.py +++ b/tests/image_gen_tests/test_image_generation.py @@ -7,7 +7,6 @@ import sys import traceback from unittest.mock import AsyncMock, MagicMock, patch - sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path diff --git a/tests/image_gen_tests/test_image_variation.py b/tests/image_gen_tests/test_image_variation.py index 301835057a7..ef528c391e1 100644 --- a/tests/image_gen_tests/test_image_variation.py +++ b/tests/image_gen_tests/test_image_variation.py @@ -6,7 +6,6 @@ import os import sys import traceback - sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path diff --git a/tests/litellm/llms/bedrock/embed/test_embedding.py b/tests/litellm/llms/bedrock/embed/test_embedding.py index 261448842f4..163c143d825 100644 --- a/tests/litellm/llms/bedrock/embed/test_embedding.py +++ b/tests/litellm/llms/bedrock/embed/test_embedding.py @@ -11,7 +11,6 @@ import pytest from litellm.types.utils import Embedding from litellm.main import bedrock_embedding, embedding, EmbeddingResponse, Usage - _mock_model_id = ( "arn:aws:bedrock:us-east-1:123412341234:application-inference-profile/abc123123" ) diff --git a/tests/litellm/llms/bedrock/test_nova_imported_models.py b/tests/litellm/llms/bedrock/test_nova_imported_models.py index e3677aaf9e6..b5fd2630cbe 100644 --- a/tests/litellm/llms/bedrock/test_nova_imported_models.py +++ b/tests/litellm/llms/bedrock/test_nova_imported_models.py @@ -11,7 +11,6 @@ from litellm.llms.bedrock.common_utils import ( ) from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig - NOVA_ARN = "arn:aws:bedrock:us-east-1:123456789012:custom-model-deployment/a1b2c3d4e5f6" NOVA_MODEL = f"bedrock/nova/{NOVA_ARN}" NOVA2_MODEL = f"bedrock/nova-2/{NOVA_ARN}" diff --git a/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py b/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py index 54ea41a6450..fde468618ea 100644 --- a/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py +++ b/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py @@ -581,17 +581,21 @@ def test_vertex_ai_text_only_embedding_uses_embed_content(): def test_filter_embed_params_drops_unsupported(): """Unsupported params like max_tokens should be filtered out.""" - result = _filter_embed_params({"dimensions": 768, "max_tokens": 256, "temperature": 0.5}) + result = _filter_embed_params( + {"dimensions": 768, "max_tokens": 256, "temperature": 0.5} + ) assert result == {"outputDimensionality": 768} def test_filter_embed_params_keeps_supported(): """All supported Gemini embedding params should pass through.""" - result = _filter_embed_params({ - "dimensions": 768, - "task_type": "RETRIEVAL_DOCUMENT", - "title": "My doc", - }) + result = _filter_embed_params( + { + "dimensions": 768, + "task_type": "RETRIEVAL_DOCUMENT", + "title": "My doc", + } + ) assert result == { "outputDimensionality": 768, "taskType": "RETRIEVAL_DOCUMENT", diff --git a/tests/litellm/test_bedrock_nemotron_super.py b/tests/litellm/test_bedrock_nemotron_super.py index 8b081f10d1d..7b4efd86b28 100644 --- a/tests/litellm/test_bedrock_nemotron_super.py +++ b/tests/litellm/test_bedrock_nemotron_super.py @@ -11,7 +11,6 @@ import pytest from litellm import get_model_info - MODEL_NAME = "nvidia.nemotron-super-3-120b" diff --git a/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py b/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py index c32917efe87..87af6a35f4b 100644 --- a/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py +++ b/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py @@ -12,7 +12,6 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( BedrockConverseMessagesProcessor, ) - MODEL = "anthropic.claude-v2" PROVIDER = "bedrock_converse" @@ -546,9 +545,9 @@ def test_bedrock_converse_sorts_text_before_tooluse_sync(): tool_indices = [i for i, b in enumerate(content) if "toolUse" in b] # All text blocks must come before all toolUse blocks - assert max(text_indices) < min(tool_indices), ( - f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" - ) + assert max(text_indices) < min( + tool_indices + ), f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" @pytest.mark.asyncio @@ -567,9 +566,9 @@ async def test_bedrock_converse_sorts_text_before_tooluse_async(): text_indices = [i for i, b in enumerate(content) if "text" in b] tool_indices = [i for i, b in enumerate(content) if "toolUse" in b] - assert max(text_indices) < min(tool_indices), ( - f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" - ) + assert max(text_indices) < min( + tool_indices + ), f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" @pytest.mark.asyncio @@ -577,7 +576,9 @@ async def test_bedrock_converse_content_ordering_sync_async_parity(): """Sync and async paths should produce identical content block ordering.""" messages = _make_tooluse_before_text_messages() sync_result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER) - async_result = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async( - messages, MODEL, PROVIDER + async_result = ( + await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async( + messages, MODEL, PROVIDER + ) ) assert sync_result == async_result diff --git a/tests/litellm_utils_tests/test_aws_secret_manager.py b/tests/litellm_utils_tests/test_aws_secret_manager.py index 674f9b3ca82..00417ea1b2e 100644 --- a/tests/litellm_utils_tests/test_aws_secret_manager.py +++ b/tests/litellm_utils_tests/test_aws_secret_manager.py @@ -10,7 +10,6 @@ from dotenv import load_dotenv import litellm.types import litellm.types.utils - load_dotenv() import io diff --git a/tests/litellm_utils_tests/test_litellm_overhead.py b/tests/litellm_utils_tests/test_litellm_overhead.py index 3a428e9d588..d5aa7eb6713 100644 --- a/tests/litellm_utils_tests/test_litellm_overhead.py +++ b/tests/litellm_utils_tests/test_litellm_overhead.py @@ -14,7 +14,6 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm - # Fake Vertex AI Gemini response for mocking FAKE_VERTEX_GEMINI_RESPONSE = { "candidates": [ diff --git a/tests/llm_responses_api_testing/conftest.py b/tests/llm_responses_api_testing/conftest.py index 80f36e159a9..1eab15a0b88 100644 --- a/tests/llm_responses_api_testing/conftest.py +++ b/tests/llm_responses_api_testing/conftest.py @@ -22,7 +22,6 @@ from tests._vcr_redis_persister import ( # noqa: E402 vcr_verbose_enabled, ) - _controller_pluginmanager = None _controller_terminal_reporter = None diff --git a/tests/llm_responses_api_testing/test_anthropic_responses_api.py b/tests/llm_responses_api_testing/test_anthropic_responses_api.py index 6537f67acb9..746d54b4d49 100644 --- a/tests/llm_responses_api_testing/test_anthropic_responses_api.py +++ b/tests/llm_responses_api_testing/test_anthropic_responses_api.py @@ -12,7 +12,6 @@ from litellm.responses.litellm_completion_transformation.transformation import ( ) from litellm.types.utils import ModelResponse - sys.path.insert(0, os.path.abspath("../..")) import litellm from litellm.integrations.custom_logger import CustomLogger diff --git a/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py b/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py index 08b1c1784e7..3d256794cb8 100644 --- a/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py +++ b/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py @@ -2,7 +2,7 @@ Test to reproduce and verify fix for Anthropic tool_result issue with empty call_id. This test reproduces the exact error: -"messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks: tool_use_id. +"messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks: tool_use_id. Each `tool_result` block must have a corresponding `tool_use` block in the previous message." The issue occurs when: diff --git a/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py b/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py index 8f5278698ba..8ce234dbcbb 100644 --- a/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py +++ b/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py @@ -2,12 +2,12 @@ Unit tests for BaseResponsesAPIStreamingIterator Tests core functionality including: -1. Processing chunks and handling ResponseCompletedEvent +1. Processing chunks and handling ResponseCompletedEvent 2. Ensuring _update_responses_api_response_id_with_model_id is called for final chunk 3. Verifying ID update is NOT called for non-final chunks (delta events) 4. Edge case handling for invalid JSON, empty chunks, and [DONE] markers -These tests ensure the streaming iterator correctly processes response chunks +These tests ensure the streaming iterator correctly processes response chunks and applies model ID updates only to completed responses, as required for proper response tracking and logging. """ diff --git a/tests/llm_responses_api_testing/test_responses_hooks.py b/tests/llm_responses_api_testing/test_responses_hooks.py index 3799a0b9121..09ac9c7a9e3 100644 --- a/tests/llm_responses_api_testing/test_responses_hooks.py +++ b/tests/llm_responses_api_testing/test_responses_hooks.py @@ -387,9 +387,7 @@ def test_process_chunk_completed_response_updates_id_and_usage_cost(monkeypatch) # Chunk must include a top-level "response" key so BaseResponsesAPIStreamingIterator # runs _update_responses_api_response_id_with_model_id (see streaming_iterator.py). event = iterator._process_chunk( - json.dumps( - {"type": "response.completed", "response": {"id": "resp_live"}} - ) + json.dumps({"type": "response.completed", "response": {"id": "resp_live"}}) ) finally: litellm.include_cost_in_streaming_usage = original_include_cost diff --git a/tests/llm_translation/conftest.py b/tests/llm_translation/conftest.py index 17ffb800ea0..ddb5c570dd2 100644 --- a/tests/llm_translation/conftest.py +++ b/tests/llm_translation/conftest.py @@ -27,7 +27,6 @@ from tests._vcr_redis_persister import ( # noqa: E402 vcr_verbose_enabled, ) - _controller_pluginmanager = None _controller_terminal_reporter = None diff --git a/tests/llm_translation/test_bedrock_anthropic_regression.py b/tests/llm_translation/test_bedrock_anthropic_regression.py index 8b8ce0a6cc8..d81e76cd4b7 100644 --- a/tests/llm_translation/test_bedrock_anthropic_regression.py +++ b/tests/llm_translation/test_bedrock_anthropic_regression.py @@ -22,10 +22,8 @@ sys.path.insert(0, os.path.abspath("../..")) import litellm from litellm import completion - # Large document for caching tests (needs 1024+ tokens for Claude models) -LARGE_DOCUMENT_FOR_CACHING = ( - """ +LARGE_DOCUMENT_FOR_CACHING = """ This is a comprehensive legal agreement between Party A and Party B. ARTICLE 1: DEFINITIONS @@ -77,9 +75,7 @@ ARTICLE 9: GENERAL PROVISIONS 9.5 Waiver of any provision shall not constitute ongoing waiver. IN WITNESS WHEREOF, the parties have executed this Agreement. -""" - * 8 -) # Repeat to ensure we have enough tokens (need 1024+ for Claude models) +""" * 8 # Repeat to ensure we have enough tokens (need 1024+ for Claude models) class TestBedrockAnthropicPromptCachingRegression: diff --git a/tests/llm_translation/test_bedrock_invoke_tests.py b/tests/llm_translation/test_bedrock_invoke_tests.py index 23f436d5b28..0258ab15c6e 100644 --- a/tests/llm_translation/test_bedrock_invoke_tests.py +++ b/tests/llm_translation/test_bedrock_invoke_tests.py @@ -3,7 +3,6 @@ import pytest import sys import os - sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path diff --git a/tests/llm_translation/test_crusoe.py b/tests/llm_translation/test_crusoe.py index 56aa4e4cd42..158152d8aeb 100644 --- a/tests/llm_translation/test_crusoe.py +++ b/tests/llm_translation/test_crusoe.py @@ -1,6 +1,7 @@ """ Tests for Crusoe provider integration """ + import os from unittest import mock @@ -96,9 +97,9 @@ def test_crusoe_models_configuration(): for model in crusoe_models: model_info = get_model_info(model) assert model_info is not None, f"Model info not found for {model}" - assert model_info.get("litellm_provider") == "crusoe", ( - f"{model} should have crusoe as provider" - ) + assert ( + model_info.get("litellm_provider") == "crusoe" + ), f"{model} should have crusoe as provider" assert model_info.get("mode") == "chat", f"{model} should be in chat mode" finally: litellm.model_cost = original_model_cost diff --git a/tests/llm_translation/test_gemini.py b/tests/llm_translation/test_gemini.py index 97b0aaee86b..3e322e1a6b4 100644 --- a/tests/llm_translation/test_gemini.py +++ b/tests/llm_translation/test_gemini.py @@ -1362,8 +1362,12 @@ def test_anthropic_thinking_param_to_gemini_3_provider_defaults(): ) # For Gemini 3, should not force thinkingLevel by default - assert "thinkingLevel" not in result, "Should not force thinkingLevel for Gemini 3" - assert "thinkingBudget" not in result, "Should NOT have thinkingBudget for Gemini 3" + assert ( + "thinkingLevel" not in result + ), "Should not force thinkingLevel for Gemini 3" + assert ( + "thinkingBudget" not in result + ), "Should NOT have thinkingBudget for Gemini 3" assert result["includeThoughts"] is True # Test 2: Anthropic thinking disabled for Gemini 3 @@ -1395,7 +1399,10 @@ def test_anthropic_thinking_param_to_gemini_3_provider_defaults(): ) assert result_zero["includeThoughts"] is False - assert "thinkingLevel" not in result_zero or result_zero.get("thinkingLevel") is None + assert ( + "thinkingLevel" not in result_zero + or result_zero.get("thinkingLevel") is None + ) # Test 4: Gemini 3 flash-preview should also follow provider defaults by default result_gemini3flashpreview = VertexGeminiConfig._map_thinking_param( @@ -1525,8 +1532,12 @@ def test_anthropic_thinking_param_via_map_openai_params(): # Check that thinkingConfig was created without forced thinkingLevel assert "thinkingConfig" in result, "Should have thinkingConfig in optional_params" thinking_config = result["thinkingConfig"] - assert "thinkingLevel" not in thinking_config, "Should not force thinkingLevel for Gemini 3 by default" - assert "thinkingBudget" not in thinking_config, "Should NOT have thinkingBudget for Gemini 3" + assert ( + "thinkingLevel" not in thinking_config + ), "Should not force thinkingLevel for Gemini 3 by default" + assert ( + "thinkingBudget" not in thinking_config + ), "Should NOT have thinkingBudget for Gemini 3" assert thinking_config["includeThoughts"] is True # Test with Gemini 2 model diff --git a/tests/llm_translation/test_gemini_image_usage.py b/tests/llm_translation/test_gemini_image_usage.py index 096f9c4796c..75ce8c81469 100644 --- a/tests/llm_translation/test_gemini_image_usage.py +++ b/tests/llm_translation/test_gemini_image_usage.py @@ -1,7 +1,7 @@ """ Test for Gemini image generation usage metadata extraction. -This test verifies the fix for issue #18323 where image_generation() +This test verifies the fix for issue #18323 where image_generation() was returning usage=0 while completion() returned proper token usage. """ diff --git a/tests/local_testing/create_mock_standard_logging_payload.py b/tests/local_testing/create_mock_standard_logging_payload.py index 2fd6a4ffa8a..828faba0b04 100644 --- a/tests/local_testing/create_mock_standard_logging_payload.py +++ b/tests/local_testing/create_mock_standard_logging_payload.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 9782bf3c2af..8ca13369ff1 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -38,7 +38,6 @@ from litellm.llms.vertex_ai.gemini.transformation import ( ) from litellm.llms.vertex_ai.vertex_llm_base import VertexBase - litellm.num_retries = 3 litellm.cache = None user_message = "Write a short poem about the sky" @@ -1104,9 +1103,7 @@ def vertex_httpx_mock_post_valid_response(*args, **kwargs): { "content": { "role": "model", - "parts": [ - { - "text": """{ + "parts": [{"text": """{ "recipes": [ {"recipe_name": "Chocolate Chip Cookies"}, {"recipe_name": "Oatmeal Raisin Cookies"}, @@ -1114,9 +1111,7 @@ def vertex_httpx_mock_post_valid_response(*args, **kwargs): {"recipe_name": "Sugar Cookies"}, {"recipe_name": "Snickerdoodles"} ] - }""" - } - ], + }"""}], }, "finishReason": "STOP", "safetyRatings": [ diff --git a/tests/local_testing/test_cache_preset_key.py b/tests/local_testing/test_cache_preset_key.py index d6518c5a073..cf496fe4b86 100644 --- a/tests/local_testing/test_cache_preset_key.py +++ b/tests/local_testing/test_cache_preset_key.py @@ -4,7 +4,7 @@ Test for preset_cache_key multiple values bug fix. This test verifies that get_cache_key doesn't raise TypeError when kwargs already contains preset_cache_key. -Issue: When get_cache_key(**kwargs) is called with kwargs containing +Issue: When get_cache_key(**kwargs) is called with kwargs containing preset_cache_key, the call to _set_preset_cache_key_in_kwargs() would fail with: TypeError: got multiple values for keyword argument 'preset_cache_key' """ diff --git a/tests/local_testing/test_caching.py b/tests/local_testing/test_caching.py index 0c7c0157651..a68e8c91584 100644 --- a/tests/local_testing/test_caching.py +++ b/tests/local_testing/test_caching.py @@ -450,12 +450,9 @@ def c(): litellm.enable_caching_on_provider_specific_optional_params = False -embedding_large_text = ( - """ +embedding_large_text = """ small text -""" - * 5 -) +""" * 5 # # test_caching_with_models() diff --git a/tests/local_testing/test_completion.py b/tests/local_testing/test_completion.py index 6341fa78006..df1b6d7a8e6 100644 --- a/tests/local_testing/test_completion.py +++ b/tests/local_testing/test_completion.py @@ -1638,15 +1638,13 @@ def custom_callback( ################################################# - print( - f""" + print(f""" Model: {model}, Messages: {messages}, User: {user}, Seed: {kwargs["seed"]}, temperature: {kwargs["temperature"]}, - """ - ) + """) assert kwargs["user"] == "ishaans app" assert kwargs["model"] == "gpt-3.5-turbo-1106" diff --git a/tests/local_testing/test_configs/custom_callbacks.py b/tests/local_testing/test_configs/custom_callbacks.py index 42f88b5d19b..7ef6c1aaad5 100644 --- a/tests/local_testing/test_configs/custom_callbacks.py +++ b/tests/local_testing/test_configs/custom_callbacks.py @@ -93,8 +93,7 @@ class testCustomCallbackProxy(CustomLogger): print("\n\n in custom callback vars my custom logger, ", vars(my_custom_logger)) - print( - f""" + print(f""" Model: {model}, Messages: {messages}, User: {user}, @@ -102,8 +101,7 @@ class testCustomCallbackProxy(CustomLogger): Cost: {cost}, Response: {response} Proxy Metadata: {metadata} - """ - ) + """) return async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time): diff --git a/tests/local_testing/test_get_llm_provider.py b/tests/local_testing/test_get_llm_provider.py index 14b9e8cd136..010a071f73e 100644 --- a/tests/local_testing/test_get_llm_provider.py +++ b/tests/local_testing/test_get_llm_provider.py @@ -477,4 +477,3 @@ def test_get_llm_provider_use_proxy_arg_true_with_direct_args(): assert provider == "litellm_proxy" assert key == arg_api_key # Should use the argument key assert base == arg_api_base # Should use the argument base - diff --git a/tests/local_testing/test_langsmith.py b/tests/local_testing/test_langsmith.py index af7ac46a1cf..6f09b2701b0 100644 --- a/tests/local_testing/test_langsmith.py +++ b/tests/local_testing/test_langsmith.py @@ -21,7 +21,6 @@ verbose_logger.setLevel(logging.DEBUG) litellm.set_verbose = True import time - # test_langsmith_logging() diff --git a/tests/local_testing/test_router_caching.py b/tests/local_testing/test_router_caching.py index cb223b661b4..ad358699a48 100644 --- a/tests/local_testing/test_router_caching.py +++ b/tests/local_testing/test_router_caching.py @@ -16,7 +16,6 @@ import litellm from litellm import Router from litellm.caching import RedisCache, RedisClusterCache - ## Scenarios ## 1. 2 models - openai + azure - 1 model group "gpt-3.5-turbo", ## 2. 2 models - openai, azure - 2 diff model groups, 1 caching group diff --git a/tests/logging_callback_tests/conftest.py b/tests/logging_callback_tests/conftest.py index 7100c8456a9..61027a58fa9 100644 --- a/tests/logging_callback_tests/conftest.py +++ b/tests/logging_callback_tests/conftest.py @@ -19,7 +19,6 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm - _LIST_ATTRS = ( "callbacks", "success_callback", diff --git a/tests/logging_callback_tests/create_mock_standard_logging_payload.py b/tests/logging_callback_tests/create_mock_standard_logging_payload.py index 2fd6a4ffa8a..828faba0b04 100644 --- a/tests/logging_callback_tests/create_mock_standard_logging_payload.py +++ b/tests/logging_callback_tests/create_mock_standard_logging_payload.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py b/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py index 3e8d59b2992..ce2b05665cd 100644 --- a/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py +++ b/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_datadog_llm_obs.py b/tests/logging_callback_tests/test_datadog_llm_obs.py index 74f642e6fa3..b192288daec 100644 --- a/tests/logging_callback_tests/test_datadog_llm_obs.py +++ b/tests/logging_callback_tests/test_datadog_llm_obs.py @@ -6,7 +6,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_gcs_pub_sub.py b/tests/logging_callback_tests/test_gcs_pub_sub.py index f4cc9735177..6c99e724afe 100644 --- a/tests/logging_callback_tests/test_gcs_pub_sub.py +++ b/tests/logging_callback_tests/test_gcs_pub_sub.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_generic_api_callback.py b/tests/logging_callback_tests/test_generic_api_callback.py index 6984b6fa00c..a2fdf0b2b04 100644 --- a/tests/logging_callback_tests/test_generic_api_callback.py +++ b/tests/logging_callback_tests/test_generic_api_callback.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_langsmith_unit_test.py b/tests/logging_callback_tests/test_langsmith_unit_test.py index 155b1f396f6..d408c1caefa 100644 --- a/tests/logging_callback_tests/test_langsmith_unit_test.py +++ b/tests/logging_callback_tests/test_langsmith_unit_test.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_log_db_redis_services.py b/tests/logging_callback_tests/test_log_db_redis_services.py index fa0c3b595a0..08e45590a8f 100644 --- a/tests/logging_callback_tests/test_log_db_redis_services.py +++ b/tests/logging_callback_tests/test_log_db_redis_services.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_view_request_resp_logs.py b/tests/logging_callback_tests/test_view_request_resp_logs.py index ea778a44e67..66463e315c3 100644 --- a/tests/logging_callback_tests/test_view_request_resp_logs.py +++ b/tests/logging_callback_tests/test_view_request_resp_logs.py @@ -25,7 +25,6 @@ from litellm.integrations.gcs_bucket.gcs_bucket import ( ) from litellm.types.utils import StandardCallbackDynamicParams - # This is the response payload that GCS would return. mock_response_data = { "id": "chatcmpl-9870a859d6df402795f75dc5fca5b2e0", diff --git a/tests/mcp_tests/test_mcp_logging.py b/tests/mcp_tests/test_mcp_logging.py index 55b49aa0d29..73de99ac894 100644 --- a/tests/mcp_tests/test_mcp_logging.py +++ b/tests/mcp_tests/test_mcp_logging.py @@ -5,7 +5,6 @@ import asyncio from typing import Optional from unittest.mock import AsyncMock, patch - sys.path.insert( 0, os.path.abspath("../../..") ) # Adds the parent directory to the system path diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py index 6af07585796..e931ba4ab9b 100644 --- a/tests/mcp_tests/test_mcp_server.py +++ b/tests/mcp_tests/test_mcp_server.py @@ -18,7 +18,6 @@ from litellm.proxy._types import LiteLLM_ObjectPermissionTable from mcp.types import Tool as MCPTool, CallToolResult, ListToolsResult from mcp.types import TextContent - mcp_server_manager = MCPServerManager() diff --git a/tests/mcp_tests/test_proxy_mcp_e2e.py b/tests/mcp_tests/test_proxy_mcp_e2e.py index 2dd57e13d3b..62fc7ad3ff8 100644 --- a/tests/mcp_tests/test_proxy_mcp_e2e.py +++ b/tests/mcp_tests/test_proxy_mcp_e2e.py @@ -20,7 +20,6 @@ from litellm.proxy.proxy_server import ( initialize, ) - CONFIG_TEMPLATE_PATH = Path("tests/mcp_tests/test_configs/test_config_mcp_e2e.yaml") MCP_SERVER_SCRIPT = Path("tests/mcp_tests/mcp_server.py") PROJECT_ROOT = Path(__file__).resolve().parents[2] diff --git a/tests/ocr_tests/base_ocr_unit_tests.py b/tests/ocr_tests/base_ocr_unit_tests.py index 2120abc8a00..5c354690ddf 100644 --- a/tests/ocr_tests/base_ocr_unit_tests.py +++ b/tests/ocr_tests/base_ocr_unit_tests.py @@ -9,7 +9,6 @@ import litellm import os from abc import ABC, abstractmethod - # Test resources TEST_IMAGE_PATH = "test_image_edit.png" TEST_PDF_URL = "https://arxiv.org/pdf/2201.04234" diff --git a/tests/ocr_tests/test_ocr_azure_document_intelligence.py b/tests/ocr_tests/test_ocr_azure_document_intelligence.py index 7269890b7b6..27428f96a21 100644 --- a/tests/ocr_tests/test_ocr_azure_document_intelligence.py +++ b/tests/ocr_tests/test_ocr_azure_document_intelligence.py @@ -110,9 +110,7 @@ class TestAzureDocumentIntelligencePagesParam: model="azure_ai/doc-intelligence/prebuilt-layout", optional_params={"pages": "1-3,5"}, ) - assert ( - f"api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}" in url - ), url + assert f"api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}" in url, url assert "pages=1-3,5" in url, url assert "/documentintelligence/documentModels/prebuilt-layout:analyze" in url @@ -168,4 +166,3 @@ class TestAzureDocumentIntelligencePagesParam: assert "pages=3,4,5,6,7,8,9" in url assert req.data == {"urlSource": "https://example.com/x.pdf"} - diff --git a/tests/old_proxy_tests/tests/bursty_load_test_completion.py b/tests/old_proxy_tests/tests/bursty_load_test_completion.py index 642bd9f14d4..41944c03aa2 100644 --- a/tests/old_proxy_tests/tests/bursty_load_test_completion.py +++ b/tests/old_proxy_tests/tests/bursty_load_test_completion.py @@ -3,7 +3,6 @@ from openai import AsyncOpenAI from litellm._uuid import uuid import traceback - litellm_client = AsyncOpenAI(api_key="test", base_url="http://0.0.0.0:8000") diff --git a/tests/old_proxy_tests/tests/load_test_embedding_100.py b/tests/old_proxy_tests/tests/load_test_embedding_100.py index 8cd4d250249..bfb4e137e80 100644 --- a/tests/old_proxy_tests/tests/load_test_embedding_100.py +++ b/tests/old_proxy_tests/tests/load_test_embedding_100.py @@ -3,7 +3,6 @@ from openai import AsyncOpenAI from litellm._uuid import uuid import traceback - litellm_client = AsyncOpenAI(api_key="test", base_url="http://0.0.0.0:8000") diff --git a/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py b/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py index 2f8455dcbe9..cde68002a75 100644 --- a/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py +++ b/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py @@ -8,7 +8,6 @@ from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator - trace.set_tracer_provider(TracerProvider()) memory_exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(memory_exporter) diff --git a/tests/openai_endpoints_tests/test_openai_batches_endpoint.py b/tests/openai_endpoints_tests/test_openai_batches_endpoint.py index ad28e8da3df..c75416edf1d 100644 --- a/tests/openai_endpoints_tests/test_openai_batches_endpoint.py +++ b/tests/openai_endpoints_tests/test_openai_batches_endpoint.py @@ -11,7 +11,6 @@ import sys import time from unittest.mock import patch, MagicMock, AsyncMock - BASE_URL = "http://localhost:4000" # Replace with your actual base URL API_KEY = "sk-1234" # Replace with your actual API key diff --git a/tests/openai_endpoints_tests/test_openai_files_endpoints.py b/tests/openai_endpoints_tests/test_openai_files_endpoints.py index 6be692b278c..547bb554835 100644 --- a/tests/openai_endpoints_tests/test_openai_files_endpoints.py +++ b/tests/openai_endpoints_tests/test_openai_files_endpoints.py @@ -6,7 +6,6 @@ import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union - BASE_URL = "http://localhost:4000" # Replace with your actual base URL API_KEY = "sk-1234" # Replace with your actual API key diff --git a/tests/otel_tests/test_e2e_model_access.py b/tests/otel_tests/test_e2e_model_access.py index 7ea75a9d61d..485031d4e93 100644 --- a/tests/otel_tests/test_e2e_model_access.py +++ b/tests/otel_tests/test_e2e_model_access.py @@ -5,7 +5,6 @@ import json from httpx import AsyncClient from typing import Any, Optional, List, Literal - # The proxy strips client-supplied `mock_response` unless the calling key or # team has this admin-metadata flag set. See `_UNTRUSTED_ROOT_CONTROL_FIELDS` # in litellm/proxy/litellm_pre_call_utils.py. diff --git a/tests/otel_tests/test_team_member_permissions.py b/tests/otel_tests/test_team_member_permissions.py index ddb8b741c45..36926b9f525 100644 --- a/tests/otel_tests/test_team_member_permissions.py +++ b/tests/otel_tests/test_team_member_permissions.py @@ -4,13 +4,13 @@ Invalid Permissions: - - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries editing a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries deleting a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries regenerating a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries editing a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries deleting a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries regenerating a key with team_id = team_id -> expect to fail. Invalid Permissions Valid Permissions: - - User tries calling /key/info with team_id, expect to get valid response + - User tries calling /key/info with team_id, expect to get valid response @@ -26,7 +26,7 @@ Invalid Permissions: - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries calling /key/info with team_id, expect to get valid response + - User tries calling /key/info with team_id, expect to get valid response diff --git a/tests/pass_through_tests/test_openai_assistants_passthrough.py b/tests/pass_through_tests/test_openai_assistants_passthrough.py index 28568005fd6..c8e9a9ef0f0 100644 --- a/tests/pass_through_tests/test_openai_assistants_passthrough.py +++ b/tests/pass_through_tests/test_openai_assistants_passthrough.py @@ -6,7 +6,6 @@ import tempfile from typing_extensions import override from openai import AssistantEventHandler - client = openai.OpenAI(base_url="http://0.0.0.0:4000/openai", api_key="sk-1234") diff --git a/tests/pass_through_tests/test_vertex_ai.py b/tests/pass_through_tests/test_vertex_ai.py index 73bf03c5000..668ca017f71 100644 --- a/tests/pass_through_tests/test_vertex_ai.py +++ b/tests/pass_through_tests/test_vertex_ai.py @@ -12,7 +12,6 @@ import os import pytest import asyncio - # Path to your service account JSON file SERVICE_ACCOUNT_FILE = "path/to/your/service-account.json" diff --git a/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py b/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py index 5fc4ecefb33..b2187762399 100644 --- a/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py +++ b/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py @@ -22,10 +22,8 @@ sys.path.insert(0, os.path.abspath("../../..")) import pytest import litellm - # Large document for caching tests (needs 1024+ tokens for Claude models) -LARGE_DOCUMENT_FOR_CACHING = ( - """ +LARGE_DOCUMENT_FOR_CACHING = """ This is a comprehensive legal agreement between Party A and Party B. ARTICLE 1: DEFINITIONS @@ -77,9 +75,7 @@ ARTICLE 9: GENERAL PROVISIONS 9.5 Waiver of any provision shall not constitute ongoing waiver. IN WITNESS WHEREOF, the parties have executed this Agreement. -""" - * 8 -) # Repeat to ensure we have enough tokens (need 1024+ for Claude models) +""" * 8 # Repeat to ensure we have enough tokens (need 1024+ for Claude models) class BaseAnthropicMessagesPromptCachingTest(ABC): diff --git a/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py b/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py index f0cc6985e66..3b1db1c327e 100644 --- a/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py +++ b/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py @@ -14,7 +14,6 @@ import io import os import time - # this file is to test litellm/proxy sys.path.insert( diff --git a/tests/proxy_admin_ui_tests/test_usage_endpoints.py b/tests/proxy_admin_ui_tests/test_usage_endpoints.py index 54ad136f082..d8ef87f9333 100644 --- a/tests/proxy_admin_ui_tests/test_usage_endpoints.py +++ b/tests/proxy_admin_ui_tests/test_usage_endpoints.py @@ -1,5 +1,5 @@ """ -Tests the following endpoints used by the UI +Tests the following endpoints used by the UI /global/spend/logs /global/spend/keys @@ -9,7 +9,7 @@ Tests the following endpoints used by the UI For all tests - test the following: -- Response is valid +- Response is valid - Response for Admin User is different from response from Internal User """ diff --git a/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py b/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py index 48eb7d85ec1..7f5fcd38c87 100644 --- a/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py +++ b/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py @@ -12,7 +12,6 @@ import pytest import asyncio from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions - # Test models from test_config.yaml # Note: bedrock-converse-claude-sonnet-4.5 removed temporarily as the Bedrock Converse API # for Claude Sonnet 4.5 may not be available in all regions/accounts diff --git a/tests/proxy_unit_tests/conftest.py b/tests/proxy_unit_tests/conftest.py index a0326f64ed7..6ff9ffe84c4 100644 --- a/tests/proxy_unit_tests/conftest.py +++ b/tests/proxy_unit_tests/conftest.py @@ -16,7 +16,6 @@ sys.path.insert( import litellm import litellm.proxy.proxy_server - # Top-level assignments of these types are the ones importlib.reload(litellm) # would have effectively reset. We snapshot them at conftest import time and # deep-copy the snapshot back before every test. diff --git a/tests/proxy_unit_tests/test_configs/custom_callbacks.py b/tests/proxy_unit_tests/test_configs/custom_callbacks.py index c7d66c068e9..ac705cd28a9 100644 --- a/tests/proxy_unit_tests/test_configs/custom_callbacks.py +++ b/tests/proxy_unit_tests/test_configs/custom_callbacks.py @@ -93,8 +93,7 @@ class testCustomCallbackProxy(CustomLogger): print("\n\n in custom callback vars my custom logger, ", vars(my_custom_logger)) - print( - f""" + print(f""" Model: {model}, Messages: {messages}, User: {user}, @@ -102,8 +101,7 @@ class testCustomCallbackProxy(CustomLogger): Cost: {cost}, Response: {response} Proxy Metadata: {metadata} - """ - ) + """) return async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time): diff --git a/tests/proxy_unit_tests/test_jwt_key_mapping.py b/tests/proxy_unit_tests/test_jwt_key_mapping.py index bf1c4a3f6c1..8e3331d17fe 100644 --- a/tests/proxy_unit_tests/test_jwt_key_mapping.py +++ b/tests/proxy_unit_tests/test_jwt_key_mapping.py @@ -27,7 +27,6 @@ from litellm.proxy.management_endpoints.jwt_key_mapping_endpoints import ( from litellm.caching.caching import DualCache from fastapi import HTTPException - # ────────────────────────────────────────────── # Tests: _resolve_jwt_to_virtual_key # ────────────────────────────────────────────── diff --git a/tests/proxy_unit_tests/test_search_api_logging.py b/tests/proxy_unit_tests/test_search_api_logging.py index 71bbe5351a2..3298389083b 100644 --- a/tests/proxy_unit_tests/test_search_api_logging.py +++ b/tests/proxy_unit_tests/test_search_api_logging.py @@ -2,7 +2,7 @@ Test search API logging and cost tracking in proxy. Tests that search API requests are properly logged to LiteLLM_SpendLogs -with correct fields populated (call_type, model, custom_llm_provider, +with correct fields populated (call_type, model, custom_llm_provider, model_group, spend, etc.) """ diff --git a/tests/proxy_unit_tests/test_user_api_key_auth.py b/tests/proxy_unit_tests/test_user_api_key_auth.py index 543cabb6b4c..6203ae06044 100644 --- a/tests/proxy_unit_tests/test_user_api_key_auth.py +++ b/tests/proxy_unit_tests/test_user_api_key_auth.py @@ -269,9 +269,7 @@ async def test_aaauser_personal_budgets(key_ownership): test_user_cache = getattr(litellm.proxy.proxy_server, "user_api_key_cache") assert ( - test_user_cache.get_cache( - key=hash_token(user_key), model_type=UserAPIKeyAuth - ) + test_user_cache.get_cache(key=hash_token(user_key), model_type=UserAPIKeyAuth) == valid_token ) diff --git a/tests/router_unit_tests/create_mock_standard_logging_payload.py b/tests/router_unit_tests/create_mock_standard_logging_payload.py index 2fd6a4ffa8a..828faba0b04 100644 --- a/tests/router_unit_tests/create_mock_standard_logging_payload.py +++ b/tests/router_unit_tests/create_mock_standard_logging_payload.py @@ -2,7 +2,6 @@ import io import os import sys - sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/router_unit_tests/test_router_handle_error.py b/tests/router_unit_tests/test_router_handle_error.py index 660b3885126..552b0048832 100644 --- a/tests/router_unit_tests/test_router_handle_error.py +++ b/tests/router_unit_tests/test_router_handle_error.py @@ -15,7 +15,6 @@ from collections import defaultdict from dotenv import load_dotenv from unittest.mock import AsyncMock, MagicMock - load_dotenv() diff --git a/tests/search_tests/test_google_pse_search.py b/tests/search_tests/test_google_pse_search.py index 21d58a95491..9ce55b428da 100644 --- a/tests/search_tests/test_google_pse_search.py +++ b/tests/search_tests/test_google_pse_search.py @@ -10,7 +10,6 @@ sys.path.insert(0, os.path.abspath("../..")) from tests.search_tests.base_search_unit_tests import BaseSearchTest - # class TestGooglePSESearch(BaseSearchTest): # """ # Tests for Google PSE Search functionality. diff --git a/tests/store_model_in_db_tests/test_callbacks_in_db.py b/tests/store_model_in_db_tests/test_callbacks_in_db.py index 4a851251a3e..51545333c65 100644 --- a/tests/store_model_in_db_tests/test_callbacks_in_db.py +++ b/tests/store_model_in_db_tests/test_callbacks_in_db.py @@ -1,9 +1,9 @@ """ PROD TEST - DO NOT Delete this Test -e2e test for langfuse callback in DB +e2e test for langfuse callback in DB - Add langfuse callback to DB - with /config/update -- wait 20 seconds for the callback to be loaded into the instance +- wait 20 seconds for the callback to be loaded into the instance - Make a /chat/completions request to the proxy - Check if the request is logged in Langfuse """ diff --git a/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py b/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py index a4f7f8187c7..694806c179b 100644 --- a/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py +++ b/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py @@ -14,7 +14,6 @@ import json import pytest from unittest.mock import AsyncMock, MagicMock, patch - SAMPLE_ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789:runtime/my_agent" SAMPLE_MODEL = f"bedrock/agentcore/{SAMPLE_ARN}" SAMPLE_PARAMS = { diff --git a/tests/test_litellm/caching/test_disk_cache.py b/tests/test_litellm/caching/test_disk_cache.py new file mode 100644 index 00000000000..d2b6fef5385 --- /dev/null +++ b/tests/test_litellm/caching/test_disk_cache.py @@ -0,0 +1,70 @@ +import os +import sys +import tempfile + +import pytest + +sys.path.insert(0, os.path.abspath("../../..")) + +from litellm.caching.disk_cache import DiskCache + + +@pytest.fixture +def tmp_cache(): + with tempfile.TemporaryDirectory() as d: + yield DiskCache(disk_cache_dir=d) + + +def test_disk_cache_uses_jsondisk(tmp_cache): + """Confirm DiskCache constructs the underlying cache with JSONDisk to + avoid pickle deserialization (CVE-2025-69872).""" + import diskcache as dc + + assert tmp_cache.disk_cache.disk.__class__ is dc.JSONDisk + + +def test_disk_cache_dict_round_trip(tmp_cache): + payload = {"response": "hello", "timestamp": 1.0} + tmp_cache.set_cache("k", payload) + assert tmp_cache.get_cache("k") == payload + + +def test_disk_cache_string_round_trip_decodes_json(tmp_cache): + """get_cache decodes JSON strings transparently for callers that stored + pre-serialized payloads.""" + tmp_cache.set_cache("k", '{"hello": "world"}') + assert tmp_cache.get_cache("k") == {"hello": "world"} + + +def test_disk_cache_string_round_trip_returns_raw_when_not_json(tmp_cache): + tmp_cache.set_cache("k", "not-json") + assert tmp_cache.get_cache("k") == "not-json" + + +def test_disk_cache_increment(tmp_cache): + tmp_cache.set_cache("counter", 42) + assert tmp_cache.increment_cache("counter", 8) == 50 + assert tmp_cache.get_cache("counter") == 50 + + +def test_disk_cache_ttl_expiry(tmp_cache): + import time + + tmp_cache.set_cache("k", "v", ttl=1) + assert tmp_cache.get_cache("k") == "v" + time.sleep(1.1) + assert tmp_cache.get_cache("k") is None + + +def test_disk_cache_delete_and_flush(tmp_cache): + tmp_cache.set_cache("a", 1) + tmp_cache.set_cache("b", 2) + tmp_cache.delete_cache("a") + assert tmp_cache.get_cache("a") is None + assert tmp_cache.get_cache("b") == 2 + tmp_cache.flush_cache() + assert tmp_cache.get_cache("b") is None + + +def test_disk_cache_missing_key_returns_none(tmp_cache): + assert tmp_cache.get_cache("never-set") is None diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py index af5e2341406..23a5d9ec990 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py @@ -30,6 +30,7 @@ def no_invitation_wait(monkeypatch): monkeypatch.setattr(BaseEmailLogger, "_wait_for_invitation_creation", _noop) + @pytest.fixture def base_email_logger(): return BaseEmailLogger() @@ -283,7 +284,10 @@ async def test_send_key_created_email_without_key( mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert "sk-secret-key-456" not in call_args["html_body"] - assert "[Key hidden for security - retrieve from dashboard]" in call_args["html_body"] + assert ( + "[Key hidden for security - retrieve from dashboard]" + in call_args["html_body"] + ) @pytest.mark.asyncio @@ -317,7 +321,10 @@ async def test_send_key_rotated_email_without_key( mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert "sk-secret-rotated-789" not in call_args["html_body"] - assert "[Key hidden for security - retrieve from dashboard]" in call_args["html_body"] + assert ( + "[Key hidden for security - retrieve from dashboard]" + in call_args["html_body"] + ) @pytest.mark.asyncio @@ -371,52 +378,52 @@ async def test_get_invitation_link_creates_new_when_none_exist(base_email_logger """Test that _get_invitation_link creates a new invitation when none exist""" # Mock prisma client with no existing invitation rows mock_prisma = mock.MagicMock() - + # Mock find_many to return empty list (no existing invitations) async def mock_find_many_empty(*args, **kwargs): return [] - + mock_prisma.db.litellm_invitationlink.find_many = mock_find_many_empty - + # Mock the create_invitation_for_user function mock_created_invitation = mock.MagicMock() mock_created_invitation.id = "new-invitation-id" - + with mock.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): with mock.patch( "litellm.proxy.management_helpers.user_invitation.create_invitation_for_user", - return_value=mock_created_invitation + return_value=mock_created_invitation, ) as mock_create_invitation: # Execute result = await base_email_logger._get_invitation_link( user_id="test-user", base_url="http://test.com" ) - + # Verify that create_invitation_for_user was called mock_create_invitation.assert_called_once() call_args = mock_create_invitation.call_args[1] assert call_args["data"].user_id == "test-user" assert call_args["user_api_key_dict"].user_id == "test-user" - + # Verify the returned link uses the new invitation ID assert result == "http://test.com/ui?invitation_id=new-invitation-id" -@pytest.mark.asyncio +@pytest.mark.asyncio async def test_get_invitation_link_uses_existing_when_available(base_email_logger): """Test that _get_invitation_link uses existing invitation when available""" # Mock prisma client with existing invitation row mock_invitation_row = mock.MagicMock() mock_invitation_row.id = "existing-invitation-id" - + mock_prisma = mock.MagicMock() - + # Mock find_many to return existing invitation async def mock_find_many_existing(*args, **kwargs): return [mock_invitation_row] - + mock_prisma.db.litellm_invitationlink.find_many = mock_find_many_existing - + with mock.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): with mock.patch( "litellm.proxy.management_helpers.user_invitation.create_invitation_for_user" @@ -425,10 +432,10 @@ async def test_get_invitation_link_uses_existing_when_available(base_email_logge result = await base_email_logger._get_invitation_link( user_id="test-user", base_url="http://test.com" ) - + # Verify that create_invitation_for_user was NOT called mock_create_invitation.assert_not_called() - + # Verify the returned link uses the existing invitation ID assert result == "http://test.com/ui?invitation_id=existing-invitation-id" @@ -438,33 +445,33 @@ async def test_get_invitation_link_creates_new_when_list_is_none(base_email_logg """Test that _get_invitation_link creates a new invitation when invitation_rows is None""" # Mock prisma client to return None mock_prisma = mock.MagicMock() - + # Mock find_many to return None async def mock_find_many_none(*args, **kwargs): return None - + mock_prisma.db.litellm_invitationlink.find_many = mock_find_many_none - + # Mock the create_invitation_for_user function mock_created_invitation = mock.MagicMock() mock_created_invitation.id = "new-invitation-from-none" - + with mock.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): with mock.patch( "litellm.proxy.management_helpers.user_invitation.create_invitation_for_user", - return_value=mock_created_invitation + return_value=mock_created_invitation, ) as mock_create_invitation: # Execute result = await base_email_logger._get_invitation_link( user_id="test-user", base_url="http://test.com" ) - + # Verify that create_invitation_for_user was called mock_create_invitation.assert_called_once() call_args = mock_create_invitation.call_args[1] assert call_args["data"].user_id == "test-user" assert call_args["user_api_key_dict"].user_id == "test-user" - + # Verify the returned link uses the new invitation ID assert result == "http://test.com/ui?invitation_id=new-invitation-from-none" @@ -495,13 +502,15 @@ async def test_get_email_params_user_invitation( user_email="test@example.com", ) - assert result.logo_url == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" + assert ( + result.logo_url + == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" + ) assert result.support_contact == "support@berri.ai" assert result.base_url == "http://test.com/ui?invitation_id=test-id" assert result.recipient_email == "test@example.com" - @pytest.fixture def mock_env_vars(monkeypatch): """Set up test environment variables""" @@ -513,69 +522,74 @@ def mock_env_vars(monkeypatch): monkeypatch.setenv("PROXY_BASE_URL", "http://test.com") monkeypatch.setenv("PROXY_API_URL", "https://test.com") + @pytest.mark.asyncio async def test_get_email_params_custom_templates_premium_user(mock_env_vars): """Test that _get_email_params returns correct values with custom templates for premium users""" # Mock premium_user as True with patch("litellm.proxy.proxy_server.premium_user", True): email_logger = BaseEmailLogger() - + # Test invitation email params invitation_params = await email_logger._get_email_params( email_event=EmailEvent.new_user_invitation, user_id="testid", user_email="test@example.com", - event_message="New User Invitation" + event_message="New User Invitation", ) - + assert invitation_params.subject == "Welcome to Test Company!" assert invitation_params.signature == "Best regards,\nTest Company Team" assert invitation_params.logo_url == "https://test-company.com/logo.png" assert invitation_params.support_contact == "support@test-company.com" assert invitation_params.base_url == "http://test.com" - + # Test key created email params key_params = await email_logger._get_email_params( email_event=EmailEvent.virtual_key_created, user_id="testid", user_email="test@example.com", - event_message="API Key Created" + event_message="API Key Created", ) - + assert key_params.subject == "Your Test Company API Key" assert key_params.signature == "Best regards,\nTest Company Team" + @pytest.mark.asyncio async def test_get_email_params_non_premium_user(mock_env_vars): """Test that non-premium users get default templates even when custom ones are provided""" # Mock premium_user as False with patch("litellm.proxy.proxy_server.premium_user", False): email_logger = BaseEmailLogger() - + # Test invitation email params email_params = await email_logger._get_email_params( email_event=EmailEvent.new_user_invitation, user_email="test@example.com", - event_message="New User Invitation" + event_message="New User Invitation", ) - + # Should use default values even though custom values are set in env assert email_params.subject == "LiteLLM: New User Invitation" assert email_params.signature == EMAIL_FOOTER - assert email_params.logo_url == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" + assert ( + email_params.logo_url + == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" + ) assert email_params.support_contact == "support@berri.ai" - # Test key created email params key_params = await email_logger._get_email_params( email_event=EmailEvent.virtual_key_created, user_email="test@example.com", - event_message="API Key Created" + event_message="API Key Created", ) - + assert key_params.subject == "LiteLLM: API Key Created" assert key_params.signature == EMAIL_FOOTER + @pytest.mark.asyncio async def test_get_email_params_default_templates(monkeypatch): """Test that _get_email_params uses default templates when custom ones aren't provided""" @@ -583,28 +597,28 @@ async def test_get_email_params_default_templates(monkeypatch): monkeypatch.delenv("EMAIL_SUBJECT_INVITATION", raising=False) monkeypatch.delenv("EMAIL_SUBJECT_KEY_CREATED", raising=False) monkeypatch.delenv("EMAIL_SIGNATURE", raising=False) - + # Mock premium_user as True (shouldn't matter since no custom values are set) with patch("litellm.proxy.proxy_server.premium_user", True): email_logger = BaseEmailLogger() - + # Test invitation email params with default template invitation_params = await email_logger._get_email_params( email_event=EmailEvent.new_user_invitation, user_email="test@example.com", - event_message="New User Invitation" + event_message="New User Invitation", ) - + assert invitation_params.subject == "LiteLLM: New User Invitation" assert invitation_params.signature == EMAIL_FOOTER - + # Test key created email params with default template key_params = await email_logger._get_email_params( email_event=EmailEvent.virtual_key_created, user_email="test@example.com", - event_message="API Key Created" + event_message="API Key Created", ) - + assert key_params.subject == "LiteLLM: API Key Created" assert key_params.signature == EMAIL_FOOTER @@ -639,7 +653,10 @@ async def test_send_soft_budget_alert_email( call_args = mock_send_email.call_args[1] assert call_args["from_email"] == BaseEmailLogger.DEFAULT_LITELLM_EMAIL assert call_args["to_email"] == ["test@example.com"] - assert call_args["subject"] == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" + assert ( + call_args["subject"] + == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" + ) assert "$100.0" in call_args["html_body"] # soft_budget assert "$105.0" in call_args["html_body"] # spend assert "$200.0" in call_args["html_body"] # max_budget @@ -673,13 +690,13 @@ async def test_send_soft_budget_alert_email_no_max_budget( call_args = mock_send_email.call_args[1] assert "$100.0" in call_args["html_body"] # soft_budget assert "$105.0" in call_args["html_body"] # spend - assert "Maximum Budget" not in call_args["html_body"] # max_budget should not be shown + assert ( + "Maximum Budget" not in call_args["html_body"] + ) # max_budget should not be shown @pytest.mark.asyncio -async def test_budget_alerts_soft_budget_crossed( - base_email_logger, mock_send_email -): +async def test_budget_alerts_soft_budget_crossed(base_email_logger, mock_send_email): """Test that budget_alerts sends email when soft budget is crossed""" user_info = CallInfo( user_id="test_user", @@ -708,11 +725,14 @@ async def test_budget_alerts_soft_budget_crossed( mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert call_args["to_email"] == ["test@example.com"] - + # Verify cache was set to prevent duplicate alerts mock_cache.async_set_cache.assert_called_once() cache_call_args = mock_cache.async_set_cache.call_args[1] - assert cache_call_args["key"] == "email_budget_alerts:soft_budget_crossed:test_user" + assert ( + cache_call_args["key"] + == "email_budget_alerts:soft_budget_crossed:test_user" + ) assert cache_call_args["value"] == "SENT" assert cache_call_args["ttl"] == EMAIL_BUDGET_ALERT_TTL @@ -766,9 +786,7 @@ async def test_budget_alerts_soft_budget_duplicate_prevention( @pytest.mark.asyncio -async def test_budget_alerts_no_budgets( - base_email_logger, mock_send_email -): +async def test_budget_alerts_no_budgets(base_email_logger, mock_send_email): """Test that budget_alerts returns early when no budgets are set""" user_info = CallInfo( user_id="test_user", @@ -817,7 +835,10 @@ async def test_budget_alerts_uses_token_for_cache_key( # Verify cache key uses token instead of user_id mock_cache.async_set_cache.assert_called_once() cache_call_args = mock_cache.async_set_cache.call_args[1] - assert cache_call_args["key"] == "email_budget_alerts:soft_budget_crossed:hashed_token_123" + assert ( + cache_call_args["key"] + == "email_budget_alerts:soft_budget_crossed:hashed_token_123" + ) @pytest.mark.asyncio @@ -838,7 +859,9 @@ async def test_get_email_params_soft_budget_crossed( ) # Should use default subject template for soft_budget_crossed - assert result.subject == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" + assert ( + result.subject == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" + ) assert result.recipient_email == "test@example.com" assert result.base_url == "http://test.com" @@ -867,16 +890,20 @@ async def test_budget_alerts_max_budget_alert_crossed( "PROXY_BASE_URL": "http://test.com", }, ): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert call_args["to_email"] == ["test@example.com"] assert "Max Budget Alert" in call_args["subject"] - + mock_cache.async_set_cache.assert_called_once() cache_call_args = mock_cache.async_set_cache.call_args[1] - assert cache_call_args["key"] == "email_budget_alerts:max_budget_alert:test_user" + assert ( + cache_call_args["key"] == "email_budget_alerts:max_budget_alert:test_user" + ) assert cache_call_args["value"] == "SENT" assert cache_call_args["ttl"] == EMAIL_BUDGET_ALERT_TTL @@ -906,15 +933,15 @@ async def test_multi_threshold_sends_crossed_thresholds( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) # spend=80 crosses 50% ($50) and 75% ($75), but not 100% ($100) assert mock_send_email.call_count == 2 # Check cache keys include threshold percentage - cache_keys = [ - c[1]["key"] for c in mock_cache.async_set_cache.call_args_list - ] + cache_keys = [c[1]["key"] for c in mock_cache.async_set_cache.call_args_list] assert "email_budget_alerts:max_budget_alert:50:hashed_key_1" in cache_keys assert "email_budget_alerts:max_budget_alert:75:hashed_key_1" in cache_keys @@ -949,7 +976,9 @@ async def test_multi_threshold_dedup_cache_prevents_resend( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) # Only 75% should fire assert mock_send_email.call_count == 1 @@ -980,7 +1009,9 @@ async def test_multi_threshold_owner_email_auto_included( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) mock_send_email.assert_called_once() to_emails = mock_send_email.call_args[1]["to_email"] @@ -1002,7 +1033,7 @@ async def test_multi_threshold_malformed_keys_skipped( event_group=Litellm_EntityType.KEY, max_budget_alert_emails={ "fifty": ["finance@co.com"], # invalid - "50": ["finance@co.com"], # valid, crossed + "50": ["finance@co.com"], # valid, crossed }, ) @@ -1012,7 +1043,9 @@ async def test_multi_threshold_malformed_keys_skipped( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) # Only the valid "50" threshold should fire assert mock_send_email.call_count == 1 @@ -1041,7 +1074,9 @@ async def test_multi_threshold_empty_emails_only_owner( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) mock_send_email.assert_called_once() to_emails = mock_send_email.call_args[1]["to_email"] @@ -1067,11 +1102,13 @@ async def test_no_map_preserves_old_single_threshold( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) + await base_email_logger.budget_alerts( + type="max_budget_alert", user_info=user_info + ) mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert call_args["to_email"] == ["test@example.com"] # Old path cache key has no threshold percentage cache_key = mock_cache.async_set_cache.call_args[1]["key"] - assert cache_key == "email_budget_alerts:max_budget_alert:test_user" \ No newline at end of file + assert cache_key == "email_budget_alerts:max_budget_alert:test_user" diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py index b07216921eb..dfb51c8bbc8 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py @@ -87,7 +87,7 @@ async def test_send_email_success(mock_env_vars): async def test_send_email_missing_api_key(): # Remove the API key from environment before initializing logger original_key = os.environ.pop("RESEND_API_KEY", None) - + try: # Initialize the logger after removing the API key logger = ResendEmailLogger() @@ -104,16 +104,19 @@ async def test_send_email_missing_api_key(): mock_response.raise_for_status.return_value = None mock_response.status_code = 200 mock_response.json.return_value = {"id": "test_email_id"} - + mock_async_client = mock.AsyncMock() mock_async_client.post.return_value = mock_response - + # Directly inject the mock client to bypass any caching logger.async_httpx_client = mock_async_client # Send email await logger.send_email( - from_email=from_email, to_email=to_email, subject=subject, html_body=html_body + from_email=from_email, + to_email=to_email, + subject=subject, + html_body=html_body, ) # Verify the HTTP client was called with None as the API key diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py index 40439a78a49..d247b02074d 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py @@ -32,12 +32,12 @@ def mock_env_vars(): # Store original values original_api_key = os.environ.get("SENDGRID_API_KEY") original_sender_email = os.environ.get("SENDGRID_SENDER_EMAIL") - + # Set test API key and remove SENDGRID_SENDER_EMAIL to ensure isolation os.environ["SENDGRID_API_KEY"] = "test_api_key" if "SENDGRID_SENDER_EMAIL" in os.environ: del os.environ["SENDGRID_SENDER_EMAIL"] - + try: yield finally: @@ -46,7 +46,7 @@ def mock_env_vars(): os.environ["SENDGRID_API_KEY"] = original_api_key elif "SENDGRID_API_KEY" in os.environ: del os.environ["SENDGRID_API_KEY"] - + if original_sender_email is not None: os.environ["SENDGRID_SENDER_EMAIL"] = original_sender_email diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py b/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py index b160ca5130c..9743e7bedc9 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py @@ -18,168 +18,282 @@ from litellm.types.utils import StandardCallbackDynamicParams class TestEnterpriseCallbackControls: - + @pytest.fixture def mock_premium_user(self): """Fixture to mock premium user check as True""" - with patch.object(EnterpriseCallbackControls, '_should_allow_dynamic_callback_disabling', return_value=True): + with patch.object( + EnterpriseCallbackControls, + "_should_allow_dynamic_callback_disabling", + return_value=True, + ): yield - - @pytest.fixture + + @pytest.fixture def mock_non_premium_user(self): """Fixture to mock premium user check as False""" - with patch.object(EnterpriseCallbackControls, '_should_allow_dynamic_callback_disabling', return_value=False): + with patch.object( + EnterpriseCallbackControls, + "_should_allow_dynamic_callback_disabling", + return_value=False, + ): yield @pytest.fixture def mock_request_headers(self): """Fixture to mock get_proxy_server_request_headers""" - with patch('enterprise.litellm_enterprise.enterprise_callbacks.callback_controls.get_proxy_server_request_headers') as mock_headers: + with patch( + "enterprise.litellm_enterprise.enterprise_callbacks.callback_controls.get_proxy_server_request_headers" + ) as mock_headers: yield mock_headers - def test_callback_disabled_langfuse_string(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_langfuse_string( + self, mock_premium_user, mock_request_headers + ): """Test that 'langfuse' string callback is disabled when specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_callback_disabled_langfuse_customlogger(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_langfuse_customlogger( + self, mock_premium_user, mock_request_headers + ): """Test that LangfusePromptManagement CustomLogger instance is disabled when 'langfuse' specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + langfuse_logger = LangfusePromptManagement() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically(langfuse_logger, litellm_params, standard_callback_dynamic_params) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + langfuse_logger, litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_callback_disabled_s3_v2_string(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_s3_v2_string( + self, mock_premium_user, mock_request_headers + ): """Test that 's3_v2' string callback is disabled when specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "s3_v2"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "s3_v2", litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_callback_disabled_s3_v2_customlogger(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_s3_v2_customlogger( + self, mock_premium_user, mock_request_headers + ): """Test that S3Logger CustomLogger instance is disabled when 's3_v2' specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "s3_v2"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock S3Logger to avoid async initialization issues - with patch('litellm.integrations.s3_v2.S3Logger.__init__', return_value=None): + with patch("litellm.integrations.s3_v2.S3Logger.__init__", return_value=None): s3_logger = S3Logger() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically(s3_logger, litellm_params, standard_callback_dynamic_params) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + s3_logger, litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_callback_disabled_datadog_string(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_datadog_string( + self, mock_premium_user, mock_request_headers + ): """Test that 'datadog' string callback is disabled when specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "datadog"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "datadog", litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_callback_disabled_datadog_customlogger(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_datadog_customlogger( + self, mock_premium_user, mock_request_headers + ): """Test that DataDogLogger CustomLogger instance is disabled when 'datadog' specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "datadog"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock DataDogLogger to avoid async initialization issues - with patch('litellm.integrations.datadog.datadog.DataDogLogger.__init__', return_value=None): + with patch( + "litellm.integrations.datadog.datadog.DataDogLogger.__init__", + return_value=None, + ): datadog_logger = DataDogLogger() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically(datadog_logger, litellm_params, standard_callback_dynamic_params) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + datadog_logger, litellm_params, standard_callback_dynamic_params + ) assert result is True def test_multiple_callbacks_disabled(self, mock_premium_user, mock_request_headers): """Test that multiple callbacks can be disabled with comma-separated list""" - mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse,datadog,s3_v2"} + mock_request_headers.return_value = { + X_LITELLM_DISABLE_CALLBACKS: "langfuse,datadog,s3_v2" + } litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - # Test each callback is disabled - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) is True - - # Test non-disabled callback is not disabled - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("prometheus", litellm_params, standard_callback_dynamic_params) is False - def test_callback_not_disabled_when_not_in_list(self, mock_premium_user, mock_request_headers): + # Test each callback is disabled + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "datadog", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "s3_v2", litellm_params, standard_callback_dynamic_params + ) + is True + ) + + # Test non-disabled callback is not disabled + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "prometheus", litellm_params, standard_callback_dynamic_params + ) + is False + ) + + def test_callback_not_disabled_when_not_in_list( + self, mock_premium_user, mock_request_headers + ): """Test that callbacks not in the disabled list are not disabled""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "datadog", litellm_params, standard_callback_dynamic_params + ) assert result is False - def test_callback_not_disabled_when_no_header(self, mock_premium_user, mock_request_headers): + def test_callback_not_disabled_when_no_header( + self, mock_premium_user, mock_request_headers + ): """Test that callbacks are not disabled when the header is not present""" mock_request_headers.return_value = {} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is False - def test_callback_not_disabled_when_header_none(self, mock_premium_user, mock_request_headers): + def test_callback_not_disabled_when_header_none( + self, mock_premium_user, mock_request_headers + ): """Test that callbacks are not disabled when the header value is None""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: None} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is False - def test_non_premium_user_cannot_disable_callbacks(self, mock_non_premium_user, mock_request_headers): + def test_non_premium_user_cannot_disable_callbacks( + self, mock_non_premium_user, mock_request_headers + ): """Test that non-premium users cannot disable callbacks even with the header""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is False - def test_case_insensitive_callback_matching(self, mock_premium_user, mock_request_headers): + def test_case_insensitive_callback_matching( + self, mock_premium_user, mock_request_headers + ): """Test that callback matching is case insensitive""" - mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "LANGFUSE,DataDog"} + mock_request_headers.return_value = { + X_LITELLM_DISABLE_CALLBACKS: "LANGFUSE,DataDog" + } litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Test lowercase callbacks are disabled - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "datadog", litellm_params, standard_callback_dynamic_params + ) + is True + ) - def test_whitespace_handling_in_disabled_callbacks(self, mock_premium_user, mock_request_headers): + def test_whitespace_handling_in_disabled_callbacks( + self, mock_premium_user, mock_request_headers + ): """Test that whitespace around callback names is handled correctly""" - mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: " langfuse , datadog , s3_v2 "} + mock_request_headers.return_value = { + X_LITELLM_DISABLE_CALLBACKS: " langfuse , datadog , s3_v2 " + } litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) is True - def test_custom_logger_not_in_registry(self, mock_premium_user, mock_request_headers): + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "datadog", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "s3_v2", litellm_params, standard_callback_dynamic_params + ) + is True + ) + + def test_custom_logger_not_in_registry( + self, mock_premium_user, mock_request_headers + ): """Test that CustomLogger not in registry is not disabled""" - mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "unknown_logger"} + mock_request_headers.return_value = { + X_LITELLM_DISABLE_CALLBACKS: "unknown_logger" + } litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Create a mock CustomLogger that's not in the registry class UnknownLogger(CustomLogger): pass - + unknown_logger = UnknownLogger() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically(unknown_logger, litellm_params, standard_callback_dynamic_params) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + unknown_logger, litellm_params, standard_callback_dynamic_params + ) assert result is False def test_exception_handling(self, mock_premium_user, mock_request_headers): @@ -188,32 +302,64 @@ class TestEnterpriseCallbackControls: mock_request_headers.side_effect = Exception("Test exception") litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is False - def test_callback_disabled_via_request_body_langfuse(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_via_request_body_langfuse( + self, mock_premium_user, mock_request_headers + ): """Test that callbacks can be disabled via request body litellm_disabled_callbacks""" mock_request_headers.return_value = {} # No headers litellm_params = {"proxy_server_request": {"url": "test"}} - standard_callback_dynamic_params = StandardCallbackDynamicParams(litellm_disabled_callbacks=["langfuse"]) - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + standard_callback_dynamic_params = StandardCallbackDynamicParams( + litellm_disabled_callbacks=["langfuse"] + ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_callback_disabled_via_request_body_multiple(self, mock_premium_user, mock_request_headers): + def test_callback_disabled_via_request_body_multiple( + self, mock_premium_user, mock_request_headers + ): """Test that multiple callbacks can be disabled via request body""" mock_request_headers.return_value = {} # No headers litellm_params = {"proxy_server_request": {"url": "test"}} - standard_callback_dynamic_params = StandardCallbackDynamicParams(litellm_disabled_callbacks=["langfuse", "datadog", "s3_v2"]) - + standard_callback_dynamic_params = StandardCallbackDynamicParams( + litellm_disabled_callbacks=["langfuse", "datadog", "s3_v2"] + ) + # Test each callback is disabled - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) is True - + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "datadog", litellm_params, standard_callback_dynamic_params + ) + is True + ) + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "s3_v2", litellm_params, standard_callback_dynamic_params + ) + is True + ) + # Test non-disabled callback is not disabled - assert EnterpriseCallbackControls.is_callback_disabled_dynamically("prometheus", litellm_params, standard_callback_dynamic_params) is False + assert ( + EnterpriseCallbackControls.is_callback_disabled_dynamically( + "prometheus", litellm_params, standard_callback_dynamic_params + ) + is False + ) def test_admin_can_disable_dynamic_callback_disabling(self, mock_request_headers): """ @@ -223,11 +369,13 @@ class TestEnterpriseCallbackControls: mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock litellm.allow_dynamic_callback_disabling set to False - with patch('litellm.allow_dynamic_callback_disabling', False): - with patch('litellm.proxy.proxy_server.premium_user', True): - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + with patch("litellm.allow_dynamic_callback_disabling", False): + with patch("litellm.proxy.proxy_server.premium_user", True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is False def test_admin_can_enable_dynamic_callback_disabling(self, mock_request_headers): @@ -238,14 +386,18 @@ class TestEnterpriseCallbackControls: mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock litellm.allow_dynamic_callback_disabling set to True - with patch('litellm.allow_dynamic_callback_disabling', True): - with patch('litellm.proxy.proxy_server.premium_user', True): - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + with patch("litellm.allow_dynamic_callback_disabling", True): + with patch("litellm.proxy.proxy_server.premium_user", True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is True - def test_default_admin_setting_allows_dynamic_callback_disabling(self, mock_request_headers): + def test_default_admin_setting_allows_dynamic_callback_disabling( + self, mock_request_headers + ): """ Test that when allow_dynamic_callback_disabling is not set, it defaults to True and allows dynamic callback disabling for premium users @@ -253,8 +405,10 @@ class TestEnterpriseCallbackControls: mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # litellm.allow_dynamic_callback_disabling should default to True - with patch('litellm.proxy.proxy_server.premium_user', True): - result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) + with patch("litellm.proxy.proxy_server.premium_user", True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically( + "langfuse", litellm_params, standard_callback_dynamic_params + ) assert result is True diff --git a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py index 6e9c3c0354b..180041b5f71 100644 --- a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py +++ b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py @@ -16,13 +16,18 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, ) - DECODED_UNIFIED_INPUT_FILE_ID = "litellm_proxy:application/octet-stream;unified_id,test-uuid;target_model_names,azure-gpt-4" -B64_UNIFIED_INPUT_FILE_ID = base64.urlsafe_b64encode(DECODED_UNIFIED_INPUT_FILE_ID.encode()).decode().rstrip("=") +B64_UNIFIED_INPUT_FILE_ID = ( + base64.urlsafe_b64encode(DECODED_UNIFIED_INPUT_FILE_ID.encode()) + .decode() + .rstrip("=") +) RAW_INPUT_FILE_ID = "file-raw-provider-abc123" DECODED_UNIFIED_BATCH_ID = "litellm_proxy;model_id:model-xyz;llm_batch_id:batch-123" -B64_UNIFIED_BATCH_ID = base64.urlsafe_b64encode(DECODED_UNIFIED_BATCH_ID.encode()).decode().rstrip("=") +B64_UNIFIED_BATCH_ID = ( + base64.urlsafe_b64encode(DECODED_UNIFIED_BATCH_ID.encode()).decode().rstrip("=") +) @pytest.mark.asyncio @@ -55,10 +60,16 @@ async def test_should_resolve_raw_input_file_id_to_unified(): mock_managed_file.unified_file_id = B64_UNIFIED_INPUT_FILE_ID mock_prisma = MagicMock() - mock_prisma.db.litellm_managedobjecttable.find_first = AsyncMock(return_value=mock_db_object) - mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock(return_value=mock_managed_file) + mock_prisma.db.litellm_managedobjecttable.find_first = AsyncMock( + return_value=mock_db_object + ) + mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock( + return_value=mock_managed_file + ) - from litellm.proxy.openai_files_endpoints.common_utils import get_batch_from_database + from litellm.proxy.openai_files_endpoints.common_utils import ( + get_batch_from_database, + ) _, response = await get_batch_from_database( batch_id=B64_UNIFIED_BATCH_ID, diff --git a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py index 420f5f9789c..c80b8a848ca 100644 --- a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py +++ b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py @@ -93,7 +93,9 @@ async def test_should_preserve_already_managed_input_file_id(): unified_batch_id = "bGl0ZWxsbV9wcm94eTpiYXRjaF9pZA" decoded_unified = "litellm_proxy:application/octet-stream;unified_id,test-123" - base64_input_file_id = base64.urlsafe_b64encode(decoded_unified.encode()).decode().rstrip("=") + base64_input_file_id = ( + base64.urlsafe_b64encode(decoded_unified.encode()).decode().rstrip("=") + ) batch_data = { "id": "batch-raw-123", diff --git a/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py b/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py index a9bf33a21ac..34c8c9c5c5d 100644 --- a/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py +++ b/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py @@ -14,63 +14,77 @@ import pytest def test_enterprise_routes_all_imports_exist(): """ Validate that all relative imports in enterprise_routes.py exist in the filesystem. - + This catches any import errors from moved/deleted modules without hardcoding specific module names. Works by checking that imported files actually exist. """ # Path to the enterprise_routes.py source file enterprise_routes_path = os.path.join( os.path.dirname(__file__), - "..", "..", "..", "..", - "enterprise", "litellm_enterprise", "proxy", "enterprise_routes.py" + "..", + "..", + "..", + "..", + "enterprise", + "litellm_enterprise", + "proxy", + "enterprise_routes.py", ) - + enterprise_routes_path = os.path.normpath(enterprise_routes_path) enterprise_proxy_dir = os.path.dirname(enterprise_routes_path) - + if not os.path.exists(enterprise_routes_path): pytest.skip(f"Enterprise routes file not found at {enterprise_routes_path}") - + # Read and parse the source file with open(enterprise_routes_path, "r") as f: source_code = f.read() - + try: tree = ast.parse(source_code) except SyntaxError as e: pytest.fail(f"Syntax error in enterprise_routes.py: {e}") - + # Check all relative imports missing_imports = [] - + for node in ast.walk(tree): if isinstance(node, ast.ImportFrom): # level > 0 means it's a relative import (. or .. etc) if node.level and node.level > 0: module = node.module or "" - + # Convert relative import to file path # e.g., "audit_logging_endpoints" -> "audit_logging_endpoints.py" # e.g., "vector_stores.endpoints" -> "vector_stores/endpoints.py" module_path = module.replace(".", os.sep) if module else "" - + # Check both .py file and package directory - file_path = os.path.join(enterprise_proxy_dir, module_path + ".py") if module_path else None - package_path = os.path.join(enterprise_proxy_dir, module_path, "__init__.py") if module_path else None - + file_path = ( + os.path.join(enterprise_proxy_dir, module_path + ".py") + if module_path + else None + ) + package_path = ( + os.path.join(enterprise_proxy_dir, module_path, "__init__.py") + if module_path + else None + ) + # If module is empty (e.g., "from . import something"), skip check if not module: continue - + file_exists = file_path and os.path.exists(file_path) package_exists = package_path and os.path.exists(package_path) - + if not file_exists and not package_exists: missing_imports.append( f"Line {node.lineno}: Cannot find '.{module}' " f"(checked: {file_path} and {package_path})" ) - + if missing_imports: error_msg = "Found imports in enterprise_routes.py that don't exist:\n" error_msg += "\n".join(missing_imports) diff --git a/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py b/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py index 852077dcf0c..3c7aace7d31 100644 --- a/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py +++ b/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py @@ -61,7 +61,7 @@ def _make_managed_files_instance_with_batches( ): """ Create a _PROXY_LiteLLMManagedFiles instance with mocked DB and batches. - + Args: file_id: The unified file ID batches: List of batch records to return from DB @@ -79,7 +79,7 @@ def _make_managed_files_instance_with_batches( # Mock prisma mock_prisma = MagicMock() - + # Mock file table queries mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock( return_value=mock_file_record @@ -87,7 +87,7 @@ def _make_managed_files_instance_with_batches( mock_prisma.db.litellm_managedfiletable.delete = AsyncMock( return_value=mock_file_record ) - + # Mock batch/object table queries mock_prisma.db.litellm_managedobjecttable.find_many = AsyncMock( return_value=batches @@ -95,11 +95,13 @@ def _make_managed_files_instance_with_batches( # Mock cache mock_cache = MagicMock() - mock_cache.async_get_cache = AsyncMock(return_value={ - "unified_file_id": file_id, - "model_mappings": {"model-123": "provider-file-abc"}, - "flat_model_file_ids": ["provider-file-abc"], - }) + mock_cache.async_get_cache = AsyncMock( + return_value={ + "unified_file_id": file_id, + "model_mappings": {"model-123": "provider-file-abc"}, + "flat_model_file_ids": ["provider-file-abc"], + } + ) mock_cache.async_set_cache = AsyncMock() instance = _PROXY_LiteLLMManagedFiles( @@ -117,17 +119,17 @@ def test_is_batch_polling_enabled_when_job_registered(): from litellm_enterprise.proxy.hooks.managed_files import ( _PROXY_LiteLLMManagedFiles, ) - + instance = _PROXY_LiteLLMManagedFiles( internal_usage_cache=MagicMock(), prisma_client=MagicMock(), ) - + # Mock scheduler with registered job mock_scheduler = MagicMock() mock_job = MagicMock() mock_scheduler.get_job.return_value = mock_job - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): assert instance._is_batch_polling_enabled() is True @@ -137,16 +139,16 @@ def test_is_batch_polling_disabled_when_job_not_registered(): from litellm_enterprise.proxy.hooks.managed_files import ( _PROXY_LiteLLMManagedFiles, ) - + instance = _PROXY_LiteLLMManagedFiles( internal_usage_cache=MagicMock(), prisma_client=MagicMock(), ) - + # Mock scheduler without registered job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = None - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): assert instance._is_batch_polling_enabled() is False @@ -156,12 +158,12 @@ def test_is_batch_polling_disabled_when_no_scheduler(): from litellm_enterprise.proxy.hooks.managed_files import ( _PROXY_LiteLLMManagedFiles, ) - + instance = _PROXY_LiteLLMManagedFiles( internal_usage_cache=MagicMock(), prisma_client=MagicMock(), ) - + with patch("litellm.proxy.proxy_server.scheduler", None): assert instance._is_batch_polling_enabled() is False @@ -174,26 +176,28 @@ async def test_get_batches_referencing_file_finds_batch_with_input_file(): """Test finding a batch that references the file as input_file_id.""" unified_file_id = _make_unified_file_id("file-input-123") unified_batch_id = _make_unified_batch_id("batch-123") - + batch_file_object = { "id": "batch-123", "input_file_id": unified_file_id, # Batch references this file "status": "validating", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="validating", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - - referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) - + + referencing_batches = await managed_files._get_batches_referencing_file( + unified_file_id + ) + assert len(referencing_batches) == 1 assert referencing_batches[0]["batch_id"] == unified_batch_id assert referencing_batches[0]["status"] == "validating" @@ -204,27 +208,29 @@ async def test_get_batches_referencing_file_finds_batch_with_output_file(): """Test finding a batch that references the file as output_file_id.""" unified_file_id = _make_unified_file_id("file-output-456") unified_batch_id = _make_unified_batch_id("batch-456") - + batch_file_object = { "id": "batch-456", "input_file_id": "file-input-different", "output_file_id": unified_file_id, # Batch references this file "status": "in_progress", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="in_progress", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - - referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) - + + referencing_batches = await managed_files._get_batches_referencing_file( + unified_file_id + ) + assert len(referencing_batches) == 1 assert referencing_batches[0]["status"] == "in_progress" @@ -234,27 +240,29 @@ async def test_get_batches_referencing_file_ignores_terminal_batches(): """Test that batches in terminal states are not returned.""" unified_file_id = _make_unified_file_id("file-123") unified_batch_id = _make_unified_batch_id("batch-completed") - + batch_file_object = { "id": "batch-completed", "input_file_id": unified_file_id, "status": "completed", } - + # Batch is in terminal state in DB batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="completed", # Terminal state file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[], # Query returns no batches (terminal states filtered out) ) - - referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) - + + referencing_batches = await managed_files._get_batches_referencing_file( + unified_file_id + ) + assert len(referencing_batches) == 0 @@ -262,26 +270,36 @@ async def test_get_batches_referencing_file_ignores_terminal_batches(): async def test_get_batches_referencing_file_finds_multiple_batches(): """Test finding multiple batches referencing the same file.""" unified_file_id = _make_unified_file_id("file-shared") - + batch1 = _make_batch_db_record( unified_object_id=_make_unified_batch_id("batch-1"), status="validating", - file_object={"id": "batch-1", "input_file_id": unified_file_id, "status": "validating"}, + file_object={ + "id": "batch-1", + "input_file_id": unified_file_id, + "status": "validating", + }, ) - + batch2 = _make_batch_db_record( unified_object_id=_make_unified_batch_id("batch-2"), status="in_progress", - file_object={"id": "batch-2", "input_file_id": unified_file_id, "status": "in_progress"}, + file_object={ + "id": "batch-2", + "input_file_id": unified_file_id, + "status": "in_progress", + }, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch1, batch2], ) - - referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) - + + referencing_batches = await managed_files._get_batches_referencing_file( + unified_file_id + ) + assert len(referencing_batches) == 2 statuses = [b["status"] for b in referencing_batches] assert "validating" in statuses @@ -300,32 +318,32 @@ async def test_file_deletion_blocked_when_batch_polling_enabled_and_batch_refere """ unified_file_id = _make_unified_file_id("file-to-delete") unified_batch_id = _make_unified_batch_id("batch-active") - + batch_file_object = { "id": "batch-active", "input_file_id": unified_file_id, "status": "validating", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="validating", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - + # Mock scheduler with registered batch cost job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() # Job exists - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files._check_file_deletion_allowed(unified_file_id) - + assert exc_info.value.status_code == 400 error_detail = exc_info.value.detail assert "Cannot delete file" in error_detail @@ -342,28 +360,28 @@ async def test_file_deletion_allowed_when_batch_polling_disabled(): """ unified_file_id = _make_unified_file_id("file-to-delete") unified_batch_id = _make_unified_batch_id("batch-active") - + batch_file_object = { "id": "batch-active", "input_file_id": unified_file_id, "status": "validating", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="validating", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - + # Mock scheduler without registered job (batch cost tracking disabled) mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = None - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): # Should not raise an exception await managed_files._check_file_deletion_allowed(unified_file_id) @@ -376,16 +394,16 @@ async def test_file_deletion_allowed_when_no_batches_reference_file(): even when batch cost tracking is enabled. """ unified_file_id = _make_unified_file_id("file-to-delete") - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[], # No batches reference this file ) - + # Mock scheduler with registered job (batch cost tracking enabled) mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): # Should not raise an exception await managed_files._check_file_deletion_allowed(unified_file_id) @@ -398,32 +416,32 @@ async def test_afile_delete_calls_check_deletion_allowed(): """ unified_file_id = _make_unified_file_id("file-to-delete") unified_batch_id = _make_unified_batch_id("batch-active") - + batch_file_object = { "id": "batch-active", "input_file_id": unified_file_id, "status": "in_progress", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="in_progress", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - + # Mock llm_router mock_router = MagicMock() mock_router.afile_delete = AsyncMock() - + # Mock scheduler with registered job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files.afile_delete( @@ -431,7 +449,7 @@ async def test_afile_delete_calls_check_deletion_allowed(): litellm_parent_otel_span=None, llm_router=mock_router, ) - + # Should raise error before calling router delete assert exc_info.value.status_code == 400 mock_router.afile_delete.assert_not_called() @@ -444,7 +462,7 @@ async def test_database_limit_respected(): This is a performance optimization - we only fetch what we need. """ unified_file_id = _make_unified_file_id("file-shared") - + # Create exactly 10 batches (what DB will return with take=10) ten_batches = [] for i in range(10): @@ -454,30 +472,32 @@ async def test_database_limit_respected(): file_object={ "id": f"batch-{i}", "input_file_id": unified_file_id, - "status": "validating" + "status": "validating", }, ) ten_batches.append(batch) - + # Mock will return only 10 batches (as DB would with take=10) managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=ten_batches, ) - - referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) - + + referencing_batches = await managed_files._get_batches_referencing_file( + unified_file_id + ) + # Should return all 10 that reference the file assert len(referencing_batches) == 10 - + # Verify error message handles "10+" case (since we got exactly 10, might be more in DB) mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files._check_file_deletion_allowed(unified_file_id) - + error_detail = exc_info.value.detail # When we get exactly 10 matches, show "10+" to indicate there might be more assert "10+ batch(es)" in error_detail @@ -491,32 +511,40 @@ async def test_error_message_includes_batch_details(): unified_file_id = _make_unified_file_id("file-to-delete") batch1_id = _make_unified_batch_id("batch-1") batch2_id = _make_unified_batch_id("batch-2") - + batch1 = _make_batch_db_record( unified_object_id=batch1_id, status="validating", - file_object={"id": "batch-1", "input_file_id": unified_file_id, "status": "validating"}, + file_object={ + "id": "batch-1", + "input_file_id": unified_file_id, + "status": "validating", + }, ) - + batch2 = _make_batch_db_record( unified_object_id=batch2_id, status="in_progress", - file_object={"id": "batch-2", "output_file_id": unified_file_id, "status": "in_progress"}, + file_object={ + "id": "batch-2", + "output_file_id": unified_file_id, + "status": "in_progress", + }, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch1, batch2], ) - + # Mock scheduler with registered job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files._check_file_deletion_allowed(unified_file_id) - + error_detail = exc_info.value.detail assert "2 batch(es)" in error_detail assert "validating" in error_detail diff --git a/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py b/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py index 9526304aff0..d4855d5b130 100644 --- a/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py +++ b/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py @@ -110,10 +110,9 @@ async def test_should_pass_credentials_to_afile_retrieve(): mock_afile_retrieve = AsyncMock(return_value=_make_file_object("file-output-abc")) - with patch( - "litellm.afile_retrieve", mock_afile_retrieve - ), patch( - "litellm.proxy.proxy_server.llm_router", mock_router + with ( + patch("litellm.afile_retrieve", mock_afile_retrieve), + patch("litellm.proxy.proxy_server.llm_router", mock_router), ): await managed_files.async_post_call_success_hook( data={}, @@ -128,7 +127,9 @@ async def test_should_pass_credentials_to_afile_retrieve(): f"afile_retrieve must receive api_key from router credentials. " f"Got kwargs: {call_kwargs.kwargs}" ) - assert call_kwargs.kwargs.get("api_base") == "https://my-azure.openai.azure.com/", ( + assert ( + call_kwargs.kwargs.get("api_base") == "https://my-azure.openai.azure.com/" + ), ( f"afile_retrieve must receive api_base from router credentials. " f"Got kwargs: {call_kwargs.kwargs}" ) @@ -150,10 +151,9 @@ async def test_should_fallback_when_no_router(): mock_afile_retrieve = AsyncMock(return_value=_make_file_object("file-output-abc")) - with patch( - "litellm.afile_retrieve", mock_afile_retrieve - ), patch( - "litellm.proxy.proxy_server.llm_router", None + with ( + patch("litellm.afile_retrieve", mock_afile_retrieve), + patch("litellm.proxy.proxy_server.llm_router", None), ): await managed_files.async_post_call_success_hook( data={}, diff --git a/tests/test_litellm/google_genai/test_google_genai_adapter.py b/tests/test_litellm/google_genai/test_google_genai_adapter.py index f21564546a8..90ef89699ea 100644 --- a/tests/test_litellm/google_genai/test_google_genai_adapter.py +++ b/tests/test_litellm/google_genai/test_google_genai_adapter.py @@ -2,6 +2,7 @@ """ Test to verify the Google GenAI generate_content adapter functionality """ + import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py b/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py index da56b094d95..8fc3eca9adf 100644 --- a/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py +++ b/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py @@ -2,6 +2,7 @@ """ Test to verify the Google GenAI adapter fixes """ + import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_handler.py b/tests/test_litellm/google_genai/test_google_genai_handler.py index 0dc218d297b..da1c0bb611b 100644 --- a/tests/test_litellm/google_genai/test_google_genai_handler.py +++ b/tests/test_litellm/google_genai/test_google_genai_handler.py @@ -2,6 +2,7 @@ """ Test to verify the Google GenAI generate_content handler functionality """ + import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_main.py b/tests/test_litellm/google_genai/test_google_genai_main.py index 5854e4b55af..1801ee10f30 100644 --- a/tests/test_litellm/google_genai/test_google_genai_main.py +++ b/tests/test_litellm/google_genai/test_google_genai_main.py @@ -2,6 +2,7 @@ """ Test to verify the Google GenAI generate_content adapter functionality """ + import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_transformation.py b/tests/test_litellm/google_genai/test_google_genai_transformation.py index 908a68110fd..691e5472997 100644 --- a/tests/test_litellm/google_genai/test_google_genai_transformation.py +++ b/tests/test_litellm/google_genai/test_google_genai_transformation.py @@ -2,6 +2,7 @@ """ Test to verify the Google GenAI transformation logic for generateContent parameters """ + import os import sys diff --git a/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py b/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py index fdf56aedbc9..0767b83e8eb 100644 --- a/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py +++ b/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py @@ -20,7 +20,6 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanE from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py b/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py index d849582b3c4..78f5259fc58 100644 --- a/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py +++ b/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py @@ -127,16 +127,14 @@ def test_input_validation(): # Create a temporary directory with a test prompt with tempfile.TemporaryDirectory() as temp_dir: prompt_file = Path(temp_dir) / "test_validation.prompt" - prompt_file.write_text( - """--- + prompt_file.write_text("""--- input: schema: name: string age: integer active: boolean --- -Hello {{name}}, you are {{age}} years old and {'active' if active else 'inactive'}.""" - ) +Hello {{name}}, you are {{age}} years old and {'active' if active else 'inactive'}.""") manager = PromptManager(prompt_directory=str(temp_dir)) @@ -248,16 +246,14 @@ def test_frontmatter_parsing(): with tempfile.TemporaryDirectory() as temp_dir: # Test with frontmatter prompt_with_frontmatter = Path(temp_dir) / "with_frontmatter.prompt" - prompt_with_frontmatter.write_text( - """--- + prompt_with_frontmatter.write_text("""--- model: gpt-4 temperature: 0.8 input: schema: topic: string --- -Write about {{topic}}.""" - ) +Write about {{topic}}.""") # Test without frontmatter prompt_without_frontmatter = Path(temp_dir) / "without_frontmatter.prompt" @@ -370,8 +366,7 @@ def test_prompt_file_to_json_conversion(): # Create a temporary prompt file with frontmatter with tempfile.TemporaryDirectory() as temp_dir: prompt_file = Path(temp_dir) / "test_conversion.prompt" - prompt_file.write_text( - """--- + prompt_file.write_text("""--- model: gpt-4 temperature: 0.7 max_tokens: 200 @@ -384,8 +379,7 @@ output: --- You are an AI assistant. Given the context: {{context}} -Please respond to: {{user_input}}""" - ) +Please respond to: {{user_input}}""") manager = PromptManager() json_data = manager.prompt_file_to_json(prompt_file) diff --git a/tests/test_litellm/integrations/test_custom_guardrail.py b/tests/test_litellm/integrations/test_custom_guardrail.py index d09c4ac2c38..ff6c87132a6 100644 --- a/tests/test_litellm/integrations/test_custom_guardrail.py +++ b/tests/test_litellm/integrations/test_custom_guardrail.py @@ -1086,9 +1086,12 @@ class TestCustomGuardrailSpendLogMatchRedaction: ][0]["match"] == "[REDACTED]" ) - assert raw["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ - "match" - ] == "GG" + assert ( + raw["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ + "match" + ] + == "GG" + ) def test_add_standard_logging_redacts_regex_field(self): cg = CustomGuardrail(guardrail_name="test-rail") diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py index a939951c430..32de9a85cfe 100644 --- a/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py @@ -18,7 +18,6 @@ from litellm.integrations.websearch_interception.handler import ( WebSearchInterceptionLogger, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py index e8bf54f7ffc..3b99167c034 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py @@ -3,7 +3,7 @@ Test Azure OpenAI Assistant Features Cost Tracking Tests cost calculation for Azure's new assistant features: - File Search (storage-based pricing) -- Code Interpreter (session-based pricing) +- Code Interpreter (session-based pricing) - Computer Use (token-based pricing) - Vector Store (storage-based pricing) """ diff --git a/tests/test_litellm/litellm_core_utils/test_core_helpers.py b/tests/test_litellm/litellm_core_utils/test_core_helpers.py index b67ea91bb0b..fc3308d6e36 100644 --- a/tests/test_litellm/litellm_core_utils/test_core_helpers.py +++ b/tests/test_litellm/litellm_core_utils/test_core_helpers.py @@ -176,7 +176,11 @@ class TestRedactNestedMatchAndRegexKeys: { "sensitiveInformationPolicy": { "piiEntities": [ - {"type": "NAME", "match": "secret-name", "action": "BLOCKED"} + { + "type": "NAME", + "match": "secret-name", + "action": "BLOCKED", + } ] }, "wordPolicy": { @@ -187,16 +191,22 @@ class TestRedactNestedMatchAndRegexKeys: "regex": "should-redact-key-named-regex", } out = redact_nested_match_and_regex_keys(payload) - assert out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ - "match" - ] == "[REDACTED]" + assert ( + out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ + "match" + ] + == "[REDACTED]" + ) assert out["assessments"][0]["wordPolicy"]["customWords"][0]["match"] == ( "[REDACTED]" ) assert out["regex"] == "[REDACTED]" - assert payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][ - 0 - ]["match"] == "secret-name" + assert ( + payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ + "match" + ] + == "secret-name" + ) def test_passes_through_none_and_str(self): assert redact_nested_match_and_regex_keys(None) is None diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py index 49d3c51e340..fe6312e5725 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py @@ -2036,23 +2036,19 @@ async def test_azure_streaming_role_preserved_with_include_usage(sync_mode: bool chunks.append(chunk) # The prompt_filter chunk should be forwarded with choices=[] - assert len(chunks[0].choices) == 0, ( - f"Expected prompt_filter chunk with choices=[], got {len(chunks[0].choices)} choices" - ) + assert ( + len(chunks[0].choices) == 0 + ), f"Expected prompt_filter chunk with choices=[], got {len(chunks[0].choices)} choices" # At least one chunk must have role='assistant' in its delta has_role = any( - len(c.choices) > 0 - and getattr(c.choices[0].delta, "role", None) == "assistant" + len(c.choices) > 0 and getattr(c.choices[0].delta, "role", None) == "assistant" for c in chunks ) assert has_role, ( "No chunk contained role='assistant' in delta (issue #24221). " "Chunk deltas: " - + str([ - c.choices[0].delta if c.choices else "no choices" - for c in chunks - ]) + + str([c.choices[0].delta if c.choices else "no choices" for c in chunks]) ) diff --git a/tests/test_litellm/litellm_core_utils/test_token_counter.py b/tests/test_litellm/litellm_core_utils/test_token_counter.py index 3aa5f012467..fa7c9e9aeab 100644 --- a/tests/test_litellm/litellm_core_utils/test_token_counter.py +++ b/tests/test_litellm/litellm_core_utils/test_token_counter.py @@ -493,7 +493,6 @@ from unittest.mock import MagicMock, patch from litellm.utils import _select_tokenizer_helper, claude_json_str, encoding - # Clear the cache at module load to ensure clean state _select_tokenizer_helper.cache_clear() diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 42efde90926..3fc0dea4748 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -2327,7 +2327,8 @@ class TestAnthropicStreamWrapperToolArgs: def _find_tool_deltas(self, events): return [ - e for e in events + e + for e in events if isinstance(e, dict) and e.get("type") == "content_block_delta" and isinstance(e.get("delta"), dict) @@ -2375,7 +2376,6 @@ class TestAnthropicStreamWrapperToolArgs: assert parsed == {"city": "Tokyo"} - def test_translate_anthropic_tool_choice_none(): """ Regression test for issue #24443. diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py index b9bda07336f..d88260629ee 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py @@ -22,7 +22,6 @@ from litellm.llms.anthropic.experimental_pass_through.messages.agentic_streaming _parse_sse_events, ) - # --------------------------------------------------------------------------- # Helpers to build SSE byte payloads # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py index 1d25d719384..c2938c5b209 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py @@ -2,7 +2,6 @@ import os import sys from typing import List - sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterator import ( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py index 07c0012b04d..dd5d1341713 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py @@ -31,13 +31,16 @@ def _call_handler_and_capture_optional_params(thinking=None, **extra_kwargs): """ captured = {} - with patch( - "litellm.llms.anthropic.experimental_pass_through.messages.handler." - "base_llm_http_handler" - ) as mock_handler, patch( - "litellm.llms.anthropic.experimental_pass_through.messages.handler." - "ProviderConfigManager" - ) as mock_pcm: + with ( + patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler." + "base_llm_http_handler" + ) as mock_handler, + patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler." + "ProviderConfigManager" + ) as mock_pcm, + ): # Make get_provider_anthropic_messages_config return a non-None config # so the handler takes the native Anthropic path mock_pcm.get_provider_anthropic_messages_config.return_value = MagicMock() @@ -119,8 +122,9 @@ class TestReasoningAutoSummaryMessages: def test_env_var_enables_auto_summary(self): """LITELLM_REASONING_AUTO_SUMMARY=true env var enables the feature.""" - with patch.object(litellm, "reasoning_auto_summary", False), patch.dict( - os.environ, {"LITELLM_REASONING_AUTO_SUMMARY": "true"} + with ( + patch.object(litellm, "reasoning_auto_summary", False), + patch.dict(os.environ, {"LITELLM_REASONING_AUTO_SUMMARY": "true"}), ): params = _call_handler_and_capture_optional_params( thinking={"type": "adaptive", "budget_tokens": 5000} diff --git a/tests/test_litellm/llms/azure/test_azure_cost_calculation.py b/tests/test_litellm/llms/azure/test_azure_cost_calculation.py index 53c91032b34..89fff013360 100644 --- a/tests/test_litellm/llms/azure/test_azure_cost_calculation.py +++ b/tests/test_litellm/llms/azure/test_azure_cost_calculation.py @@ -8,7 +8,6 @@ import litellm from litellm.llms.azure.cost_calculation import cost_per_token from litellm.types.utils import Usage - # Register a test model with tier-specific pricing TEST_MODEL = "test-azure-gpt-4.1" TEST_MODEL_COST = { diff --git a/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py b/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py index 20260c744f8..b9316fb9af1 100644 --- a/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py +++ b/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py @@ -459,18 +459,21 @@ class TestAzureAIServiceTierCostCalculation: @pytest.fixture(autouse=True) def register_test_model(self): import litellm - litellm.register_model(model_cost={ - "test-azure-ai-model": { - "input_cost_per_token": 0.001, - "output_cost_per_token": 0.002, - "input_cost_per_token_priority": 0.01, - "output_cost_per_token_priority": 0.02, - "input_cost_per_token_flex": 0.0005, - "output_cost_per_token_flex": 0.001, - "litellm_provider": "azure_ai", - "max_tokens": 8192, + + litellm.register_model( + model_cost={ + "test-azure-ai-model": { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "input_cost_per_token_priority": 0.01, + "output_cost_per_token_priority": 0.02, + "input_cost_per_token_flex": 0.0005, + "output_cost_per_token_flex": 0.001, + "litellm_provider": "azure_ai", + "max_tokens": 8192, + } } - }) + ) def test_service_tier_priority_higher_cost(self): """Priority tier should cost more than standard for azure_ai.""" diff --git a/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py b/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py index b5fcd9d8219..1efd10e921b 100644 --- a/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py +++ b/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py @@ -10,7 +10,6 @@ from litellm.llms.base_llm.managed_resources.isolation import ( ) from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth - # --------------------------------------------------------------------------- # build_owner_filter # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py index a415d550215..100f91c4e5a 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py +++ b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py @@ -1,7 +1,6 @@ import os import sys - sys.path.insert( 0, os.path.abspath("../../../../..") ) # Adds the parent directory to the system path diff --git a/tests/test_litellm/llms/crusoe/test_crusoe.py b/tests/test_litellm/llms/crusoe/test_crusoe.py index 0a05126919a..32a2f174f3d 100644 --- a/tests/test_litellm/llms/crusoe/test_crusoe.py +++ b/tests/test_litellm/llms/crusoe/test_crusoe.py @@ -39,7 +39,10 @@ def test_crusoe_dynamic_config_env_vars(): with patch.dict( os.environ, - {"CRUSOE_API_KEY": "test-key", "CRUSOE_API_BASE": "https://custom.crusoe.com/v1"}, + { + "CRUSOE_API_KEY": "test-key", + "CRUSOE_API_BASE": "https://custom.crusoe.com/v1", + }, ): api_base, api_key = config._get_openai_compatible_provider_info(None, None) @@ -69,7 +72,9 @@ def test_crusoe_supported_params(): from litellm.llms.openai_like.json_loader import JSONProviderRegistry config = create_config_class(JSONProviderRegistry.get("crusoe"))() - params = config.get_supported_openai_params(model="meta-llama/Llama-3.3-70B-Instruct") + params = config.get_supported_openai_params( + model="meta-llama/Llama-3.3-70B-Instruct" + ) assert isinstance(params, list) assert len(params) > 0 @@ -91,7 +96,9 @@ def test_crusoe_param_mapping_max_completion_tokens(): drop_params=False, ) - assert "max_tokens" in optional_params, "max_completion_tokens should be mapped to max_tokens" + assert ( + "max_tokens" in optional_params + ), "max_completion_tokens should be mapped to max_tokens" assert optional_params["max_tokens"] == 1024 assert "max_completion_tokens" not in optional_params diff --git a/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py b/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py index 5515e6ce815..660ab88c2b2 100644 --- a/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py +++ b/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py @@ -158,4 +158,7 @@ def test_socket_factory_uses_tcp_keepalive_when_keepidle_unavailable(monkeypatch assert ( setsockopt_calls[(socket.IPPROTO_TCP, fake_socket_module.TCP_KEEPALIVE)] == 60 ) - assert (socket.IPPROTO_TCP, getattr(socket, "TCP_KEEPIDLE", -1)) not in setsockopt_calls + assert ( + socket.IPPROTO_TCP, + getattr(socket, "TCP_KEEPIDLE", -1), + ) not in setsockopt_calls diff --git a/tests/test_litellm/llms/custom_httpx/test_mock_transport.py b/tests/test_litellm/llms/custom_httpx/test_mock_transport.py index c2d4e146428..b3b5f82f02f 100644 --- a/tests/test_litellm/llms/custom_httpx/test_mock_transport.py +++ b/tests/test_litellm/llms/custom_httpx/test_mock_transport.py @@ -10,7 +10,6 @@ import pytest from litellm.llms.custom_httpx.mock_transport import MockOpenAITransport - # --------------------------------------------------------------------------- # Non-streaming # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/databricks/test_databricks_e2e.py b/tests/test_litellm/llms/databricks/test_databricks_e2e.py index 669f9e94639..90846f28920 100644 --- a/tests/test_litellm/llms/databricks/test_databricks_e2e.py +++ b/tests/test_litellm/llms/databricks/test_databricks_e2e.py @@ -17,13 +17,13 @@ Purpose: LiteLLM Integration Tests: This test file includes tests for different ways of calling Databricks via LiteLLM: - + 1. LiteLLM SDK Direct - Using litellm.completion() with user_agent parameter 2. LangChain + LiteLLM - Using ChatLiteLLM wrapper (requires langchain-community) 3. LiteLLM Async - Using litellm.acompletion() async API 4. LiteLLM Streaming - Using litellm.completion() with stream=True 5. LiteLLM Embedding - Using litellm.embedding() with user_agent parameter - + All tests use the CUSTOM_USER_AGENT value from the config file and call Databricks endpoints through LiteLLM's unified interface. @@ -31,7 +31,7 @@ Prerequisites: - Valid Databricks workspace access - Configured credentials (OAuth Service Principal, PAT, or Databricks CLI) - Access to serving endpoints (e.g., databricks-gpt-oss-120b) - + Optional Dependencies (for LiteLLM integration tests): - pip install langchain-litellm # For LangChain tests (recommended) diff --git a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py index d59ab975ef2..a950b4a5f2c 100644 --- a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py @@ -53,7 +53,7 @@ def test_file(): ) def test_audio_file_handling(fixture_name, request): handler = DeepgramAudioTranscriptionConfig() - (audio_file, expected_output) = request.getfixturevalue(fixture_name) + audio_file, expected_output = request.getfixturevalue(fixture_name) result = handler.transform_audio_transcription_request( model="deepseek-audio-transcription", audio_file=audio_file, diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index 6c846a90c71..7c787f20b8b 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -255,12 +255,19 @@ class TestGitHubCopilotAuthenticator: "user_code": "UC", "verification_uri": "https://example.com", } - with patch.dict(os.environ, {"GITHUB_COPILOT_DEVICE_CODE_URL": custom_url}), \ - patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client): + with ( + patch.dict(os.environ, {"GITHUB_COPILOT_DEVICE_CODE_URL": custom_url}), + patch( + "litellm.llms.github_copilot.authenticator._get_httpx_client", + return_value=mock_client, + ), + ): authenticator._get_device_code() assert mock_client.post.call_args[0][0] == custom_url - def test_get_device_code_with_custom_client_id(self, authenticator, mock_http_client): + def test_get_device_code_with_custom_client_id( + self, authenticator, mock_http_client + ): """GITHUB_COPILOT_CLIENT_ID env var must appear as client_id in the device-code request body.""" mock_client, mock_response = mock_http_client custom_id = "custom_client_id" @@ -269,30 +276,49 @@ class TestGitHubCopilotAuthenticator: "user_code": "UC", "verification_uri": "https://example.com", } - with patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), \ - patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client): + with ( + patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), + patch( + "litellm.llms.github_copilot.authenticator._get_httpx_client", + return_value=mock_client, + ), + ): authenticator._get_device_code() assert mock_client.post.call_args[1]["json"]["client_id"] == custom_id - def test_poll_for_access_token_with_custom_url(self, authenticator, mock_http_client): + def test_poll_for_access_token_with_custom_url( + self, authenticator, mock_http_client + ): """GITHUB_COPILOT_ACCESS_TOKEN_URL env var must be used by _poll_for_access_token at call time.""" mock_client, mock_response = mock_http_client custom_url = "https://custom.example.com/token" mock_response.json.return_value = {"access_token": "tok"} - with patch.dict(os.environ, {"GITHUB_COPILOT_ACCESS_TOKEN_URL": custom_url}), \ - patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ - patch("time.sleep"): + with ( + patch.dict(os.environ, {"GITHUB_COPILOT_ACCESS_TOKEN_URL": custom_url}), + patch( + "litellm.llms.github_copilot.authenticator._get_httpx_client", + return_value=mock_client, + ), + patch("time.sleep"), + ): authenticator._poll_for_access_token("dc") assert mock_client.post.call_args[0][0] == custom_url - def test_poll_for_access_token_with_custom_client_id(self, authenticator, mock_http_client): + def test_poll_for_access_token_with_custom_client_id( + self, authenticator, mock_http_client + ): """GITHUB_COPILOT_CLIENT_ID env var must appear as client_id in the polling request body.""" mock_client, mock_response = mock_http_client custom_id = "custom_client_id" mock_response.json.return_value = {"access_token": "tok"} - with patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), \ - patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ - patch("time.sleep"): + with ( + patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), + patch( + "litellm.llms.github_copilot.authenticator._get_httpx_client", + return_value=mock_client, + ), + patch("time.sleep"), + ): authenticator._poll_for_access_token("dc") assert mock_client.post.call_args[1]["json"]["client_id"] == custom_id @@ -301,9 +327,13 @@ class TestGitHubCopilotAuthenticator: mock_client, mock_response = mock_http_client custom_url = "https://custom.example.com/api-key" mock_response.json.return_value = {"token": "api-tok", "expires_at": 9999999999} - with patch.dict(os.environ, {"GITHUB_COPILOT_API_KEY_URL": custom_url}), \ - patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ - patch.object(authenticator, "get_access_token", return_value="access-tok"): + with ( + patch.dict(os.environ, {"GITHUB_COPILOT_API_KEY_URL": custom_url}), + patch( + "litellm.llms.github_copilot.authenticator._get_httpx_client", + return_value=mock_client, + ), + patch.object(authenticator, "get_access_token", return_value="access-tok"), + ): authenticator._refresh_api_key() assert mock_client.get.call_args[0][0] == custom_url - diff --git a/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py b/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py index 560796ea58d..e6acc51a585 100644 --- a/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py +++ b/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py @@ -11,7 +11,6 @@ sys.path.insert( import litellm import pytest - MOCK_EMBEDDING_RESPONSE = [[0.1, 0.2, 0.3, 0.4, 0.5]] diff --git a/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py b/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py index b4744a7ed18..e398699bfdd 100644 --- a/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py +++ b/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py @@ -666,7 +666,9 @@ class TestKimiK26ModelRegistry: def test_kimi_k26_in_model_cost_map(self, model_cost_map): """kimi-k2.6 should be present in the model cost map.""" - assert "moonshot/kimi-k2.6" in model_cost_map, "moonshot/kimi-k2.6 not found in model_cost" + assert ( + "moonshot/kimi-k2.6" in model_cost_map + ), "moonshot/kimi-k2.6 not found in model_cost" def test_kimi_k26_pricing(self, model_cost_map): """kimi-k2.6 pricing should match official Kimi API rates.""" diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py index d408f55c004..c6971cfd556 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py @@ -1,7 +1,7 @@ """ Test file for Perplexity cost calculator functionality. -Tests the cost calculation for Perplexity models including citation tokens, +Tests the cost calculation for Perplexity models including citation tokens, search queries, and reasoning tokens. """ diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_integration.py b/tests/test_litellm/llms/perplexity/test_perplexity_integration.py index 1b03fd7df88..cd76fcc93c2 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_integration.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_integration.py @@ -1,7 +1,7 @@ """ Integration tests for Perplexity cost calculation and transformation. -Tests the end-to-end functionality of Perplexity cost calculation +Tests the end-to-end functionality of Perplexity cost calculation including integration with the main LiteLLM cost calculator. """ diff --git a/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py b/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py index 407e1d19fb3..c237e28081e 100644 --- a/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py @@ -10,7 +10,6 @@ from litellm.llms.scaleway.audio_transcription.transformation import ( ) from litellm.types.utils import TranscriptionResponse - # --------------------------------------------------------------------------- # get_complete_url # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/test_polling_url_origin_match.py b/tests/test_litellm/llms/test_polling_url_origin_match.py index f1f910bc731..b6b932ad937 100644 --- a/tests/test_litellm/llms/test_polling_url_origin_match.py +++ b/tests/test_litellm/llms/test_polling_url_origin_match.py @@ -15,7 +15,6 @@ from unittest.mock import MagicMock, patch import httpx import pytest - # Azure DALL-E sync + async paths route through ``assert_same_origin`` # the same way as the cases below. The helper itself is unit-tested in # ``tests/test_litellm/litellm_core_utils/test_url_utils.py``; the diff --git a/tests/test_litellm/llms/test_predibase_transformation.py b/tests/test_litellm/llms/test_predibase_transformation.py index 1600878a586..7d67996906a 100644 --- a/tests/test_litellm/llms/test_predibase_transformation.py +++ b/tests/test_litellm/llms/test_predibase_transformation.py @@ -233,7 +233,9 @@ def test_predibase_transform_response_non_dict_payload(): raw_response.headers = {} raw_response.json.return_value = [] - with pytest.raises(PredibaseError, match="'completion_response' is not a dictionary"): + with pytest.raises( + PredibaseError, match="'completion_response' is not a dictionary" + ): config.transform_response( model="predibase-model", raw_response=raw_response, @@ -375,7 +377,9 @@ def test_predibase_transform_response_best_of_invalid_value_falls_back(monkeypat assert result.choices[0].message.content == "primary-output" -def test_predibase_transform_response_empty_output_sets_completion_tokens_zero(monkeypatch): +def test_predibase_transform_response_empty_output_sets_completion_tokens_zero( + monkeypatch, +): config = PredibaseConfig() logging_obj = Mock() encoding = Mock() @@ -429,7 +433,10 @@ def test_predibase_transform_response_usage_fallbacks(monkeypatch): raw_response = httpx.Response( status_code=200, - json={"generated_text": "ok", "details": {"tokens": [], "finish_reason": "stop"}}, + json={ + "generated_text": "ok", + "details": {"tokens": [], "finish_reason": "stop"}, + }, ) result = config.transform_response( @@ -539,13 +546,17 @@ def test_predibase_completion_sync_returns_transform_response(monkeypatch): def fake_transform_response(self, **kwargs): return expected - monkeypatch.setattr(PredibaseConfig, "validate_environment", fake_validate_environment) + monkeypatch.setattr( + PredibaseConfig, "validate_environment", fake_validate_environment + ) monkeypatch.setattr(PredibaseConfig, "get_complete_url", fake_get_complete_url) monkeypatch.setattr(PredibaseConfig, "transform_request", fake_transform_request) monkeypatch.setattr(PredibaseConfig, "transform_response", fake_transform_response) monkeypatch.setattr( "litellm.module_level_client.post", - lambda *args, **kwargs: httpx.Response(status_code=200, json={"generated_text": "ok"}), + lambda *args, **kwargs: httpx.Response( + status_code=200, json={"generated_text": "ok"} + ), ) result = handler.completion( @@ -586,7 +597,9 @@ def test_predibase_completion_passes_existing_config_to_async_completion(monkeyp captured["async_kwargs"] = kwargs return "async-result" - monkeypatch.setattr(PredibaseConfig, "validate_environment", fake_validate_environment) + monkeypatch.setattr( + PredibaseConfig, "validate_environment", fake_validate_environment + ) monkeypatch.setattr(PredibaseConfig, "get_complete_url", fake_get_complete_url) monkeypatch.setattr(PredibaseConfig, "transform_request", fake_transform_request) monkeypatch.setattr(handler, "async_completion", fake_async_completion) diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py b/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py index 6d913ad5d1d..ef084dcc004 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py @@ -22,7 +22,6 @@ from litellm.llms.vertex_ai.gemini.transformation import ( ) from litellm.types.llms.vertex_ai import HttpxPartType - # --- Response extraction tests --- diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index 353d19b0198..fe97727bb89 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -3499,7 +3499,12 @@ def test_video_metadata_supported_for_all_gemini_models(): } ] - for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-pro-preview"]: + for model in [ + "gemini-1.5-pro", + "gemini-2.5-flash", + "gemini-2.5-pro", + "gemini-3-pro-preview", + ]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = None @@ -3509,19 +3514,25 @@ def test_video_metadata_supported_for_all_gemini_models(): break assert file_part is not None, f"{model}: file part should exist" - assert "video_metadata" in file_part, f"{model}: video_metadata should be present" + assert ( + "video_metadata" in file_part + ), f"{model}: video_metadata should be present" assert file_part["video_metadata"]["fps"] == 5, f"{model}: fps should be 5" # Per-part media_resolution is Gemini 3+ only; 2.x uses generation_config global for model in ["gemini-3-pro-preview"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = next(p for p in contents[0]["parts"] if "file_data" in p) - assert "media_resolution" in file_part, f"{model}: media_resolution should be present" + assert ( + "media_resolution" in file_part + ), f"{model}: media_resolution should be present" for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = next(p for p in contents[0]["parts"] if "file_data" in p) - assert "media_resolution" not in file_part, f"{model}: per-part media_resolution should not be set" + assert ( + "media_resolution" not in file_part + ), f"{model}: per-part media_resolution should not be set" def test_chunk_parser_handles_prompt_feedback_block(): @@ -4154,8 +4165,9 @@ def test_vertex_ai_usage_metadata_with_document_tokens_in_prompt(): # DOCUMENT tokens should be included in text_tokens: 8 (TEXT) + 774 (DOCUMENT) = 782 assert result.prompt_tokens_details is not None - assert result.prompt_tokens_details.text_tokens == 782, \ - "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)" + assert ( + result.prompt_tokens_details.text_tokens == 782 + ), "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)" # Verify completion token details assert result.completion_tokens_details is not None @@ -4190,8 +4202,9 @@ def test_vertex_ai_usage_metadata_with_document_tokens_cached(): # DOCUMENT cached tokens map to cached_text_tokens, so: # text_tokens = (8 TEXT + 774 DOCUMENT) - 400 cached = 382 - assert result.prompt_tokens_details.text_tokens == 382, \ - "text_tokens should be (8 + 774) - 400 cached = 382" + assert ( + result.prompt_tokens_details.text_tokens == 382 + ), "text_tokens should be (8 + 774) - 400 cached = 382" assert result.prompt_tokens_details.cached_tokens == 400 diff --git a/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py b/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py index bb4e6c67e9e..4bd0b93027d 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py @@ -20,7 +20,6 @@ from litellm.llms.vertex_ai.gemini_embeddings.batch_embed_content_transformation from litellm.types.llms.vertex_ai import VertexAIBatchEmbeddingsResponseObject from litellm.types.utils import EmbeddingResponse - IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" GCS_URL = "gs://my-bucket/image.png" @@ -72,7 +71,9 @@ class TestBuildPartForInput: assert part["file_data"]["file_uri"] == GCS_URL def test_file_reference_resolved(self): - resolved = {"files/abc": {"mime_type": "image/jpeg", "uri": "https://example.com/abc"}} + resolved = { + "files/abc": {"mime_type": "image/jpeg", "uri": "https://example.com/abc"} + } part = _build_part_for_input("files/abc", resolved_files=resolved) assert part["file_data"] is not None assert part["file_data"]["mime_type"] == "image/jpeg" @@ -94,7 +95,9 @@ class TestTransformOpenaiInputGeminiContent: def test_multiple_texts(self): result = transform_openai_input_gemini_content( - input=["hello", "world"], model="gemini-embedding-2-preview", optional_params={} + input=["hello", "world"], + model="gemini-embedding-2-preview", + optional_params={}, ) assert len(result["requests"]) == 2 assert result["requests"][0]["content"]["parts"][0]["text"] == "hello" @@ -109,7 +112,10 @@ class TestTransformOpenaiInputGeminiContent: ) assert len(result["requests"]) == 2 # First request is text - assert result["requests"][0]["content"]["parts"][0]["text"] == "The food was delicious" + assert ( + result["requests"][0]["content"]["parts"][0]["text"] + == "The food was delicious" + ) # Second request is image assert result["requests"][1]["content"]["parts"][0]["inline_data"] is not None diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index 6c549af2cc5..b47f8c6af49 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -1469,11 +1469,7 @@ def test_vertex_request_labels_from_litellm_params_extracts_requester_metadata() def test_vertex_request_labels_from_litellm_params_accepts_litellm_metadata(): - lp = { - "litellm_metadata": { - "requester_metadata": {"team": "platform", "count": 3} - } - } + lp = {"litellm_metadata": {"requester_metadata": {"team": "platform", "count": 3}}} assert vertex_request_labels_from_litellm_params(lp) == {"team": "platform"} diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py b/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py index 91261b63252..80fe002176e 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py @@ -37,5 +37,8 @@ def test_create_vertex_url_openapi_vs_deployed_endpoint( def test_model_id_in_json_body_heuristic() -> None: - assert _vertex_model_garden_model_id_in_json_body("xai/grok-4.1-fast-reasoning") is True + assert ( + _vertex_model_garden_model_id_in_json_body("xai/grok-4.1-fast-reasoning") + is True + ) assert _vertex_model_garden_model_id_in_json_body("5464397967697903616") is False diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index 6e0dadcd4d8..3a5ea4e88cb 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -880,7 +880,7 @@ class TestMCPPublicRouteGuard: with patch( "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", ) as mock_auth: - (auth_result, *_rest) = await MCPRequestHandler.process_mcp_request(scope) + auth_result, *_rest = await MCPRequestHandler.process_mcp_request(scope) mock_auth.assert_not_called() assert isinstance(auth_result, UserAPIKeyAuth) @@ -997,7 +997,7 @@ class TestMCPOAuth2FallbackTargetGating: mock_mgr.get_mcp_server_by_name.return_value = ( TestMCPOAuth2FallbackTargetGating._make_server(MCPAuth.oauth2) ) - (auth_result, *_rest) = await MCPRequestHandler.process_mcp_request(scope) + auth_result, *_rest = await MCPRequestHandler.process_mcp_request(scope) assert isinstance(auth_result, UserAPIKeyAuth) async def test_fallback_blocked_when_any_target_in_header_is_not_oauth2(self): diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py index 078adf72d4c..37e593e2a07 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py @@ -25,7 +25,6 @@ from litellm.proxy._experimental.mcp_server.db import ( ) from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper - SALT_KEY = "test-salt-key-for-byok-credential-tests-1234" diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py index 8f09e2410c4..1084e64eab3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py @@ -8,7 +8,6 @@ import pytest from litellm.proxy._experimental.mcp_server.utils import is_tool_name_prefixed - # --------------------------------------------------------------------------- # Legacy behaviour (no known_server_prefixes passed) # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index 06f95159c08..a23678d5a95 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -2946,7 +2946,7 @@ async def test_list_tools_with_legacy_db_m2m_server_resolves_oauth2_flow(): """ P1 Regression: list_tools path must apply _resolve_oauth2_flow to legacy DB rows where oauth2_flow is NULL but M2M credentials are present. - + Without this fix, has_client_credentials returns False and the caller's Authorization header is forwarded upstream instead of being blocked. """ @@ -3044,7 +3044,7 @@ async def test_call_tool_empty_extra_headers_returns_none(): """ P2 Regression: When all configured extra_headers are filtered out (e.g. Authorization for M2M), the resulting extra_headers should be None, not {}. - + Downstream code that checks `if extra_headers is None` will behave differently if an empty dict is passed instead. """ @@ -3071,7 +3071,10 @@ async def test_call_tool_empty_extra_headers_returns_none(): extra_headers=["Authorization"], # Will be filtered out for M2M ) - raw_headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"} + raw_headers = { + "Authorization": "Bearer sk-1234", + "Content-Type": "application/json", + } captured_extra_headers = None @@ -3108,8 +3111,8 @@ async def test_call_tool_empty_extra_headers_returns_none(): pass # We only care about the captured headers # With P2 fix: extra_headers should be None (not {}) when all headers filtered - assert captured_extra_headers is None, ( - "P2 API consistency issue: expected None for empty extra_headers, got: " - + str(captured_extra_headers) + assert ( + captured_extra_headers is None + ), "P2 API consistency issue: expected None for empty extra_headers, got: " + str( + captured_extra_headers ) - diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py index a0bfbff4222..4cc764f3c6d 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py @@ -497,31 +497,39 @@ async def test_per_user_oauth_missing_stored_token_returns_preemptive_401(): oauth_server.auth_type = MCPAuth.oauth2 oauth_server.needs_user_oauth_token = True - with patch( - "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", - new_callable=AsyncMock, - return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), - ), patch( - "litellm.proxy._experimental.mcp_server.server.set_auth_context", - ), patch( - "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", - True, - ), patch( - "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", - new_callable=AsyncMock, - return_value=False, - ), patch( - "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", - new_callable=AsyncMock, - return_value=None, - ) as mock_get_stored_token, patch( - "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", - return_value=oauth_server, - ), patch.object( - session_manager, - "handle_request", - new_callable=AsyncMock, - ) as mock_handle_request: + with ( + patch( + "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", + new_callable=AsyncMock, + return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), + ), + patch( + "litellm.proxy._experimental.mcp_server.server.set_auth_context", + ), + patch( + "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", + True, + ), + patch( + "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", + new_callable=AsyncMock, + return_value=False, + ), + patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new_callable=AsyncMock, + return_value=None, + ) as mock_get_stored_token, + patch( + "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", + return_value=oauth_server, + ), + patch.object( + session_manager, + "handle_request", + new_callable=AsyncMock, + ) as mock_handle_request, + ): with pytest.raises(HTTPException) as exc_info: await handle_streamable_http_mcp(scope, receive, send) @@ -562,31 +570,39 @@ async def test_per_user_oauth_with_stored_token_skips_preemptive_401(): oauth_server.auth_type = MCPAuth.oauth2 oauth_server.needs_user_oauth_token = True - with patch( - "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", - new_callable=AsyncMock, - return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), - ), patch( - "litellm.proxy._experimental.mcp_server.server.set_auth_context", - ), patch( - "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", - True, - ), patch( - "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", - new_callable=AsyncMock, - return_value=False, - ), patch( - "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", - new_callable=AsyncMock, - return_value={"Authorization": "Bearer cached-token"}, - ) as mock_get_stored_token, patch( - "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", - return_value=oauth_server, - ), patch.object( - session_manager, - "handle_request", - new_callable=AsyncMock, - ) as mock_handle_request: + with ( + patch( + "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", + new_callable=AsyncMock, + return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), + ), + patch( + "litellm.proxy._experimental.mcp_server.server.set_auth_context", + ), + patch( + "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", + True, + ), + patch( + "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", + new_callable=AsyncMock, + return_value=False, + ), + patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new_callable=AsyncMock, + return_value={"Authorization": "Bearer cached-token"}, + ) as mock_get_stored_token, + patch( + "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", + return_value=oauth_server, + ), + patch.object( + session_manager, + "handle_request", + new_callable=AsyncMock, + ) as mock_handle_request, + ): await handle_streamable_http_mcp(scope, receive, send) assert mock_get_stored_token.await_count == 1 diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py index 2558df8533b..2888077a23a 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py @@ -489,9 +489,7 @@ class TestGetToolsByNames: {"name": "send_email", "description": "send mail"}, ] - matched = filter_instance._get_tools_by_names( - ["send_email"], available_tools - ) + matched = filter_instance._get_tools_by_names(["send_email"], available_tools) assert len(matched) == 1 assert matched[0]["name"] == "send_email" @@ -503,9 +501,7 @@ class TestGetToolsByNames: client_name = "litellm_" + canonical available_tools = [{"name": client_name, "description": "scrape"}] - matched = filter_instance._get_tools_by_names( - [canonical], available_tools - ) + matched = filter_instance._get_tools_by_names([canonical], available_tools) assert len(matched) == 1 # Must return the incoming tool unchanged so the client-facing @@ -516,13 +512,9 @@ class TestGetToolsByNames: """Some clients use dash as alias separator; accept that too.""" filter_instance = self._make_filter() canonical = "weather_svc-get_weather" - available_tools = [ - {"name": "mcp-" + canonical, "description": "weather"} - ] + available_tools = [{"name": "mcp-" + canonical, "description": "weather"}] - matched = filter_instance._get_tools_by_names( - [canonical], available_tools - ) + matched = filter_instance._get_tools_by_names([canonical], available_tools) assert len(matched) == 1 assert matched[0]["name"] == "mcp-" + canonical @@ -552,9 +544,7 @@ class TestGetToolsByNames: {"name": "litellm_" + canonical, "description": "wrapped"}, ] - matched = filter_instance._get_tools_by_names( - [canonical], available_tools - ) + matched = filter_instance._get_tools_by_names([canonical], available_tools) assert len(matched) == 1 assert matched[0]["name"] == canonical @@ -567,9 +557,7 @@ class TestGetToolsByNames: separator-anchored suffixes of ``litellm_api-fs-read_file``. """ filter_instance = self._make_filter() - available_tools = [ - {"name": "litellm_api-fs-read_file", "description": "read"} - ] + available_tools = [{"name": "litellm_api-fs-read_file", "description": "read"}] matched = filter_instance._get_tools_by_names( ["fs-read_file", "api-fs-read_file"], available_tools @@ -590,9 +578,7 @@ class TestGetToolsByNames: {"name": "my_" + canonical, "description": "plain search"}, ] - matched = filter_instance._get_tools_by_names( - [canonical], available_tools - ) + matched = filter_instance._get_tools_by_names([canonical], available_tools) assert len(matched) == 1 assert matched[0]["name"] == "my_" + canonical diff --git a/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py b/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py index 554f98d7209..06da009134f 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py @@ -16,7 +16,6 @@ import pytest from litellm.constants import DEFAULT_A2A_AGENT_TIMEOUT - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py b/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py index 93ba9dc922c..7cbd12a4ea6 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py @@ -14,7 +14,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest - # --------------------------------------------------------------------------- # Helper: build a minimal mock agent # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/auth/test_onboarding.py b/tests/test_litellm/proxy/auth/test_onboarding.py index c81f4cb7d66..d55a5472af1 100644 --- a/tests/test_litellm/proxy/auth/test_onboarding.py +++ b/tests/test_litellm/proxy/auth/test_onboarding.py @@ -18,7 +18,6 @@ from fastapi import HTTPException import litellm from litellm.proxy._types import InvitationClaim - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/common_utils/test_cache_codec.py b/tests/test_litellm/proxy/common_utils/test_cache_codec.py index 044d4c2d1a7..8aa16ddbce1 100644 --- a/tests/test_litellm/proxy/common_utils/test_cache_codec.py +++ b/tests/test_litellm/proxy/common_utils/test_cache_codec.py @@ -54,7 +54,9 @@ class TestCacheCodecSerialize: def test_with_model_type_already_correct_instance_skips_revalidation(self): """Fast-path: value is already model_type — model_validate must NOT be called.""" m = _SampleModel(name="fast", count=7) - with patch.object(_SampleModel, "model_validate", wraps=_SampleModel.model_validate) as mock_validate: + with patch.object( + _SampleModel, "model_validate", wraps=_SampleModel.model_validate + ) as mock_validate: out = CacheCodec.serialize(m, model_type=_SampleModel) assert out == {"name": "fast", "count": 7} mock_validate.assert_not_called() @@ -62,7 +64,9 @@ class TestCacheCodecSerialize: def test_with_model_type_subclass_instance_skips_revalidation(self): """Subclass is isinstance of base → should also take the fast path.""" sub = _SampleSubModel(name="sub", count=2) - with patch.object(_SampleModel, "model_validate", wraps=_SampleModel.model_validate) as mock_validate: + with patch.object( + _SampleModel, "model_validate", wraps=_SampleModel.model_validate + ) as mock_validate: out = CacheCodec.serialize(sub, model_type=_SampleModel) assert out == {"name": "sub", "count": 2} mock_validate.assert_not_called() diff --git a/tests/test_litellm/proxy/conftest.py b/tests/test_litellm/proxy/conftest.py index 20236ebdf45..d01c47d9751 100644 --- a/tests/test_litellm/proxy/conftest.py +++ b/tests/test_litellm/proxy/conftest.py @@ -14,7 +14,6 @@ import pytest import yaml from fastapi.testclient import TestClient - _PROXY_MODULE_GLOBALS_TO_ISOLATE = ( "master_key", "prisma_client", diff --git a/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py b/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py index 434f7953c21..3586c9a80ee 100644 --- a/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py +++ b/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py @@ -2,6 +2,7 @@ """ Test to verify the Google GenAI proxy API endpoints """ + import os import sys from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py index fb952d4b18b..399852acd40 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py @@ -314,15 +314,13 @@ class TestContentFilterGuardrail: # Create a temporary blocked words file with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - f.write( - """blocked_words: + f.write("""blocked_words: - keyword: "test_keyword" action: "BLOCK" description: "Test keyword" - keyword: "another_word" action: "MASK" -""" - ) +""") temp_file = f.name try: diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py index bccfb4a1cb5..ee7440d1195 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py @@ -2,6 +2,7 @@ """ Test OpenAI Moderation Guardrail """ + import os import sys diff --git a/tests/test_litellm/proxy/guardrails/test_content_utils.py b/tests/test_litellm/proxy/guardrails/test_content_utils.py index 099fca78a62..d3428dfbafa 100644 --- a/tests/test_litellm/proxy/guardrails/test_content_utils.py +++ b/tests/test_litellm/proxy/guardrails/test_content_utils.py @@ -8,7 +8,6 @@ from litellm.proxy.guardrails._content_utils import ( walk_user_text, ) - # ── iter_message_text ──────────────────────────────────────────────────────────── diff --git a/tests/test_litellm/proxy/guardrails/test_custom_code_security.py b/tests/test_litellm/proxy/guardrails/test_custom_code_security.py index 00cf3f317c9..defdba10a60 100644 --- a/tests/test_litellm/proxy/guardrails/test_custom_code_security.py +++ b/tests/test_litellm/proxy/guardrails/test_custom_code_security.py @@ -5,7 +5,6 @@ from litellm.proxy.guardrails.guardrail_hooks.custom_code.custom_code_guardrail CustomCodeGuardrail, ) - # str.mro() + generator gi_code + code.replace(co_names=...) + __setattr__ # to swap a function's bytecode and read http_get's real builtins dict. BYTECODE_REWRITE_PAYLOAD = ( diff --git a/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py b/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py index c9fde4ffbae..9178a66611e 100644 --- a/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py +++ b/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py @@ -13,7 +13,6 @@ from litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge import ( initialize_guardrail, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -39,8 +38,20 @@ def _make_guardrail(**overrides) -> LLMAsAJudgeGuardrail: def _make_verdict_response(overall_score: float) -> dict: return { "verdicts": [ - {"criterion_name": "Accuracy", "score": overall_score, "reasoning": "ok", "passed": True, "weight": 60}, - {"criterion_name": "Safety", "score": overall_score, "reasoning": "ok", "passed": True, "weight": 40}, + { + "criterion_name": "Accuracy", + "score": overall_score, + "reasoning": "ok", + "passed": True, + "weight": 60, + }, + { + "criterion_name": "Safety", + "score": overall_score, + "reasoning": "ok", + "passed": True, + "weight": 40, + }, ], "overall_score": overall_score, } @@ -90,7 +101,15 @@ def test_build_judge_prompt_missing_name_and_weight(): def _make_litellm_params(**overrides): params = MagicMock() - for attr in ("guardrail_name", "judge_model", "criteria", "on_failure", "overall_threshold", "mode", "default_on"): + for attr in ( + "guardrail_name", + "judge_model", + "criteria", + "on_failure", + "overall_threshold", + "mode", + "default_on", + ): setattr(params, attr, None) for k, v in overrides.items(): setattr(params, k, v) @@ -98,12 +117,19 @@ def _make_litellm_params(**overrides): def _make_guardrail_dict(name="g", **litellm_params_overrides): - raw = {"judge_model": "gpt-4o-mini", "criteria": CRITERIA_100, "on_failure": "block", "overall_threshold": 80.0} + raw = { + "judge_model": "gpt-4o-mini", + "criteria": CRITERIA_100, + "on_failure": "block", + "overall_threshold": 80.0, + } raw.update(litellm_params_overrides) return {"guardrail_name": name, "litellm_params": raw} -@patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.logging_callback_manager") +@patch( + "litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.logging_callback_manager" +) def test_initialize_guardrail_ok(mock_mgr): lp = _make_litellm_params() g = _make_guardrail_dict() @@ -160,11 +186,18 @@ async def test_apply_guardrail_empty_response_passthrough(): @patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.acompletion") async def test_apply_guardrail_passes_above_threshold(mock_completion): mock_completion.return_value = MagicMock( - choices=[MagicMock(message=MagicMock(content=json.dumps(_make_verdict_response(90.0))))] + choices=[ + MagicMock( + message=MagicMock(content=json.dumps(_make_verdict_response(90.0))) + ) + ] ) guardrail = _make_guardrail(overall_threshold=80.0) inputs = {"texts": ["good response"]} - request_data: dict = {"messages": [{"role": "user", "content": "hi"}], "metadata": {}} + request_data: dict = { + "messages": [{"role": "user", "content": "hi"}], + "metadata": {}, + } result = await guardrail.apply_guardrail(inputs, request_data, "response") assert result is inputs assert request_data["metadata"]["eval_information"]["passed"] is True @@ -174,7 +207,11 @@ async def test_apply_guardrail_passes_above_threshold(mock_completion): @patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.acompletion") async def test_apply_guardrail_blocks_below_threshold(mock_completion): mock_completion.return_value = MagicMock( - choices=[MagicMock(message=MagicMock(content=json.dumps(_make_verdict_response(50.0))))] + choices=[ + MagicMock( + message=MagicMock(content=json.dumps(_make_verdict_response(50.0))) + ) + ] ) guardrail = _make_guardrail(overall_threshold=80.0, on_failure="block") inputs = {"texts": ["bad response"]} @@ -188,7 +225,11 @@ async def test_apply_guardrail_blocks_below_threshold(mock_completion): @patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.acompletion") async def test_apply_guardrail_log_mode_does_not_block(mock_completion): mock_completion.return_value = MagicMock( - choices=[MagicMock(message=MagicMock(content=json.dumps(_make_verdict_response(50.0))))] + choices=[ + MagicMock( + message=MagicMock(content=json.dumps(_make_verdict_response(50.0))) + ) + ] ) guardrail = _make_guardrail(overall_threshold=80.0, on_failure="log") inputs = {"texts": ["bad response"]} diff --git a/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py b/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py index 6daa3e1430d..f7b4eb762a6 100644 --- a/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py +++ b/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py @@ -232,9 +232,9 @@ def test_qostodian_nexus_builtin_extra_headers(): ] for header in expected_headers: - assert header in instance.extra_headers, ( - f"Expected built-in header '{header}' to be in extra_headers" - ) + assert ( + header in instance.extra_headers + ), f"Expected built-in header '{header}' to be in extra_headers" def test_qostodian_nexus_extra_headers_merged(): diff --git a/tests/test_litellm/proxy/hooks/test_batch_file_validation.py b/tests/test_litellm/proxy/hooks/test_batch_file_validation.py index 7f1006543bb..c26e90108f8 100644 --- a/tests/test_litellm/proxy/hooks/test_batch_file_validation.py +++ b/tests/test_litellm/proxy/hooks/test_batch_file_validation.py @@ -14,7 +14,6 @@ from fastapi import HTTPException from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth - # --------------------------------------------------------------------------- # Token counter — covers all three batch payload shapes # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py b/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py index 0855c201945..4686c5e0475 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py +++ b/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py @@ -14,7 +14,6 @@ import pytest from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth - # --------------------------------------------------------------------------- # /team/daily/activity — per-team admin/permission requirement # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py index b15b9d622e4..8f5e1da995b 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py @@ -11,7 +11,6 @@ import litellm.proxy.proxy_server as ps from litellm.proxy.proxy_server import app from litellm.proxy._types import UserAPIKeyAuth, LitellmUserRoles, CommonProxyErrors - sys.path.insert( 0, os.path.abspath("../../../") ) # Adds the parent directory to the system path diff --git a/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py b/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py index 63e584e49bc..6720002c2bf 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py +++ b/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py @@ -26,7 +26,6 @@ from litellm.proxy.management_endpoints.key_management_endpoints import ( delete_verification_tokens, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py b/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py index bd982480d60..b3d5b7b4a0a 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py +++ b/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py @@ -14,7 +14,6 @@ from fastapi import HTTPException from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth - # --------------------------------------------------------------------------- # /project/update — _check_user_permission_for_project # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py b/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py index 443089b5f01..b0aae5ce393 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py @@ -25,7 +25,6 @@ from litellm.proxy.management_endpoints.team_endpoints import ( ) from litellm.proxy.proxy_server import ProxyConfig - # --------------------------------------------------------------------------- # _update_config_fields: default_team_params loaded from DB on startup # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py index a337ff6d888..bf55fd560e0 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py @@ -17,7 +17,6 @@ sys.path.insert(0, os.path.abspath("../../..")) from litellm.proxy.management_endpoints.workflow_management_endpoints import router - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py b/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py index e8a74e41dae..da899cd62f4 100644 --- a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py +++ b/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py @@ -17,7 +17,6 @@ from litellm.proxy.management_endpoints.usage_endpoints.ai_usage_chat import ( stream_usage_ai_chat, ) - SAMPLE_AGGREGATED_RESPONSE = { "results": [ { 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 c9be00afed0..0e7e0ba3211 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 @@ -1092,9 +1092,9 @@ class TestVertexAIPassThroughHandler: assert result is not None assert result["result"] is not None - assert result["kwargs"].get("custom_llm_provider") == "gemini", ( - "Google AI Studio embedContent URLs must set custom_llm_provider=gemini, not vertex_ai" - ) + assert ( + result["kwargs"].get("custom_llm_provider") == "gemini" + ), "Google AI Studio embedContent URLs must set custom_llm_provider=gemini, not vertex_ai" assert result["kwargs"].get("model") == "gemini-embedding-2-preview" mock_completion_cost.assert_called_once() diff --git a/tests/test_litellm/proxy/test_fastapi_offline_routes.py b/tests/test_litellm/proxy/test_fastapi_offline_routes.py index f3fc3d3ea28..7788e54b0b7 100644 --- a/tests/test_litellm/proxy/test_fastapi_offline_routes.py +++ b/tests/test_litellm/proxy/test_fastapi_offline_routes.py @@ -1,7 +1,7 @@ """ Unit test for testing /routes endpoint with FastAPIOffline app initialization. -This test verifies that the /routes endpoint works correctly when the proxy +This test verifies that the /routes endpoint works correctly when the proxy server is initialized using FastAPIOffline instead of regular FastAPI. """ diff --git a/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py b/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py index 2d8a9f30c1b..5d1767bfb02 100644 --- a/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py +++ b/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py @@ -233,4 +233,6 @@ async def test_filter_db_fallback_receives_resolved_model_names(): "gpt-4o", "gpt-5", }, f"DB query should receive resolved model names, got {queried_names}" - assert "Group-A" not in queried_names, "Raw access group name should not be in DB query" + assert ( + "Group-A" not in queried_names + ), "Raw access group name should not be in DB query" diff --git a/tests/test_litellm/proxy/test_model_level_guardrails.py b/tests/test_litellm/proxy/test_model_level_guardrails.py index e83f8c67caa..a2aed063d5e 100644 --- a/tests/test_litellm/proxy/test_model_level_guardrails.py +++ b/tests/test_litellm/proxy/test_model_level_guardrails.py @@ -19,7 +19,6 @@ from litellm.proxy.utils import ( _merge_guardrails_with_existing, ) - # --------------------------------------------------------------------------- # Unit tests for _check_and_merge_model_level_guardrails # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/test_redis_auth_cache_flag.py b/tests/test_litellm/proxy/test_redis_auth_cache_flag.py index d0cb5ec5465..3cb2f5e434c 100644 --- a/tests/test_litellm/proxy/test_redis_auth_cache_flag.py +++ b/tests/test_litellm/proxy/test_redis_auth_cache_flag.py @@ -16,7 +16,6 @@ import litellm.proxy.proxy_server as ps from litellm.caching.caching import RedisCache from litellm.caching.dual_cache import DualCache - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/test_team_org_move.py b/tests/test_litellm/proxy/test_team_org_move.py index 2dc961bec85..ad5f9c38594 100644 --- a/tests/test_litellm/proxy/test_team_org_move.py +++ b/tests/test_litellm/proxy/test_team_org_move.py @@ -6,6 +6,7 @@ Covers the SSO/Entra scenario where: - Non-proxy-admins (team admins) must have all team members pre-added to the org, preserving the original security model (no privilege escalation via team move). """ + from unittest.mock import AsyncMock, MagicMock import pytest @@ -47,10 +48,10 @@ def _make_org(organization_id="org-1", members=None, models=None): def _make_team(team_id="team-1", member_ids=None, organization_id=None): - members = [ - Member(user_id=uid, role="user") for uid in (member_ids or []) - ] - members.append(Member(user_id=SpecialProxyStrings.default_user_id.value, role="admin")) + members = [Member(user_id=uid, role="user") for uid in (member_ids or [])] + members.append( + Member(user_id=SpecialProxyStrings.default_user_id.value, role="admin") + ) return LiteLLM_TeamTable( team_id=team_id, team_alias="test-team", @@ -120,12 +121,18 @@ class TestValidateTeamOrgChange: team = _make_team(member_ids=["u1"], organization_id="org-1") org = _make_org(organization_id="org-1") - assert validate_team_org_change( - team=team, organization=org, llm_router=router, is_proxy_admin=False - ) is True - assert validate_team_org_change( - team=team, organization=org, llm_router=router, is_proxy_admin=True - ) is True + assert ( + validate_team_org_change( + team=team, organization=org, llm_router=router, is_proxy_admin=False + ) + is True + ) + assert ( + validate_team_org_change( + team=team, organization=org, llm_router=router, is_proxy_admin=True + ) + is True + ) def test_default_user_excluded_from_membership_check(self): """default_user_id is never checked for org membership.""" @@ -149,6 +156,7 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock() import litellm.proxy.management_endpoints.team_endpoints as te + original = te.add_member_to_organization te.add_member_to_organization = mock_add @@ -174,6 +182,7 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock() import litellm.proxy.management_endpoints.team_endpoints as te + original = te.add_member_to_organization te.add_member_to_organization = mock_add @@ -197,6 +206,7 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock() import litellm.proxy.management_endpoints.team_endpoints as te + original = te.add_member_to_organization te.add_member_to_organization = mock_add @@ -219,6 +229,7 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock(side_effect=Exception("duplicate key")) import litellm.proxy.management_endpoints.team_endpoints as te + original = te.add_member_to_organization te.add_member_to_organization = mock_add diff --git a/tests/test_litellm/responses/test_responses_api_bridge_flag.py b/tests/test_litellm/responses/test_responses_api_bridge_flag.py index 463af6562f1..ca3a2a8a64e 100644 --- a/tests/test_litellm/responses/test_responses_api_bridge_flag.py +++ b/tests/test_litellm/responses/test_responses_api_bridge_flag.py @@ -151,9 +151,7 @@ class TestUseResponsesApiBridgeFlag: output=[ {"type": "message", "content": [{"type": "text", "text": "Answer"}]} ], - usage=ResponseAPIUsage( - input_tokens=10, output_tokens=5, total_tokens=15 - ), + usage=ResponseAPIUsage(input_tokens=10, output_tokens=5, total_tokens=15), ) mock_call_aresponses.return_value = mock_response @@ -202,9 +200,7 @@ class TestUseResponsesApiBridgeFlag: "arguments": '{"queries": ["test query"]}', } ], - usage=ResponseAPIUsage( - input_tokens=10, output_tokens=5, total_tokens=15 - ), + usage=ResponseAPIUsage(input_tokens=10, output_tokens=5, total_tokens=15), ) second_response = ResponsesAPIResponse( id="resp_second", @@ -216,9 +212,7 @@ class TestUseResponsesApiBridgeFlag: "content": [{"type": "text", "text": "Final answer"}], } ], - usage=ResponseAPIUsage( - input_tokens=20, output_tokens=10, total_tokens=30 - ), + usage=ResponseAPIUsage(input_tokens=20, output_tokens=10, total_tokens=30), ) mock_bridge_handler.side_effect = [first_response, second_response] @@ -267,9 +261,7 @@ class TestUseResponsesApiBridgeFlag: "content": [{"type": "text", "text": "Native response"}], } ], - usage=ResponseAPIUsage( - input_tokens=10, output_tokens=5, total_tokens=15 - ), + usage=ResponseAPIUsage(input_tokens=10, output_tokens=5, total_tokens=15), ) result = await litellm.aresponses( diff --git a/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py b/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py index 604155e1221..2703d7b28c6 100644 --- a/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py +++ b/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py @@ -454,9 +454,7 @@ def test_finalize_prunes_stale_adaptive_router_hooks_from_callbacks(): Router(model_list=model_list) # simulate hot-reload adaptive_hooks = [ - cb - for cb in litellm.callbacks - if isinstance(cb, AdaptiveRouterPostCallHook) + cb for cb in litellm.callbacks if isinstance(cb, AdaptiveRouterPostCallHook) ] assert len(adaptive_hooks) == 1, ( f"expected exactly one AdaptiveRouterPostCallHook after hot-reload, " diff --git a/tests/test_litellm/test_acompletion_session_reuse_e2e.py b/tests/test_litellm/test_acompletion_session_reuse_e2e.py index 79b947bb146..a6f2be09cef 100644 --- a/tests/test_litellm/test_acompletion_session_reuse_e2e.py +++ b/tests/test_litellm/test_acompletion_session_reuse_e2e.py @@ -22,7 +22,6 @@ sys.path.insert(0, os.path.abspath("../../..")) import litellm - # ============================================================================ # HELPER FUNCTION # ============================================================================ diff --git a/tests/test_litellm/test_anthropic_skills_transformation.py b/tests/test_litellm/test_anthropic_skills_transformation.py index 1b917f08ca9..1b41b05f6d7 100644 --- a/tests/test_litellm/test_anthropic_skills_transformation.py +++ b/tests/test_litellm/test_anthropic_skills_transformation.py @@ -22,7 +22,6 @@ from litellm.types.llms.anthropic_skills import ( ) from litellm.types.router import GenericLiteLLMParams - FAKE_API_KEY = "sk-ant-test-key-1234" FAKE_API_BASE = "https://api.anthropic.com" diff --git a/tests/test_litellm/test_dashscope_image_generation.py b/tests/test_litellm/test_dashscope_image_generation.py index af95e2ca6b4..aa3c955f838 100644 --- a/tests/test_litellm/test_dashscope_image_generation.py +++ b/tests/test_litellm/test_dashscope_image_generation.py @@ -18,7 +18,6 @@ from litellm.llms.dashscope.image_generation.transformation import ( from litellm.types.utils import ImageObject, ImageResponse from litellm.utils import get_llm_provider - # --------------------------------------------------------------------------- # 1. Provider detection # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/test_deepseek_model_metadata.py b/tests/test_litellm/test_deepseek_model_metadata.py index 4900af5d97d..2b86b61b019 100644 --- a/tests/test_litellm/test_deepseek_model_metadata.py +++ b/tests/test_litellm/test_deepseek_model_metadata.py @@ -23,7 +23,6 @@ from litellm.utils import ( supports_response_schema, ) - # --------------------------------------------------------------------------- # Data-level tests – verify the JSON files are in sync # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index f9f92a85cb2..ee11ebb9702 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -10,7 +10,6 @@ from unittest.mock import patch from litellm import verbose_logger from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases - # --------------------------------------------------------------------------- # Core expansion behaviour # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/test_nested_drop_params.py b/tests/test_litellm/test_nested_drop_params.py index bb1305ffde4..fbcd5302308 100644 --- a/tests/test_litellm/test_nested_drop_params.py +++ b/tests/test_litellm/test_nested_drop_params.py @@ -7,7 +7,6 @@ This tests the new JSONPath-like syntax for removing nested fields. import os import sys - # Add parent directory to path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) diff --git a/tests/test_litellm/test_openai_embedding_encoding_format_default.py b/tests/test_litellm/test_openai_embedding_encoding_format_default.py index 94e4e3c81e5..1cf71139d87 100644 --- a/tests/test_litellm/test_openai_embedding_encoding_format_default.py +++ b/tests/test_litellm/test_openai_embedding_encoding_format_default.py @@ -51,9 +51,7 @@ def test_openai_embedding_encoding_format_default( @pytest.mark.parametrize("env_none", ["none", "NONE", " none "]) -def test_openai_embedding_encoding_format_env_none_omits_param( - monkeypatch, env_none -): +def test_openai_embedding_encoding_format_env_none_omits_param(monkeypatch, env_none): """LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT=none omits encoding_format (provider default).""" monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", env_none) diff --git a/tests/test_litellm/test_router_google_genai.py b/tests/test_litellm/test_router_google_genai.py index 81dd7bbdc40..b8179e568a7 100644 --- a/tests/test_litellm/test_router_google_genai.py +++ b/tests/test_litellm/test_router_google_genai.py @@ -2,6 +2,7 @@ """ Test to verify the new Google GenAI router methods """ + import asyncio import os import sys diff --git a/tests/test_litellm/test_streaming_connection_cleanup.py b/tests/test_litellm/test_streaming_connection_cleanup.py index 5a81a3ffb17..53bbe8abd3c 100644 --- a/tests/test_litellm/test_streaming_connection_cleanup.py +++ b/tests/test_litellm/test_streaming_connection_cleanup.py @@ -19,7 +19,6 @@ from litellm.llms.custom_httpx.aiohttp_transport import ( LiteLLMAiohttpTransport, ) - # ── aiohttp transport layer tests ────────────────────────────── diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 65305e1a81e..f493b84f447 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1500,8 +1500,7 @@ class TestProxyFunctionCalling: assert result is True, "Resolvable model names work with fallback logic" # Documentation notes: - print( - """ + print(""" PROXY MODEL RESOLUTION BEHAVIOR: ✅ WORKS (with current fallback logic): @@ -1516,8 +1515,7 @@ class TestProxyFunctionCalling: 💡 SOLUTION: Use LiteLLM proxy server with proper model_list configuration that maps custom names to underlying models. - """ - ) + """) @pytest.mark.parametrize( "proxy_model_with_hints,expected_result", @@ -1879,8 +1877,7 @@ class TestProxyFunctionCalling: This test provides documentation on how the proxy server configuration would typically map custom model names to underlying models. """ - print( - """ + print(""" REAL-WORLD PROXY SERVER CONFIGURATION EXAMPLE: =============================================== @@ -1933,8 +1930,7 @@ class TestProxyFunctionCalling: - Consistent request/response format - Enhanced streaming support for function calls - """ - ) + """) # Verify that direct underlying models work as expected bedrock_models = [ @@ -2148,8 +2144,7 @@ class TestProxyFunctionCalling: This test provides documentation on how the proxy server configuration would typically map custom model names to underlying models. """ - print( - """ + print(""" REAL-WORLD PROXY SERVER CONFIGURATION EXAMPLE: =============================================== @@ -2202,8 +2197,7 @@ class TestProxyFunctionCalling: - Consistent request/response format - Enhanced streaming support for function calls - """ - ) + """) # Verify that direct underlying models work as expected bedrock_models = [ @@ -2417,8 +2411,7 @@ class TestProxyFunctionCalling: This test provides documentation on how the proxy server configuration would typically map custom model names to underlying models. """ - print( - """ + print(""" REAL-WORLD PROXY SERVER CONFIGURATION EXAMPLE: =============================================== @@ -2471,8 +2464,7 @@ class TestProxyFunctionCalling: - Consistent request/response format - Enhanced streaming support for function calls - """ - ) + """) # Verify that direct underlying models work as expected bedrock_models = [ diff --git a/tests/test_litellm/types/test_completion.py b/tests/test_litellm/types/test_completion.py index f24b00df3fc..b753a8e2abb 100644 --- a/tests/test_litellm/types/test_completion.py +++ b/tests/test_litellm/types/test_completion.py @@ -1,7 +1,7 @@ """ Tests for litellm.types.completion module -This test suite validates the CompletionRequest model and its compatibility with +This test suite validates the CompletionRequest model and its compatibility with OpenAI ChatCompletion API message formats. Usage: diff --git a/tests/test_litellm/types/test_prometheus_label_value_sanitize.py b/tests/test_litellm/types/test_prometheus_label_value_sanitize.py index 9ff7eb460e0..d8b90197c63 100644 --- a/tests/test_litellm/types/test_prometheus_label_value_sanitize.py +++ b/tests/test_litellm/types/test_prometheus_label_value_sanitize.py @@ -22,7 +22,7 @@ from litellm.types.integrations.prometheus import ( # Escapes per Prometheus text format ('he said "hi"', 'he said \\"hi\\"'), (r"path\to\file", r"path\\to\\file"), - (r'quote\"slash\\', r'quote\\\"slash\\\\'), + (r"quote\"slash\\", r"quote\\\"slash\\\\"), # Non-string inputs get coerced to str first (123, "123"), (True, "True"), @@ -31,4 +31,3 @@ from litellm.types.integrations.prometheus import ( ) def test_sanitize_prometheus_label_value_expected_outputs(value, expected): assert _sanitize_prometheus_label_value(value) == expected - diff --git a/tests/test_passthrough_endpoints.py b/tests/test_passthrough_endpoints.py index 47ac7511aa1..bf9417912c5 100644 --- a/tests/test_passthrough_endpoints.py +++ b/tests/test_passthrough_endpoints.py @@ -10,7 +10,6 @@ import json import os import dotenv - dotenv.load_dotenv() diff --git a/tests/vector_store_tests/rag/base_rag_tests.py b/tests/vector_store_tests/rag/base_rag_tests.py index 2c5a2540a7e..c357965da53 100644 --- a/tests/vector_store_tests/rag/base_rag_tests.py +++ b/tests/vector_store_tests/rag/base_rag_tests.py @@ -124,9 +124,7 @@ class BaseRAGTest(ABC): Test document {unique_id} for RAG ingestion and query. LiteLLM provides a unified interface for 100+ LLMs. This content should be retrievable via semantic search. - """.encode( - "utf-8" - ) + """.encode("utf-8") file_data = (filename, text_content, "text/plain") ingest_options = self.get_base_ingest_options() diff --git a/tests/vector_store_tests/rag/test_rag_vertex_ai.py b/tests/vector_store_tests/rag/test_rag_vertex_ai.py index c99840bb0fe..872383899d8 100644 --- a/tests/vector_store_tests/rag/test_rag_vertex_ai.py +++ b/tests/vector_store_tests/rag/test_rag_vertex_ai.py @@ -151,9 +151,7 @@ class TestRAGVertexAI(BaseRAGTest): Test document {unique_id} for Vertex AI RAG corpus creation. This tests the automatic corpus creation feature. The corpus should be created and the file should be uploaded successfully. - """.encode( - "utf-8" - ) + """.encode("utf-8") file_data = (filename, text_content, "text/plain") # Get base options WITHOUT corpus_id to trigger creation @@ -215,9 +213,7 @@ class TestRAGVertexAI(BaseRAGTest): text_content = f""" Test document {unique_id} for existing Vertex AI RAG corpus. This tests file upload to a pre-existing corpus. - """.encode( - "utf-8" - ) + """.encode("utf-8") file_data = (filename, text_content, "text/plain") ingest_options = self.get_base_ingest_options() diff --git a/tests/vector_store_tests/test_milvus_vector_store.py b/tests/vector_store_tests/test_milvus_vector_store.py index 6627f6006d1..a0faf9d4ecd 100644 --- a/tests/vector_store_tests/test_milvus_vector_store.py +++ b/tests/vector_store_tests/test_milvus_vector_store.py @@ -12,7 +12,6 @@ import litellm from litellm.vector_stores import asearch as vector_store_asearch from litellm.vector_stores import search as vector_store_search - # Mock response from actual Milvus API MOCK_MILVUS_SEARCH_RESPONSE = { "code": 0, diff --git a/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py b/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py index 7cace338616..94c92c7fb94 100644 --- a/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py +++ b/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py @@ -7,7 +7,6 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch import litellm - # Mock response from actual Vertex AI Search API MOCK_VERTEX_SEARCH_RESPONSE = { "results": [ diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py b/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py index 8e92065c696..9ca335430f6 100644 --- a/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py +++ b/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py @@ -12,7 +12,6 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse - app = FastAPI(title="Mock LLM Server") app.add_middleware( CORSMiddleware, diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index ca4e469de08..9fccf34d995 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -231,23 +231,23 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", - "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.4" + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.7.tgz", - "integrity": "sha512-8CO/UQ4tzDd7ula+/CVimJIVWez99UJlbMyIgk8xOnhAVPKLnBZmUFYVgugS441v2ZqUq5EnSh6B0Ua0liSFAA==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -255,7 +255,7 @@ "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.5" + "lru-cache": "^11.2.6" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -301,9 +301,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -317,9 +317,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -350,9 +350,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -366,13 +366,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", + "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", "dev": true, "funding": [ { @@ -386,17 +386,17 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", + "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", "dev": true, "funding": [ { @@ -410,21 +410,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -438,16 +438,16 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.26", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", - "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", + "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", "dev": true, "funding": [ { @@ -459,12 +459,20 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -478,25 +486,25 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -504,9 +512,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -527,9 +535,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -544,9 +552,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -561,9 +569,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -578,9 +586,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -595,9 +603,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -612,9 +620,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -629,9 +637,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -646,9 +654,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -663,9 +671,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -680,9 +688,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -697,9 +705,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -714,9 +722,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -731,9 +739,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -748,9 +756,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -765,9 +773,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -782,9 +790,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -799,9 +807,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -816,9 +824,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -833,9 +841,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -850,9 +858,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -867,9 +875,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -884,9 +892,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -901,9 +909,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -918,9 +926,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -935,9 +943,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -952,9 +960,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -1011,15 +1019,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1052,20 +1060,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1113,9 +1121,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.10.0.tgz", - "integrity": "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -1131,22 +1139,22 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react": { @@ -1178,9 +1186,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@headlessui/react": { @@ -1218,12 +1226,12 @@ } }, "node_modules/@headlessui/react/node_modules/@floating-ui/react-dom": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", - "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.5" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -1252,29 +1260,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1304,9 +1326,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, "engines": { @@ -1769,10 +1791,37 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@internationalized/date": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", + "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.6.tgz", + "integrity": "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.8.tgz", + "integrity": "sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { @@ -1815,16 +1864,22 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@next/env": { @@ -1882,9 +1937,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1901,9 +1953,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1920,9 +1969,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1939,9 +1985,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2029,9 +2072,9 @@ } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.17.0.tgz", - "integrity": "sha512-kVnY21v0GyZ/+LG6EIO48wK3mE79BUuakHUYLIqobO/Qqq4mJsjuYXMSn3JtLcKZpN1HDVit4UHpGJHef1lrlw==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", + "integrity": "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==", "cpu": [ "arm" ], @@ -2043,9 +2086,9 @@ ] }, "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.17.0.tgz", - "integrity": "sha512-Pf8e3XcsK9a8RHInoAtEcrwf2vp7V9bSturyUUYxw9syW6E7cGi7z9+6ADXxm+8KAevVfLA7pfBg8NXTvz/HOw==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.19.1.tgz", + "integrity": "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==", "cpu": [ "arm64" ], @@ -2057,9 +2100,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.17.0.tgz", - "integrity": "sha512-lVSgKt3biecofXVr8e1hnfX0IYMd4A6VCxmvOmHsFt5Zbmt0lkO4S2ap2bvQwYDYh5ghUNamC7M2L8K6vishhQ==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.19.1.tgz", + "integrity": "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==", "cpu": [ "arm64" ], @@ -2071,9 +2114,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.17.0.tgz", - "integrity": "sha512-+/raxVJE1bo7R4fA9Yp0wm3slaCOofTEeUzM01YqEGcRDLHB92WRGjRhagMG2wGlvqFuSiTp81DwSbBVo/g6AQ==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.19.1.tgz", + "integrity": "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==", "cpu": [ "x64" ], @@ -2085,9 +2128,9 @@ ] }, "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.17.0.tgz", - "integrity": "sha512-x9Ks56n+n8h0TLhzA6sJXa2tGh3uvMGpBppg6PWf8oF0s5S/3p/J6k1vJJ9lIUtTmenfCQEGKnFokpRP4fLTLg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.19.1.tgz", + "integrity": "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==", "cpu": [ "x64" ], @@ -2099,9 +2142,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.17.0.tgz", - "integrity": "sha512-Wf3w07Ow9kXVJrS0zmsaFHKOGhXKXE8j1tNyy+qIYDsQWQ4UQZVx5SjlDTcqBnFerlp3Z3Is0RjmVzgoLG3qkA==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.19.1.tgz", + "integrity": "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==", "cpu": [ "arm" ], @@ -2113,9 +2156,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.17.0.tgz", - "integrity": "sha512-N0OKA1al1gQ5Gm7Fui1RWlXaHRNZlwMoBLn3TVtSXX+WbnlZoVyDqqOqFL8+pVEHhhxEA2LR8kmM0JO6FAk6dg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.19.1.tgz", + "integrity": "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==", "cpu": [ "arm" ], @@ -2127,9 +2170,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.17.0.tgz", - "integrity": "sha512-wdcQ7Niad9JpjZIGEeqKJnTvczVunqlZ/C06QzR5zOQNeLVRScQ9S5IesKWUAPsJQDizV+teQX53nTK+Z5Iy+g==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.19.1.tgz", + "integrity": "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==", "cpu": [ "arm64" ], @@ -2141,9 +2184,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.17.0.tgz", - "integrity": "sha512-65B2/t39HQN5AEhkLsC+9yBD1iRUkKOIhfmJEJ7g6wQ9kylra7JRmNmALFjbsj0VJsoSQkpM8K07kUZuNJ9Kxw==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.19.1.tgz", + "integrity": "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==", "cpu": [ "arm64" ], @@ -2155,9 +2198,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.17.0.tgz", - "integrity": "sha512-kExgm3TLK21dNMmcH+xiYGbc6BUWvT03PUZ2aYn8mUzGPeeORklBhg3iYcaBI3ZQHB25412X1Z6LLYNjt4aIaA==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.19.1.tgz", + "integrity": "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==", "cpu": [ "ppc64" ], @@ -2169,9 +2212,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.17.0.tgz", - "integrity": "sha512-1utUJC714/ydykZQE8c7QhpEyM4SaslMfRXxN9G61KYazr6ndt85LaubK3EZCSD50vVEfF4PVwFysCSO7LN9uA==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.19.1.tgz", + "integrity": "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==", "cpu": [ "riscv64" ], @@ -2183,9 +2226,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.17.0.tgz", - "integrity": "sha512-mayiYOl3LMmtO2CLn4I5lhanfxEo0LAqlT/EQyFbu1ZN3RS+Xa7Q3JEM0wBpVIyfO/pqFrjvC5LXw/mHNDEL7A==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.19.1.tgz", + "integrity": "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==", "cpu": [ "riscv64" ], @@ -2197,9 +2240,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.17.0.tgz", - "integrity": "sha512-Ow/yI+CrUHxIIhn/Y1sP/xoRKbCC3x9O1giKr3G/pjMe+TCJ5ZmfqVWU61JWwh1naC8X5Xa7uyLnbzyYqPsHfg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.19.1.tgz", + "integrity": "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==", "cpu": [ "s390x" ], @@ -2211,9 +2254,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.17.0.tgz", - "integrity": "sha512-Z4J7XlPMQOLPANyu6y3B3V417Md4LKH5bV6bhqgaG99qLHmU5LV2k9ErV14fSqoRc/GU/qOpqMdotxiJqN/YWg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.19.1.tgz", + "integrity": "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==", "cpu": [ "x64" ], @@ -2225,9 +2268,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.17.0.tgz", - "integrity": "sha512-0effK+8lhzXsgsh0Ny2ngdnTPF30v6QQzVFApJ1Ctk315YgpGkghkelvrLYYgtgeFJFrzwmOJ2nDvCrUFKsS2Q==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.19.1.tgz", + "integrity": "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==", "cpu": [ "x64" ], @@ -2239,9 +2282,9 @@ ] }, "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.17.0.tgz", - "integrity": "sha512-kFB48dRUW6RovAICZaxHKdtZe+e94fSTNA2OedXokzMctoU54NPZcv0vUX5PMqyikLIKJBIlW7laQidnAzNrDA==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.19.1.tgz", + "integrity": "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==", "cpu": [ "arm64" ], @@ -2253,9 +2296,9 @@ ] }, "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.17.0.tgz", - "integrity": "sha512-a3elKSBLPT0OoRPxTkCIIc+4xnOELolEBkPyvdj01a6PSdSmyJ1NExWjWLaXnT6wBMblvKde5RmSwEi3j+jZpg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.19.1.tgz", + "integrity": "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==", "cpu": [ "wasm32" ], @@ -2269,27 +2312,10 @@ "node": ">=14.0.0" } }, - "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.17.0.tgz", - "integrity": "sha512-4eszUsSDb9YVx0RtYkPWkxxtSZIOgfeiX//nG5cwRRArg178w4RCqEF1kbKPud9HPrp1rXh7gE4x911OhvTnPg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.19.1.tgz", + "integrity": "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==", "cpu": [ "arm64" ], @@ -2301,9 +2327,9 @@ ] }, "node_modules/@oxc-resolver/binding-win32-ia32-msvc": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.17.0.tgz", - "integrity": "sha512-t946xTXMmR7yGH0KAe9rB055/X4EPIu93JUvjchl2cizR5QbuwkUV7vLS2BS6x6sfvDoQb6rWYnV1HCci6tBSg==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-11.19.1.tgz", + "integrity": "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==", "cpu": [ "ia32" ], @@ -2315,9 +2341,9 @@ ] }, "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.17.0.tgz", - "integrity": "sha512-pX6s2kMXLQg+hlqKk5UqOW09iLLxnTkvn8ohpYp2Mhsm2yzDPCx9dyOHiB/CQixLzTkLQgWWJykN4Z3UfRKW4Q==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.19.1.tgz", + "integrity": "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==", "cpu": [ "x64" ], @@ -2394,9 +2420,9 @@ } }, "node_modules/@rc-component/mini-decimal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", - "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.0" @@ -2499,16 +2525,13 @@ } }, "node_modules/@react-aria/focus": { - "version": "3.21.3", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.3.tgz", - "integrity": "sha512-FsquWvjSCwC2/sBk4b+OqJyONETUIXQ2vM0YdPAuC+QFQh2DT6TIBo6dOZVSezlhudDla69xFBd6JvCFq1AbUw==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.22.0.tgz", + "integrity": "sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg==", "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.26.0", - "@react-aria/utils": "^3.32.0", - "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" + "react-aria": "3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", @@ -2516,80 +2539,24 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.26.0.tgz", - "integrity": "sha512-AAEcHiltjfbmP1i9iaVw34Mb7kbkiHpYdqieWufldh4aplWgsF11YQZOfaCJW4QoR2ML4Zzoa9nfFwLXA52R7Q==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.28.0.tgz", + "integrity": "sha512-OXwdU1EWFdMxmr/K1CXNGJzmNlCClByb+PuCaqUyzBymHPCGVhawirLIon/CrIN5psh3AiWpHSh4H0WeJdVpng==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-aria/utils": "^3.32.0", - "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.32.0.tgz", - "integrity": "sha512-/7Rud06+HVBIlTwmwmJa2W8xVtgxgzm0+kLbuFooZRzKDON6hhozS1dOMR/YLMxyJOaYOTpImcP4vRR9gL1hEg==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.11.0", - "@react-types/shared": "^3.32.1", + "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" + "react-aria": "3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, - "node_modules/@react-stately/flags": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", - "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", - "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, "node_modules/@react-types/shared": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", - "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.34.0.tgz", + "integrity": "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ==", "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" @@ -2605,9 +2572,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", "cpu": [ "arm" ], @@ -2619,9 +2586,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", "cpu": [ "arm64" ], @@ -2633,9 +2600,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], @@ -2647,9 +2614,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", "cpu": [ "x64" ], @@ -2661,9 +2628,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", "cpu": [ "arm64" ], @@ -2675,9 +2642,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", "cpu": [ "x64" ], @@ -2689,9 +2656,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", "cpu": [ "arm" ], @@ -2703,9 +2670,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", "cpu": [ "arm" ], @@ -2717,9 +2684,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", "cpu": [ "arm64" ], @@ -2731,9 +2698,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", "cpu": [ "arm64" ], @@ -2745,9 +2712,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", "cpu": [ "loong64" ], @@ -2759,9 +2726,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", "cpu": [ "loong64" ], @@ -2773,9 +2740,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", "cpu": [ "ppc64" ], @@ -2787,9 +2754,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", "cpu": [ "ppc64" ], @@ -2801,9 +2768,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", "cpu": [ "riscv64" ], @@ -2815,9 +2782,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", "cpu": [ "riscv64" ], @@ -2829,9 +2796,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", "cpu": [ "s390x" ], @@ -2843,9 +2810,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", "cpu": [ "x64" ], @@ -2857,9 +2824,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", "cpu": [ "x64" ], @@ -2871,9 +2838,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", "cpu": [ "x64" ], @@ -2885,9 +2852,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", "cpu": [ "arm64" ], @@ -2899,9 +2866,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", "cpu": [ "arm64" ], @@ -2913,9 +2880,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", "cpu": [ "ia32" ], @@ -2927,9 +2894,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", "cpu": [ "x64" ], @@ -2941,9 +2908,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", "cpu": [ "x64" ], @@ -2962,16 +2929,16 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "dev": true, "license": "MIT" }, "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -3070,12 +3037,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.18", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", - "integrity": "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==", + "version": "3.13.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz", + "integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.18" + "@tanstack/virtual-core": "3.14.0" }, "funding": { "type": "github", @@ -3100,9 +3067,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.18", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", - "integrity": "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", + "integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==", "license": "MIT", "funding": { "type": "github", @@ -3228,9 +3195,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -3330,9 +3297,9 @@ "license": "MIT" }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -3500,20 +3467,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/type-utils": "8.59.2", + "@typescript-eslint/utils": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3523,9 +3490,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.59.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -3539,16 +3506,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "engines": { @@ -3559,19 +3526,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.59.2", + "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "engines": { @@ -3582,18 +3549,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3604,9 +3571,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", "dev": true, "license": "MIT", "engines": { @@ -3617,21 +3584,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2", + "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3641,14 +3608,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", "dev": true, "license": "MIT", "engines": { @@ -3660,21 +3627,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.59.2", + "@typescript-eslint/tsconfig-utils": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3684,20 +3651,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.59.2", + "@typescript-eslint/types": "8.59.2", + "@typescript-eslint/typescript-estree": "8.59.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3707,19 +3674,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.59.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3729,6 +3696,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -3962,6 +3942,19 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", @@ -4188,9 +4181,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -4233,9 +4226,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4584,9 +4577,9 @@ "license": "MIT" }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", - "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", "dev": true, "license": "MIT", "dependencies": { @@ -4672,9 +4665,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", "dev": true, "license": "MPL-2.0", "engines": { @@ -4712,12 +4705,15 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz", + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bidi-js": { @@ -4768,9 +4764,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -4788,11 +4784,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -4812,15 +4808,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -4880,9 +4876,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", - "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "funding": [ { "type": "opencollective", @@ -5132,14 +5128,14 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -5635,9 +5631,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.283", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", - "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "version": "1.5.349", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", + "integrity": "sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==", "dev": true, "license": "ISC" }, @@ -5649,22 +5645,22 @@ "license": "MIT" }, "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -5749,16 +5745,16 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", - "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", @@ -5770,7 +5766,7 @@ "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", - "safe-array-concat": "^1.1.3" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5842,9 +5838,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5855,32 +5851,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -6011,15 +6007,15 @@ } }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -6235,24 +6231,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6812,9 +6790,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz", - "integrity": "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -6976,9 +6954,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7392,12 +7370,12 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -7821,12 +7799,13 @@ } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/js-tokens": { @@ -8051,16 +8030,6 @@ "node": ">= 6" } }, - "node_modules/knip/node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/knip/node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -8075,9 +8044,9 @@ } }, "node_modules/knip/node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", "funding": { @@ -8209,9 +8178,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8323,9 +8292,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -8577,9 +8546,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -9248,11 +9217,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -9294,9 +9263,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -9416,6 +9385,35 @@ "node": ">=10.5.0" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -9459,9 +9457,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", "dev": true, "license": "MIT" }, @@ -9687,35 +9685,35 @@ } }, "node_modules/oxc-resolver": { - "version": "11.17.0", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.17.0.tgz", - "integrity": "sha512-R5P2Tw6th+nQJdNcZGfuppBS/sM0x1EukqYffmlfX2xXLgLGCCPwu4ruEr9Sx29mrpkHgITc130Qps2JR90NdQ==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.19.1.tgz", + "integrity": "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.17.0", - "@oxc-resolver/binding-android-arm64": "11.17.0", - "@oxc-resolver/binding-darwin-arm64": "11.17.0", - "@oxc-resolver/binding-darwin-x64": "11.17.0", - "@oxc-resolver/binding-freebsd-x64": "11.17.0", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.17.0", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.17.0", - "@oxc-resolver/binding-linux-arm64-gnu": "11.17.0", - "@oxc-resolver/binding-linux-arm64-musl": "11.17.0", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.17.0", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.17.0", - "@oxc-resolver/binding-linux-riscv64-musl": "11.17.0", - "@oxc-resolver/binding-linux-s390x-gnu": "11.17.0", - "@oxc-resolver/binding-linux-x64-gnu": "11.17.0", - "@oxc-resolver/binding-linux-x64-musl": "11.17.0", - "@oxc-resolver/binding-openharmony-arm64": "11.17.0", - "@oxc-resolver/binding-wasm32-wasi": "11.17.0", - "@oxc-resolver/binding-win32-arm64-msvc": "11.17.0", - "@oxc-resolver/binding-win32-ia32-msvc": "11.17.0", - "@oxc-resolver/binding-win32-x64-msvc": "11.17.0" + "@oxc-resolver/binding-android-arm-eabi": "11.19.1", + "@oxc-resolver/binding-android-arm64": "11.19.1", + "@oxc-resolver/binding-darwin-arm64": "11.19.1", + "@oxc-resolver/binding-darwin-x64": "11.19.1", + "@oxc-resolver/binding-freebsd-x64": "11.19.1", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", + "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", + "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", + "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", + "@oxc-resolver/binding-linux-x64-musl": "11.19.1", + "@oxc-resolver/binding-openharmony-arm64": "11.19.1", + "@oxc-resolver/binding-wasm32-wasi": "11.19.1", + "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", + "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", + "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "node_modules/p-limit": { @@ -9795,13 +9793,13 @@ "license": "MIT" }, "node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "entities": "^8.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -9834,9 +9832,9 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -9844,7 +9842,7 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -9990,6 +9988,27 @@ "postcss": "^8.0.0" } }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/postcss-js": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", @@ -10845,6 +10864,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-aria": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.48.0.tgz", + "integrity": "sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.1", + "@internationalized/number": "^3.6.6", + "@internationalized/string": "^3.2.8", + "@react-types/shared": "^3.34.0", + "@swc/helpers": "^0.5.0", + "aria-hidden": "^1.2.3", + "clsx": "^2.0.0", + "react-stately": "3.46.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/react-copy-to-clipboard": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.1.tgz", @@ -10859,9 +10899,9 @@ } }, "node_modules/react-day-picker": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", - "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.2.tgz", + "integrity": "sha512-LK68OTbHB3oJNhl9cA0qVizzp3o26w61YSjAFkYi67N86iro32wx86kSNeFU/hq+gI8m1yzWhnomMLfZ041RzQ==", "license": "MIT", "funding": { "type": "individual", @@ -10869,7 +10909,7 @@ }, "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-dom": { @@ -10946,6 +10986,23 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-stately": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.46.0.tgz", + "integrity": "sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.1", + "@internationalized/number": "^3.6.6", + "@internationalized/string": "^3.2.8", + "@react-types/shared": "^3.34.0", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/react-syntax-highlighter": { "version": "15.6.6", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", @@ -11308,12 +11365,16 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -11358,9 +11419,9 @@ } }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", "dev": true, "license": "MIT", "dependencies": { @@ -11374,31 +11435,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" } }, @@ -11426,15 +11487,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -11512,9 +11573,9 @@ } }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, "license": "ISC", "bin": { @@ -11662,14 +11723,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -12037,9 +12098,9 @@ } }, "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", "license": "MIT" }, "node_modules/sucrase": { @@ -12177,16 +12238,46 @@ "node": ">= 6" } }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", "dev": true, "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", - "minimatch": "^9.0.4" + "minimatch": "^10.2.2" }, "engines": { "node": ">=18" @@ -12243,13 +12334,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -12289,22 +12380,22 @@ } }, "node_modules/tldts": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.21.tgz", - "integrity": "sha512-Plu6V8fF/XU6d2k8jPtlQf5F4Xx2hAin4r2C2ca7wR8NK5MbRTo9huLUWRe28f3Uk8bYZfg74tit/dSjc18xnw==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.21" + "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.21", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.21.tgz", - "integrity": "sha512-oVOMdHvgjqyzUZH1rOESgJP1uNe2bVrfK0jUHHmiM2rpEiRbf3j4BrsIc6JigJRbHGanQwuZv/R+LTcHsw+bLA==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", "dev": true, "license": "MIT" }, @@ -12337,9 +12428,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12389,9 +12480,9 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -12719,6 +12810,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13233,6 +13333,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "extraneous": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/uv.lock b/uv.lock index dde75bc1383..c6338c213e7 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-02T19:27:53.095661475Z" exclude-newer-span = "P3D" [manifest] @@ -486,7 +486,7 @@ wheels = [ [[package]] name = "black" -version = "24.10.0" +version = "26.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -494,28 +494,33 @@ dependencies = [ { name = "packaging" }, { name = "pathspec" }, { name = "platformdirs" }, + { name = "pytokens" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0d/cc2fb42b8c50d80143221515dd7e4766995bd07c56c9a3ed30baf080b6dc/black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", size = 645813, upload-time = "2024-10-07T19:20:50.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/f3/465c0eb5cddf7dbbfe1fecd9b875d1dcf51b88923cd2c1d7e9ab95c6336b/black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", size = 1623211, upload-time = "2024-10-07T19:26:12.43Z" }, - { url = "https://files.pythonhosted.org/packages/df/57/b6d2da7d200773fdfcc224ffb87052cf283cec4d7102fab450b4a05996d8/black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", size = 1457139, upload-time = "2024-10-07T19:25:06.453Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c5/9023b7673904a5188f9be81f5e129fff69f51f5515655fbd1d5a4e80a47b/black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", size = 1753774, upload-time = "2024-10-07T19:23:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/e1/32/df7f18bd0e724e0d9748829765455d6643ec847b3f87e77456fc99d0edab/black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e", size = 1414209, upload-time = "2024-10-07T19:24:42.54Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cc/7496bb63a9b06a954d3d0ac9fe7a73f3bf1cd92d7a58877c27f4ad1e9d41/black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", size = 1607468, upload-time = "2024-10-07T19:26:14.966Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e3/69a738fb5ba18b5422f50b4f143544c664d7da40f09c13969b2fd52900e0/black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", size = 1437270, upload-time = "2024-10-07T19:25:24.291Z" }, - { url = "https://files.pythonhosted.org/packages/c9/9b/2db8045b45844665c720dcfe292fdaf2e49825810c0103e1191515fc101a/black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", size = 1737061, upload-time = "2024-10-07T19:23:52.18Z" }, - { url = "https://files.pythonhosted.org/packages/a3/95/17d4a09a5be5f8c65aa4a361444d95edc45def0de887810f508d3f65db7a/black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", size = 1423293, upload-time = "2024-10-07T19:24:41.7Z" }, - { url = "https://files.pythonhosted.org/packages/90/04/bf74c71f592bcd761610bbf67e23e6a3cff824780761f536512437f1e655/black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", size = 1644256, upload-time = "2024-10-07T19:27:53.355Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ea/a77bab4cf1887f4b2e0bce5516ea0b3ff7d04ba96af21d65024629afedb6/black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", size = 1448534, upload-time = "2024-10-07T19:26:44.953Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3e/443ef8bc1fbda78e61f79157f303893f3fddf19ca3c8989b163eb3469a12/black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", size = 1761892, upload-time = "2024-10-07T19:24:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/52/93/eac95ff229049a6901bc84fec6908a5124b8a0b7c26ea766b3b8a5debd22/black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", size = 1434796, upload-time = "2024-10-07T19:25:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a0/a993f58d4ecfba035e61fca4e9f64a2ecae838fc9f33ab798c62173ed75c/black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", size = 1643986, upload-time = "2024-10-07T19:28:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/37/d5/602d0ef5dfcace3fb4f79c436762f130abd9ee8d950fa2abdbf8bbc555e0/black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", size = 1448085, upload-time = "2024-10-07T19:28:12.093Z" }, - { url = "https://files.pythonhosted.org/packages/47/6d/a3a239e938960df1a662b93d6230d4f3e9b4a22982d060fc38c42f45a56b/black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", size = 1760928, upload-time = "2024-10-07T19:24:15.233Z" }, - { url = "https://files.pythonhosted.org/packages/dd/cf/af018e13b0eddfb434df4d9cd1b2b7892bab119f7a20123e93f6910982e8/black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", size = 1436875, upload-time = "2024-10-07T19:24:42.762Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a7/4b27c50537ebca8bec139b872861f9d2bf501c5ec51fcf897cb924d9e264/black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", size = 206898, upload-time = "2024-10-07T19:20:48.317Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -1263,15 +1268,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, ] -[[package]] -name = "diskcache" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -3101,9 +3097,6 @@ dependencies = [ ] [package.optional-dependencies] -caching = [ - { name = "diskcache" }, -] extra-proxy = [ { name = "a2a-sdk" }, { name = "azure-identity" }, @@ -3277,7 +3270,6 @@ requires-dist = [ { name = "cryptography", marker = "extra == 'proxy'", specifier = "==46.0.7" }, { name = "ddtrace", marker = "extra == 'proxy-runtime'", specifier = "==2.19.0" }, { name = "detect-secrets", marker = "extra == 'proxy-runtime'", specifier = "==1.5.0" }, - { name = "diskcache", marker = "extra == 'caching'", specifier = "==5.6.3" }, { name = "fastapi", marker = "extra == 'proxy'", specifier = "==0.124.4" }, { name = "fastapi-sso", marker = "extra == 'proxy'", specifier = "==0.19.0" }, { name = "fastuuid", specifier = "==0.14.0" }, @@ -3365,7 +3357,7 @@ ci = [ { name = "traceloop-sdk", specifier = "==0.33.12" }, ] dev = [ - { name = "black", specifier = "==24.10.0" }, + { name = "black", specifier = "==26.3.1" }, { name = "diff-cover", specifier = "==9.7.2" }, { name = "fakeredis", specifier = "==2.34.1" }, { name = "fastapi-offline", specifier = "==1.7.6" }, @@ -6025,6 +6017,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, ] +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + [[package]] name = "pytz" version = "2026.1.post1" From 6769c8b66fe5b9d280c590a882133bcdd20af844 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Tue, 5 May 2026 20:45:08 +0000 Subject: [PATCH 2/7] [Fix] CI: license check for black 26.3.1 + skip disk_cache test without diskcache - Add black to liccheck.ini Authorized Packages (MIT-licensed). - pytest.importorskip("diskcache") at top of test_disk_cache.py so the test skips cleanly when diskcache isn't installed (it's no longer pulled in by the dev group after the CVE-2025-69872 mitigation). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/code_coverage_tests/liccheck.ini | 1 + tests/test_litellm/caching/test_disk_cache.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/code_coverage_tests/liccheck.ini b/tests/code_coverage_tests/liccheck.ini index 2100aa1377f..23e31f8886b 100644 --- a/tests/code_coverage_tests/liccheck.ini +++ b/tests/code_coverage_tests/liccheck.ini @@ -171,3 +171,4 @@ langgraph: >=1.0.10 # MIT License langgraph-prebuilt: >=1.0.8 # MIT License - https://github.com/langchain-ai/langgraph/blob/main/LICENSE pytest-rerunfailures: >=15.1 # MPL 2.0 license pytest-recording: >=0.13.4 # MIT license +black: >=24.10.0 # MIT License - https://github.com/psf/black/blob/main/LICENSE diff --git a/tests/test_litellm/caching/test_disk_cache.py b/tests/test_litellm/caching/test_disk_cache.py index d2b6fef5385..470e2064c38 100644 --- a/tests/test_litellm/caching/test_disk_cache.py +++ b/tests/test_litellm/caching/test_disk_cache.py @@ -4,6 +4,8 @@ import tempfile import pytest +pytest.importorskip("diskcache") + sys.path.insert(0, os.path.abspath("../../..")) from litellm.caching.disk_cache import DiskCache From 5bafa8b3a23c96d720fdc0f8f6eb380787362bfd Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 7 May 2026 23:04:52 +0000 Subject: [PATCH 3/7] Drop dep bumps + black-26 reformat to clear fork CI policy PR was blocked by .github/workflows/guard-fork-dependencies.yml: fork PRs cannot modify uv.lock. Reverting: - uv.lock + pyproject.toml black bump (24.10.0 -> 26.3.1) and the 295 files of mechanical Black 26 reformat coupled to it - pyproject.toml diskcache extra change (kept the runtime mitigation in litellm/caching/disk_cache.py via JSONDisk) Kept: - Dockerfile cache narrowing (drops ~660 MB of uv build cache that surfaced cached setuptools as CVE findings) - litellm/caching/disk_cache.py: dc.JSONDisk to neutralize CVE-2025-69872 - ui/litellm-dashboard/package-lock.json + litellm-js/spend-logs/package-lock.json: next/postcss/hono/uuid CVE bumps (these are not blocked by the fork guard) - tests/test_litellm/caching/test_disk_cache.py - tests/code_coverage_tests/liccheck.ini: harmless black authorization Black + gitpython + langchain dep upgrades will need a follow-up from a maintainer pushing a branch in the canonical BerriAI/litellm repo. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...to_update_price_and_context_window_file.py | 90 +-- .../workflows/run_llm_translation_tests.py | 512 ++++++------- ci_cd/run_migration.py | 1 + cookbook/benchmark/benchmark.py | 1 + .../braintrust_prompt_wrapper_server.py | 1 + cookbook/livekit_agent_sdk/main.py | 2 +- cookbook/misc/migrate_proxy_config.py | 4 +- .../mock_bedrock_guardrail_server.py | 6 +- db_scripts/create_views.py | 6 +- .../enterprise_callbacks/callback_controls.py | 139 ++-- .../send_emails/base_email.py | 14 +- .../send_emails/sendgrid_email.py | 3 +- .../send_emails/smtp_email.py | 1 - .../litellm_core_utils/litellm_logging.py | 1 - .../proxy/audit_logging_endpoints.py | 10 +- .../litellm_enterprise/proxy/auth/__init__.py | 2 +- .../proxy/common_utils/check_batch_cost.py | 70 +- .../common_utils/check_responses_cost.py | 33 +- .../proxy/hooks/managed_vector_stores.py | 92 +-- .../internal_user_endpoints.py | 1 + .../proxy/vector_stores/endpoints.py | 6 +- .../types/enterprise_callbacks/send_emails.py | 12 +- litellm/_uuid.py | 1 + .../exceptions/exception_mapping_utils.py | 1 + .../exceptions/exceptions.py | 1 + .../transformation.py | 2 +- litellm/compression/content_detection.py | 1 + litellm/files/types.py | 1 + litellm/google_genai/adapters/__init__.py | 4 +- .../SlackAlerting/batching_handler.py | 6 +- litellm/integrations/SlackAlerting/utils.py | 2 +- .../integrations/additional_logging_utils.py | 2 +- litellm/integrations/custom_batch_logger.py | 2 +- litellm/integrations/focus/transformer.py | 1 + litellm/integrations/opik/utils.py | 2 +- litellm/integrations/s3_v2.py | 4 +- litellm/interactions/__init__.py | 12 +- litellm/interactions/main.py | 10 +- litellm/litellm_core_utils/litellm_logging.py | 12 +- .../prompt_templates/factory.py | 4 +- .../specialty_caches/dynamic_logging_cache.py | 4 +- .../messages/agentic_streaming_iterator.py | 1 + .../azure/chat/o_series_transformation.py | 8 +- .../azure_ai/embed/cohere_transformation.py | 2 +- .../llms/azure_ai/rerank/transformation.py | 2 +- .../embed/amazon_titan_g1_transformation.py | 2 +- .../bedrock/embed/cohere_transformation.py | 2 +- .../bedrock_mantle/chat/transformation.py | 1 + litellm/llms/cohere/embed/handler.py | 2 +- litellm/llms/custom_httpx/mock_transport.py | 1 + litellm/llms/dashscope/cost_calculator.py | 2 +- litellm/llms/datarobot/chat/transformation.py | 2 +- .../llms/deepinfra/rerank/transformation.py | 2 +- litellm/llms/deepseek/cost_calculator.py | 2 +- .../text_to_speech/transformation.py | 1 + litellm/llms/gemini/videos/transformation.py | 2 +- .../llms/infinity/rerank/transformation.py | 2 +- litellm/llms/jina_ai/rerank/transformation.py | 2 +- .../llms/lm_studio/embed/transformation.py | 2 +- litellm/llms/novita/chat/transformation.py | 2 +- .../llms/nvidia_nim/chat/transformation.py | 4 +- litellm/llms/nvidia_nim/embed.py | 2 +- .../openai/chat/o_series_transformation.py | 12 +- litellm/llms/openai/common_utils.py | 2 +- .../image_generation/transformation.py | 1 + .../sagemaker/completion/transformation.py | 2 +- .../sagemaker/embedding/transformation.py | 2 +- litellm/llms/sap/credentials.py | 2 +- litellm/llms/snowflake/chat/transformation.py | 1 + litellm/llms/together_ai/chat.py | 2 +- litellm/llms/together_ai/embed.py | 2 +- .../llms/together_ai/rerank/transformation.py | 2 +- .../context_caching/transformation.py | 4 +- .../llms/vertex_ai/gemini/transformation.py | 6 +- .../batch_embed_content_transformation.py | 2 +- .../text_to_speech/text_to_speech_handler.py | 2 +- .../llms/vllm/completion/transformation.py | 2 +- .../embedding/transformation_contextual.py | 4 +- .../proxy/common_utils/custom_openapi_spec.py | 6 +- .../proxy/common_utils/http_parsing_utils.py | 2 +- .../common_utils/openai_endpoint_utils.py | 2 +- .../pass_through_endpoints.py | 2 +- litellm/proxy/db/create_views.py | 6 +- litellm/proxy/guardrails/_content_utils.py | 1 + .../guardrail_hooks/akto/__init__.py | 1 + .../proxy/hooks/litellm_skills/__init__.py | 2 +- .../budget_management_endpoints.py | 4 +- .../customer_endpoints.py | 4 +- .../model_management_endpoints.py | 2 +- .../sso/custom_microsoft_sso.py | 2 +- .../management_endpoints/team_endpoints.py | 4 +- .../user_agent_analytics_endpoints.py | 2 +- .../cursor_passthrough_logging_handler.py | 1 + litellm/proxy/proxy_cli.py | 11 +- litellm/proxy/proxy_server.py | 6 +- .../spend_management_endpoints.py | 6 +- litellm/proxy/utils.py | 12 +- .../vertex_ai_endpoints/langfuse_endpoints.py | 2 +- litellm/router.py | 2 +- .../router_strategy/adaptive_router/hooks.py | 2 +- .../adaptive_router/signals.py | 1 + litellm/router_strategy/budget_limiter.py | 8 +- litellm/router_utils/get_retry_from_policy.py | 2 +- .../router_utils/pattern_match_deployments.py | 2 +- .../track_deployment_metrics.py | 2 +- litellm/secret_managers/aws_secret_manager.py | 2 +- .../secret_managers/aws_secret_manager_v2.py | 2 +- litellm/vector_store_files/utils.py | 4 +- pyproject.toml | 8 +- scripts/adaptive_router_demo/eval.py | 54 +- scripts/adaptive_router_demo/traffic.py | 59 +- scripts/benchmark_mock.py | 1 + scripts/benchmark_proxy_vs_provider.py | 2 +- scripts/eval_compression.py | 180 +++-- .../benchmark_get_all_latest_health_checks.py | 2 +- .../test_batch_custom_pricing.py | 1 + .../test_hosted_vllm_batches_and_files.py | 1 + tests/benchmarks/test_benchmarks.py | 1 + .../check_fastuuid_usage.py | 1 + tests/enterprise/conftest.py | 2 + .../integrations/test_prometheus.py | 86 +-- .../proxy/auth/test_user_api_key_auth.py | 11 +- .../test_apply_guardrail_endpoint.py | 4 +- .../proxy/hooks/test_managed_files.py | 674 ++++++++---------- .../test_internal_user_endpoints.py | 63 +- .../test_project_endpoints_prisma.py | 4 +- .../guardrails_tests/test_akto_guardrails.py | 1 + .../guardrails_tests/test_custom_guardrail.py | 1 + .../test_eu_ai_act_article5.py | 1 + .../test_sg_mas_ai_guardrails.py | 1 + .../test_sg_pdpa_guardrails.py | 1 + .../image_gen_tests/test_image_generation.py | 1 + tests/image_gen_tests/test_image_variation.py | 1 + .../llms/bedrock/embed/test_embedding.py | 1 + .../llms/bedrock/test_nova_imported_models.py | 1 + .../vertex_ai/test_gemini_batch_embeddings.py | 16 +- tests/litellm/test_bedrock_nemotron_super.py | 1 + .../test_bedrock_converse_dedup_factory.py | 19 +- .../test_aws_secret_manager.py | 1 + .../test_litellm_overhead.py | 1 + .../test_anthropic_responses_api.py | 1 + ...est_anthropic_tool_result_empty_call_id.py | 2 +- ...t_base_responses_api_streaming_iterator.py | 4 +- .../test_responses_hooks.py | 4 +- .../test_bedrock_anthropic_regression.py | 8 +- .../test_bedrock_invoke_tests.py | 1 + tests/llm_translation/test_crusoe.py | 7 +- tests/llm_translation/test_gemini.py | 21 +- .../test_gemini_image_usage.py | 2 +- .../create_mock_standard_logging_payload.py | 1 + .../test_amazing_vertex_completion.py | 9 +- tests/local_testing/test_cache_preset_key.py | 2 +- tests/local_testing/test_caching.py | 7 +- tests/local_testing/test_completion.py | 6 +- .../test_configs/custom_callbacks.py | 6 +- tests/local_testing/test_get_llm_provider.py | 1 + tests/local_testing/test_langsmith.py | 1 + tests/local_testing/test_router_caching.py | 1 + .../create_mock_standard_logging_payload.py | 1 + .../test_bedrock_knowledgebase_hook.py | 1 + .../test_datadog_llm_obs.py | 1 + .../test_gcs_pub_sub.py | 1 + .../test_generic_api_callback.py | 1 + .../test_langsmith_unit_test.py | 1 + .../test_log_db_redis_services.py | 1 + .../test_view_request_resp_logs.py | 1 + tests/mcp_tests/test_mcp_logging.py | 1 + tests/mcp_tests/test_mcp_server.py | 1 + tests/mcp_tests/test_proxy_mcp_e2e.py | 1 + tests/ocr_tests/base_ocr_unit_tests.py | 1 + .../test_ocr_azure_document_intelligence.py | 5 +- .../tests/bursty_load_test_completion.py | 1 + .../tests/load_test_embedding_100.py | 1 + .../test_openai_request_with_traceparent.py | 1 + .../test_openai_batches_endpoint.py | 1 + .../test_openai_files_endpoints.py | 1 + tests/otel_tests/test_e2e_model_access.py | 1 + .../test_team_member_permissions.py | 12 +- .../test_openai_assistants_passthrough.py | 1 + tests/pass_through_tests/test_vertex_ai.py | 1 + ..._anthropic_messages_prompt_caching_test.py | 8 +- .../test_route_check_unit_tests.py | 1 + .../test_usage_endpoints.py | 4 +- .../test_claude_agent_sdk.py | 1 + tests/proxy_unit_tests/conftest.py | 1 + .../test_configs/custom_callbacks.py | 6 +- .../proxy_unit_tests/test_jwt_key_mapping.py | 1 + .../test_search_api_logging.py | 2 +- .../create_mock_standard_logging_payload.py | 1 + .../test_router_handle_error.py | 1 + tests/search_tests/test_google_pse_search.py | 1 + .../test_callbacks_in_db.py | 4 +- .../test_bedrock_agentcore_a2a.py | 1 + .../send_emails/test_base_email.py | 179 ++--- .../send_emails/test_resend_email.py | 11 +- .../send_emails/test_sendgrid_email.py | 6 +- .../test_callback_controls.py | 328 +++------ .../test_batch_retrieve_input_file_id.py | 23 +- ..._retrieve_returns_unified_input_file_id.py | 4 +- .../proxy/test_enterprise_routes.py | 48 +- .../proxy/test_file_deletion_blocking.py | 188 +++-- .../proxy/test_managed_files_hook.py | 18 +- .../google_genai/test_google_genai_adapter.py | 1 - .../test_google_genai_adapter_fixes.py | 1 - .../google_genai/test_google_genai_handler.py | 1 - .../google_genai/test_google_genai_main.py | 1 - .../test_google_genai_transformation.py | 1 - .../arize/test_arize_otel_coexistence.py | 1 + .../dotprompt/test_prompt_manager.py | 18 +- .../integrations/test_custom_guardrail.py | 9 +- .../test_websearch_thinking_constraint.py | 1 + .../test_azure_assistant_cost_tracking.py | 2 +- .../litellm_core_utils/test_core_helpers.py | 24 +- .../test_streaming_handler.py | 14 +- .../litellm_core_utils/test_token_counter.py | 1 + ...al_pass_through_adapters_transformation.py | 4 +- .../test_agentic_streaming_iterator.py | 1 + .../messages/test_parallel_tool_calls.py | 1 + .../test_reasoning_auto_summary_messages.py | 22 +- .../llms/azure/test_azure_cost_calculation.py | 1 + .../azure_ai/test_azure_ai_cost_calculator.py | 25 +- .../test_managed_resource_isolation.py | 1 + .../llms/bedrock/chat/test_invoke_handler.py | 1 + tests/test_litellm/llms/crusoe/test_crusoe.py | 13 +- .../custom_httpx/test_aiohttp_so_keepalive.py | 5 +- .../llms/custom_httpx/test_mock_transport.py | 1 + .../llms/databricks/test_databricks_e2e.py | 6 +- ...gram_audio_transcription_transformation.py | 2 +- .../test_github_copilot_authenticator.py | 64 +- .../test_huggingface_embedding_handler.py | 1 + .../test_moonshot_chat_transformation.py | 4 +- .../test_perplexity_cost_calculator.py | 2 +- .../perplexity/test_perplexity_integration.py | 2 +- ...eway_audio_transcription_transformation.py | 1 + .../llms/test_polling_url_origin_match.py | 1 + .../llms/test_predibase_transformation.py | 25 +- .../gemini/test_context_circulation.py | 1 + ...test_vertex_and_google_ai_studio_gemini.py | 29 +- ...test_batch_embed_content_transformation.py | 14 +- .../vertex_ai/test_vertex_ai_common_utils.py | 6 +- .../test_vertex_model_garden_openapi.py | 5 +- .../auth/test_user_api_key_auth_mcp.py | 4 +- .../mcp_server/test_db_credentials.py | 1 + .../mcp_server/test_is_tool_name_prefixed.py | 1 + .../mcp_server/test_mcp_stale_session.py | 116 ++- .../mcp_server/test_semantic_tool_filter.py | 28 +- .../test_agent_header_isolation.py | 1 + .../agent_endpoints/test_agent_headers.py | 1 + .../proxy/auth/test_onboarding.py | 1 + .../proxy/common_utils/test_cache_codec.py | 8 +- tests/test_litellm/proxy/conftest.py | 1 + .../test_google_api_endpoints.py | 1 - .../content_filter/test_content_filter.py | 6 +- .../openai/test_moderations.py | 1 - .../proxy/guardrails/test_content_utils.py | 1 + .../guardrails/test_custom_code_security.py | 1 + .../proxy/guardrails/test_llm_as_a_judge.py | 61 +- .../test_qostodian_nexus_guardrail.py | 6 +- .../proxy/hooks/test_batch_file_validation.py | 1 + .../test_activity_tenant_scoping.py | 1 + .../test_budget_endpoints.py | 1 + .../test_delete_verification_tokens_failed.py | 1 + .../test_project_org_authz.py | 1 + .../test_team_default_params.py | 1 + .../test_workflow_management_endpoints.py | 1 + .../usage_endpoints/test_ai_usage_chat.py | 1 + .../test_llm_pass_through_endpoints.py | 6 +- .../proxy/test_fastapi_offline_routes.py | 2 +- ...test_filter_models_by_team_access_group.py | 4 +- .../proxy/test_model_level_guardrails.py | 1 + .../proxy/test_redis_auth_cache_flag.py | 1 + .../test_litellm/proxy/test_team_org_move.py | 31 +- .../test_responses_api_bridge_flag.py | 16 +- .../adaptive_router/test_router_dispatch.py | 4 +- .../test_acompletion_session_reuse_e2e.py | 1 + .../test_anthropic_skills_transformation.py | 1 + .../test_dashscope_image_generation.py | 1 + .../test_deepseek_model_metadata.py | 1 + tests/test_litellm/test_model_cost_aliases.py | 1 + tests/test_litellm/test_nested_drop_params.py | 1 + ...penai_embedding_encoding_format_default.py | 4 +- .../test_litellm/test_router_google_genai.py | 1 - .../test_streaming_connection_cleanup.py | 1 + tests/test_litellm/test_utils.py | 24 +- tests/test_litellm/types/test_completion.py | 2 +- .../test_prometheus_label_value_sanitize.py | 3 +- tests/test_passthrough_endpoints.py | 1 + .../vector_store_tests/rag/base_rag_tests.py | 4 +- .../rag/test_rag_vertex_ai.py | 8 +- .../test_milvus_vector_store.py | 1 + .../test_vertex_ai_search_api_vector_store.py | 1 + .../fixtures/mock_llm_server/server.py | 1 + 292 files changed, 1814 insertions(+), 2305 deletions(-) diff --git a/.github/workflows/auto_update_price_and_context_window_file.py b/.github/workflows/auto_update_price_and_context_window_file.py index b92a0568e37..461d8d347d9 100644 --- a/.github/workflows/auto_update_price_and_context_window_file.py +++ b/.github/workflows/auto_update_price_and_context_window_file.py @@ -2,7 +2,6 @@ import asyncio import aiohttp import json - # Asynchronously fetch data from a given URL async def fetch_data(url): try: @@ -16,24 +15,22 @@ async def fetch_data(url): resp_json = await resp.json() print("Fetch the data from URL.") # Return the 'data' field from the JSON response - return resp_json["data"] + return resp_json['data'] except Exception as e: # Print an error message if fetching data fails print("Error fetching data from URL:", e) return None - # Synchronize local data with remote data def sync_local_data_with_remote(local_data, remote_data): # Update existing keys in local_data with values from remote_data - for key in set(local_data) & set(remote_data): + for key in (set(local_data) & set(remote_data)): local_data[key].update(remote_data[key]) # Add new keys from remote_data to local_data - for key in set(remote_data) - set(local_data): + for key in (set(remote_data) - set(local_data)): local_data[key] = remote_data[key] - # Write data to the json file def write_to_file(file_path, data): try: @@ -46,7 +43,6 @@ def write_to_file(file_path, data): # Print an error message if writing to file fails print("Error updating JSON file:", e) - # Update the existing models and add the missing models for OpenRouter def transform_openrouter_data(data): transformed = {} @@ -58,41 +54,33 @@ def transform_openrouter_data(data): } # Add 'max_output_tokens' as a field if it is not None - if ( - "top_provider" in row - and "max_completion_tokens" in row["top_provider"] - and row["top_provider"]["max_completion_tokens"] is not None - ): - obj["max_output_tokens"] = int(row["top_provider"]["max_completion_tokens"]) + if "top_provider" in row and "max_completion_tokens" in row["top_provider"] and row["top_provider"]["max_completion_tokens"] is not None: + obj['max_output_tokens'] = int(row["top_provider"]["max_completion_tokens"]) # Add the field 'output_cost_per_token' - obj.update( - { - "output_cost_per_token": float(row["pricing"]["completion"]), - } - ) + obj.update({ + "output_cost_per_token": float(row["pricing"]["completion"]), + }) # Add field 'input_cost_per_image' if it exists and is non-zero - if ( - "pricing" in row - and "image" in row["pricing"] - and float(row["pricing"]["image"]) != 0.0 - ): - obj["input_cost_per_image"] = float(row["pricing"]["image"]) + if "pricing" in row and "image" in row["pricing"] and float(row["pricing"]["image"]) != 0.0: + obj['input_cost_per_image'] = float(row["pricing"]["image"]) # Add the fields 'litellm_provider' and 'mode' - obj.update({"litellm_provider": "openrouter", "mode": "chat"}) + obj.update({ + "litellm_provider": "openrouter", + "mode": "chat" + }) # Add the 'supports_vision' field if the modality is 'multimodal' - if row.get("architecture", {}).get("modality") == "multimodal": - obj["supports_vision"] = True + if row.get('architecture', {}).get('modality') == 'multimodal': + obj['supports_vision'] = True # Use a composite key to store the transformed object transformed[f'openrouter/{row["id"]}'] = obj return transformed - # Update the existing models and add the missing models for Vercel AI Gateway def transform_vercel_ai_gateway_data(data): transformed = {} @@ -101,30 +89,20 @@ def transform_vercel_ai_gateway_data(data): "max_tokens": row["context_window"], "input_cost_per_token": float(row["pricing"]["input"]), "output_cost_per_token": float(row["pricing"]["output"]), - "max_output_tokens": row["max_tokens"], - "max_input_tokens": row["context_window"], + 'max_output_tokens': row['max_tokens'], + 'max_input_tokens': row["context_window"], } # Handle cache pricing if available if "pricing" in row: - if ( - "input_cache_read" in row["pricing"] - and row["pricing"]["input_cache_read"] is not None - ): - obj["cache_read_input_token_cost"] = float( - f"{float(row['pricing']['input_cache_read']):e}" - ) - - if ( - "input_cache_write" in row["pricing"] - and row["pricing"]["input_cache_write"] is not None - ): - obj["cache_creation_input_token_cost"] = float( - f"{float(row['pricing']['input_cache_write']):e}" - ) + if "input_cache_read" in row["pricing"] and row["pricing"]["input_cache_read"] is not None: + obj['cache_read_input_token_cost'] = float(f"{float(row['pricing']['input_cache_read']):e}") + + if "input_cache_write" in row["pricing"] and row["pricing"]["input_cache_write"] is not None: + obj['cache_creation_input_token_cost'] = float(f"{float(row['pricing']['input_cache_write']):e}") mode = "embedding" if "embedding" in row["id"].lower() else "chat" - + obj.update({"litellm_provider": "vercel_ai_gateway", "mode": mode}) transformed[f'vercel_ai_gateway/{row["id"]}'] = obj @@ -148,31 +126,24 @@ def load_local_data(file_path): print("Error decoding JSON:", e) return None - def main(): - local_file_path = ( - "model_prices_and_context_window.json" # Path to the local data file - ) - openrouter_url = ( - "https://openrouter.ai/api/v1/models" # URL to fetch OpenRouter data - ) - vercel_ai_gateway_url = ( - "https://ai-gateway.vercel.sh/v1/models" # URL to fetch Vercel AI Gateway data - ) + local_file_path = "model_prices_and_context_window.json" # Path to the local data file + openrouter_url = "https://openrouter.ai/api/v1/models" # URL to fetch OpenRouter data + vercel_ai_gateway_url = "https://ai-gateway.vercel.sh/v1/models" # URL to fetch Vercel AI Gateway data # Load local data from file local_data = load_local_data(local_file_path) - + # Fetch OpenRouter data openrouter_data = asyncio.run(fetch_data(openrouter_url)) # Transform the fetched OpenRouter data openrouter_data = transform_openrouter_data(openrouter_data) - + # Fetch Vercel AI Gateway data vercel_data = asyncio.run(fetch_data(vercel_ai_gateway_url)) # Transform the fetched Vercel AI Gateway data vercel_data = transform_vercel_ai_gateway_data(vercel_data) - + # Combine both datasets all_remote_data = {**openrouter_data, **vercel_data} @@ -183,7 +154,6 @@ def main(): else: print("Failed to fetch model data from either local file or URL.") - # Entry point of the script if __name__ == "__main__": main() diff --git a/.github/workflows/run_llm_translation_tests.py b/.github/workflows/run_llm_translation_tests.py index 22be769a739..3f3a70efe92 100644 --- a/.github/workflows/run_llm_translation_tests.py +++ b/.github/workflows/run_llm_translation_tests.py @@ -16,75 +16,64 @@ from pathlib import Path import json from typing import Dict, List, Tuple, Optional - # ANSI color codes for terminal output class Colors: - GREEN = "\033[92m" - RED = "\033[91m" - YELLOW = "\033[93m" - BLUE = "\033[94m" - PURPLE = "\033[95m" - CYAN = "\033[96m" - RESET = "\033[0m" - BOLD = "\033[1m" - + GREEN = '\033[92m' + RED = '\033[91m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + PURPLE = '\033[95m' + CYAN = '\033[96m' + RESET = '\033[0m' + BOLD = '\033[1m' def print_colored(message: str, color: str = Colors.RESET): """Print colored message to terminal""" print(f"{color}{message}{Colors.RESET}") - def get_provider_from_test_file(test_file: str) -> str: """Map test file names to provider names""" provider_mapping = { - "test_anthropic": "Anthropic", - "test_azure": "Azure", - "test_bedrock": "AWS Bedrock", - "test_openai": "OpenAI", - "test_vertex": "Google Vertex AI", - "test_gemini": "Google Vertex AI", - "test_cohere": "Cohere", - "test_databricks": "Databricks", - "test_groq": "Groq", - "test_together": "Together AI", - "test_mistral": "Mistral", - "test_deepseek": "DeepSeek", - "test_replicate": "Replicate", - "test_huggingface": "HuggingFace", - "test_fireworks": "Fireworks AI", - "test_perplexity": "Perplexity", - "test_cloudflare": "Cloudflare", - "test_voyage": "Voyage AI", - "test_xai": "xAI", - "test_nvidia": "NVIDIA", - "test_watsonx": "IBM watsonx", - "test_azure_ai": "Azure AI", - "test_snowflake": "Snowflake", - "test_infinity": "Infinity", - "test_jina": "Jina AI", - "test_deepgram": "Deepgram", - "test_clarifai": "Clarifai", - "test_triton": "Triton", + 'test_anthropic': 'Anthropic', + 'test_azure': 'Azure', + 'test_bedrock': 'AWS Bedrock', + 'test_openai': 'OpenAI', + 'test_vertex': 'Google Vertex AI', + 'test_gemini': 'Google Vertex AI', + 'test_cohere': 'Cohere', + 'test_databricks': 'Databricks', + 'test_groq': 'Groq', + 'test_together': 'Together AI', + 'test_mistral': 'Mistral', + 'test_deepseek': 'DeepSeek', + 'test_replicate': 'Replicate', + 'test_huggingface': 'HuggingFace', + 'test_fireworks': 'Fireworks AI', + 'test_perplexity': 'Perplexity', + 'test_cloudflare': 'Cloudflare', + 'test_voyage': 'Voyage AI', + 'test_xai': 'xAI', + 'test_nvidia': 'NVIDIA', + 'test_watsonx': 'IBM watsonx', + 'test_azure_ai': 'Azure AI', + 'test_snowflake': 'Snowflake', + 'test_infinity': 'Infinity', + 'test_jina': 'Jina AI', + 'test_deepgram': 'Deepgram', + 'test_clarifai': 'Clarifai', + 'test_triton': 'Triton', } - + for key, provider in provider_mapping.items(): if key in test_file: return provider - + # For cross-provider test files - if any( - name in test_file - for name in [ - "test_optional_params", - "test_prompt_factory", - "test_router", - "test_text_completion", - ] - ): - return f"Cross-Provider Tests ({test_file})" - - return "Other Tests" - + if any(name in test_file for name in ['test_optional_params', 'test_prompt_factory', + 'test_router', 'test_text_completion']): + return f'Cross-Provider Tests ({test_file})' + + return 'Other Tests' def format_duration(seconds: float) -> str: """Format duration in human-readable format""" @@ -100,355 +89,290 @@ def format_duration(seconds: float) -> str: return f"{hours}h {minutes}m" -def generate_markdown_report( - junit_xml_path: str, output_path: str, tag: str = None, commit: str = None -): +def generate_markdown_report(junit_xml_path: str, output_path: str, tag: str = None, commit: str = None): """Generate a beautiful markdown report from JUnit XML""" try: tree = ET.parse(junit_xml_path) root = tree.getroot() - + # Handle both testsuite and testsuites root - if root.tag == "testsuites": - suites = root.findall("testsuite") + if root.tag == 'testsuites': + suites = root.findall('testsuite') else: suites = [root] - + # Overall statistics total_tests = 0 total_failures = 0 total_errors = 0 total_skipped = 0 total_time = 0.0 - + # Provider breakdown - provider_stats = defaultdict( - lambda: {"passed": 0, "failed": 0, "skipped": 0, "errors": 0, "time": 0.0} - ) + provider_stats = defaultdict(lambda: {'passed': 0, 'failed': 0, 'skipped': 0, 'errors': 0, 'time': 0.0}) provider_tests = defaultdict(list) - + for suite in suites: - total_tests += int(suite.get("tests", 0)) - total_failures += int(suite.get("failures", 0)) - total_errors += int(suite.get("errors", 0)) - total_skipped += int(suite.get("skipped", 0)) - total_time += float(suite.get("time", 0)) - - for testcase in suite.findall("testcase"): - classname = testcase.get("classname", "") - test_name = testcase.get("name", "") - test_time = float(testcase.get("time", 0)) - + total_tests += int(suite.get('tests', 0)) + total_failures += int(suite.get('failures', 0)) + total_errors += int(suite.get('errors', 0)) + total_skipped += int(suite.get('skipped', 0)) + total_time += float(suite.get('time', 0)) + + for testcase in suite.findall('testcase'): + classname = testcase.get('classname', '') + test_name = testcase.get('name', '') + test_time = float(testcase.get('time', 0)) + # Extract test file name from classname - if "." in classname: - parts = classname.split(".") - test_file = parts[-2] if len(parts) > 1 else "unknown" + if '.' in classname: + parts = classname.split('.') + test_file = parts[-2] if len(parts) > 1 else 'unknown' else: - test_file = "unknown" - + test_file = 'unknown' + provider = get_provider_from_test_file(test_file) - provider_stats[provider]["time"] += test_time - + provider_stats[provider]['time'] += test_time + # Check test status - if testcase.find("failure") is not None: - provider_stats[provider]["failed"] += 1 - failure = testcase.find("failure") - failure_msg = ( - failure.get("message", "") if failure is not None else "" - ) - provider_tests[provider].append( - { - "name": test_name, - "status": "FAILED", - "time": test_time, - "message": failure_msg, - } - ) - elif testcase.find("error") is not None: - provider_stats[provider]["errors"] += 1 - error = testcase.find("error") - error_msg = error.get("message", "") if error is not None else "" - provider_tests[provider].append( - { - "name": test_name, - "status": "ERROR", - "time": test_time, - "message": error_msg, - } - ) - elif testcase.find("skipped") is not None: - provider_stats[provider]["skipped"] += 1 - skip = testcase.find("skipped") - skip_msg = skip.get("message", "") if skip is not None else "" - provider_tests[provider].append( - { - "name": test_name, - "status": "SKIPPED", - "time": test_time, - "message": skip_msg, - } - ) + if testcase.find('failure') is not None: + provider_stats[provider]['failed'] += 1 + failure = testcase.find('failure') + failure_msg = failure.get('message', '') if failure is not None else '' + provider_tests[provider].append({ + 'name': test_name, + 'status': 'FAILED', + 'time': test_time, + 'message': failure_msg + }) + elif testcase.find('error') is not None: + provider_stats[provider]['errors'] += 1 + error = testcase.find('error') + error_msg = error.get('message', '') if error is not None else '' + provider_tests[provider].append({ + 'name': test_name, + 'status': 'ERROR', + 'time': test_time, + 'message': error_msg + }) + elif testcase.find('skipped') is not None: + provider_stats[provider]['skipped'] += 1 + skip = testcase.find('skipped') + skip_msg = skip.get('message', '') if skip is not None else '' + provider_tests[provider].append({ + 'name': test_name, + 'status': 'SKIPPED', + 'time': test_time, + 'message': skip_msg + }) else: - provider_stats[provider]["passed"] += 1 - provider_tests[provider].append( - { - "name": test_name, - "status": "PASSED", - "time": test_time, - "message": "", - } - ) - + provider_stats[provider]['passed'] += 1 + provider_tests[provider].append({ + 'name': test_name, + 'status': 'PASSED', + 'time': test_time, + 'message': '' + }) + passed = total_tests - total_failures - total_errors - total_skipped - + # Generate the markdown report - with open(output_path, "w") as f: + with open(output_path, 'w') as f: # Header f.write("# LLM Translation Test Results\n\n") - + # Metadata table f.write("## Test Run Information\n\n") f.write("| Field | Value |\n") f.write("|-------|-------|\n") f.write(f"| **Tag** | `{tag or 'N/A'}` |\n") - f.write( - f"| **Date** | {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} |\n" - ) + f.write(f"| **Date** | {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')} |\n") f.write(f"| **Commit** | `{commit or 'N/A'}` |\n") f.write(f"| **Duration** | {format_duration(total_time)} |\n") f.write("\n") - + # Overall statistics with visual elements f.write("## Overall Statistics\n\n") - + # Summary box f.write("```\n") f.write(f"Total Tests: {total_tests}\n") - f.write( - f"├── Passed: {passed:>4} ({(passed/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" - ) - f.write( - f"├── Failed: {total_failures:>4} ({(total_failures/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" - ) - f.write( - f"├── Errors: {total_errors:>4} ({(total_errors/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" - ) - f.write( - f"└── Skipped: {total_skipped:>4} ({(total_skipped/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n" - ) + f.write(f"├── Passed: {passed:>4} ({(passed/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") + f.write(f"├── Failed: {total_failures:>4} ({(total_failures/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") + f.write(f"├── Errors: {total_errors:>4} ({(total_errors/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") + f.write(f"└── Skipped: {total_skipped:>4} ({(total_skipped/total_tests)*100 if total_tests > 0 else 0:.1f}%)\n") f.write("```\n\n") - + + # Provider summary table f.write("## Results by Provider\n\n") - f.write( - "| Provider | Total | Pass | Fail | Error | Skip | Pass Rate | Duration |\n" - ) - f.write( - "|----------|-------|------|------|-------|------|-----------|----------|" - ) - + f.write("| Provider | Total | Pass | Fail | Error | Skip | Pass Rate | Duration |\n") + f.write("|----------|-------|------|------|-------|------|-----------|----------|") + # Sort providers: specific providers first, then cross-provider tests sorted_providers = [] cross_provider = [] for p in sorted(provider_stats.keys()): - if "Cross-Provider" in p or p == "Other Tests": + if 'Cross-Provider' in p or p == 'Other Tests': cross_provider.append(p) else: sorted_providers.append(p) - + all_providers = sorted_providers + cross_provider - + for provider in all_providers: stats = provider_stats[provider] - total = ( - stats["passed"] - + stats["failed"] - + stats["errors"] - + stats["skipped"] - ) - pass_rate = (stats["passed"] / total * 100) if total > 0 else 0 - - f.write( - f"\n| {provider} | {total} | {stats['passed']} | {stats['failed']} | " - ) + total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped'] + pass_rate = (stats['passed'] / total * 100) if total > 0 else 0 + + f.write(f"\n| {provider} | {total} | {stats['passed']} | {stats['failed']} | ") f.write(f"{stats['errors']} | {stats['skipped']} | {pass_rate:.1f}% | ") f.write(f"{format_duration(stats['time'])} |") - + # Detailed test results by provider f.write("\n\n## Detailed Test Results\n\n") - + for provider in sorted_providers: if provider_tests[provider]: stats = provider_stats[provider] - total = ( - stats["passed"] - + stats["failed"] - + stats["errors"] - + stats["skipped"] - ) - + total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped'] + f.write(f"### {provider}\n\n") f.write(f"**Summary:** {stats['passed']}/{total} passed ") - f.write( - f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%) " - ) + f.write(f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%) ") f.write(f"in {format_duration(stats['time'])}\n\n") - + # Group tests by status tests_by_status = defaultdict(list) for test in provider_tests[provider]: - tests_by_status[test["status"]].append(test) - + tests_by_status[test['status']].append(test) + # Show failed tests first (if any) - if tests_by_status["FAILED"]: + if tests_by_status['FAILED']: f.write("
\nFailed Tests\n\n") - for test in tests_by_status["FAILED"]: + for test in tests_by_status['FAILED']: f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n") - if test["message"]: + if test['message']: # Truncate long error messages - msg = ( - test["message"][:200] + "..." - if len(test["message"]) > 200 - else test["message"] - ) + msg = test['message'][:200] + '...' if len(test['message']) > 200 else test['message'] f.write(f" > {msg}\n") f.write("\n
\n\n") - + # Show errors (if any) - if tests_by_status["ERROR"]: + if tests_by_status['ERROR']: f.write("
\nError Tests\n\n") - for test in tests_by_status["ERROR"]: + for test in tests_by_status['ERROR']: f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n") f.write("\n
\n\n") - + # Show passed tests in collapsible section - if tests_by_status["PASSED"]: + if tests_by_status['PASSED']: f.write("
\nPassed Tests\n\n") - for test in tests_by_status["PASSED"]: + for test in tests_by_status['PASSED']: f.write(f"- `{test['name']}` ({test['time']:.2f}s)\n") f.write("\n
\n\n") - + # Show skipped tests (if any) - if tests_by_status["SKIPPED"]: + if tests_by_status['SKIPPED']: f.write("
\nSkipped Tests\n\n") - for test in tests_by_status["SKIPPED"]: + for test in tests_by_status['SKIPPED']: f.write(f"- `{test['name']}`\n") f.write("\n
\n\n") - + # Cross-provider tests in a separate section if cross_provider: f.write("### Cross-Provider Tests\n\n") for provider in cross_provider: if provider_tests[provider]: stats = provider_stats[provider] - total = ( - stats["passed"] - + stats["failed"] - + stats["errors"] - + stats["skipped"] - ) - + total = stats['passed'] + stats['failed'] + stats['errors'] + stats['skipped'] + f.write(f"#### {provider}\n\n") f.write(f"**Summary:** {stats['passed']}/{total} passed ") - f.write( - f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%)\n\n" - ) - + f.write(f"({(stats['passed']/total)*100 if total > 0 else 0:.1f}%)\n\n") + # For cross-provider tests, just show counts f.write(f"- Passed: {stats['passed']}\n") - if stats["failed"] > 0: + if stats['failed'] > 0: f.write(f"- Failed: {stats['failed']}\n") - if stats["errors"] > 0: + if stats['errors'] > 0: f.write(f"- Errors: {stats['errors']}\n") - if stats["skipped"] > 0: + if stats['skipped'] > 0: f.write(f"- Skipped: {stats['skipped']}\n") f.write("\n") - + + print_colored(f"Report generated: {output_path}", Colors.GREEN) - + except Exception as e: print_colored(f"Error generating report: {e}", Colors.RED) raise - -def run_tests( - test_path: str = "tests/llm_translation/", - junit_xml: str = "test-results/junit.xml", - report_path: str = "test-results/llm_translation_report.md", - tag: str = None, - commit: str = None, -) -> int: +def run_tests(test_path: str = "tests/llm_translation/", + junit_xml: str = "test-results/junit.xml", + report_path: str = "test-results/llm_translation_report.md", + tag: str = None, + commit: str = None) -> int: """Run the LLM translation tests and generate report""" - + # Create test results directory os.makedirs(os.path.dirname(junit_xml), exist_ok=True) - + print_colored("Starting LLM Translation Tests", Colors.BOLD + Colors.BLUE) print_colored(f"Test directory: {test_path}", Colors.CYAN) print_colored(f"Output: {junit_xml}", Colors.CYAN) print() - + # Run pytest cmd = [ - "uv", - "run", - "--no-sync", - "pytest", - test_path, + "uv", "run", "--no-sync", "pytest", test_path, f"--junitxml={junit_xml}", "-v", "--tb=short", "--maxfail=500", - "-n", - "auto", + "-n", "auto" ] - + # Add timeout if pytest-timeout is installed try: - subprocess.run( - ["uv", "run", "--no-sync", "python", "-c", "import pytest_timeout"], - capture_output=True, - check=True, - ) + subprocess.run(["uv", "run", "--no-sync", "python", "-c", "import pytest_timeout"], + capture_output=True, check=True) cmd.extend(["--timeout=300"]) except: - print_colored( - "Warning: pytest-timeout not installed, skipping timeout option", - Colors.YELLOW, - ) - + print_colored("Warning: pytest-timeout not installed, skipping timeout option", Colors.YELLOW) + print_colored("Running pytest with command:", Colors.YELLOW) print(f" {' '.join(cmd)}") print() - + # Run the tests result = subprocess.run(cmd, capture_output=False) - + # Generate the report regardless of test outcome if os.path.exists(junit_xml): print() print_colored("Generating test report...", Colors.BLUE) generate_markdown_report(junit_xml, report_path, tag, commit) - + # Print summary to console print() print_colored("Test Summary:", Colors.BOLD + Colors.PURPLE) - + # Parse XML for quick summary tree = ET.parse(junit_xml) root = tree.getroot() - - if root.tag == "testsuites": - suites = root.findall("testsuite") + + if root.tag == 'testsuites': + suites = root.findall('testsuite') else: suites = [root] - - total = sum(int(s.get("tests", 0)) for s in suites) - failures = sum(int(s.get("failures", 0)) for s in suites) - errors = sum(int(s.get("errors", 0)) for s in suites) - skipped = sum(int(s.get("skipped", 0)) for s in suites) + + total = sum(int(s.get('tests', 0)) for s in suites) + failures = sum(int(s.get('failures', 0)) for s in suites) + errors = sum(int(s.get('errors', 0)) for s in suites) + skipped = sum(int(s.get('skipped', 0)) for s in suites) passed = total - failures - errors - skipped - + print(f" Total: {total}") print_colored(f" Passed: {passed}", Colors.GREEN) if failures > 0: @@ -457,75 +381,59 @@ def run_tests( print_colored(f" Errors: {errors}", Colors.RED) if skipped > 0: print_colored(f" Skipped: {skipped}", Colors.YELLOW) - + if total > 0: pass_rate = (passed / total) * 100 - color = ( - Colors.GREEN - if pass_rate >= 80 - else Colors.YELLOW if pass_rate >= 60 else Colors.RED - ) + color = Colors.GREEN if pass_rate >= 80 else Colors.YELLOW if pass_rate >= 60 else Colors.RED print_colored(f" Pass Rate: {pass_rate:.1f}%", color) else: print_colored("No test results found!", Colors.RED) - + print() print_colored("Test run complete!", Colors.BOLD + Colors.GREEN) - + return result.returncode - if __name__ == "__main__": import argparse - + parser = argparse.ArgumentParser(description="Run LLM Translation Tests") - parser.add_argument( - "--test-path", default="tests/llm_translation/", help="Path to test directory" - ) - parser.add_argument( - "--junit-xml", - default="test-results/junit.xml", - help="Path for JUnit XML output", - ) - parser.add_argument( - "--report", - default="test-results/llm_translation_report.md", - help="Path for markdown report", - ) + parser.add_argument("--test-path", default="tests/llm_translation/", + help="Path to test directory") + parser.add_argument("--junit-xml", default="test-results/junit.xml", + help="Path for JUnit XML output") + parser.add_argument("--report", default="test-results/llm_translation_report.md", + help="Path for markdown report") parser.add_argument("--tag", help="Git tag or version") parser.add_argument("--commit", help="Git commit SHA") - + args = parser.parse_args() - + # Get git info if not provided if not args.commit: try: - result = subprocess.run( - ["git", "rev-parse", "HEAD"], capture_output=True, text=True - ) + result = subprocess.run(["git", "rev-parse", "HEAD"], + capture_output=True, text=True) if result.returncode == 0: args.commit = result.stdout.strip() except: pass - + if not args.tag: try: - result = subprocess.run( - ["git", "describe", "--tags", "--abbrev=0"], - capture_output=True, - text=True, - ) + result = subprocess.run(["git", "describe", "--tags", "--abbrev=0"], + capture_output=True, text=True) if result.returncode == 0: args.tag = result.stdout.strip() except: pass - + exit_code = run_tests( test_path=args.test_path, junit_xml=args.junit_xml, report_path=args.report, tag=args.tag, - commit=args.commit, + commit=args.commit ) - + sys.exit(exit_code) diff --git a/ci_cd/run_migration.py b/ci_cd/run_migration.py index 760aee66da8..feec4046ee1 100644 --- a/ci_cd/run_migration.py +++ b/ci_cd/run_migration.py @@ -9,6 +9,7 @@ from pathlib import Path import testing.postgresql + DESTRUCTIVE_PATTERN = re.compile(r"\bDROP\s+(COLUMN|TABLE|INDEX)\b", re.IGNORECASE) DEFAULT_BASE_BRANCH = "litellm_internal_staging" diff --git a/cookbook/benchmark/benchmark.py b/cookbook/benchmark/benchmark.py index 2b3ddacad80..b38d185a166 100644 --- a/cookbook/benchmark/benchmark.py +++ b/cookbook/benchmark/benchmark.py @@ -6,6 +6,7 @@ from tabulate import tabulate from termcolor import colored import os + # Define the list of models to benchmark # select any LLM listed here: https://docs.litellm.ai/docs/providers models = ["gpt-3.5-turbo", "claude-2"] diff --git a/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py b/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py index a7049c68ced..6379314c5b6 100644 --- a/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py +++ b/cookbook/litellm_proxy_server/braintrust_prompt_wrapper_server.py @@ -22,6 +22,7 @@ from fastapi import FastAPI, HTTPException, Header, Query from fastapi.responses import JSONResponse import uvicorn + app = FastAPI( title="Braintrust Prompt Wrapper", description="Wrapper server for Braintrust prompts to work with LiteLLM", diff --git a/cookbook/livekit_agent_sdk/main.py b/cookbook/livekit_agent_sdk/main.py index 26a42b40d07..c68e5534ea8 100644 --- a/cookbook/livekit_agent_sdk/main.py +++ b/cookbook/livekit_agent_sdk/main.py @@ -2,7 +2,7 @@ Simple xAI Voice Agent using LiveKit SDK with LiteLLM Gateway This example shows how to use LiveKit's xAI realtime plugin through LiteLLM proxy. -LiteLLM acts as a unified interface, allowing you to switch between xAI, OpenAI, +LiteLLM acts as a unified interface, allowing you to switch between xAI, OpenAI, and Azure realtime APIs without changing your agent code. """ diff --git a/cookbook/misc/migrate_proxy_config.py b/cookbook/misc/migrate_proxy_config.py index dc039070a5d..31c3f32c08a 100644 --- a/cookbook/misc/migrate_proxy_config.py +++ b/cookbook/misc/migrate_proxy_config.py @@ -1,14 +1,14 @@ """ LiteLLM Migration Script! -Takes a config.yaml and calls /model/new +Takes a config.yaml and calls /model/new Inputs: - File path to config.yaml - Proxy base url to your hosted proxy Step 1: Reads your config.yaml -Step 2: reads `model_list` and loops through all models +Step 2: reads `model_list` and loops through all models Step 3: calls `/model/new` for each model """ diff --git a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py index 9f70bd20c2b..7bf9cc32484 100644 --- a/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py +++ b/cookbook/mock_guardrail_server/mock_bedrock_guardrail_server.py @@ -518,7 +518,8 @@ if __name__ == "__main__": print(f"Endpoint: POST /guardrail/{{id}}/version/{{version}}/apply") print("=" * 80) print("\nExample curl command:") - print(f""" + print( + f""" curl -X POST "http://{host}:{port}/guardrail/test-guardrail/version/1/apply" \\ -H "Authorization: Bearer {bearer_token}" \\ -H "Content-Type: application/json" \\ @@ -532,7 +533,8 @@ curl -X POST "http://{host}:{port}/guardrail/test-guardrail/version/1/apply" \\ }} ] }}' - """) + """ + ) print("=" * 80) uvicorn.run(app, host=host, port=port) diff --git a/db_scripts/create_views.py b/db_scripts/create_views.py index 82f5b451db9..3027b38958d 100644 --- a/db_scripts/create_views.py +++ b/db_scripts/create_views.py @@ -34,7 +34,8 @@ async def check_view_exists(): # noqa: PLR0915 print("LiteLLM_VerificationTokenView Exists!") # noqa except Exception: # If an error occurs, the view does not exist, so create it - await db.execute_raw(""" + await db.execute_raw( + """ CREATE VIEW "LiteLLM_VerificationTokenView" AS SELECT v.*, @@ -44,7 +45,8 @@ async def check_view_exists(): # noqa: PLR0915 t.rpm_limit AS team_rpm_limit FROM "LiteLLM_VerificationToken" v LEFT JOIN "LiteLLM_TeamTable" t ON v.team_id = t.team_id; - """) + """ + ) print("LiteLLM_VerificationTokenView Created!") # noqa diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py b/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py index 7353b995d2a..8824f4c02de 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/callback_controls.py @@ -14,74 +14,53 @@ from litellm.types.utils import StandardCallbackDynamicParams class EnterpriseCallbackControls: @staticmethod def is_callback_disabled_dynamically( - callback: litellm.CALLBACK_TYPES, - litellm_params: dict, - standard_callback_dynamic_params: StandardCallbackDynamicParams, - ) -> bool: - """ - Check if a callback is disabled via the x-litellm-disable-callbacks header or via `litellm_disabled_callbacks` in standard_callback_dynamic_params. - - Args: - callback: The callback to check (can be string, CustomLogger instance, or callable) - litellm_params: Parameters containing proxy server request info - - Returns: - bool: True if the callback should be disabled, False otherwise - """ - from litellm.litellm_core_utils.custom_logger_registry import ( - CustomLoggerRegistry, - ) - - try: - disabled_callbacks = EnterpriseCallbackControls.get_disabled_callbacks( - litellm_params, standard_callback_dynamic_params + callback: litellm.CALLBACK_TYPES, + litellm_params: dict, + standard_callback_dynamic_params: StandardCallbackDynamicParams + ) -> bool: + """ + Check if a callback is disabled via the x-litellm-disable-callbacks header or via `litellm_disabled_callbacks` in standard_callback_dynamic_params. + + Args: + callback: The callback to check (can be string, CustomLogger instance, or callable) + litellm_params: Parameters containing proxy server request info + + Returns: + bool: True if the callback should be disabled, False otherwise + """ + from litellm.litellm_core_utils.custom_logger_registry import ( + CustomLoggerRegistry, ) - verbose_logger.debug( - f"Dynamically disabled callbacks from {X_LITELLM_DISABLE_CALLBACKS}: {disabled_callbacks}" - ) - verbose_logger.debug( - f"Checking if {callback} is disabled via headers. Disable callbacks from headers: {disabled_callbacks}" - ) - if disabled_callbacks is not None: - ######################################################### - # premium user check - ######################################################### - if ( - not EnterpriseCallbackControls._should_allow_dynamic_callback_disabling() - ): - return False - ######################################################### - if isinstance(callback, str): - if callback.lower() in disabled_callbacks: - verbose_logger.debug( - f"Not logging to {callback} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}" - ) - return True - elif isinstance(callback, CustomLogger): - # get the string name of the callback - callback_str = ( - CustomLoggerRegistry.get_callback_str_from_class_type( - callback.__class__ - ) - ) - if ( - callback_str is not None - and callback_str.lower() in disabled_callbacks - ): - verbose_logger.debug( - f"Not logging to {callback_str} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}" - ) - return True - return False - except Exception as e: - verbose_logger.debug(f"Error checking disabled callbacks header: {str(e)}") - return False + try: + disabled_callbacks = EnterpriseCallbackControls.get_disabled_callbacks(litellm_params, standard_callback_dynamic_params) + verbose_logger.debug(f"Dynamically disabled callbacks from {X_LITELLM_DISABLE_CALLBACKS}: {disabled_callbacks}") + verbose_logger.debug(f"Checking if {callback} is disabled via headers. Disable callbacks from headers: {disabled_callbacks}") + if disabled_callbacks is not None: + ######################################################### + # premium user check + ######################################################### + if not EnterpriseCallbackControls._should_allow_dynamic_callback_disabling(): + return False + ######################################################### + if isinstance(callback, str): + if callback.lower() in disabled_callbacks: + verbose_logger.debug(f"Not logging to {callback} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}") + return True + elif isinstance(callback, CustomLogger): + # get the string name of the callback + callback_str = CustomLoggerRegistry.get_callback_str_from_class_type(callback.__class__) + if callback_str is not None and callback_str.lower() in disabled_callbacks: + verbose_logger.debug(f"Not logging to {callback_str} because it is disabled via {X_LITELLM_DISABLE_CALLBACKS}") + return True + return False + except Exception as e: + verbose_logger.debug( + f"Error checking disabled callbacks header: {str(e)}" + ) + return False @staticmethod - def get_disabled_callbacks( - litellm_params: dict, - standard_callback_dynamic_params: StandardCallbackDynamicParams, - ) -> Optional[List[str]]: + def get_disabled_callbacks(litellm_params: dict, standard_callback_dynamic_params: StandardCallbackDynamicParams) -> Optional[List[str]]: """ Get the disabled callbacks from the standard callback dynamic params. """ @@ -92,24 +71,18 @@ class EnterpriseCallbackControls: request_headers = get_proxy_server_request_headers(litellm_params) disabled_callbacks = request_headers.get(X_LITELLM_DISABLE_CALLBACKS, None) if disabled_callbacks is not None: - disabled_callbacks = set( - [cb.strip().lower() for cb in disabled_callbacks.split(",")] - ) + disabled_callbacks = set([cb.strip().lower() for cb in disabled_callbacks.split(",")]) return list(disabled_callbacks) + ######################################################### # check if disabled via request body ######################################################### - if ( - standard_callback_dynamic_params.get("litellm_disabled_callbacks", None) - is not None - ): - return standard_callback_dynamic_params.get( - "litellm_disabled_callbacks", None - ) - + if standard_callback_dynamic_params.get("litellm_disabled_callbacks", None) is not None: + return standard_callback_dynamic_params.get("litellm_disabled_callbacks", None) + return None - + @staticmethod def _should_allow_dynamic_callback_disabling(): import litellm @@ -117,14 +90,10 @@ class EnterpriseCallbackControls: # Check if admin has disabled this feature if litellm.allow_dynamic_callback_disabling is not True: - verbose_logger.debug( - "Dynamic callback disabling is disabled by admin via litellm.allow_dynamic_callback_disabling" - ) + verbose_logger.debug("Dynamic callback disabling is disabled by admin via litellm.allow_dynamic_callback_disabling") return False - + if premium_user: return True - verbose_logger.warning( - f"Disabling callbacks using request headers is an enterprise feature. {CommonProxyErrors.not_premium_user.value}" - ) - return False + verbose_logger.warning(f"Disabling callbacks using request headers is an enterprise feature. {CommonProxyErrors.not_premium_user.value}") + return False \ No newline at end of file diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index 4fb6679a6eb..89c3b854686 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -349,10 +349,8 @@ class BaseEmailLogger(CustomLogger): ) # Calculate percentage and alert threshold - percentage = ( - threshold_pct - if threshold_pct is not None - else int(EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE * 100) + percentage = threshold_pct if threshold_pct is not None else int( + EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE * 100 ) threshold_fraction = percentage / 100.0 alert_threshold_str = ( @@ -611,7 +609,9 @@ class BaseEmailLogger(CustomLogger): continue _id = user_info.token or user_info.user_id or "default_id" - _cache_key = f"email_budget_alerts:max_budget_alert:{threshold_pct}:{_id}" + _cache_key = ( + f"email_budget_alerts:max_budget_alert:{threshold_pct}:{_id}" + ) result = await _cache.async_get_cache(key=_cache_key) if result is not None: @@ -630,9 +630,7 @@ class BaseEmailLogger(CustomLogger): continue recipient_emails = list(set(emails)) - event_message = ( - f"Max Budget Alert - {threshold_pct}% of Maximum Budget Reached" - ) + event_message = f"Max Budget Alert - {threshold_pct}% of Maximum Budget Reached" webhook_event = WebhookEvent( event="max_budget_alert", event_message=event_message, diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py index a1e8def2bb2..8fc2d66d531 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py @@ -15,6 +15,7 @@ from litellm.llms.custom_httpx.http_handler import ( from .base_email import BaseEmailLogger + SENDGRID_API_ENDPOINT = "https://api.sendgrid.com/v3/mail/send" @@ -78,4 +79,4 @@ class SendGridEmailLogger(BaseEmailLogger): verbose_logger.debug( f"SendGrid response status={response.status_code}, body={response.text}" ) - return + return \ No newline at end of file diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py index 8e4dbde437b..8efdaf231b7 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py @@ -1,7 +1,6 @@ """ This is the litellm SMTP email integration """ - import asyncio from typing import List diff --git a/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py b/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py index 24941e90ab8..44ba0063ffe 100644 --- a/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py +++ b/enterprise/litellm_enterprise/litellm_core_utils/litellm_logging.py @@ -1,7 +1,6 @@ """ Enterprise specific logging utils """ - from litellm.litellm_core_utils.litellm_logging import StandardLoggingMetadata diff --git a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py index 4f2eaa3c468..18ac29b9781 100644 --- a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py @@ -153,11 +153,11 @@ async def get_audit_logs( # Return paginated response return PaginatedAuditLogResponse( - audit_logs=( - [AuditLogResponse(**audit_log.model_dump()) for audit_log in audit_logs] - if audit_logs - else [] - ), + audit_logs=[ + AuditLogResponse(**audit_log.model_dump()) for audit_log in audit_logs + ] + if audit_logs + else [], total=total_count, page=page, page_size=page_size, diff --git a/enterprise/litellm_enterprise/proxy/auth/__init__.py b/enterprise/litellm_enterprise/proxy/auth/__init__.py index dc70b57ab55..f67826ca7fa 100644 --- a/enterprise/litellm_enterprise/proxy/auth/__init__.py +++ b/enterprise/litellm_enterprise/proxy/auth/__init__.py @@ -7,4 +7,4 @@ including custom SSO handlers and advanced authentication features. from .custom_sso_handler import EnterpriseCustomSSOHandler -__all__ = ["EnterpriseCustomSSOHandler"] +__all__ = ["EnterpriseCustomSSOHandler"] \ No newline at end of file diff --git a/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py b/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py index 3aef2bd77cb..356f6ecd4b5 100644 --- a/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py +++ b/enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py @@ -53,9 +53,7 @@ class CheckBatchCost: "user_api_key_alias": getattr(user_row, "user_alias", None), } except Exception as e: - verbose_proxy_logger.error( - f"CheckBatchCost: could not look up user {user_id} for batch {batch_id}: {e}" - ) + verbose_proxy_logger.error(f"CheckBatchCost: could not look up user {user_id} for batch {batch_id}: {e}") return {} async def _cleanup_stale_managed_objects(self) -> None: @@ -64,22 +62,11 @@ class CheckBatchCost: in non-terminal states as 'stale_expired'. These will never complete and should not be polled. """ - cutoff = datetime.now(timezone.utc) - timedelta( - days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS - ) + cutoff = datetime.now(timezone.utc) - timedelta(days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS) result = await self.prisma_client.db.litellm_managedobjecttable.update_many( where={ "file_purpose": "batch", - "status": { - "not_in": [ - "completed", - "complete", - "failed", - "expired", - "cancelled", - "stale_expired", - ] - }, + "status": {"not_in": ["completed", "complete", "failed", "expired", "cancelled", "stale_expired"]}, "created_at": {"lt": cutoff}, }, data={"status": "stale_expired"}, @@ -133,12 +120,9 @@ class CheckBatchCost: try: from litellm.integrations.prometheus import PrometheusLogger - prom_logger = PrometheusLogger.get_instance() except Exception as e: - verbose_proxy_logger.error( - f"CheckBatchCost: could not get Prometheus logger: {e}" - ) + verbose_proxy_logger.error(f"CheckBatchCost: could not get Prometheus logger: {e}") prom_logger = None processed_models: List[Tuple[Optional[str], Optional[str]]] = [] @@ -177,11 +161,7 @@ class CheckBatchCost: order={"created_at": "asc"}, ) except Exception as query_err: - if ( - "batch_processed" not in str(query_err).lower() - and "unknown column" not in str(query_err).lower() - and "does not exist" not in str(query_err).lower() - ): + if "batch_processed" not in str(query_err).lower() and "unknown column" not in str(query_err).lower() and "does not exist" not in str(query_err).lower(): raise # Permanent schema gap — cache the result so future cycles skip straight to fallback self._has_batch_processed_column = False @@ -236,13 +216,14 @@ class CheckBatchCost: f"Skipping job {unified_object_id} because of error querying model ID: {model_id} for cost and usage of batch ID: {batch_id}: {e}" ) if prom_logger: - prom_logger.record_check_batch_cost_error( - "provider_retrieval_error" - ) + prom_logger.record_check_batch_cost_error("provider_retrieval_error") continue ## RETRIEVE THE BATCH JOB OUTPUT FILE - if response.status == "completed" and response.output_file_id is not None: + if ( + response.status == "completed" + and response.output_file_id is not None + ): verbose_proxy_logger.info( f"Batch ID: {batch_id} is complete, tracking cost and usage" ) @@ -269,25 +250,20 @@ class CheckBatchCost: decoded = _is_base64_encoded_unified_file_id(raw_output_file_id) if decoded: try: - raw_output_file_id = decoded.split("llm_output_file_id,")[ - 1 - ].split(";")[0] + raw_output_file_id = decoded.split("llm_output_file_id,")[1].split(";")[0] except (IndexError, AttributeError): pass - credentials = ( - self.llm_router.get_deployment_credentials_with_provider(model_id) - or {} - ) + credentials = self.llm_router.get_deployment_credentials_with_provider(model_id) or {} _file_content = await afile_content( file_id=raw_output_file_id, **credentials, ) # Access content - handle both direct attribute and method call - if hasattr(_file_content, "content"): + if hasattr(_file_content, 'content'): content_bytes = _file_content.content # type: ignore[union-attr] - elif hasattr(_file_content, "read"): + elif hasattr(_file_content, 'read'): content_bytes = await _file_content.read() # type: ignore[misc] else: content_bytes = _file_content # type: ignore[assignment] @@ -314,9 +290,7 @@ class CheckBatchCost: f"Skipping job {unified_object_id} because it is not a valid deployment info" ) if prom_logger: - prom_logger.record_check_batch_cost_error( - "deployment_not_found" - ) + prom_logger.record_check_batch_cost_error("deployment_not_found") continue custom_llm_provider = deployment_info.litellm_params.custom_llm_provider litellm_model_name = deployment_info.litellm_params.model @@ -328,11 +302,7 @@ class CheckBatchCost: # Pass deployment model_info so custom batch pricing # (input_cost_per_token_batches etc.) is used for cost calc - deployment_model_info = ( - deployment_info.model_info.model_dump() - if deployment_info.model_info - else {} - ) + deployment_model_info = deployment_info.model_info.model_dump() if deployment_info.model_info else {} batch_cost, batch_usage, batch_models = ( await calculate_batch_cost_and_usage( file_content_dictionary=file_content_as_dict, @@ -379,9 +349,7 @@ class CheckBatchCost: # Record batch duration (completed_at - created_at) if prom_logger and response.completed_at and response.created_at: - duration_seconds = float( - response.completed_at - response.created_at - ) + duration_seconds = float(response.completed_at - response.created_at) if duration_seconds >= 0: prom_logger.record_managed_batch_duration( duration_seconds=duration_seconds, @@ -390,9 +358,7 @@ class CheckBatchCost: ) # Track this job for the final metrics summary - processed_models.append( - (model_name, str(llm_provider) if llm_provider else None) - ) + processed_models.append((model_name, str(llm_provider) if llm_provider else None)) # mark the job as complete try: diff --git a/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py b/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py index 8b2d15c1578..dc0168683c8 100644 --- a/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py +++ b/enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py @@ -33,7 +33,9 @@ class CheckResponsesCost: self.prisma_client: PrismaClient = prisma_client self.llm_router: Router = llm_router - async def _expire_stale_rows(self, cutoff: datetime, batch_size: int) -> int: + async def _expire_stale_rows( + self, cutoff: datetime, batch_size: int + ) -> int: """Execute the bounded UPDATE that marks stale rows as 'stale_expired'. Isolated so it can be swapped / mocked in tests without touching the @@ -72,9 +74,7 @@ class CheckResponsesCost: rows per invocation to avoid overwhelming the DB when there is a large backlog. """ - cutoff = datetime.now(timezone.utc) - timedelta( - days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS - ) + cutoff = datetime.now(timezone.utc) - timedelta(days=MANAGED_OBJECT_STALENESS_CUTOFF_DAYS) result = await self._expire_stale_rows(cutoff, STALE_OBJECT_CLEANUP_BATCH_SIZE) if result > 0: verbose_proxy_logger.warning( @@ -105,7 +105,7 @@ class CheckResponsesCost: take=MAX_OBJECTS_PER_POLL_CYCLE, order={"created_at": "asc"}, ) - + verbose_proxy_logger.debug(f"Found {len(jobs)} response jobs to check") completed_jobs = [] @@ -120,33 +120,29 @@ class CheckResponsesCost: # Get the stored response object to extract model information stored_response = job.file_object model_name = stored_response.get("model", None) - + # Decrypt the response ID - responses_id_security, _, _ = ( - ResponsesIDSecurity()._decrypt_response_id(unified_object_id) - ) - + responses_id_security, _, _ = ResponsesIDSecurity()._decrypt_response_id(unified_object_id) + # Prepare metadata with model information for cost tracking litellm_metadata = { "user_api_key_user_id": job.created_by or "default-user-id", } - + # Add model information if available if model_name: litellm_metadata["model"] = model_name - litellm_metadata["model_group"] = ( - model_name # Use same value for model_group - ) - + litellm_metadata["model_group"] = model_name # Use same value for model_group + response = await litellm.aget_responses( response_id=responses_id_security, litellm_metadata=litellm_metadata, ) - + verbose_proxy_logger.debug( f"Response {unified_object_id} status: {response.status}, model: {model_name}" ) - + except Exception as e: verbose_proxy_logger.info( f"Skipping job {unified_object_id} due to error: {e}" @@ -159,7 +155,7 @@ class CheckResponsesCost: f"Response {unified_object_id} is complete. Cost automatically tracked by aget_responses." ) completed_jobs.append(job) - + elif response.status in ["failed", "cancelled"]: verbose_proxy_logger.info( f"Response {unified_object_id} has status {response.status}, marking as complete" @@ -175,3 +171,4 @@ class CheckResponsesCost: verbose_proxy_logger.info( f"Marked {len(completed_jobs)} response jobs as completed" ) + diff --git a/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py b/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py index 70634537c55..254d816039c 100644 --- a/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py +++ b/enterprise/litellm_enterprise/proxy/hooks/managed_vector_stores.py @@ -41,7 +41,7 @@ class _PROXY_LiteLLMManagedVectorStores( ): """ Managed vector stores with target_model_names support. - + This class provides functionality to: - Create vector stores across multiple models - Retrieve vector stores by unified ID @@ -77,14 +77,14 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> str: """ Generate the format string for the unified vector store ID. - + Format: litellm_proxy:vector_store;unified_id,;target_model_names,;resource_id,;model_id, """ # VectorStoreCreateResponse is a TypedDict, so resource_object is a dictionary # Extract provider resource ID from the response provider_resource_id = resource_object.get("id", "") - + # Model ID is stored in hidden params if the response object supports it # For TypedDict responses, we need to check if _hidden_params was added hidden_params: Dict[str, Any] = {} @@ -109,18 +109,20 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> VectorStoreCreateResponse: """ Create a vector store for a specific model. - + Args: llm_router: LiteLLM router instance model: Model name to create vector store for request_data: Request data for vector store creation litellm_parent_otel_span: OpenTelemetry span for tracing - + Returns: VectorStoreCreateResponse from the provider """ # Use the router to create the vector store - response = await llm_router.avector_store_create(model=model, **request_data) + response = await llm_router.avector_store_create( + model=model, **request_data + ) return response # ============================================================================ @@ -137,14 +139,14 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> VectorStoreCreateResponse: """ Create a vector store across multiple models. - + Args: create_request: Vector store creation request parameters llm_router: LiteLLM router instance target_model_names_list: List of target model names litellm_parent_otel_span: OpenTelemetry span for tracing user_api_key_dict: User API key authentication details - + Returns: VectorStoreCreateResponse with unified ID """ @@ -194,7 +196,7 @@ class _PROXY_LiteLLMManagedVectorStores( # VectorStoreCreateResponse is a TypedDict, so we need to create a new dict with the unified ID response = responses[0].copy() response["id"] = unified_id - + verbose_logger.info( f"Successfully created managed vector store with unified ID: {unified_id}" ) @@ -210,13 +212,13 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> Dict[str, Any]: """ List vector stores created by a user. - + Args: user_api_key_dict: User API key authentication details limit: Maximum number of vector stores to return after: Cursor for pagination order: Sort order ('asc' or 'desc') - + Returns: Dictionary with list of vector stores and pagination info """ @@ -236,23 +238,23 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> bool: """ Check if user has access to a vector store. - + Args: vector_store_id: The unified vector store ID user_api_key_dict: User API key authentication details - + Returns: True if user has access, False otherwise """ is_unified_id = is_base64_encoded_unified_id(vector_store_id) - + if is_unified_id: # Check access for managed vector store return await self.can_user_access_unified_resource_id( vector_store_id, user_api_key_dict, ) - + # Not a managed vector store, allow access return True @@ -261,22 +263,24 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> bool: """ Check if user has access to a managed vector store in request data. - + Args: data: Request data containing vector_store_id user_api_key_dict: User API key authentication details - + Returns: True if this is a managed vector store and user has access - + Raises: HTTPException: If user doesn't have access """ vector_store_id = cast(Optional[str], data.get("vector_store_id")) is_unified_id = ( - is_base64_encoded_unified_id(vector_store_id) if vector_store_id else False + is_base64_encoded_unified_id(vector_store_id) + if vector_store_id + else False ) - + if is_unified_id and vector_store_id: if await self.can_user_access_unified_resource_id( vector_store_id, user_api_key_dict @@ -287,7 +291,7 @@ class _PROXY_LiteLLMManagedVectorStores( status_code=403, detail=f"User {user_api_key_dict.user_id} does not have access to vector store {vector_store_id}", ) - + return False # ============================================================================ @@ -303,18 +307,18 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> Union[Exception, str, Dict, None]: """ Pre-call hook to handle vector store operations. - + This hook intercepts vector store requests and: - Validates access for managed vector stores - Transforms unified IDs to provider-specific IDs - Adds model routing information - + Args: user_api_key_dict: User API key authentication details cache: Cache instance data: Request data call_type: Type of call being made - + Returns: Modified request data or None """ @@ -326,40 +330,40 @@ class _PROXY_LiteLLMManagedVectorStores( # Handle vector store search operations if call_type == "avector_store_search": vector_store_id = data.get("vector_store_id") - + if vector_store_id: # Check if it's a managed vector store ID decoded_id = is_base64_encoded_unified_id(vector_store_id) - + if decoded_id: verbose_logger.debug( f"Processing managed vector store search: {vector_store_id}" ) - + # Check access has_access = await self.can_user_access_unified_resource_id( vector_store_id, user_api_key_dict ) - + if not has_access: raise HTTPException( status_code=403, detail=f"User {user_api_key_dict.user_id} does not have access to vector store {vector_store_id}", ) - + # Parse the unified ID to extract components parsed_id = parse_unified_id(vector_store_id) - + if parsed_id: # Extract the model ID and provider resource ID model_id = parsed_id.get("model_id") provider_resource_id = parsed_id.get("provider_resource_id") target_model_names = parsed_id.get("target_model_names", []) - + verbose_logger.debug( f"Decoded vector store - model_id: {model_id}, provider_resource_id: {provider_resource_id}, target_model_names: {target_model_names}" ) - + # Determine which model to use for routing # Priority: model_id (deployment ID) > first target_model_name routing_model = None @@ -367,28 +371,28 @@ class _PROXY_LiteLLMManagedVectorStores( routing_model = model_id elif target_model_names and len(target_model_names) > 0: routing_model = target_model_names[0] - + # Set the model for routing if routing_model: data["model"] = routing_model verbose_logger.info( f"Routing vector store search to model: {routing_model}" ) - + # Replace the unified ID with the provider-specific ID if provider_resource_id: data["vector_store_id"] = provider_resource_id verbose_logger.debug( f"Replaced unified ID with provider resource ID: {provider_resource_id}" ) - + # Handle vector store retrieve/delete operations elif call_type in ("avector_store_retrieve", "avector_store_delete"): await self.check_managed_vector_store_access(data, user_api_key_dict) - + # If it's a managed vector store, we'll handle it in the endpoint # No need to transform here as the endpoint will route to the hook - + return data # ============================================================================ @@ -403,15 +407,15 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> Any: """ Post-call hook to transform responses. - + This hook can be used to transform responses if needed. For now, it just passes through the response. - + Args: data: Request data user_api_key_dict: User API key authentication details response: Response from the provider - + Returns: Potentially modified response """ @@ -432,21 +436,21 @@ class _PROXY_LiteLLMManagedVectorStores( ) -> List[Dict]: """ Filter deployments based on vector store availability. - + This is used by the router to select only deployments that have the vector store available. - + Note: This method signature is a compromise between CustomLogger and BaseManagedResource parent classes which have incompatible signatures. The type: ignore[override] is necessary due to this multiple inheritance conflict. - + Args: model: Model name healthy_deployments: List of healthy deployments messages: Messages (unused for vector stores, required by CustomLogger interface) request_kwargs: Request kwargs containing vector_store_id and mappings parent_otel_span: OpenTelemetry span for tracing - + Returns: Filtered list of deployments """ diff --git a/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py b/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py index 48b6dd76348..2f53f9e9281 100644 --- a/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py @@ -2,6 +2,7 @@ Enterprise internal user management endpoints """ + from fastapi import APIRouter, Depends, HTTPException from litellm.proxy._types import UserAPIKeyAuth diff --git a/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py b/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py index cf8c38719d7..5e799599862 100644 --- a/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py +++ b/enterprise/litellm_enterprise/proxy/vector_stores/endpoints.py @@ -147,12 +147,12 @@ async def list_vector_stores( vector_stores_from_db = await VectorStoreRegistry._get_vector_stores_from_db( prisma_client=prisma_client ) - + # Also clean up in-memory registry to remove any deleted vector stores if litellm.vector_store_registry is not None: db_vector_store_ids = { - vs.get("vector_store_id") - for vs in vector_stores_from_db + vs.get("vector_store_id") + for vs in vector_stores_from_db if vs.get("vector_store_id") } # Remove any in-memory vector stores that no longer exist in database diff --git a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py index d9d5a989abb..380b0a6facb 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -39,23 +39,15 @@ class EmailEvent(str, enum.Enum): soft_budget_crossed = "Soft Budget Crossed" max_budget_alert = "Max Budget Alert" - class EmailEventSettings(BaseModel): event: EmailEvent enabled: bool - - class EmailEventSettingsUpdateRequest(BaseModel): settings: List[EmailEventSettings] - - class EmailEventSettingsResponse(BaseModel): settings: List[EmailEventSettings] - - class DefaultEmailSettings(BaseModel): """Default settings for email events""" - settings: Dict[EmailEvent, bool] = Field( default_factory=lambda: { EmailEvent.virtual_key_created: True, # On by default @@ -65,12 +57,10 @@ class DefaultEmailSettings(BaseModel): EmailEvent.max_budget_alert: True, # On by default } ) - def to_dict(self) -> Dict[str, bool]: """Convert to dictionary with string keys for storage""" return {event.value: enabled for event, enabled in self.settings.items()} - @classmethod def get_defaults(cls) -> Dict[str, bool]: """Get the default settings as a dictionary with string keys""" - return cls().to_dict() + return cls().to_dict() \ No newline at end of file diff --git a/litellm/_uuid.py b/litellm/_uuid.py index 2b7c3b82d35..52acf647dd8 100644 --- a/litellm/_uuid.py +++ b/litellm/_uuid.py @@ -6,6 +6,7 @@ Always uses fastuuid for performance. import fastuuid as _uuid # type: ignore + # Expose a module-like alias so callers can use: uuid.uuid4() uuid = _uuid diff --git a/litellm/anthropic_interface/exceptions/exception_mapping_utils.py b/litellm/anthropic_interface/exceptions/exception_mapping_utils.py index 4548185bbdc..28020e763f4 100644 --- a/litellm/anthropic_interface/exceptions/exception_mapping_utils.py +++ b/litellm/anthropic_interface/exceptions/exception_mapping_utils.py @@ -9,6 +9,7 @@ from typing import Dict, Optional from .exceptions import AnthropicErrorResponse, AnthropicErrorType + # HTTP status code -> Anthropic error type # Source: https://docs.anthropic.com/en/api/errors ANTHROPIC_ERROR_TYPE_MAP: Dict[int, AnthropicErrorType] = { diff --git a/litellm/anthropic_interface/exceptions/exceptions.py b/litellm/anthropic_interface/exceptions/exceptions.py index b289e493e6b..984390fa702 100644 --- a/litellm/anthropic_interface/exceptions/exceptions.py +++ b/litellm/anthropic_interface/exceptions/exceptions.py @@ -2,6 +2,7 @@ from typing_extensions import Literal, Required, TypedDict + # Known Anthropic error types # Source: https://docs.anthropic.com/en/api/errors AnthropicErrorType = Literal[ diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 25860e72f2f..da3b9184edb 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -97,7 +97,7 @@ def _build_reasoning_item( def _reasoning_item_to_response_input( - r_item: Union[ChatCompletionReasoningItem, Dict[str, Any]], + r_item: Union[ChatCompletionReasoningItem, Dict[str, Any]] ) -> Dict[str, Any]: """Convert a stored ChatCompletionReasoningItem back to a Responses API input item.""" r_input: Dict[str, Any] = { diff --git a/litellm/compression/content_detection.py b/litellm/compression/content_detection.py index 975117eb608..0655a42daf5 100644 --- a/litellm/compression/content_detection.py +++ b/litellm/compression/content_detection.py @@ -5,6 +5,7 @@ Auto-detect content type per message: code, JSON, or text. import json import re + _CODE_KEYWORDS = re.compile( r"\b(?:def |function |class |import |from |require\(|#include|fn |func |const |let |var |public |private |static )\b" ) diff --git a/litellm/files/types.py b/litellm/files/types.py index ba42a39f666..688bc86f0cf 100644 --- a/litellm/files/types.py +++ b/litellm/files/types.py @@ -1,5 +1,6 @@ from typing import AsyncIterator, Dict, Iterator, Literal, NamedTuple, Union + FileContentProvider = Literal[ "openai", "azure", "vertex_ai", "bedrock", "hosted_vllm", "anthropic", "manus" ] diff --git a/litellm/google_genai/adapters/__init__.py b/litellm/google_genai/adapters/__init__.py index 6fbe7d95a55..bfa9e712678 100644 --- a/litellm/google_genai/adapters/__init__.py +++ b/litellm/google_genai/adapters/__init__.py @@ -1,10 +1,10 @@ """ Google GenAI Adapters for LiteLLM -This module provides adapters for transforming Google GenAI generate_content requests +This module provides adapters for transforming Google GenAI generate_content requests to/from LiteLLM completion format with full support for: - Text content transformation -- Tool calling (function declarations, function calls, function responses) +- Tool calling (function declarations, function calls, function responses) - Streaming (both regular and tool calling) - Mixed content (text + tool calls) """ diff --git a/litellm/integrations/SlackAlerting/batching_handler.py b/litellm/integrations/SlackAlerting/batching_handler.py index 828f3eb4175..fdce2e04793 100644 --- a/litellm/integrations/SlackAlerting/batching_handler.py +++ b/litellm/integrations/SlackAlerting/batching_handler.py @@ -1,9 +1,9 @@ """ -Handles Batching + sending Httpx Post requests to slack +Handles Batching + sending Httpx Post requests to slack -Slack alerts are sent every 10s or when events are greater than X events +Slack alerts are sent every 10s or when events are greater than X events -see custom_batch_logger.py for more details / defaults +see custom_batch_logger.py for more details / defaults """ from typing import TYPE_CHECKING, Any diff --git a/litellm/integrations/SlackAlerting/utils.py b/litellm/integrations/SlackAlerting/utils.py index e2580768178..e695266c88b 100644 --- a/litellm/integrations/SlackAlerting/utils.py +++ b/litellm/integrations/SlackAlerting/utils.py @@ -18,7 +18,7 @@ else: def process_slack_alerting_variables( - alert_to_webhook_url: Optional[Dict[AlertType, Union[List[str], str]]], + alert_to_webhook_url: Optional[Dict[AlertType, Union[List[str], str]]] ) -> Optional[Dict[AlertType, Union[List[str], str]]]: """ process alert_to_webhook_url diff --git a/litellm/integrations/additional_logging_utils.py b/litellm/integrations/additional_logging_utils.py index 59319140a18..795afd81d41 100644 --- a/litellm/integrations/additional_logging_utils.py +++ b/litellm/integrations/additional_logging_utils.py @@ -1,5 +1,5 @@ """ -Base class for Additional Logging Utils for CustomLoggers +Base class for Additional Logging Utils for CustomLoggers - Health Check for the logging util - Get Request / Response Payload for the logging util diff --git a/litellm/integrations/custom_batch_logger.py b/litellm/integrations/custom_batch_logger.py index 6da7aec5559..f9d4496c21f 100644 --- a/litellm/integrations/custom_batch_logger.py +++ b/litellm/integrations/custom_batch_logger.py @@ -1,5 +1,5 @@ """ -Custom Logger that handles batching logic +Custom Logger that handles batching logic Use this if you want your logs to be stored in memory and flushed periodically. """ diff --git a/litellm/integrations/focus/transformer.py b/litellm/integrations/focus/transformer.py index 6f4433b4a05..b7d28e3dbb9 100644 --- a/litellm/integrations/focus/transformer.py +++ b/litellm/integrations/focus/transformer.py @@ -9,6 +9,7 @@ import polars as pl from .schema import FOCUS_NORMALIZED_SCHEMA + _TAG_KEYS = ( "team_id", "team_alias", diff --git a/litellm/integrations/opik/utils.py b/litellm/integrations/opik/utils.py index 43577505c11..b0ab5991c91 100644 --- a/litellm/integrations/opik/utils.py +++ b/litellm/integrations/opik/utils.py @@ -105,7 +105,7 @@ def _remove_nulls(x: Dict[str, Any]) -> Dict[str, Any]: def get_traces_and_spans_from_payload( - payload: List[Dict[str, Any]], + payload: List[Dict[str, Any]] ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: """ Separate traces and spans from payload. diff --git a/litellm/integrations/s3_v2.py b/litellm/integrations/s3_v2.py index 4ed8a809a13..332e84dd07d 100644 --- a/litellm/integrations/s3_v2.py +++ b/litellm/integrations/s3_v2.py @@ -1,8 +1,8 @@ """ s3 Bucket Logging Integration -async_log_success_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 -async_log_failure_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 +async_log_success_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 +async_log_failure_event: Processes the event, stores it in memory for DEFAULT_S3_FLUSH_INTERVAL_SECONDS seconds or until DEFAULT_S3_BATCH_SIZE and then flushes to s3 NOTE 1: S3 does not provide a BATCH PUT API endpoint, so we create tasks to upload each element individually """ diff --git a/litellm/interactions/__init__.py b/litellm/interactions/__init__.py index 0077ac82076..e1125b649a6 100644 --- a/litellm/interactions/__init__.py +++ b/litellm/interactions/__init__.py @@ -5,28 +5,28 @@ This module provides SDK methods for Google's Interactions API. Usage: import litellm - + # Create an interaction with a model response = litellm.interactions.create( model="gemini-2.5-flash", input="Hello, how are you?" ) - + # Create an interaction with an agent response = litellm.interactions.create( agent="deep-research-pro-preview-12-2025", input="Research the current state of cancer research" ) - + # Async version response = await litellm.interactions.acreate(...) - + # Get an interaction response = litellm.interactions.get(interaction_id="...") - + # Delete an interaction result = litellm.interactions.delete(interaction_id="...") - + # Cancel an interaction result = litellm.interactions.cancel(interaction_id="...") diff --git a/litellm/interactions/main.py b/litellm/interactions/main.py index be9f2b99e0e..ab429ef6db5 100644 --- a/litellm/interactions/main.py +++ b/litellm/interactions/main.py @@ -8,25 +8,25 @@ Per OpenAPI spec (https://ai.google.dev/static/api/interactions.openapi.json): Usage: import litellm - + # Create an interaction with a model response = litellm.interactions.create( model="gemini-2.5-flash", input="Hello, how are you?" ) - + # Create an interaction with an agent response = litellm.interactions.create( agent="deep-research-pro-preview-12-2025", input="Research the current state of cancer research" ) - + # Async version response = await litellm.interactions.acreate(...) - + # Get an interaction response = litellm.interactions.get(interaction_id="...") - + # Delete an interaction result = litellm.interactions.delete(interaction_id="...") """ diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index cf20dbba70b..a815442c2f9 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -994,8 +994,10 @@ class Logging(LiteLLMLoggingBaseClass): try: # [Non-blocking Extra Debug Information in metadata] if turn_off_message_logging is True: - _metadata["raw_request"] = "redacted by litellm. \ + _metadata["raw_request"] = ( + "redacted by litellm. \ 'litellm.turn_off_message_logging=True'" + ) else: curl_command = self._get_request_curl_command( api_base=additional_args.get("api_base", ""), @@ -1029,8 +1031,12 @@ class Logging(LiteLLMLoggingBaseClass): error=str(e), ) ) - _metadata["raw_request"] = "Unable to Log \ - raw request: {}".format(str(e)) + _metadata["raw_request"] = ( + "Unable to Log \ + raw request: {}".format( + str(e) + ) + ) if getattr(self, "logger_fn", None) and callable(self.logger_fn): try: self.logger_fn( diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index b96785c59fe..abe9e016e26 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -5533,7 +5533,9 @@ def default_response_schema_prompt(response_schema: dict) -> str: prompt_str = """Use this JSON schema: ```json {} - ```""".format(response_schema) + ```""".format( + response_schema + ) return prompt_str diff --git a/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py b/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py index 0a6a4e82c72..13341f27a61 100644 --- a/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py +++ b/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py @@ -1,9 +1,9 @@ """ This is a cache for LangfuseLoggers. -Langfuse Python SDK initializes a thread for each client. +Langfuse Python SDK initializes a thread for each client. -This ensures we do +This ensures we do 1. Proper cleanup of Langfuse initialized clients. 2. Re-use created langfuse clients. """ diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py index d693d50b8e5..d0780c82d06 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py @@ -13,6 +13,7 @@ from typing import Any, AsyncIterator, Dict, List, Optional, cast from litellm._logging import verbose_logger + # --------------------------------------------------------------------------- # SSE parsing helpers (module-level to keep the class lean) # --------------------------------------------------------------------------- diff --git a/litellm/llms/azure/chat/o_series_transformation.py b/litellm/llms/azure/chat/o_series_transformation.py index 0a73597a4e4..cae7513245c 100644 --- a/litellm/llms/azure/chat/o_series_transformation.py +++ b/litellm/llms/azure/chat/o_series_transformation.py @@ -4,10 +4,10 @@ Support for o1 and o3 model families https://platform.openai.com/docs/guides/reasoning Translations handled by LiteLLM: -- modalities: image => drop param (if user opts in to dropping param) -- role: system ==> translate to role 'user' -- streaming => faked by LiteLLM -- Tools, response_format => drop param (if user opts in to dropping param) +- modalities: image => drop param (if user opts in to dropping param) +- role: system ==> translate to role 'user' +- streaming => faked by LiteLLM +- Tools, response_format => drop param (if user opts in to dropping param) - Logprobs => drop param (if user opts in to dropping param) - Temperature => drop param (if user opts in to dropping param) """ diff --git a/litellm/llms/azure_ai/embed/cohere_transformation.py b/litellm/llms/azure_ai/embed/cohere_transformation.py index bbbfb60fbde..64433c21b61 100644 --- a/litellm/llms/azure_ai/embed/cohere_transformation.py +++ b/litellm/llms/azure_ai/embed/cohere_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Azure AI Cohere's /v1/embed. +Transformation logic from OpenAI /v1/embeddings format to Azure AI Cohere's /v1/embed. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/azure_ai/rerank/transformation.py b/litellm/llms/azure_ai/rerank/transformation.py index f64133afa8b..b5993040ea0 100644 --- a/litellm/llms/azure_ai/rerank/transformation.py +++ b/litellm/llms/azure_ai/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Translate between Cohere's `/rerank` format and Azure AI's `/rerank` format. +Translate between Cohere's `/rerank` format and Azure AI's `/rerank` format. """ from typing import Optional diff --git a/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py b/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py index 64a79b73273..2747551af81 100644 --- a/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py +++ b/litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Bedrock Amazon Titan G1 /invoke format. +Transformation logic from OpenAI /v1/embeddings format to Bedrock Amazon Titan G1 /invoke format. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/bedrock/embed/cohere_transformation.py b/litellm/llms/bedrock/embed/cohere_transformation.py index 885c91f9754..d00cb74aae0 100644 --- a/litellm/llms/bedrock/embed/cohere_transformation.py +++ b/litellm/llms/bedrock/embed/cohere_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Bedrock Cohere /invoke format. +Transformation logic from OpenAI /v1/embeddings format to Bedrock Cohere /invoke format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/bedrock_mantle/chat/transformation.py b/litellm/llms/bedrock_mantle/chat/transformation.py index 81a56030a5c..e413bb22b2d 100644 --- a/litellm/llms/bedrock_mantle/chat/transformation.py +++ b/litellm/llms/bedrock_mantle/chat/transformation.py @@ -16,6 +16,7 @@ from litellm.secret_managers.main import get_secret_str from ...openai_like.chat.transformation import OpenAILikeChatConfig + BEDROCK_MANTLE_DEFAULT_REGION = "us-east-1" diff --git a/litellm/llms/cohere/embed/handler.py b/litellm/llms/cohere/embed/handler.py index 81b6a1c7aec..3ab8baf7ba8 100644 --- a/litellm/llms/cohere/embed/handler.py +++ b/litellm/llms/cohere/embed/handler.py @@ -1,5 +1,5 @@ """ -Legacy /v1/embedding handler for Bedrock Cohere. +Legacy /v1/embedding handler for Bedrock Cohere. """ import json diff --git a/litellm/llms/custom_httpx/mock_transport.py b/litellm/llms/custom_httpx/mock_transport.py index ad93cc134ee..c9844753e0e 100644 --- a/litellm/llms/custom_httpx/mock_transport.py +++ b/litellm/llms/custom_httpx/mock_transport.py @@ -13,6 +13,7 @@ from typing import Tuple import httpx + # --------------------------------------------------------------------------- # Pre-built response templates # --------------------------------------------------------------------------- diff --git a/litellm/llms/dashscope/cost_calculator.py b/litellm/llms/dashscope/cost_calculator.py index 8bb7f605b82..9b3e3851162 100644 --- a/litellm/llms/dashscope/cost_calculator.py +++ b/litellm/llms/dashscope/cost_calculator.py @@ -1,5 +1,5 @@ """ -Cost calculator for Dashscope Chat models. +Cost calculator for Dashscope Chat models. Handles tiered pricing and prompt caching scenarios. """ diff --git a/litellm/llms/datarobot/chat/transformation.py b/litellm/llms/datarobot/chat/transformation.py index f81e2420930..23ce63c25b2 100644 --- a/litellm/llms/datarobot/chat/transformation.py +++ b/litellm/llms/datarobot/chat/transformation.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/chat/completions` endpoint. +Support for OpenAI's `/v1/chat/completions` endpoint. Calls done in OpenAI/openai.py as DataRobot is openai-compatible. """ diff --git a/litellm/llms/deepinfra/rerank/transformation.py b/litellm/llms/deepinfra/rerank/transformation.py index e4bfbcb2513..276735f4758 100644 --- a/litellm/llms/deepinfra/rerank/transformation.py +++ b/litellm/llms/deepinfra/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Translate between Cohere's `/rerank` format and Deepinfra's `/rerank` format. +Translate between Cohere's `/rerank` format and Deepinfra's `/rerank` format. """ from typing import Any, Dict, List, Optional, Union diff --git a/litellm/llms/deepseek/cost_calculator.py b/litellm/llms/deepseek/cost_calculator.py index e652ebeac54..0f4490cb3df 100644 --- a/litellm/llms/deepseek/cost_calculator.py +++ b/litellm/llms/deepseek/cost_calculator.py @@ -1,5 +1,5 @@ """ -Cost calculator for DeepSeek Chat models. +Cost calculator for DeepSeek Chat models. Handles prompt caching scenario. """ diff --git a/litellm/llms/elevenlabs/text_to_speech/transformation.py b/litellm/llms/elevenlabs/text_to_speech/transformation.py index 612fc687ef9..6a59911701b 100644 --- a/litellm/llms/elevenlabs/text_to_speech/transformation.py +++ b/litellm/llms/elevenlabs/text_to_speech/transformation.py @@ -22,6 +22,7 @@ from litellm.types.utils import all_litellm_params from ..common_utils import ElevenLabsException + if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.types.llms.openai import HttpxBinaryResponseContent diff --git a/litellm/llms/gemini/videos/transformation.py b/litellm/llms/gemini/videos/transformation.py index 9714c8a3923..c7116940b22 100644 --- a/litellm/llms/gemini/videos/transformation.py +++ b/litellm/llms/gemini/videos/transformation.py @@ -55,7 +55,7 @@ def _convert_image_to_gemini_format(image_file) -> Dict[str, str]: def _usage_video_resolution_from_parameters( - parameters: Dict[str, Any], + parameters: Dict[str, Any] ) -> Optional[str]: """Normalize Veo ``parameters.resolution`` for usage and cost tracking.""" res = parameters.get("resolution") diff --git a/litellm/llms/infinity/rerank/transformation.py b/litellm/llms/infinity/rerank/transformation.py index b9804605454..314bf2f8a36 100644 --- a/litellm/llms/infinity/rerank/transformation.py +++ b/litellm/llms/infinity/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from Cohere's /v1/rerank format to Infinity's `/v1/rerank` format. +Transformation logic from Cohere's /v1/rerank format to Infinity's `/v1/rerank` format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/jina_ai/rerank/transformation.py b/litellm/llms/jina_ai/rerank/transformation.py index 56be754fc34..ad4416925a6 100644 --- a/litellm/llms/jina_ai/rerank/transformation.py +++ b/litellm/llms/jina_ai/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from Cohere's /v1/rerank format to Jina AI's `/v1/rerank` format. +Transformation logic from Cohere's /v1/rerank format to Jina AI's `/v1/rerank` format. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/lm_studio/embed/transformation.py b/litellm/llms/lm_studio/embed/transformation.py index 87f4f6e73d5..1285550c30f 100644 --- a/litellm/llms/lm_studio/embed/transformation.py +++ b/litellm/llms/lm_studio/embed/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to LM Studio's `/v1/embeddings` format. +Transformation logic from OpenAI /v1/embeddings format to LM Studio's `/v1/embeddings` format. Why separate file? Make it easy to see how transformation works diff --git a/litellm/llms/novita/chat/transformation.py b/litellm/llms/novita/chat/transformation.py index 5a64a124ade..c05d2d7b2c5 100644 --- a/litellm/llms/novita/chat/transformation.py +++ b/litellm/llms/novita/chat/transformation.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/chat/completions` endpoint. +Support for OpenAI's `/v1/chat/completions` endpoint. Calls done in OpenAI/openai.py as Novita AI is openai-compatible. diff --git a/litellm/llms/nvidia_nim/chat/transformation.py b/litellm/llms/nvidia_nim/chat/transformation.py index 2ef92a90626..b8f8b04eb53 100644 --- a/litellm/llms/nvidia_nim/chat/transformation.py +++ b/litellm/llms/nvidia_nim/chat/transformation.py @@ -1,7 +1,7 @@ """ -Nvidia NIM endpoint: https://docs.api.nvidia.com/nim/reference/databricks-dbrx-instruct-infer +Nvidia NIM endpoint: https://docs.api.nvidia.com/nim/reference/databricks-dbrx-instruct-infer -This is OpenAI compatible +This is OpenAI compatible This file only contains param mapping logic diff --git a/litellm/llms/nvidia_nim/embed.py b/litellm/llms/nvidia_nim/embed.py index 61c8e8244e4..24c6cc34e4d 100644 --- a/litellm/llms/nvidia_nim/embed.py +++ b/litellm/llms/nvidia_nim/embed.py @@ -1,7 +1,7 @@ """ Nvidia NIM embeddings endpoint: https://docs.api.nvidia.com/nim/reference/nvidia-nv-embedqa-e5-v5-infer -This is OpenAI compatible +This is OpenAI compatible This file only contains param mapping logic diff --git a/litellm/llms/openai/chat/o_series_transformation.py b/litellm/llms/openai/chat/o_series_transformation.py index 8db7ecf7b3a..02ae2cc9750 100644 --- a/litellm/llms/openai/chat/o_series_transformation.py +++ b/litellm/llms/openai/chat/o_series_transformation.py @@ -1,14 +1,14 @@ """ -Support for o1/o3 model family +Support for o1/o3 model family https://platform.openai.com/docs/guides/reasoning Translations handled by LiteLLM: -- modalities: image => drop param (if user opts in to dropping param) -- role: system ==> translate to role 'user' -- streaming => faked by LiteLLM -- Tools, response_format => drop param (if user opts in to dropping param) -- Logprobs => drop param (if user opts in to dropping param) +- modalities: image => drop param (if user opts in to dropping param) +- role: system ==> translate to role 'user' +- streaming => faked by LiteLLM +- Tools, response_format => drop param (if user opts in to dropping param) +- Logprobs => drop param (if user opts in to dropping param) """ from typing import Any, Coroutine, List, Literal, Optional, Union, cast, overload diff --git a/litellm/llms/openai/common_utils.py b/litellm/llms/openai/common_utils.py index 381f215a13f..c13a976c1b9 100644 --- a/litellm/llms/openai/common_utils.py +++ b/litellm/llms/openai/common_utils.py @@ -201,7 +201,7 @@ class BaseOpenAILLM: @staticmethod def get_openai_client_initialization_param_fields( - client_type: Literal["openai", "azure"], + client_type: Literal["openai", "azure"] ) -> Tuple[str, ...]: """Returns a tuple of fields that are used to initialize the OpenAI client""" if client_type == "openai": diff --git a/litellm/llms/openrouter/image_generation/transformation.py b/litellm/llms/openrouter/image_generation/transformation.py index 9c2293eb3f1..a55716a5e50 100644 --- a/litellm/llms/openrouter/image_generation/transformation.py +++ b/litellm/llms/openrouter/image_generation/transformation.py @@ -49,6 +49,7 @@ from litellm.types.utils import ( ) from litellm.llms.openrouter.common_utils import OpenRouterException + if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj else: diff --git a/litellm/llms/sagemaker/completion/transformation.py b/litellm/llms/sagemaker/completion/transformation.py index 8fd32bc4460..3e4e2460cdb 100644 --- a/litellm/llms/sagemaker/completion/transformation.py +++ b/litellm/llms/sagemaker/completion/transformation.py @@ -1,7 +1,7 @@ """ Translate from OpenAI's `/v1/chat/completions` to Sagemaker's `/invoke` -In the Huggingface TGI format. +In the Huggingface TGI format. """ import json diff --git a/litellm/llms/sagemaker/embedding/transformation.py b/litellm/llms/sagemaker/embedding/transformation.py index 09bdb9295e7..04430171187 100644 --- a/litellm/llms/sagemaker/embedding/transformation.py +++ b/litellm/llms/sagemaker/embedding/transformation.py @@ -1,7 +1,7 @@ """ Translate from OpenAI's `/v1/embeddings` to Sagemaker's `/invoke` -In the Huggingface TGI format. +In the Huggingface TGI format. """ from typing import TYPE_CHECKING, Any, List, Optional, Union diff --git a/litellm/llms/sap/credentials.py b/litellm/llms/sap/credentials.py index dd307ddf496..0ae351783e8 100644 --- a/litellm/llms/sap/credentials.py +++ b/litellm/llms/sap/credentials.py @@ -207,7 +207,7 @@ def resolve_resource_group(sources: List[Source]) -> Optional[str]: def _parse_service_key_once( - service_key: Optional[Union[str, dict]], + service_key: Optional[Union[str, dict]] ) -> Optional[Dict[str, Any]]: """ Pre-parse service_key if it's a string to avoid repeated JSON parsing. diff --git a/litellm/llms/snowflake/chat/transformation.py b/litellm/llms/snowflake/chat/transformation.py index 23bb6f44757..3e590680a75 100644 --- a/litellm/llms/snowflake/chat/transformation.py +++ b/litellm/llms/snowflake/chat/transformation.py @@ -14,6 +14,7 @@ from ...openai_like.chat.transformation import OpenAIGPTConfig from ..utils import SnowflakeBaseConfig + if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj diff --git a/litellm/llms/together_ai/chat.py b/litellm/llms/together_ai/chat.py index 238849cc1ec..7efb12fc1b2 100644 --- a/litellm/llms/together_ai/chat.py +++ b/litellm/llms/together_ai/chat.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/chat/completions` endpoint. +Support for OpenAI's `/v1/chat/completions` endpoint. Calls done in OpenAI/openai.py as TogetherAI is openai-compatible. diff --git a/litellm/llms/together_ai/embed.py b/litellm/llms/together_ai/embed.py index 6a39b94acfc..577df0256cc 100644 --- a/litellm/llms/together_ai/embed.py +++ b/litellm/llms/together_ai/embed.py @@ -1,5 +1,5 @@ """ -Support for OpenAI's `/v1/embeddings` endpoint. +Support for OpenAI's `/v1/embeddings` endpoint. Calls done in OpenAI/openai.py as TogetherAI is openai-compatible. diff --git a/litellm/llms/together_ai/rerank/transformation.py b/litellm/llms/together_ai/rerank/transformation.py index f4d642bd25a..63b593dfe42 100644 --- a/litellm/llms/together_ai/rerank/transformation.py +++ b/litellm/llms/together_ai/rerank/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from Cohere's /v1/rerank format to Together AI's `/v1/rerank` format. +Transformation logic from Cohere's /v1/rerank format to Together AI's `/v1/rerank` format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/vertex_ai/context_caching/transformation.py b/litellm/llms/vertex_ai/context_caching/transformation.py index ef71357d1d2..950edbeb478 100644 --- a/litellm/llms/vertex_ai/context_caching/transformation.py +++ b/litellm/llms/vertex_ai/context_caching/transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic for context caching. +Transformation logic for context caching. Why separate file? Make it easy to see how transformation works """ @@ -19,7 +19,7 @@ from ..gemini.transformation import ( def get_first_continuous_block_idx( - filtered_messages: List[Tuple[int, AllMessageValues]], # (idx, message) + filtered_messages: List[Tuple[int, AllMessageValues]] # (idx, message) ) -> int: """ Find the array index that ends the first continuous sequence of message blocks. diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index d20c26b8a26..9afa5dec465 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -632,14 +632,16 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 contents.append(ContentType(role="user", parts=tool_call_responses)) if len(contents) == 0: - verbose_logger.warning(""" + verbose_logger.warning( + """ No contents in messages. Contents are required. See https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/generateContent#request-body. If the original request did not comply to OpenAI API requirements it should have failed by now, but LiteLLM does not check for missing messages. Setting an empty content to prevent an 400 error. Relevant Issue - https://github.com/BerriAI/litellm/issues/9733 - """) + """ + ) contents.append(ContentType(role="user", parts=[PartType(text=" ")])) return contents except Exception as e: diff --git a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py index ba6e6f0c056..e1b365c9f42 100644 --- a/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py +++ b/litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py @@ -1,5 +1,5 @@ """ -Transformation logic from OpenAI /v1/embeddings format to Google AI Studio /batchEmbedContents format. +Transformation logic from OpenAI /v1/embeddings format to Google AI Studio /batchEmbedContents format. Why separate file? Make it easy to see how transformation works """ diff --git a/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py b/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py index b835ad7d8fa..9d9015c2b91 100644 --- a/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py +++ b/litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py @@ -139,7 +139,7 @@ class VertexTextToSpeechAPI(VertexLLM): ########## End of logging ############ ####### Send the request ################### if _is_async is True: - return self.async_audio_speech( # type: ignore + return self.async_audio_speech( # type:ignore logging_obj=logging_obj, url=url, headers=headers, request=request ) sync_handler = _get_httpx_client() diff --git a/litellm/llms/vllm/completion/transformation.py b/litellm/llms/vllm/completion/transformation.py index e03b07f9897..ec4c07e95d8 100644 --- a/litellm/llms/vllm/completion/transformation.py +++ b/litellm/llms/vllm/completion/transformation.py @@ -1,5 +1,5 @@ """ -Translates from OpenAI's `/v1/chat/completions` to the VLLM sdk `llm.generate`. +Translates from OpenAI's `/v1/chat/completions` to the VLLM sdk `llm.generate`. NOT RECOMMENDED FOR PRODUCTION USE. Use `hosted_vllm/` instead. """ diff --git a/litellm/llms/voyage/embedding/transformation_contextual.py b/litellm/llms/voyage/embedding/transformation_contextual.py index 1f5ca99f47d..40328062e09 100644 --- a/litellm/llms/voyage/embedding/transformation_contextual.py +++ b/litellm/llms/voyage/embedding/transformation_contextual.py @@ -1,6 +1,6 @@ """ -This module is used to transform the request and response for the Voyage contextualized embeddings API. -This would be used for all the contextualized embeddings models in Voyage. +This module is used to transform the request and response for the Voyage contextualized embeddings API. +This would be used for all the contextualized embeddings models in Voyage. """ from typing import List, Optional, Union diff --git a/litellm/proxy/common_utils/custom_openapi_spec.py b/litellm/proxy/common_utils/custom_openapi_spec.py index fa3cb02195b..a93749c3952 100644 --- a/litellm/proxy/common_utils/custom_openapi_spec.py +++ b/litellm/proxy/common_utils/custom_openapi_spec.py @@ -324,7 +324,7 @@ class CustomOpenAPISpec: @staticmethod def add_chat_completion_request_schema( - openapi_schema: Dict[str, Any], + openapi_schema: Dict[str, Any] ) -> Dict[str, Any]: """ Add ProxyChatCompletionRequest schema to chat completion endpoints for documentation. @@ -380,7 +380,7 @@ class CustomOpenAPISpec: @staticmethod def add_responses_api_request_schema( - openapi_schema: Dict[str, Any], + openapi_schema: Dict[str, Any] ) -> Dict[str, Any]: """ Add ResponsesAPIRequestParams schema to responses API endpoints for documentation. @@ -410,7 +410,7 @@ class CustomOpenAPISpec: @staticmethod def add_llm_api_request_schema_body( - openapi_schema: Dict[str, Any], + openapi_schema: Dict[str, Any] ) -> Dict[str, Any]: """ Add LLM API request schema bodies to OpenAPI specification for documentation. diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 7b5a068edfe..71abdfa5e9e 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -257,7 +257,7 @@ async def get_form_data(request: Request) -> Dict[str, Any]: async def convert_upload_files_to_file_data( - form_data: Dict[str, Any], + form_data: Dict[str, Any] ) -> Dict[str, Any]: """ Convert FastAPI UploadFile objects to file data tuples for litellm. diff --git a/litellm/proxy/common_utils/openai_endpoint_utils.py b/litellm/proxy/common_utils/openai_endpoint_utils.py index 905967fa465..c4bfe11aec1 100644 --- a/litellm/proxy/common_utils/openai_endpoint_utils.py +++ b/litellm/proxy/common_utils/openai_endpoint_utils.py @@ -1,5 +1,5 @@ """ -Contains utils used by OpenAI compatible endpoints +Contains utils used by OpenAI compatible endpoints """ from typing import Optional, Set diff --git a/litellm/proxy/config_management_endpoints/pass_through_endpoints.py b/litellm/proxy/config_management_endpoints/pass_through_endpoints.py index 4ebd989dc53..5ff02b8bce0 100644 --- a/litellm/proxy/config_management_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/config_management_endpoints/pass_through_endpoints.py @@ -1,5 +1,5 @@ """ -What is this? +What is this? CRUD endpoints for managing pass-through endpoints """ diff --git a/litellm/proxy/db/create_views.py b/litellm/proxy/db/create_views.py index 97525a528d0..d84cebcf05a 100644 --- a/litellm/proxy/db/create_views.py +++ b/litellm/proxy/db/create_views.py @@ -34,7 +34,8 @@ async def create_missing_views(db: _db): # noqa: PLR0915 if not any(marker in error_msg for marker in _VIEW_NOT_FOUND_MARKERS): raise # If an error occurs, the view does not exist, so create it - await db.execute_raw(""" + await db.execute_raw( + """ CREATE VIEW "LiteLLM_VerificationTokenView" AS SELECT v.*, @@ -46,7 +47,8 @@ async def create_missing_views(db: _db): # noqa: PLR0915 FROM "LiteLLM_VerificationToken" v LEFT JOIN "LiteLLM_TeamTable" t ON v.team_id = t.team_id LEFT JOIN "LiteLLM_ProjectTable" p ON v.project_id = p.project_id; - """) + """ + ) verbose_logger.debug("LiteLLM_VerificationTokenView Created!") diff --git a/litellm/proxy/guardrails/_content_utils.py b/litellm/proxy/guardrails/_content_utils.py index 766ef0cf9f6..7cad1352a79 100644 --- a/litellm/proxy/guardrails/_content_utils.py +++ b/litellm/proxy/guardrails/_content_utils.py @@ -10,6 +10,7 @@ every text fragment. from typing import Any, Callable, Dict, FrozenSet, Iterator, List + # Call types whose body carries free-form chat / prompt text that # text-content guardrails (banned keywords, content moderation, secret # detection, …) should inspect. The proxy ingress passes ``route_type`` diff --git a/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py index 1e3dd906b9f..c4aaea709ba 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py @@ -4,6 +4,7 @@ from litellm.types.guardrails import SupportedGuardrailIntegrations from .akto import AktoGuardrail + if TYPE_CHECKING: from litellm.types.guardrails import Guardrail, LitellmParams diff --git a/litellm/proxy/hooks/litellm_skills/__init__.py b/litellm/proxy/hooks/litellm_skills/__init__.py index 1507b652ab4..057cf3d8b38 100644 --- a/litellm/proxy/hooks/litellm_skills/__init__.py +++ b/litellm/proxy/hooks/litellm_skills/__init__.py @@ -6,7 +6,7 @@ The actual skill logic is in litellm/llms/litellm_proxy/skills/. Usage: from litellm.proxy.hooks.litellm_skills import SkillsInjectionHook - + # Register hook in proxy litellm.callbacks.append(SkillsInjectionHook()) """ diff --git a/litellm/proxy/management_endpoints/budget_management_endpoints.py b/litellm/proxy/management_endpoints/budget_management_endpoints.py index 20735b35528..81b133e6c81 100644 --- a/litellm/proxy/management_endpoints/budget_management_endpoints.py +++ b/litellm/proxy/management_endpoints/budget_management_endpoints.py @@ -1,9 +1,9 @@ """ BUDGET MANAGEMENT -All /budget management endpoints +All /budget management endpoints -/budget/new +/budget/new /budget/info /budget/update /budget/delete diff --git a/litellm/proxy/management_endpoints/customer_endpoints.py b/litellm/proxy/management_endpoints/customer_endpoints.py index 1fd8320db20..4889f0b7f80 100644 --- a/litellm/proxy/management_endpoints/customer_endpoints.py +++ b/litellm/proxy/management_endpoints/customer_endpoints.py @@ -1,9 +1,9 @@ """ CUSTOMER MANAGEMENT -All /customer management endpoints +All /customer management endpoints -/customer/new +/customer/new /customer/info /customer/update /customer/delete diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index b14d58e8337..af84bc123ff 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -529,7 +529,7 @@ async def _update_existing_team_model_assignment( """ def _get_team_public_model_name( - model_info: Optional[Union[dict, str]], + model_info: Optional[Union[dict, str]] ) -> Optional[str]: if isinstance(model_info, dict): value = model_info.get("team_public_model_name") diff --git a/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py b/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py index 04e44c623d1..191212d6f0b 100644 --- a/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py +++ b/litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py @@ -7,7 +7,7 @@ variables. Environment Variables: - MICROSOFT_AUTHORIZATION_ENDPOINT: Custom authorization endpoint URL -- MICROSOFT_TOKEN_ENDPOINT: Custom token endpoint URL +- MICROSOFT_TOKEN_ENDPOINT: Custom token endpoint URL - MICROSOFT_USERINFO_ENDPOINT: Custom userinfo endpoint URL If these are not set, the default Microsoft endpoints are used. diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 564ea85200c..259624f1e18 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -4347,7 +4347,9 @@ async def list_team( except Exception as e: team_exception = """Invalid team object for team_id: {}. team_object={}. Error: {} - """.format(team.team_id, team.model_dump(), str(e)) + """.format( + team.team_id, team.model_dump(), str(e) + ) verbose_proxy_logger.exception(team_exception) continue # Sort the responses by team_alias diff --git a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py index ebd276fbee5..872b6fa2250 100644 --- a/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py +++ b/litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py @@ -3,7 +3,7 @@ User Agent Analytics Endpoints This module provides optimized endpoints for tracking user agent activity metrics including: - Daily Active Users (DAU) by tags for configurable number of days -- Weekly Active Users (WAU) by tags for configurable number of weeks +- Weekly Active Users (WAU) by tags for configurable number of weeks - Monthly Active Users (MAU) by tags for configurable number of months - Summary analytics by tags diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py index e7696e5a18a..a104f962630 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py @@ -18,6 +18,7 @@ from litellm.litellm_core_utils.litellm_logging import ( from litellm.proxy._types import PassThroughEndpointLoggingTypedDict from litellm.types.utils import StandardPassThroughResponseObject + CURSOR_AGENT_ENDPOINTS: Dict[str, str] = { "POST /v0/agents": "cursor:agent:create", "GET /v0/agents": "cursor:agent:list", diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index d7e04aea704..90bbfdd25a4 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -292,7 +292,9 @@ class ProxyInitializationHelpers: _endpoint_str = ( f"curl --location 'http://0.0.0.0:{port}/chat/completions' \\" ) - curl_command = _endpoint_str + """ + curl_command = ( + _endpoint_str + + """ --header 'Content-Type: application/json' \\ --data ' { "model": "gpt-3.5-turbo", @@ -305,6 +307,7 @@ class ProxyInitializationHelpers: }' \n """ + ) print() # noqa print( # noqa '\033[1;34mLiteLLM: Test your local proxy with: "litellm --test" This runs an openai.ChatCompletion request to your proxy [In a new terminal tab]\033[0m\n' @@ -380,9 +383,11 @@ class ProxyInitializationHelpers: with open(os.devnull, "w") as devnull: subprocess.Popen(command, stdout=devnull, stderr=devnull) except Exception as e: - print(f""" + print( # noqa + f""" LiteLLM Warning: proxy started with `ollama` model\n`ollama serve` failed with Exception{e}. \nEnsure you run `ollama serve` - """) # noqa # noqa + """ + ) # noqa @staticmethod def _is_port_in_use(port): diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0614db4fb12..c96d0acb008 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2688,9 +2688,11 @@ def run_ollama_serve(): with open(os.devnull, "w") as devnull: subprocess.Popen(command, stdout=devnull, stderr=devnull) except Exception as e: - verbose_proxy_logger.debug(f""" + verbose_proxy_logger.debug( + f""" LiteLLM Warning: proxy started with `ollama` model\n`ollama serve` failed with Exception{e}. \nEnsure you run `ollama serve` - """) + """ + ) def _get_process_rss_mb() -> Optional[float]: diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index e3019801aae..d030fabe8b5 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -3184,14 +3184,16 @@ async def provider_budgets() -> ProviderBudgetResponse: async def get_spend_by_tags( prisma_client: PrismaClient, start_date=None, end_date=None ): - response = await prisma_client.db.query_raw(""" + response = await prisma_client.db.query_raw( + """ SELECT jsonb_array_elements_text(request_tags) AS individual_request_tag, COUNT(*) AS log_count, SUM(spend) AS total_spend FROM "LiteLLM_SpendLogs" GROUP BY individual_request_tag; - """) + """ + ) return response diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index e0a956c1669..a52dc8e55fb 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -2712,7 +2712,8 @@ class PrismaClient: required_view = "LiteLLM_VerificationTokenView" expected_views_str = ", ".join(f"'{view}'" for view in expected_views) pg_schema = os.getenv("DATABASE_SCHEMA", "public") - ret = await self.db.query_raw(f""" + ret = await self.db.query_raw( + f""" WITH existing_views AS ( SELECT viewname FROM pg_views @@ -2724,7 +2725,8 @@ class PrismaClient: (SELECT COUNT(*) FROM existing_views) AS view_count, ARRAY_AGG(viewname) AS view_names FROM existing_views - """) + """ + ) expected_total_views = len(expected_views) if ret[0]["view_count"] == expected_total_views: verbose_proxy_logger.info("All necessary views exist!") @@ -2733,7 +2735,8 @@ class PrismaClient: ## check if required view exists ## if ret[0]["view_names"] and required_view not in ret[0]["view_names"]: await self.health_check() # make sure we can connect to db - await self.db.execute_raw(""" + await self.db.execute_raw( + """ CREATE VIEW "LiteLLM_VerificationTokenView" AS SELECT v.*, @@ -2743,7 +2746,8 @@ class PrismaClient: t.rpm_limit AS team_rpm_limit FROM "LiteLLM_VerificationToken" v LEFT JOIN "LiteLLM_TeamTable" t ON v.team_id = t.team_id; - """) + """ + ) verbose_proxy_logger.info( "LiteLLM_VerificationTokenView Created in DB!" diff --git a/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py b/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py index b47f6a747db..8ce1bedcf90 100644 --- a/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py +++ b/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py @@ -1,5 +1,5 @@ """ -What is this? +What is this? Logging Pass-Through Endpoints """ diff --git a/litellm/router.py b/litellm/router.py index c3537ffd64b..7512ee387dc 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -826,7 +826,7 @@ class Router: @staticmethod def _normalize_strategy( - strategy: Union[RoutingStrategy, str, None], + strategy: Union[RoutingStrategy, str, None] ) -> Optional[str]: if strategy is None: return None diff --git a/litellm/router_strategy/adaptive_router/hooks.py b/litellm/router_strategy/adaptive_router/hooks.py index 99fe5e26f7f..9e346006ac1 100644 --- a/litellm/router_strategy/adaptive_router/hooks.py +++ b/litellm/router_strategy/adaptive_router/hooks.py @@ -103,7 +103,7 @@ def _last_user_content(messages: Optional[List[Dict[str, Any]]]) -> Optional[str def _recent_tool_results( - messages: Optional[List[Dict[str, Any]]], + messages: Optional[List[Dict[str, Any]]] ) -> List[Dict[str, Any]]: """Extract the current turn's tool result payloads from the request messages. diff --git a/litellm/router_strategy/adaptive_router/signals.py b/litellm/router_strategy/adaptive_router/signals.py index 5e33a64d27f..a48bdea1eb6 100644 --- a/litellm/router_strategy/adaptive_router/signals.py +++ b/litellm/router_strategy/adaptive_router/signals.py @@ -24,6 +24,7 @@ from litellm.router_strategy.adaptive_router.config import ( TOOL_CALL_HISTORY_MAX, ) + # ---- Public types --------------------------------------------------------- diff --git a/litellm/router_strategy/budget_limiter.py b/litellm/router_strategy/budget_limiter.py index da41577e99a..be27b852478 100644 --- a/litellm/router_strategy/budget_limiter.py +++ b/litellm/router_strategy/budget_limiter.py @@ -10,11 +10,11 @@ This means you can use this with weighted-pick, lowest-latency, simple-shuffle, Example: ``` openai: - budget_limit: 0.000000000001 - time_period: 1d + budget_limit: 0.000000000001 + time_period: 1d anthropic: - budget_limit: 100 - time_period: 7d + budget_limit: 100 + time_period: 7d ``` """ diff --git a/litellm/router_utils/get_retry_from_policy.py b/litellm/router_utils/get_retry_from_policy.py index 162d6428f85..ec326ebb50d 100644 --- a/litellm/router_utils/get_retry_from_policy.py +++ b/litellm/router_utils/get_retry_from_policy.py @@ -1,5 +1,5 @@ """ -Get num retries for an exception. +Get num retries for an exception. - Account for retry policy by exception type. """ diff --git a/litellm/router_utils/pattern_match_deployments.py b/litellm/router_utils/pattern_match_deployments.py index 48f85a83411..17b453d6031 100644 --- a/litellm/router_utils/pattern_match_deployments.py +++ b/litellm/router_utils/pattern_match_deployments.py @@ -34,7 +34,7 @@ class PatternUtils: @staticmethod def sorted_patterns( - patterns: Dict[str, List[Dict]], + patterns: Dict[str, List[Dict]] ) -> List[Tuple[str, List[Dict]]]: """ Cached property for patterns sorted by specificity. diff --git a/litellm/router_utils/router_callbacks/track_deployment_metrics.py b/litellm/router_utils/router_callbacks/track_deployment_metrics.py index 9039b0df8e6..1f226879d03 100644 --- a/litellm/router_utils/router_callbacks/track_deployment_metrics.py +++ b/litellm/router_utils/router_callbacks/track_deployment_metrics.py @@ -1,5 +1,5 @@ """ -Helper functions to get/set num success and num failures per deployment +Helper functions to get/set num success and num failures per deployment set_deployment_failures_for_current_minute diff --git a/litellm/secret_managers/aws_secret_manager.py b/litellm/secret_managers/aws_secret_manager.py index 60d0a713eff..fbe951e6492 100644 --- a/litellm/secret_managers/aws_secret_manager.py +++ b/litellm/secret_managers/aws_secret_manager.py @@ -4,7 +4,7 @@ This is a file for the AWS Secret Manager Integration Relevant issue: https://github.com/BerriAI/litellm/issues/1883 Requires: -* `os.environ["AWS_REGION_NAME"], +* `os.environ["AWS_REGION_NAME"], * `pip install boto3>=1.28.57` """ diff --git a/litellm/secret_managers/aws_secret_manager_v2.py b/litellm/secret_managers/aws_secret_manager_v2.py index 4461e34396e..c1b4d019dcf 100644 --- a/litellm/secret_managers/aws_secret_manager_v2.py +++ b/litellm/secret_managers/aws_secret_manager_v2.py @@ -10,7 +10,7 @@ Handles Async Operations for: Relevant issue: https://github.com/BerriAI/litellm/issues/1883 Requires: -* `os.environ["AWS_REGION_NAME"], +* `os.environ["AWS_REGION_NAME"], * `pip install boto3>=1.28.57` """ diff --git a/litellm/vector_store_files/utils.py b/litellm/vector_store_files/utils.py index 1ee5b47e306..ffe73516bda 100644 --- a/litellm/vector_store_files/utils.py +++ b/litellm/vector_store_files/utils.py @@ -21,7 +21,7 @@ class VectorStoreFileRequestUtils: @staticmethod def get_create_request_params( - params: Dict[str, Any], + params: Dict[str, Any] ) -> VectorStoreFileCreateRequest: filtered = VectorStoreFileRequestUtils._filter_params( params=params, model=VectorStoreFileCreateRequest @@ -37,7 +37,7 @@ class VectorStoreFileRequestUtils: @staticmethod def get_update_request_params( - params: Dict[str, Any], + params: Dict[str, Any] ) -> VectorStoreFileUpdateRequest: filtered = VectorStoreFileRequestUtils._filter_params( params=params, model=VectorStoreFileUpdateRequest diff --git a/pyproject.toml b/pyproject.toml index 9b48c841e9b..7ff388f1840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,11 +76,7 @@ utils = [ # Not in Docker or PyPI proxy extra. "numpydoc==1.8.0", ] -# diskcache intentionally unpinned: CVE-2025-69872 (pickle RCE) has no -# upstream fix. Stub kept so `pip install litellm[caching]` doesn't warn; -# DiskCache loads diskcache lazily and forces JSONDisk for safety. See -# litellm/caching/disk_cache.py. -caching = [] +caching = ["diskcache==5.6.3"] semantic-router = [ "semantic-router==0.1.12; python_version < '3.14'", "aurelio-sdk==0.0.19; python_version < '3.14'", @@ -130,7 +126,7 @@ litellm-proxy = "litellm.proxy.client.cli:cli" dev = [ "diff-cover==9.7.2", "flake8==7.3.0", - "black==26.3.1", + "black==24.10.0", "mypy==1.19.0", "pytest==9.0.3", "pytest-mock==3.15.1", diff --git a/scripts/adaptive_router_demo/eval.py b/scripts/adaptive_router_demo/eval.py index 66b91c71932..b02e4a37d31 100644 --- a/scripts/adaptive_router_demo/eval.py +++ b/scripts/adaptive_router_demo/eval.py @@ -35,7 +35,7 @@ import httpx class EvalCase: category: str prompt: str - ideal: str # criteria the judge checks the response against + ideal: str # criteria the judge checks the response against EVAL_CASES: List[EvalCase] = [ @@ -177,19 +177,14 @@ async def evaluate( async with httpx.AsyncClient() as client: for i, case in enumerate(EVAL_CASES, 1): print(f"\n[{i}/{len(EVAL_CASES)}] category={case.category}") - print( - f" prompt : {case.prompt[:80]}{'…' if len(case.prompt) > 80 else ''}" - ) + print(f" prompt : {case.prompt[:80]}{'…' if len(case.prompt) > 80 else ''}") session_id = f"eval-{uuid.uuid4()}" # Round 1: single-turn real request — get the actual LLM response to judge. try: response, chosen = await _chat( - client, - proxy_url, - api_key, - router, + client, proxy_url, api_key, router, [{"role": "user", "content": case.prompt}], session_id=session_id, ) @@ -199,25 +194,16 @@ async def evaluate( continue print(f" model : {chosen or router}") - print( - f" response : {response[:120].replace(chr(10), ' ')}{'…' if len(response) > 120 else ''}" - ) + print(f" response : {response[:120].replace(chr(10), ' ')}{'…' if len(response) > 120 else ''}") # Judge the real response. judge_msgs = [ {"role": "system", "content": JUDGE_SYSTEM}, - { - "role": "user", - "content": _judge_user(case.prompt, case.ideal, response), - }, + {"role": "user", "content": _judge_user(case.prompt, case.ideal, response)}, ] try: verdict, _ = await _chat( - client, - proxy_url, - api_key, - judge_model, - judge_msgs, + client, proxy_url, api_key, judge_model, judge_msgs, ) except Exception as exc: # noqa: BLE001 print(f" ERROR calling judge: {exc}", file=sys.stderr) @@ -241,19 +227,15 @@ async def evaluate( # On PASS → satisfaction follow-up (+alpha). On FAIL → neutral (no signal). follow_up = SATISFY_FOLLOWUP if is_pass else NEUTRAL_FOLLOWUP bandit_msgs = [ - {"role": "user", "content": case.prompt}, + {"role": "user", "content": case.prompt}, {"role": "assistant", "content": response}, - {"role": "user", "content": "ok continue"}, + {"role": "user", "content": "ok continue"}, {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": follow_up}, + {"role": "user", "content": follow_up}, ] try: await _chat( - client, - proxy_url, - api_key, - router, - bandit_msgs, + client, proxy_url, api_key, router, bandit_msgs, session_id=session_id, ) except Exception as exc: # noqa: BLE001 @@ -275,17 +257,11 @@ async def evaluate( # Entry point # --------------------------------------------------------------------------- def main() -> None: - ap = argparse.ArgumentParser( - description="Evaluate the adaptive router with LLM-as-judge." - ) - ap.add_argument("--proxy-url", default="http://localhost:4000") - ap.add_argument("--api-key", required=True, help="proxy API key") - ap.add_argument( - "--router", default="smart-cheap-router", help="adaptive router model name" - ) - ap.add_argument( - "--judge-model", default="smart", help="model name for the judge (via proxy)" - ) + ap = argparse.ArgumentParser(description="Evaluate the adaptive router with LLM-as-judge.") + ap.add_argument("--proxy-url", default="http://localhost:4000") + ap.add_argument("--api-key", required=True, help="proxy API key") + ap.add_argument("--router", default="smart-cheap-router", help="adaptive router model name") + ap.add_argument("--judge-model", default="smart", help="model name for the judge (via proxy)") args = ap.parse_args() asyncio.run(evaluate(args.proxy_url, args.api_key, args.router, args.judge_model)) diff --git a/scripts/adaptive_router_demo/traffic.py b/scripts/adaptive_router_demo/traffic.py index dd64d95c90c..eae5506eaee 100644 --- a/scripts/adaptive_router_demo/traffic.py +++ b/scripts/adaptive_router_demo/traffic.py @@ -72,8 +72,8 @@ PROMPTS: Dict[str, List[str]] = { # so that signals attribute to the right (type, model) bandit cell. SATISFY: Dict[str, str] = { "code_generation": "thanks, that works! now write me a python function that does the inverse", - "factual_lookup": "perfect, thanks! who is the current prime minister?", - "writing": "great, thanks! now write a follow-up email confirming attendance", + "factual_lookup": "perfect, thanks! who is the current prime minister?", + "writing": "great, thanks! now write a follow-up email confirming attendance", } # Neutral follow-up — does not match any signal regex, does not move the bandit. @@ -83,8 +83,8 @@ NEUTRAL_FOLLOWUP = "ok, noted" # Defaults: smart dominates code/writing; both are fine for factual_lookup. ORACLE: Dict[str, Dict[str, float]] = { "code_generation": {"smart": 0.92, "fast": 0.35}, - "factual_lookup": {"smart": 0.90, "fast": 0.85}, - "writing": {"smart": 0.85, "fast": 0.55}, + "factual_lookup": {"smart": 0.90, "fast": 0.85}, + "writing": {"smart": 0.85, "fast": 0.55}, } # Fabricated assistant turn — content doesn't matter for the hook, only the role. @@ -94,11 +94,11 @@ FAB_ASSISTANT = "Got it. Working on that now." def _build_messages(prompt: str, last_user: str) -> List[Dict[str, str]]: """5-message conversation that passes the SIGNAL_GATE_MIN_MESSAGES=4 gate.""" return [ - {"role": "user", "content": prompt}, + {"role": "user", "content": prompt}, {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": "ok continue"}, + {"role": "user", "content": "ok continue"}, {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": last_user}, + {"role": "user", "content": last_user}, ] @@ -155,11 +155,7 @@ async def _drive_one_session( # # Round 1: neutral follow-up → no signal fires, but we learn the pick. ok, chosen = await _send( - client, - proxy_url, - api_key, - router, - session_id, + client, proxy_url, api_key, router, session_id, _build_messages(prompt, NEUTRAL_FOLLOWUP), mock_response=FAB_ASSISTANT, ) @@ -175,15 +171,10 @@ async def _drive_one_session( # follow-up matches satisfaction → +alpha for (request_type, chosen). history = _build_messages(prompt, NEUTRAL_FOLLOWUP) + [ {"role": "assistant", "content": FAB_ASSISTANT}, - {"role": "user", "content": follow_up}, + {"role": "user", "content": follow_up}, ] await _send( - client, - proxy_url, - api_key, - router, - session_id, - history, + client, proxy_url, api_key, router, session_id, history, mock_response=FAB_ASSISTANT, ) return chosen @@ -192,22 +183,13 @@ async def _drive_one_session( async def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--proxy-url", default="http://localhost:4000") - ap.add_argument( - "--api-key", required=True, help="proxy key with /v1/chat/completions perms" - ) - ap.add_argument("--router", default="smart-cheap-router") - ap.add_argument("--rounds", type=int, default=100) - ap.add_argument( - "--rate", - type=float, - default=0.5, - help="seconds between sessions; lower = faster", - ) - ap.add_argument( - "--types", - default="code_generation,factual_lookup,writing", - help="comma-separated subset of request types to drive", - ) + ap.add_argument("--api-key", required=True, help="proxy key with /v1/chat/completions perms") + ap.add_argument("--router", default="smart-cheap-router") + ap.add_argument("--rounds", type=int, default=100) + ap.add_argument("--rate", type=float, default=0.5, + help="seconds between sessions; lower = faster") + ap.add_argument("--types", default="code_generation,factual_lookup,writing", + help="comma-separated subset of request types to drive") args = ap.parse_args() types = [t.strip() for t in args.types.split(",") if t.strip() in PROMPTS] @@ -225,12 +207,7 @@ async def main() -> None: rt = random.choice(types) prompt = random.choice(PROMPTS[rt]) chosen = await _drive_one_session( - client, - args.proxy_url, - args.api_key, - args.router, - rt, - prompt, + client, args.proxy_url, args.api_key, args.router, rt, prompt, ) if chosen: counts[(rt, chosen)] = counts.get((rt, chosen), 0) + 1 diff --git a/scripts/benchmark_mock.py b/scripts/benchmark_mock.py index 6c3a6ad8944..55dbb1d4134 100644 --- a/scripts/benchmark_mock.py +++ b/scripts/benchmark_mock.py @@ -8,6 +8,7 @@ import statistics import aiohttp + REQUEST_BODY = { "model": "db-openai-endpoint", "messages": [{"role": "user", "content": "hi"}], diff --git a/scripts/benchmark_proxy_vs_provider.py b/scripts/benchmark_proxy_vs_provider.py index 8e1289e3c29..6196580b230 100755 --- a/scripts/benchmark_proxy_vs_provider.py +++ b/scripts/benchmark_proxy_vs_provider.py @@ -11,7 +11,7 @@ USAGE EXAMPLES: export PROVIDER_URL='https://api.openai.com/v1/chat/completions' export LITELLM_PROXY_API_KEY='sk-1234' export PROVIDER_API_KEY='sk-openai-key' - + # Run from scripts directory cd scripts python benchmark_proxy_vs_provider.py diff --git a/scripts/eval_compression.py b/scripts/eval_compression.py index 44265eb1518..a169cc02d74 100644 --- a/scripts/eval_compression.py +++ b/scripts/eval_compression.py @@ -42,7 +42,8 @@ from litellm.types.utils import CallTypes PROBLEMS = [ { "id": "has_close_elements", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def has_close_elements(numbers: List[float], threshold: float) -> bool: @@ -53,8 +54,10 @@ PROBLEMS = [ >>> has_close_elements([1.0, 2.8, 3.0, 4.0, 5.0, 2.0], 0.3) True \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert has_close_elements([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.3) == True assert has_close_elements([1.0, 2.0, 3.9, 4.0, 5.0, 2.2], 0.05) == False assert has_close_elements([1.0, 2.0, 5.9, 4.0, 5.0], 0.95) == True @@ -62,11 +65,13 @@ PROBLEMS = [ assert has_close_elements([1.0, 2.0, 3.0, 4.0, 5.0], 2.0) == True assert has_close_elements([], 0.5) == False print("PASSED") - """), + """ + ), }, { "id": "separate_paren_groups", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def separate_paren_groups(paren_string: str) -> List[str]: @@ -77,18 +82,22 @@ PROBLEMS = [ >>> separate_paren_groups('( ) (( )) (( )( ))') ['()', '(())', '(()())'] \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert separate_paren_groups('(()()) ((())) () ((())()())') == ['(()())', '((()))', '()', '((())()())'] assert separate_paren_groups('() (()) ((())) (((())))') == ['()', '(())', '((()))', '(((())))'] assert separate_paren_groups('(()(()))') == ['(()(()))'] assert separate_paren_groups('( ) (( )) (( )( ))') == ['()', '(())', '(()())'] print("PASSED") - """), + """ + ), }, { "id": "truncate_number", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ def truncate_number(number: float) -> float: \"\"\"Given a positive floating point number, it can be decomposed into an integer part (largest integer smaller than given number) and decimals @@ -97,17 +106,21 @@ PROBLEMS = [ >>> truncate_number(3.5) 0.5 \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert truncate_number(3.5) == 0.5 assert abs(truncate_number(1.33) - 0.33) < 1e-6 assert abs(truncate_number(123.456) - 0.456) < 1e-6 print("PASSED") - """), + """ + ), }, { "id": "below_zero", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def below_zero(operations: List[int]) -> bool: @@ -119,8 +132,10 @@ PROBLEMS = [ >>> below_zero([1, 2, -4, 5]) True \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert below_zero([]) == False assert below_zero([1, 2, -3, 1, 2, -3]) == False assert below_zero([1, 2, -4, 5, 6]) == True @@ -128,11 +143,13 @@ PROBLEMS = [ assert below_zero([1, -1, 2, -2, 5, -5, 4, -5]) == True assert below_zero([1, -2]) == True print("PASSED") - """), + """ + ), }, { "id": "mean_absolute_deviation", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def mean_absolute_deviation(numbers: List[float]) -> float: @@ -144,17 +161,21 @@ PROBLEMS = [ >>> mean_absolute_deviation([1.0, 2.0, 3.0, 4.0]) 1.0 \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert abs(mean_absolute_deviation([1.0, 2.0, 3.0, 4.0]) - 1.0) < 1e-6 assert abs(mean_absolute_deviation([1.0, 2.0, 3.0, 4.0, 5.0]) - 1.2) < 1e-6 assert abs(mean_absolute_deviation([1.0, 1.0, 1.0, 1.0]) - 0.0) < 1e-6 print("PASSED") - """), + """ + ), }, { "id": "intersperse", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def intersperse(numbers: List[int], delimiter: int) -> List[int]: @@ -164,17 +185,21 @@ PROBLEMS = [ >>> intersperse([1, 2, 3], 4) [1, 4, 2, 4, 3] \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert intersperse([], 7) == [] assert intersperse([5, 6, 3, 2], 8) == [5, 8, 6, 8, 3, 8, 2] assert intersperse([2, 2, 2], 2) == [2, 2, 2, 2, 2] print("PASSED") - """), + """ + ), }, { "id": "parse_nested_parens", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def parse_nested_parens(paren_string: str) -> List[int]: @@ -184,17 +209,21 @@ PROBLEMS = [ >>> parse_nested_parens('(()()) ((())) () ((())())') [2, 3, 1, 3] \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert parse_nested_parens('(()()) ((())) () ((())())') == [2, 3, 1, 3] assert parse_nested_parens('() (()) ((())) (((())))') == [1, 2, 3, 4] assert parse_nested_parens('(()(())((())))') == [4] print("PASSED") - """), + """ + ), }, { "id": "filter_by_substring", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def filter_by_substring(strings: List[str], substring: str) -> List[str]: @@ -204,18 +233,22 @@ PROBLEMS = [ >>> filter_by_substring(['abc', 'bacd', 'cde', 'array'], 'a') ['abc', 'bacd', 'array'] \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert filter_by_substring([], 'john') == [] assert filter_by_substring(['xxx', 'asd', 'xxy', 'john doe', 'xxxuj', 'xxx'], 'xxx') == ['xxx', 'xxxuj', 'xxx'] assert filter_by_substring(['xxx', 'asd', 'aaber', 'john doe', 'xxxuj', 'xxx'], 'xx') == ['xxx', 'xxxuj', 'xxx'] assert filter_by_substring(['grunt', 'hierarchial', 'abc', 'hierarchial'], 'hi') == ['hierarchial', 'hierarchial'] print("PASSED") - """), + """ + ), }, { "id": "sum_product", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List, Tuple def sum_product(numbers: List[int]) -> Tuple[int, int]: @@ -226,19 +259,23 @@ PROBLEMS = [ >>> sum_product([1, 2, 3, 4]) (10, 24) \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert sum_product([]) == (0, 1) assert sum_product([1, 1, 1]) == (3, 1) assert sum_product([100, 0]) == (100, 0) assert sum_product([3, 5, 7]) == (15, 105) assert sum_product([10]) == (10, 10) print("PASSED") - """), + """ + ), }, { "id": "max_element", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def max_element(l: List[int]) -> int: @@ -248,17 +285,21 @@ PROBLEMS = [ >>> max_element([5, 3, -5, 2, -3, 3, 9, 0, 123, 1, -10]) 123 \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert max_element([1, 2, 3]) == 3 assert max_element([5, 3, -5, 2, -3, 3, 9, 0, 124, 1, -10]) == 124 assert max_element([-1, -2, -3]) == -1 print("PASSED") - """), + """ + ), }, { "id": "fizz_buzz", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ def fizz_buzz(n: int) -> int: \"\"\"Return the number of times the digit 7 appears in integers less than n which are divisible by 11 or 13. >>> fizz_buzz(50) @@ -268,8 +309,10 @@ PROBLEMS = [ >>> fizz_buzz(79) 3 \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert fizz_buzz(50) == 0 assert fizz_buzz(78) == 2 assert fizz_buzz(79) == 3 @@ -277,11 +320,13 @@ PROBLEMS = [ assert fizz_buzz(200) == 6 assert fizz_buzz(4000) == 192 print("PASSED") - """), + """ + ), }, { "id": "sort_by_binary_len", - "prompt": textwrap.dedent("""\ + "prompt": textwrap.dedent( + """\ from typing import List def sort_array(arr: List[int]) -> List[int]: @@ -294,8 +339,10 @@ PROBLEMS = [ >>> sort_array([1, 0, 2, 3, 4]) [0, 1, 2, 4, 3] \"\"\" - """), - "tests": textwrap.dedent("""\ + """ + ), + "tests": textwrap.dedent( + """\ assert sort_array([1, 5, 2, 3, 4]) == [1, 2, 4, 3, 5] assert sort_array([-2, -3, -4, -5, -6]) == [-6, -5, -4, -3, -2] assert sort_array([1, 0, 2, 3, 4]) == [0, 1, 2, 4, 3] @@ -303,7 +350,8 @@ PROBLEMS = [ assert sort_array([2, 5, 77, 4, 5, 3, 5, 7, 2, 3, 4]) == [2, 2, 4, 4, 3, 3, 5, 5, 5, 7, 77] assert sort_array([3, 6, 44, 12, 32, 5]) == [32, 3, 5, 6, 12, 44] print("PASSED") - """), + """ + ), }, ] @@ -312,7 +360,8 @@ PROBLEMS = [ # compressor to identify and drop them. DISTRACTOR_SNIPPETS = [ # distractor 0 — database connection pool - textwrap.dedent("""\ + textwrap.dedent( + """\ # db_pool.py import threading from contextlib import contextmanager @@ -359,9 +408,11 @@ DISTRACTOR_SNIPPETS = [ for conn in self._pool: conn.close() self._pool.clear() - """), + """ + ), # distractor 1 — HTTP retry logic - textwrap.dedent("""\ + textwrap.dedent( + """\ # http_retry.py import time import random @@ -405,9 +456,11 @@ DISTRACTOR_SNIPPETS = [ resp = requests.get(url, params=params, timeout=30) resp.raise_for_status() return resp.json() - """), + """ + ), # distractor 2 — LRU cache implementation - textwrap.dedent("""\ + textwrap.dedent( + """\ # lru_cache.py from collections import OrderedDict from threading import RLock @@ -456,9 +509,11 @@ DISTRACTOR_SNIPPETS = [ def __contains__(self, key): return key in self._cache - """), + """ + ), # distractor 3 — CSV report generator - textwrap.dedent("""\ + textwrap.dedent( + """\ # report_gen.py import csv import io @@ -511,9 +566,11 @@ DISTRACTOR_SNIPPETS = [ except (ValueError, KeyError): return False return self.filter_rows(in_range) - """), + """ + ), # distractor 4 — async task queue - textwrap.dedent("""\ + textwrap.dedent( + """\ # task_queue.py import asyncio import logging @@ -583,9 +640,11 @@ DISTRACTOR_SNIPPETS = [ async def shutdown(self): for w in self._workers: w.cancel() - """), + """ + ), # distractor 5 — config parser with env var interpolation - textwrap.dedent("""\ + textwrap.dedent( + """\ # config_parser.py import os import re @@ -648,7 +707,8 @@ DISTRACTOR_SNIPPETS = [ if val is None: raise ConfigError(f"Required config key missing: {key}") return val - """), + """ + ), ] diff --git a/scripts/health_check/benchmark_get_all_latest_health_checks.py b/scripts/health_check/benchmark_get_all_latest_health_checks.py index 5d0957d4f7d..45845554c86 100644 --- a/scripts/health_check/benchmark_get_all_latest_health_checks.py +++ b/scripts/health_check/benchmark_get_all_latest_health_checks.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Bench LiteLLM_HealthCheckTable + PrismaClient +Bench LiteLLM_HealthCheckTable + PrismaClient - set DATABASE_URL to your Postgres - Run ```prisma generate``` to install prisma client before running test ) - This test writes to the default "public" database. Make sure to run cleanup after testing diff --git a/tests/batches_tests/test_batch_custom_pricing.py b/tests/batches_tests/test_batch_custom_pricing.py index 911fd8e3113..f4e84b46bee 100644 --- a/tests/batches_tests/test_batch_custom_pricing.py +++ b/tests/batches_tests/test_batch_custom_pricing.py @@ -18,6 +18,7 @@ from litellm.batches.batch_utils import ( from litellm.cost_calculator import batch_cost_calculator from litellm.types.utils import Usage + # --- helpers --- diff --git a/tests/batches_tests/test_hosted_vllm_batches_and_files.py b/tests/batches_tests/test_hosted_vllm_batches_and_files.py index b7f21f6cab2..c7a25c71c53 100644 --- a/tests/batches_tests/test_hosted_vllm_batches_and_files.py +++ b/tests/batches_tests/test_hosted_vllm_batches_and_files.py @@ -20,6 +20,7 @@ sys.path.insert(0, os.path.abspath("../..")) import litellm + SERVER_URL = "https://exampleopenaiendpoint-production-0ee2.up.railway.app/v1" diff --git a/tests/benchmarks/test_benchmarks.py b/tests/benchmarks/test_benchmarks.py index d5ade58980a..123dad93e11 100644 --- a/tests/benchmarks/test_benchmarks.py +++ b/tests/benchmarks/test_benchmarks.py @@ -12,6 +12,7 @@ import litellm from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider from litellm.litellm_core_utils.token_counter import token_counter + # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- diff --git a/tests/code_coverage_tests/check_fastuuid_usage.py b/tests/code_coverage_tests/check_fastuuid_usage.py index bb3176b57a3..e0433454371 100644 --- a/tests/code_coverage_tests/check_fastuuid_usage.py +++ b/tests/code_coverage_tests/check_fastuuid_usage.py @@ -2,6 +2,7 @@ import ast import os from typing import List, Dict, Any + ALLOWED_FILE = os.path.normpath("litellm/_uuid.py") diff --git a/tests/enterprise/conftest.py b/tests/enterprise/conftest.py index 524ab85b938..0365bbbcfa0 100644 --- a/tests/enterprise/conftest.py +++ b/tests/enterprise/conftest.py @@ -23,6 +23,8 @@ def event_loop(): loop.close() + + @pytest.fixture(scope="function", autouse=True) def setup_and_teardown(): """ diff --git a/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py b/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py index 6bdc808c4f3..212c5d4a322 100644 --- a/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py +++ b/tests/enterprise/litellm_enterprise/integrations/test_prometheus.py @@ -944,8 +944,8 @@ def test_callback_failure_metric_different_callbacks(prometheus_logger): async def test_langfuse_callback_failure_metric(prometheus_logger): """ Test that Langfuse callback failures are properly tracked in Prometheus metrics. - - This test verifies that when Langfuse logging fails, the + + This test verifies that when Langfuse logging fails, the litellm_callback_logging_failures_metric is incremented with callback_name="langfuse". """ from unittest.mock import MagicMock, patch @@ -957,20 +957,16 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): # Get initial value initial_value = 0 try: - initial_value = ( - prometheus_logger.litellm_callback_logging_failures_metric.labels( - callback_name="langfuse" - )._value.get() - ) + initial_value = prometheus_logger.litellm_callback_logging_failures_metric.labels( + callback_name="langfuse" + )._value.get() except Exception: initial_value = 0 - + # Create Langfuse logger with mocked initialization - with patch( - "litellm.integrations.langfuse.langfuse_prompt_management.langfuse_client_init" - ): + with patch("litellm.integrations.langfuse.langfuse_prompt_management.langfuse_client_init"): langfuse_logger = LangfusePromptManagement() - + # Mock the log_event_on_langfuse to raise an exception with patch( "litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler.get_langfuse_logger_for_request" @@ -978,16 +974,14 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): mock_logger = MagicMock() mock_logger.log_event_on_langfuse.side_effect = Exception("Langfuse API error") mock_get_logger.return_value = mock_logger - + # Mock handle_callback_failure to track calls - with patch.object( - prometheus_logger, "increment_callback_logging_failure" - ) as mock_increment: + with patch.object(prometheus_logger, "increment_callback_logging_failure") as mock_increment: # Inject prometheus logger into the langfuse logger - langfuse_logger.handle_callback_failure = ( - lambda callback_name: mock_increment(callback_name=callback_name) + langfuse_logger.handle_callback_failure = lambda callback_name: mock_increment( + callback_name=callback_name ) - + # Call async_log_success_event - should catch exception and increment metric await langfuse_logger.async_log_success_event( kwargs={}, @@ -995,10 +989,10 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): start_time=None, end_time=None, ) - + # Verify that increment was called with correct callback name mock_increment.assert_called_once_with(callback_name="langfuse") - + print("✓ Langfuse callback failure metric test passed") @@ -1006,8 +1000,8 @@ async def test_langfuse_callback_failure_metric(prometheus_logger): async def test_langfuse_otel_callback_failure_metric(prometheus_logger): """ Test that Langfuse OTEL callback failures are properly tracked in Prometheus metrics. - - This test verifies that when Langfuse OTEL logging fails, the + + This test verifies that when Langfuse OTEL logging fails, the litellm_callback_logging_failures_metric is incremented with callback_name="langfuse_otel". """ from unittest.mock import MagicMock, patch @@ -1017,58 +1011,50 @@ async def test_langfuse_otel_callback_failure_metric(prometheus_logger): # Get initial value initial_value = 0 try: - initial_value = ( - prometheus_logger.litellm_callback_logging_failures_metric.labels( - callback_name="langfuse_otel" - )._value.get() - ) + initial_value = prometheus_logger.litellm_callback_logging_failures_metric.labels( + callback_name="langfuse_otel" + )._value.get() except Exception: initial_value = 0 - + # Create Langfuse OTEL logger with mocked initialization - with patch( - "litellm.integrations.opentelemetry.OpenTelemetry.__init__", return_value=None - ): + with patch("litellm.integrations.opentelemetry.OpenTelemetry.__init__", return_value=None): langfuse_otel_logger = LangfuseOtelLogger(callback_name="langfuse_otel") langfuse_otel_logger.callback_name = "langfuse_otel" - + # Mock handle_callback_failure to track calls - with patch.object( - prometheus_logger, "increment_callback_logging_failure" - ) as mock_increment: + with patch.object(prometheus_logger, "increment_callback_logging_failure") as mock_increment: # Inject prometheus logger into the langfuse otel logger - langfuse_otel_logger.handle_callback_failure = ( - lambda callback_name: mock_increment(callback_name=callback_name) + langfuse_otel_logger.handle_callback_failure = lambda callback_name: mock_increment( + callback_name=callback_name ) - + # Test that the OpenTelemetry base class set_attributes exception handler works # This is where langfuse_otel failures are caught and tracked - with patch.object( - langfuse_otel_logger, "set_attributes" - ) as mock_set_attributes: + with patch.object(langfuse_otel_logger, "set_attributes") as mock_set_attributes: # Simulate the exception handling in set_attributes def set_attributes_with_error(*args, **kwargs): # This simulates what happens in the real set_attributes method try: raise Exception("Attribute error") except Exception as e: - langfuse_otel_logger.handle_callback_failure( - callback_name=langfuse_otel_logger.callback_name - ) - + langfuse_otel_logger.handle_callback_failure(callback_name=langfuse_otel_logger.callback_name) + mock_set_attributes.side_effect = set_attributes_with_error - + # Call set_attributes try: langfuse_otel_logger.set_attributes( - span=MagicMock(), kwargs={}, response_obj={} + span=MagicMock(), + kwargs={}, + response_obj={} ) except Exception: pass - + # Verify that increment was called with correct callback name mock_increment.assert_called_with(callback_name="langfuse_otel") - + print("✓ Langfuse OTEL callback failure metric test passed") diff --git a/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py b/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py index ab5c5576625..a45df5df008 100644 --- a/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py +++ b/tests/enterprise/litellm_enterprise/proxy/auth/test_user_api_key_auth.py @@ -49,13 +49,10 @@ async def test_enterprise_custom_auth_returns_string(): mock_user_auth = AsyncMock(return_value="sk-test-key") request = MagicMock(spec=Request) - with ( - patch( - "litellm.proxy.auth.user_api_key_auth.enterprise_custom_auth", - mock_user_auth, - ), - patch("litellm.proxy.proxy_server.master_key", "sk-1234"), - patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), + with patch( + "litellm.proxy.auth.user_api_key_auth.enterprise_custom_auth", mock_user_auth + ), patch("litellm.proxy.proxy_server.master_key", "sk-1234"), patch( + "litellm.proxy.proxy_server.prisma_client", MagicMock() ): # Verify the key is correctly handled in _user_api_key_auth_builder with patch( diff --git a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py index b7fd665c410..0d27df50d15 100644 --- a/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py +++ b/tests/enterprise/litellm_enterprise/proxy/guardrails/test_apply_guardrail_endpoint.py @@ -105,9 +105,7 @@ async def test_apply_guardrail_endpoint_with_presidio_guardrail(): mock_guardrail = Mock(spec=CustomGuardrail) # Simulate masking PII entities - returns GenericGuardrailAPIInputs (dict with texts key) mock_guardrail.apply_guardrail = AsyncMock( - return_value={ - "texts": ["My name is [PERSON] and my email is [EMAIL_ADDRESS]"] - } + return_value={"texts": ["My name is [PERSON] and my email is [EMAIL_ADDRESS]"]} ) # Configure the registry to return our mock guardrail diff --git a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py index 3f9c2486d9c..9f4ca4ed108 100644 --- a/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py +++ b/tests/enterprise/litellm_enterprise/proxy/hooks/test_managed_files.py @@ -95,9 +95,9 @@ async def test_async_pre_call_deployment_hook_resolves_model_id_from_litellm_met kwargs=kwargs, call_type=CallTypes.acreate_batch ) - assert ( - result["input_file_id"] == provider_file_id - ), f"Expected provider file ID '{provider_file_id}', got '{result['input_file_id']}'" + assert result["input_file_id"] == provider_file_id, ( + f"Expected provider file ID '{provider_file_id}', got '{result['input_file_id']}'" + ) @pytest.mark.asyncio @@ -134,9 +134,9 @@ async def test_async_pre_call_deployment_hook_prefers_top_level_model_info(): kwargs=kwargs, call_type=CallTypes.acreate_batch ) - assert ( - result["input_file_id"] == top_level_provider_file - ), "Should prefer top-level model_info over litellm_metadata" + assert result["input_file_id"] == top_level_provider_file, ( + "Should prefer top-level model_info over litellm_metadata" + ) @pytest.mark.asyncio @@ -162,9 +162,9 @@ async def test_async_pre_call_deployment_hook_no_model_info_leaves_file_id_uncha kwargs=kwargs, call_type=CallTypes.acreate_batch ) - assert ( - result["input_file_id"] == managed_file_id - ), "File ID should remain unchanged when model_info is not available" + assert result["input_file_id"] == managed_file_id, ( + "File ID should remain unchanged when model_info is not available" + ) # def test_list_managed_files(): @@ -341,9 +341,7 @@ async def test_async_pre_call_hook_for_unified_finetuning_job(): @pytest.mark.asyncio -@pytest.mark.parametrize( - "call_type", ["afile_content", "afile_delete", "afile_retrieve"] -) +@pytest.mark.parametrize("call_type", ["afile_content", "afile_delete", "afile_retrieve"]) async def test_can_user_call_unified_file_id(call_type): """ Test that on file retrieve, delete, and content we check if the user has access to the file @@ -603,7 +601,7 @@ async def test_error_file_id_for_failed_batch(): "litellm_model_name": "gpt-4o", "unified_batch_id": "litellm_proxy;model_id:test-model-id;llm_batch_id:batch_abc123", } - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=AsyncMock() ) @@ -622,11 +620,12 @@ async def test_error_file_id_for_failed_batch(): # Mock the afile_retrieve to simulate retrieving error file metadata with patch("litellm.afile_retrieve", new_callable=AsyncMock) as mock_retrieve: mock_retrieve.return_value = error_file_object - + user_api_key_dict = UserAPIKeyAuth( - user_id="test-user-123", parent_otel_span=MagicMock() + user_id="test-user-123", + parent_otel_span=MagicMock() ) - + response = await proxy_managed_files.async_post_call_success_hook( data={}, user_api_key_dict=user_api_key_dict, @@ -637,9 +636,7 @@ async def test_error_file_id_for_failed_batch(): assert cast(LiteLLMBatch, response).error_file_id is not None assert not cast(LiteLLMBatch, response).error_file_id.startswith("error-") # Verify it's a base64 encoded managed file ID - assert _is_base64_encoded_unified_file_id( - cast(LiteLLMBatch, response).error_file_id - ) + assert _is_base64_encoded_unified_file_id(cast(LiteLLMBatch, response).error_file_id) @pytest.mark.asyncio @@ -653,7 +650,7 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation(): # Use AsyncMock instead of real database connection prisma_client = AsyncMock() - + batch = LiteLLMBatch( id="bGl0ZWxsbV9wcm94eTttb2RlbF9pZDoxMjM0NTY3OTtsbG1fYmF0Y2hfaWQ6YmF0Y2hfNjg1YzVlNWQ2Mzk4ODE5MGI4NWJkYjIxNDdiYTEzMWQ", completion_window="24h", @@ -681,10 +678,8 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation(): # first retrieve batch tasks = [] first_create_task = asyncio.create_task - with patch("asyncio.create_task") as mock_create_task: - mock_create_task.side_effect = ( - lambda coro: tasks.append(first_create_task(coro)) or tasks[-1] - ) + with patch('asyncio.create_task') as mock_create_task: + mock_create_task.side_effect = lambda coro: tasks.append(first_create_task(coro)) or tasks[-1] response = await proxy_managed_files.async_post_call_success_hook( data={}, @@ -705,10 +700,8 @@ async def test_async_post_call_success_hook_twice_assert_no_unique_violation(): # second retrieve batch tasks = [] second_create_task = asyncio.create_task - with patch("asyncio.create_task") as mock_create_task: - mock_create_task.side_effect = ( - lambda coro: tasks.append(second_create_task(coro)) or tasks[-1] - ) + with patch('asyncio.create_task') as mock_create_task: + mock_create_task.side_effect = lambda coro: tasks.append(second_create_task(coro)) or tasks[-1] await proxy_managed_files.async_post_call_success_hook( data={}, @@ -735,7 +728,7 @@ def test_update_responses_input_with_unified_file_id(): # Create a base64-encoded unified file ID # This decodes to: litellm_proxy:application/pdf;unified_id,6c0b5890-8914-48e0-b8f4-0ae5ed3c14a5;target_model_names,gpt-4o;llm_output_file_id,file-ECBPW7ML9g7XHdwGgUPZaM;llm_output_file_model_id,e26453f9e76e7993680d0068d98c1f4cc205bbad0967a33c664893568ca743c2 unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" - + # Test input with unified file ID in content array input_data = [ { @@ -752,18 +745,15 @@ def test_update_responses_input_with_unified_file_id(): ], } ] - + # Update the input updated_input = update_responses_input_with_model_file_ids(input=input_data) - + # Verify the file_id was updated to the provider-specific file ID assert updated_input[0]["content"][0]["type"] == "input_file" assert updated_input[0]["content"][0]["file_id"] == "file-ECBPW7ML9g7XHdwGgUPZaM" assert updated_input[0]["content"][1]["type"] == "input_text" - assert ( - updated_input[0]["content"][1]["text"] - == "What is the first dragon in the book?" - ) + assert updated_input[0]["content"][1]["text"] == "What is the first dragon in the book?" def test_update_responses_input_with_regular_file_id(): @@ -777,7 +767,7 @@ def test_update_responses_input_with_regular_file_id(): # Regular OpenAI file ID (not a unified file ID) regular_file_id = "file-abc123xyz" - + input_data = [ { "role": "user", @@ -793,10 +783,10 @@ def test_update_responses_input_with_regular_file_id(): ], } ] - + # Update the input updated_input = update_responses_input_with_model_file_ids(input=input_data) - + # Verify the file_id was kept unchanged (regular OpenAI file ID) assert updated_input[0]["content"][0]["type"] == "input_file" assert updated_input[0]["content"][0]["file_id"] == regular_file_id @@ -810,11 +800,11 @@ def test_update_responses_input_with_string_input(): from litellm.litellm_core_utils.prompt_templates.common_utils import ( update_responses_input_with_model_file_ids, ) - + input_data = "What is AI?" - + updated_input = update_responses_input_with_model_file_ids(input=input_data) - + assert updated_input == input_data assert isinstance(updated_input, str) @@ -832,7 +822,7 @@ def test_update_responses_input_with_multiple_file_ids(): unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" # Regular OpenAI file ID regular_file_id = "file-regular123" - + input_data = [ { "role": "user", @@ -852,9 +842,9 @@ def test_update_responses_input_with_multiple_file_ids(): ], } ] - + updated_input = update_responses_input_with_model_file_ids(input=input_data) - + # Verify unified file ID was updated assert updated_input[0]["content"][0]["file_id"] == "file-ECBPW7ML9g7XHdwGgUPZaM" # Verify regular file ID was kept unchanged @@ -874,7 +864,7 @@ def test_update_responses_input_with_model_file_id_mapping(): # Managed file ID (unified) managed_file_id = "litellm_proxy_file_123" - + # Model file ID mapping model_file_id_mapping = { managed_file_id: { @@ -882,7 +872,7 @@ def test_update_responses_input_with_model_file_id_mapping(): "model_id_2": "azure_file_xyz", } } - + input_data = [ { "role": "user", @@ -898,24 +888,24 @@ def test_update_responses_input_with_model_file_id_mapping(): ], } ] - + # Update input with model_id_1 mapping updated_input = update_responses_input_with_model_file_ids( input=input_data, model_id="model_id_1", model_file_id_mapping=model_file_id_mapping, ) - + # Verify the file_id was mapped to the correct provider-specific file ID assert updated_input[0]["content"][0]["file_id"] == "openai_file_abc" - + # Test with different model_id updated_input_2 = update_responses_input_with_model_file_ids( input=input_data, model_id="model_id_2", model_file_id_mapping=model_file_id_mapping, ) - + assert updated_input_2[0]["content"][0]["file_id"] == "azure_file_xyz" @@ -923,7 +913,7 @@ def test_update_responses_tools_with_model_file_id_mapping(): """ Test that update_responses_tools_with_model_file_ids correctly maps file IDs in code_interpreter tools with container.file_ids. - + This is a regression test for the issue where managed file IDs in tools.container.file_ids were not being replaced with provider-specific file IDs, causing "string too long" errors from OpenAI. @@ -935,7 +925,7 @@ def test_update_responses_tools_with_model_file_id_mapping(): # Managed file IDs managed_file_id_1 = "litellm_proxy_file_123" managed_file_id_2 = "litellm_proxy_file_456" - + # Model file ID mapping model_file_id_mapping = { managed_file_id_1: { @@ -945,7 +935,7 @@ def test_update_responses_tools_with_model_file_id_mapping(): "model_id_1": "openai_file_def", }, } - + tools = [ { "type": "code_interpreter", @@ -955,20 +945,17 @@ def test_update_responses_tools_with_model_file_id_mapping(): }, } ] - + # Update tools with model mapping updated_tools = update_responses_tools_with_model_file_ids( tools=tools, model_id="model_id_1", model_file_id_mapping=model_file_id_mapping, ) - + # Verify the file IDs were mapped to provider-specific file IDs assert updated_tools[0]["type"] == "code_interpreter" - assert updated_tools[0]["container"]["file_ids"] == [ - "openai_file_abc", - "openai_file_def", - ] + assert updated_tools[0]["container"]["file_ids"] == ["openai_file_abc", "openai_file_def"] def test_update_responses_tools_without_mapping(): @@ -981,7 +968,7 @@ def test_update_responses_tools_without_mapping(): ) regular_file_id = "file-abc123" - + tools = [ { "type": "code_interpreter", @@ -991,14 +978,14 @@ def test_update_responses_tools_without_mapping(): }, } ] - + # Update tools without mapping updated_tools = update_responses_tools_with_model_file_ids( tools=tools, model_id=None, model_file_id_mapping=None, ) - + # Verify the file ID was kept unchanged assert updated_tools[0]["container"]["file_ids"] == [regular_file_id] @@ -1014,13 +1001,13 @@ def test_update_responses_tools_with_mixed_file_ids(): managed_file_id = "litellm_proxy_file_123" regular_file_id = "file-abc123" - + model_file_id_mapping = { managed_file_id: { "model_id_1": "openai_file_abc", }, } - + tools = [ { "type": "code_interpreter", @@ -1030,19 +1017,16 @@ def test_update_responses_tools_with_mixed_file_ids(): }, } ] - + # Update tools updated_tools = update_responses_tools_with_model_file_ids( tools=tools, model_id="model_id_1", model_file_id_mapping=model_file_id_mapping, ) - + # Verify managed file ID was mapped and regular file ID was kept - assert updated_tools[0]["container"]["file_ids"] == [ - "openai_file_abc", - regular_file_id, - ] + assert updated_tools[0]["container"]["file_ids"] == ["openai_file_abc", regular_file_id] def test_get_file_ids_from_responses_tools(): @@ -1053,7 +1037,7 @@ def test_get_file_ids_from_responses_tools(): proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=MagicMock() ) - + tools = [ { "type": "code_interpreter", @@ -1063,9 +1047,9 @@ def test_get_file_ids_from_responses_tools(): }, } ] - + file_ids = proxy_managed_files.get_file_ids_from_responses_tools(tools) - + assert file_ids == ["file-123", "file-456"] @@ -1076,7 +1060,7 @@ def test_get_file_ids_from_responses_tools_multiple_tools(): proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=MagicMock() ) - + tools = [ { "type": "code_interpreter", @@ -1096,9 +1080,9 @@ def test_get_file_ids_from_responses_tools_multiple_tools(): }, }, ] - + file_ids = proxy_managed_files.get_file_ids_from_responses_tools(tools) - + # Should extract file IDs only from code_interpreter tools assert file_ids == ["file-123", "file-456", "file-789"] @@ -1110,15 +1094,15 @@ def test_get_file_ids_from_responses_tools_empty(): proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=MagicMock() ) - + # Test with None file_ids = proxy_managed_files.get_file_ids_from_responses_tools(None) assert file_ids == [] - + # Test with empty list file_ids = proxy_managed_files.get_file_ids_from_responses_tools([]) assert file_ids == [] - + # Test with tools without file_ids tools = [{"type": "file_search"}] file_ids = proxy_managed_files.get_file_ids_from_responses_tools(tools) @@ -1135,30 +1119,30 @@ async def test_check_file_ids_access_with_unified_file_ids(): # Create a unified file ID unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" regular_file_id = "file-abc123" - + # Mock the access check to return True prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock can_user_call_unified_file_id to return True proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=True) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + # Should not raise an exception for accessible files await proxy_managed_files.check_file_ids_access( [unified_file_id, regular_file_id], user_api_key_dict, ) - + # Verify can_user_call_unified_file_id was called for the unified file ID proxy_managed_files.can_user_call_unified_file_id.assert_called_once_with( unified_file_id, user_api_key_dict @@ -1171,32 +1155,32 @@ async def test_check_file_ids_access_denied(): Test that check_file_ids_access raises HTTPException when user doesn't have access. """ from litellm.proxy._types import UserAPIKeyAuth - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock can_user_call_unified_file_id to return False (access denied) proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=False) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + # Should raise HTTPException with 403 status code with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.check_file_ids_access( [unified_file_id], user_api_key_dict, ) - + assert exc_info.value.status_code == 403 assert "does not have access to the file" in exc_info.value.detail @@ -1207,32 +1191,32 @@ async def test_check_file_ids_access_with_regular_files_only(): Test that check_file_ids_access doesn't check access for regular (non-unified) file IDs. """ from litellm.proxy._types import UserAPIKeyAuth - + regular_file_id_1 = "file-abc123" regular_file_id_2 = "file-xyz789" - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock can_user_call_unified_file_id (should not be called for regular files) proxy_managed_files.can_user_call_unified_file_id = AsyncMock() - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + # Should not raise exception and should not call can_user_call_unified_file_id await proxy_managed_files.check_file_ids_access( [regular_file_id_1, regular_file_id_2], user_api_key_dict, ) - + # Verify can_user_call_unified_file_id was NOT called proxy_managed_files.can_user_call_unified_file_id.assert_not_called() @@ -1243,31 +1227,31 @@ async def test_completion_with_file_access_check(): Test that completion call type checks file access before processing. """ from litellm.proxy._types import UserAPIKeyAuth - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" - + prisma_client = AsyncMock() prisma_client.db.litellm_managedfiletable.find_first = AsyncMock(return_value=None) - + internal_usage_cache = MagicMock() internal_usage_cache.async_get_cache = AsyncMock(return_value=None) - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock the get_model_file_id_mapping to return empty dict proxy_managed_files.get_model_file_id_mapping = AsyncMock(return_value={}) - + # Mock access check to allow access proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=True) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + data = { "messages": [ { @@ -1283,7 +1267,7 @@ async def test_completion_with_file_access_check(): ], "model": "gpt-4", } - + # Should not raise exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=user_api_key_dict, @@ -1291,7 +1275,7 @@ async def test_completion_with_file_access_check(): data=data, call_type="acompletion", ) - + # Verify access check was called proxy_managed_files.can_user_call_unified_file_id.assert_called_once() @@ -1302,32 +1286,32 @@ async def test_responses_with_file_access_check(): Test that responses API checks file access for files in both input and tools. """ from litellm.proxy._types import UserAPIKeyAuth - + unified_file_id_1 = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9wZGY7dW5pZmllZF9pZCw2YzBiNTg5MC04OTE0LTQ4ZTAtYjhmNC0wYWU1ZWQzYzE0YTU7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1FQ0JQVzdNTDlnN1hIZHdHZ1VQWmFNO2xsbV9vdXRwdXRfZmlsZV9tb2RlbF9pZCxlMjY0NTNmOWU3NmU3OTkzNjgwZDAwNjhkOThjMWY0Y2MyMDViYmFkMDk2N2EzM2M2NjQ4OTM1NjhjYTc0M2My" unified_file_id_2 = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsNzc3Nzc3Nzc7dGFyZ2V0X21vZGVsX25hbWVzLGdwdC00bztsbG1fb3V0cHV0X2ZpbGVfaWQsZmlsZS1YWVo7bGxtX291dHB1dF9maWxlX21vZGVsX2lkLG1vZGVsXzEyMw" - + prisma_client = AsyncMock() prisma_client.db.litellm_managedfiletable.find_first = AsyncMock(return_value=None) - + internal_usage_cache = MagicMock() internal_usage_cache.async_get_cache = AsyncMock(return_value=None) - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock the get_model_file_id_mapping to return empty dict proxy_managed_files.get_model_file_id_mapping = AsyncMock(return_value={}) - + # Mock access check to allow access proxy_managed_files.can_user_call_unified_file_id = AsyncMock(return_value=True) - + user_api_key_dict = UserAPIKeyAuth( user_id="test_user_123", parent_otel_span=MagicMock(), ) - + data = { "input": [ { @@ -1349,7 +1333,7 @@ async def test_responses_with_file_access_check(): ], "model": "gpt-4", } - + # Should not raise exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=user_api_key_dict, @@ -1357,7 +1341,7 @@ async def test_responses_with_file_access_check(): data=data, call_type="aresponses", ) - + # Verify access check was called for both file IDs assert proxy_managed_files.can_user_call_unified_file_id.call_count == 2 @@ -1369,19 +1353,17 @@ async def test_store_unified_file_id_with_none_file_object(): (e.g., for batch output files that are stored before file metadata is available). """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - prisma_client.db.litellm_managedfiletable.create = AsyncMock( - return_value=MagicMock() - ) + prisma_client.db.litellm_managedfiletable.create = AsyncMock(return_value=MagicMock()) internal_usage_cache = MagicMock() internal_usage_cache.async_set_cache = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Store with file_object=None (simulating batch output file storage) await proxy_managed_files.store_unified_file_id( file_id="test-unified-file-id", @@ -1390,7 +1372,7 @@ async def test_store_unified_file_id_with_none_file_object(): model_mappings={"model-123": "file-provider-xyz"}, user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), ) - + # Verify DB create was called with expected data (without file_object) prisma_client.db.litellm_managedfiletable.create.assert_called_once() call_args = prisma_client.db.litellm_managedfiletable.create.call_args @@ -1405,38 +1387,34 @@ async def test_afile_delete_returns_provider_response_when_stored_file_object_no stored file_object is None (e.g., for batch output files). """ from litellm.types.llms.openai import OpenAIFileObject - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsdGVzdC1pZDt0YXJnZXRfbW9kZWxfbmFtZXMsZ3B0LTRvO2xsbV9vdXRwdXRfZmlsZV9pZCxmaWxlLXByb3ZpZGVyLXh5ejtsbG1fb3V0cHV0X2ZpbGVfbW9kZWxfaWQsbW9kZWwtMTIz" - + prisma_client = AsyncMock() db_record = MagicMock() db_record.model_mappings = '{"model-123": "file-provider-xyz"}' - prisma_client.db.litellm_managedfiletable.find_first = AsyncMock( - return_value=db_record - ) + prisma_client.db.litellm_managedfiletable.find_first = AsyncMock(return_value=db_record) prisma_client.db.litellm_managedfiletable.delete = AsyncMock() - + internal_usage_cache = MagicMock() - internal_usage_cache.async_get_cache = AsyncMock( - return_value={ - "unified_file_id": unified_file_id, - "model_mappings": {"model-123": "file-provider-xyz"}, - "flat_model_file_ids": ["file-provider-xyz"], - "file_object": None, - "created_by": "test-user", - "updated_by": "test-user", - } - ) + internal_usage_cache.async_get_cache = AsyncMock(return_value={ + "unified_file_id": unified_file_id, + "model_mappings": {"model-123": "file-provider-xyz"}, + "flat_model_file_ids": ["file-provider-xyz"], + "file_object": None, + "created_by": "test-user", + "updated_by": "test-user", + }) internal_usage_cache.async_set_cache = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock the delete_unified_file_id to return None (simulating file_object=None) proxy_managed_files.delete_unified_file_id = AsyncMock(return_value=None) - + # Mock router response provider_delete_response = OpenAIFileObject( id="file-provider-xyz", @@ -1446,16 +1424,16 @@ async def test_afile_delete_returns_provider_response_when_stored_file_object_no filename="test.jsonl", purpose="batch", ) - + mock_router = MagicMock() mock_router.afile_delete = AsyncMock(return_value=provider_delete_response) - + result = await proxy_managed_files.afile_delete( file_id=unified_file_id, litellm_parent_otel_span=None, llm_router=mock_router, ) - + # Should return the provider response with the unified file ID assert result is not None assert result.id == unified_file_id @@ -1468,21 +1446,21 @@ async def test_afile_retrieve_fetches_from_provider_when_file_object_none(): file_object is None (e.g., for batch output files). """ from litellm.types.llms.openai import OpenAIFileObject - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return a stored object with file_object=None stored_file = MagicMock() stored_file.file_object = None stored_file.model_mappings = {"model-123": "file-provider-xyz"} proxy_managed_files.get_unified_file_id = AsyncMock(return_value=stored_file) - + # Mock the router and provider response provider_file_response = OpenAIFileObject( id="file-provider-xyz", @@ -1492,25 +1470,23 @@ async def test_afile_retrieve_fetches_from_provider_when_file_object_none(): filename="output.jsonl", purpose="batch_output", ) - + mock_router = MagicMock() - mock_router.get_deployment_credentials_with_provider = MagicMock( - return_value={ - "api_key": "test-key", - "api_base": "https://api.openai.com", - } - ) - + mock_router.get_deployment_credentials_with_provider = MagicMock(return_value={ + "api_key": "test-key", + "api_base": "https://api.openai.com", + }) + with patch("litellm.afile_retrieve", new_callable=AsyncMock) as mock_afile_retrieve: mock_afile_retrieve.return_value = provider_file_response - + unified_file_id = "test-unified-file-id" result = await proxy_managed_files.afile_retrieve( file_id=unified_file_id, litellm_parent_otel_span=None, llm_router=mock_router, ) - + # Should return the provider response with the unified file ID assert result is not None assert result.id == unified_file_id @@ -1525,27 +1501,27 @@ async def test_afile_retrieve_raises_error_when_no_router_and_file_object_none() """ prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return a stored object with file_object=None stored_file = MagicMock() stored_file.file_object = None stored_file.model_mappings = {"model-123": "file-provider-xyz"} proxy_managed_files.get_unified_file_id = AsyncMock(return_value=stored_file) - + unified_file_id = "test-unified-file-id" - + with pytest.raises(Exception) as exc_info: await proxy_managed_files.afile_retrieve( file_id=unified_file_id, litellm_parent_otel_span=None, llm_router=None, ) - + assert "llm_router is required" in str(exc_info.value) @@ -1556,15 +1532,15 @@ async def test_afile_retrieve_returns_stored_file_object_when_exists(): (the normal case for user-uploaded files). """ from litellm.types.llms.openai import OpenAIFileObject - + prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return a stored object WITH file_object stored_file_object = OpenAIFileObject( id="test-unified-file-id", @@ -1577,13 +1553,13 @@ async def test_afile_retrieve_returns_stored_file_object_when_exists(): stored_file = MagicMock() stored_file.file_object = stored_file_object proxy_managed_files.get_unified_file_id = AsyncMock(return_value=stored_file) - + result = await proxy_managed_files.afile_retrieve( file_id="test-unified-file-id", litellm_parent_otel_span=None, llm_router=None, ) - + # Should return the stored file object directly assert result == stored_file_object @@ -1596,21 +1572,21 @@ async def test_afile_retrieve_raises_error_for_non_managed_file(): """ prisma_client = AsyncMock() internal_usage_cache = MagicMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( internal_usage_cache=internal_usage_cache, prisma_client=prisma_client, ) - + # Mock get_unified_file_id to return None (file not found) proxy_managed_files.get_unified_file_id = AsyncMock(return_value=None) - + with pytest.raises(Exception) as exc_info: await proxy_managed_files.afile_retrieve( file_id="non-existent-file-id", litellm_parent_otel_span=None, ) - + assert "not found" in str(exc_info.value) @@ -1621,58 +1597,54 @@ async def test_list_batches_from_managed_objects_table(): from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + batch_record_1 = MagicMock() batch_record_1.unified_object_id = "unified-batch-id-1" - batch_record_1.file_object = json.dumps( - { - "id": "batch_abc123", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567890, - "input_file_id": "file-input-1", - "request_counts": {"total": 1, "completed": 1, "failed": 0}, - } - ) - + batch_record_1.file_object = json.dumps({ + "id": "batch_abc123", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567890, + "input_file_id": "file-input-1", + "request_counts": {"total": 1, "completed": 1, "failed": 0}, + }) + batch_record_2 = MagicMock() batch_record_2.unified_object_id = "unified-batch-id-2" - batch_record_2.file_object = json.dumps( - { - "id": "batch_xyz789", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "in_progress", - "created_at": 1234567891, - "input_file_id": "file-input-2", - "request_counts": {"total": 5, "completed": 2, "failed": 0}, - } - ) - + batch_record_2.file_object = json.dumps({ + "id": "batch_xyz789", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "in_progress", + "created_at": 1234567891, + "input_file_id": "file-input-2", + "request_counts": {"total": 5, "completed": 2, "failed": 0}, + }) + prisma_client.db.litellm_managedobjecttable.find_many.return_value = [ batch_record_1, batch_record_2, ] - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + result = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), limit=10, ) - + assert result["object"] == "list" assert len(result["data"]) == 2 assert result["data"][0].id == "unified-batch-id-1" assert result["data"][1].id == "unified-batch-id-2" assert result["first_id"] == "unified-batch-id-1" assert result["last_id"] == "unified-batch-id-2" - + # Should filter by user_id (created_by) prisma_client.db.litellm_managedobjecttable.find_many.assert_called_once_with( where={"file_purpose": "batch", "created_by": "test-user"}, @@ -1687,21 +1659,21 @@ async def test_list_batches_from_managed_objects_table_empty_list(): prisma_client = AsyncMock() prisma_client.db.litellm_managedobjecttable.find_many.return_value = [] - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + result = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="test-user"), ) - + assert result["object"] == "list" assert len(result["data"]) == 0 assert result["first_id"] is None assert result["last_id"] is None assert result["has_more"] is False - + # Verify where clause includes created_by filter # Default take is 20 when no limit is provided prisma_client.db.litellm_managedobjecttable.find_many.assert_called_once_with( @@ -1713,7 +1685,6 @@ async def test_list_batches_from_managed_objects_table_empty_list(): def _create_unified_batch_id(model_id: str, batch_id: str) -> str: import base64 - unified_str = f"litellm_proxy;model_id:{model_id};llm_batch_id:{batch_id}" return base64.urlsafe_b64encode(unified_str.encode()).decode().rstrip("=") @@ -1723,11 +1694,11 @@ async def test_list_batches_from_managed_objects_table_provider_filter_raises_ex from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # Filtering by provider should raise Exception with pytest.raises(Exception) as exc_info: await proxy_managed_files.list_user_batches( @@ -1735,11 +1706,11 @@ async def test_list_batches_from_managed_objects_table_provider_filter_raises_ex limit=10, provider="openai", ) - + assert str(exc_info.value) == ( "Filtering by 'provider' is not supported when using managed batches." ) - + # Verify find_many was NOT called since exception is raised before database query prisma_client.db.litellm_managedobjecttable.find_many.assert_not_called() @@ -1749,7 +1720,7 @@ async def test_list_batches_from_managed_objects_table_target_model_name_filter_ from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) @@ -1761,64 +1732,59 @@ async def test_list_batches_from_managed_objects_table_target_model_name_filter_ limit=10, target_model_names="gpt-4o,gpt-3.5", ) - + assert str(exc_info.value) == ( "Filtering by 'target_model_names' is not supported when using managed batches." ) - + # Verify find_many was NOT called since exception is raised before database query prisma_client.db.litellm_managedobjecttable.find_many.assert_not_called() - @pytest.mark.asyncio async def test_list_batches_from_managed_objects_table_filters_by_created_by(): from litellm.proxy._types import UserAPIKeyAuth prisma_client = AsyncMock() - + # Create batch for user1 batch_user1 = MagicMock() batch_user1.unified_object_id = "unified-batch-user1" - batch_user1.file_object = json.dumps( - { - "id": "batch_user1_abc", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567890, - "input_file_id": "file-input-user1", - "request_counts": {"total": 1, "completed": 1, "failed": 0}, - } - ) - + batch_user1.file_object = json.dumps({ + "id": "batch_user1_abc", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567890, + "input_file_id": "file-input-user1", + "request_counts": {"total": 1, "completed": 1, "failed": 0}, + }) + # Create batch for user2 batch_user2 = MagicMock() batch_user2.unified_object_id = "unified-batch-user2" - batch_user2.file_object = json.dumps( - { - "id": "batch_user2_xyz", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567891, - "input_file_id": "file-input-user2", - "request_counts": {"total": 2, "completed": 2, "failed": 0}, - } - ) - + batch_user2.file_object = json.dumps({ + "id": "batch_user2_xyz", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567891, + "input_file_id": "file-input-user2", + "request_counts": {"total": 2, "completed": 2, "failed": 0}, + }) + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # Query with user1's API key - should only return user1's batch prisma_client.db.litellm_managedobjecttable.find_many.return_value = [batch_user1] result_user1 = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="user1"), limit=10, ) - + assert len(result_user1["data"]) == 1 assert result_user1["data"][0].id == "unified-batch-user1" prisma_client.db.litellm_managedobjecttable.find_many.assert_called_with( @@ -1826,14 +1792,14 @@ async def test_list_batches_from_managed_objects_table_filters_by_created_by(): take=10, order={"created_at": "desc"}, ) - + # Query with user2's API key - should only return user2's batch prisma_client.db.litellm_managedobjecttable.find_many.return_value = [batch_user2] result_user2 = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="user2"), limit=10, ) - + assert len(result_user2["data"]) == 1 assert result_user2["data"][0].id == "unified-batch-user2" prisma_client.db.litellm_managedobjecttable.find_many.assert_called_with( @@ -1856,7 +1822,7 @@ async def test_return_unified_file_id_includes_expires_at(): filename="test.jsonl", purpose="batch", status="uploaded", - expires_at=1234657890, + expires_at=1234657890, ) file_object._hidden_params = {"model_id": "test-model-id"} @@ -1896,27 +1862,25 @@ async def test_return_unified_file_id_includes_expires_at(): async def test_user_b_cannot_retrieve_user_a_batch(): """ Test that User B cannot retrieve a batch created by User A. - + This verifies batch isolation between users at the database/hook level. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User B tries to retrieve User A's batch - unified_batch_id = ( - "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - ) - + unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -1926,7 +1890,7 @@ async def test_user_b_cannot_retrieve_user_a_batch(): data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -1937,23 +1901,21 @@ async def test_user_b_cannot_cancel_user_a_batch(): Test that User B cannot cancel a batch created by User A. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User B tries to cancel User A's batch - unified_batch_id = ( - "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - ) - + unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -1963,7 +1925,7 @@ async def test_user_b_cannot_cancel_user_a_batch(): data={"batch_id": unified_batch_id}, call_type="acancel_batch", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -1972,28 +1934,26 @@ async def test_user_b_cannot_cancel_user_a_batch(): async def test_user_a_can_retrieve_own_batch(): """ Test that User A can successfully retrieve their own batch. - + This is a positive test case to ensure permission checks don't block legitimate access. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User A retrieves their own batch - unified_batch_id = ( - "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - ) - + unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + # Should not raise an exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2003,7 +1963,7 @@ async def test_user_a_can_retrieve_own_batch(): data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + # Should successfully return the decoded batch_id assert "batch_id" in result assert result["model"] == "my-model" @@ -2015,23 +1975,21 @@ async def test_user_b_cannot_retrieve_user_a_file(): Test that User B cannot retrieve a file created by User A. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User B tries to retrieve User A's file - unified_file_id = ( - "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - ) - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2041,7 +1999,7 @@ async def test_user_b_cannot_retrieve_user_a_file(): data={"file_id": unified_file_id}, call_type="afile_retrieve", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -2052,23 +2010,21 @@ async def test_user_b_cannot_download_user_a_file_content(): Test that User B cannot download file content for User A's file. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User B tries to download User A's file content - unified_file_id = ( - "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - ) - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2078,7 +2034,7 @@ async def test_user_b_cannot_download_user_a_file_content(): data={"file_id": unified_file_id}, call_type="afile_content", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -2089,23 +2045,21 @@ async def test_user_b_cannot_delete_user_a_file(): Test that User B cannot delete a file created by User A. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User B tries to delete User A's file - unified_file_id = ( - "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - ) - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + with pytest.raises(HTTPException) as exc_info: await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2115,7 +2069,7 @@ async def test_user_b_cannot_delete_user_a_file(): data={"file_id": unified_file_id}, call_type="afile_delete", ) - + # Should raise 403 Permission Denied assert exc_info.value.status_code == 403 @@ -2124,38 +2078,34 @@ async def test_user_b_cannot_delete_user_a_file(): async def test_user_a_can_retrieve_own_file(): """ Test that User A can successfully retrieve their own file. - + Positive test case to ensure permission checks work correctly for the owner. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return User A as the creator file_record = MagicMock() file_record.created_by = "user_a_id" file_record.model_mappings = '{"model-123": "file-abc123"}' - file_record.file_object = json.dumps( - { - "id": "file-abc123", - "object": "file", - "bytes": 1234, - "created_at": 1234567890, - "filename": "test.jsonl", - "purpose": "batch", - } - ) + file_record.file_object = json.dumps({ + "id": "file-abc123", + "object": "file", + "bytes": 1234, + "created_at": 1234567890, + "filename": "test.jsonl", + "purpose": "batch", + }) prisma_client.db.litellm_managedfiletable.find_first.return_value = file_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( MagicMock(), prisma_client=prisma_client ) - + # User A retrieves their own file - unified_file_id = ( - "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" - ) - + unified_file_id = "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29uO3VuaWZpZWRfaWQsZmlsZS1hYmMxMjM" + # Should not raise an exception result = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( @@ -2165,7 +2115,7 @@ async def test_user_a_can_retrieve_own_file(): data={"file_id": unified_file_id}, call_type="afile_retrieve", ) - + # Should successfully return the decoded file_id assert "file_id" in result @@ -2174,46 +2124,44 @@ async def test_user_a_can_retrieve_own_file(): async def test_list_batches_only_returns_user_own_batches(): """ Test that list_user_batches only returns batches created by the requesting user. - + This ensures users cannot see other users' batches in list operations. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Create batches for User A batch_user_a = MagicMock() batch_user_a.unified_object_id = "batch-user-a" - batch_user_a.file_object = json.dumps( - { - "id": "batch_a", - "object": "batch", - "endpoint": "/v1/chat/completions", - "completion_window": "24h", - "status": "completed", - "created_at": 1234567890, - "input_file_id": "file-a", - "request_counts": {"total": 1, "completed": 1, "failed": 0}, - } - ) - + batch_user_a.file_object = json.dumps({ + "id": "batch_a", + "object": "batch", + "endpoint": "/v1/chat/completions", + "completion_window": "24h", + "status": "completed", + "created_at": 1234567890, + "input_file_id": "file-a", + "request_counts": {"total": 1, "completed": 1, "failed": 0}, + }) + # Mock database to only return User A's batches prisma_client.db.litellm_managedobjecttable.find_many.return_value = [batch_user_a] - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - + # User A requests their batches result = await proxy_managed_files.list_user_batches( user_api_key_dict=UserAPIKeyAuth(user_id="user_a_id"), limit=10, ) - + # Should only return User A's batches assert len(result["data"]) == 1 assert result["data"][0].id == "batch-user-a" - + # Verify the database query filtered by user_id prisma_client.db.litellm_managedobjecttable.find_many.assert_called_once_with( where={"file_purpose": "batch", "created_by": "user_a_id"}, @@ -2226,49 +2174,51 @@ async def test_list_batches_only_returns_user_own_batches(): async def test_same_user_different_keys_can_access_batch(): """ Test that different API keys for the same user can access the same batch. - + This verifies that permission checks are based on user_id, not API key, allowing users to have multiple keys that can all access their resources. """ from litellm.proxy._types import UserAPIKeyAuth - + prisma_client = AsyncMock() - + # Mock database to return the user_id as creator batch_record = MagicMock() batch_record.created_by = "user_a_id" prisma_client.db.litellm_managedobjecttable.find_first.return_value = batch_record - + proxy_managed_files = _PROXY_LiteLLMManagedFiles( DualCache(), prisma_client=prisma_client ) - - unified_batch_id = ( - "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" - ) - + + unified_batch_id = "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpteS1tb2RlbDtsbG1fYmF0Y2hfaWQ6YmF0Y2hfYWJjMTIz" + # First API key for User A retrieves the batch result1 = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( - user_id="user_a_id", api_key="key-1", parent_otel_span=MagicMock() + user_id="user_a_id", + api_key="key-1", + parent_otel_span=MagicMock() ), cache=MagicMock(), data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + assert "batch_id" in result1 - + # Second API key for the same User A retrieves the batch result2 = await proxy_managed_files.async_pre_call_hook( user_api_key_dict=UserAPIKeyAuth( - user_id="user_a_id", api_key="key-2", parent_otel_span=MagicMock() + user_id="user_a_id", + api_key="key-2", + parent_otel_span=MagicMock() ), cache=MagicMock(), data={"batch_id": unified_batch_id}, call_type="aretrieve_batch", ) - + assert "batch_id" in result2 # Both keys should get the same result assert result1["batch_id"] == result2["batch_id"] diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py index 699ab8d1c4c..69c3b4cb59a 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_internal_user_endpoints.py @@ -31,16 +31,12 @@ class TestAvailableEnterpriseUsers: self, client, mock_user_api_key_auth ): """Test when max_users is set and user count is within limit""" - with ( - patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, - patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), - patch( - "litellm.proxy.proxy_server.premium_user_data", - {"max_users": 10}, - ), + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), patch( + "litellm.proxy.proxy_server.premium_user_data", + {"max_users": 10}, ): # Mock database count mock_prisma.db.litellm_usertable.count = AsyncMock(return_value=5) @@ -70,16 +66,12 @@ class TestAvailableEnterpriseUsers: self, client, mock_user_api_key_auth ): """Test when max_users is not set (premium_user_data is None or doesn't contain max_users)""" - with ( - patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, - patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), - patch( - "litellm.proxy.proxy_server.premium_user_data", - None, - ), + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), patch( + "litellm.proxy.proxy_server.premium_user_data", + None, ): # Mock database count mock_prisma.db.litellm_usertable.count = AsyncMock(return_value=3) @@ -107,16 +99,12 @@ class TestAvailableEnterpriseUsers: self, client, mock_user_api_key_auth ): """Test the current bug where total_users_remaining can be negative""" - with ( - patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, - patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), - patch( - "litellm.proxy.proxy_server.premium_user_data", - {"key": "value"}, - ), + with patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma, patch( + "litellm.proxy.proxy_server.premium_user", + True, + ), patch( + "litellm.proxy.proxy_server.premium_user_data", + {"key": "value"}, ): # Mock database count higher than max_users to trigger the bug mock_prisma.db.litellm_usertable.count = AsyncMock(return_value=8) @@ -152,15 +140,12 @@ class TestAvailableEnterpriseUsers: """Test when prisma_client is None (no database connection)""" from litellm.proxy._types import CommonProxyErrors - with ( - patch( - "litellm.proxy.proxy_server.prisma_client", - None, - ), - patch( - "litellm.proxy.proxy_server.premium_user", - True, - ), + with patch( + "litellm.proxy.proxy_server.prisma_client", + None, + ), patch( + "litellm.proxy.proxy_server.premium_user", + True, ): # Override the dependency client.app.dependency_overrides[mock_user_api_key_auth] = lambda: { diff --git a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index 8e0a3655b57..52cb94ff346 100644 --- a/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py +++ b/tests/enterprise/litellm_enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -801,9 +801,7 @@ async def test_list_projects_returns_timestamps(): from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch - from litellm_enterprise.proxy.management_endpoints.project_endpoints import ( - list_projects, - ) + from litellm_enterprise.proxy.management_endpoints.project_endpoints import list_projects from litellm.proxy._types import LiteLLM_ProjectTable now = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc) diff --git a/tests/guardrails_tests/test_akto_guardrails.py b/tests/guardrails_tests/test_akto_guardrails.py index 488899d8317..83421f13136 100644 --- a/tests/guardrails_tests/test_akto_guardrails.py +++ b/tests/guardrails_tests/test_akto_guardrails.py @@ -14,6 +14,7 @@ from litellm.proxy.guardrails.guardrail_registry import ( ) from litellm.proxy.guardrails.guardrail_hooks.akto.akto import AktoGuardrail + # --------------------------------------------------------------------------- # Registry tests # --------------------------------------------------------------------------- diff --git a/tests/guardrails_tests/test_custom_guardrail.py b/tests/guardrails_tests/test_custom_guardrail.py index 95ac82b8d0f..af1270756f2 100644 --- a/tests/guardrails_tests/test_custom_guardrail.py +++ b/tests/guardrails_tests/test_custom_guardrail.py @@ -6,6 +6,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/guardrails_tests/test_eu_ai_act_article5.py b/tests/guardrails_tests/test_eu_ai_act_article5.py index d602b206b53..bda7bf6f517 100644 --- a/tests/guardrails_tests/test_eu_ai_act_article5.py +++ b/tests/guardrails_tests/test_eu_ai_act_article5.py @@ -21,6 +21,7 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor ContentFilterCategoryConfig, ) + # Test cases: (sentence, expected_result, reason) TEST_CASES = [ # ALWAYS BLOCK - Explicit prohibited practices (1-10) diff --git a/tests/guardrails_tests/test_sg_mas_ai_guardrails.py b/tests/guardrails_tests/test_sg_mas_ai_guardrails.py index 47bfd1b8a10..668ee704692 100644 --- a/tests/guardrails_tests/test_sg_mas_ai_guardrails.py +++ b/tests/guardrails_tests/test_sg_mas_ai_guardrails.py @@ -23,6 +23,7 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor ContentFilterCategoryConfig, ) + # ── helpers ────────────────────────────────────────────────────────────── POLICY_DIR = os.path.abspath( diff --git a/tests/guardrails_tests/test_sg_pdpa_guardrails.py b/tests/guardrails_tests/test_sg_pdpa_guardrails.py index 03f55777f21..fd7133bc745 100644 --- a/tests/guardrails_tests/test_sg_pdpa_guardrails.py +++ b/tests/guardrails_tests/test_sg_pdpa_guardrails.py @@ -28,6 +28,7 @@ from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter impor ContentFilterCategoryConfig, ) + # ── helpers ────────────────────────────────────────────────────────────── POLICY_DIR = os.path.abspath( diff --git a/tests/image_gen_tests/test_image_generation.py b/tests/image_gen_tests/test_image_generation.py index bd39206fd02..5152e3e0129 100644 --- a/tests/image_gen_tests/test_image_generation.py +++ b/tests/image_gen_tests/test_image_generation.py @@ -7,6 +7,7 @@ import sys import traceback from unittest.mock import AsyncMock, MagicMock, patch + sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path diff --git a/tests/image_gen_tests/test_image_variation.py b/tests/image_gen_tests/test_image_variation.py index ef528c391e1..301835057a7 100644 --- a/tests/image_gen_tests/test_image_variation.py +++ b/tests/image_gen_tests/test_image_variation.py @@ -6,6 +6,7 @@ import os import sys import traceback + sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path diff --git a/tests/litellm/llms/bedrock/embed/test_embedding.py b/tests/litellm/llms/bedrock/embed/test_embedding.py index 163c143d825..261448842f4 100644 --- a/tests/litellm/llms/bedrock/embed/test_embedding.py +++ b/tests/litellm/llms/bedrock/embed/test_embedding.py @@ -11,6 +11,7 @@ import pytest from litellm.types.utils import Embedding from litellm.main import bedrock_embedding, embedding, EmbeddingResponse, Usage + _mock_model_id = ( "arn:aws:bedrock:us-east-1:123412341234:application-inference-profile/abc123123" ) diff --git a/tests/litellm/llms/bedrock/test_nova_imported_models.py b/tests/litellm/llms/bedrock/test_nova_imported_models.py index b5fd2630cbe..e3677aaf9e6 100644 --- a/tests/litellm/llms/bedrock/test_nova_imported_models.py +++ b/tests/litellm/llms/bedrock/test_nova_imported_models.py @@ -11,6 +11,7 @@ from litellm.llms.bedrock.common_utils import ( ) from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig + NOVA_ARN = "arn:aws:bedrock:us-east-1:123456789012:custom-model-deployment/a1b2c3d4e5f6" NOVA_MODEL = f"bedrock/nova/{NOVA_ARN}" NOVA2_MODEL = f"bedrock/nova-2/{NOVA_ARN}" diff --git a/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py b/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py index fde468618ea..54ea41a6450 100644 --- a/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py +++ b/tests/litellm/llms/vertex_ai/test_gemini_batch_embeddings.py @@ -581,21 +581,17 @@ def test_vertex_ai_text_only_embedding_uses_embed_content(): def test_filter_embed_params_drops_unsupported(): """Unsupported params like max_tokens should be filtered out.""" - result = _filter_embed_params( - {"dimensions": 768, "max_tokens": 256, "temperature": 0.5} - ) + result = _filter_embed_params({"dimensions": 768, "max_tokens": 256, "temperature": 0.5}) assert result == {"outputDimensionality": 768} def test_filter_embed_params_keeps_supported(): """All supported Gemini embedding params should pass through.""" - result = _filter_embed_params( - { - "dimensions": 768, - "task_type": "RETRIEVAL_DOCUMENT", - "title": "My doc", - } - ) + result = _filter_embed_params({ + "dimensions": 768, + "task_type": "RETRIEVAL_DOCUMENT", + "title": "My doc", + }) assert result == { "outputDimensionality": 768, "taskType": "RETRIEVAL_DOCUMENT", diff --git a/tests/litellm/test_bedrock_nemotron_super.py b/tests/litellm/test_bedrock_nemotron_super.py index 7b4efd86b28..8b081f10d1d 100644 --- a/tests/litellm/test_bedrock_nemotron_super.py +++ b/tests/litellm/test_bedrock_nemotron_super.py @@ -11,6 +11,7 @@ import pytest from litellm import get_model_info + MODEL_NAME = "nvidia.nemotron-super-3-120b" diff --git a/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py b/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py index 87af6a35f4b..c32917efe87 100644 --- a/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py +++ b/tests/litellm_core_utils/test_bedrock_converse_dedup_factory.py @@ -12,6 +12,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( BedrockConverseMessagesProcessor, ) + MODEL = "anthropic.claude-v2" PROVIDER = "bedrock_converse" @@ -545,9 +546,9 @@ def test_bedrock_converse_sorts_text_before_tooluse_sync(): tool_indices = [i for i, b in enumerate(content) if "toolUse" in b] # All text blocks must come before all toolUse blocks - assert max(text_indices) < min( - tool_indices - ), f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" + assert max(text_indices) < min(tool_indices), ( + f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" + ) @pytest.mark.asyncio @@ -566,9 +567,9 @@ async def test_bedrock_converse_sorts_text_before_tooluse_async(): text_indices = [i for i, b in enumerate(content) if "text" in b] tool_indices = [i for i, b in enumerate(content) if "toolUse" in b] - assert max(text_indices) < min( - tool_indices - ), f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" + assert max(text_indices) < min(tool_indices), ( + f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}" + ) @pytest.mark.asyncio @@ -576,9 +577,7 @@ async def test_bedrock_converse_content_ordering_sync_async_parity(): """Sync and async paths should produce identical content block ordering.""" messages = _make_tooluse_before_text_messages() sync_result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER) - async_result = ( - await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async( - messages, MODEL, PROVIDER - ) + async_result = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async( + messages, MODEL, PROVIDER ) assert sync_result == async_result diff --git a/tests/litellm_utils_tests/test_aws_secret_manager.py b/tests/litellm_utils_tests/test_aws_secret_manager.py index 00417ea1b2e..674f9b3ca82 100644 --- a/tests/litellm_utils_tests/test_aws_secret_manager.py +++ b/tests/litellm_utils_tests/test_aws_secret_manager.py @@ -10,6 +10,7 @@ from dotenv import load_dotenv import litellm.types import litellm.types.utils + load_dotenv() import io diff --git a/tests/litellm_utils_tests/test_litellm_overhead.py b/tests/litellm_utils_tests/test_litellm_overhead.py index d5aa7eb6713..3a428e9d588 100644 --- a/tests/litellm_utils_tests/test_litellm_overhead.py +++ b/tests/litellm_utils_tests/test_litellm_overhead.py @@ -14,6 +14,7 @@ sys.path.insert( ) # Adds the parent directory to the system path import litellm + # Fake Vertex AI Gemini response for mocking FAKE_VERTEX_GEMINI_RESPONSE = { "candidates": [ diff --git a/tests/llm_responses_api_testing/test_anthropic_responses_api.py b/tests/llm_responses_api_testing/test_anthropic_responses_api.py index 746d54b4d49..6537f67acb9 100644 --- a/tests/llm_responses_api_testing/test_anthropic_responses_api.py +++ b/tests/llm_responses_api_testing/test_anthropic_responses_api.py @@ -12,6 +12,7 @@ from litellm.responses.litellm_completion_transformation.transformation import ( ) from litellm.types.utils import ModelResponse + sys.path.insert(0, os.path.abspath("../..")) import litellm from litellm.integrations.custom_logger import CustomLogger diff --git a/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py b/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py index 3d256794cb8..08b1c1784e7 100644 --- a/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py +++ b/tests/llm_responses_api_testing/test_anthropic_tool_result_empty_call_id.py @@ -2,7 +2,7 @@ Test to reproduce and verify fix for Anthropic tool_result issue with empty call_id. This test reproduces the exact error: -"messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks: tool_use_id. +"messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks: tool_use_id. Each `tool_result` block must have a corresponding `tool_use` block in the previous message." The issue occurs when: diff --git a/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py b/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py index 8ce234dbcbb..8f5278698ba 100644 --- a/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py +++ b/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py @@ -2,12 +2,12 @@ Unit tests for BaseResponsesAPIStreamingIterator Tests core functionality including: -1. Processing chunks and handling ResponseCompletedEvent +1. Processing chunks and handling ResponseCompletedEvent 2. Ensuring _update_responses_api_response_id_with_model_id is called for final chunk 3. Verifying ID update is NOT called for non-final chunks (delta events) 4. Edge case handling for invalid JSON, empty chunks, and [DONE] markers -These tests ensure the streaming iterator correctly processes response chunks +These tests ensure the streaming iterator correctly processes response chunks and applies model ID updates only to completed responses, as required for proper response tracking and logging. """ diff --git a/tests/llm_responses_api_testing/test_responses_hooks.py b/tests/llm_responses_api_testing/test_responses_hooks.py index 09ac9c7a9e3..3799a0b9121 100644 --- a/tests/llm_responses_api_testing/test_responses_hooks.py +++ b/tests/llm_responses_api_testing/test_responses_hooks.py @@ -387,7 +387,9 @@ def test_process_chunk_completed_response_updates_id_and_usage_cost(monkeypatch) # Chunk must include a top-level "response" key so BaseResponsesAPIStreamingIterator # runs _update_responses_api_response_id_with_model_id (see streaming_iterator.py). event = iterator._process_chunk( - json.dumps({"type": "response.completed", "response": {"id": "resp_live"}}) + json.dumps( + {"type": "response.completed", "response": {"id": "resp_live"}} + ) ) finally: litellm.include_cost_in_streaming_usage = original_include_cost diff --git a/tests/llm_translation/test_bedrock_anthropic_regression.py b/tests/llm_translation/test_bedrock_anthropic_regression.py index d81e76cd4b7..8b8ce0a6cc8 100644 --- a/tests/llm_translation/test_bedrock_anthropic_regression.py +++ b/tests/llm_translation/test_bedrock_anthropic_regression.py @@ -22,8 +22,10 @@ sys.path.insert(0, os.path.abspath("../..")) import litellm from litellm import completion + # Large document for caching tests (needs 1024+ tokens for Claude models) -LARGE_DOCUMENT_FOR_CACHING = """ +LARGE_DOCUMENT_FOR_CACHING = ( + """ This is a comprehensive legal agreement between Party A and Party B. ARTICLE 1: DEFINITIONS @@ -75,7 +77,9 @@ ARTICLE 9: GENERAL PROVISIONS 9.5 Waiver of any provision shall not constitute ongoing waiver. IN WITNESS WHEREOF, the parties have executed this Agreement. -""" * 8 # Repeat to ensure we have enough tokens (need 1024+ for Claude models) +""" + * 8 +) # Repeat to ensure we have enough tokens (need 1024+ for Claude models) class TestBedrockAnthropicPromptCachingRegression: diff --git a/tests/llm_translation/test_bedrock_invoke_tests.py b/tests/llm_translation/test_bedrock_invoke_tests.py index 0258ab15c6e..23f436d5b28 100644 --- a/tests/llm_translation/test_bedrock_invoke_tests.py +++ b/tests/llm_translation/test_bedrock_invoke_tests.py @@ -3,6 +3,7 @@ import pytest import sys import os + sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system path diff --git a/tests/llm_translation/test_crusoe.py b/tests/llm_translation/test_crusoe.py index 158152d8aeb..56aa4e4cd42 100644 --- a/tests/llm_translation/test_crusoe.py +++ b/tests/llm_translation/test_crusoe.py @@ -1,7 +1,6 @@ """ Tests for Crusoe provider integration """ - import os from unittest import mock @@ -97,9 +96,9 @@ def test_crusoe_models_configuration(): for model in crusoe_models: model_info = get_model_info(model) assert model_info is not None, f"Model info not found for {model}" - assert ( - model_info.get("litellm_provider") == "crusoe" - ), f"{model} should have crusoe as provider" + assert model_info.get("litellm_provider") == "crusoe", ( + f"{model} should have crusoe as provider" + ) assert model_info.get("mode") == "chat", f"{model} should be in chat mode" finally: litellm.model_cost = original_model_cost diff --git a/tests/llm_translation/test_gemini.py b/tests/llm_translation/test_gemini.py index 3e322e1a6b4..97b0aaee86b 100644 --- a/tests/llm_translation/test_gemini.py +++ b/tests/llm_translation/test_gemini.py @@ -1362,12 +1362,8 @@ def test_anthropic_thinking_param_to_gemini_3_provider_defaults(): ) # For Gemini 3, should not force thinkingLevel by default - assert ( - "thinkingLevel" not in result - ), "Should not force thinkingLevel for Gemini 3" - assert ( - "thinkingBudget" not in result - ), "Should NOT have thinkingBudget for Gemini 3" + assert "thinkingLevel" not in result, "Should not force thinkingLevel for Gemini 3" + assert "thinkingBudget" not in result, "Should NOT have thinkingBudget for Gemini 3" assert result["includeThoughts"] is True # Test 2: Anthropic thinking disabled for Gemini 3 @@ -1399,10 +1395,7 @@ def test_anthropic_thinking_param_to_gemini_3_provider_defaults(): ) assert result_zero["includeThoughts"] is False - assert ( - "thinkingLevel" not in result_zero - or result_zero.get("thinkingLevel") is None - ) + assert "thinkingLevel" not in result_zero or result_zero.get("thinkingLevel") is None # Test 4: Gemini 3 flash-preview should also follow provider defaults by default result_gemini3flashpreview = VertexGeminiConfig._map_thinking_param( @@ -1532,12 +1525,8 @@ def test_anthropic_thinking_param_via_map_openai_params(): # Check that thinkingConfig was created without forced thinkingLevel assert "thinkingConfig" in result, "Should have thinkingConfig in optional_params" thinking_config = result["thinkingConfig"] - assert ( - "thinkingLevel" not in thinking_config - ), "Should not force thinkingLevel for Gemini 3 by default" - assert ( - "thinkingBudget" not in thinking_config - ), "Should NOT have thinkingBudget for Gemini 3" + assert "thinkingLevel" not in thinking_config, "Should not force thinkingLevel for Gemini 3 by default" + assert "thinkingBudget" not in thinking_config, "Should NOT have thinkingBudget for Gemini 3" assert thinking_config["includeThoughts"] is True # Test with Gemini 2 model diff --git a/tests/llm_translation/test_gemini_image_usage.py b/tests/llm_translation/test_gemini_image_usage.py index 75ce8c81469..096f9c4796c 100644 --- a/tests/llm_translation/test_gemini_image_usage.py +++ b/tests/llm_translation/test_gemini_image_usage.py @@ -1,7 +1,7 @@ """ Test for Gemini image generation usage metadata extraction. -This test verifies the fix for issue #18323 where image_generation() +This test verifies the fix for issue #18323 where image_generation() was returning usage=0 while completion() returned proper token usage. """ diff --git a/tests/local_testing/create_mock_standard_logging_payload.py b/tests/local_testing/create_mock_standard_logging_payload.py index 828faba0b04..2fd6a4ffa8a 100644 --- a/tests/local_testing/create_mock_standard_logging_payload.py +++ b/tests/local_testing/create_mock_standard_logging_payload.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 8ca13369ff1..9782bf3c2af 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -38,6 +38,7 @@ from litellm.llms.vertex_ai.gemini.transformation import ( ) from litellm.llms.vertex_ai.vertex_llm_base import VertexBase + litellm.num_retries = 3 litellm.cache = None user_message = "Write a short poem about the sky" @@ -1103,7 +1104,9 @@ def vertex_httpx_mock_post_valid_response(*args, **kwargs): { "content": { "role": "model", - "parts": [{"text": """{ + "parts": [ + { + "text": """{ "recipes": [ {"recipe_name": "Chocolate Chip Cookies"}, {"recipe_name": "Oatmeal Raisin Cookies"}, @@ -1111,7 +1114,9 @@ def vertex_httpx_mock_post_valid_response(*args, **kwargs): {"recipe_name": "Sugar Cookies"}, {"recipe_name": "Snickerdoodles"} ] - }"""}], + }""" + } + ], }, "finishReason": "STOP", "safetyRatings": [ diff --git a/tests/local_testing/test_cache_preset_key.py b/tests/local_testing/test_cache_preset_key.py index cf496fe4b86..d6518c5a073 100644 --- a/tests/local_testing/test_cache_preset_key.py +++ b/tests/local_testing/test_cache_preset_key.py @@ -4,7 +4,7 @@ Test for preset_cache_key multiple values bug fix. This test verifies that get_cache_key doesn't raise TypeError when kwargs already contains preset_cache_key. -Issue: When get_cache_key(**kwargs) is called with kwargs containing +Issue: When get_cache_key(**kwargs) is called with kwargs containing preset_cache_key, the call to _set_preset_cache_key_in_kwargs() would fail with: TypeError: got multiple values for keyword argument 'preset_cache_key' """ diff --git a/tests/local_testing/test_caching.py b/tests/local_testing/test_caching.py index a68e8c91584..0c7c0157651 100644 --- a/tests/local_testing/test_caching.py +++ b/tests/local_testing/test_caching.py @@ -450,9 +450,12 @@ def c(): litellm.enable_caching_on_provider_specific_optional_params = False -embedding_large_text = """ +embedding_large_text = ( + """ small text -""" * 5 +""" + * 5 +) # # test_caching_with_models() diff --git a/tests/local_testing/test_completion.py b/tests/local_testing/test_completion.py index df1b6d7a8e6..6341fa78006 100644 --- a/tests/local_testing/test_completion.py +++ b/tests/local_testing/test_completion.py @@ -1638,13 +1638,15 @@ def custom_callback( ################################################# - print(f""" + print( + f""" Model: {model}, Messages: {messages}, User: {user}, Seed: {kwargs["seed"]}, temperature: {kwargs["temperature"]}, - """) + """ + ) assert kwargs["user"] == "ishaans app" assert kwargs["model"] == "gpt-3.5-turbo-1106" diff --git a/tests/local_testing/test_configs/custom_callbacks.py b/tests/local_testing/test_configs/custom_callbacks.py index 7ef6c1aaad5..42f88b5d19b 100644 --- a/tests/local_testing/test_configs/custom_callbacks.py +++ b/tests/local_testing/test_configs/custom_callbacks.py @@ -93,7 +93,8 @@ class testCustomCallbackProxy(CustomLogger): print("\n\n in custom callback vars my custom logger, ", vars(my_custom_logger)) - print(f""" + print( + f""" Model: {model}, Messages: {messages}, User: {user}, @@ -101,7 +102,8 @@ class testCustomCallbackProxy(CustomLogger): Cost: {cost}, Response: {response} Proxy Metadata: {metadata} - """) + """ + ) return async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time): diff --git a/tests/local_testing/test_get_llm_provider.py b/tests/local_testing/test_get_llm_provider.py index 010a071f73e..14b9e8cd136 100644 --- a/tests/local_testing/test_get_llm_provider.py +++ b/tests/local_testing/test_get_llm_provider.py @@ -477,3 +477,4 @@ def test_get_llm_provider_use_proxy_arg_true_with_direct_args(): assert provider == "litellm_proxy" assert key == arg_api_key # Should use the argument key assert base == arg_api_base # Should use the argument base + diff --git a/tests/local_testing/test_langsmith.py b/tests/local_testing/test_langsmith.py index 6f09b2701b0..af7ac46a1cf 100644 --- a/tests/local_testing/test_langsmith.py +++ b/tests/local_testing/test_langsmith.py @@ -21,6 +21,7 @@ verbose_logger.setLevel(logging.DEBUG) litellm.set_verbose = True import time + # test_langsmith_logging() diff --git a/tests/local_testing/test_router_caching.py b/tests/local_testing/test_router_caching.py index ad358699a48..cb223b661b4 100644 --- a/tests/local_testing/test_router_caching.py +++ b/tests/local_testing/test_router_caching.py @@ -16,6 +16,7 @@ import litellm from litellm import Router from litellm.caching import RedisCache, RedisClusterCache + ## Scenarios ## 1. 2 models - openai + azure - 1 model group "gpt-3.5-turbo", ## 2. 2 models - openai, azure - 2 diff model groups, 1 caching group diff --git a/tests/logging_callback_tests/create_mock_standard_logging_payload.py b/tests/logging_callback_tests/create_mock_standard_logging_payload.py index 828faba0b04..2fd6a4ffa8a 100644 --- a/tests/logging_callback_tests/create_mock_standard_logging_payload.py +++ b/tests/logging_callback_tests/create_mock_standard_logging_payload.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py b/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py index ce2b05665cd..3e8d59b2992 100644 --- a/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py +++ b/tests/logging_callback_tests/test_bedrock_knowledgebase_hook.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_datadog_llm_obs.py b/tests/logging_callback_tests/test_datadog_llm_obs.py index b192288daec..74f642e6fa3 100644 --- a/tests/logging_callback_tests/test_datadog_llm_obs.py +++ b/tests/logging_callback_tests/test_datadog_llm_obs.py @@ -6,6 +6,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_gcs_pub_sub.py b/tests/logging_callback_tests/test_gcs_pub_sub.py index 6c99e724afe..f4cc9735177 100644 --- a/tests/logging_callback_tests/test_gcs_pub_sub.py +++ b/tests/logging_callback_tests/test_gcs_pub_sub.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_generic_api_callback.py b/tests/logging_callback_tests/test_generic_api_callback.py index a2fdf0b2b04..6984b6fa00c 100644 --- a/tests/logging_callback_tests/test_generic_api_callback.py +++ b/tests/logging_callback_tests/test_generic_api_callback.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_langsmith_unit_test.py b/tests/logging_callback_tests/test_langsmith_unit_test.py index d408c1caefa..155b1f396f6 100644 --- a/tests/logging_callback_tests/test_langsmith_unit_test.py +++ b/tests/logging_callback_tests/test_langsmith_unit_test.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_log_db_redis_services.py b/tests/logging_callback_tests/test_log_db_redis_services.py index 08e45590a8f..fa0c3b595a0 100644 --- a/tests/logging_callback_tests/test_log_db_redis_services.py +++ b/tests/logging_callback_tests/test_log_db_redis_services.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/logging_callback_tests/test_view_request_resp_logs.py b/tests/logging_callback_tests/test_view_request_resp_logs.py index 66463e315c3..ea778a44e67 100644 --- a/tests/logging_callback_tests/test_view_request_resp_logs.py +++ b/tests/logging_callback_tests/test_view_request_resp_logs.py @@ -25,6 +25,7 @@ from litellm.integrations.gcs_bucket.gcs_bucket import ( ) from litellm.types.utils import StandardCallbackDynamicParams + # This is the response payload that GCS would return. mock_response_data = { "id": "chatcmpl-9870a859d6df402795f75dc5fca5b2e0", diff --git a/tests/mcp_tests/test_mcp_logging.py b/tests/mcp_tests/test_mcp_logging.py index 73de99ac894..55b49aa0d29 100644 --- a/tests/mcp_tests/test_mcp_logging.py +++ b/tests/mcp_tests/test_mcp_logging.py @@ -5,6 +5,7 @@ import asyncio from typing import Optional from unittest.mock import AsyncMock, patch + sys.path.insert( 0, os.path.abspath("../../..") ) # Adds the parent directory to the system path diff --git a/tests/mcp_tests/test_mcp_server.py b/tests/mcp_tests/test_mcp_server.py index e931ba4ab9b..6af07585796 100644 --- a/tests/mcp_tests/test_mcp_server.py +++ b/tests/mcp_tests/test_mcp_server.py @@ -18,6 +18,7 @@ from litellm.proxy._types import LiteLLM_ObjectPermissionTable from mcp.types import Tool as MCPTool, CallToolResult, ListToolsResult from mcp.types import TextContent + mcp_server_manager = MCPServerManager() diff --git a/tests/mcp_tests/test_proxy_mcp_e2e.py b/tests/mcp_tests/test_proxy_mcp_e2e.py index 62fc7ad3ff8..2dd57e13d3b 100644 --- a/tests/mcp_tests/test_proxy_mcp_e2e.py +++ b/tests/mcp_tests/test_proxy_mcp_e2e.py @@ -20,6 +20,7 @@ from litellm.proxy.proxy_server import ( initialize, ) + CONFIG_TEMPLATE_PATH = Path("tests/mcp_tests/test_configs/test_config_mcp_e2e.yaml") MCP_SERVER_SCRIPT = Path("tests/mcp_tests/mcp_server.py") PROJECT_ROOT = Path(__file__).resolve().parents[2] diff --git a/tests/ocr_tests/base_ocr_unit_tests.py b/tests/ocr_tests/base_ocr_unit_tests.py index a09158ce18c..ae65efd952d 100644 --- a/tests/ocr_tests/base_ocr_unit_tests.py +++ b/tests/ocr_tests/base_ocr_unit_tests.py @@ -9,6 +9,7 @@ import litellm import os from abc import ABC, abstractmethod + # Test resources TEST_IMAGE_PATH = "test_image_edit.png" # Tiny in-repo PDF served via jsdelivr (sha-pinned, immutable). The arxiv diff --git a/tests/ocr_tests/test_ocr_azure_document_intelligence.py b/tests/ocr_tests/test_ocr_azure_document_intelligence.py index 27428f96a21..7269890b7b6 100644 --- a/tests/ocr_tests/test_ocr_azure_document_intelligence.py +++ b/tests/ocr_tests/test_ocr_azure_document_intelligence.py @@ -110,7 +110,9 @@ class TestAzureDocumentIntelligencePagesParam: model="azure_ai/doc-intelligence/prebuilt-layout", optional_params={"pages": "1-3,5"}, ) - assert f"api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}" in url, url + assert ( + f"api-version={AZURE_DOCUMENT_INTELLIGENCE_API_VERSION}" in url + ), url assert "pages=1-3,5" in url, url assert "/documentintelligence/documentModels/prebuilt-layout:analyze" in url @@ -166,3 +168,4 @@ class TestAzureDocumentIntelligencePagesParam: assert "pages=3,4,5,6,7,8,9" in url assert req.data == {"urlSource": "https://example.com/x.pdf"} + diff --git a/tests/old_proxy_tests/tests/bursty_load_test_completion.py b/tests/old_proxy_tests/tests/bursty_load_test_completion.py index 41944c03aa2..642bd9f14d4 100644 --- a/tests/old_proxy_tests/tests/bursty_load_test_completion.py +++ b/tests/old_proxy_tests/tests/bursty_load_test_completion.py @@ -3,6 +3,7 @@ from openai import AsyncOpenAI from litellm._uuid import uuid import traceback + litellm_client = AsyncOpenAI(api_key="test", base_url="http://0.0.0.0:8000") diff --git a/tests/old_proxy_tests/tests/load_test_embedding_100.py b/tests/old_proxy_tests/tests/load_test_embedding_100.py index bfb4e137e80..8cd4d250249 100644 --- a/tests/old_proxy_tests/tests/load_test_embedding_100.py +++ b/tests/old_proxy_tests/tests/load_test_embedding_100.py @@ -3,6 +3,7 @@ from openai import AsyncOpenAI from litellm._uuid import uuid import traceback + litellm_client = AsyncOpenAI(api_key="test", base_url="http://0.0.0.0:8000") diff --git a/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py b/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py index cde68002a75..2f8455dcbe9 100644 --- a/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py +++ b/tests/old_proxy_tests/tests/test_openai_request_with_traceparent.py @@ -8,6 +8,7 @@ from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator + trace.set_tracer_provider(TracerProvider()) memory_exporter = InMemorySpanExporter() span_processor = SimpleSpanProcessor(memory_exporter) diff --git a/tests/openai_endpoints_tests/test_openai_batches_endpoint.py b/tests/openai_endpoints_tests/test_openai_batches_endpoint.py index c75416edf1d..ad28e8da3df 100644 --- a/tests/openai_endpoints_tests/test_openai_batches_endpoint.py +++ b/tests/openai_endpoints_tests/test_openai_batches_endpoint.py @@ -11,6 +11,7 @@ import sys import time from unittest.mock import patch, MagicMock, AsyncMock + BASE_URL = "http://localhost:4000" # Replace with your actual base URL API_KEY = "sk-1234" # Replace with your actual API key diff --git a/tests/openai_endpoints_tests/test_openai_files_endpoints.py b/tests/openai_endpoints_tests/test_openai_files_endpoints.py index 547bb554835..6be692b278c 100644 --- a/tests/openai_endpoints_tests/test_openai_files_endpoints.py +++ b/tests/openai_endpoints_tests/test_openai_files_endpoints.py @@ -6,6 +6,7 @@ import aiohttp, openai from openai import OpenAI, AsyncOpenAI from typing import Optional, List, Union + BASE_URL = "http://localhost:4000" # Replace with your actual base URL API_KEY = "sk-1234" # Replace with your actual API key diff --git a/tests/otel_tests/test_e2e_model_access.py b/tests/otel_tests/test_e2e_model_access.py index 485031d4e93..7ea75a9d61d 100644 --- a/tests/otel_tests/test_e2e_model_access.py +++ b/tests/otel_tests/test_e2e_model_access.py @@ -5,6 +5,7 @@ import json from httpx import AsyncClient from typing import Any, Optional, List, Literal + # The proxy strips client-supplied `mock_response` unless the calling key or # team has this admin-metadata flag set. See `_UNTRUSTED_ROOT_CONTROL_FIELDS` # in litellm/proxy/litellm_pre_call_utils.py. diff --git a/tests/otel_tests/test_team_member_permissions.py b/tests/otel_tests/test_team_member_permissions.py index 36926b9f525..ddb8b741c45 100644 --- a/tests/otel_tests/test_team_member_permissions.py +++ b/tests/otel_tests/test_team_member_permissions.py @@ -4,13 +4,13 @@ Invalid Permissions: - - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries editing a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries deleting a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries regenerating a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries editing a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries deleting a key with team_id = team_id -> expect to fail. Invalid Permissions + - User tries regenerating a key with team_id = team_id -> expect to fail. Invalid Permissions Valid Permissions: - - User tries calling /key/info with team_id, expect to get valid response + - User tries calling /key/info with team_id, expect to get valid response @@ -26,7 +26,7 @@ Invalid Permissions: - User tries creating a key with team_id = team_id -> expect to fail. Invalid Permissions - - User tries calling /key/info with team_id, expect to get valid response + - User tries calling /key/info with team_id, expect to get valid response diff --git a/tests/pass_through_tests/test_openai_assistants_passthrough.py b/tests/pass_through_tests/test_openai_assistants_passthrough.py index c8e9a9ef0f0..28568005fd6 100644 --- a/tests/pass_through_tests/test_openai_assistants_passthrough.py +++ b/tests/pass_through_tests/test_openai_assistants_passthrough.py @@ -6,6 +6,7 @@ import tempfile from typing_extensions import override from openai import AssistantEventHandler + client = openai.OpenAI(base_url="http://0.0.0.0:4000/openai", api_key="sk-1234") diff --git a/tests/pass_through_tests/test_vertex_ai.py b/tests/pass_through_tests/test_vertex_ai.py index 668ca017f71..73bf03c5000 100644 --- a/tests/pass_through_tests/test_vertex_ai.py +++ b/tests/pass_through_tests/test_vertex_ai.py @@ -12,6 +12,7 @@ import os import pytest import asyncio + # Path to your service account JSON file SERVICE_ACCOUNT_FILE = "path/to/your/service-account.json" diff --git a/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py b/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py index b2187762399..5fc4ecefb33 100644 --- a/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py +++ b/tests/pass_through_unit_tests/base_anthropic_messages_prompt_caching_test.py @@ -22,8 +22,10 @@ sys.path.insert(0, os.path.abspath("../../..")) import pytest import litellm + # Large document for caching tests (needs 1024+ tokens for Claude models) -LARGE_DOCUMENT_FOR_CACHING = """ +LARGE_DOCUMENT_FOR_CACHING = ( + """ This is a comprehensive legal agreement between Party A and Party B. ARTICLE 1: DEFINITIONS @@ -75,7 +77,9 @@ ARTICLE 9: GENERAL PROVISIONS 9.5 Waiver of any provision shall not constitute ongoing waiver. IN WITNESS WHEREOF, the parties have executed this Agreement. -""" * 8 # Repeat to ensure we have enough tokens (need 1024+ for Claude models) +""" + * 8 +) # Repeat to ensure we have enough tokens (need 1024+ for Claude models) class BaseAnthropicMessagesPromptCachingTest(ABC): diff --git a/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py b/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py index 3b1db1c327e..f0cc6985e66 100644 --- a/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py +++ b/tests/proxy_admin_ui_tests/test_route_check_unit_tests.py @@ -14,6 +14,7 @@ import io import os import time + # this file is to test litellm/proxy sys.path.insert( diff --git a/tests/proxy_admin_ui_tests/test_usage_endpoints.py b/tests/proxy_admin_ui_tests/test_usage_endpoints.py index d8ef87f9333..54ad136f082 100644 --- a/tests/proxy_admin_ui_tests/test_usage_endpoints.py +++ b/tests/proxy_admin_ui_tests/test_usage_endpoints.py @@ -1,5 +1,5 @@ """ -Tests the following endpoints used by the UI +Tests the following endpoints used by the UI /global/spend/logs /global/spend/keys @@ -9,7 +9,7 @@ Tests the following endpoints used by the UI For all tests - test the following: -- Response is valid +- Response is valid - Response for Admin User is different from response from Internal User """ diff --git a/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py b/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py index 7f5fcd38c87..48eb7d85ec1 100644 --- a/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py +++ b/tests/proxy_e2e_anthropic_messages_tests/test_claude_agent_sdk.py @@ -12,6 +12,7 @@ import pytest import asyncio from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions + # Test models from test_config.yaml # Note: bedrock-converse-claude-sonnet-4.5 removed temporarily as the Bedrock Converse API # for Claude Sonnet 4.5 may not be available in all regions/accounts diff --git a/tests/proxy_unit_tests/conftest.py b/tests/proxy_unit_tests/conftest.py index 6ff9ffe84c4..a0326f64ed7 100644 --- a/tests/proxy_unit_tests/conftest.py +++ b/tests/proxy_unit_tests/conftest.py @@ -16,6 +16,7 @@ sys.path.insert( import litellm import litellm.proxy.proxy_server + # Top-level assignments of these types are the ones importlib.reload(litellm) # would have effectively reset. We snapshot them at conftest import time and # deep-copy the snapshot back before every test. diff --git a/tests/proxy_unit_tests/test_configs/custom_callbacks.py b/tests/proxy_unit_tests/test_configs/custom_callbacks.py index ac705cd28a9..c7d66c068e9 100644 --- a/tests/proxy_unit_tests/test_configs/custom_callbacks.py +++ b/tests/proxy_unit_tests/test_configs/custom_callbacks.py @@ -93,7 +93,8 @@ class testCustomCallbackProxy(CustomLogger): print("\n\n in custom callback vars my custom logger, ", vars(my_custom_logger)) - print(f""" + print( + f""" Model: {model}, Messages: {messages}, User: {user}, @@ -101,7 +102,8 @@ class testCustomCallbackProxy(CustomLogger): Cost: {cost}, Response: {response} Proxy Metadata: {metadata} - """) + """ + ) return async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time): diff --git a/tests/proxy_unit_tests/test_jwt_key_mapping.py b/tests/proxy_unit_tests/test_jwt_key_mapping.py index 8e3331d17fe..bf1c4a3f6c1 100644 --- a/tests/proxy_unit_tests/test_jwt_key_mapping.py +++ b/tests/proxy_unit_tests/test_jwt_key_mapping.py @@ -27,6 +27,7 @@ from litellm.proxy.management_endpoints.jwt_key_mapping_endpoints import ( from litellm.caching.caching import DualCache from fastapi import HTTPException + # ────────────────────────────────────────────── # Tests: _resolve_jwt_to_virtual_key # ────────────────────────────────────────────── diff --git a/tests/proxy_unit_tests/test_search_api_logging.py b/tests/proxy_unit_tests/test_search_api_logging.py index 3298389083b..71bbe5351a2 100644 --- a/tests/proxy_unit_tests/test_search_api_logging.py +++ b/tests/proxy_unit_tests/test_search_api_logging.py @@ -2,7 +2,7 @@ Test search API logging and cost tracking in proxy. Tests that search API requests are properly logged to LiteLLM_SpendLogs -with correct fields populated (call_type, model, custom_llm_provider, +with correct fields populated (call_type, model, custom_llm_provider, model_group, spend, etc.) """ diff --git a/tests/router_unit_tests/create_mock_standard_logging_payload.py b/tests/router_unit_tests/create_mock_standard_logging_payload.py index 828faba0b04..2fd6a4ffa8a 100644 --- a/tests/router_unit_tests/create_mock_standard_logging_payload.py +++ b/tests/router_unit_tests/create_mock_standard_logging_payload.py @@ -2,6 +2,7 @@ import io import os import sys + sys.path.insert(0, os.path.abspath("../..")) import asyncio diff --git a/tests/router_unit_tests/test_router_handle_error.py b/tests/router_unit_tests/test_router_handle_error.py index 552b0048832..660b3885126 100644 --- a/tests/router_unit_tests/test_router_handle_error.py +++ b/tests/router_unit_tests/test_router_handle_error.py @@ -15,6 +15,7 @@ from collections import defaultdict from dotenv import load_dotenv from unittest.mock import AsyncMock, MagicMock + load_dotenv() diff --git a/tests/search_tests/test_google_pse_search.py b/tests/search_tests/test_google_pse_search.py index 9ce55b428da..21d58a95491 100644 --- a/tests/search_tests/test_google_pse_search.py +++ b/tests/search_tests/test_google_pse_search.py @@ -10,6 +10,7 @@ sys.path.insert(0, os.path.abspath("../..")) from tests.search_tests.base_search_unit_tests import BaseSearchTest + # class TestGooglePSESearch(BaseSearchTest): # """ # Tests for Google PSE Search functionality. diff --git a/tests/store_model_in_db_tests/test_callbacks_in_db.py b/tests/store_model_in_db_tests/test_callbacks_in_db.py index 51545333c65..4a851251a3e 100644 --- a/tests/store_model_in_db_tests/test_callbacks_in_db.py +++ b/tests/store_model_in_db_tests/test_callbacks_in_db.py @@ -1,9 +1,9 @@ """ PROD TEST - DO NOT Delete this Test -e2e test for langfuse callback in DB +e2e test for langfuse callback in DB - Add langfuse callback to DB - with /config/update -- wait 20 seconds for the callback to be loaded into the instance +- wait 20 seconds for the callback to be loaded into the instance - Make a /chat/completions request to the proxy - Check if the request is logged in Langfuse """ diff --git a/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py b/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py index 694806c179b..a4f7f8187c7 100644 --- a/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py +++ b/tests/test_litellm/a2a_protocol/providers/bedrock_agentcore/test_bedrock_agentcore_a2a.py @@ -14,6 +14,7 @@ import json import pytest from unittest.mock import AsyncMock, MagicMock, patch + SAMPLE_ARN = "arn:aws:bedrock-agentcore:us-west-2:123456789:runtime/my_agent" SAMPLE_MODEL = f"bedrock/agentcore/{SAMPLE_ARN}" SAMPLE_PARAMS = { diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py index 23a5d9ec990..af5e2341406 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_base_email.py @@ -30,7 +30,6 @@ def no_invitation_wait(monkeypatch): monkeypatch.setattr(BaseEmailLogger, "_wait_for_invitation_creation", _noop) - @pytest.fixture def base_email_logger(): return BaseEmailLogger() @@ -284,10 +283,7 @@ async def test_send_key_created_email_without_key( mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert "sk-secret-key-456" not in call_args["html_body"] - assert ( - "[Key hidden for security - retrieve from dashboard]" - in call_args["html_body"] - ) + assert "[Key hidden for security - retrieve from dashboard]" in call_args["html_body"] @pytest.mark.asyncio @@ -321,10 +317,7 @@ async def test_send_key_rotated_email_without_key( mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert "sk-secret-rotated-789" not in call_args["html_body"] - assert ( - "[Key hidden for security - retrieve from dashboard]" - in call_args["html_body"] - ) + assert "[Key hidden for security - retrieve from dashboard]" in call_args["html_body"] @pytest.mark.asyncio @@ -378,52 +371,52 @@ async def test_get_invitation_link_creates_new_when_none_exist(base_email_logger """Test that _get_invitation_link creates a new invitation when none exist""" # Mock prisma client with no existing invitation rows mock_prisma = mock.MagicMock() - + # Mock find_many to return empty list (no existing invitations) async def mock_find_many_empty(*args, **kwargs): return [] - + mock_prisma.db.litellm_invitationlink.find_many = mock_find_many_empty - + # Mock the create_invitation_for_user function mock_created_invitation = mock.MagicMock() mock_created_invitation.id = "new-invitation-id" - + with mock.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): with mock.patch( "litellm.proxy.management_helpers.user_invitation.create_invitation_for_user", - return_value=mock_created_invitation, + return_value=mock_created_invitation ) as mock_create_invitation: # Execute result = await base_email_logger._get_invitation_link( user_id="test-user", base_url="http://test.com" ) - + # Verify that create_invitation_for_user was called mock_create_invitation.assert_called_once() call_args = mock_create_invitation.call_args[1] assert call_args["data"].user_id == "test-user" assert call_args["user_api_key_dict"].user_id == "test-user" - + # Verify the returned link uses the new invitation ID assert result == "http://test.com/ui?invitation_id=new-invitation-id" -@pytest.mark.asyncio +@pytest.mark.asyncio async def test_get_invitation_link_uses_existing_when_available(base_email_logger): """Test that _get_invitation_link uses existing invitation when available""" # Mock prisma client with existing invitation row mock_invitation_row = mock.MagicMock() mock_invitation_row.id = "existing-invitation-id" - + mock_prisma = mock.MagicMock() - + # Mock find_many to return existing invitation async def mock_find_many_existing(*args, **kwargs): return [mock_invitation_row] - + mock_prisma.db.litellm_invitationlink.find_many = mock_find_many_existing - + with mock.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): with mock.patch( "litellm.proxy.management_helpers.user_invitation.create_invitation_for_user" @@ -432,10 +425,10 @@ async def test_get_invitation_link_uses_existing_when_available(base_email_logge result = await base_email_logger._get_invitation_link( user_id="test-user", base_url="http://test.com" ) - + # Verify that create_invitation_for_user was NOT called mock_create_invitation.assert_not_called() - + # Verify the returned link uses the existing invitation ID assert result == "http://test.com/ui?invitation_id=existing-invitation-id" @@ -445,33 +438,33 @@ async def test_get_invitation_link_creates_new_when_list_is_none(base_email_logg """Test that _get_invitation_link creates a new invitation when invitation_rows is None""" # Mock prisma client to return None mock_prisma = mock.MagicMock() - + # Mock find_many to return None async def mock_find_many_none(*args, **kwargs): return None - + mock_prisma.db.litellm_invitationlink.find_many = mock_find_many_none - + # Mock the create_invitation_for_user function mock_created_invitation = mock.MagicMock() mock_created_invitation.id = "new-invitation-from-none" - + with mock.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma): with mock.patch( "litellm.proxy.management_helpers.user_invitation.create_invitation_for_user", - return_value=mock_created_invitation, + return_value=mock_created_invitation ) as mock_create_invitation: # Execute result = await base_email_logger._get_invitation_link( user_id="test-user", base_url="http://test.com" ) - + # Verify that create_invitation_for_user was called mock_create_invitation.assert_called_once() call_args = mock_create_invitation.call_args[1] assert call_args["data"].user_id == "test-user" assert call_args["user_api_key_dict"].user_id == "test-user" - + # Verify the returned link uses the new invitation ID assert result == "http://test.com/ui?invitation_id=new-invitation-from-none" @@ -502,15 +495,13 @@ async def test_get_email_params_user_invitation( user_email="test@example.com", ) - assert ( - result.logo_url - == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" - ) + assert result.logo_url == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" assert result.support_contact == "support@berri.ai" assert result.base_url == "http://test.com/ui?invitation_id=test-id" assert result.recipient_email == "test@example.com" + @pytest.fixture def mock_env_vars(monkeypatch): """Set up test environment variables""" @@ -522,74 +513,69 @@ def mock_env_vars(monkeypatch): monkeypatch.setenv("PROXY_BASE_URL", "http://test.com") monkeypatch.setenv("PROXY_API_URL", "https://test.com") - @pytest.mark.asyncio async def test_get_email_params_custom_templates_premium_user(mock_env_vars): """Test that _get_email_params returns correct values with custom templates for premium users""" # Mock premium_user as True with patch("litellm.proxy.proxy_server.premium_user", True): email_logger = BaseEmailLogger() - + # Test invitation email params invitation_params = await email_logger._get_email_params( email_event=EmailEvent.new_user_invitation, user_id="testid", user_email="test@example.com", - event_message="New User Invitation", + event_message="New User Invitation" ) - + assert invitation_params.subject == "Welcome to Test Company!" assert invitation_params.signature == "Best regards,\nTest Company Team" assert invitation_params.logo_url == "https://test-company.com/logo.png" assert invitation_params.support_contact == "support@test-company.com" assert invitation_params.base_url == "http://test.com" - + # Test key created email params key_params = await email_logger._get_email_params( email_event=EmailEvent.virtual_key_created, user_id="testid", user_email="test@example.com", - event_message="API Key Created", + event_message="API Key Created" ) - + assert key_params.subject == "Your Test Company API Key" assert key_params.signature == "Best regards,\nTest Company Team" - @pytest.mark.asyncio async def test_get_email_params_non_premium_user(mock_env_vars): """Test that non-premium users get default templates even when custom ones are provided""" # Mock premium_user as False with patch("litellm.proxy.proxy_server.premium_user", False): email_logger = BaseEmailLogger() - + # Test invitation email params email_params = await email_logger._get_email_params( email_event=EmailEvent.new_user_invitation, user_email="test@example.com", - event_message="New User Invitation", + event_message="New User Invitation" ) - + # Should use default values even though custom values are set in env assert email_params.subject == "LiteLLM: New User Invitation" assert email_params.signature == EMAIL_FOOTER - assert ( - email_params.logo_url - == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" - ) + assert email_params.logo_url == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" assert email_params.support_contact == "support@berri.ai" + # Test key created email params key_params = await email_logger._get_email_params( email_event=EmailEvent.virtual_key_created, user_email="test@example.com", - event_message="API Key Created", + event_message="API Key Created" ) - + assert key_params.subject == "LiteLLM: API Key Created" assert key_params.signature == EMAIL_FOOTER - @pytest.mark.asyncio async def test_get_email_params_default_templates(monkeypatch): """Test that _get_email_params uses default templates when custom ones aren't provided""" @@ -597,28 +583,28 @@ async def test_get_email_params_default_templates(monkeypatch): monkeypatch.delenv("EMAIL_SUBJECT_INVITATION", raising=False) monkeypatch.delenv("EMAIL_SUBJECT_KEY_CREATED", raising=False) monkeypatch.delenv("EMAIL_SIGNATURE", raising=False) - + # Mock premium_user as True (shouldn't matter since no custom values are set) with patch("litellm.proxy.proxy_server.premium_user", True): email_logger = BaseEmailLogger() - + # Test invitation email params with default template invitation_params = await email_logger._get_email_params( email_event=EmailEvent.new_user_invitation, user_email="test@example.com", - event_message="New User Invitation", + event_message="New User Invitation" ) - + assert invitation_params.subject == "LiteLLM: New User Invitation" assert invitation_params.signature == EMAIL_FOOTER - + # Test key created email params with default template key_params = await email_logger._get_email_params( email_event=EmailEvent.virtual_key_created, user_email="test@example.com", - event_message="API Key Created", + event_message="API Key Created" ) - + assert key_params.subject == "LiteLLM: API Key Created" assert key_params.signature == EMAIL_FOOTER @@ -653,10 +639,7 @@ async def test_send_soft_budget_alert_email( call_args = mock_send_email.call_args[1] assert call_args["from_email"] == BaseEmailLogger.DEFAULT_LITELLM_EMAIL assert call_args["to_email"] == ["test@example.com"] - assert ( - call_args["subject"] - == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" - ) + assert call_args["subject"] == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" assert "$100.0" in call_args["html_body"] # soft_budget assert "$105.0" in call_args["html_body"] # spend assert "$200.0" in call_args["html_body"] # max_budget @@ -690,13 +673,13 @@ async def test_send_soft_budget_alert_email_no_max_budget( call_args = mock_send_email.call_args[1] assert "$100.0" in call_args["html_body"] # soft_budget assert "$105.0" in call_args["html_body"] # spend - assert ( - "Maximum Budget" not in call_args["html_body"] - ) # max_budget should not be shown + assert "Maximum Budget" not in call_args["html_body"] # max_budget should not be shown @pytest.mark.asyncio -async def test_budget_alerts_soft_budget_crossed(base_email_logger, mock_send_email): +async def test_budget_alerts_soft_budget_crossed( + base_email_logger, mock_send_email +): """Test that budget_alerts sends email when soft budget is crossed""" user_info = CallInfo( user_id="test_user", @@ -725,14 +708,11 @@ async def test_budget_alerts_soft_budget_crossed(base_email_logger, mock_send_em mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert call_args["to_email"] == ["test@example.com"] - + # Verify cache was set to prevent duplicate alerts mock_cache.async_set_cache.assert_called_once() cache_call_args = mock_cache.async_set_cache.call_args[1] - assert ( - cache_call_args["key"] - == "email_budget_alerts:soft_budget_crossed:test_user" - ) + assert cache_call_args["key"] == "email_budget_alerts:soft_budget_crossed:test_user" assert cache_call_args["value"] == "SENT" assert cache_call_args["ttl"] == EMAIL_BUDGET_ALERT_TTL @@ -786,7 +766,9 @@ async def test_budget_alerts_soft_budget_duplicate_prevention( @pytest.mark.asyncio -async def test_budget_alerts_no_budgets(base_email_logger, mock_send_email): +async def test_budget_alerts_no_budgets( + base_email_logger, mock_send_email +): """Test that budget_alerts returns early when no budgets are set""" user_info = CallInfo( user_id="test_user", @@ -835,10 +817,7 @@ async def test_budget_alerts_uses_token_for_cache_key( # Verify cache key uses token instead of user_id mock_cache.async_set_cache.assert_called_once() cache_call_args = mock_cache.async_set_cache.call_args[1] - assert ( - cache_call_args["key"] - == "email_budget_alerts:soft_budget_crossed:hashed_token_123" - ) + assert cache_call_args["key"] == "email_budget_alerts:soft_budget_crossed:hashed_token_123" @pytest.mark.asyncio @@ -859,9 +838,7 @@ async def test_get_email_params_soft_budget_crossed( ) # Should use default subject template for soft_budget_crossed - assert ( - result.subject == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" - ) + assert result.subject == "LiteLLM: Soft Budget Crossed - Total Soft Budget: $100.0" assert result.recipient_email == "test@example.com" assert result.base_url == "http://test.com" @@ -890,20 +867,16 @@ async def test_budget_alerts_max_budget_alert_crossed( "PROXY_BASE_URL": "http://test.com", }, ): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert call_args["to_email"] == ["test@example.com"] assert "Max Budget Alert" in call_args["subject"] - + mock_cache.async_set_cache.assert_called_once() cache_call_args = mock_cache.async_set_cache.call_args[1] - assert ( - cache_call_args["key"] == "email_budget_alerts:max_budget_alert:test_user" - ) + assert cache_call_args["key"] == "email_budget_alerts:max_budget_alert:test_user" assert cache_call_args["value"] == "SENT" assert cache_call_args["ttl"] == EMAIL_BUDGET_ALERT_TTL @@ -933,15 +906,15 @@ async def test_multi_threshold_sends_crossed_thresholds( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) # spend=80 crosses 50% ($50) and 75% ($75), but not 100% ($100) assert mock_send_email.call_count == 2 # Check cache keys include threshold percentage - cache_keys = [c[1]["key"] for c in mock_cache.async_set_cache.call_args_list] + cache_keys = [ + c[1]["key"] for c in mock_cache.async_set_cache.call_args_list + ] assert "email_budget_alerts:max_budget_alert:50:hashed_key_1" in cache_keys assert "email_budget_alerts:max_budget_alert:75:hashed_key_1" in cache_keys @@ -976,9 +949,7 @@ async def test_multi_threshold_dedup_cache_prevents_resend( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) # Only 75% should fire assert mock_send_email.call_count == 1 @@ -1009,9 +980,7 @@ async def test_multi_threshold_owner_email_auto_included( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) mock_send_email.assert_called_once() to_emails = mock_send_email.call_args[1]["to_email"] @@ -1033,7 +1002,7 @@ async def test_multi_threshold_malformed_keys_skipped( event_group=Litellm_EntityType.KEY, max_budget_alert_emails={ "fifty": ["finance@co.com"], # invalid - "50": ["finance@co.com"], # valid, crossed + "50": ["finance@co.com"], # valid, crossed }, ) @@ -1043,9 +1012,7 @@ async def test_multi_threshold_malformed_keys_skipped( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) # Only the valid "50" threshold should fire assert mock_send_email.call_count == 1 @@ -1074,9 +1041,7 @@ async def test_multi_threshold_empty_emails_only_owner( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) mock_send_email.assert_called_once() to_emails = mock_send_email.call_args[1]["to_email"] @@ -1102,13 +1067,11 @@ async def test_no_map_preserves_old_single_threshold( base_email_logger.internal_usage_cache = mock_cache with mock.patch.dict(os.environ, {"PROXY_BASE_URL": "http://test.com"}): - await base_email_logger.budget_alerts( - type="max_budget_alert", user_info=user_info - ) + await base_email_logger.budget_alerts(type="max_budget_alert", user_info=user_info) mock_send_email.assert_called_once() call_args = mock_send_email.call_args[1] assert call_args["to_email"] == ["test@example.com"] # Old path cache key has no threshold percentage cache_key = mock_cache.async_set_cache.call_args[1]["key"] - assert cache_key == "email_budget_alerts:max_budget_alert:test_user" + assert cache_key == "email_budget_alerts:max_budget_alert:test_user" \ No newline at end of file diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py index dfb51c8bbc8..b07216921eb 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_resend_email.py @@ -87,7 +87,7 @@ async def test_send_email_success(mock_env_vars): async def test_send_email_missing_api_key(): # Remove the API key from environment before initializing logger original_key = os.environ.pop("RESEND_API_KEY", None) - + try: # Initialize the logger after removing the API key logger = ResendEmailLogger() @@ -104,19 +104,16 @@ async def test_send_email_missing_api_key(): mock_response.raise_for_status.return_value = None mock_response.status_code = 200 mock_response.json.return_value = {"id": "test_email_id"} - + mock_async_client = mock.AsyncMock() mock_async_client.post.return_value = mock_response - + # Directly inject the mock client to bypass any caching logger.async_httpx_client = mock_async_client # Send email await logger.send_email( - from_email=from_email, - to_email=to_email, - subject=subject, - html_body=html_body, + from_email=from_email, to_email=to_email, subject=subject, html_body=html_body ) # Verify the HTTP client was called with None as the API key diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py index d247b02074d..40439a78a49 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/send_emails/test_sendgrid_email.py @@ -32,12 +32,12 @@ def mock_env_vars(): # Store original values original_api_key = os.environ.get("SENDGRID_API_KEY") original_sender_email = os.environ.get("SENDGRID_SENDER_EMAIL") - + # Set test API key and remove SENDGRID_SENDER_EMAIL to ensure isolation os.environ["SENDGRID_API_KEY"] = "test_api_key" if "SENDGRID_SENDER_EMAIL" in os.environ: del os.environ["SENDGRID_SENDER_EMAIL"] - + try: yield finally: @@ -46,7 +46,7 @@ def mock_env_vars(): os.environ["SENDGRID_API_KEY"] = original_api_key elif "SENDGRID_API_KEY" in os.environ: del os.environ["SENDGRID_API_KEY"] - + if original_sender_email is not None: os.environ["SENDGRID_SENDER_EMAIL"] = original_sender_email diff --git a/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py b/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py index 9743e7bedc9..b160ca5130c 100644 --- a/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py +++ b/tests/test_litellm/enterprise/enterprise_callbacks/test_callback_controls.py @@ -18,282 +18,168 @@ from litellm.types.utils import StandardCallbackDynamicParams class TestEnterpriseCallbackControls: - + @pytest.fixture def mock_premium_user(self): """Fixture to mock premium user check as True""" - with patch.object( - EnterpriseCallbackControls, - "_should_allow_dynamic_callback_disabling", - return_value=True, - ): + with patch.object(EnterpriseCallbackControls, '_should_allow_dynamic_callback_disabling', return_value=True): yield - - @pytest.fixture + + @pytest.fixture def mock_non_premium_user(self): """Fixture to mock premium user check as False""" - with patch.object( - EnterpriseCallbackControls, - "_should_allow_dynamic_callback_disabling", - return_value=False, - ): + with patch.object(EnterpriseCallbackControls, '_should_allow_dynamic_callback_disabling', return_value=False): yield @pytest.fixture def mock_request_headers(self): """Fixture to mock get_proxy_server_request_headers""" - with patch( - "enterprise.litellm_enterprise.enterprise_callbacks.callback_controls.get_proxy_server_request_headers" - ) as mock_headers: + with patch('enterprise.litellm_enterprise.enterprise_callbacks.callback_controls.get_proxy_server_request_headers') as mock_headers: yield mock_headers - def test_callback_disabled_langfuse_string( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_langfuse_string(self, mock_premium_user, mock_request_headers): """Test that 'langfuse' string callback is disabled when specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is True - def test_callback_disabled_langfuse_customlogger( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_langfuse_customlogger(self, mock_premium_user, mock_request_headers): """Test that LangfusePromptManagement CustomLogger instance is disabled when 'langfuse' specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + langfuse_logger = LangfusePromptManagement() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - langfuse_logger, litellm_params, standard_callback_dynamic_params - ) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically(langfuse_logger, litellm_params, standard_callback_dynamic_params) assert result is True - def test_callback_disabled_s3_v2_string( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_s3_v2_string(self, mock_premium_user, mock_request_headers): """Test that 's3_v2' string callback is disabled when specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "s3_v2"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "s3_v2", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) assert result is True - def test_callback_disabled_s3_v2_customlogger( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_s3_v2_customlogger(self, mock_premium_user, mock_request_headers): """Test that S3Logger CustomLogger instance is disabled when 's3_v2' specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "s3_v2"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock S3Logger to avoid async initialization issues - with patch("litellm.integrations.s3_v2.S3Logger.__init__", return_value=None): + with patch('litellm.integrations.s3_v2.S3Logger.__init__', return_value=None): s3_logger = S3Logger() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - s3_logger, litellm_params, standard_callback_dynamic_params - ) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically(s3_logger, litellm_params, standard_callback_dynamic_params) assert result is True - def test_callback_disabled_datadog_string( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_datadog_string(self, mock_premium_user, mock_request_headers): """Test that 'datadog' string callback is disabled when specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "datadog"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "datadog", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) assert result is True - def test_callback_disabled_datadog_customlogger( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_datadog_customlogger(self, mock_premium_user, mock_request_headers): """Test that DataDogLogger CustomLogger instance is disabled when 'datadog' specified in headers""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "datadog"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock DataDogLogger to avoid async initialization issues - with patch( - "litellm.integrations.datadog.datadog.DataDogLogger.__init__", - return_value=None, - ): + with patch('litellm.integrations.datadog.datadog.DataDogLogger.__init__', return_value=None): datadog_logger = DataDogLogger() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - datadog_logger, litellm_params, standard_callback_dynamic_params - ) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically(datadog_logger, litellm_params, standard_callback_dynamic_params) assert result is True def test_multiple_callbacks_disabled(self, mock_premium_user, mock_request_headers): """Test that multiple callbacks can be disabled with comma-separated list""" - mock_request_headers.return_value = { - X_LITELLM_DISABLE_CALLBACKS: "langfuse,datadog,s3_v2" - } + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse,datadog,s3_v2"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Test each callback is disabled - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "datadog", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "s3_v2", litellm_params, standard_callback_dynamic_params - ) - is True - ) - + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) is True + # Test non-disabled callback is not disabled - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "prometheus", litellm_params, standard_callback_dynamic_params - ) - is False - ) + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("prometheus", litellm_params, standard_callback_dynamic_params) is False - def test_callback_not_disabled_when_not_in_list( - self, mock_premium_user, mock_request_headers - ): + def test_callback_not_disabled_when_not_in_list(self, mock_premium_user, mock_request_headers): """Test that callbacks not in the disabled list are not disabled""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "datadog", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) assert result is False - def test_callback_not_disabled_when_no_header( - self, mock_premium_user, mock_request_headers - ): + def test_callback_not_disabled_when_no_header(self, mock_premium_user, mock_request_headers): """Test that callbacks are not disabled when the header is not present""" mock_request_headers.return_value = {} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is False - def test_callback_not_disabled_when_header_none( - self, mock_premium_user, mock_request_headers - ): + def test_callback_not_disabled_when_header_none(self, mock_premium_user, mock_request_headers): """Test that callbacks are not disabled when the header value is None""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: None} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is False - def test_non_premium_user_cannot_disable_callbacks( - self, mock_non_premium_user, mock_request_headers - ): + def test_non_premium_user_cannot_disable_callbacks(self, mock_non_premium_user, mock_request_headers): """Test that non-premium users cannot disable callbacks even with the header""" mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is False - def test_case_insensitive_callback_matching( - self, mock_premium_user, mock_request_headers - ): + def test_case_insensitive_callback_matching(self, mock_premium_user, mock_request_headers): """Test that callback matching is case insensitive""" - mock_request_headers.return_value = { - X_LITELLM_DISABLE_CALLBACKS: "LANGFUSE,DataDog" - } + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "LANGFUSE,DataDog"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Test lowercase callbacks are disabled - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "datadog", litellm_params, standard_callback_dynamic_params - ) - is True - ) + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True - def test_whitespace_handling_in_disabled_callbacks( - self, mock_premium_user, mock_request_headers - ): + def test_whitespace_handling_in_disabled_callbacks(self, mock_premium_user, mock_request_headers): """Test that whitespace around callback names is handled correctly""" - mock_request_headers.return_value = { - X_LITELLM_DISABLE_CALLBACKS: " langfuse , datadog , s3_v2 " - } + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: " langfuse , datadog , s3_v2 "} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() + + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) is True - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "datadog", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "s3_v2", litellm_params, standard_callback_dynamic_params - ) - is True - ) - - def test_custom_logger_not_in_registry( - self, mock_premium_user, mock_request_headers - ): + def test_custom_logger_not_in_registry(self, mock_premium_user, mock_request_headers): """Test that CustomLogger not in registry is not disabled""" - mock_request_headers.return_value = { - X_LITELLM_DISABLE_CALLBACKS: "unknown_logger" - } + mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "unknown_logger"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Create a mock CustomLogger that's not in the registry class UnknownLogger(CustomLogger): pass - + unknown_logger = UnknownLogger() - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - unknown_logger, litellm_params, standard_callback_dynamic_params - ) + result = EnterpriseCallbackControls.is_callback_disabled_dynamically(unknown_logger, litellm_params, standard_callback_dynamic_params) assert result is False def test_exception_handling(self, mock_premium_user, mock_request_headers): @@ -302,64 +188,32 @@ class TestEnterpriseCallbackControls: mock_request_headers.side_effect = Exception("Test exception") litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is False - def test_callback_disabled_via_request_body_langfuse( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_via_request_body_langfuse(self, mock_premium_user, mock_request_headers): """Test that callbacks can be disabled via request body litellm_disabled_callbacks""" mock_request_headers.return_value = {} # No headers litellm_params = {"proxy_server_request": {"url": "test"}} - standard_callback_dynamic_params = StandardCallbackDynamicParams( - litellm_disabled_callbacks=["langfuse"] - ) - - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + standard_callback_dynamic_params = StandardCallbackDynamicParams(litellm_disabled_callbacks=["langfuse"]) + + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is True - def test_callback_disabled_via_request_body_multiple( - self, mock_premium_user, mock_request_headers - ): + def test_callback_disabled_via_request_body_multiple(self, mock_premium_user, mock_request_headers): """Test that multiple callbacks can be disabled via request body""" mock_request_headers.return_value = {} # No headers litellm_params = {"proxy_server_request": {"url": "test"}} - standard_callback_dynamic_params = StandardCallbackDynamicParams( - litellm_disabled_callbacks=["langfuse", "datadog", "s3_v2"] - ) - + standard_callback_dynamic_params = StandardCallbackDynamicParams(litellm_disabled_callbacks=["langfuse", "datadog", "s3_v2"]) + # Test each callback is disabled - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "datadog", litellm_params, standard_callback_dynamic_params - ) - is True - ) - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "s3_v2", litellm_params, standard_callback_dynamic_params - ) - is True - ) - + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("datadog", litellm_params, standard_callback_dynamic_params) is True + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("s3_v2", litellm_params, standard_callback_dynamic_params) is True + # Test non-disabled callback is not disabled - assert ( - EnterpriseCallbackControls.is_callback_disabled_dynamically( - "prometheus", litellm_params, standard_callback_dynamic_params - ) - is False - ) + assert EnterpriseCallbackControls.is_callback_disabled_dynamically("prometheus", litellm_params, standard_callback_dynamic_params) is False def test_admin_can_disable_dynamic_callback_disabling(self, mock_request_headers): """ @@ -369,13 +223,11 @@ class TestEnterpriseCallbackControls: mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock litellm.allow_dynamic_callback_disabling set to False - with patch("litellm.allow_dynamic_callback_disabling", False): - with patch("litellm.proxy.proxy_server.premium_user", True): - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + with patch('litellm.allow_dynamic_callback_disabling', False): + with patch('litellm.proxy.proxy_server.premium_user', True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is False def test_admin_can_enable_dynamic_callback_disabling(self, mock_request_headers): @@ -386,18 +238,14 @@ class TestEnterpriseCallbackControls: mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # Mock litellm.allow_dynamic_callback_disabling set to True - with patch("litellm.allow_dynamic_callback_disabling", True): - with patch("litellm.proxy.proxy_server.premium_user", True): - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + with patch('litellm.allow_dynamic_callback_disabling', True): + with patch('litellm.proxy.proxy_server.premium_user', True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is True - def test_default_admin_setting_allows_dynamic_callback_disabling( - self, mock_request_headers - ): + def test_default_admin_setting_allows_dynamic_callback_disabling(self, mock_request_headers): """ Test that when allow_dynamic_callback_disabling is not set, it defaults to True and allows dynamic callback disabling for premium users @@ -405,10 +253,8 @@ class TestEnterpriseCallbackControls: mock_request_headers.return_value = {X_LITELLM_DISABLE_CALLBACKS: "langfuse"} litellm_params = {"proxy_server_request": {"url": "test"}} standard_callback_dynamic_params = StandardCallbackDynamicParams() - + # litellm.allow_dynamic_callback_disabling should default to True - with patch("litellm.proxy.proxy_server.premium_user", True): - result = EnterpriseCallbackControls.is_callback_disabled_dynamically( - "langfuse", litellm_params, standard_callback_dynamic_params - ) + with patch('litellm.proxy.proxy_server.premium_user', True): + result = EnterpriseCallbackControls.is_callback_disabled_dynamically("langfuse", litellm_params, standard_callback_dynamic_params) assert result is True diff --git a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py index 180041b5f71..6e9c3c0354b 100644 --- a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py +++ b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_input_file_id.py @@ -16,18 +16,13 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( _is_base64_encoded_unified_file_id, ) + DECODED_UNIFIED_INPUT_FILE_ID = "litellm_proxy:application/octet-stream;unified_id,test-uuid;target_model_names,azure-gpt-4" -B64_UNIFIED_INPUT_FILE_ID = ( - base64.urlsafe_b64encode(DECODED_UNIFIED_INPUT_FILE_ID.encode()) - .decode() - .rstrip("=") -) +B64_UNIFIED_INPUT_FILE_ID = base64.urlsafe_b64encode(DECODED_UNIFIED_INPUT_FILE_ID.encode()).decode().rstrip("=") RAW_INPUT_FILE_ID = "file-raw-provider-abc123" DECODED_UNIFIED_BATCH_ID = "litellm_proxy;model_id:model-xyz;llm_batch_id:batch-123" -B64_UNIFIED_BATCH_ID = ( - base64.urlsafe_b64encode(DECODED_UNIFIED_BATCH_ID.encode()).decode().rstrip("=") -) +B64_UNIFIED_BATCH_ID = base64.urlsafe_b64encode(DECODED_UNIFIED_BATCH_ID.encode()).decode().rstrip("=") @pytest.mark.asyncio @@ -60,16 +55,10 @@ async def test_should_resolve_raw_input_file_id_to_unified(): mock_managed_file.unified_file_id = B64_UNIFIED_INPUT_FILE_ID mock_prisma = MagicMock() - mock_prisma.db.litellm_managedobjecttable.find_first = AsyncMock( - return_value=mock_db_object - ) - mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock( - return_value=mock_managed_file - ) + mock_prisma.db.litellm_managedobjecttable.find_first = AsyncMock(return_value=mock_db_object) + mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock(return_value=mock_managed_file) - from litellm.proxy.openai_files_endpoints.common_utils import ( - get_batch_from_database, - ) + from litellm.proxy.openai_files_endpoints.common_utils import get_batch_from_database _, response = await get_batch_from_database( batch_id=B64_UNIFIED_BATCH_ID, diff --git a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py index c80b8a848ca..420f5f9789c 100644 --- a/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py +++ b/tests/test_litellm/enterprise/proxy/test_batch_retrieve_returns_unified_input_file_id.py @@ -93,9 +93,7 @@ async def test_should_preserve_already_managed_input_file_id(): unified_batch_id = "bGl0ZWxsbV9wcm94eTpiYXRjaF9pZA" decoded_unified = "litellm_proxy:application/octet-stream;unified_id,test-123" - base64_input_file_id = ( - base64.urlsafe_b64encode(decoded_unified.encode()).decode().rstrip("=") - ) + base64_input_file_id = base64.urlsafe_b64encode(decoded_unified.encode()).decode().rstrip("=") batch_data = { "id": "batch-raw-123", diff --git a/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py b/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py index 34c8c9c5c5d..a9bf33a21ac 100644 --- a/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py +++ b/tests/test_litellm/enterprise/proxy/test_enterprise_routes.py @@ -14,77 +14,63 @@ import pytest def test_enterprise_routes_all_imports_exist(): """ Validate that all relative imports in enterprise_routes.py exist in the filesystem. - + This catches any import errors from moved/deleted modules without hardcoding specific module names. Works by checking that imported files actually exist. """ # Path to the enterprise_routes.py source file enterprise_routes_path = os.path.join( os.path.dirname(__file__), - "..", - "..", - "..", - "..", - "enterprise", - "litellm_enterprise", - "proxy", - "enterprise_routes.py", + "..", "..", "..", "..", + "enterprise", "litellm_enterprise", "proxy", "enterprise_routes.py" ) - + enterprise_routes_path = os.path.normpath(enterprise_routes_path) enterprise_proxy_dir = os.path.dirname(enterprise_routes_path) - + if not os.path.exists(enterprise_routes_path): pytest.skip(f"Enterprise routes file not found at {enterprise_routes_path}") - + # Read and parse the source file with open(enterprise_routes_path, "r") as f: source_code = f.read() - + try: tree = ast.parse(source_code) except SyntaxError as e: pytest.fail(f"Syntax error in enterprise_routes.py: {e}") - + # Check all relative imports missing_imports = [] - + for node in ast.walk(tree): if isinstance(node, ast.ImportFrom): # level > 0 means it's a relative import (. or .. etc) if node.level and node.level > 0: module = node.module or "" - + # Convert relative import to file path # e.g., "audit_logging_endpoints" -> "audit_logging_endpoints.py" # e.g., "vector_stores.endpoints" -> "vector_stores/endpoints.py" module_path = module.replace(".", os.sep) if module else "" - + # Check both .py file and package directory - file_path = ( - os.path.join(enterprise_proxy_dir, module_path + ".py") - if module_path - else None - ) - package_path = ( - os.path.join(enterprise_proxy_dir, module_path, "__init__.py") - if module_path - else None - ) - + file_path = os.path.join(enterprise_proxy_dir, module_path + ".py") if module_path else None + package_path = os.path.join(enterprise_proxy_dir, module_path, "__init__.py") if module_path else None + # If module is empty (e.g., "from . import something"), skip check if not module: continue - + file_exists = file_path and os.path.exists(file_path) package_exists = package_path and os.path.exists(package_path) - + if not file_exists and not package_exists: missing_imports.append( f"Line {node.lineno}: Cannot find '.{module}' " f"(checked: {file_path} and {package_path})" ) - + if missing_imports: error_msg = "Found imports in enterprise_routes.py that don't exist:\n" error_msg += "\n".join(missing_imports) diff --git a/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py b/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py index 3c7aace7d31..852077dcf0c 100644 --- a/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py +++ b/tests/test_litellm/enterprise/proxy/test_file_deletion_blocking.py @@ -61,7 +61,7 @@ def _make_managed_files_instance_with_batches( ): """ Create a _PROXY_LiteLLMManagedFiles instance with mocked DB and batches. - + Args: file_id: The unified file ID batches: List of batch records to return from DB @@ -79,7 +79,7 @@ def _make_managed_files_instance_with_batches( # Mock prisma mock_prisma = MagicMock() - + # Mock file table queries mock_prisma.db.litellm_managedfiletable.find_first = AsyncMock( return_value=mock_file_record @@ -87,7 +87,7 @@ def _make_managed_files_instance_with_batches( mock_prisma.db.litellm_managedfiletable.delete = AsyncMock( return_value=mock_file_record ) - + # Mock batch/object table queries mock_prisma.db.litellm_managedobjecttable.find_many = AsyncMock( return_value=batches @@ -95,13 +95,11 @@ def _make_managed_files_instance_with_batches( # Mock cache mock_cache = MagicMock() - mock_cache.async_get_cache = AsyncMock( - return_value={ - "unified_file_id": file_id, - "model_mappings": {"model-123": "provider-file-abc"}, - "flat_model_file_ids": ["provider-file-abc"], - } - ) + mock_cache.async_get_cache = AsyncMock(return_value={ + "unified_file_id": file_id, + "model_mappings": {"model-123": "provider-file-abc"}, + "flat_model_file_ids": ["provider-file-abc"], + }) mock_cache.async_set_cache = AsyncMock() instance = _PROXY_LiteLLMManagedFiles( @@ -119,17 +117,17 @@ def test_is_batch_polling_enabled_when_job_registered(): from litellm_enterprise.proxy.hooks.managed_files import ( _PROXY_LiteLLMManagedFiles, ) - + instance = _PROXY_LiteLLMManagedFiles( internal_usage_cache=MagicMock(), prisma_client=MagicMock(), ) - + # Mock scheduler with registered job mock_scheduler = MagicMock() mock_job = MagicMock() mock_scheduler.get_job.return_value = mock_job - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): assert instance._is_batch_polling_enabled() is True @@ -139,16 +137,16 @@ def test_is_batch_polling_disabled_when_job_not_registered(): from litellm_enterprise.proxy.hooks.managed_files import ( _PROXY_LiteLLMManagedFiles, ) - + instance = _PROXY_LiteLLMManagedFiles( internal_usage_cache=MagicMock(), prisma_client=MagicMock(), ) - + # Mock scheduler without registered job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = None - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): assert instance._is_batch_polling_enabled() is False @@ -158,12 +156,12 @@ def test_is_batch_polling_disabled_when_no_scheduler(): from litellm_enterprise.proxy.hooks.managed_files import ( _PROXY_LiteLLMManagedFiles, ) - + instance = _PROXY_LiteLLMManagedFiles( internal_usage_cache=MagicMock(), prisma_client=MagicMock(), ) - + with patch("litellm.proxy.proxy_server.scheduler", None): assert instance._is_batch_polling_enabled() is False @@ -176,28 +174,26 @@ async def test_get_batches_referencing_file_finds_batch_with_input_file(): """Test finding a batch that references the file as input_file_id.""" unified_file_id = _make_unified_file_id("file-input-123") unified_batch_id = _make_unified_batch_id("batch-123") - + batch_file_object = { "id": "batch-123", "input_file_id": unified_file_id, # Batch references this file "status": "validating", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="validating", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - - referencing_batches = await managed_files._get_batches_referencing_file( - unified_file_id - ) - + + referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) + assert len(referencing_batches) == 1 assert referencing_batches[0]["batch_id"] == unified_batch_id assert referencing_batches[0]["status"] == "validating" @@ -208,29 +204,27 @@ async def test_get_batches_referencing_file_finds_batch_with_output_file(): """Test finding a batch that references the file as output_file_id.""" unified_file_id = _make_unified_file_id("file-output-456") unified_batch_id = _make_unified_batch_id("batch-456") - + batch_file_object = { "id": "batch-456", "input_file_id": "file-input-different", "output_file_id": unified_file_id, # Batch references this file "status": "in_progress", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="in_progress", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - - referencing_batches = await managed_files._get_batches_referencing_file( - unified_file_id - ) - + + referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) + assert len(referencing_batches) == 1 assert referencing_batches[0]["status"] == "in_progress" @@ -240,29 +234,27 @@ async def test_get_batches_referencing_file_ignores_terminal_batches(): """Test that batches in terminal states are not returned.""" unified_file_id = _make_unified_file_id("file-123") unified_batch_id = _make_unified_batch_id("batch-completed") - + batch_file_object = { "id": "batch-completed", "input_file_id": unified_file_id, "status": "completed", } - + # Batch is in terminal state in DB batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="completed", # Terminal state file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[], # Query returns no batches (terminal states filtered out) ) - - referencing_batches = await managed_files._get_batches_referencing_file( - unified_file_id - ) - + + referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) + assert len(referencing_batches) == 0 @@ -270,36 +262,26 @@ async def test_get_batches_referencing_file_ignores_terminal_batches(): async def test_get_batches_referencing_file_finds_multiple_batches(): """Test finding multiple batches referencing the same file.""" unified_file_id = _make_unified_file_id("file-shared") - + batch1 = _make_batch_db_record( unified_object_id=_make_unified_batch_id("batch-1"), status="validating", - file_object={ - "id": "batch-1", - "input_file_id": unified_file_id, - "status": "validating", - }, + file_object={"id": "batch-1", "input_file_id": unified_file_id, "status": "validating"}, ) - + batch2 = _make_batch_db_record( unified_object_id=_make_unified_batch_id("batch-2"), status="in_progress", - file_object={ - "id": "batch-2", - "input_file_id": unified_file_id, - "status": "in_progress", - }, + file_object={"id": "batch-2", "input_file_id": unified_file_id, "status": "in_progress"}, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch1, batch2], ) - - referencing_batches = await managed_files._get_batches_referencing_file( - unified_file_id - ) - + + referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) + assert len(referencing_batches) == 2 statuses = [b["status"] for b in referencing_batches] assert "validating" in statuses @@ -318,32 +300,32 @@ async def test_file_deletion_blocked_when_batch_polling_enabled_and_batch_refere """ unified_file_id = _make_unified_file_id("file-to-delete") unified_batch_id = _make_unified_batch_id("batch-active") - + batch_file_object = { "id": "batch-active", "input_file_id": unified_file_id, "status": "validating", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="validating", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - + # Mock scheduler with registered batch cost job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() # Job exists - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files._check_file_deletion_allowed(unified_file_id) - + assert exc_info.value.status_code == 400 error_detail = exc_info.value.detail assert "Cannot delete file" in error_detail @@ -360,28 +342,28 @@ async def test_file_deletion_allowed_when_batch_polling_disabled(): """ unified_file_id = _make_unified_file_id("file-to-delete") unified_batch_id = _make_unified_batch_id("batch-active") - + batch_file_object = { "id": "batch-active", "input_file_id": unified_file_id, "status": "validating", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="validating", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - + # Mock scheduler without registered job (batch cost tracking disabled) mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = None - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): # Should not raise an exception await managed_files._check_file_deletion_allowed(unified_file_id) @@ -394,16 +376,16 @@ async def test_file_deletion_allowed_when_no_batches_reference_file(): even when batch cost tracking is enabled. """ unified_file_id = _make_unified_file_id("file-to-delete") - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[], # No batches reference this file ) - + # Mock scheduler with registered job (batch cost tracking enabled) mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): # Should not raise an exception await managed_files._check_file_deletion_allowed(unified_file_id) @@ -416,32 +398,32 @@ async def test_afile_delete_calls_check_deletion_allowed(): """ unified_file_id = _make_unified_file_id("file-to-delete") unified_batch_id = _make_unified_batch_id("batch-active") - + batch_file_object = { "id": "batch-active", "input_file_id": unified_file_id, "status": "in_progress", } - + batch_record = _make_batch_db_record( unified_object_id=unified_batch_id, status="in_progress", file_object=batch_file_object, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch_record], ) - + # Mock llm_router mock_router = MagicMock() mock_router.afile_delete = AsyncMock() - + # Mock scheduler with registered job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files.afile_delete( @@ -449,7 +431,7 @@ async def test_afile_delete_calls_check_deletion_allowed(): litellm_parent_otel_span=None, llm_router=mock_router, ) - + # Should raise error before calling router delete assert exc_info.value.status_code == 400 mock_router.afile_delete.assert_not_called() @@ -462,7 +444,7 @@ async def test_database_limit_respected(): This is a performance optimization - we only fetch what we need. """ unified_file_id = _make_unified_file_id("file-shared") - + # Create exactly 10 batches (what DB will return with take=10) ten_batches = [] for i in range(10): @@ -472,32 +454,30 @@ async def test_database_limit_respected(): file_object={ "id": f"batch-{i}", "input_file_id": unified_file_id, - "status": "validating", + "status": "validating" }, ) ten_batches.append(batch) - + # Mock will return only 10 batches (as DB would with take=10) managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=ten_batches, ) - - referencing_batches = await managed_files._get_batches_referencing_file( - unified_file_id - ) - + + referencing_batches = await managed_files._get_batches_referencing_file(unified_file_id) + # Should return all 10 that reference the file assert len(referencing_batches) == 10 - + # Verify error message handles "10+" case (since we got exactly 10, might be more in DB) mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files._check_file_deletion_allowed(unified_file_id) - + error_detail = exc_info.value.detail # When we get exactly 10 matches, show "10+" to indicate there might be more assert "10+ batch(es)" in error_detail @@ -511,40 +491,32 @@ async def test_error_message_includes_batch_details(): unified_file_id = _make_unified_file_id("file-to-delete") batch1_id = _make_unified_batch_id("batch-1") batch2_id = _make_unified_batch_id("batch-2") - + batch1 = _make_batch_db_record( unified_object_id=batch1_id, status="validating", - file_object={ - "id": "batch-1", - "input_file_id": unified_file_id, - "status": "validating", - }, + file_object={"id": "batch-1", "input_file_id": unified_file_id, "status": "validating"}, ) - + batch2 = _make_batch_db_record( unified_object_id=batch2_id, status="in_progress", - file_object={ - "id": "batch-2", - "output_file_id": unified_file_id, - "status": "in_progress", - }, + file_object={"id": "batch-2", "output_file_id": unified_file_id, "status": "in_progress"}, ) - + managed_files = _make_managed_files_instance_with_batches( file_id=unified_file_id, batches=[batch1, batch2], ) - + # Mock scheduler with registered job mock_scheduler = MagicMock() mock_scheduler.get_job.return_value = MagicMock() - + with patch("litellm.proxy.proxy_server.scheduler", mock_scheduler): with pytest.raises(HTTPException) as exc_info: await managed_files._check_file_deletion_allowed(unified_file_id) - + error_detail = exc_info.value.detail assert "2 batch(es)" in error_detail assert "validating" in error_detail diff --git a/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py b/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py index d4855d5b130..9526304aff0 100644 --- a/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py +++ b/tests/test_litellm/enterprise/proxy/test_managed_files_hook.py @@ -110,9 +110,10 @@ async def test_should_pass_credentials_to_afile_retrieve(): mock_afile_retrieve = AsyncMock(return_value=_make_file_object("file-output-abc")) - with ( - patch("litellm.afile_retrieve", mock_afile_retrieve), - patch("litellm.proxy.proxy_server.llm_router", mock_router), + with patch( + "litellm.afile_retrieve", mock_afile_retrieve + ), patch( + "litellm.proxy.proxy_server.llm_router", mock_router ): await managed_files.async_post_call_success_hook( data={}, @@ -127,9 +128,7 @@ async def test_should_pass_credentials_to_afile_retrieve(): f"afile_retrieve must receive api_key from router credentials. " f"Got kwargs: {call_kwargs.kwargs}" ) - assert ( - call_kwargs.kwargs.get("api_base") == "https://my-azure.openai.azure.com/" - ), ( + assert call_kwargs.kwargs.get("api_base") == "https://my-azure.openai.azure.com/", ( f"afile_retrieve must receive api_base from router credentials. " f"Got kwargs: {call_kwargs.kwargs}" ) @@ -151,9 +150,10 @@ async def test_should_fallback_when_no_router(): mock_afile_retrieve = AsyncMock(return_value=_make_file_object("file-output-abc")) - with ( - patch("litellm.afile_retrieve", mock_afile_retrieve), - patch("litellm.proxy.proxy_server.llm_router", None), + with patch( + "litellm.afile_retrieve", mock_afile_retrieve + ), patch( + "litellm.proxy.proxy_server.llm_router", None ): await managed_files.async_post_call_success_hook( data={}, diff --git a/tests/test_litellm/google_genai/test_google_genai_adapter.py b/tests/test_litellm/google_genai/test_google_genai_adapter.py index 90ef89699ea..f21564546a8 100644 --- a/tests/test_litellm/google_genai/test_google_genai_adapter.py +++ b/tests/test_litellm/google_genai/test_google_genai_adapter.py @@ -2,7 +2,6 @@ """ Test to verify the Google GenAI generate_content adapter functionality """ - import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py b/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py index 8fc3eca9adf..da56b094d95 100644 --- a/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py +++ b/tests/test_litellm/google_genai/test_google_genai_adapter_fixes.py @@ -2,7 +2,6 @@ """ Test to verify the Google GenAI adapter fixes """ - import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_handler.py b/tests/test_litellm/google_genai/test_google_genai_handler.py index da1c0bb611b..0dc218d297b 100644 --- a/tests/test_litellm/google_genai/test_google_genai_handler.py +++ b/tests/test_litellm/google_genai/test_google_genai_handler.py @@ -2,7 +2,6 @@ """ Test to verify the Google GenAI generate_content handler functionality """ - import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_main.py b/tests/test_litellm/google_genai/test_google_genai_main.py index 1801ee10f30..5854e4b55af 100644 --- a/tests/test_litellm/google_genai/test_google_genai_main.py +++ b/tests/test_litellm/google_genai/test_google_genai_main.py @@ -2,7 +2,6 @@ """ Test to verify the Google GenAI generate_content adapter functionality """ - import json import os import sys diff --git a/tests/test_litellm/google_genai/test_google_genai_transformation.py b/tests/test_litellm/google_genai/test_google_genai_transformation.py index 691e5472997..908a68110fd 100644 --- a/tests/test_litellm/google_genai/test_google_genai_transformation.py +++ b/tests/test_litellm/google_genai/test_google_genai_transformation.py @@ -2,7 +2,6 @@ """ Test to verify the Google GenAI transformation logic for generateContent parameters """ - import os import sys diff --git a/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py b/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py index 0767b83e8eb..fdf56aedbc9 100644 --- a/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py +++ b/tests/test_litellm/integrations/arize/test_arize_otel_coexistence.py @@ -20,6 +20,7 @@ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanE from litellm.integrations.opentelemetry import OpenTelemetry, OpenTelemetryConfig + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py b/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py index 78f5259fc58..d849582b3c4 100644 --- a/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py +++ b/tests/test_litellm/integrations/dotprompt/test_prompt_manager.py @@ -127,14 +127,16 @@ def test_input_validation(): # Create a temporary directory with a test prompt with tempfile.TemporaryDirectory() as temp_dir: prompt_file = Path(temp_dir) / "test_validation.prompt" - prompt_file.write_text("""--- + prompt_file.write_text( + """--- input: schema: name: string age: integer active: boolean --- -Hello {{name}}, you are {{age}} years old and {'active' if active else 'inactive'}.""") +Hello {{name}}, you are {{age}} years old and {'active' if active else 'inactive'}.""" + ) manager = PromptManager(prompt_directory=str(temp_dir)) @@ -246,14 +248,16 @@ def test_frontmatter_parsing(): with tempfile.TemporaryDirectory() as temp_dir: # Test with frontmatter prompt_with_frontmatter = Path(temp_dir) / "with_frontmatter.prompt" - prompt_with_frontmatter.write_text("""--- + prompt_with_frontmatter.write_text( + """--- model: gpt-4 temperature: 0.8 input: schema: topic: string --- -Write about {{topic}}.""") +Write about {{topic}}.""" + ) # Test without frontmatter prompt_without_frontmatter = Path(temp_dir) / "without_frontmatter.prompt" @@ -366,7 +370,8 @@ def test_prompt_file_to_json_conversion(): # Create a temporary prompt file with frontmatter with tempfile.TemporaryDirectory() as temp_dir: prompt_file = Path(temp_dir) / "test_conversion.prompt" - prompt_file.write_text("""--- + prompt_file.write_text( + """--- model: gpt-4 temperature: 0.7 max_tokens: 200 @@ -379,7 +384,8 @@ output: --- You are an AI assistant. Given the context: {{context}} -Please respond to: {{user_input}}""") +Please respond to: {{user_input}}""" + ) manager = PromptManager() json_data = manager.prompt_file_to_json(prompt_file) diff --git a/tests/test_litellm/integrations/test_custom_guardrail.py b/tests/test_litellm/integrations/test_custom_guardrail.py index ff6c87132a6..d09c4ac2c38 100644 --- a/tests/test_litellm/integrations/test_custom_guardrail.py +++ b/tests/test_litellm/integrations/test_custom_guardrail.py @@ -1086,12 +1086,9 @@ class TestCustomGuardrailSpendLogMatchRedaction: ][0]["match"] == "[REDACTED]" ) - assert ( - raw["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ - "match" - ] - == "GG" - ) + assert raw["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ + "match" + ] == "GG" def test_add_standard_logging_redacts_regex_field(self): cg = CustomGuardrail(guardrail_name="test-rail") diff --git a/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py b/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py index 32de9a85cfe..a939951c430 100644 --- a/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py +++ b/tests/test_litellm/integrations/websearch_interception/test_websearch_thinking_constraint.py @@ -18,6 +18,7 @@ from litellm.integrations.websearch_interception.handler import ( WebSearchInterceptionLogger, ) + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py index 3b99167c034..e8bf54f7ffc 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_azure_assistant_cost_tracking.py @@ -3,7 +3,7 @@ Test Azure OpenAI Assistant Features Cost Tracking Tests cost calculation for Azure's new assistant features: - File Search (storage-based pricing) -- Code Interpreter (session-based pricing) +- Code Interpreter (session-based pricing) - Computer Use (token-based pricing) - Vector Store (storage-based pricing) """ diff --git a/tests/test_litellm/litellm_core_utils/test_core_helpers.py b/tests/test_litellm/litellm_core_utils/test_core_helpers.py index fc3308d6e36..b67ea91bb0b 100644 --- a/tests/test_litellm/litellm_core_utils/test_core_helpers.py +++ b/tests/test_litellm/litellm_core_utils/test_core_helpers.py @@ -176,11 +176,7 @@ class TestRedactNestedMatchAndRegexKeys: { "sensitiveInformationPolicy": { "piiEntities": [ - { - "type": "NAME", - "match": "secret-name", - "action": "BLOCKED", - } + {"type": "NAME", "match": "secret-name", "action": "BLOCKED"} ] }, "wordPolicy": { @@ -191,22 +187,16 @@ class TestRedactNestedMatchAndRegexKeys: "regex": "should-redact-key-named-regex", } out = redact_nested_match_and_regex_keys(payload) - assert ( - out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ - "match" - ] - == "[REDACTED]" - ) + assert out["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ + "match" + ] == "[REDACTED]" assert out["assessments"][0]["wordPolicy"]["customWords"][0]["match"] == ( "[REDACTED]" ) assert out["regex"] == "[REDACTED]" - assert ( - payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][0][ - "match" - ] - == "secret-name" - ) + assert payload["assessments"][0]["sensitiveInformationPolicy"]["piiEntities"][ + 0 + ]["match"] == "secret-name" def test_passes_through_none_and_str(self): assert redact_nested_match_and_regex_keys(None) is None diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py index fe6312e5725..49d3c51e340 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py @@ -2036,19 +2036,23 @@ async def test_azure_streaming_role_preserved_with_include_usage(sync_mode: bool chunks.append(chunk) # The prompt_filter chunk should be forwarded with choices=[] - assert ( - len(chunks[0].choices) == 0 - ), f"Expected prompt_filter chunk with choices=[], got {len(chunks[0].choices)} choices" + assert len(chunks[0].choices) == 0, ( + f"Expected prompt_filter chunk with choices=[], got {len(chunks[0].choices)} choices" + ) # At least one chunk must have role='assistant' in its delta has_role = any( - len(c.choices) > 0 and getattr(c.choices[0].delta, "role", None) == "assistant" + len(c.choices) > 0 + and getattr(c.choices[0].delta, "role", None) == "assistant" for c in chunks ) assert has_role, ( "No chunk contained role='assistant' in delta (issue #24221). " "Chunk deltas: " - + str([c.choices[0].delta if c.choices else "no choices" for c in chunks]) + + str([ + c.choices[0].delta if c.choices else "no choices" + for c in chunks + ]) ) diff --git a/tests/test_litellm/litellm_core_utils/test_token_counter.py b/tests/test_litellm/litellm_core_utils/test_token_counter.py index fa7c9e9aeab..3aa5f012467 100644 --- a/tests/test_litellm/litellm_core_utils/test_token_counter.py +++ b/tests/test_litellm/litellm_core_utils/test_token_counter.py @@ -493,6 +493,7 @@ from unittest.mock import MagicMock, patch from litellm.utils import _select_tokenizer_helper, claude_json_str, encoding + # Clear the cache at module load to ensure clean state _select_tokenizer_helper.cache_clear() diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 3fc0dea4748..42efde90926 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -2327,8 +2327,7 @@ class TestAnthropicStreamWrapperToolArgs: def _find_tool_deltas(self, events): return [ - e - for e in events + e for e in events if isinstance(e, dict) and e.get("type") == "content_block_delta" and isinstance(e.get("delta"), dict) @@ -2376,6 +2375,7 @@ class TestAnthropicStreamWrapperToolArgs: assert parsed == {"city": "Tokyo"} + def test_translate_anthropic_tool_choice_none(): """ Regression test for issue #24443. diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py index d88260629ee..b9bda07336f 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py @@ -22,6 +22,7 @@ from litellm.llms.anthropic.experimental_pass_through.messages.agentic_streaming _parse_sse_events, ) + # --------------------------------------------------------------------------- # Helpers to build SSE byte payloads # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py index c2938c5b209..1d25d719384 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py @@ -2,6 +2,7 @@ import os import sys from typing import List + sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterator import ( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py index dd5d1341713..07c0012b04d 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py @@ -31,16 +31,13 @@ def _call_handler_and_capture_optional_params(thinking=None, **extra_kwargs): """ captured = {} - with ( - patch( - "litellm.llms.anthropic.experimental_pass_through.messages.handler." - "base_llm_http_handler" - ) as mock_handler, - patch( - "litellm.llms.anthropic.experimental_pass_through.messages.handler." - "ProviderConfigManager" - ) as mock_pcm, - ): + with patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler." + "base_llm_http_handler" + ) as mock_handler, patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler." + "ProviderConfigManager" + ) as mock_pcm: # Make get_provider_anthropic_messages_config return a non-None config # so the handler takes the native Anthropic path mock_pcm.get_provider_anthropic_messages_config.return_value = MagicMock() @@ -122,9 +119,8 @@ class TestReasoningAutoSummaryMessages: def test_env_var_enables_auto_summary(self): """LITELLM_REASONING_AUTO_SUMMARY=true env var enables the feature.""" - with ( - patch.object(litellm, "reasoning_auto_summary", False), - patch.dict(os.environ, {"LITELLM_REASONING_AUTO_SUMMARY": "true"}), + with patch.object(litellm, "reasoning_auto_summary", False), patch.dict( + os.environ, {"LITELLM_REASONING_AUTO_SUMMARY": "true"} ): params = _call_handler_and_capture_optional_params( thinking={"type": "adaptive", "budget_tokens": 5000} diff --git a/tests/test_litellm/llms/azure/test_azure_cost_calculation.py b/tests/test_litellm/llms/azure/test_azure_cost_calculation.py index 89fff013360..53c91032b34 100644 --- a/tests/test_litellm/llms/azure/test_azure_cost_calculation.py +++ b/tests/test_litellm/llms/azure/test_azure_cost_calculation.py @@ -8,6 +8,7 @@ import litellm from litellm.llms.azure.cost_calculation import cost_per_token from litellm.types.utils import Usage + # Register a test model with tier-specific pricing TEST_MODEL = "test-azure-gpt-4.1" TEST_MODEL_COST = { diff --git a/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py b/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py index b9316fb9af1..20260c744f8 100644 --- a/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py +++ b/tests/test_litellm/llms/azure_ai/test_azure_ai_cost_calculator.py @@ -459,21 +459,18 @@ class TestAzureAIServiceTierCostCalculation: @pytest.fixture(autouse=True) def register_test_model(self): import litellm - - litellm.register_model( - model_cost={ - "test-azure-ai-model": { - "input_cost_per_token": 0.001, - "output_cost_per_token": 0.002, - "input_cost_per_token_priority": 0.01, - "output_cost_per_token_priority": 0.02, - "input_cost_per_token_flex": 0.0005, - "output_cost_per_token_flex": 0.001, - "litellm_provider": "azure_ai", - "max_tokens": 8192, - } + litellm.register_model(model_cost={ + "test-azure-ai-model": { + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "input_cost_per_token_priority": 0.01, + "output_cost_per_token_priority": 0.02, + "input_cost_per_token_flex": 0.0005, + "output_cost_per_token_flex": 0.001, + "litellm_provider": "azure_ai", + "max_tokens": 8192, } - ) + }) def test_service_tier_priority_higher_cost(self): """Priority tier should cost more than standard for azure_ai.""" diff --git a/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py b/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py index 1efd10e921b..b5fcd9d8219 100644 --- a/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py +++ b/tests/test_litellm/llms/base_llm/test_managed_resource_isolation.py @@ -10,6 +10,7 @@ from litellm.llms.base_llm.managed_resources.isolation import ( ) from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + # --------------------------------------------------------------------------- # build_owner_filter # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py index 100f91c4e5a..a415d550215 100644 --- a/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py +++ b/tests/test_litellm/llms/bedrock/chat/test_invoke_handler.py @@ -1,6 +1,7 @@ import os import sys + sys.path.insert( 0, os.path.abspath("../../../../..") ) # Adds the parent directory to the system path diff --git a/tests/test_litellm/llms/crusoe/test_crusoe.py b/tests/test_litellm/llms/crusoe/test_crusoe.py index 32a2f174f3d..0a05126919a 100644 --- a/tests/test_litellm/llms/crusoe/test_crusoe.py +++ b/tests/test_litellm/llms/crusoe/test_crusoe.py @@ -39,10 +39,7 @@ def test_crusoe_dynamic_config_env_vars(): with patch.dict( os.environ, - { - "CRUSOE_API_KEY": "test-key", - "CRUSOE_API_BASE": "https://custom.crusoe.com/v1", - }, + {"CRUSOE_API_KEY": "test-key", "CRUSOE_API_BASE": "https://custom.crusoe.com/v1"}, ): api_base, api_key = config._get_openai_compatible_provider_info(None, None) @@ -72,9 +69,7 @@ def test_crusoe_supported_params(): from litellm.llms.openai_like.json_loader import JSONProviderRegistry config = create_config_class(JSONProviderRegistry.get("crusoe"))() - params = config.get_supported_openai_params( - model="meta-llama/Llama-3.3-70B-Instruct" - ) + params = config.get_supported_openai_params(model="meta-llama/Llama-3.3-70B-Instruct") assert isinstance(params, list) assert len(params) > 0 @@ -96,9 +91,7 @@ def test_crusoe_param_mapping_max_completion_tokens(): drop_params=False, ) - assert ( - "max_tokens" in optional_params - ), "max_completion_tokens should be mapped to max_tokens" + assert "max_tokens" in optional_params, "max_completion_tokens should be mapped to max_tokens" assert optional_params["max_tokens"] == 1024 assert "max_completion_tokens" not in optional_params diff --git a/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py b/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py index 660ab88c2b2..5515e6ce815 100644 --- a/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py +++ b/tests/test_litellm/llms/custom_httpx/test_aiohttp_so_keepalive.py @@ -158,7 +158,4 @@ def test_socket_factory_uses_tcp_keepalive_when_keepidle_unavailable(monkeypatch assert ( setsockopt_calls[(socket.IPPROTO_TCP, fake_socket_module.TCP_KEEPALIVE)] == 60 ) - assert ( - socket.IPPROTO_TCP, - getattr(socket, "TCP_KEEPIDLE", -1), - ) not in setsockopt_calls + assert (socket.IPPROTO_TCP, getattr(socket, "TCP_KEEPIDLE", -1)) not in setsockopt_calls diff --git a/tests/test_litellm/llms/custom_httpx/test_mock_transport.py b/tests/test_litellm/llms/custom_httpx/test_mock_transport.py index b3b5f82f02f..c2d4e146428 100644 --- a/tests/test_litellm/llms/custom_httpx/test_mock_transport.py +++ b/tests/test_litellm/llms/custom_httpx/test_mock_transport.py @@ -10,6 +10,7 @@ import pytest from litellm.llms.custom_httpx.mock_transport import MockOpenAITransport + # --------------------------------------------------------------------------- # Non-streaming # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/databricks/test_databricks_e2e.py b/tests/test_litellm/llms/databricks/test_databricks_e2e.py index 90846f28920..669f9e94639 100644 --- a/tests/test_litellm/llms/databricks/test_databricks_e2e.py +++ b/tests/test_litellm/llms/databricks/test_databricks_e2e.py @@ -17,13 +17,13 @@ Purpose: LiteLLM Integration Tests: This test file includes tests for different ways of calling Databricks via LiteLLM: - + 1. LiteLLM SDK Direct - Using litellm.completion() with user_agent parameter 2. LangChain + LiteLLM - Using ChatLiteLLM wrapper (requires langchain-community) 3. LiteLLM Async - Using litellm.acompletion() async API 4. LiteLLM Streaming - Using litellm.completion() with stream=True 5. LiteLLM Embedding - Using litellm.embedding() with user_agent parameter - + All tests use the CUSTOM_USER_AGENT value from the config file and call Databricks endpoints through LiteLLM's unified interface. @@ -31,7 +31,7 @@ Prerequisites: - Valid Databricks workspace access - Configured credentials (OAuth Service Principal, PAT, or Databricks CLI) - Access to serving endpoints (e.g., databricks-gpt-oss-120b) - + Optional Dependencies (for LiteLLM integration tests): - pip install langchain-litellm # For LangChain tests (recommended) diff --git a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py index a950b4a5f2c..d59ab975ef2 100644 --- a/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/deepgram/audio_transcription/test_deepgram_audio_transcription_transformation.py @@ -53,7 +53,7 @@ def test_file(): ) def test_audio_file_handling(fixture_name, request): handler = DeepgramAudioTranscriptionConfig() - audio_file, expected_output = request.getfixturevalue(fixture_name) + (audio_file, expected_output) = request.getfixturevalue(fixture_name) result = handler.transform_audio_transcription_request( model="deepseek-audio-transcription", audio_file=audio_file, diff --git a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py index 7c787f20b8b..6c846a90c71 100644 --- a/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py +++ b/tests/test_litellm/llms/github_copilot/test_github_copilot_authenticator.py @@ -255,19 +255,12 @@ class TestGitHubCopilotAuthenticator: "user_code": "UC", "verification_uri": "https://example.com", } - with ( - patch.dict(os.environ, {"GITHUB_COPILOT_DEVICE_CODE_URL": custom_url}), - patch( - "litellm.llms.github_copilot.authenticator._get_httpx_client", - return_value=mock_client, - ), - ): + with patch.dict(os.environ, {"GITHUB_COPILOT_DEVICE_CODE_URL": custom_url}), \ + patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client): authenticator._get_device_code() assert mock_client.post.call_args[0][0] == custom_url - def test_get_device_code_with_custom_client_id( - self, authenticator, mock_http_client - ): + def test_get_device_code_with_custom_client_id(self, authenticator, mock_http_client): """GITHUB_COPILOT_CLIENT_ID env var must appear as client_id in the device-code request body.""" mock_client, mock_response = mock_http_client custom_id = "custom_client_id" @@ -276,49 +269,30 @@ class TestGitHubCopilotAuthenticator: "user_code": "UC", "verification_uri": "https://example.com", } - with ( - patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), - patch( - "litellm.llms.github_copilot.authenticator._get_httpx_client", - return_value=mock_client, - ), - ): + with patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), \ + patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client): authenticator._get_device_code() assert mock_client.post.call_args[1]["json"]["client_id"] == custom_id - def test_poll_for_access_token_with_custom_url( - self, authenticator, mock_http_client - ): + def test_poll_for_access_token_with_custom_url(self, authenticator, mock_http_client): """GITHUB_COPILOT_ACCESS_TOKEN_URL env var must be used by _poll_for_access_token at call time.""" mock_client, mock_response = mock_http_client custom_url = "https://custom.example.com/token" mock_response.json.return_value = {"access_token": "tok"} - with ( - patch.dict(os.environ, {"GITHUB_COPILOT_ACCESS_TOKEN_URL": custom_url}), - patch( - "litellm.llms.github_copilot.authenticator._get_httpx_client", - return_value=mock_client, - ), - patch("time.sleep"), - ): + with patch.dict(os.environ, {"GITHUB_COPILOT_ACCESS_TOKEN_URL": custom_url}), \ + patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ + patch("time.sleep"): authenticator._poll_for_access_token("dc") assert mock_client.post.call_args[0][0] == custom_url - def test_poll_for_access_token_with_custom_client_id( - self, authenticator, mock_http_client - ): + def test_poll_for_access_token_with_custom_client_id(self, authenticator, mock_http_client): """GITHUB_COPILOT_CLIENT_ID env var must appear as client_id in the polling request body.""" mock_client, mock_response = mock_http_client custom_id = "custom_client_id" mock_response.json.return_value = {"access_token": "tok"} - with ( - patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), - patch( - "litellm.llms.github_copilot.authenticator._get_httpx_client", - return_value=mock_client, - ), - patch("time.sleep"), - ): + with patch.dict(os.environ, {"GITHUB_COPILOT_CLIENT_ID": custom_id}), \ + patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ + patch("time.sleep"): authenticator._poll_for_access_token("dc") assert mock_client.post.call_args[1]["json"]["client_id"] == custom_id @@ -327,13 +301,9 @@ class TestGitHubCopilotAuthenticator: mock_client, mock_response = mock_http_client custom_url = "https://custom.example.com/api-key" mock_response.json.return_value = {"token": "api-tok", "expires_at": 9999999999} - with ( - patch.dict(os.environ, {"GITHUB_COPILOT_API_KEY_URL": custom_url}), - patch( - "litellm.llms.github_copilot.authenticator._get_httpx_client", - return_value=mock_client, - ), - patch.object(authenticator, "get_access_token", return_value="access-tok"), - ): + with patch.dict(os.environ, {"GITHUB_COPILOT_API_KEY_URL": custom_url}), \ + patch("litellm.llms.github_copilot.authenticator._get_httpx_client", return_value=mock_client), \ + patch.object(authenticator, "get_access_token", return_value="access-tok"): authenticator._refresh_api_key() assert mock_client.get.call_args[0][0] == custom_url + diff --git a/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py b/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py index e6acc51a585..560796ea58d 100644 --- a/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py +++ b/tests/test_litellm/llms/huggingface/embedding/test_huggingface_embedding_handler.py @@ -11,6 +11,7 @@ sys.path.insert( import litellm import pytest + MOCK_EMBEDDING_RESPONSE = [[0.1, 0.2, 0.3, 0.4, 0.5]] diff --git a/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py b/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py index e398699bfdd..b4744a7ed18 100644 --- a/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py +++ b/tests/test_litellm/llms/moonshot/test_moonshot_chat_transformation.py @@ -666,9 +666,7 @@ class TestKimiK26ModelRegistry: def test_kimi_k26_in_model_cost_map(self, model_cost_map): """kimi-k2.6 should be present in the model cost map.""" - assert ( - "moonshot/kimi-k2.6" in model_cost_map - ), "moonshot/kimi-k2.6 not found in model_cost" + assert "moonshot/kimi-k2.6" in model_cost_map, "moonshot/kimi-k2.6 not found in model_cost" def test_kimi_k26_pricing(self, model_cost_map): """kimi-k2.6 pricing should match official Kimi API rates.""" diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py index c6971cfd556..d408f55c004 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py @@ -1,7 +1,7 @@ """ Test file for Perplexity cost calculator functionality. -Tests the cost calculation for Perplexity models including citation tokens, +Tests the cost calculation for Perplexity models including citation tokens, search queries, and reasoning tokens. """ diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_integration.py b/tests/test_litellm/llms/perplexity/test_perplexity_integration.py index cd76fcc93c2..1b03fd7df88 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_integration.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_integration.py @@ -1,7 +1,7 @@ """ Integration tests for Perplexity cost calculation and transformation. -Tests the end-to-end functionality of Perplexity cost calculation +Tests the end-to-end functionality of Perplexity cost calculation including integration with the main LiteLLM cost calculator. """ diff --git a/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py b/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py index c237e28081e..407e1d19fb3 100644 --- a/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py +++ b/tests/test_litellm/llms/scaleway/test_scaleway_audio_transcription_transformation.py @@ -10,6 +10,7 @@ from litellm.llms.scaleway.audio_transcription.transformation import ( ) from litellm.types.utils import TranscriptionResponse + # --------------------------------------------------------------------------- # get_complete_url # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/llms/test_polling_url_origin_match.py b/tests/test_litellm/llms/test_polling_url_origin_match.py index b6b932ad937..f1f910bc731 100644 --- a/tests/test_litellm/llms/test_polling_url_origin_match.py +++ b/tests/test_litellm/llms/test_polling_url_origin_match.py @@ -15,6 +15,7 @@ from unittest.mock import MagicMock, patch import httpx import pytest + # Azure DALL-E sync + async paths route through ``assert_same_origin`` # the same way as the cases below. The helper itself is unit-tested in # ``tests/test_litellm/litellm_core_utils/test_url_utils.py``; the diff --git a/tests/test_litellm/llms/test_predibase_transformation.py b/tests/test_litellm/llms/test_predibase_transformation.py index 7d67996906a..1600878a586 100644 --- a/tests/test_litellm/llms/test_predibase_transformation.py +++ b/tests/test_litellm/llms/test_predibase_transformation.py @@ -233,9 +233,7 @@ def test_predibase_transform_response_non_dict_payload(): raw_response.headers = {} raw_response.json.return_value = [] - with pytest.raises( - PredibaseError, match="'completion_response' is not a dictionary" - ): + with pytest.raises(PredibaseError, match="'completion_response' is not a dictionary"): config.transform_response( model="predibase-model", raw_response=raw_response, @@ -377,9 +375,7 @@ def test_predibase_transform_response_best_of_invalid_value_falls_back(monkeypat assert result.choices[0].message.content == "primary-output" -def test_predibase_transform_response_empty_output_sets_completion_tokens_zero( - monkeypatch, -): +def test_predibase_transform_response_empty_output_sets_completion_tokens_zero(monkeypatch): config = PredibaseConfig() logging_obj = Mock() encoding = Mock() @@ -433,10 +429,7 @@ def test_predibase_transform_response_usage_fallbacks(monkeypatch): raw_response = httpx.Response( status_code=200, - json={ - "generated_text": "ok", - "details": {"tokens": [], "finish_reason": "stop"}, - }, + json={"generated_text": "ok", "details": {"tokens": [], "finish_reason": "stop"}}, ) result = config.transform_response( @@ -546,17 +539,13 @@ def test_predibase_completion_sync_returns_transform_response(monkeypatch): def fake_transform_response(self, **kwargs): return expected - monkeypatch.setattr( - PredibaseConfig, "validate_environment", fake_validate_environment - ) + monkeypatch.setattr(PredibaseConfig, "validate_environment", fake_validate_environment) monkeypatch.setattr(PredibaseConfig, "get_complete_url", fake_get_complete_url) monkeypatch.setattr(PredibaseConfig, "transform_request", fake_transform_request) monkeypatch.setattr(PredibaseConfig, "transform_response", fake_transform_response) monkeypatch.setattr( "litellm.module_level_client.post", - lambda *args, **kwargs: httpx.Response( - status_code=200, json={"generated_text": "ok"} - ), + lambda *args, **kwargs: httpx.Response(status_code=200, json={"generated_text": "ok"}), ) result = handler.completion( @@ -597,9 +586,7 @@ def test_predibase_completion_passes_existing_config_to_async_completion(monkeyp captured["async_kwargs"] = kwargs return "async-result" - monkeypatch.setattr( - PredibaseConfig, "validate_environment", fake_validate_environment - ) + monkeypatch.setattr(PredibaseConfig, "validate_environment", fake_validate_environment) monkeypatch.setattr(PredibaseConfig, "get_complete_url", fake_get_complete_url) monkeypatch.setattr(PredibaseConfig, "transform_request", fake_transform_request) monkeypatch.setattr(handler, "async_completion", fake_async_completion) diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py b/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py index ef084dcc004..6d913ad5d1d 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py @@ -22,6 +22,7 @@ from litellm.llms.vertex_ai.gemini.transformation import ( ) from litellm.types.llms.vertex_ai import HttpxPartType + # --- Response extraction tests --- diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py index fe97727bb89..353d19b0198 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_and_google_ai_studio_gemini.py @@ -3499,12 +3499,7 @@ def test_video_metadata_supported_for_all_gemini_models(): } ] - for model in [ - "gemini-1.5-pro", - "gemini-2.5-flash", - "gemini-2.5-pro", - "gemini-3-pro-preview", - ]: + for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-pro-preview"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = None @@ -3514,25 +3509,19 @@ def test_video_metadata_supported_for_all_gemini_models(): break assert file_part is not None, f"{model}: file part should exist" - assert ( - "video_metadata" in file_part - ), f"{model}: video_metadata should be present" + assert "video_metadata" in file_part, f"{model}: video_metadata should be present" assert file_part["video_metadata"]["fps"] == 5, f"{model}: fps should be 5" # Per-part media_resolution is Gemini 3+ only; 2.x uses generation_config global for model in ["gemini-3-pro-preview"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = next(p for p in contents[0]["parts"] if "file_data" in p) - assert ( - "media_resolution" in file_part - ), f"{model}: media_resolution should be present" + assert "media_resolution" in file_part, f"{model}: media_resolution should be present" for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro"]: contents = _gemini_convert_messages_with_history(messages=messages, model=model) file_part = next(p for p in contents[0]["parts"] if "file_data" in p) - assert ( - "media_resolution" not in file_part - ), f"{model}: per-part media_resolution should not be set" + assert "media_resolution" not in file_part, f"{model}: per-part media_resolution should not be set" def test_chunk_parser_handles_prompt_feedback_block(): @@ -4165,9 +4154,8 @@ def test_vertex_ai_usage_metadata_with_document_tokens_in_prompt(): # DOCUMENT tokens should be included in text_tokens: 8 (TEXT) + 774 (DOCUMENT) = 782 assert result.prompt_tokens_details is not None - assert ( - result.prompt_tokens_details.text_tokens == 782 - ), "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)" + assert result.prompt_tokens_details.text_tokens == 782, \ + "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)" # Verify completion token details assert result.completion_tokens_details is not None @@ -4202,9 +4190,8 @@ def test_vertex_ai_usage_metadata_with_document_tokens_cached(): # DOCUMENT cached tokens map to cached_text_tokens, so: # text_tokens = (8 TEXT + 774 DOCUMENT) - 400 cached = 382 - assert ( - result.prompt_tokens_details.text_tokens == 382 - ), "text_tokens should be (8 + 774) - 400 cached = 382" + assert result.prompt_tokens_details.text_tokens == 382, \ + "text_tokens should be (8 + 774) - 400 cached = 382" assert result.prompt_tokens_details.cached_tokens == 400 diff --git a/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py b/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py index 4bd0b93027d..bb4e6c67e9e 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/gemini_embeddings/test_batch_embed_content_transformation.py @@ -20,6 +20,7 @@ from litellm.llms.vertex_ai.gemini_embeddings.batch_embed_content_transformation from litellm.types.llms.vertex_ai import VertexAIBatchEmbeddingsResponseObject from litellm.types.utils import EmbeddingResponse + IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII" GCS_URL = "gs://my-bucket/image.png" @@ -71,9 +72,7 @@ class TestBuildPartForInput: assert part["file_data"]["file_uri"] == GCS_URL def test_file_reference_resolved(self): - resolved = { - "files/abc": {"mime_type": "image/jpeg", "uri": "https://example.com/abc"} - } + resolved = {"files/abc": {"mime_type": "image/jpeg", "uri": "https://example.com/abc"}} part = _build_part_for_input("files/abc", resolved_files=resolved) assert part["file_data"] is not None assert part["file_data"]["mime_type"] == "image/jpeg" @@ -95,9 +94,7 @@ class TestTransformOpenaiInputGeminiContent: def test_multiple_texts(self): result = transform_openai_input_gemini_content( - input=["hello", "world"], - model="gemini-embedding-2-preview", - optional_params={}, + input=["hello", "world"], model="gemini-embedding-2-preview", optional_params={} ) assert len(result["requests"]) == 2 assert result["requests"][0]["content"]["parts"][0]["text"] == "hello" @@ -112,10 +109,7 @@ class TestTransformOpenaiInputGeminiContent: ) assert len(result["requests"]) == 2 # First request is text - assert ( - result["requests"][0]["content"]["parts"][0]["text"] - == "The food was delicious" - ) + assert result["requests"][0]["content"]["parts"][0]["text"] == "The food was delicious" # Second request is image assert result["requests"][1]["content"]["parts"][0]["inline_data"] is not None diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py index b47f8c6af49..6c549af2cc5 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_ai_common_utils.py @@ -1469,7 +1469,11 @@ def test_vertex_request_labels_from_litellm_params_extracts_requester_metadata() def test_vertex_request_labels_from_litellm_params_accepts_litellm_metadata(): - lp = {"litellm_metadata": {"requester_metadata": {"team": "platform", "count": 3}}} + lp = { + "litellm_metadata": { + "requester_metadata": {"team": "platform", "count": 3} + } + } assert vertex_request_labels_from_litellm_params(lp) == {"team": "platform"} diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py b/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py index 80fe002176e..91261b63252 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex_model_garden_openapi.py @@ -37,8 +37,5 @@ def test_create_vertex_url_openapi_vs_deployed_endpoint( def test_model_id_in_json_body_heuristic() -> None: - assert ( - _vertex_model_garden_model_id_in_json_body("xai/grok-4.1-fast-reasoning") - is True - ) + assert _vertex_model_garden_model_id_in_json_body("xai/grok-4.1-fast-reasoning") is True assert _vertex_model_garden_model_id_in_json_body("5464397967697903616") is False diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index 3a5ea4e88cb..6e0dadcd4d8 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -880,7 +880,7 @@ class TestMCPPublicRouteGuard: with patch( "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", ) as mock_auth: - auth_result, *_rest = await MCPRequestHandler.process_mcp_request(scope) + (auth_result, *_rest) = await MCPRequestHandler.process_mcp_request(scope) mock_auth.assert_not_called() assert isinstance(auth_result, UserAPIKeyAuth) @@ -997,7 +997,7 @@ class TestMCPOAuth2FallbackTargetGating: mock_mgr.get_mcp_server_by_name.return_value = ( TestMCPOAuth2FallbackTargetGating._make_server(MCPAuth.oauth2) ) - auth_result, *_rest = await MCPRequestHandler.process_mcp_request(scope) + (auth_result, *_rest) = await MCPRequestHandler.process_mcp_request(scope) assert isinstance(auth_result, UserAPIKeyAuth) async def test_fallback_blocked_when_any_target_in_header_is_not_oauth2(self): diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py index 37e593e2a07..078adf72d4c 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_db_credentials.py @@ -25,6 +25,7 @@ from litellm.proxy._experimental.mcp_server.db import ( ) from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper + SALT_KEY = "test-salt-key-for-byok-credential-tests-1234" diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py index 1084e64eab3..8f09e2410c4 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py @@ -8,6 +8,7 @@ import pytest from litellm.proxy._experimental.mcp_server.utils import is_tool_name_prefixed + # --------------------------------------------------------------------------- # Legacy behaviour (no known_server_prefixes passed) # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py index 4cc764f3c6d..a0bfbff4222 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py @@ -497,39 +497,31 @@ async def test_per_user_oauth_missing_stored_token_returns_preemptive_401(): oauth_server.auth_type = MCPAuth.oauth2 oauth_server.needs_user_oauth_token = True - with ( - patch( - "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", - new_callable=AsyncMock, - return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), - ), - patch( - "litellm.proxy._experimental.mcp_server.server.set_auth_context", - ), - patch( - "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", - True, - ), - patch( - "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", - new_callable=AsyncMock, - return_value=False, - ), - patch( - "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", - new_callable=AsyncMock, - return_value=None, - ) as mock_get_stored_token, - patch( - "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", - return_value=oauth_server, - ), - patch.object( - session_manager, - "handle_request", - new_callable=AsyncMock, - ) as mock_handle_request, - ): + with patch( + "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", + new_callable=AsyncMock, + return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), + ), patch( + "litellm.proxy._experimental.mcp_server.server.set_auth_context", + ), patch( + "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", + True, + ), patch( + "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", + new_callable=AsyncMock, + return_value=False, + ), patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new_callable=AsyncMock, + return_value=None, + ) as mock_get_stored_token, patch( + "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", + return_value=oauth_server, + ), patch.object( + session_manager, + "handle_request", + new_callable=AsyncMock, + ) as mock_handle_request: with pytest.raises(HTTPException) as exc_info: await handle_streamable_http_mcp(scope, receive, send) @@ -570,39 +562,31 @@ async def test_per_user_oauth_with_stored_token_skips_preemptive_401(): oauth_server.auth_type = MCPAuth.oauth2 oauth_server.needs_user_oauth_token = True - with ( - patch( - "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", - new_callable=AsyncMock, - return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), - ), - patch( - "litellm.proxy._experimental.mcp_server.server.set_auth_context", - ), - patch( - "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", - True, - ), - patch( - "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", - new_callable=AsyncMock, - return_value=False, - ), - patch( - "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", - new_callable=AsyncMock, - return_value={"Authorization": "Bearer cached-token"}, - ) as mock_get_stored_token, - patch( - "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", - return_value=oauth_server, - ), - patch.object( - session_manager, - "handle_request", - new_callable=AsyncMock, - ) as mock_handle_request, - ): + with patch( + "litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context", + new_callable=AsyncMock, + return_value=(user_auth, None, ["repro_oauth_server"], None, None, None), + ), patch( + "litellm.proxy._experimental.mcp_server.server.set_auth_context", + ), patch( + "litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED", + True, + ), patch( + "litellm.proxy._experimental.mcp_server.server._handle_stale_mcp_session", + new_callable=AsyncMock, + return_value=False, + ), patch( + "litellm.proxy._experimental.mcp_server.server._get_user_oauth_extra_headers_from_db", + new_callable=AsyncMock, + return_value={"Authorization": "Bearer cached-token"}, + ) as mock_get_stored_token, patch( + "litellm.proxy._experimental.mcp_server.server.global_mcp_server_manager.get_mcp_server_by_name", + return_value=oauth_server, + ), patch.object( + session_manager, + "handle_request", + new_callable=AsyncMock, + ) as mock_handle_request: await handle_streamable_http_mcp(scope, receive, send) assert mock_get_stored_token.await_count == 1 diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py index 2888077a23a..2558df8533b 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py @@ -489,7 +489,9 @@ class TestGetToolsByNames: {"name": "send_email", "description": "send mail"}, ] - matched = filter_instance._get_tools_by_names(["send_email"], available_tools) + matched = filter_instance._get_tools_by_names( + ["send_email"], available_tools + ) assert len(matched) == 1 assert matched[0]["name"] == "send_email" @@ -501,7 +503,9 @@ class TestGetToolsByNames: client_name = "litellm_" + canonical available_tools = [{"name": client_name, "description": "scrape"}] - matched = filter_instance._get_tools_by_names([canonical], available_tools) + matched = filter_instance._get_tools_by_names( + [canonical], available_tools + ) assert len(matched) == 1 # Must return the incoming tool unchanged so the client-facing @@ -512,9 +516,13 @@ class TestGetToolsByNames: """Some clients use dash as alias separator; accept that too.""" filter_instance = self._make_filter() canonical = "weather_svc-get_weather" - available_tools = [{"name": "mcp-" + canonical, "description": "weather"}] + available_tools = [ + {"name": "mcp-" + canonical, "description": "weather"} + ] - matched = filter_instance._get_tools_by_names([canonical], available_tools) + matched = filter_instance._get_tools_by_names( + [canonical], available_tools + ) assert len(matched) == 1 assert matched[0]["name"] == "mcp-" + canonical @@ -544,7 +552,9 @@ class TestGetToolsByNames: {"name": "litellm_" + canonical, "description": "wrapped"}, ] - matched = filter_instance._get_tools_by_names([canonical], available_tools) + matched = filter_instance._get_tools_by_names( + [canonical], available_tools + ) assert len(matched) == 1 assert matched[0]["name"] == canonical @@ -557,7 +567,9 @@ class TestGetToolsByNames: separator-anchored suffixes of ``litellm_api-fs-read_file``. """ filter_instance = self._make_filter() - available_tools = [{"name": "litellm_api-fs-read_file", "description": "read"}] + available_tools = [ + {"name": "litellm_api-fs-read_file", "description": "read"} + ] matched = filter_instance._get_tools_by_names( ["fs-read_file", "api-fs-read_file"], available_tools @@ -578,7 +590,9 @@ class TestGetToolsByNames: {"name": "my_" + canonical, "description": "plain search"}, ] - matched = filter_instance._get_tools_by_names([canonical], available_tools) + matched = filter_instance._get_tools_by_names( + [canonical], available_tools + ) assert len(matched) == 1 assert matched[0]["name"] == "my_" + canonical diff --git a/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py b/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py index 06da009134f..554f98d7209 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_agent_header_isolation.py @@ -16,6 +16,7 @@ import pytest from litellm.constants import DEFAULT_A2A_AGENT_TIMEOUT + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py b/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py index 7cbd12a4ea6..93ba9dc922c 100644 --- a/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py +++ b/tests/test_litellm/proxy/agent_endpoints/test_agent_headers.py @@ -14,6 +14,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest + # --------------------------------------------------------------------------- # Helper: build a minimal mock agent # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/auth/test_onboarding.py b/tests/test_litellm/proxy/auth/test_onboarding.py index d55a5472af1..c81f4cb7d66 100644 --- a/tests/test_litellm/proxy/auth/test_onboarding.py +++ b/tests/test_litellm/proxy/auth/test_onboarding.py @@ -18,6 +18,7 @@ from fastapi import HTTPException import litellm from litellm.proxy._types import InvitationClaim + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/common_utils/test_cache_codec.py b/tests/test_litellm/proxy/common_utils/test_cache_codec.py index 8aa16ddbce1..044d4c2d1a7 100644 --- a/tests/test_litellm/proxy/common_utils/test_cache_codec.py +++ b/tests/test_litellm/proxy/common_utils/test_cache_codec.py @@ -54,9 +54,7 @@ class TestCacheCodecSerialize: def test_with_model_type_already_correct_instance_skips_revalidation(self): """Fast-path: value is already model_type — model_validate must NOT be called.""" m = _SampleModel(name="fast", count=7) - with patch.object( - _SampleModel, "model_validate", wraps=_SampleModel.model_validate - ) as mock_validate: + with patch.object(_SampleModel, "model_validate", wraps=_SampleModel.model_validate) as mock_validate: out = CacheCodec.serialize(m, model_type=_SampleModel) assert out == {"name": "fast", "count": 7} mock_validate.assert_not_called() @@ -64,9 +62,7 @@ class TestCacheCodecSerialize: def test_with_model_type_subclass_instance_skips_revalidation(self): """Subclass is isinstance of base → should also take the fast path.""" sub = _SampleSubModel(name="sub", count=2) - with patch.object( - _SampleModel, "model_validate", wraps=_SampleModel.model_validate - ) as mock_validate: + with patch.object(_SampleModel, "model_validate", wraps=_SampleModel.model_validate) as mock_validate: out = CacheCodec.serialize(sub, model_type=_SampleModel) assert out == {"name": "sub", "count": 2} mock_validate.assert_not_called() diff --git a/tests/test_litellm/proxy/conftest.py b/tests/test_litellm/proxy/conftest.py index d01c47d9751..20236ebdf45 100644 --- a/tests/test_litellm/proxy/conftest.py +++ b/tests/test_litellm/proxy/conftest.py @@ -14,6 +14,7 @@ import pytest import yaml from fastapi.testclient import TestClient + _PROXY_MODULE_GLOBALS_TO_ISOLATE = ( "master_key", "prisma_client", diff --git a/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py b/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py index 3586c9a80ee..434f7953c21 100644 --- a/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py +++ b/tests/test_litellm/proxy/google_endpoints/test_google_api_endpoints.py @@ -2,7 +2,6 @@ """ Test to verify the Google GenAI proxy API endpoints """ - import os import sys from unittest.mock import AsyncMock, MagicMock, patch diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py index 399852acd40..fb952d4b18b 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/content_filter/test_content_filter.py @@ -314,13 +314,15 @@ class TestContentFilterGuardrail: # Create a temporary blocked words file with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - f.write("""blocked_words: + f.write( + """blocked_words: - keyword: "test_keyword" action: "BLOCK" description: "Test keyword" - keyword: "another_word" action: "MASK" -""") +""" + ) temp_file = f.name try: diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py index ee7440d1195..bccfb4a1cb5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py @@ -2,7 +2,6 @@ """ Test OpenAI Moderation Guardrail """ - import os import sys diff --git a/tests/test_litellm/proxy/guardrails/test_content_utils.py b/tests/test_litellm/proxy/guardrails/test_content_utils.py index d3428dfbafa..099fca78a62 100644 --- a/tests/test_litellm/proxy/guardrails/test_content_utils.py +++ b/tests/test_litellm/proxy/guardrails/test_content_utils.py @@ -8,6 +8,7 @@ from litellm.proxy.guardrails._content_utils import ( walk_user_text, ) + # ── iter_message_text ──────────────────────────────────────────────────────────── diff --git a/tests/test_litellm/proxy/guardrails/test_custom_code_security.py b/tests/test_litellm/proxy/guardrails/test_custom_code_security.py index defdba10a60..00cf3f317c9 100644 --- a/tests/test_litellm/proxy/guardrails/test_custom_code_security.py +++ b/tests/test_litellm/proxy/guardrails/test_custom_code_security.py @@ -5,6 +5,7 @@ from litellm.proxy.guardrails.guardrail_hooks.custom_code.custom_code_guardrail CustomCodeGuardrail, ) + # str.mro() + generator gi_code + code.replace(co_names=...) + __setattr__ # to swap a function's bytecode and read http_get's real builtins dict. BYTECODE_REWRITE_PAYLOAD = ( diff --git a/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py b/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py index 9178a66611e..c9fde4ffbae 100644 --- a/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py +++ b/tests/test_litellm/proxy/guardrails/test_llm_as_a_judge.py @@ -13,6 +13,7 @@ from litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge import ( initialize_guardrail, ) + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -38,20 +39,8 @@ def _make_guardrail(**overrides) -> LLMAsAJudgeGuardrail: def _make_verdict_response(overall_score: float) -> dict: return { "verdicts": [ - { - "criterion_name": "Accuracy", - "score": overall_score, - "reasoning": "ok", - "passed": True, - "weight": 60, - }, - { - "criterion_name": "Safety", - "score": overall_score, - "reasoning": "ok", - "passed": True, - "weight": 40, - }, + {"criterion_name": "Accuracy", "score": overall_score, "reasoning": "ok", "passed": True, "weight": 60}, + {"criterion_name": "Safety", "score": overall_score, "reasoning": "ok", "passed": True, "weight": 40}, ], "overall_score": overall_score, } @@ -101,15 +90,7 @@ def test_build_judge_prompt_missing_name_and_weight(): def _make_litellm_params(**overrides): params = MagicMock() - for attr in ( - "guardrail_name", - "judge_model", - "criteria", - "on_failure", - "overall_threshold", - "mode", - "default_on", - ): + for attr in ("guardrail_name", "judge_model", "criteria", "on_failure", "overall_threshold", "mode", "default_on"): setattr(params, attr, None) for k, v in overrides.items(): setattr(params, k, v) @@ -117,19 +98,12 @@ def _make_litellm_params(**overrides): def _make_guardrail_dict(name="g", **litellm_params_overrides): - raw = { - "judge_model": "gpt-4o-mini", - "criteria": CRITERIA_100, - "on_failure": "block", - "overall_threshold": 80.0, - } + raw = {"judge_model": "gpt-4o-mini", "criteria": CRITERIA_100, "on_failure": "block", "overall_threshold": 80.0} raw.update(litellm_params_overrides) return {"guardrail_name": name, "litellm_params": raw} -@patch( - "litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.logging_callback_manager" -) +@patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.logging_callback_manager") def test_initialize_guardrail_ok(mock_mgr): lp = _make_litellm_params() g = _make_guardrail_dict() @@ -186,18 +160,11 @@ async def test_apply_guardrail_empty_response_passthrough(): @patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.acompletion") async def test_apply_guardrail_passes_above_threshold(mock_completion): mock_completion.return_value = MagicMock( - choices=[ - MagicMock( - message=MagicMock(content=json.dumps(_make_verdict_response(90.0))) - ) - ] + choices=[MagicMock(message=MagicMock(content=json.dumps(_make_verdict_response(90.0))))] ) guardrail = _make_guardrail(overall_threshold=80.0) inputs = {"texts": ["good response"]} - request_data: dict = { - "messages": [{"role": "user", "content": "hi"}], - "metadata": {}, - } + request_data: dict = {"messages": [{"role": "user", "content": "hi"}], "metadata": {}} result = await guardrail.apply_guardrail(inputs, request_data, "response") assert result is inputs assert request_data["metadata"]["eval_information"]["passed"] is True @@ -207,11 +174,7 @@ async def test_apply_guardrail_passes_above_threshold(mock_completion): @patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.acompletion") async def test_apply_guardrail_blocks_below_threshold(mock_completion): mock_completion.return_value = MagicMock( - choices=[ - MagicMock( - message=MagicMock(content=json.dumps(_make_verdict_response(50.0))) - ) - ] + choices=[MagicMock(message=MagicMock(content=json.dumps(_make_verdict_response(50.0))))] ) guardrail = _make_guardrail(overall_threshold=80.0, on_failure="block") inputs = {"texts": ["bad response"]} @@ -225,11 +188,7 @@ async def test_apply_guardrail_blocks_below_threshold(mock_completion): @patch("litellm.proxy.guardrails.guardrail_hooks.llm_as_a_judge.litellm.acompletion") async def test_apply_guardrail_log_mode_does_not_block(mock_completion): mock_completion.return_value = MagicMock( - choices=[ - MagicMock( - message=MagicMock(content=json.dumps(_make_verdict_response(50.0))) - ) - ] + choices=[MagicMock(message=MagicMock(content=json.dumps(_make_verdict_response(50.0))))] ) guardrail = _make_guardrail(overall_threshold=80.0, on_failure="log") inputs = {"texts": ["bad response"]} diff --git a/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py b/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py index f7b4eb762a6..6daa3e1430d 100644 --- a/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py +++ b/tests/test_litellm/proxy/guardrails/test_qostodian_nexus_guardrail.py @@ -232,9 +232,9 @@ def test_qostodian_nexus_builtin_extra_headers(): ] for header in expected_headers: - assert ( - header in instance.extra_headers - ), f"Expected built-in header '{header}' to be in extra_headers" + assert header in instance.extra_headers, ( + f"Expected built-in header '{header}' to be in extra_headers" + ) def test_qostodian_nexus_extra_headers_merged(): diff --git a/tests/test_litellm/proxy/hooks/test_batch_file_validation.py b/tests/test_litellm/proxy/hooks/test_batch_file_validation.py index c26e90108f8..7f1006543bb 100644 --- a/tests/test_litellm/proxy/hooks/test_batch_file_validation.py +++ b/tests/test_litellm/proxy/hooks/test_batch_file_validation.py @@ -14,6 +14,7 @@ from fastapi import HTTPException from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + # --------------------------------------------------------------------------- # Token counter — covers all three batch payload shapes # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py b/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py index 4686c5e0475..0855c201945 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py +++ b/tests/test_litellm/proxy/management_endpoints/test_activity_tenant_scoping.py @@ -14,6 +14,7 @@ import pytest from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + # --------------------------------------------------------------------------- # /team/daily/activity — per-team admin/permission requirement # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py index 8f5e1da995b..b15b9d622e4 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py @@ -11,6 +11,7 @@ import litellm.proxy.proxy_server as ps from litellm.proxy.proxy_server import app from litellm.proxy._types import UserAPIKeyAuth, LitellmUserRoles, CommonProxyErrors + sys.path.insert( 0, os.path.abspath("../../../") ) # Adds the parent directory to the system path diff --git a/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py b/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py index 6720002c2bf..63e584e49bc 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py +++ b/tests/test_litellm/proxy/management_endpoints/test_delete_verification_tokens_failed.py @@ -26,6 +26,7 @@ from litellm.proxy.management_endpoints.key_management_endpoints import ( delete_verification_tokens, ) + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py b/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py index b3d5b7b4a0a..bd982480d60 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py +++ b/tests/test_litellm/proxy/management_endpoints/test_project_org_authz.py @@ -14,6 +14,7 @@ from fastapi import HTTPException from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth + # --------------------------------------------------------------------------- # /project/update — _check_user_permission_for_project # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py b/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py index b0aae5ce393..443089b5f01 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_default_params.py @@ -25,6 +25,7 @@ from litellm.proxy.management_endpoints.team_endpoints import ( ) from litellm.proxy.proxy_server import ProxyConfig + # --------------------------------------------------------------------------- # _update_config_fields: default_team_params loaded from DB on startup # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py index bf55fd560e0..a337ff6d888 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_workflow_management_endpoints.py @@ -17,6 +17,7 @@ sys.path.insert(0, os.path.abspath("../../..")) from litellm.proxy.management_endpoints.workflow_management_endpoints import router + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py b/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py index da899cd62f4..e8a74e41dae 100644 --- a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py +++ b/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_ai_usage_chat.py @@ -17,6 +17,7 @@ from litellm.proxy.management_endpoints.usage_endpoints.ai_usage_chat import ( stream_usage_ai_chat, ) + SAMPLE_AGGREGATED_RESPONSE = { "results": [ { 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 0e7e0ba3211..c9be00afed0 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 @@ -1092,9 +1092,9 @@ class TestVertexAIPassThroughHandler: assert result is not None assert result["result"] is not None - assert ( - result["kwargs"].get("custom_llm_provider") == "gemini" - ), "Google AI Studio embedContent URLs must set custom_llm_provider=gemini, not vertex_ai" + assert result["kwargs"].get("custom_llm_provider") == "gemini", ( + "Google AI Studio embedContent URLs must set custom_llm_provider=gemini, not vertex_ai" + ) assert result["kwargs"].get("model") == "gemini-embedding-2-preview" mock_completion_cost.assert_called_once() diff --git a/tests/test_litellm/proxy/test_fastapi_offline_routes.py b/tests/test_litellm/proxy/test_fastapi_offline_routes.py index 7788e54b0b7..f3fc3d3ea28 100644 --- a/tests/test_litellm/proxy/test_fastapi_offline_routes.py +++ b/tests/test_litellm/proxy/test_fastapi_offline_routes.py @@ -1,7 +1,7 @@ """ Unit test for testing /routes endpoint with FastAPIOffline app initialization. -This test verifies that the /routes endpoint works correctly when the proxy +This test verifies that the /routes endpoint works correctly when the proxy server is initialized using FastAPIOffline instead of regular FastAPI. """ diff --git a/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py b/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py index 5d1767bfb02..2d8a9f30c1b 100644 --- a/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py +++ b/tests/test_litellm/proxy/test_filter_models_by_team_access_group.py @@ -233,6 +233,4 @@ async def test_filter_db_fallback_receives_resolved_model_names(): "gpt-4o", "gpt-5", }, f"DB query should receive resolved model names, got {queried_names}" - assert ( - "Group-A" not in queried_names - ), "Raw access group name should not be in DB query" + assert "Group-A" not in queried_names, "Raw access group name should not be in DB query" diff --git a/tests/test_litellm/proxy/test_model_level_guardrails.py b/tests/test_litellm/proxy/test_model_level_guardrails.py index 993f8b918dd..3d74edd772b 100644 --- a/tests/test_litellm/proxy/test_model_level_guardrails.py +++ b/tests/test_litellm/proxy/test_model_level_guardrails.py @@ -19,6 +19,7 @@ from litellm.proxy.utils import ( _merge_guardrails_with_existing, ) + # --------------------------------------------------------------------------- # Unit tests for _check_and_merge_model_level_guardrails # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/test_redis_auth_cache_flag.py b/tests/test_litellm/proxy/test_redis_auth_cache_flag.py index 3cb2f5e434c..d0cb5ec5465 100644 --- a/tests/test_litellm/proxy/test_redis_auth_cache_flag.py +++ b/tests/test_litellm/proxy/test_redis_auth_cache_flag.py @@ -16,6 +16,7 @@ import litellm.proxy.proxy_server as ps from litellm.caching.caching import RedisCache from litellm.caching.dual_cache import DualCache + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/proxy/test_team_org_move.py b/tests/test_litellm/proxy/test_team_org_move.py index ad5f9c38594..2dc961bec85 100644 --- a/tests/test_litellm/proxy/test_team_org_move.py +++ b/tests/test_litellm/proxy/test_team_org_move.py @@ -6,7 +6,6 @@ Covers the SSO/Entra scenario where: - Non-proxy-admins (team admins) must have all team members pre-added to the org, preserving the original security model (no privilege escalation via team move). """ - from unittest.mock import AsyncMock, MagicMock import pytest @@ -48,10 +47,10 @@ def _make_org(organization_id="org-1", members=None, models=None): def _make_team(team_id="team-1", member_ids=None, organization_id=None): - members = [Member(user_id=uid, role="user") for uid in (member_ids or [])] - members.append( - Member(user_id=SpecialProxyStrings.default_user_id.value, role="admin") - ) + members = [ + Member(user_id=uid, role="user") for uid in (member_ids or []) + ] + members.append(Member(user_id=SpecialProxyStrings.default_user_id.value, role="admin")) return LiteLLM_TeamTable( team_id=team_id, team_alias="test-team", @@ -121,18 +120,12 @@ class TestValidateTeamOrgChange: team = _make_team(member_ids=["u1"], organization_id="org-1") org = _make_org(organization_id="org-1") - assert ( - validate_team_org_change( - team=team, organization=org, llm_router=router, is_proxy_admin=False - ) - is True - ) - assert ( - validate_team_org_change( - team=team, organization=org, llm_router=router, is_proxy_admin=True - ) - is True - ) + assert validate_team_org_change( + team=team, organization=org, llm_router=router, is_proxy_admin=False + ) is True + assert validate_team_org_change( + team=team, organization=org, llm_router=router, is_proxy_admin=True + ) is True def test_default_user_excluded_from_membership_check(self): """default_user_id is never checked for org membership.""" @@ -156,7 +149,6 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock() import litellm.proxy.management_endpoints.team_endpoints as te - original = te.add_member_to_organization te.add_member_to_organization = mock_add @@ -182,7 +174,6 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock() import litellm.proxy.management_endpoints.team_endpoints as te - original = te.add_member_to_organization te.add_member_to_organization = mock_add @@ -206,7 +197,6 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock() import litellm.proxy.management_endpoints.team_endpoints as te - original = te.add_member_to_organization te.add_member_to_organization = mock_add @@ -229,7 +219,6 @@ class TestAutoAddTeamMembersToOrg: mock_add = AsyncMock(side_effect=Exception("duplicate key")) import litellm.proxy.management_endpoints.team_endpoints as te - original = te.add_member_to_organization te.add_member_to_organization = mock_add diff --git a/tests/test_litellm/responses/test_responses_api_bridge_flag.py b/tests/test_litellm/responses/test_responses_api_bridge_flag.py index ca3a2a8a64e..463af6562f1 100644 --- a/tests/test_litellm/responses/test_responses_api_bridge_flag.py +++ b/tests/test_litellm/responses/test_responses_api_bridge_flag.py @@ -151,7 +151,9 @@ class TestUseResponsesApiBridgeFlag: output=[ {"type": "message", "content": [{"type": "text", "text": "Answer"}]} ], - usage=ResponseAPIUsage(input_tokens=10, output_tokens=5, total_tokens=15), + usage=ResponseAPIUsage( + input_tokens=10, output_tokens=5, total_tokens=15 + ), ) mock_call_aresponses.return_value = mock_response @@ -200,7 +202,9 @@ class TestUseResponsesApiBridgeFlag: "arguments": '{"queries": ["test query"]}', } ], - usage=ResponseAPIUsage(input_tokens=10, output_tokens=5, total_tokens=15), + usage=ResponseAPIUsage( + input_tokens=10, output_tokens=5, total_tokens=15 + ), ) second_response = ResponsesAPIResponse( id="resp_second", @@ -212,7 +216,9 @@ class TestUseResponsesApiBridgeFlag: "content": [{"type": "text", "text": "Final answer"}], } ], - usage=ResponseAPIUsage(input_tokens=20, output_tokens=10, total_tokens=30), + usage=ResponseAPIUsage( + input_tokens=20, output_tokens=10, total_tokens=30 + ), ) mock_bridge_handler.side_effect = [first_response, second_response] @@ -261,7 +267,9 @@ class TestUseResponsesApiBridgeFlag: "content": [{"type": "text", "text": "Native response"}], } ], - usage=ResponseAPIUsage(input_tokens=10, output_tokens=5, total_tokens=15), + usage=ResponseAPIUsage( + input_tokens=10, output_tokens=5, total_tokens=15 + ), ) result = await litellm.aresponses( diff --git a/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py b/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py index 2703d7b28c6..604155e1221 100644 --- a/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py +++ b/tests/test_litellm/router_strategy/adaptive_router/test_router_dispatch.py @@ -454,7 +454,9 @@ def test_finalize_prunes_stale_adaptive_router_hooks_from_callbacks(): Router(model_list=model_list) # simulate hot-reload adaptive_hooks = [ - cb for cb in litellm.callbacks if isinstance(cb, AdaptiveRouterPostCallHook) + cb + for cb in litellm.callbacks + if isinstance(cb, AdaptiveRouterPostCallHook) ] assert len(adaptive_hooks) == 1, ( f"expected exactly one AdaptiveRouterPostCallHook after hot-reload, " diff --git a/tests/test_litellm/test_acompletion_session_reuse_e2e.py b/tests/test_litellm/test_acompletion_session_reuse_e2e.py index a6f2be09cef..79b947bb146 100644 --- a/tests/test_litellm/test_acompletion_session_reuse_e2e.py +++ b/tests/test_litellm/test_acompletion_session_reuse_e2e.py @@ -22,6 +22,7 @@ sys.path.insert(0, os.path.abspath("../../..")) import litellm + # ============================================================================ # HELPER FUNCTION # ============================================================================ diff --git a/tests/test_litellm/test_anthropic_skills_transformation.py b/tests/test_litellm/test_anthropic_skills_transformation.py index 1b41b05f6d7..1b917f08ca9 100644 --- a/tests/test_litellm/test_anthropic_skills_transformation.py +++ b/tests/test_litellm/test_anthropic_skills_transformation.py @@ -22,6 +22,7 @@ from litellm.types.llms.anthropic_skills import ( ) from litellm.types.router import GenericLiteLLMParams + FAKE_API_KEY = "sk-ant-test-key-1234" FAKE_API_BASE = "https://api.anthropic.com" diff --git a/tests/test_litellm/test_dashscope_image_generation.py b/tests/test_litellm/test_dashscope_image_generation.py index aa3c955f838..af95e2ca6b4 100644 --- a/tests/test_litellm/test_dashscope_image_generation.py +++ b/tests/test_litellm/test_dashscope_image_generation.py @@ -18,6 +18,7 @@ from litellm.llms.dashscope.image_generation.transformation import ( from litellm.types.utils import ImageObject, ImageResponse from litellm.utils import get_llm_provider + # --------------------------------------------------------------------------- # 1. Provider detection # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/test_deepseek_model_metadata.py b/tests/test_litellm/test_deepseek_model_metadata.py index 2b86b61b019..4900af5d97d 100644 --- a/tests/test_litellm/test_deepseek_model_metadata.py +++ b/tests/test_litellm/test_deepseek_model_metadata.py @@ -23,6 +23,7 @@ from litellm.utils import ( supports_response_schema, ) + # --------------------------------------------------------------------------- # Data-level tests – verify the JSON files are in sync # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/test_model_cost_aliases.py b/tests/test_litellm/test_model_cost_aliases.py index ee11ebb9702..f9f92a85cb2 100644 --- a/tests/test_litellm/test_model_cost_aliases.py +++ b/tests/test_litellm/test_model_cost_aliases.py @@ -10,6 +10,7 @@ from unittest.mock import patch from litellm import verbose_logger from litellm.litellm_core_utils.get_model_cost_map import _expand_model_aliases + # --------------------------------------------------------------------------- # Core expansion behaviour # --------------------------------------------------------------------------- diff --git a/tests/test_litellm/test_nested_drop_params.py b/tests/test_litellm/test_nested_drop_params.py index fbcd5302308..bb1305ffde4 100644 --- a/tests/test_litellm/test_nested_drop_params.py +++ b/tests/test_litellm/test_nested_drop_params.py @@ -7,6 +7,7 @@ This tests the new JSONPath-like syntax for removing nested fields. import os import sys + # Add parent directory to path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) diff --git a/tests/test_litellm/test_openai_embedding_encoding_format_default.py b/tests/test_litellm/test_openai_embedding_encoding_format_default.py index 1cf71139d87..94e4e3c81e5 100644 --- a/tests/test_litellm/test_openai_embedding_encoding_format_default.py +++ b/tests/test_litellm/test_openai_embedding_encoding_format_default.py @@ -51,7 +51,9 @@ def test_openai_embedding_encoding_format_default( @pytest.mark.parametrize("env_none", ["none", "NONE", " none "]) -def test_openai_embedding_encoding_format_env_none_omits_param(monkeypatch, env_none): +def test_openai_embedding_encoding_format_env_none_omits_param( + monkeypatch, env_none +): """LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT=none omits encoding_format (provider default).""" monkeypatch.setenv("LITELLM_DEFAULT_EMBEDDING_ENCODING_FORMAT", env_none) diff --git a/tests/test_litellm/test_router_google_genai.py b/tests/test_litellm/test_router_google_genai.py index b8179e568a7..81dd7bbdc40 100644 --- a/tests/test_litellm/test_router_google_genai.py +++ b/tests/test_litellm/test_router_google_genai.py @@ -2,7 +2,6 @@ """ Test to verify the new Google GenAI router methods """ - import asyncio import os import sys diff --git a/tests/test_litellm/test_streaming_connection_cleanup.py b/tests/test_litellm/test_streaming_connection_cleanup.py index 53bbe8abd3c..5a81a3ffb17 100644 --- a/tests/test_litellm/test_streaming_connection_cleanup.py +++ b/tests/test_litellm/test_streaming_connection_cleanup.py @@ -19,6 +19,7 @@ from litellm.llms.custom_httpx.aiohttp_transport import ( LiteLLMAiohttpTransport, ) + # ── aiohttp transport layer tests ────────────────────────────── diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index f493b84f447..65305e1a81e 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -1500,7 +1500,8 @@ class TestProxyFunctionCalling: assert result is True, "Resolvable model names work with fallback logic" # Documentation notes: - print(""" + print( + """ PROXY MODEL RESOLUTION BEHAVIOR: ✅ WORKS (with current fallback logic): @@ -1515,7 +1516,8 @@ class TestProxyFunctionCalling: 💡 SOLUTION: Use LiteLLM proxy server with proper model_list configuration that maps custom names to underlying models. - """) + """ + ) @pytest.mark.parametrize( "proxy_model_with_hints,expected_result", @@ -1877,7 +1879,8 @@ class TestProxyFunctionCalling: This test provides documentation on how the proxy server configuration would typically map custom model names to underlying models. """ - print(""" + print( + """ REAL-WORLD PROXY SERVER CONFIGURATION EXAMPLE: =============================================== @@ -1930,7 +1933,8 @@ class TestProxyFunctionCalling: - Consistent request/response format - Enhanced streaming support for function calls - """) + """ + ) # Verify that direct underlying models work as expected bedrock_models = [ @@ -2144,7 +2148,8 @@ class TestProxyFunctionCalling: This test provides documentation on how the proxy server configuration would typically map custom model names to underlying models. """ - print(""" + print( + """ REAL-WORLD PROXY SERVER CONFIGURATION EXAMPLE: =============================================== @@ -2197,7 +2202,8 @@ class TestProxyFunctionCalling: - Consistent request/response format - Enhanced streaming support for function calls - """) + """ + ) # Verify that direct underlying models work as expected bedrock_models = [ @@ -2411,7 +2417,8 @@ class TestProxyFunctionCalling: This test provides documentation on how the proxy server configuration would typically map custom model names to underlying models. """ - print(""" + print( + """ REAL-WORLD PROXY SERVER CONFIGURATION EXAMPLE: =============================================== @@ -2464,7 +2471,8 @@ class TestProxyFunctionCalling: - Consistent request/response format - Enhanced streaming support for function calls - """) + """ + ) # Verify that direct underlying models work as expected bedrock_models = [ diff --git a/tests/test_litellm/types/test_completion.py b/tests/test_litellm/types/test_completion.py index b753a8e2abb..f24b00df3fc 100644 --- a/tests/test_litellm/types/test_completion.py +++ b/tests/test_litellm/types/test_completion.py @@ -1,7 +1,7 @@ """ Tests for litellm.types.completion module -This test suite validates the CompletionRequest model and its compatibility with +This test suite validates the CompletionRequest model and its compatibility with OpenAI ChatCompletion API message formats. Usage: diff --git a/tests/test_litellm/types/test_prometheus_label_value_sanitize.py b/tests/test_litellm/types/test_prometheus_label_value_sanitize.py index d8b90197c63..9ff7eb460e0 100644 --- a/tests/test_litellm/types/test_prometheus_label_value_sanitize.py +++ b/tests/test_litellm/types/test_prometheus_label_value_sanitize.py @@ -22,7 +22,7 @@ from litellm.types.integrations.prometheus import ( # Escapes per Prometheus text format ('he said "hi"', 'he said \\"hi\\"'), (r"path\to\file", r"path\\to\\file"), - (r"quote\"slash\\", r"quote\\\"slash\\\\"), + (r'quote\"slash\\', r'quote\\\"slash\\\\'), # Non-string inputs get coerced to str first (123, "123"), (True, "True"), @@ -31,3 +31,4 @@ from litellm.types.integrations.prometheus import ( ) def test_sanitize_prometheus_label_value_expected_outputs(value, expected): assert _sanitize_prometheus_label_value(value) == expected + diff --git a/tests/test_passthrough_endpoints.py b/tests/test_passthrough_endpoints.py index bf9417912c5..47ac7511aa1 100644 --- a/tests/test_passthrough_endpoints.py +++ b/tests/test_passthrough_endpoints.py @@ -10,6 +10,7 @@ import json import os import dotenv + dotenv.load_dotenv() diff --git a/tests/vector_store_tests/rag/base_rag_tests.py b/tests/vector_store_tests/rag/base_rag_tests.py index c357965da53..2c5a2540a7e 100644 --- a/tests/vector_store_tests/rag/base_rag_tests.py +++ b/tests/vector_store_tests/rag/base_rag_tests.py @@ -124,7 +124,9 @@ class BaseRAGTest(ABC): Test document {unique_id} for RAG ingestion and query. LiteLLM provides a unified interface for 100+ LLMs. This content should be retrievable via semantic search. - """.encode("utf-8") + """.encode( + "utf-8" + ) file_data = (filename, text_content, "text/plain") ingest_options = self.get_base_ingest_options() diff --git a/tests/vector_store_tests/rag/test_rag_vertex_ai.py b/tests/vector_store_tests/rag/test_rag_vertex_ai.py index 872383899d8..c99840bb0fe 100644 --- a/tests/vector_store_tests/rag/test_rag_vertex_ai.py +++ b/tests/vector_store_tests/rag/test_rag_vertex_ai.py @@ -151,7 +151,9 @@ class TestRAGVertexAI(BaseRAGTest): Test document {unique_id} for Vertex AI RAG corpus creation. This tests the automatic corpus creation feature. The corpus should be created and the file should be uploaded successfully. - """.encode("utf-8") + """.encode( + "utf-8" + ) file_data = (filename, text_content, "text/plain") # Get base options WITHOUT corpus_id to trigger creation @@ -213,7 +215,9 @@ class TestRAGVertexAI(BaseRAGTest): text_content = f""" Test document {unique_id} for existing Vertex AI RAG corpus. This tests file upload to a pre-existing corpus. - """.encode("utf-8") + """.encode( + "utf-8" + ) file_data = (filename, text_content, "text/plain") ingest_options = self.get_base_ingest_options() diff --git a/tests/vector_store_tests/test_milvus_vector_store.py b/tests/vector_store_tests/test_milvus_vector_store.py index a0faf9d4ecd..6627f6006d1 100644 --- a/tests/vector_store_tests/test_milvus_vector_store.py +++ b/tests/vector_store_tests/test_milvus_vector_store.py @@ -12,6 +12,7 @@ import litellm from litellm.vector_stores import asearch as vector_store_asearch from litellm.vector_stores import search as vector_store_search + # Mock response from actual Milvus API MOCK_MILVUS_SEARCH_RESPONSE = { "code": 0, diff --git a/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py b/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py index 94c92c7fb94..7cace338616 100644 --- a/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py +++ b/tests/vector_store_tests/test_vertex_ai_search_api_vector_store.py @@ -7,6 +7,7 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch import litellm + # Mock response from actual Vertex AI Search API MOCK_VERTEX_SEARCH_RESPONSE = { "results": [ diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py b/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py index 9ca335430f6..8e92065c696 100644 --- a/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py +++ b/ui/litellm-dashboard/e2e_tests/fixtures/mock_llm_server/server.py @@ -12,6 +12,7 @@ from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse + app = FastAPI(title="Mock LLM Server") app.add_middleware( CORSMiddleware, From 4d6e2bc3daaff2710bee40a2322ef2460dbce6f3 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 7 May 2026 23:36:04 +0000 Subject: [PATCH 4/7] Bump wolfi-base to latest (2026-05-05) Pin the new digest published 2026-05-05: sha256:31da6565... (from sha256:3258be47...). Delta in baseline packages: zlib 1.3.2-r2 -> 1.3.2-r3. glibc stays at 2.43-r7 (still the latest available; whatever further glibc fixes for CVE-2026-5450 / CVE-2026-5928 land in -r8+ from Chainguard, this PR doesn't touch those). Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 846351839af..75e6143a329 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Base image for building -ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31 +ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9 # Runtime image -ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:3258be472764337fd13095bcbb3182da170243b5819fd67ad4c0754590588b31 +ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:31da6565f35af6401031c1d7aa91dc84ac76c5c48edd17fb90f0ed9e3173c7a9 ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a FROM $UV_IMAGE AS uvbin From f08b1b63fa713c8ada3a499515cfa8b0e6b81444 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 7 May 2026 23:56:43 +0000 Subject: [PATCH 5/7] Bump npm 11.12.1 -> 11.14.0 to clear ip-address GHSA-v2v4-37r5-5v8g Empirical grype scan of the built runtime image flagged ip-address@10.1.0 (Medium) bundled inside /usr/local/lib/node_modules/npm. npm@11.14.0 bundles ip-address@10.1.1 which carries the fix. Verified by rebuilding the image and rescanning: ip-address finding gone. Co-Authored-By: Claude Opus 4.7 (1M context) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 75e6143a329..091527eeb18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,7 +69,7 @@ FROM $LITELLM_RUNTIME_IMAGE AS runtime USER root RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile supervisor && \ - npm install -g npm@11.12.1 tar@7.5.11 glob@13.0.6 @isaacs/brace-expansion@5.0.1 brace-expansion@5.0.5 minimatch@10.2.4 diff@8.0.3 picomatch@4.0.4 && \ + npm install -g npm@11.14.0 tar@7.5.11 glob@13.0.6 @isaacs/brace-expansion@5.0.1 brace-expansion@5.0.5 minimatch@10.2.4 diff@8.0.3 picomatch@4.0.4 && \ GLOBAL="$(npm root -g)" && \ for pkg in tar glob @isaacs/brace-expansion brace-expansion minimatch diff picomatch; do \ name="${pkg##*/}"; \ From 9a2aeb0a411d179b70e871a582c7f68606e35e7b Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Fri, 8 May 2026 00:03:05 +0000 Subject: [PATCH 6/7] Drop diskcache changes from PR Revert disk_cache.py JSONDisk swap + remove test_disk_cache.py. The JSONDisk migration is backwards-incompatible (existing pickle caches become unreadable; non-JSON values raise unguarded TypeError on set) and warrants its own focused PR with a feature flag rather than riding along with the CVE/dep-bump sweep. CVE-2025-69872 remains unmitigated at the diskcache layer; users concerned about pickle-RCE on cache-dir writers can avoid Cache(type="disk") or pin a fork until upstream ships a fix. Co-Authored-By: Claude Opus 4.7 (1M context) --- litellm/caching/disk_cache.py | 25 ++++--- tests/test_litellm/caching/test_disk_cache.py | 72 ------------------- 2 files changed, 12 insertions(+), 85 deletions(-) delete mode 100644 tests/test_litellm/caching/test_disk_cache.py diff --git a/litellm/caching/disk_cache.py b/litellm/caching/disk_cache.py index a02327f7784..e32c29b3bc6 100644 --- a/litellm/caching/disk_cache.py +++ b/litellm/caching/disk_cache.py @@ -17,14 +17,14 @@ class DiskCache(BaseCache): import diskcache as dc except ModuleNotFoundError as e: raise ModuleNotFoundError( - "DiskCache requires the `diskcache` package. Install it with " - "`pip install diskcache`." + "Please install litellm with `litellm[caching]` to use disk caching." ) from e - # JSONDisk avoids pickle deserialization (CVE-2025-69872). Values - # passed to set_cache must be JSON-serializable. - cache_dir = disk_cache_dir if disk_cache_dir is not None else ".litellm_cache" - self.disk_cache = dc.Cache(cache_dir, disk=dc.JSONDisk) + # if users don't provider one, use the default litellm cache + if disk_cache_dir is None: + self.disk_cache = dc.Cache(".litellm_cache") + else: + self.disk_cache = dc.Cache(disk_cache_dir) def set_cache(self, key, value, **kwargs): if "ttl" in kwargs: @@ -43,15 +43,14 @@ class DiskCache(BaseCache): self.set_cache(key=cache_key, value=cache_value) def get_cache(self, key, **kwargs): - cached = self.disk_cache.get(key) - if cached is None: - return None - if isinstance(cached, str): + original_cached_response = self.disk_cache.get(key) + if original_cached_response: try: - return json.loads(cached) + cached_response = json.loads(original_cached_response) # type: ignore except Exception: - return cached - return cached + cached_response = original_cached_response + return cached_response + return None def batch_get_cache(self, keys: list, **kwargs): return_val = [] diff --git a/tests/test_litellm/caching/test_disk_cache.py b/tests/test_litellm/caching/test_disk_cache.py deleted file mode 100644 index 470e2064c38..00000000000 --- a/tests/test_litellm/caching/test_disk_cache.py +++ /dev/null @@ -1,72 +0,0 @@ -import os -import sys -import tempfile - -import pytest - -pytest.importorskip("diskcache") - -sys.path.insert(0, os.path.abspath("../../..")) - -from litellm.caching.disk_cache import DiskCache - - -@pytest.fixture -def tmp_cache(): - with tempfile.TemporaryDirectory() as d: - yield DiskCache(disk_cache_dir=d) - - -def test_disk_cache_uses_jsondisk(tmp_cache): - """Confirm DiskCache constructs the underlying cache with JSONDisk to - avoid pickle deserialization (CVE-2025-69872).""" - import diskcache as dc - - assert tmp_cache.disk_cache.disk.__class__ is dc.JSONDisk - - -def test_disk_cache_dict_round_trip(tmp_cache): - payload = {"response": "hello", "timestamp": 1.0} - tmp_cache.set_cache("k", payload) - assert tmp_cache.get_cache("k") == payload - - -def test_disk_cache_string_round_trip_decodes_json(tmp_cache): - """get_cache decodes JSON strings transparently for callers that stored - pre-serialized payloads.""" - tmp_cache.set_cache("k", '{"hello": "world"}') - assert tmp_cache.get_cache("k") == {"hello": "world"} - - -def test_disk_cache_string_round_trip_returns_raw_when_not_json(tmp_cache): - tmp_cache.set_cache("k", "not-json") - assert tmp_cache.get_cache("k") == "not-json" - - -def test_disk_cache_increment(tmp_cache): - tmp_cache.set_cache("counter", 42) - assert tmp_cache.increment_cache("counter", 8) == 50 - assert tmp_cache.get_cache("counter") == 50 - - -def test_disk_cache_ttl_expiry(tmp_cache): - import time - - tmp_cache.set_cache("k", "v", ttl=1) - assert tmp_cache.get_cache("k") == "v" - time.sleep(1.1) - assert tmp_cache.get_cache("k") is None - - -def test_disk_cache_delete_and_flush(tmp_cache): - tmp_cache.set_cache("a", 1) - tmp_cache.set_cache("b", 2) - tmp_cache.delete_cache("a") - assert tmp_cache.get_cache("a") is None - assert tmp_cache.get_cache("b") == 2 - tmp_cache.flush_cache() - assert tmp_cache.get_cache("b") is None - - -def test_disk_cache_missing_key_returns_none(tmp_cache): - assert tmp_cache.get_cache("never-set") is None From f74fd4382cfd72aa39b6b8ea46c0178a7108fd8c Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Fri, 8 May 2026 00:04:11 +0000 Subject: [PATCH 7/7] Drop liccheck black allowlist entry The entry was added to cover the now-reverted black 24.10.0 -> 26.3.1 bump. With the bump dropped, upstream's existing liccheck setup is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/code_coverage_tests/liccheck.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/code_coverage_tests/liccheck.ini b/tests/code_coverage_tests/liccheck.ini index ce395d73c25..5a09403c570 100644 --- a/tests/code_coverage_tests/liccheck.ini +++ b/tests/code_coverage_tests/liccheck.ini @@ -172,4 +172,3 @@ langgraph: >=1.0.10 # MIT License langgraph-prebuilt: >=1.0.8 # MIT License - https://github.com/langchain-ai/langgraph/blob/main/LICENSE pytest-rerunfailures: >=15.1 # MPL 2.0 license pytest-recording: >=0.13.4 # MIT license -black: >=24.10.0 # MIT License - https://github.com/psf/black/blob/main/LICENSE