Skip to content

Commit 885f2c2

Browse files
authored
fix(openai): handle content blocks without type key in responses api conversion (#36725)
1 parent 01a324a commit 885f2c2

4 files changed

Lines changed: 54 additions & 6 deletions

File tree

libs/core/langchain_core/messages/block_translators/openai.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,9 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
335335

336336
# Reasoning
337337
if reasoning := message.additional_kwargs.get("reasoning"):
338-
if isinstance(message, AIMessageChunk) and message.chunk_position != "last":
339-
buckets["reasoning"].append({**reasoning, "type": "reasoning"})
340-
else:
341-
buckets["reasoning"].append(reasoning)
338+
if "type" not in reasoning:
339+
reasoning = {**reasoning, "type": "reasoning"}
340+
buckets["reasoning"].append(reasoning)
342341

343342
# Refusal
344343
if refusal := message.additional_kwargs.get("refusal"):

libs/partners/openai/langchain_openai/chat_models/_compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ def _convert_from_v1_to_responses(
407407
) -> list[dict[str, Any]]:
408408
new_content: list = []
409409
for block in content:
410+
if "type" not in block:
411+
continue
410412
if block["type"] == "text" and "annotations" in block:
411413
# Need a copy because we're changing the annotations list
412414
new_block = dict(block)

libs/partners/openai/tests/unit_tests/chat_models/test_base.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2897,6 +2897,52 @@ def test_convert_from_v1_to_responses(
28972897
assert message_v1 != result
28982898

28992899

2900+
def test_convert_from_v1_to_responses_missing_type() -> None:
2901+
"""Regression: blocks without 'type' should be skipped, not raise KeyError."""
2902+
content: list = [
2903+
{"type": "text", "text": "Hello", "annotations": []},
2904+
{"summary": [{"type": "summary_text", "text": "..."}]}, # no "type" key
2905+
{"index": 0}, # no "type" key
2906+
]
2907+
result = _convert_from_v1_to_responses(content, [])
2908+
# Blocks without "type" should be skipped
2909+
assert len(result) == 1
2910+
assert result[0] == {"type": "text", "text": "Hello", "annotations": []}
2911+
2912+
2913+
def test_v03_reasoning_without_type_roundtrip() -> None:
2914+
"""Regression: v0.3 reasoning stored without 'type' key should roundtrip."""
2915+
message_v03 = AIMessage(
2916+
content=[
2917+
{"type": "text", "text": "Hello!", "annotations": []},
2918+
],
2919+
additional_kwargs={
2920+
# Reasoning stored without "type" (as produced by streaming v0.3 path)
2921+
"reasoning": {
2922+
"id": "rs_123",
2923+
"summary": [{"type": "summary_text", "text": "Thinking..."}],
2924+
},
2925+
},
2926+
response_metadata={"id": "resp_123"},
2927+
id="msg_123",
2928+
)
2929+
2930+
converted = _convert_from_v03_ai_message(message_v03)
2931+
2932+
# Reasoning block should have "type" restored
2933+
reasoning_blocks = [
2934+
b
2935+
for b in converted.content
2936+
if isinstance(b, dict) and b.get("type") == "reasoning"
2937+
]
2938+
assert len(reasoning_blocks) == 1
2939+
assert reasoning_blocks[0]["type"] == "reasoning"
2940+
2941+
# Full pipeline should not raise
2942+
result = _construct_responses_api_input([converted])
2943+
assert len(result) > 0
2944+
2945+
29002946
def test_get_last_messages() -> None:
29012947
messages: list[BaseMessage] = [HumanMessage("Hello")]
29022948
last_messages, previous_response_id = _get_last_messages(messages)

libs/partners/openai/uv.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)