fix: align OpenAI tool message sanitizer (#8350)

Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
F. Abyssalis
2026-06-27 16:27:32 +08:00
committed by GitHub
parent 771911a893
commit d4fa9d3d5d
2 changed files with 180 additions and 1 deletions

View File

@@ -477,7 +477,40 @@ class ProviderOpenAIOfficial(Provider):
cleaned.append(msg)
payloads["messages"] = cleaned
# Drop orphaned or duplicate tool messages whose assistant(tool_calls)
# was removed by context truncation / compression.
pending_tool_call_ids: set[str] = set()
final: list = []
removed_tool_messages = 0
for msg in cleaned:
if not isinstance(msg, dict):
final.append(msg)
pending_tool_call_ids = set()
continue
role = msg.get("role")
if role == "assistant" and msg.get("tool_calls"):
pending_tool_call_ids = {
tc["id"]
for tc in msg["tool_calls"]
if isinstance(tc, dict) and "id" in tc
}
final.append(msg)
elif role == "tool":
tool_call_id = msg.get("tool_call_id")
if tool_call_id in pending_tool_call_ids:
final.append(msg)
pending_tool_call_ids.remove(tool_call_id)
else:
removed_tool_messages += 1
else:
pending_tool_call_ids = set()
final.append(msg)
if removed_tool_messages:
logger.debug(
"Filtered %d orphaned or duplicate tool message(s)",
removed_tool_messages,
)
payloads["messages"] = final
async def _query(
self,

View File

@@ -1479,6 +1479,152 @@ async def test_query_stream_extracts_usage_from_empty_choices_chunk(monkeypatch)
await provider.terminate()
def test_sanitize_assistant_messages_removes_orphaned_tool_messages():
payloads = {
"messages": [
{"role": "user", "content": "hello"},
{
"role": "tool",
"tool_call_id": "missing_call",
"content": "stale result",
},
{"role": "user", "content": "continue"},
]
}
ProviderOpenAIOfficial._sanitize_assistant_messages(payloads)
assert payloads["messages"] == [
{"role": "user", "content": "hello"},
{"role": "user", "content": "continue"},
]
def test_sanitize_assistant_messages_keeps_valid_tool_messages_only():
payloads = {
"messages": [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_00",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_00", "content": "one"},
{
"role": "tool",
"tool_call_id": "",
"content": "empty id should not be valid",
},
]
}
ProviderOpenAIOfficial._sanitize_assistant_messages(payloads)
assert payloads["messages"] == [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_00",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_00", "content": "one"},
]
def test_sanitize_assistant_messages_removes_stale_duplicate_tool_message():
payloads = {
"messages": [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_00",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_00", "content": "one"},
{
"role": "tool",
"tool_call_id": "call_00",
"content": "stale duplicate",
},
{"role": "assistant", "content": "done"},
]
}
ProviderOpenAIOfficial._sanitize_assistant_messages(payloads)
assert payloads["messages"] == [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_00",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "tool", "tool_call_id": "call_00", "content": "one"},
{"role": "assistant", "content": "done"},
]
def test_sanitize_assistant_messages_resets_tool_ids_after_non_tool_message():
payloads = {
"messages": [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_00",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "user", "content": "new turn"},
{
"role": "tool",
"tool_call_id": "call_00",
"content": "stale late result",
},
]
}
ProviderOpenAIOfficial._sanitize_assistant_messages(payloads)
assert payloads["messages"] == [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_00",
"type": "function",
"function": {"name": "search", "arguments": "{}"},
}
],
},
{"role": "user", "content": "new turn"},
]
@pytest.mark.asyncio
async def test_query_filters_empty_assistant_message_without_tool_calls(monkeypatch):
"""Test that empty assistant messages without tool_calls are filtered out."""