mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
7ac764970b
commit
153b205d3e
2 changed files with 64 additions and 101 deletions
|
|
@ -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"}]}}"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue