diff --git a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py index 84c6b220e62..1036acddc13 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py +++ b/litellm/proxy/guardrails/guardrail_hooks/headroom/headroom.py @@ -390,6 +390,24 @@ class HeadroomGuardrail(CustomGuardrail): value: Final = headers.get(BYPASS_HEADER) return str(value).lower() == "true" + def _record_nothing_to_compress(self, request_data: dict[str, object]) -> None: + """Log a zero-savings run when every message is protected. + + Without an entry, a request headroom looked at and found nothing to + compress is indistinguishable in the spend logs and on the cost + optimization dashboard from one where headroom never ran at all. + """ + now = time.time() + self.add_standard_logging_guardrail_information_to_request_data( + guardrail_json_response={"tokens_before": 0, "tokens_after": 0, "tokens_saved": 0}, + request_data=request_data, + guardrail_status="success", + guardrail_provider=HEADROOM_GUARDRAIL_PROVIDER, + start_time=now, + end_time=now, + duration=0.0, + ) + def _request_headers(self) -> dict[str, str]: headers: Final[dict[str, str]] = {"Content-Type": "application/json"} if self.headroom_api_key: @@ -643,6 +661,7 @@ class HeadroomGuardrail(CustomGuardrail): protected_indices: Final = _protected_indices(messages) compressible: Final = [m for i, m in enumerate(messages) if i not in protected_indices] if not compressible: + self._record_nothing_to_compress(request_data) return inputs model: Final = self.headroom_model or request_data.get("model") 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 7a2772ce78c..661793d47e3 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_headroom.py @@ -2021,6 +2021,31 @@ async def test_nothing_compressible_returns_inputs_untouched(guardrail: Headroom assert result is inputs +@pytest.mark.asyncio +async def test_nothing_compressible_is_logged_as_a_zero_savings_run(guardrail: HeadroomGuardrail): + """A single-turn request never reaches /v1/compress, so without an entry the + cost optimization dashboard cannot tell it apart from a request headroom was + never applied to; both read as no compression at all.""" + request_data: dict = {"model": "gpt-4o"} + inputs = GenericGuardrailAPIInputs( + texts=["A" * 5000], + structured_messages=[ + {"role": "system", "content": "sys"}, + {"role": "user", "content": "A" * 5000}, + ], + ) + + with patch.object(guardrail.async_handler, "post", new_callable=AsyncMock): + await guardrail.apply_guardrail(inputs=inputs, request_data=request_data, input_type="request") + + entries = _recorded_guardrail_entries(request_data) + assert len(entries) == 1 + assert entries[0]["guardrail_provider"] == "headroom" + assert entries[0]["guardrail_status"] == "success" + assert entries[0]["guardrail_response"] == {"tokens_before": 0, "tokens_after": 0, "tokens_saved": 0} + assert extract_compression_saved_tokens({"guardrail_information": entries}) == 0 + + @pytest.mark.asyncio async def test_fail_open_returns_the_caller_inputs_object(): """Translation handlers detect a rewrite by object identity, so a request