Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion litellm/proxy/hooks/responses_id_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ def _is_responses_api_create_route(request_route: str | None) -> bool:
return canonical in _RESPONSES_API_CREATE_ROUTES


async def _single_chunk(response: object) -> AsyncGenerator[object, None]:
"""Wrap one non-streaming response as a one-item async stream so the
streaming iterator hook can treat it uniformly."""
yield response


class ResponsesIDSecurity(CustomLogger):
def __init__(
self,
Expand Down Expand Up @@ -333,7 +339,13 @@ async def async_post_call_streaming_iterator_hook(
# Create a request-scoped cache for consistent encryption across streaming chunks.
request_encryption_cache: Final[dict[str, str]] = {}

async for chunk in response:
# A streaming request can still yield a single non-streaming ModelResponse
# here — e.g. when an agentic loop (websearch_interception) resolves the
# turn without streaming. Such an object is not async-iterable, so collect
# the chunks from the real stream, or fall back to the one response.
chunks = response if hasattr(response, "__aiter__") else _single_chunk(response)

async for chunk in chunks:
if (
isinstance(chunk, BaseLiteLLMOpenAIResponseObject)
and _is_responses_api_create_route(user_api_key_dict.request_route)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_litellm/test_responses_id_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,3 +1094,39 @@ async def test_stranger_forging_their_own_id_never_reaches_someone_elses_respons

assert result["response_id"] == "resp_strangerownprovideridcccccccc"
assert result["response_id"] != victim_provider_id


class TestStreamingIteratorNonStreamingResponse:
"""A streaming chat request can still hand this hook a single, non-streaming
ModelResponse — e.g. when an agentic loop (websearch_interception) resolves the
turn without streaming. That object has no __aiter__, so `async for` used to
raise `TypeError: 'async for' requires ... got ModelResponse` (#41411). The hook
must pass such a response through as one item."""
Comment on lines +1099 to +1104

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant test docstring

This docstring repeats implementation and failure context, violating the repository rule that source comments must explain complex business logic. Remove it before merging

Suggested change
class TestStreamingIteratorNonStreamingResponse:
"""A streaming chat request can still hand this hook a single, non-streaming
ModelResponse — e.g. when an agentic loop (websearch_interception) resolves the
turn without streaming. That object has no __aiter__, so `async for` used to
raise `TypeError: 'async for' requires ... got ModelResponse` (#41411). The hook
must pass such a response through as one item."""
class TestStreamingIteratorNonStreamingResponse:

Context Used: AGENTS.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


@pytest.mark.asyncio
async def test_non_streaming_response_passed_through(self, monkeypatch):
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-test-salt-key-abcdefghij")
from litellm.types.utils import Choices, Message, ModelResponse

response = ModelResponse(
choices=[Choices(index=0, message=Message(role="assistant", content="hi"))]
)
assert not hasattr(response, "__aiter__")

mock_auth = MagicMock()
mock_auth.user_id = "user-a"
mock_auth.team_id = "team-a"
mock_auth.request_route = "/chat/completions"

hook = ResponsesIDSecurity()
collected = [
out
async for out in hook.async_post_call_streaming_iterator_hook(
user_api_key_dict=mock_auth,
response=response,
request_data={},
)
]

assert len(collected) == 1
assert collected[0] is response
Loading