fix(openai): add type: message to Responses API input items (#35693)

This commit is contained in:
Giulio Leone
2026-03-15 17:43:16 +01:00
committed by GitHub
parent 6f27c2b2c1
commit 9e4a6013be
33 changed files with 74 additions and 5 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"},

View File

@@ -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(
[

View File

@@ -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=[

View File

@@ -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")])

View File

@@ -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",