fix(proxy): address Greptile review — streaming request_data, OCR backward compat, test coverage

- Pass request_data to end-of-stream process_output_streaming_response call
- Restore inputs.update() in OCR handler for third-party guardrail providers
- Add streaming end-to-end test for guardrail logging passthrough
This commit is contained in:
michelligabriele 2026-03-18 00:42:01 +01:00
parent ae454fd700
commit d8fd9a20ed
3 changed files with 111 additions and 2 deletions

View file

@ -134,12 +134,17 @@ class OCRHandler(BaseTranslation):
request_data = {}
# Add user metadata if available
if "litellm_metadata" not in request_data and user_api_key_dict is not None:
if user_api_key_dict is not None:
user_metadata = self.transform_user_api_key_dict_to_metadata(
user_api_key_dict
)
if user_metadata:
request_data["litellm_metadata"] = user_metadata
# Preserve original behavior: inject metadata into inputs for
# third-party guardrail providers that read it from there
inputs.update(user_metadata) # type: ignore
# Also store in request_data for the logging pipeline
if "litellm_metadata" not in request_data:
request_data["litellm_metadata"] = user_metadata
guardrailed_inputs = await guardrail_to_apply.apply_guardrail(
inputs=inputs,

View file

@ -459,6 +459,7 @@ class UnifiedLLMGuardrails(CustomLogger):
guardrail_to_apply=guardrail_to_apply,
litellm_logging_obj=request_data.get("litellm_logging_obj"),
user_api_key_dict=user_api_key_dict,
request_data=request_data,
)
except HTTPException as e:
if call_type is not None and CallTypes(call_type) in A2A_CALL_TYPES:

View file

@ -170,3 +170,106 @@ async def test_openai_moderation_guardrail_streaming_harmful_content():
assert exc_info.value.status_code == 400
assert "Violated OpenAI moderation policy" in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_openai_moderation_streaming_end_of_stream_request_data_passthrough():
"""Test that streaming end-of-stream guardrail info flows through to the
real request_data (Bug 1 fix for streaming path)."""
from litellm.types.llms.openai import (
OpenAIModerationResponse,
OpenAIModerationResult,
)
with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}):
openai_guardrail = OpenAIModerationGuardrail(
guardrail_name="test-openai-moderation",
event_hook="post_call",
)
unified_guardrail = UnifiedLLMGuardrails()
mock_mod_response = OpenAIModerationResponse(
id="modr-stream-test",
model="omni-moderation-latest",
results=[
OpenAIModerationResult(
flagged=False,
categories={"hate": False, "violence": False},
category_scores={"hate": 0.001, "violence": 0.002},
category_applied_input_types={"hate": [], "violence": []},
)
],
)
async def mock_stream():
import litellm
chunks_data = ["Hello", " world"]
for i, content in enumerate(chunks_data):
chunk = MagicMock(spec=ModelResponseStream)
chunk.model = "gpt-4"
choice = MagicMock()
choice.delta = MagicMock()
choice.delta.content = content
choice.finish_reason = "stop" if i == len(chunks_data) - 1 else None
chunk.choices = [choice]
yield chunk
import litellm
mock_model_response = ModelResponse(
id="mock-stream-response",
model="gpt-4",
choices=[
litellm.Choices(
index=0,
message=litellm.Message(
role="assistant", content="Hello world"
),
finish_reason="stop",
)
],
)
request_data = {
"messages": [{"role": "user", "content": "hi"}],
"guardrail_to_apply": openai_guardrail,
"metadata": {
"guardrails": ["test-openai-moderation"],
"guardrail_config": {"streaming_sampling_rate": 1},
},
}
with patch.object(
openai_guardrail, "async_make_request", return_value=mock_mod_response
), patch(
"litellm.llms.openai.chat.guardrail_translation.handler.stream_chunk_builder",
return_value=mock_model_response,
):
user_api_key_dict = UserAPIKeyAuth(
api_key="test", request_route="/chat/completions"
)
async for _ in unified_guardrail.async_post_call_streaming_iterator_hook(
user_api_key_dict=user_api_key_dict,
response=mock_stream(),
request_data=request_data,
):
pass
# Verify guardrail info reached the REAL request_data (not a throwaway)
guardrail_info_list = request_data["metadata"].get(
"standard_logging_guardrail_information"
)
assert guardrail_info_list is not None, (
"Guardrail info should be in request_data after streaming"
)
info = guardrail_info_list[0]
assert info["guardrail_status"] == "success"
# Full moderation response dict, NOT the simplified "allow" string
guardrail_resp = info["guardrail_response"]
assert isinstance(guardrail_resp, dict), (
f"Expected full moderation response dict, got {type(guardrail_resp)}: {guardrail_resp}"
)
assert "results" in guardrail_resp