From bbee8692a80bf2be2a917c88fb3fb2ba76b4e5a3 Mon Sep 17 00:00:00 2001 From: Tejas Chopra Date: Wed, 16 Sep 2026 21:27:45 -0700 Subject: [PATCH 1/2] fix(responses): stop agentic follow-up from passing request params twice --- litellm/llms/custom_httpx/llm_http_handler.py | 22 +++++----- .../custom_httpx/test_llm_http_handler.py | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 2fe4130a310..b4f2a74b3e6 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -5573,17 +5573,19 @@ 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 + **{ + 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], } - 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] 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 c39779972c0..5d6060c7666 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 @@ -3713,3 +3713,47 @@ def test_image_edit_handler_keeps_the_sync_transform(): assert config.transform_calls == ["sync"] assert captured["body"] == {"transformed_by": "sync"} assert response.data[0].b64_json == "sync" + + +@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 From 3298b416878ec8b2d409b47799f76e4f9bcbc0f7 Mon Sep 17 00:00:00 2001 From: Tejas Chopra Date: Wed, 16 Sep 2026 21:44:10 -0700 Subject: [PATCH 2/2] refactor(responses): build agentic follow-up kwargs as one frozen mapping --- litellm/llms/custom_httpx/llm_http_handler.py | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index b4f2a74b3e6..50420ab3301 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 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, Any, Final, Literal, Optional, TypedDict, TypeVar, Union, cast, get_type_hints from urllib.parse import parse_qs, urlencode, urlparse, urlunparse @@ -5572,20 +5573,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 - }, - **{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], - } + 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(