From 153b205d3efbb963059d9a4be90aa96dc6b284f6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 18 Aug 2026 19:37:50 -0700 Subject: [PATCH] test: build redaction and batch limiter fixtures the way production does (#37416) Two suites broke because they stood in for production objects with stand-ins that no longer answer the same way. The redaction test faked a ResponsesAPIResponse and then reassigned builtins.isinstance so the fake would pass the type check. Redaction now gates on a tuple of accepted types, and the patched isinstance only recognised the bare class, so the fake fell through to the generic branch and the assertions ran against a plain dict. Building a real ResponsesAPIResponse drops the builtins patch entirely and exercises the same type gate production takes. The batch rate limiter tests constructed _PROXY_BatchRateLimiter with parallel_request_limiter=None even though the parameter is not optional. That stayed harmless until the output-token estimate started reading the limiter, which turned it into an AttributeError. Inject the limiter the proxy injects, sharing one InternalUsageCache the way _add_proxy_hooks does. --- tests/batches_tests/test_batch_rate_limits.py | 20 ++- .../test_logging_redaction_e2e_test.py | 145 +++++++----------- 2 files changed, 64 insertions(+), 101 deletions(-) diff --git a/tests/batches_tests/test_batch_rate_limits.py b/tests/batches_tests/test_batch_rate_limits.py index 2c804d21ace..ae02c1be12c 100644 --- a/tests/batches_tests/test_batch_rate_limits.py +++ b/tests/batches_tests/test_batch_rate_limits.py @@ -27,6 +27,16 @@ from litellm.proxy.hooks.parallel_request_limiter_v3 import ( from litellm.proxy.utils import InternalUsageCache +def _build_batch_limiter() -> _PROXY_BatchRateLimiter: + internal_usage_cache = InternalUsageCache(dual_cache=DualCache()) + return _PROXY_BatchRateLimiter( + internal_usage_cache=internal_usage_cache, + parallel_request_limiter=_PROXY_MaxParallelRequestsHandler_v3( + internal_usage_cache=internal_usage_cache + ), + ) + + def get_expected_batch_file_usage(file_path: str) -> tuple[int, int]: """ Helper function to calculate expected request count and token count from a batch JSONL file. @@ -69,10 +79,7 @@ async def test_batch_rate_limits(): """ litellm._turn_on_debug() CUSTOM_LLM_PROVIDER = "openai" - BATCH_LIMITER = _PROXY_BatchRateLimiter( - internal_usage_cache=None, - parallel_request_limiter=None, - ) + BATCH_LIMITER = _build_batch_limiter() file_name = "openai_batch_completions.jsonl" _current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -580,10 +587,7 @@ async def test_batch_rate_limiter_without_user_context(tmp_path): CUSTOM_LLM_PROVIDER = "openai" # Setup - BATCH_LIMITER = _PROXY_BatchRateLimiter( - internal_usage_cache=None, - parallel_request_limiter=None, - ) + BATCH_LIMITER = _build_batch_limiter() # Create a simple batch file batch_content = """{"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hello"}]}}""" diff --git a/tests/logging_callback_tests/test_logging_redaction_e2e_test.py b/tests/logging_callback_tests/test_logging_redaction_e2e_test.py index d56d5e51b04..3b42595b959 100644 --- a/tests/logging_callback_tests/test_logging_redaction_e2e_test.py +++ b/tests/logging_callback_tests/test_logging_redaction_e2e_test.py @@ -317,110 +317,69 @@ async def test_redaction_responses_api_stream(): @pytest.mark.asyncio async def test_redaction_responses_api_with_reasoning_summary(): """Test that reasoning summary in ResponsesAPIResponse output is properly redacted""" + import litellm from litellm.litellm_core_utils.redact_messages import perform_redaction - # Create a simple mock object with output items that have reasoning summaries - class MockResponsesAPIResponse: - def __init__(self): - self.output = [ - # Reasoning item with summary - type( - "obj", - (object,), + response = litellm.ResponsesAPIResponse( + id="resp_123", + created_at=1234567890, + output=[ + { + "type": "reasoning", + "id": "rs_123", + "summary": [ { - "type": "reasoning", - "id": "rs_123", - "summary": [ - type( - "obj", - (object,), - { - "text": "This is a detailed reasoning summary that should be redacted", - "type": "summary_text", - }, - )() - ], - }, - )(), - # Message item with content - type( - "obj", - (object,), + "type": "summary_text", + "text": "This is a detailed reasoning summary that should be redacted", + } + ], + }, + { + "type": "message", + "id": "msg_123", + "status": "completed", + "role": "assistant", + "content": [ { - "type": "message", - "id": "msg_123", - "content": [ - type( - "obj", - (object,), - { - "text": "This is the actual message content", - "type": "output_text", - }, - )() - ], - }, - )(), - ] - self.reasoning = {"effort": "low", "summary": "auto"} + "type": "output_text", + "text": "This is the actual message content", + "annotations": [], + } + ], + }, + ], + reasoning={"effort": "low", "summary": "auto"}, + ) - # Mock as ResponsesAPIResponse so perform_redaction recognizes it - mock_response = MockResponsesAPIResponse() - mock_response.__class__.__name__ = "ResponsesAPIResponse" + model_call_details = { + "messages": [{"role": "user", "content": "test"}], + "prompt": "test prompt", + "input": "test input", + } - # Patch isinstance to recognize our mock as ResponsesAPIResponse - import litellm + redacted_result = perform_redaction(model_call_details, response) - original_isinstance = isinstance + assert isinstance( + redacted_result, litellm.ResponsesAPIResponse + ), "Redaction should preserve the ResponsesAPIResponse type" - def patched_isinstance(obj, cls): - if ( - cls == litellm.ResponsesAPIResponse - and obj.__class__.__name__ == "ResponsesAPIResponse" - ): - return True - return original_isinstance(obj, cls) + reasoning_item = redacted_result.output[0] + assert ( + reasoning_item.summary[0].text == "redacted-by-litellm" + ), "Reasoning summary text should be redacted" - import builtins + message_item = redacted_result.output[1] + assert ( + message_item.content[0].text == "redacted-by-litellm" + ), "Message content text should be redacted" - builtins.isinstance = patched_isinstance + assert ( + redacted_result.reasoning is None + ), "Top-level reasoning field should be None" - try: - model_call_details = { - "messages": [{"role": "user", "content": "test"}], - "prompt": "test prompt", - "input": "test input", - } - - # Perform redaction - redacted_result = perform_redaction(model_call_details, mock_response) - - # Verify reasoning summary text is redacted - reasoning_item = redacted_result.output[0] - assert ( - reasoning_item.summary[0].text == "redacted-by-litellm" - ), "Reasoning summary text should be redacted" - - # Verify message content is also redacted - message_item = redacted_result.output[1] - assert ( - message_item.content[0].text == "redacted-by-litellm" - ), "Message content text should be redacted" - - # Verify top-level reasoning field is removed - assert ( - redacted_result.reasoning is None - ), "Top-level reasoning field should be None" - - # Verify input messages are redacted - assert ( - model_call_details["messages"][0]["content"] == "redacted-by-litellm" - ), "Input messages should be redacted" - - print("✓ Reasoning summary redaction test passed") - finally: - # Restore original isinstance - builtins.isinstance = original_isinstance + assert ( + model_call_details["messages"][0]["content"] == "redacted-by-litellm" + ), "Input messages should be redacted" @pytest.mark.asyncio