openai[patch]: add attribute to always use previous_response_id (#31734)

This commit is contained in:
ccurme 2025-06-25 15:01:43 -04:00 committed by GitHub
parent b02bd67788
commit 0bf223d6cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 170 additions and 1 deletions

View File

@ -138,6 +138,7 @@ def test_configurable() -> None:
"extra_body": None, "extra_body": None,
"include_response_headers": False, "include_response_headers": False,
"stream_usage": False, "stream_usage": False,
"use_previous_response_id": False,
"use_responses_api": None, "use_responses_api": None,
}, },
"kwargs": { "kwargs": {

View File

@ -607,6 +607,40 @@ class BaseChatOpenAI(BaseChatModel):
.. versionadded:: 0.3.24 .. versionadded:: 0.3.24
""" """
use_previous_response_id: bool = False
"""If True, always pass ``previous_response_id`` using the ID of the most recent
response. Responses API only.
Input messages up to the most recent response will be dropped from request
payloads.
For example, the following two are equivalent:
.. code-block:: python
llm = ChatOpenAI(
model="o4-mini",
use_previous_response_id=True,
)
llm.invoke(
[
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
HumanMessage("How are you?"),
]
)
.. code-block:: python
llm = ChatOpenAI(
model="o4-mini",
use_responses_api=True,
)
llm.invoke([HumanMessage("How are you?")], previous_response_id="resp_123")
.. versionadded:: 0.3.26
"""
use_responses_api: Optional[bool] = None use_responses_api: Optional[bool] = None
"""Whether to use the Responses API instead of the Chat API. """Whether to use the Responses API instead of the Chat API.
@ -1081,6 +1115,8 @@ class BaseChatOpenAI(BaseChatModel):
return True return True
elif self.truncation is not None: elif self.truncation is not None:
return True return True
elif self.use_previous_response_id:
return True
else: else:
return _use_responses_api(payload) return _use_responses_api(payload)
@ -1097,7 +1133,14 @@ class BaseChatOpenAI(BaseChatModel):
payload = {**self._default_params, **kwargs} payload = {**self._default_params, **kwargs}
if self._use_responses_api(payload): if self._use_responses_api(payload):
payload = _construct_responses_api_payload(messages, payload) if self.use_previous_response_id:
last_messages, previous_response_id = _get_last_messages(messages)
payload_to_use = last_messages if previous_response_id else messages
if previous_response_id:
payload["previous_response_id"] = previous_response_id
payload = _construct_responses_api_payload(payload_to_use, payload)
else:
payload = _construct_responses_api_payload(messages, payload)
else: else:
payload["messages"] = [_convert_message_to_dict(m) for m in messages] payload["messages"] = [_convert_message_to_dict(m) for m in messages]
return payload return payload
@ -3202,6 +3245,30 @@ def _use_responses_api(payload: dict) -> bool:
return bool(uses_builtin_tools or responses_only_args.intersection(payload)) return bool(uses_builtin_tools or responses_only_args.intersection(payload))
def _get_last_messages(
messages: Sequence[BaseMessage],
) -> tuple[Sequence[BaseMessage], Optional[str]]:
"""
Return
1. Every message after the most-recent AIMessage that has a non-empty
``response_metadata["id"]`` (may be an empty list),
2. That id.
If the most-recent AIMessage does not have an id (or there is no
AIMessage at all) the entire conversation is returned together with ``None``.
"""
for i in range(len(messages) - 1, -1, -1):
msg = messages[i]
if isinstance(msg, AIMessage):
response_id = msg.response_metadata.get("id")
if response_id:
return messages[i + 1 :], response_id
else:
return messages, None
return messages, None
def _construct_responses_api_payload( def _construct_responses_api_payload(
messages: Sequence[BaseMessage], payload: dict messages: Sequence[BaseMessage], payload: dict
) -> dict: ) -> dict:

View File

@ -12,6 +12,7 @@ from langchain_core.load import dumps, loads
from langchain_core.messages import ( from langchain_core.messages import (
AIMessage, AIMessage,
AIMessageChunk, AIMessageChunk,
BaseMessage,
FunctionMessage, FunctionMessage,
HumanMessage, HumanMessage,
InvalidToolCall, InvalidToolCall,
@ -59,6 +60,7 @@ from langchain_openai.chat_models.base import (
_convert_to_openai_response_format, _convert_to_openai_response_format,
_create_usage_metadata, _create_usage_metadata,
_format_message_content, _format_message_content,
_get_last_messages,
_oai_structured_outputs_parser, _oai_structured_outputs_parser,
) )
@ -2151,3 +2153,102 @@ def test_compat() -> None:
message_v03_output = _convert_to_v03_ai_message(message) message_v03_output = _convert_to_v03_ai_message(message)
assert message_v03_output == message_v03 assert message_v03_output == message_v03
assert message_v03_output is not message_v03 assert message_v03_output is not message_v03
def test_get_last_messages() -> None:
messages: list[BaseMessage] = [HumanMessage("Hello")]
last_messages, previous_response_id = _get_last_messages(messages)
assert last_messages == [HumanMessage("Hello")]
assert previous_response_id is None
messages = [
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
HumanMessage("How are you?"),
]
last_messages, previous_response_id = _get_last_messages(messages)
assert last_messages == [HumanMessage("How are you?")]
assert previous_response_id == "resp_123"
messages = [
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
HumanMessage("How are you?"),
AIMessage("Well thanks.", response_metadata={"id": "resp_456"}),
HumanMessage("Great."),
]
last_messages, previous_response_id = _get_last_messages(messages)
assert last_messages == [HumanMessage("Great.")]
assert previous_response_id == "resp_456"
messages = [
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
HumanMessage("What's the weather?"),
AIMessage(
"",
response_metadata={"id": "resp_456"},
tool_calls=[
{
"type": "tool_call",
"name": "get_weather",
"id": "call_123",
"args": {"location": "San Francisco"},
}
],
),
ToolMessage("It's sunny.", tool_call_id="call_123"),
]
last_messages, previous_response_id = _get_last_messages(messages)
assert last_messages == [ToolMessage("It's sunny.", tool_call_id="call_123")]
assert previous_response_id == "resp_456"
messages = [
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
HumanMessage("How are you?"),
AIMessage("Well thanks.", response_metadata={"id": "resp_456"}),
HumanMessage("Good."),
HumanMessage("Great."),
]
last_messages, previous_response_id = _get_last_messages(messages)
assert last_messages == [HumanMessage("Good."), HumanMessage("Great.")]
assert previous_response_id == "resp_456"
messages = [
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
]
last_messages, response_id = _get_last_messages(messages)
assert last_messages == []
assert response_id == "resp_123"
def test_get_request_payload_use_previous_response_id() -> None:
# Default - don't use previous_response ID
llm = ChatOpenAI(model="o4-mini", use_responses_api=True)
messages = [
HumanMessage("Hello"),
AIMessage("Hi there!", response_metadata={"id": "resp_123"}),
HumanMessage("How are you?"),
]
payload = llm._get_request_payload(messages)
assert "previous_response_id" not in payload
assert len(payload["input"]) == 3
# Use previous response ID
llm = ChatOpenAI(
model="o4-mini",
# Specifying use_previous_response_id automatically engages Responses API
use_previous_response_id=True,
)
payload = llm._get_request_payload(messages)
assert payload["previous_response_id"] == "resp_123"
assert len(payload["input"]) == 1
# Check single message
messages = [HumanMessage("Hello")]
payload = llm._get_request_payload(messages)
assert "previous_response_id" not in payload
assert len(payload["input"]) == 1