mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-01 14:47:02 +00:00
fix(openrouter): strip Responses reasoning IDs (#38383)
Closes #37777 --- OpenRouter can return OpenAI Responses reasoning item IDs such as `rs_*` in assistant reasoning details. Those IDs are not reliably resolvable on a later OpenRouter turn, so replaying them can make otherwise-valid multi-turn conversations fail with a provider 404. This keeps the useful reasoning payload while removing only the ephemeral Responses item IDs before serializing `reasoning_details` back into request history. Non-Responses IDs and reasoning text are left intact.
This commit is contained in:
@@ -1125,6 +1125,19 @@ def _merge_reasoning_run(run: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
return merged_entry
|
||||
|
||||
|
||||
def _strip_ephemeral_reasoning_ids(value: Any) -> Any:
|
||||
"""Remove OpenAI Responses reasoning item IDs from outbound payloads."""
|
||||
if isinstance(value, list):
|
||||
return [_strip_ephemeral_reasoning_ids(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: _strip_ephemeral_reasoning_ids(item)
|
||||
for key, item in value.items()
|
||||
if not (key == "id" and isinstance(item, str) and item.startswith("rs_"))
|
||||
}
|
||||
return value
|
||||
|
||||
|
||||
def _merge_reasoning_details(
|
||||
details: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -1244,8 +1257,8 @@ def _convert_message_to_dict(message: BaseMessage) -> dict[str, Any]: # noqa: C
|
||||
if "reasoning_content" in message.additional_kwargs:
|
||||
message_dict["reasoning"] = message.additional_kwargs["reasoning_content"]
|
||||
if "reasoning_details" in message.additional_kwargs:
|
||||
message_dict["reasoning_details"] = _merge_reasoning_details(
|
||||
message.additional_kwargs["reasoning_details"]
|
||||
message_dict["reasoning_details"] = _strip_ephemeral_reasoning_ids(
|
||||
_merge_reasoning_details(message.additional_kwargs["reasoning_details"])
|
||||
)
|
||||
elif isinstance(message, SystemMessage):
|
||||
message_dict = {"role": "system", "content": message.content}
|
||||
|
||||
@@ -1295,6 +1295,44 @@ class TestMessageConversion:
|
||||
result = _convert_message_to_dict(msg)
|
||||
assert result["reasoning_details"] == details
|
||||
|
||||
def test_ai_message_reasoning_details_strips_responses_ids(self) -> None:
|
||||
"""OpenAI Responses `rs_*` item IDs are stripped before replay."""
|
||||
response_id = "rs_053a05e24b0da75e0169fa358ea9fc81908b18aff8157798c1"
|
||||
details = [
|
||||
{
|
||||
"type": "reasoning.text",
|
||||
"id": response_id,
|
||||
"text": "step-by-step",
|
||||
"index": 0,
|
||||
}
|
||||
]
|
||||
msg = AIMessage(
|
||||
content="Answer",
|
||||
additional_kwargs={"reasoning_details": details},
|
||||
)
|
||||
result = _convert_message_to_dict(msg)
|
||||
assert result["reasoning_details"] == [
|
||||
{"type": "reasoning.text", "text": "step-by-step", "index": 0}
|
||||
]
|
||||
assert response_id.startswith("rs_")
|
||||
assert details[0]["id"] == response_id
|
||||
|
||||
def test_ai_message_reasoning_details_preserves_non_responses_ids(self) -> None:
|
||||
"""Non-Responses IDs are preserved in reasoning details."""
|
||||
details = [
|
||||
{
|
||||
"type": "reasoning.text",
|
||||
"id": "reasoning_abc123",
|
||||
"text": "step-by-step",
|
||||
}
|
||||
]
|
||||
msg = AIMessage(
|
||||
content="Answer",
|
||||
additional_kwargs={"reasoning_details": details},
|
||||
)
|
||||
result = _convert_message_to_dict(msg)
|
||||
assert result["reasoning_details"] == details
|
||||
|
||||
def test_ai_message_unindexed_reasoning_details_not_merged(self) -> None:
|
||||
"""Entries without an `index` are passed through unchanged."""
|
||||
details = [
|
||||
|
||||
Reference in New Issue
Block a user