mirror of
https://github.com/hwchase17/langchain.git
synced 2026-03-16 18:13:33 +00:00
fix(openai): add type: message to Responses API input items (#35693)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
"""Configuration for unit tests."""
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator, Sequence
|
||||
from importlib import util
|
||||
from typing import Any
|
||||
@@ -41,6 +42,7 @@ def remove_response_headers(response: dict[str, Any]) -> dict[str, Any]:
|
||||
def vcr_config() -> dict[str, Any]:
|
||||
"""Extend the default configuration coming from langchain_tests."""
|
||||
config = base_vcr_config()
|
||||
config["match_on"] = [m if m != "body" else "json_body" for m in config.get("match_on", [])]
|
||||
config.setdefault("filter_headers", []).extend(_EXTRA_HEADERS)
|
||||
config["before_record_request"] = remove_request_headers
|
||||
config["before_record_response"] = remove_response_headers
|
||||
@@ -49,9 +51,27 @@ def vcr_config() -> dict[str, Any]:
|
||||
return config
|
||||
|
||||
|
||||
def _json_body_matcher(r1: Any, r2: Any) -> None:
|
||||
"""Match request bodies as parsed JSON, ignoring key order."""
|
||||
b1 = r1.body or b""
|
||||
b2 = r2.body or b""
|
||||
if isinstance(b1, bytes):
|
||||
b1 = b1.decode("utf-8")
|
||||
if isinstance(b2, bytes):
|
||||
b2 = b2.decode("utf-8")
|
||||
try:
|
||||
j1 = json.loads(b1)
|
||||
j2 = json.loads(b2)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
assert b1 == b2, f"body mismatch (non-JSON):\n{b1}\n!=\n{b2}"
|
||||
return
|
||||
assert j1 == j2, f"body mismatch:\n{j1}\n!=\n{j2}"
|
||||
|
||||
|
||||
def pytest_recording_configure(config: dict[str, Any], vcr: VCR) -> None: # noqa: ARG001
|
||||
vcr.register_persister(CustomPersister())
|
||||
vcr.register_serializer("yaml.gz", CustomSerializer())
|
||||
vcr.register_matcher("json_body", _json_body_matcher)
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
|
||||
@@ -4387,8 +4387,10 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list:
|
||||
pass
|
||||
msg["content"] = new_blocks
|
||||
if msg["content"]:
|
||||
msg["type"] = "message"
|
||||
input_.append(msg)
|
||||
else:
|
||||
msg["type"] = "message"
|
||||
input_.append(msg)
|
||||
else:
|
||||
input_.append(msg)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -718,7 +718,9 @@ def test_image_token_counting_jpeg() -> None:
|
||||
actual = model.get_num_tokens_from_messages([message])
|
||||
assert expected == actual
|
||||
|
||||
image_data = base64.b64encode(httpx.get(image_url, timeout=10.0).content).decode("utf-8")
|
||||
image_data = base64.b64encode(httpx.get(image_url, timeout=10.0).content).decode(
|
||||
"utf-8"
|
||||
)
|
||||
message = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "describe the weather in this image"},
|
||||
@@ -750,7 +752,9 @@ def test_image_token_counting_png() -> None:
|
||||
actual = model.get_num_tokens_from_messages([message])
|
||||
assert expected == actual
|
||||
|
||||
image_data = base64.b64encode(httpx.get(image_url, timeout=10.0).content).decode("utf-8")
|
||||
image_data = base64.b64encode(httpx.get(image_url, timeout=10.0).content).decode(
|
||||
"utf-8"
|
||||
)
|
||||
message = HumanMessage(
|
||||
content=[
|
||||
{"type": "text", "text": "how many dice are in this image"},
|
||||
|
||||
@@ -92,7 +92,9 @@ class TestOpenAIStandard(ChatModelIntegrationTests):
|
||||
def test_openai_pdf_inputs(self, model: BaseChatModel) -> None:
|
||||
"""Test that the model can process PDF inputs."""
|
||||
url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
||||
pdf_data = base64.b64encode(httpx.get(url, timeout=10.0).content).decode("utf-8")
|
||||
pdf_data = base64.b64encode(httpx.get(url, timeout=10.0).content).decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
message = HumanMessage(
|
||||
[
|
||||
|
||||
@@ -87,7 +87,9 @@ class TestOpenAIResponses(TestOpenAIStandard):
|
||||
def test_openai_pdf_tool_messages(self, model: BaseChatModel) -> None:
|
||||
"""Test that the model can process PDF inputs in `ToolMessage` objects."""
|
||||
url = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
|
||||
pdf_data = base64.b64encode(httpx.get(url, timeout=10.0).content).decode("utf-8")
|
||||
pdf_data = base64.b64encode(httpx.get(url, timeout=10.0).content).decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
tool_message = ToolMessage(
|
||||
content_blocks=[
|
||||
|
||||
@@ -2151,6 +2151,7 @@ def test__construct_responses_api_input_human_message_with_text_blocks_conversio
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[0]["role"] == "user"
|
||||
assert isinstance(result[0]["content"], list)
|
||||
assert len(result[0]["content"]) == 1
|
||||
@@ -2267,6 +2268,7 @@ def test__construct_responses_api_input_human_message_with_image_url_conversion(
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[0]["role"] == "user"
|
||||
assert isinstance(result[0]["content"], list)
|
||||
assert len(result[0]["content"]) == 2
|
||||
@@ -2459,17 +2461,21 @@ def test__construct_responses_api_input_multiple_message_types() -> None:
|
||||
assert len(result) == len(messages)
|
||||
|
||||
# Check system message
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[0]["role"] == "system"
|
||||
assert result[0]["content"] == "You are a helpful assistant."
|
||||
|
||||
assert result[1]["type"] == "message"
|
||||
assert result[1]["role"] == "system"
|
||||
assert result[1]["content"] == [
|
||||
{"type": "input_text", "text": "You are a very helpful assistant!"}
|
||||
]
|
||||
|
||||
# Check human message
|
||||
assert result[2]["type"] == "message"
|
||||
assert result[2]["role"] == "user"
|
||||
assert result[2]["content"] == "What's the weather in San Francisco?"
|
||||
assert result[3]["type"] == "message"
|
||||
assert result[3]["role"] == "user"
|
||||
assert result[3]["content"] == [
|
||||
{"type": "input_text", "text": "What's the weather in San Francisco?"}
|
||||
@@ -2519,14 +2525,47 @@ def test__construct_responses_api_input_multiple_message_types() -> None:
|
||||
payload = llm._get_request_payload(message_dicts)
|
||||
result = payload["input"]
|
||||
assert len(result) == 2
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[0]["role"] == "developer"
|
||||
assert result[0]["content"] == "This is a developer message."
|
||||
assert result[1]["type"] == "message"
|
||||
assert result[1]["role"] == "developer"
|
||||
assert result[1]["content"] == [
|
||||
{"type": "input_text", "text": "This is a developer message!"}
|
||||
]
|
||||
|
||||
|
||||
def test__construct_responses_api_input_message_type_on_all_roles() -> None:
|
||||
"""Test that user/system/developer messages include type: 'message'.
|
||||
|
||||
Regression test for https://github.com/langchain-ai/langchain/issues/35688.
|
||||
Strict OpenAI-compatible endpoints (e.g. Azure AI Foundry) require the
|
||||
'type' field on every input item; omitting it causes HTTP 400.
|
||||
"""
|
||||
messages: list = [
|
||||
SystemMessage(content="You are helpful."),
|
||||
HumanMessage(content="Hello"),
|
||||
HumanMessage(content=[{"type": "text", "text": "Hello again"}]),
|
||||
]
|
||||
result = _construct_responses_api_input(messages)
|
||||
|
||||
assert len(result) == 3
|
||||
for item in result:
|
||||
assert item["type"] == "message", (
|
||||
f"Expected type='message' for role={item['role']}, got {item.get('type')!r}"
|
||||
)
|
||||
|
||||
# Also test developer messages via dict input
|
||||
llm = ChatOpenAI(model="o4-mini", use_responses_api=True)
|
||||
payload = llm._get_request_payload(
|
||||
[{"role": "developer", "content": "Translate English to Italian"}]
|
||||
)
|
||||
result = payload["input"]
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "message"
|
||||
assert result[0]["role"] == "developer"
|
||||
|
||||
|
||||
def test_service_tier() -> None:
|
||||
llm = ChatOpenAI(model="o4-mini", service_tier="flex")
|
||||
payload = llm._get_request_payload([HumanMessage("Hello")])
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_custom_tool() -> None:
|
||||
]
|
||||
payload = llm._get_request_payload(message_history) # type: ignore[attr-defined]
|
||||
expected_input = [
|
||||
{"content": "Use the tool", "role": "user"},
|
||||
{"content": "Use the tool", "role": "user", "type": "message"},
|
||||
{
|
||||
"type": "custom_tool_call",
|
||||
"id": "ctc_abc123",
|
||||
|
||||
Reference in New Issue
Block a user