diff --git a/libs/langchain_v1/tests/cassettes/test_inference_to_native_output[True].yaml.gz b/libs/langchain_v1/tests/cassettes/test_inference_to_native_output[True].yaml.gz index cdfb2a66ad2..f2a20a6afa0 100644 Binary files a/libs/langchain_v1/tests/cassettes/test_inference_to_native_output[True].yaml.gz and b/libs/langchain_v1/tests/cassettes/test_inference_to_native_output[True].yaml.gz differ diff --git a/libs/langchain_v1/tests/cassettes/test_inference_to_tool_output[True].yaml.gz b/libs/langchain_v1/tests/cassettes/test_inference_to_tool_output[True].yaml.gz index 95cc9005e84..9ce51362769 100644 Binary files a/libs/langchain_v1/tests/cassettes/test_inference_to_tool_output[True].yaml.gz and b/libs/langchain_v1/tests/cassettes/test_inference_to_tool_output[True].yaml.gz differ diff --git a/libs/langchain_v1/tests/cassettes/test_strict_mode[True].yaml.gz b/libs/langchain_v1/tests/cassettes/test_strict_mode[True].yaml.gz index 3a1ff3ced07..dbe8be819c7 100644 Binary files a/libs/langchain_v1/tests/cassettes/test_strict_mode[True].yaml.gz and b/libs/langchain_v1/tests/cassettes/test_strict_mode[True].yaml.gz differ diff --git a/libs/langchain_v1/tests/unit_tests/conftest.py b/libs/langchain_v1/tests/unit_tests/conftest.py index a55b6a0ca21..18ff19da297 100644 --- a/libs/langchain_v1/tests/unit_tests/conftest.py +++ b/libs/langchain_v1/tests/unit_tests/conftest.py @@ -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: diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 57312208cd2..629b34ee36d 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -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) diff --git a/libs/partners/openai/tests/cassettes/TestOpenAIResponses.test_stream_time.yaml.gz b/libs/partners/openai/tests/cassettes/TestOpenAIResponses.test_stream_time.yaml.gz index b954ebfd34f..3a9caa43016 100644 Binary files a/libs/partners/openai/tests/cassettes/TestOpenAIResponses.test_stream_time.yaml.gz and b/libs/partners/openai/tests/cassettes/TestOpenAIResponses.test_stream_time.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_agent_loop.yaml.gz b/libs/partners/openai/tests/cassettes/test_agent_loop.yaml.gz index 9440f6b0e6a..cd8e18e8316 100644 Binary files a/libs/partners/openai/tests/cassettes/test_agent_loop.yaml.gz and b/libs/partners/openai/tests/cassettes/test_agent_loop.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_agent_loop_streaming.yaml.gz b/libs/partners/openai/tests/cassettes/test_agent_loop_streaming.yaml.gz index 9e7faf6d94f..92f7a535e44 100644 Binary files a/libs/partners/openai/tests/cassettes/test_agent_loop_streaming.yaml.gz and b/libs/partners/openai/tests/cassettes/test_agent_loop_streaming.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_client_executed_tool_search.yaml.gz b/libs/partners/openai/tests/cassettes/test_client_executed_tool_search.yaml.gz index 232b4782825..6e4ed0e8ffb 100644 Binary files a/libs/partners/openai/tests/cassettes/test_client_executed_tool_search.yaml.gz and b/libs/partners/openai/tests/cassettes/test_client_executed_tool_search.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_code_interpreter.yaml.gz b/libs/partners/openai/tests/cassettes/test_code_interpreter.yaml.gz index 924ecc6cddd..429efbcb8c8 100644 Binary files a/libs/partners/openai/tests/cassettes/test_code_interpreter.yaml.gz and b/libs/partners/openai/tests/cassettes/test_code_interpreter.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_compaction.yaml.gz b/libs/partners/openai/tests/cassettes/test_compaction.yaml.gz index 5406d5ab80e..4ea65906cfc 100644 Binary files a/libs/partners/openai/tests/cassettes/test_compaction.yaml.gz and b/libs/partners/openai/tests/cassettes/test_compaction.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_compaction_streaming.yaml.gz b/libs/partners/openai/tests/cassettes/test_compaction_streaming.yaml.gz index 9c765bb389f..60bc0656874 100644 Binary files a/libs/partners/openai/tests/cassettes/test_compaction_streaming.yaml.gz and b/libs/partners/openai/tests/cassettes/test_compaction_streaming.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_custom_tool.yaml.gz b/libs/partners/openai/tests/cassettes/test_custom_tool.yaml.gz index 3a0ea3d888f..d83a5e70ad2 100644 Binary files a/libs/partners/openai/tests/cassettes/test_custom_tool.yaml.gz and b/libs/partners/openai/tests/cassettes/test_custom_tool.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_file_search.yaml.gz b/libs/partners/openai/tests/cassettes/test_file_search.yaml.gz index 4c896356533..d449768cb67 100644 Binary files a/libs/partners/openai/tests/cassettes/test_file_search.yaml.gz and b/libs/partners/openai/tests/cassettes/test_file_search.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_function_calling.yaml.gz b/libs/partners/openai/tests/cassettes/test_function_calling.yaml.gz index 197a8402cf6..99b64d5a1cd 100644 Binary files a/libs/partners/openai/tests/cassettes/test_function_calling.yaml.gz and b/libs/partners/openai/tests/cassettes/test_function_calling.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_image_generation_multi_turn.yaml.gz b/libs/partners/openai/tests/cassettes/test_image_generation_multi_turn.yaml.gz index 61ae4c52862..37dbb9f1d5c 100644 Binary files a/libs/partners/openai/tests/cassettes/test_image_generation_multi_turn.yaml.gz and b/libs/partners/openai/tests/cassettes/test_image_generation_multi_turn.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_image_generation_streaming.yaml.gz b/libs/partners/openai/tests/cassettes/test_image_generation_streaming.yaml.gz index eac0a3848a6..64b2439177b 100644 Binary files a/libs/partners/openai/tests/cassettes/test_image_generation_streaming.yaml.gz and b/libs/partners/openai/tests/cassettes/test_image_generation_streaming.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_incomplete_response.yaml.gz b/libs/partners/openai/tests/cassettes/test_incomplete_response.yaml.gz index f9ac5b9da95..0ce1dc9d8e5 100644 Binary files a/libs/partners/openai/tests/cassettes/test_incomplete_response.yaml.gz and b/libs/partners/openai/tests/cassettes/test_incomplete_response.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_mcp_builtin.yaml.gz b/libs/partners/openai/tests/cassettes/test_mcp_builtin.yaml.gz index 93b83eac273..638c46cb074 100644 Binary files a/libs/partners/openai/tests/cassettes/test_mcp_builtin.yaml.gz and b/libs/partners/openai/tests/cassettes/test_mcp_builtin.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_mcp_builtin_zdr.yaml.gz b/libs/partners/openai/tests/cassettes/test_mcp_builtin_zdr.yaml.gz index 5befec093cf..8f0fb68744c 100644 Binary files a/libs/partners/openai/tests/cassettes/test_mcp_builtin_zdr.yaml.gz and b/libs/partners/openai/tests/cassettes/test_mcp_builtin_zdr.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_parsed_pydantic_schema.yaml.gz b/libs/partners/openai/tests/cassettes/test_parsed_pydantic_schema.yaml.gz index 13c0b8896de..8ae9ed33ee9 100644 Binary files a/libs/partners/openai/tests/cassettes/test_parsed_pydantic_schema.yaml.gz and b/libs/partners/openai/tests/cassettes/test_parsed_pydantic_schema.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_reasoning.yaml.gz b/libs/partners/openai/tests/cassettes/test_reasoning.yaml.gz index d598966c99a..70b9b43d493 100644 Binary files a/libs/partners/openai/tests/cassettes/test_reasoning.yaml.gz and b/libs/partners/openai/tests/cassettes/test_reasoning.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api.yaml.gz b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api.yaml.gz index f833103aa33..8816485fe6c 100644 Binary files a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api.yaml.gz and b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.yaml.gz b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.yaml.gz index 344e172f79c..f3337c65b2c 100644 Binary files a/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.yaml.gz and b/libs/partners/openai/tests/cassettes/test_schema_parsing_failures_responses_api_async.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_stream_reasoning_summary.yaml.gz b/libs/partners/openai/tests/cassettes/test_stream_reasoning_summary.yaml.gz index ac89d7580cd..f818e8bce67 100644 Binary files a/libs/partners/openai/tests/cassettes/test_stream_reasoning_summary.yaml.gz and b/libs/partners/openai/tests/cassettes/test_stream_reasoning_summary.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_tool_search.yaml.gz b/libs/partners/openai/tests/cassettes/test_tool_search.yaml.gz index 2498b937c93..0e2a87d7344 100644 Binary files a/libs/partners/openai/tests/cassettes/test_tool_search.yaml.gz and b/libs/partners/openai/tests/cassettes/test_tool_search.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_tool_search_streaming.yaml.gz b/libs/partners/openai/tests/cassettes/test_tool_search_streaming.yaml.gz index 0d22a1f317e..e094d1272a5 100644 Binary files a/libs/partners/openai/tests/cassettes/test_tool_search_streaming.yaml.gz and b/libs/partners/openai/tests/cassettes/test_tool_search_streaming.yaml.gz differ diff --git a/libs/partners/openai/tests/cassettes/test_web_search.yaml.gz b/libs/partners/openai/tests/cassettes/test_web_search.yaml.gz index a202dfe9c61..e81511f3212 100644 Binary files a/libs/partners/openai/tests/cassettes/test_web_search.yaml.gz and b/libs/partners/openai/tests/cassettes/test_web_search.yaml.gz differ diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py index 28012204af2..473da166922 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py @@ -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"}, diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py index c572a850b6b..291d72c4a0d 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py @@ -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( [ diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py b/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py index 37466928ab1..cee734f0f0f 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py @@ -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=[ diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 70d5db49fe8..291fee686b1 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -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")]) diff --git a/libs/partners/openai/tests/unit_tests/test_tools.py b/libs/partners/openai/tests/unit_tests/test_tools.py index 07a3b86d412..5bb40615c55 100644 --- a/libs/partners/openai/tests/unit_tests/test_tools.py +++ b/libs/partners/openai/tests/unit_tests/test_tools.py @@ -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",