fix(openai): handle content blocks without type key in responses api conversion (#36725)

This commit is contained in:
William FH
2026-04-14 12:13:40 -07:00
committed by GitHub
parent 01a324af0e
commit 885f2c2c2d
4 changed files with 54 additions and 6 deletions

View File

@@ -335,10 +335,9 @@ def _convert_from_v03_ai_message(message: AIMessage) -> AIMessage:
# Reasoning # Reasoning
if reasoning := message.additional_kwargs.get("reasoning"): if reasoning := message.additional_kwargs.get("reasoning"):
if isinstance(message, AIMessageChunk) and message.chunk_position != "last": if "type" not in reasoning:
buckets["reasoning"].append({**reasoning, "type": "reasoning"}) reasoning = {**reasoning, "type": "reasoning"}
else: buckets["reasoning"].append(reasoning)
buckets["reasoning"].append(reasoning)
# Refusal # Refusal
if refusal := message.additional_kwargs.get("refusal"): if refusal := message.additional_kwargs.get("refusal"):

View File

@@ -407,6 +407,8 @@ def _convert_from_v1_to_responses(
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
new_content: list = [] new_content: list = []
for block in content: for block in content:
if "type" not in block:
continue
if block["type"] == "text" and "annotations" in block: if block["type"] == "text" and "annotations" in block:
# Need a copy because we're changing the annotations list # Need a copy because we're changing the annotations list
new_block = dict(block) new_block = dict(block)

View File

@@ -2897,6 +2897,52 @@ def test_convert_from_v1_to_responses(
assert message_v1 != result 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: def test_get_last_messages() -> None:
messages: list[BaseMessage] = [HumanMessage("Hello")] messages: list[BaseMessage] = [HumanMessage("Hello")]
last_messages, previous_response_id = _get_last_messages(messages) last_messages, previous_response_id = _get_last_messages(messages)

View File

@@ -591,6 +591,7 @@ test = [
{ name = "langchain-tests", editable = "../../standard-tests" }, { name = "langchain-tests", editable = "../../standard-tests" },
{ name = "pytest", specifier = ">=8.0.0,<10.0.0" }, { name = "pytest", specifier = ">=8.0.0,<10.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.23.2,<2.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-cov", specifier = ">=4.0.0,<8.0.0" },
{ name = "pytest-mock" }, { name = "pytest-mock" },
{ name = "pytest-socket", specifier = ">=0.6.0,<1.0.0" }, { name = "pytest-socket", specifier = ">=0.6.0,<1.0.0" },
@@ -614,7 +615,7 @@ typing = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "1.2.25" version = "1.3.0a2"
source = { editable = "../../core" } source = { editable = "../../core" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@@ -761,7 +762,7 @@ typing = [
[[package]] [[package]]
name = "langchain-tests" name = "langchain-tests"
version = "1.1.5" version = "1.1.6"
source = { editable = "../../standard-tests" } source = { editable = "../../standard-tests" }
dependencies = [ dependencies = [
{ name = "httpx" }, { name = "httpx" },