From 29d52cd855615788dbf3dd746b6597c124c7f745 Mon Sep 17 00:00:00 2001 From: aayush598 Date: Wed, 5 Aug 2026 20:50:55 +0530 Subject: [PATCH] fix(proxy): improve Headroom 404 compression error diagnostics --- .../guardrail_hooks/headroom/headroom.py | 25 +++++++- .../guardrail_hooks/test_headroom.py | 58 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py index 61220819d486..8bfd5cca58a6 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py +++ b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py @@ -148,6 +148,27 @@ def _restore_protected_messages( ] +def _build_compress_failure_detail(status_code: int, body: str) -> dict[str, object]: + """Build error details for failed /v1/compress responses. + + Adds troubleshooting hints for known deployment-related errors while + preserving the upstream status code and response body. + """ + if status_code == 404: + return { + "status_code": status_code, + "body": body, + "hint": ( + "The Headroom compression endpoint returned HTTP 404. " + "Verify that the configured Headroom endpoint is correct and that " + "the compression endpoint is available. If you are using a " + "self-hosted deployment, some deployments require enabling remote " + "compression (for example, HEADROOM_COMPRESS_ALLOW_REMOTE=1)." + ), + } + return {"status_code": status_code, "body": body} + + def extract_hashes_from_messages(messages: list[dict[str, object]]) -> list[str]: hashes: Final[list[str]] = [] for msg in messages: @@ -417,7 +438,7 @@ async def _call_compress( self._handle_compress_failure( messages, "Headroom compression service returned an error", - {"status_code": e.response.status_code, "body": e.response.text}, + _build_compress_failure_detail(e.response.status_code, e.response.text), ), False, {}, @@ -449,7 +470,7 @@ async def _call_compress( self._handle_compress_failure( messages, "Headroom compression service returned an error", - {"status_code": response.status_code, "body": response.text}, + _build_compress_failure_detail(response.status_code, response.text), ), False, {}, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py index 00ab39357b49..7a2772ce78c0 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py @@ -1040,6 +1040,64 @@ async def test_apply_guardrail_http_status_error_raises(): assert exc_info.value.status_code == 502 +@pytest.mark.asyncio +async def test_apply_guardrail_404_error_includes_troubleshooting_hint(): + """404 responses include a troubleshooting hint for self-hosted Headroom deployments.""" + guardrail = _make_guardrail() + + inputs = GenericGuardrailAPIInputs( + texts=["hello"], + structured_messages=ORIGINAL_MESSAGES, + ) + + with patch.object( + guardrail.async_handler, + "post", + new_callable=AsyncMock, + side_effect=_make_http_status_error(404, "Not Found"), + ): + with pytest.raises(HTTPException) as exc_info: + await guardrail.apply_guardrail( + inputs=inputs, + request_data={}, + input_type="request", + ) + + assert exc_info.value.status_code == 502 + assert exc_info.value.detail["status_code"] == 404 + assert exc_info.value.detail["body"] == "Not Found" + assert "hint" in exc_info.value.detail + assert "HEADROOM_COMPRESS_ALLOW_REMOTE=1" in exc_info.value.detail["hint"] + + +@pytest.mark.asyncio +async def test_apply_guardrail_non_404_error_omits_troubleshooting_hint(): + guardrail = _make_guardrail() + + inputs = GenericGuardrailAPIInputs( + texts=["hello"], + structured_messages=ORIGINAL_MESSAGES, + ) + + with patch.object( + guardrail.async_handler, + "post", + new_callable=AsyncMock, + side_effect=_make_http_status_error(500, "headroom internal error"), + ): + with pytest.raises(HTTPException) as exc_info: + await guardrail.apply_guardrail( + inputs=inputs, + request_data={}, + input_type="request", + ) + + assert exc_info.value.status_code == 502 + assert exc_info.value.detail["status_code"] == 500 + assert exc_info.value.detail["body"] == "headroom internal error" + assert "hint" not in exc_info.value.detail + + @pytest.mark.asyncio async def test_apply_guardrail_http_status_error_fail_open_forwards_uncompressed(): guardrail = _make_guardrail(unreachable_fallback="fail_open")