diff --git a/tests/unit/proxy/guardrails/guardrail_hooks/test_headroom.py b/tests/unit/proxy/guardrails/guardrail_hooks/test_headroom.py index 6f4b1816150..5bdcdddf4b7 100644 --- a/tests/unit/proxy/guardrails/guardrail_hooks/test_headroom.py +++ b/tests/unit/proxy/guardrails/guardrail_hooks/test_headroom.py @@ -2008,6 +2008,45 @@ async def test_apply_guardrail_sends_textless_parts_rows_unflattened( assert wire_messages[1]["content"] == "D" * 5000 +@pytest.mark.asyncio +async def test_sdk_echoed_null_tool_calls_never_reach_compression_service(guardrail: HeadroomGuardrail): + """LIT-6921: OpenAI SDK assistant messages echoed back as history carry + ``"tool_calls": null``, which headroom-ai 0.27-0.30 answer with a 503. The + chat handler sends no row carrying that null and forwards the echoed rows + unchanged.""" + from openai.types.chat import ChatCompletionMessage + + from litellm.llms.openai.chat.guardrail_translation.handler import OpenAIChatCompletionsHandler + + def _sdk_assistant(**fields: object) -> dict: + return ChatCompletionMessage.model_validate({"role": "assistant", **fields}).model_dump() + + tool_call = {"id": "call_1", "type": "function", "function": {"name": "lookup", "arguments": "{}"}} + history = [ + {"role": "user", "content": "E" * 5000}, + _sdk_assistant(content=None, tool_calls=[tool_call]), + {"role": "tool", "tool_call_id": "call_1", "content": "R" * 5000}, + _sdk_assistant(content="summary"), + {"role": "user", "content": "F" * 5000}, + _sdk_assistant(content="noted"), + {"role": "user", "content": "and now?"}, + ] + assert history[3]["tool_calls"] is None + sent: dict = {} + + def _echo(**kwargs): + sent["messages"] = kwargs["json"]["messages"] + return _make_compress_response(json.loads(json.dumps(sent["messages"]))) + + data = {"model": "claude-sonnet-5", "messages": json.loads(json.dumps(history))} + with patch.object(guardrail.async_handler, "post", new_callable=AsyncMock, side_effect=_echo): + result = await OpenAIChatCompletionsHandler().process_input_messages(data=data, guardrail_to_apply=guardrail) + + assert [row["role"] for row in sent["messages"]] == ["user", "tool", "user"] + assert all(row.get("tool_calls", []) is not None for row in sent["messages"]) + assert [result["messages"][index] for index in (1, 3, 5)] == [history[index] for index in (1, 3, 5)] + + @pytest.mark.asyncio async def test_fail_open_returns_original_parts_shapes(): guardrail = _make_guardrail(unreachable_fallback="fail_open")