mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 18:50:33 +00:00
fix(openai): handle content blocks without type key in responses api conversion (#36725)
This commit is contained in:
@@ -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"):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
5
libs/partners/openai/uv.lock
generated
5
libs/partners/openai/uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user