fix(responses-guardrails): keep a lone system message an input item when the rebuild empties input

When a guardrail's rewrite cannot be patched in place, the handler rebuilds
the request through the chat-to-Responses converter. A rebuild left holding
only the system message folded it into `instructions` over an empty `input`,
which the Responses API rejects. That already happened for string content;
folding list content made it happen for list content too, where main kept
the item. The rebuild now sends that message as a system item, as the chat
bridge already does for a system-only request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
poodle64 2026-10-10 21:02:17 +10:00
parent b118d8b22e
commit 10d23a60dd
2 changed files with 30 additions and 0 deletions

View file

@ -505,6 +505,15 @@ def _patch_or_convert_request_fields(
input_items, converted_instructions = (
LiteLLMResponsesTransformationHandler().convert_chat_completion_messages_to_responses_api(structured_messages)
)
if not input_items and converted_instructions is not None:
# The Responses API rejects an empty input, so a lone system message stays a system item, as the chat
# bridge sends a system-only request.
system_item: Final = {
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": converted_instructions}],
}
return _RequestFields(input=(system_item,), instructions=None)
return _RequestFields(input=tuple(input_items), instructions=converted_instructions)

View file

@ -3128,6 +3128,27 @@ class TestPatchEdgeBranches:
*trailing,
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"system_content",
["Answer from the memo only.", [{"type": "input_text", "text": "Answer from the memo only."}]],
)
async def test_fallback_leaving_only_the_system_message_keeps_it_an_input_item(self, system_content):
"""The Responses API rejects an empty input, so a rebuild left holding only the system
message sends it as a system item rather than as instructions over an empty input."""
handler = OpenAIResponsesHandler()
data = {
"model": "gpt-5.6",
"input": [{"role": "system", "content": system_content}, {"role": "user", "content": "memo " * 400}],
}
result = await handler.process_input_messages(data, DroppingRewriteGuardrail())
assert "instructions" not in result
assert result["input"] == [
{"type": "message", "role": "system", "content": [{"type": "input_text", "text": "Answer from the memo only."}]}
]
def test_item_rewrite_field_ignores_non_string_type(self):
from litellm.llms.openai.responses.guardrail_translation.handler import _item_rewrite_field