mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 09:40:30 +08:00
fix: align OpenAI tool message sanitizer (#8350)
Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user