diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index d3f9973473c..32e82551f8f 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -376,7 +376,6 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): headers: dict, requested_query_params: Optional[dict] = None, custom_body: Optional[dict] = None, - custom_raw_body: Optional[Union[str, bytes]] = None, ) -> httpx.Response: """ Make a non-streaming HTTP request @@ -390,14 +389,6 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): headers=headers, params=requested_query_params, ) - elif custom_raw_body is not None: - response = await async_client.request( - method=request.method, - url=url, - headers=headers, - params=requested_query_params, - content=custom_raw_body, - ) else: response = await async_client.request( method=request.method, @@ -416,7 +407,6 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): headers: dict, requested_query_params: Optional[dict] = None, _parsed_body: Optional[dict] = None, - custom_raw_body: Optional[Union[str, bytes]] = None, forward_multipart: bool = False, ) -> httpx.Response: """ @@ -431,14 +421,6 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): headers=headers, params=requested_query_params, ) - elif custom_raw_body is not None: - response = await async_client.request( - method=request.method, - url=url, - headers=headers, - params=requested_query_params, - content=custom_raw_body, - ) elif ( HttpPassThroughEndpointHelpers.is_multipart(request) is True and forward_multipart @@ -678,7 +660,6 @@ async def pass_through_request( # noqa: PLR0915 custom_headers: dict, user_api_key_dict: UserAPIKeyAuth, custom_body: Optional[dict] = None, - custom_raw_body: Optional[Union[str, bytes]] = None, forward_headers: Optional[bool] = False, merge_query_params: Optional[bool] = False, query_params: Optional[dict] = None, @@ -697,9 +678,6 @@ async def pass_through_request( # noqa: PLR0915 custom_headers: The custom headers user_api_key_dict: The user API key dictionary custom_body: The custom body - custom_raw_body: Exact request body bytes/str for upstream (e.g. SigV4-signed). - When set, this is sent as ``content=...`` instead of re-encoding ``custom_body`` - as JSON, so signatures and Content-Length stay consistent. forward_headers: Whether to forward headers merge_query_params: Whether to merge query params query_params: The query params @@ -758,15 +736,20 @@ async def pass_through_request( # noqa: PLR0915 str(url) ) + # SigV4-signed callers (e.g. Bedrock) attach the exact bytes that were + # signed via request.state; we must send those instead of re-encoding the + # parsed dict (hooks mutate it, breaking the signature / Content-Length). + state_raw_body: Optional[Union[str, bytes]] = getattr( + request.state, LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY, None + ) + # Skip body parsing for multipart requests - make_multipart_http_request will handle it # But if custom_body is provided (e.g., JSON parsed despite multipart content-type), use it is_multipart = ( - HttpPassThroughEndpointHelpers.is_multipart(request) - and custom_body is None - and custom_raw_body is None + HttpPassThroughEndpointHelpers.is_multipart(request) and not custom_body ) - if custom_body is not None: + if custom_body: _parsed_body = custom_body elif is_multipart: # Don't parse multipart body here - it will be handled by make_multipart_http_request @@ -908,22 +891,20 @@ async def pass_through_request( # noqa: PLR0915 ) ) else: - if custom_raw_body is not None: - req = async_client.build_request( - "POST", - url, - content=custom_raw_body, - params=requested_query_params, - headers=headers, - ) - else: - req = async_client.build_request( - "POST", - url, - json=_parsed_body, - params=requested_query_params, - headers=headers, - ) + # SigV4-signed callers (Bedrock) supply the exact pre-signed bytes; + # otherwise httpx encodes the parsed JSON dict as before. + body_kwargs: Dict[str, Any] = ( + {"content": state_raw_body} + if state_raw_body is not None + else {"json": _parsed_body} + ) + req = async_client.build_request( + "POST", + url, + params=requested_query_params, + headers=headers, + **body_kwargs, + ) response = await async_client.send(req, stream=stream) @@ -951,18 +932,28 @@ async def pass_through_request( # noqa: PLR0915 status_code=response.status_code, ) - response = ( - await HttpPassThroughEndpointHelpers.non_streaming_http_request_handler( - request=request, - async_client=async_client, + if state_raw_body is not None: + # SigV4-signed callers (Bedrock) require the exact pre-signed bytes + # to be forwarded so the signature/Content-Length stay valid. + response = await async_client.request( + method=request.method, url=url, headers=headers, - requested_query_params=requested_query_params, - _parsed_body=_parsed_body, - custom_raw_body=custom_raw_body, - forward_multipart=is_multipart, + params=requested_query_params, + content=state_raw_body, + ) + else: + response = ( + await HttpPassThroughEndpointHelpers.non_streaming_http_request_handler( + request=request, + async_client=async_client, + url=url, + headers=headers, + requested_query_params=requested_query_params, + _parsed_body=_parsed_body, + forward_multipart=is_multipart, + ) ) - ) verbose_proxy_logger.debug("response.headers= %s", response.headers) if _is_streaming_response(response) is True: @@ -1390,11 +1381,6 @@ def create_pass_through_route( LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY, None, ) - state_raw_body: Optional[Union[str, bytes]] = getattr( - request.state, - LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY, - None, - ) final_custom_body: Optional[dict] = None if isinstance(state_custom_body, dict): final_custom_body = state_custom_body @@ -1415,7 +1401,6 @@ def create_pass_through_route( ), stream=is_streaming_request or stream, custom_body=final_custom_body, - custom_raw_body=state_raw_body, cost_per_request=cast(Optional[float], param_cost_per_request), custom_llm_provider=custom_llm_provider, guardrails_config=cast(Optional[dict], param_guardrails), diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py index f9568478785..21e965fa580 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -18,7 +18,6 @@ sys.path.insert( from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( HttpPassThroughEndpointHelpers, LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY, - LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY, pass_through_request, ) from litellm.proxy.pass_through_endpoints.success_handler import ( @@ -2206,8 +2205,6 @@ async def test_create_pass_through_route_custom_body_url_target(): setattr( mock_request.state, LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY, bedrock_body ) - signed_body = json.dumps(bedrock_body) - setattr(mock_request.state, LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY, signed_body) await endpoint_func( request=mock_request, @@ -2221,7 +2218,8 @@ async def test_create_pass_through_route_custom_body_url_target(): # The critical assertion: custom_body takes precedence over # the body parsed from the raw request assert call_kwargs["custom_body"] == bedrock_body - assert call_kwargs["custom_raw_body"] == signed_body + # HeadersDict-like custom_headers (e.g. botocore SigV4) must be coerced + # to a plain dict so signed headers actually reach the upstream. assert call_kwargs["custom_headers"] == { "authorization": "AWS4-HMAC-SHA256 signed", "content-type": "application/json",