diff --git a/.circleci/config.yml b/.circleci/config.yml index f4c5573ddd..62e5b77dc6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1457,6 +1457,7 @@ jobs: # - run: python ./tests/documentation_tests/test_general_setting_keys.py - run: python ./tests/code_coverage_tests/check_licenses.py - run: python ./tests/code_coverage_tests/router_code_coverage.py + - run: python ./tests/code_coverage_tests/info_log_check.py - run: python ./tests/code_coverage_tests/test_ban_set_verbose.py - run: python ./tests/code_coverage_tests/code_qa_check_tests.py - run: python ./tests/code_coverage_tests/test_proxy_types_import.py diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py b/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py index 6735998960..e290013248 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/llm_guard.py @@ -63,7 +63,7 @@ class _ENTERPRISE_LLMGuard(CustomLogger): analyze_url, json=analyze_payload ) as response: redacted_text = await response.json() - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"LLM Guard: Received response - {redacted_text}" ) if redacted_text is not None: diff --git a/litellm/_redis.py b/litellm/_redis.py index 8371ef5bbc..bcb305985f 100644 --- a/litellm/_redis.py +++ b/litellm/_redis.py @@ -142,7 +142,10 @@ def create_gcp_iam_redis_connect_func( """ def iam_connect(self): """Initialize the connection and authenticate using GCP IAM""" - from redis.exceptions import AuthenticationError, AuthenticationWrongNumberOfArgsError + from redis.exceptions import ( + AuthenticationError, + AuthenticationWrongNumberOfArgsError, + ) from redis.utils import str_if_bytes self._parser.on_connect(self) @@ -395,7 +398,7 @@ def get_redis_async_client( # Handle GCP IAM authentication for async clusters redis_connect_func = cluster_kwargs.pop("redis_connect_func", None) from litellm import get_secret_str - + # Get GCP service account - first try from redis_connect_func, then from environment gcp_service_account = None if redis_connect_func and hasattr(redis_connect_func, '_gcp_service_account'): @@ -403,22 +406,22 @@ def get_redis_async_client( else: gcp_service_account = redis_kwargs.get("gcp_service_account") or get_secret_str("REDIS_GCP_SERVICE_ACCOUNT") - verbose_logger.info(f"DEBUG: Redis cluster kwargs: redis_connect_func={redis_connect_func is not None}, gcp_service_account_provided={gcp_service_account is not None}") + verbose_logger.debug(f"DEBUG: Redis cluster kwargs: redis_connect_func={redis_connect_func is not None}, gcp_service_account_provided={gcp_service_account is not None}") # If GCP IAM is configured (indicated by redis_connect_func), generate access token and use as password if redis_connect_func and gcp_service_account: - verbose_logger.info("DEBUG: Generating IAM token for service account (value not logged for security reasons)") + verbose_logger.debug("DEBUG: Generating IAM token for service account (value not logged for security reasons)") try: # Generate IAM access token using the helper function access_token = _generate_gcp_iam_access_token(gcp_service_account) cluster_kwargs["password"] = access_token - verbose_logger.info("DEBUG: Successfully generated GCP IAM access token for async Redis cluster") + verbose_logger.debug("DEBUG: Successfully generated GCP IAM access token for async Redis cluster") except Exception as e: verbose_logger.error(f"Failed to generate GCP IAM access token: {e}") from redis.exceptions import AuthenticationError raise AuthenticationError("Failed to generate GCP IAM access token") else: - verbose_logger.info(f"DEBUG: Not using GCP IAM auth - redis_connect_func={redis_connect_func is not None}, gcp_service_account={gcp_service_account}") + verbose_logger.debug(f"DEBUG: Not using GCP IAM auth - redis_connect_func={redis_connect_func is not None}, gcp_service_account_provided={gcp_service_account is not None}") new_startup_nodes: List[ClusterNode] = [] diff --git a/litellm/integrations/cloudzero/cloudzero.py b/litellm/integrations/cloudzero/cloudzero.py index ab4ec234bf..ca15962b72 100644 --- a/litellm/integrations/cloudzero/cloudzero.py +++ b/litellm/integrations/cloudzero/cloudzero.py @@ -123,7 +123,7 @@ class CloudZeroLogger(CustomLogger): ) if data.is_empty(): - verbose_logger.info("CloudZero Logger: No usage data found to export") + verbose_logger.debug("CloudZero Logger: No usage data found to export") return verbose_logger.debug(f"CloudZero Logger: Processing {len(data)} records") @@ -146,7 +146,7 @@ class CloudZeroLogger(CustomLogger): verbose_logger.debug(f"CloudZero Logger: Transmitting {len(cbf_data)} records to CloudZero") streamer.send_batched(cbf_data, operation=operation) - verbose_logger.info(f"CloudZero Logger: Successfully exported {len(cbf_data)} records to CloudZero") + verbose_logger.debug(f"CloudZero Logger: Successfully exported {len(cbf_data)} records to CloudZero") except Exception as e: verbose_logger.error(f"CloudZero Logger: Error exporting usage data: {str(e)}") @@ -218,7 +218,7 @@ class CloudZeroLogger(CustomLogger): unique_services = len(set(record.get('resource/service', '') for record in cbf_data_dict if record.get('resource/service'))) total_tokens = sum(record.get('usage/amount', 0) for record in cbf_data_dict) - verbose_logger.info(f"CloudZero Logger: Dry run completed for {len(cbf_data)} records") + verbose_logger.debug(f"CloudZero Logger: Dry run completed for {len(cbf_data)} records") return { "usage_data": usage_data_sample, diff --git a/litellm/integrations/weights_biases.py b/litellm/integrations/weights_biases.py index 63d87c9bd9..0d011e26ae 100644 --- a/litellm/integrations/weights_biases.py +++ b/litellm/integrations/weights_biases.py @@ -44,7 +44,7 @@ try: request, response, time_elapsed ) else: - logger.info(f"Unknown OpenAI response object: {response['object']}") + logger.debug(f"Unknown OpenAI response object: {response['object']}") except Exception as e: logger.warning(f"Failed to resolve request/response: {e}") return None diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 6a203748c9..19d7c5512b 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -10,7 +10,6 @@ import subprocess import sys import time import traceback -import fastuuid as uuid from datetime import datetime as dt_object from functools import lru_cache from typing import ( @@ -27,6 +26,7 @@ from typing import ( cast, ) +import fastuuid as uuid from httpx import Response from pydantic import BaseModel @@ -4504,7 +4504,7 @@ def get_standard_logging_object_payload( def emit_standard_logging_payload(payload: StandardLoggingPayload): if os.getenv("LITELLM_PRINT_STANDARD_LOGGING_PAYLOAD"): - verbose_logger.info(json.dumps(payload, indent=4)) + print(json.dumps(payload, indent=4)) # noqa def get_standard_logging_metadata( diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 6de2931356..91d04e84a7 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -470,7 +470,7 @@ def _transform_request_body( metadata = litellm_params["metadata"] if "requester_metadata" in metadata: rm = metadata["requester_metadata"] - labels = {k: v for k, v in rm.items() if type(v) is str} + labels = {k: v for k, v in rm.items() if isinstance(v, str)} filtered_params = { k: v for k, v in optional_params.items() if k in config_fields diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 5f363f36da..3277f19c7d 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -154,7 +154,7 @@ class DBSpendUpdateWriter: prisma_client=prisma_client, ) else: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "disable_spend_logs=True. Skipping writing spend logs to db. Other spend updates - Key/User/Team table will still occur." ) @@ -252,7 +252,7 @@ class DBSpendUpdateWriter: ) ) except Exception as e: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "\033[91m" + f"Update User DB call failed to execute {str(e)}\n{traceback.format_exc()}" ) @@ -294,7 +294,7 @@ class DBSpendUpdateWriter: except Exception: pass except Exception as e: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Update Team DB failed to execute - {str(e)}\n{traceback.format_exc()}" ) raise e @@ -320,7 +320,7 @@ class DBSpendUpdateWriter: ) ) except Exception as e: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Update Org DB failed to execute - {str(e)}\n{traceback.format_exc()}" ) raise e @@ -331,7 +331,7 @@ class DBSpendUpdateWriter: prisma_client: Optional[PrismaClient] = None, spend_logs_url: Optional[str] = os.getenv("SPEND_LOGS_URL"), ) -> Optional[PrismaClient]: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "Writing spend log to db - request_id: {}, spend: {}".format( payload.get("request_id"), payload.get("spend") ) @@ -959,7 +959,7 @@ class DBSpendUpdateWriter: }, ) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Processed {len(transactions_to_process)} daily {entity_type} transactions in {time.time() - start_time:.2f}s" ) @@ -1087,7 +1087,7 @@ class DBSpendUpdateWriter: return None request_status = prisma_client.get_request_status(payload) - verbose_proxy_logger.info(f"Logged request status: {request_status}") + verbose_proxy_logger.debug(f"Logged request status: {request_status}") _metadata: SpendLogsMetadata = json.loads(payload["metadata"]) usage_obj = _metadata.get("usage_object", {}) or {} if isinstance(payload["startTime"], datetime): diff --git a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py index c0bdc06dc6..689777ad5c 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py +++ b/litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py @@ -73,7 +73,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai self.api_base = api_base self.api_version = kwargs.get("api_version") or "2024-09-01" - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Initialized Azure Prompt Shield Guardrail: {guardrail_name}" ) @@ -131,7 +131,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai Raises HTTPException if content should be blocked. """ - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "Azure Prompt Shield: Running pre-call prompt scan, on call_type: %s", call_type, ) @@ -145,7 +145,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai user_prompt = self.get_user_prompt(new_messages) if user_prompt: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Azure Prompt Shield: User prompt: {user_prompt}" ) azure_prompt_shield_response = await self.async_make_request( @@ -180,7 +180,7 @@ class AzureContentSafetyPromptShieldGuardrail(AzureGuardrailBase, CustomGuardrai Raises HTTPException if response should be blocked. """ - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "Azure Prompt Shield: Running post-call response scan" ) diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index 33c1526d1c..e167e73ac4 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -232,7 +232,7 @@ class LakeraAIGuardrail(CustomGuardrail): lakera_response=lakera_guardrail_response, masked_entity_count=masked_entity_count, ) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "Lakera AI: Masked PII in messages instead of blocking request" ) else: @@ -299,7 +299,7 @@ class LakeraAIGuardrail(CustomGuardrail): lakera_response=lakera_guardrail_response, masked_entity_count=masked_entity_count, ) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "Lakera AI: Masked PII in messages instead of blocking request" ) else: diff --git a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py index fb2552471c..aad2a69ca5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py +++ b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py @@ -86,7 +86,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): if not self.api_key: raise ValueError("OpenAI Moderation: api_key is required. Set OPENAI_API_KEY environment variable or pass it in configuration.") - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Initialized OpenAI Moderation Guardrail: {guardrail_name} with model: {self.model}" ) @@ -201,7 +201,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): Raises HTTPException if content should be blocked. """ - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "OpenAI Moderation: Running pre-call prompt scan, on call_type: %s", call_type, ) @@ -219,7 +219,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): user_prompt = self.get_user_prompt(new_messages) if user_prompt: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"OpenAI Moderation: User prompt: {user_prompt[:100]}..." # Log first 100 chars for debugging ) @@ -256,7 +256,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): Raises HTTPException if content should be blocked. """ - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "OpenAI Moderation: Running moderation hook, on call_type: %s", call_type, ) @@ -295,14 +295,14 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): Raises HTTPException if response should be blocked. """ - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "OpenAI Moderation: Running post-call response scan" ) # Extract response text for moderation response_text = self._extract_response_text(response) if response_text: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"OpenAI Moderation: Response text: {response_text[:100]}..." # Log first 100 chars ) @@ -333,7 +333,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): from litellm.main import stream_chunk_builder from litellm.types.utils import TextCompletionResponse - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "OpenAI Moderation: Running streaming response scan" ) @@ -362,7 +362,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): # Extract response text for moderation response_text = self._extract_response_text(assembled_model_response) if response_text: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"OpenAI Moderation: Streaming response text: {response_text[:100]}..." # Log first 100 chars ) diff --git a/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py b/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py index c3649c712b..619323e907 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py +++ b/litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py @@ -19,7 +19,12 @@ from litellm.proxy.common_utils.callback_utils import ( add_guardrail_to_applied_guardrails_header, ) from litellm.types.guardrails import GuardrailEventHooks -from litellm.types.utils import Choices, LLMResponseTypes, ModelResponse, TextCompletionResponse +from litellm.types.utils import ( + Choices, + LLMResponseTypes, + ModelResponse, + TextCompletionResponse, +) if TYPE_CHECKING: from litellm.types.proxy.guardrails.guardrail_hooks.base import GuardrailConfigModel @@ -92,7 +97,7 @@ class PangeaHandler(CustomGuardrail): # Pass relevant kwargs to the parent class super().__init__(guardrail_name=guardrail_name, **kwargs) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Initialized Pangea Guardrail: name={guardrail_name}, recipe={pangea_input_recipe}, api_base={self.api_base}" ) @@ -147,7 +152,7 @@ class PangeaHandler(CustomGuardrail): "guardrail_name": self.guardrail_name, }, ) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Pangea Guardrail ({hook_name}): Request passed. Response: {result.get('result', {}).get('detectors')}" ) diff --git a/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py b/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py index bc3a6e1a7c..db72a9e9d2 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py +++ b/litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py @@ -64,7 +64,7 @@ class PanwPrismaAirsHandler(CustomGuardrail): ) self.profile_name = profile_name - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Initialized PANW Prisma AIRS Guardrail: {guardrail_name}" ) @@ -253,7 +253,7 @@ class PanwPrismaAirsHandler(CustomGuardrail): Raises HTTPException if content should be blocked. """ - verbose_proxy_logger.info("PANW Prisma AIRS: Running pre-call prompt scan") + verbose_proxy_logger.debug("PANW Prisma AIRS: Running pre-call prompt scan") # Extract prompt text from messages messages = data.get("messages", []) @@ -280,7 +280,7 @@ class PanwPrismaAirsHandler(CustomGuardrail): category = scan_result.get("category", "unknown") if action == "allow": - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"PANW Prisma AIRS: Response allowed (Category: {category})" ) @@ -305,7 +305,7 @@ class PanwPrismaAirsHandler(CustomGuardrail): Raises HTTPException if response should be blocked. """ - verbose_proxy_logger.info("PANW Prisma AIRS: Running post-call response scan") + verbose_proxy_logger.debug("PANW Prisma AIRS: Running post-call response scan") # Extract response text response_text = self._extract_response_text(response) @@ -331,7 +331,7 @@ class PanwPrismaAirsHandler(CustomGuardrail): category = scan_result.get("category", "unknown") if action == "allow": - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"PANW Prisma AIRS: Response allowed (Category: {category})" ) diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 8477874020..9feaa28004 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -428,7 +428,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): messages[index][ "content" ] = r # replace content with redacted string - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Presidio PII Masking: Redacted pii message: {data['messages']}" ) data["messages"] = messages @@ -513,7 +513,7 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): messages[index][ "content" ] = r # replace content with redacted string - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Presidio PII Masking: Redacted pii message: {messages}" ) kwargs["messages"] = messages diff --git a/litellm/proxy/hooks/proxy_track_cost_callback.py b/litellm/proxy/hooks/proxy_track_cost_callback.py index 77c739ab95..0fcec361e3 100644 --- a/litellm/proxy/hooks/proxy_track_cost_callback.py +++ b/litellm/proxy/hooks/proxy_track_cost_callback.py @@ -128,7 +128,7 @@ class _ProxyDBLogger(CustomLogger): user_api_key = metadata.get("user_api_key", None) if kwargs.get("cache_hit", False) is True: response_cost = 0.0 - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Cache Hit: response_cost {response_cost}, for user_id {user_id}" ) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 8a3507e239..0bf957f265 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -473,7 +473,7 @@ async def _common_key_generation_helper( # noqa: PLR0915 data = apply_enterprise_key_management_params(data, team_table) except Exception as e: - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "litellm.proxy.proxy_server.generate_key_fn(): Enterprise key management params not applied - {}".format( str(e) ) @@ -2004,7 +2004,7 @@ async def _rotate_master_key( # 2. process model table if models: decrypted_models = proxy_config.decrypt_model_list_from_db(new_models=models) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( "ABLE TO DECRYPT MODELS - len(decrypted_models): %s", len(decrypted_models) ) new_models = [] @@ -2018,9 +2018,9 @@ async def _rotate_master_key( ) if new_model: new_models.append(jsonify_object(new_model.model_dump())) - verbose_proxy_logger.info("Resetting proxy model table") + verbose_proxy_logger.debug("Resetting proxy model table") await prisma_client.db.litellm_proxymodeltable.delete_many() - verbose_proxy_logger.info("Creating %s models", len(new_models)) + verbose_proxy_logger.debug("Creating %s models", len(new_models)) await prisma_client.db.litellm_proxymodeltable.create_many( data=new_models, ) diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 2e1a684e39..9180e6100b 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -1023,7 +1023,7 @@ async def update_public_model_groups( # Save the updated config await proxy_config.save_config(new_config=config) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Updated public model groups to: {request.model_groups} by user: {user_api_key_dict.user_id}" ) @@ -1090,7 +1090,7 @@ async def update_useful_links( # Save the updated config await proxy_config.save_config(new_config=config) - verbose_proxy_logger.info( + verbose_proxy_logger.debug( f"Updated useful links to: {request.useful_links} by user: {user_api_key_dict.user_id}" ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9f1566b2e0..a5dcf0b8e4 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3548,7 +3548,7 @@ def giveup(e): return True # giveup if queuing max parallel request limits is disabled if result: - verbose_proxy_logger.info(json.dumps({"event": "giveup", "exception": str(e)})) + verbose_proxy_logger.debug(json.dumps({"event": "giveup", "exception": str(e)})) return result diff --git a/litellm/router.py b/litellm/router.py index 6255c2fdf9..1491adf4df 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -4031,7 +4031,7 @@ class Router: else: raise - verbose_router_logger.info( + verbose_router_logger.debug( f"Retrying request with num_retries: {num_retries}" ) # decides how long to sleep before retry @@ -4681,7 +4681,7 @@ class Router: elif self._has_default_fallbacks(): # default fallbacks set return True - verbose_router_logger.info( + verbose_router_logger.debug( "Content Policy Error occurred. No available fallbacks. Returning original response. model={}, content_policy_fallbacks={}".format( model, content_policy_fallbacks ) diff --git a/litellm/secret_managers/hashicorp_secret_manager.py b/litellm/secret_managers/hashicorp_secret_manager.py index e5911ffa9b..c462d0b464 100644 --- a/litellm/secret_managers/hashicorp_secret_manager.py +++ b/litellm/secret_managers/hashicorp_secret_manager.py @@ -106,7 +106,7 @@ class HashicorpSecretManager(BaseSecretManager): resp.raise_for_status() token = resp.json()["auth"]["client_token"] _lease_duration = resp.json()["auth"]["lease_duration"] - verbose_logger.info("Successfully obtained Vault token via TLS cert auth.") + verbose_logger.debug("Successfully obtained Vault token via TLS cert auth.") self.cache.set_cache( key="hcp_vault_token", value=token, ttl=_lease_duration ) diff --git a/litellm/types/llms/vertex_ai.py b/litellm/types/llms/vertex_ai.py index 353e842857..2687b79f72 100644 --- a/litellm/types/llms/vertex_ai.py +++ b/litellm/types/llms/vertex_ai.py @@ -281,7 +281,7 @@ class RequestBody(TypedDict, total=False): safetySettings: List[SafetSettingsConfig] generationConfig: GenerationConfig cachedContent: str - labels: dict[str, str] + labels: Dict[str, str] speechConfig: SpeechConfig diff --git a/tests/code_coverage_tests/info_log_check.py b/tests/code_coverage_tests/info_log_check.py new file mode 100644 index 0000000000..e8541b4358 --- /dev/null +++ b/tests/code_coverage_tests/info_log_check.py @@ -0,0 +1,337 @@ +import ast +import os +import re +from typing import List, Dict, Any + + +class SensitiveLogDetector(ast.NodeVisitor): + """ + Detects logger.info() statements that might log sensitive request/response data. + """ + + def __init__(self): + self.violations = [] + self.current_file = None + + def set_file(self, file_path: str): + """Set the current file being analyzed""" + self.current_file = file_path + + def visit_Call(self, node): + """Visit function calls to detect logger.info() with sensitive data""" + if self._is_logger_info_call(node): + # Check all arguments to the logger.info() call + for arg in node.args: + if self._contains_sensitive_data(arg): + violation = { + "file": self.current_file, + "line": node.lineno, + "call": self._get_call_string(node), + "reason": self._get_violation_reason(arg), + "arg": self._get_arg_string(arg) + } + self.violations.append(violation) + + self.generic_visit(node) + + def _is_logger_info_call(self, node) -> bool: + """Check if this is a logger.info() call""" + if not isinstance(node.func, ast.Attribute): + return False + + # Check for various logger patterns: + # logger.info(), verbose_logger.info(), verbose_proxy_logger.info(), etc. + if node.func.attr == "info": + if isinstance(node.func.value, ast.Name): + logger_name = node.func.value.id + return any(pattern in logger_name.lower() for pattern in ["logger", "log"]) + + return False + + def _contains_sensitive_data(self, arg) -> bool: + """Check if the argument might contain sensitive data""" + # Convert argument to string for analysis + arg_str = self._get_arg_string(arg).lower() + + # Skip obvious non-sensitive patterns + non_sensitive_patterns = [ + r'^["\'][\w\s\-_:.,!?]*["\']$', # Simple static strings + r'^["\'][^{%]*["\']$', # Strings without format placeholders + ] + + # Skip common safe phrases that contain sensitive keywords + safe_phrases = [ + r'request\s+(completed|finished|started|processing)', + r'response\s+(sent|received|processed)', + r'data\s+(inserted|updated|deleted|saved)\s+into', + r'(successfully|failed)\s+(request|response)', + r'(starting|ending|completed)\s+(request|response)', + r'no\s+(usage\s+)?data\s+found', + r'found\s+\d+.*records', + r'exported\s+\d+.*records', + ] + + for pattern in non_sensitive_patterns: + if re.search(pattern, arg_str): + # Check if it's a safe phrase first + for safe_pattern in safe_phrases: + if re.search(safe_pattern, arg_str, re.IGNORECASE): + return False + + # Then check if the static string mentions sensitive keywords + if not any(keyword in arg_str for keyword in + ['request', 'response', 'data', 'body', 'payload', 'token', 'auth', 'credential']): + return False + + # Direct variable/attribute patterns that are likely sensitive + sensitive_patterns = [ + r'\brequest\b(?!\s*(id|status|method))', # request but not request_id, request_status, request_method + r'\bresponse\b(?!\s*(status|code|time))', # response but not response_status, response_code + r'\bdata\b(?=[\.\[\s]|$)', # data followed by . [ space or end + r'\bbody\b(?=[\.\[\s]|$)', + r'\bpayload\b(?=[\.\[\s]|$)', + r'\bmessages?\b(?=[\.\[\s]|$)', + r'\bcontent\b(?=[\.\[\s]|$)', + r'\binput\b(?=[\.\[\s]|$)', + r'\boutput\b(?=[\.\[\s]|$)', + r'\bargs\b(?=[\.\[\s]|$)', + r'\bkwargs\b(?=[\.\[\s]|$)', + r'\bparams\b(?=[\.\[\s]|$)', + r'\bheaders\b(?=[\.\[\s]|$)', + r'\bapi_key\b', + r'\btoken\b(?!\s*(name|id))', # token but not token_name, token_id + r'\bauth\b(?=[\.\[\s]|$)', + r'\bcredentials?\b' + ] + + # Check for direct variable references with context + for pattern in sensitive_patterns: + if re.search(pattern, arg_str): + return True + + # Check for format strings that might interpolate sensitive data + if self._is_format_string_with_sensitive_data(arg): + return True + + # Check for JSON dumps or string formatting of objects + if self._is_object_serialization(arg): + return True + + return False + + def _is_format_string_with_sensitive_data(self, arg) -> bool: + """Check if this is a format string that might contain sensitive data""" + # Check for f-strings + if isinstance(arg, ast.JoinedStr): + for value in arg.values: + if isinstance(value, ast.FormattedValue): + value_str = self._get_arg_string(value.value).lower() + if any(pattern in value_str for pattern in + ['request', 'response', 'data', 'body', 'content', 'messages']): + return True + + # Check for .format() calls + if isinstance(arg, ast.Call) and isinstance(arg.func, ast.Attribute): + if arg.func.attr == "format": + # Check the base string for suspicious patterns + base_str = self._get_arg_string(arg.func.value).lower() + if "{}" in base_str or "{" in base_str: + # Check format arguments for sensitive data + for format_arg in arg.args: + format_str = self._get_arg_string(format_arg).lower() + if any(pattern in format_str for pattern in + ['request', 'response', 'data', 'body', 'content']): + return True + + return False + + def _is_object_serialization(self, arg) -> bool: + """Check if this is serializing an object that might contain sensitive data""" + arg_str = self._get_arg_string(arg) + + # Check for json.dumps() calls + if isinstance(arg, ast.Call): + if (isinstance(arg.func, ast.Attribute) and + arg.func.attr == "dumps" and + isinstance(arg.func.value, ast.Name) and + arg.func.value.id == "json"): + return True + + # Check for str() calls on potentially sensitive objects + if (isinstance(arg.func, ast.Name) and arg.func.id == "str" and + len(arg.args) > 0): + obj_str = self._get_arg_string(arg.args[0]).lower() + if any(pattern in obj_str for pattern in + ['request', 'response', 'data', 'body']): + return True + + return False + + def _get_violation_reason(self, arg) -> str: + """Get a human-readable reason for the violation""" + arg_str = self._get_arg_string(arg).lower() + + if 'request' in arg_str: + return "Potentially logging request data" + elif 'response' in arg_str: + return "Potentially logging response data" + elif any(pattern in arg_str for pattern in ['data', 'body', 'payload', 'content']): + return "Potentially logging sensitive data/body/content" + elif any(pattern in arg_str for pattern in ['messages', 'input', 'output']): + return "Potentially logging message/input/output data" + elif any(pattern in arg_str for pattern in ['api_key', 'token', 'auth', 'credentials']): + return "Potentially logging authentication data" + else: + return "Potentially logging sensitive data" + + def _get_call_string(self, node) -> str: + """Get string representation of the function call""" + try: + if hasattr(ast, 'unparse'): + return ast.unparse(node) + else: + # Fallback for older Python versions + return f"{self._get_arg_string(node.func)}(...)" + except: + return "logger.info(...)" + + def _get_arg_string(self, arg) -> str: + """Get string representation of an argument""" + try: + if hasattr(ast, 'unparse'): + return ast.unparse(arg) + else: + # Fallback for older Python versions + if isinstance(arg, ast.Name): + return arg.id + elif isinstance(arg, ast.Attribute): + return f"{self._get_arg_string(arg.value)}.{arg.attr}" + elif isinstance(arg, ast.Str): + return repr(arg.s) + elif isinstance(arg, ast.Constant): + return repr(arg.value) + else: + return str(type(arg).__name__) + except: + return "unknown" + + +def check_sensitive_logging(base_dir: str) -> List[Dict[str, Any]]: + """ + Check for logger.info() statements that might log sensitive data. + + Args: + base_dir: Base directory to scan (typically the litellm root) + + Returns: + List of violations found + """ + detector = SensitiveLogDetector() + all_violations = [] + + # Directories to scan - only main litellm codebase + scan_dirs = [ + "litellm", + "enterprise" # Include enterprise directory if it exists + ] + + # Directories to exclude (third-party code, venvs, etc.) + exclude_dirs = { + "venv", "venv313", ".venv", "env", ".env", + "node_modules", "__pycache__", ".git", + "build", "dist", ".tox", "clean_env", + "litellm_env", "myenv", "py313_env", + "venv_sip_bypass", "mypyc_env" + } + + for scan_dir in scan_dirs: + dir_path = os.path.join(base_dir, scan_dir) + if not os.path.exists(dir_path): + print(f"Warning: Directory {dir_path} does not exist, skipping.") + continue + + print(f"Scanning directory: {dir_path}") + + for root, dirs, files in os.walk(dir_path): + # Skip excluded directories + dirs[:] = [d for d in dirs if d not in exclude_dirs] + + # Skip if we're in a virtual environment or third-party directory + relative_root = os.path.relpath(root, base_dir) + if any(excluded in relative_root.split(os.sep) for excluded in exclude_dirs): + continue + + for file in files: + if file.endswith(".py"): + file_path = os.path.join(root, file) + relative_path = os.path.relpath(file_path, base_dir) + + # Skip files that are clearly third-party or generated + if any(excluded in relative_path for excluded in exclude_dirs): + continue + + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + tree = ast.parse(content) + + detector.set_file(relative_path) + detector.visit(tree) + + except SyntaxError as e: + print(f"Warning: Syntax error in file {relative_path}: {e}") + continue + except UnicodeDecodeError as e: + print(f"Warning: Unicode decode error in file {relative_path}: {e}") + continue + except Exception as e: + print(f"Warning: Error processing file {relative_path}: {e}") + continue + + return detector.violations + + +def main(): + """Main function to run the sensitive logging check""" + # Get the base directory (assume we're running from tests/code_coverage_tests/) + ################### + # Running locally + ################### + # current_dir = os.path.dirname(os.path.abspath(__file__)) + # base_dir = os.path.join(current_dir, "..", "..") + # base_dir = os.path.abspath(base_dir) + + ################### + # Running in CI/CD + ################### + base_dir = "./litellm" # Adjust this path as needed + + print(f"Checking for sensitive logging in: {base_dir}") + + violations = check_sensitive_logging(base_dir) + + if violations: + print(f"\n❌ Found {len(violations)} potential violations:") + print("=" * 80) + + for i, violation in enumerate(violations, 1): + print(f"\n{i}. {violation['file']}:{violation['line']}") + print(f" Reason: {violation['reason']}") + print(f" Call: {violation['call']}") + print(f" Argument: {violation['arg']}") + + print("\n" + "=" * 80) + print("⚠️ SECURITY WARNING:") + print("These logger.info() statements may log sensitive request/response data.") + print("Consider changing them to logger.debug() or removing sensitive data.") + print("This is critical for PII compliance and security.") + print("Please contact @ishaan-jaff for more details about this check. DO NOT VIOLATE THIS CHECK.") + + return 1 # Exit with error code + else: + print("\n✅ No sensitive logging violations found!") + return 0 + + +if __name__ == "__main__": + exit(main())