diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 7f1ff5298ba..db821f42a90 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -4,6 +4,7 @@ import ssl from collections.abc import AsyncGenerator, AsyncIterator, Coroutine, Iterator, Mapping, Sequence from contextlib import asynccontextmanager from functools import lru_cache +from itertools import chain from types import MappingProxyType, ModuleType from typing import ( TYPE_CHECKING, @@ -5613,18 +5614,29 @@ class BaseLLMHTTPHandler: } internal_keys: Final = {"litellm_logging_obj"} - kwargs_for_followup: Final = { - k: v - for k, v in kwargs.items() - if not is_interception_internal_key(k, prefixes=NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES) - and k != "_code_interpreter_interception_converted_stream" - and k not in internal_keys - and k not in optional_params - } - kwargs_for_followup.update(patch.kwargs) - kwargs_for_followup["_agentic_loop_depth"] = depth + 1 - kwargs_for_followup["max_agentic_loops"] = max_loops - kwargs_for_followup["_agentic_loop_fingerprints"] = fingerprints + [fingerprint] + kwargs_for_followup: Final = MappingProxyType( + { + key: value + for key, value in chain( + ( + (k, v) + for k, v in kwargs.items() + if not is_interception_internal_key( + k, prefixes=NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES + ) + and k != "_code_interpreter_interception_converted_stream" + and k not in internal_keys + and k not in optional_params + ), + ((k, v) for k, v in patch.kwargs.items() if k not in optional_params), + ( + ("_agentic_loop_depth", depth + 1), + ("max_agentic_loops", max_loops), + ("_agentic_loop_fingerprints", fingerprints + [fingerprint]), + ), + ) + } + ) try: response: ResponsesAPIResponse | BaseResponsesAPIStreamingIterator = await litellm.aresponses( diff --git a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py index 420adc9338e..fa4b7439dd8 100644 --- a/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py +++ b/tests/test_litellm/llms/custom_httpx/test_llm_http_handler.py @@ -4007,3 +4007,47 @@ async def test_async_realtime_bridges_a_transcription_session_through_the_provid assert events[6]["usage"] == {"type": "duration", "seconds": 2.0} assert speech_client.requests[0].streaming_config.config.model == "chirp_3" assert [bytes(request.audio) for request in speech_client.requests[1:]] == [b"\x00\x01" * 800, b"\x00\x01" * 800] + + +@pytest.mark.asyncio +async def test_responses_agentic_followup_does_not_repeat_request_params_from_plan_kwargs(monkeypatch): + """A plan whose kwargs repeat a request param must not crash the Responses follow-up with a duplicate keyword""" + from litellm.integrations.custom_logger import CustomLogger + from litellm.types.integrations.custom_logger import AgenticLoopPlan, AgenticLoopRequestPatch + + followup_calls: list[dict[str, object]] = [] + + async def fake_aresponses(**kwargs: object) -> str: + followup_calls.append(kwargs) + return "followup-response" + + monkeypatch.setattr(litellm, "aresponses", fake_aresponses) + request_kwargs: Final = {"prompt_cache_key": "thread-1", "metadata": {"user": "u1"}} + plan: Final = AgenticLoopPlan( + run_agentic_loop=True, + request_patch=AgenticLoopRequestPatch( + model="gpt-5", + messages=[{"role": "user", "content": "x"}], + optional_params={"prompt_cache_key": "thread-1"}, + kwargs=dict(request_kwargs), + ), + ) + + response: Final = await BaseLLMHTTPHandler()._execute_responses_agentic_plan( + plan=plan, + model="gpt-5", + response_api_optional_request_params={"prompt_cache_key": "thread-1"}, + logging_obj=Mock(litellm_call_id="call-1"), + kwargs=dict(request_kwargs), + depth=0, + max_loops=3, + fingerprints=[], + fingerprint="fp", + callback=CustomLogger(), + ) + + assert response == "followup-response" + assert len(followup_calls) == 1 + assert followup_calls[0]["prompt_cache_key"] == "thread-1" + assert followup_calls[0]["metadata"] == {"user": "u1"} + assert followup_calls[0]["_agentic_loop_depth"] == 1