mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
fix(openai): accommodate dict response items in streaming (#36899)
This commit is contained in:
@@ -4589,6 +4589,15 @@ def _construct_lc_result_from_responses_api(
|
||||
return ChatResult(generations=[ChatGeneration(message=message)])
|
||||
|
||||
|
||||
def _coerce_chunk_response(resp: Any) -> Any:
|
||||
# dict `response` items on stream events have been observed in the wild
|
||||
if isinstance(resp, dict):
|
||||
from openai.types.responses import Response
|
||||
|
||||
return Response.model_validate(resp)
|
||||
return resp
|
||||
|
||||
|
||||
def _convert_responses_chunk_to_generation_chunk(
|
||||
chunk: Any,
|
||||
current_index: int, # index in content
|
||||
@@ -4686,14 +4695,16 @@ def _convert_responses_chunk_to_generation_chunk(
|
||||
}
|
||||
)
|
||||
elif chunk.type == "response.created":
|
||||
id = chunk.response.id
|
||||
response_metadata["id"] = chunk.response.id # Backwards compatibility
|
||||
response = _coerce_chunk_response(chunk.response)
|
||||
id = response.id
|
||||
response_metadata["id"] = response.id # Backwards compatibility
|
||||
elif chunk.type in ("response.completed", "response.incomplete"):
|
||||
response = _coerce_chunk_response(chunk.response)
|
||||
msg = cast(
|
||||
AIMessage,
|
||||
(
|
||||
_construct_lc_result_from_responses_api(
|
||||
chunk.response, schema=schema, output_version=output_version
|
||||
response, schema=schema, output_version=output_version
|
||||
)
|
||||
.generations[0]
|
||||
.message
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -986,3 +987,32 @@ def test_responses_stream_function_call_preserves_namespace() -> None:
|
||||
assert first_block.get("namespace") == "my_namespace", (
|
||||
f"Expected namespace 'my_namespace', got {first_block.get('namespace')}"
|
||||
)
|
||||
|
||||
|
||||
def test_responses_stream_tolerates_dict_response_field() -> None:
|
||||
"""Regression test for `AttributeError: 'dict' object has no attribute 'id'`.
|
||||
|
||||
The OpenAI SDK types `<event>.response` strictly as `Response`, but raw dicts
|
||||
have been observed in the wild.
|
||||
"""
|
||||
stream = copy.deepcopy(responses_stream)
|
||||
first_event = stream[0]
|
||||
assert isinstance(first_event, ResponseCreatedEvent)
|
||||
first_event.response = first_event.response.model_dump(mode="json") # type: ignore[assignment]
|
||||
assert isinstance(first_event.response, dict)
|
||||
|
||||
llm = ChatOpenAI(model="o4-mini", use_responses_api=True)
|
||||
mock_client = MagicMock()
|
||||
|
||||
def mock_create(*args: Any, **kwargs: Any) -> MockSyncContextManager:
|
||||
return MockSyncContextManager(stream)
|
||||
|
||||
mock_client.responses.create = mock_create
|
||||
|
||||
full: BaseMessageChunk | None = None
|
||||
with patch.object(llm, "root_client", mock_client):
|
||||
for chunk in llm.stream("test"):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
assert full.id == "resp_123"
|
||||
|
||||
8
libs/partners/openai/uv.lock
generated
8
libs/partners/openai/uv.lock
generated
@@ -624,7 +624,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.3.0a2"
|
||||
version = "1.3.0"
|
||||
source = { editable = "../../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -1120,7 +1120,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.29.0"
|
||||
version = "2.32.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -1132,9 +1132,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user