diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 857adf5b9f1..f1c8add7152 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -278,6 +278,23 @@ def _has_pre_call_deployment_hook(logging_obj: LiteLLMLoggingObj) -> bool: return False +def _mask_presigned_request_headers(transformed_request: bytes | str | dict) -> bytes | str | dict: + """A pre-signed request carries its auth inside its own ``headers`` key, which + logging treats as request body (only the top-level headers channel gets masked), + so mask it here before the request is handed to ``pre_call``.""" + if not isinstance(transformed_request, dict): + return transformed_request + request_headers: Final = transformed_request.get("headers") + if not isinstance(request_headers, dict): + return transformed_request + + from litellm.litellm_core_utils.litellm_logging import ( + _get_masked_values, # pyright: ignore[reportPrivateUsage] # the shared header-masking helper has no public name + ) + + return {**transformed_request, "headers": _get_masked_values(request_headers)} + + def _aws_signing_overrides(optional_params: Mapping[str, Any], litellm_params: Mapping[str, Any]) -> Mapping[str, Any]: return MappingProxyType( { @@ -3692,7 +3709,7 @@ class BaseLLMHTTPHandler: "complete_input_dict": ( "" if isinstance(transformed_request, dict) and "streaming_media_upload" in transformed_request - else transformed_request + else _mask_presigned_request_headers(transformed_request) ), "api_base": api_base, "headers": headers, @@ -4115,7 +4132,7 @@ class BaseLLMHTTPHandler: input="", api_key="", additional_args={ - "complete_input_dict": transformed_request, + "complete_input_dict": _mask_presigned_request_headers(transformed_request), "api_base": api_base, "headers": headers, }, @@ -4194,7 +4211,7 @@ class BaseLLMHTTPHandler: input="", api_key="", additional_args={ - "complete_input_dict": transformed_request, + "complete_input_dict": _mask_presigned_request_headers(transformed_request), "api_base": api_base, "headers": headers, "batch_id": batch_id, diff --git a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py index 95dceccb2f5..6252947ef56 100644 --- a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py +++ b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py @@ -2761,6 +2761,68 @@ def test_direct_vector_store_search_debug_log_omits_stored_credentials(caplog, i assert "sk-embedding-s3cret" not in logged +@pytest.mark.asyncio +async def test_async_retrieve_batch_masks_presigned_auth_header_in_raw_request_log(): + """Regression: a pre-signed retrieve-batch request (Mistral, Bedrock) embeds its auth + header inside the transformed request, which pre_call logs verbatim as the raw request + body, so the provider key landed unmasked in raw_request_typed_dict and every + raw-request callback.""" + from litellm.litellm_core_utils.litellm_logging import Logging as LitellmLogging + from litellm.llms.mistral.batches.transformation import MistralBatchesConfig + + provider_key = "mistral-s3cret-provider-key-123456" + job_payload = { + "id": "batch-1", + "input_files": ["file-1"], + "endpoint": "/v1/ocr", + "model": "mistral-ocr-latest", + "status": "SUCCESS", + "created_at": 1_757_400_000, + } + sent_requests = [] + + def _capture(request: httpx.Request) -> httpx.Response: + sent_requests.append(request) + return httpx.Response(200, json=job_payload) + + client = AsyncHTTPHandler() + client.client = httpx.AsyncClient(transport=httpx.MockTransport(_capture)) + + logging_obj = LitellmLogging( + model="mistral/mistral-ocr-latest", + messages=[], + stream=False, + call_type="batch_retrieve", + start_time=time.time(), + litellm_call_id="batch-retrieve-call-id", + function_id="batch-retrieve-function-id", + log_raw_request_response=True, + ) + logging_obj.update_environment_variables( + model="mistral/mistral-ocr-latest", + optional_params={}, + litellm_params={"litellm_call_id": "batch-retrieve-call-id", "metadata": {}}, + ) + + result = await BaseLLMHTTPHandler().retrieve_batch( + batch_id="batch-1", + litellm_params={"api_key": provider_key}, + provider_config=MistralBatchesConfig(), + headers={}, + api_base=None, + api_key=provider_key, + logging_obj=logging_obj, + _is_async=True, + client=client, + model="mistral/mistral-ocr-latest", + ) + + assert result.id == "batch-1" + assert sent_requests[0].headers["Authorization"] == f"Bearer {provider_key}" + raw_request_body = logging_obj.model_call_details["raw_request_typed_dict"]["raw_request_body"] + assert provider_key not in json.dumps(raw_request_body) + + @pytest.mark.asyncio async def test_async_anthropic_messages_handler_carries_deployment_vertex_location_for_pricing(monkeypatch): """