From 17d3d892cd1f05273c23b36bd8f3a13ee4220939 Mon Sep 17 00:00:00 2001 From: ccurme Date: Mon, 21 Sep 2026 10:52:15 -0400 Subject: [PATCH] fix(openai): expose inferred Responses API routing at initialization (#40715) Co-authored-by: open-swe[bot] --- .../langchain_openai/chat_models/base.py | 9 ++- .../tests/unit_tests/chat_models/test_base.py | 64 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index f141c11bb6..77de14e3ab 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -1206,7 +1206,7 @@ class BaseChatOpenAI(BaseChatModel): use_responses_api: bool | None = None """Whether to use the Responses API instead of the Chat API. - If not specified then will be inferred based on invocation params. + If not specified, set to `True` when instance settings require the Responses API, !!! version-added "Added in `langchain-openai` 0.3.9" """ @@ -1325,6 +1325,13 @@ class BaseChatOpenAI(BaseChatModel): self._add_version("langchain-openai", __version__) return self + @model_validator(mode="after") + def _infer_use_responses_api(self) -> Self: + """Expose unconditional instance-level Responses API routing.""" + if self.use_responses_api is None and self._use_responses_api({}): + self.use_responses_api = True + return self + @model_validator(mode="after") def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" 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 7576a83667..80f272bd3a 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 @@ -4593,6 +4593,70 @@ def test_gpt_5_temperature_case_insensitive( assert payload["temperature"] == 0.7 +@pytest.mark.parametrize( + "kwargs", + [ + {"output_version": "responses/v1"}, + {"context_management": []}, + {"include": []}, + {"reasoning": {}}, + {"truncation": "auto"}, + {"use_previous_response_id": True}, + {"model": "gpt-5-pro"}, + {"model": "gpt-5.3-codex"}, + ], +) +@pytest.mark.parametrize("explicit", [None, True, False]) +def test_infer_use_responses_api(kwargs: dict, explicit: bool | None) -> None: + llm = ChatOpenAI(**kwargs, use_responses_api=explicit) + expected = explicit if explicit is not None else True + assert llm.use_responses_api is expected + assert llm._use_responses_api({}) is expected + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"output_version": "v1"}, + {"reasoning_effort": "low"}, + {"model": "gpt-6-astra"}, + {"model_kwargs": {"text": {}}}, + {"model_kwargs": {"tools": [{"type": "web_search"}]}}, + ], +) +def test_infer_use_responses_api_remains_dynamic(kwargs: dict) -> None: + llm = ChatOpenAI(**kwargs) + assert llm.use_responses_api is None + assert llm._use_responses_api({"tools": [{"type": "web_search"}]}) + assert llm._use_responses_api({"text": {}}) + assert not llm._use_responses_api({}) + + +def test_infer_use_responses_api_from_output_version_env( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("LC_OUTPUT_VERSION", "responses/v1") + assert ChatOpenAI().use_responses_api is True + + +def test_inferred_responses_api_bind_tools_strict() -> None: + llm = ChatOpenAI(reasoning={}) + tool = { + "type": "function", + "function": { + "name": "get_weather", + "parameters": {"type": "object", "properties": {}}, + }, + } + bound = llm.bind_tools( + [tool], + response_format={"title": "Weather", "type": "object", "properties": {}}, + ) + assert isinstance(bound, RunnableBinding) + assert "strict" not in bound.kwargs["tools"][0]["function"] + + def test_gpt_6_tools_use_responses_api() -> None: llm = ChatOpenAI(model="gpt-6-astra") tools = [