diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index fc7a6048538..c6cdfbf44b5 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -105,6 +105,7 @@ _CHAT_COMPLETION_FIELDS: Final = frozenset((*ModelResponse.model_fields, "usage" _RESPONSES_API_ONLY_FIELDS: Final = frozenset((*Response.model_fields, *ResponsesAPIResponse.model_fields)) - frozenset( ChatCompletion.model_fields ) +_RESPONSES_INSTRUCTIONS_MAX_LENGTH: Final = 1_048_576 def _bridged_prompt_cache_breakpoint(marker: object, drop_params: bool, target_trims_breakpoints: bool) -> object: @@ -1199,9 +1200,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): ) # 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. + # It also caps instructions at 1,048,576 characters, counted here in UTF-8 bytes to stay on the safe side. 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" + not converted_input_items + or _payload_type(typed_optional_params.get("response_format")) == "json_object" + or len(converted_instructions.encode("utf-8")) > _RESPONSES_INSTRUCTIONS_MAX_LENGTH ) target_input_items: Final = ( _without_audio_input_parts(converted_input_items) diff --git a/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index 19aefb430c7..02b76549652 100644 --- a/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -1901,6 +1901,50 @@ def test_transform_request_json_object_keeps_system_prompt_where_the_json_word_c assert (result["input"], result.get("instructions")) == (expected_input, expected_instructions), result +def _system_input_item(system_prompt: str) -> dict: + return { + "type": "message", + "role": "system", + "content": [{"type": "input_text", "text": system_prompt}], + } + + +@pytest.mark.parametrize( + ("system_prompt", "expected_input", "expected_instructions"), + [ + ("You are a helpful assistant.", [_PLAIN_USER_ITEM], "You are a helpful assistant."), + ("a" * 1_048_576, [_PLAIN_USER_ITEM], "a" * 1_048_576), + ("a" * 1_048_577, [_system_input_item("a" * 1_048_577), _PLAIN_USER_ITEM], None), + ("é" * 524_289, [_system_input_item("é" * 524_289), _PLAIN_USER_ITEM], None), + ], + ids=[ + "short_system_stays_in_instructions", + "system_at_cap_stays_in_instructions", + "system_over_cap_moves_to_input", + "multibyte_system_over_cap_in_bytes_moves_to_input", + ], +) +def test_transform_request_overlong_system_prompt_moves_to_input(system_prompt, expected_input, expected_instructions): + """Overlong system prompts go in input, since OpenAI caps instructions at 1,048,576 (#50243). + + The cap is measured in UTF-8 bytes, so a prompt under the cap in characters still moves + to input when its byte length is over it. + """ + result = LiteLLMResponsesTransformationHandler().transform_request( + model="openai/gpt-6.1-terra", + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": "List three colors."}, + ], + optional_params={}, + 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'