From 885f2c2c2d7027f5601d1be016eb454418e83aa3 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:13:40 -0700 Subject: [PATCH] fix(openai): handle content blocks without type key in responses api conversion (#36725) --- .../messages/block_translators/openai.py | 7 ++- .../langchain_openai/chat_models/_compat.py | 2 + .../tests/unit_tests/chat_models/test_base.py | 46 +++++++++++++++++++ libs/partners/openai/uv.lock | 5 +- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/libs/core/langchain_core/messages/block_translators/openai.py b/libs/core/langchain_core/messages/block_translators/openai.py index be32c444911..627459254c5 100644 --- a/libs/core/langchain_core/messages/block_translators/openai.py +++ b/libs/core/langchain_core/messages/block_translators/openai.py @@ -335,10 +335,9 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage: # Reasoning if reasoning := message.additional_kwargs.get("reasoning"): - if isinstance(message, AIMessageChunk) and message.chunk_position != "last": - buckets["reasoning"].append({**reasoning, "type": "reasoning"}) - else: - buckets["reasoning"].append(reasoning) + if "type" not in reasoning: + reasoning = {**reasoning, "type": "reasoning"} + buckets["reasoning"].append(reasoning) # Refusal if refusal := message.additional_kwargs.get("refusal"): diff --git a/libs/partners/openai/langchain_openai/chat_models/_compat.py b/libs/partners/openai/langchain_openai/chat_models/_compat.py index 35f0aac554a..1a7506ccd18 100644 --- a/libs/partners/openai/langchain_openai/chat_models/_compat.py +++ b/libs/partners/openai/langchain_openai/chat_models/_compat.py @@ -407,6 +407,8 @@ def _convert_from_v1_to_responses( ) -> list[dict[str, Any]]: new_content: list = [] for block in content: + if "type" not in block: + continue if block["type"] == "text" and "annotations" in block: # Need a copy because we're changing the annotations list new_block = dict(block) diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 7b0253e93f1..481485242eb 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -2897,6 +2897,52 @@ def test_convert_from_v1_to_responses( assert message_v1 != result +def test_convert_from_v1_to_responses_missing_type() -> None: + """Regression: blocks without 'type' should be skipped, not raise KeyError.""" + content: list = [ + {"type": "text", "text": "Hello", "annotations": []}, + {"summary": [{"type": "summary_text", "text": "..."}]}, # no "type" key + {"index": 0}, # no "type" key + ] + result = _convert_from_v1_to_responses(content, []) + # Blocks without "type" should be skipped + assert len(result) == 1 + assert result[0] == {"type": "text", "text": "Hello", "annotations": []} + + +def test_v03_reasoning_without_type_roundtrip() -> None: + """Regression: v0.3 reasoning stored without 'type' key should roundtrip.""" + message_v03 = AIMessage( + content=[ + {"type": "text", "text": "Hello!", "annotations": []}, + ], + additional_kwargs={ + # Reasoning stored without "type" (as produced by streaming v0.3 path) + "reasoning": { + "id": "rs_123", + "summary": [{"type": "summary_text", "text": "Thinking..."}], + }, + }, + response_metadata={"id": "resp_123"}, + id="msg_123", + ) + + converted = _convert_from_v03_ai_message(message_v03) + + # Reasoning block should have "type" restored + reasoning_blocks = [ + b + for b in converted.content + if isinstance(b, dict) and b.get("type") == "reasoning" + ] + assert len(reasoning_blocks) == 1 + assert reasoning_blocks[0]["type"] == "reasoning" + + # Full pipeline should not raise + result = _construct_responses_api_input([converted]) + assert len(result) > 0 + + def test_get_last_messages() -> None: messages: list[BaseMessage] = [HumanMessage("Hello")] last_messages, previous_response_id = _get_last_messages(messages) diff --git a/libs/partners/openai/uv.lock b/libs/partners/openai/uv.lock index b61c087e5ef..f1ce76e045f 100644 --- a/libs/partners/openai/uv.lock +++ b/libs/partners/openai/uv.lock @@ -591,6 +591,7 @@ test = [ { name = "langchain-tests", editable = "../../standard-tests" }, { name = "pytest", specifier = ">=8.0.0,<10.0.0" }, { name = "pytest-asyncio", specifier = ">=0.23.2,<2.0.0" }, + { name = "pytest-benchmark", specifier = ">=5.1.0,<6.0.0" }, { name = "pytest-cov", specifier = ">=4.0.0,<8.0.0" }, { name = "pytest-mock" }, { name = "pytest-socket", specifier = ">=0.6.0,<1.0.0" }, @@ -614,7 +615,7 @@ typing = [ [[package]] name = "langchain-core" -version = "1.2.25" +version = "1.3.0a2" source = { editable = "../../core" } dependencies = [ { name = "jsonpatch" }, @@ -761,7 +762,7 @@ typing = [ [[package]] name = "langchain-tests" -version = "1.1.5" +version = "1.1.6" source = { editable = "../../standard-tests" } dependencies = [ { name = "httpx" },