From 586f08ff5b92de262eee6deb943929c3d90044e3 Mon Sep 17 00:00:00 2001 From: Fenghuang Date: Wed, 22 Jul 2026 16:27:10 +0900 Subject: [PATCH] fix: drop empty additional_tools input items too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up Codex turns re-send the additional_tools item with an EMPTY tools list; the empty item is still a non-standard input variant strict providers reject, so it must be dropped even with nothing to merge — keying the rewrite off relocated tools alone made every turn after the first fail again. (Codex >= 26.707 'Responses Lite'; Codex issue #32086.) Co-Authored-By: Claude Fable 5 --- litellm/utils.py | 15 +++++++++++++-- tests/test_litellm/test_utils.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 252018214d4..261da68682c 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7478,19 +7478,30 @@ def relocate_input_additional_tools( the request valid AND lets filter_tools_by_allowed_types apply the deployment's allowlist to the merged list. - Returns ``(input, tools)``; both unchanged when there is nothing to move. + Follow-up Codex turns re-send the item with an EMPTY tools list (tools + were established earlier in the thread); the empty item is still a + non-standard variant strict providers reject, so it is dropped even when + it carries nothing to merge (Codex >= 26.707 "Responses Lite" + serialization; see Codex issue #32086). + + Returns ``(input, tools)``; both unchanged when no additional_tools item + is present. """ if not isinstance(input, list): return input, tools + found = False relocated: List[dict] = [] kept_items = [] for item in input: if isinstance(item, dict) and item.get("type") == "additional_tools": + found = True relocated.extend(item.get("tools") or []) else: kept_items.append(item) - if not relocated: + if not found: return input, tools + if not relocated: + return kept_items, tools merged = list(tools) if tools is not None else [] merged.extend(relocated) return kept_items, merged diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 7dcce9d7189..63d05e3b3fc 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -4976,3 +4976,14 @@ def test_relocate_input_additional_tools() -> None: # string input passes through s_input, s_tools = relocate_input_additional_tools(input="hello", tools=None) assert s_input == "hello" and s_tools is None + + # follow-up Codex turns re-send the item with an EMPTY tools list — the + # non-standard item must STILL be dropped (strict providers reject it), + # with the tools param left untouched + turn2_input = [ + {"role": "developer", "type": "additional_tools", "tools": []}, + {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "next"}]}, + ] + t2_input, t2_tools = relocate_input_additional_tools(input=turn2_input, tools=None) + assert [i.get("type") for i in t2_input] == ["message"] + assert t2_tools is None