mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-17 17:51:20 +08:00
修复:过滤空助手消息,以防止在严格API上出现400错误(fix: filter empty assistant messages to prevent 400 error on strict APIs) (#7202)
* fix: filter empty assistant messages to prevent 400 error on strict APIs Some OpenAI-compatible APIs (e.g., Moonshot) reject requests with empty content in assistant messages when no tool_calls are present. This fix cleans up the messages payload before sending to avoid 'message at position X must not be empty' errors. Closes related issue with fallback provider behavior. * test(openai): add tests for empty assistant message filtering * refactor(openai): simplify empty assistant message filtering logic * style: format code --------- Co-authored-by: RC-CHN <1051989940@qq.com>
This commit is contained in:
@@ -1173,3 +1173,360 @@ async def test_parse_openai_completion_raises_empty_model_output_error():
|
||||
await provider._parse_openai_completion(completion, tools=None)
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@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."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": ""}, # Should be filtered
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The empty assistant message should be filtered out
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 2
|
||||
assert messages[0] == {"role": "user", "content": "hello"}
|
||||
assert messages[1] == {"role": "user", "content": "world"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_filters_null_content_assistant_message_without_tool_calls(
|
||||
monkeypatch,
|
||||
):
|
||||
"""Test that assistant messages with null content and no tool_calls are filtered."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": None}, # Should be filtered
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The null content assistant message should be filtered out
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 2
|
||||
assert messages[0] == {"role": "user", "content": "hello"}
|
||||
assert messages[1] == {"role": "user", "content": "world"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_converts_empty_content_to_none_with_tool_calls(monkeypatch):
|
||||
"""Test that empty content with tool_calls is converted to None (OpenAI spec)."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-123",
|
||||
"type": "function",
|
||||
"function": {"name": "test", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The assistant message with tool_calls should be kept but content set to None
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[1]["role"] == "assistant"
|
||||
assert messages[1]["content"] is None
|
||||
assert messages[1]["tool_calls"] is not None
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_keeps_valid_assistant_message_with_content(monkeypatch):
|
||||
"""Test that valid assistant messages with content are kept."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "response"},
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# All messages should be kept
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[1] == {"role": "assistant", "content": "response"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_keeps_assistant_message_with_tool_calls_and_none_content(
|
||||
monkeypatch,
|
||||
):
|
||||
"""Test that assistant messages with tool_calls and None content are kept."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-123",
|
||||
"type": "function",
|
||||
"function": {"name": "test", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The assistant message with tool_calls should be kept
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[1]["role"] == "assistant"
|
||||
assert messages[1]["content"] is None
|
||||
assert messages[1]["tool_calls"] is not None
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_does_not_filter_user_or_system_messages(monkeypatch):
|
||||
"""Test that user and system messages are not affected by the filter."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "system", "content": ""}, # Empty system message
|
||||
{"role": "user", "content": ""}, # Empty user message
|
||||
{"role": "assistant", "content": ""}, # Should be filtered
|
||||
{"role": "user", "content": "hello"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# Only assistant message should be filtered
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[0] == {"role": "system", "content": ""}
|
||||
assert messages[1] == {"role": "user", "content": ""}
|
||||
assert messages[2] == {"role": "user", "content": "hello"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
Reference in New Issue
Block a user