fix(responses): keep the system prompt in input for json_object on the chat to responses bridge (#49957)

The bridge moves leading system messages into the Responses instructions
field. With response_format json_object, Azure requires the word json in
input and ignores instructions, so a request that follows the documented
rule 400s after translation. Send the system text as a system input item
when json_object is requested, the same shape already used for
system-only requests

Resolves LIT-10426
This commit is contained in:
mrinal-berri 2026-11-04 13:55:50 -08:00 • committed by GitHub
parent 1967b41368
commit 9703e01cd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 8 deletions

View file

@ -1180,11 +1180,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
include_namespace=True,
replay_openai_history=provider == "openai",
)
# OpenAI's Responses API rejects an empty input. For a system-only
# request, carry the system message as a system-role input item instead
# of instructions, mirroring how non-string system content is already
# handled in convert_chat_completion_messages_to_responses_api.
is_system_only_request: Final = not converted_input_items and converted_instructions is not None
# Keep the system text in input instead of instructions when the Responses API needs it there.
# It rejects an empty input, and json_object requires the word "json" in input, ignoring instructions.
typed_optional_params: Final = cast("Mapping[str, object]", optional_params) # cast-ok: untyped dict
system_goes_in_input: Final = converted_instructions is not None and (
not converted_input_items or _payload_type(typed_optional_params.get("response_format")) == "json_object"
)
target_input_items: Final = (
_without_audio_input_parts(converted_input_items)
if _drops_audio_input(model, litellm_params)
@ -1196,12 +1197,13 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
"type": "message",
"role": "system",
"content": [{"type": "input_text", "text": converted_instructions}],
}
},
*target_input_items,
]
if is_system_only_request
if system_goes_in_input
else target_input_items
)
instructions: Final = None if is_system_only_request else converted_instructions
instructions: Final = None if system_goes_in_input else converted_instructions
# Build responses API request using the reverse transformation logic
responses_api_request: Final = ResponsesAPIOptionalRequestParams()

View file

@ -1856,6 +1856,51 @@ def test_transform_request_system_only_message_maps_to_system_input_item():
assert not result.get("instructions")
_JSON_SYSTEM_PROMPT: Final = "You output JSON objects."
_PLAIN_USER_ITEM: Final = {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "List three colors."}],
}
_COLORS_SCHEMA: Final = {
"type": "json_schema",
"json_schema": {"name": "colors", "schema": {"type": "object", "properties": {}}},
}
@pytest.mark.parametrize(
("response_format", "expected_input", "expected_instructions"),
[
(
{"type": "json_object"},
[
{"type": "message", "role": "system", "content": [{"type": "input_text", "text": _JSON_SYSTEM_PROMPT}]},
_PLAIN_USER_ITEM,
],
None,
),
(_COLORS_SCHEMA, [_PLAIN_USER_ITEM], _JSON_SYSTEM_PROMPT),
],
ids=["json_object_keeps_system_in_input", "json_schema_keeps_system_in_instructions"],
)
def test_transform_request_json_object_keeps_system_prompt_where_the_json_word_check_reads_it(
response_format, expected_input, expected_instructions
):
result = LiteLLMResponsesTransformationHandler().transform_request(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": _JSON_SYSTEM_PROMPT},
{"role": "user", "content": "List three colors."},
],
optional_params={"response_format": response_format},
litellm_params={},
headers={},
litellm_logging_obj=Mock(),
)
assert (result["input"], result.get("instructions")) == (expected_input, expected_instructions), result
def test_transform_request_single_char_keys_not_matched():
"""Test that single-character keys are not incorrectly matched to 'metadata' or 'previous_response_id'