feat(openai): support openai sdk 2.0 (#33168)

This commit is contained in:
ccurme
2025-09-30 16:34:00 -04:00
committed by GitHub
parent 0795be2a04
commit 64141072a3
4 changed files with 64 additions and 25 deletions

View File

@@ -3611,6 +3611,55 @@ def _construct_responses_api_payload(
return payload
def _convert_chat_completions_blocks_to_responses(
block: dict[str, Any],
) -> dict[str, Any]:
"""Convert chat completions content blocks to responses API format.
Only handles text, image, file blocks. Others pass through.
"""
if block["type"] == "text":
# chat api: {"type": "text", "text": "..."}
# responses api: {"type": "input_text", "text": "..."}
return {"type": "input_text", "text": block["text"]}
if block["type"] == "image_url":
# chat api: {"type": "image_url", "image_url": {"url": "...", "detail": "..."}} # noqa: E501
# responses api: {"type": "image_url", "image_url": "...", "detail": "...", "file_id": "..."} # noqa: E501
new_block = {
"type": "input_image",
"image_url": block["image_url"]["url"],
}
if block["image_url"].get("detail"):
new_block["detail"] = block["image_url"]["detail"]
return new_block
if block["type"] == "file":
return {"type": "input_file", **block["file"]}
return block
def _ensure_valid_tool_message_content(tool_output: Any) -> Union[str, list[dict]]:
if isinstance(tool_output, str):
return tool_output
if isinstance(tool_output, list) and all(
isinstance(block, dict)
and block.get("type")
in (
"input_text",
"input_image",
"input_file",
"text",
"image_url",
"file",
)
for block in tool_output
):
return [
_convert_chat_completions_blocks_to_responses(block)
for block in tool_output
]
return _stringify(tool_output)
def _make_computer_call_output_from_message(message: ToolMessage) -> dict:
computer_call_output: dict = {
"call_id": message.tool_call_id,
@@ -3684,8 +3733,7 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
)
input_.append(computer_call_output)
else:
if not isinstance(tool_output, str):
tool_output = _stringify(tool_output)
tool_output = _ensure_valid_tool_message_content(tool_output)
function_call_output = {
"type": "function_call_output",
"output": tool_output,
@@ -3785,23 +3833,10 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
new_blocks = []
non_message_item_types = ("mcp_approval_response",)
for block in msg["content"]:
# chat api: {"type": "text", "text": "..."}
# responses api: {"type": "input_text", "text": "..."}
if block["type"] == "text":
new_blocks.append({"type": "input_text", "text": block["text"]})
# chat api: {"type": "image_url", "image_url": {"url": "...", "detail": "..."}} # noqa: E501
# responses api: {"type": "image_url", "image_url": "...", "detail": "...", "file_id": "..."} # noqa: E501
elif block["type"] == "image_url":
new_block = {
"type": "input_image",
"image_url": block["image_url"]["url"],
}
if block["image_url"].get("detail"):
new_block["detail"] = block["image_url"]["detail"]
new_blocks.append(new_block)
elif block["type"] == "file":
new_block = {"type": "input_file", **block["file"]}
new_blocks.append(new_block)
if block["type"] in ("text", "image_url", "file"):
new_blocks.append(
_convert_chat_completions_blocks_to_responses(block)
)
elif block["type"] in ("input_text", "input_image", "input_file"):
new_blocks.append(block)
elif block["type"] in non_message_item_types:

View File

@@ -8,7 +8,7 @@ license = { text = "MIT" }
requires-python = ">=3.9.0,<4.0.0"
dependencies = [
"langchain-core>=0.3.76,<2.0.0",
"openai>=1.104.2,<2.0.0",
"openai>=1.104.2,<3.0.0",
"tiktoken>=0.7.0,<1.0.0",
]
name = "langchain-openai"

View File

@@ -22,6 +22,10 @@ class TestOpenAIResponses(TestOpenAIStandard):
def chat_model_params(self) -> dict:
return {"model": "gpt-4o-mini", "use_responses_api": True}
@property
def supports_image_tool_message(self) -> bool:
return True
@pytest.mark.xfail(reason="Unsupported.")
def test_stop_sequence(self, model: BaseChatModel) -> None:
super().test_stop_sequence(model)

View File

@@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.9.0, <4.0.0"
resolution-markers = [
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
@@ -647,7 +647,7 @@ typing = [
[package.metadata]
requires-dist = [
{ name = "langchain-core", editable = "../../core" },
{ name = "openai", specifier = ">=1.104.2,<2.0.0" },
{ name = "openai", specifier = ">=1.104.2,<3.0.0" },
{ name = "tiktoken", specifier = ">=0.7.0,<1.0.0" },
]
@@ -1183,7 +1183,7 @@ wheels = [
[[package]]
name = "openai"
version = "1.107.3"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -1195,9 +1195,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e3/24/7fb5749bcf66b52209e3ece05cb4eaeae2102e95f8ae77589e8afaf70ba8/openai-1.107.3.tar.gz", hash = "sha256:69bb8032b05c5f00f7660e422f70f9aabc94793b9a30c5f899360ed21e46314f", size = 564194, upload-time = "2025-09-15T20:09:20.159Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/5d/74fa2b0358ef15d113b1a6ca2323cee0034020b085a81a94eeddc6914de9/openai-2.0.0.tar.gz", hash = "sha256:6b9513b485f856b0be6bc44c518831acb58e37a12bed72fcc52b1177d1fb34a8", size = 565732, upload-time = "2025-09-30T17:35:57.632Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/1d/58ad0084451f64a9193de48c0afd63047682ffdedb6ae1d494a203e03fd5/openai-1.107.3-py3-none-any.whl", hash = "sha256:4ca54a847235ac04c6320da70fdc06b62d71439de9ec0aa40d5690c3064d4025", size = 947600, upload-time = "2025-09-15T20:09:18.219Z" },
{ url = "https://files.pythonhosted.org/packages/69/41/86ddc9cdd885acc02ee50ec24ea1c5e324eea0c7a471ee841a7088653558/openai-2.0.0-py3-none-any.whl", hash = "sha256:a79f493651f9843a6c54789a83f3b2db56df0e1770f7dcbe98bcf0e967ee2148", size = 955538, upload-time = "2025-09-30T17:35:54.695Z" },
]
[[package]]