From 075bb3d2458a5a967c5d09969cd77d987398a3b5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:15:36 +0000 Subject: [PATCH] fix(responses-bridge): emit annotations on output_text input items Closes #35213 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../transformation.py | 8 +- ...responses_transformation_transformation.py | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 89a44fcdeef..4af4e90b640 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -740,7 +740,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): if role == "user" or role == "system" or role == "tool": return {"type": "input_text", "text": content} else: - return {"type": "output_text", "text": content} + return {"type": "output_text", "text": content, "annotations": []} def _convert_content_to_responses_format_image( self, content: "ChatCompletionImageObject", role: str @@ -851,9 +851,9 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): "computer_screenshot", "summary_text", ]: - # Already in responses API format - result.append(item) - verbose_logger.debug(f"Chat provider: passthrough -> {item}") + passthrough = {"annotations": [], **item} if item_type == "output_text" else item + result.append(passthrough) + verbose_logger.debug(f"Chat provider: passthrough -> {passthrough}") else: # Default to input_text for unknown types converted = self._convert_content_str_to_input_text(str(item.get("text", item)), role) diff --git a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index a111b932f2c..f516758de65 100644 --- a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -2962,3 +2962,79 @@ async def test_acompletion_bridge_normalizes_stream_options_on_the_wire( assert "stream_options" not in request_body else: assert request_body["stream_options"] == expected_wire_stream_options + + +def test_assistant_history_output_text_items_include_annotations(): + """ + Regression test for issue #35213: assistant history replayed through the + chat -> responses bridge produced ``output_text`` blocks without the + ``annotations`` key, which strict Responses backends reject with + "Required property 'annotations' is missing". + """ + from openai.types.responses.response_output_text_param import ResponseOutputTextParam + from pydantic import TypeAdapter + + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) + + handler = LiteLLMResponsesTransformationHandler() + + messages = [ + {"role": "user", "content": "first question"}, + {"role": "assistant", "content": "plain string answer"}, + {"role": "user", "content": "second question"}, + {"role": "assistant", "content": [{"type": "text", "text": "list text answer"}]}, + {"role": "user", "content": "third question"}, + {"role": "assistant", "content": [{"type": "output_text", "text": "passthrough answer"}]}, + ] + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + output_text_blocks = [ + block + for item in input_items + if item.get("role") == "assistant" + for block in item["content"] + if block.get("type") == "output_text" + ] + assert len(output_text_blocks) == 3 + assert [block["text"] for block in output_text_blocks] == [ + "plain string answer", + "list text answer", + "passthrough answer", + ] + output_text_adapter = TypeAdapter(ResponseOutputTextParam) + for block in output_text_blocks: + assert block["annotations"] == [] + output_text_adapter.validate_python(block) + + +def test_assistant_history_output_text_preserves_existing_annotations(): + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + LiteLLMResponsesTransformationHandler, + ) + + annotations = [ + { + "type": "url_citation", + "url": "https://example.com", + "title": "example", + "start_index": 0, + "end_index": 5, + } + ] + handler = LiteLLMResponsesTransformationHandler() + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api( + [ + {"role": "user", "content": "hi"}, + { + "role": "assistant", + "content": [{"type": "output_text", "text": "cited", "annotations": annotations}], + }, + ] + ) + + assistant_item = next(item for item in input_items if item.get("role") == "assistant") + assert assistant_item["content"][0]["annotations"] == annotations