From 79a61ade5fb3ba0d8424d0e67aadb8d86184e8d1 Mon Sep 17 00:00:00 2001 From: Kerry Lu Date: Tue, 20 Oct 2026 23:59:46 +0000 Subject: [PATCH] fix(decisions): log non-UTF-8 bytes as base64 so the request reaches validation and gets a 400 (#47564) Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Backport of #47564 to stable/1.106.x Cherry-picked from ce63f48ed2ee3a18b7860b72f292e90ebeb1a986 (main) --- litellm/decisions/call.py | 2 +- tests/unit/decisions/test_call.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/litellm/decisions/call.py b/litellm/decisions/call.py index 932bfb15a91..8f58faddfd0 100644 --- a/litellm/decisions/call.py +++ b/litellm/decisions/call.py @@ -418,7 +418,7 @@ def _jsonable_fallback(value: object) -> object: def _logged_request_message(caller_request: object) -> ChatCompletionUserMessage: caller_fields: Final = _LOGGED_FIELDS_ADAPTER.validate_python( - to_jsonable_python(caller_request, fallback=_jsonable_fallback) + to_jsonable_python(caller_request, fallback=_jsonable_fallback, bytes_mode="base64") ) sent_fields: Final = json.dumps( dict((name, value) for name, value in caller_fields.items() if value not in (None, [], {})) diff --git a/tests/unit/decisions/test_call.py b/tests/unit/decisions/test_call.py index 03c135369b8..36a16dd5ad1 100644 --- a/tests/unit/decisions/test_call.py +++ b/tests/unit/decisions/test_call.py @@ -1,3 +1,4 @@ +import base64 import json from collections.abc import Mapping from datetime import datetime, timezone @@ -112,3 +113,22 @@ def test_decisions_request_is_logged_even_when_its_questions_fail_validation(): logged: Final = _logged_request(logging_obj) assert logged["questions"][0]["instructions"] == QUESTION_TEXT + + +def test_non_utf8_bytes_in_the_request_are_logged_and_rejected_with_a_400(): + logging_obj: Final = _logging_obj("adecisions") + raw_input: Final = b"\xff\xfe not utf-8" + + with pytest.raises(litellm.BadRequestError): + _prepare_decisions_call( + **CONNECTION, + decision_input=raw_input, + questions=[{"type": "predicate", "instructions": QUESTION_TEXT}], + audio=None, + videos=None, + kwargs={"litellm_logging_obj": logging_obj}, + ) + + logged: Final = _logged_request(logging_obj) + assert base64.urlsafe_b64decode(logged["input"]) == raw_input + assert logged["questions"][0]["instructions"] == QUESTION_TEXT