From e323ea243a11830677e2015c9390b3912af78457 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 28 Oct 2026 08:37:57 +0000 Subject: [PATCH] fix: preserve json mode preamble text Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/constants.py | 1 + litellm/llms/anthropic/chat/transformation.py | 8 +++++++- litellm/llms/bedrock/chat/converse_transformation.py | 11 +++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/litellm/constants.py b/litellm/constants.py index 8cc51d0ab5e..8a2e69e84d1 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1633,6 +1633,7 @@ OPENAI_FINISH_REASONS: Final = [ ] HUMANLOOP_PROMPT_CACHE_TTL_SECONDS: Final = get_env_int("HUMANLOOP_PROMPT_CACHE_TTL_SECONDS", 60) # 1 minute RESPONSE_FORMAT_TOOL_NAME = "json_tool_call" # default tool name used when converting response format to tool call +JSON_MODE_PREAMBLE_TEXT_KEY: Final = "json_mode_preamble_text" ########################### Logging Callback Constants ########################### AZURE_STORAGE_MSFT_VERSION: Final = "2019-07-07" diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 92e7c84031c..409fc963553 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -21,6 +21,7 @@ from litellm.constants import ( DEFAULT_REASONING_EFFORT_MEDIUM_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_XHIGH_THINKING_BUDGET, + JSON_MODE_PREAMBLE_TEXT_KEY, RESPONSE_FORMAT_TOOL_NAME, ) from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, map_finish_reason @@ -3056,7 +3057,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): REFUSAL_STOP_FIELDS.validate_python(completion_response) ) completion_response["stop_reason"] = "stop" - if refusal_fields: + if text_content and text_content.strip(): + json_mode_message.provider_specific_fields = { + **refusal_fields, + JSON_MODE_PREAMBLE_TEXT_KEY: text_content, + } + elif refusal_fields: json_mode_message.provider_specific_fields = dict(refusal_fields) if thinking_blocks: json_mode_message.thinking_blocks = thinking_blocks diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index f54c352fd6e..63c6d8a252b 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -19,6 +19,7 @@ import litellm from litellm._logging import verbose_logger from litellm.constants import ( BEDROCK_MIN_THINKING_BUDGET_TOKENS, + JSON_MODE_PREAMBLE_TEXT_KEY, RESPONSE_FORMAT_TOOL_NAME, ) from litellm.litellm_core_utils.core_helpers import ( @@ -2676,6 +2677,16 @@ class AmazonConverseConfig(BaseConfig): json_mode_content_str: str | None = tools[0]["function"].get("arguments") if json_mode_content_str is not None: json_mode_content_str = AmazonConverseConfig._unwrap_bedrock_properties(json_mode_content_str) + message_fields: Final = as_str_mapping(chat_completion_message) + existing_content: Final = (message_fields or {}).get("content") + if isinstance(existing_content, str) and existing_content.strip(): + existing_provider_fields: Final = TypeAdapter(dict[str, object]).validate_python( + (message_fields.get("provider_specific_fields") or {}) if message_fields is not None else {} + ) + chat_completion_message["provider_specific_fields"] = { # rebind-ok: response is built in place + **existing_provider_fields, + JSON_MODE_PREAMBLE_TEXT_KEY: existing_content, + } chat_completion_message["content"] = json_mode_content_str return None