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.
This commit is contained in:
yuneng-jiang 2026-08-18 19:37:50 -07:00 committed by GitHub
parent 7ac764970b
commit 153b205d3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 64 additions and 101 deletions

View file

@ -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"}]}}"""

View file

@ -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