Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,9 @@ def _is_empty(content: Any) -> bool:

content = msg.get("content")
tool_calls = msg.get("tool_calls")
reasoning_content = msg.get("reasoning_content")

if _is_empty(content) and not tool_calls:
if _is_empty(content) and not tool_calls and not reasoning_content:
logger.warning(f"过滤第 {idx} 条空 assistant 消息 (无工具调用)")
continue

Expand Down Expand Up @@ -1015,6 +1016,16 @@ def _finally_convert_payload(self, payloads: dict) -> None:
model in deepseek_reasoning_models
or "api.deepseek.com" in self.client.base_url.host
)
# MiMo 推理模型(MiMo-V2.5-Pro / MiMo-V2.5 / MiMo-V2-Pro / MiMo-V2-Omni / MiMo-V2-Flash)
# 要求 assistant 历史消息必须回传 reasoning_content,否则返回 400
mimo_reasoning_models = {
"mimo-v2.5-pro",
"mimo-v2.5",
"mimo-v2-pro",
"mimo-v2-omni",
"mimo-v2-flash",
}
Comment on lines +1021 to +1027
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了提高性能,建议将 mimo_reasoning_models 定义为类常量或模块级常量,以避免在每次 _finally_convert_payload 调用时都重新创建该集合。

is_mimo_reasoning = model in mimo_reasoning_models
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

根据 PR 描述,应该通过模型名称集合和 xiaomimimo.com 端点双重判断是否为 MiMo 推理模型。目前代码中缺少了对端点的判断。此外,根据项目规则,新功能的实现应配套相应的单元测试。

        is_mimo_reasoning = (
            model in mimo_reasoning_models
            or "xiaomimimo.com" in self.client.base_url.host
        )
References
  1. New functionality, such as handling attachments, should be accompanied by corresponding unit tests.

for message in payloads.get("messages", []):
if message.get("role") == "assistant" and isinstance(
message.get("content"), list
Expand Down Expand Up @@ -1043,6 +1054,15 @@ def _finally_convert_payload(self, payloads: dict) -> None:
# history messages, even when the reasoning content is empty.
message["reasoning_content"] = ""

if (
message.get("role") == "assistant"
and is_mimo_reasoning
and "reasoning_content" not in message
):
# MiMo 推理模型要求 assistant 历史消息回传 reasoning_content,
# 缺失时 API 返回 400。参见 MiMo 官方文档。
message["reasoning_content"] = ""

# Gemini 的 function_response 要求 google.protobuf.Struct(即 JSON 对象),
# 纯文本会触发 400 Invalid argument,需要包一层 JSON。
if is_gemini and message.get("role") == "tool":
Expand Down
123 changes: 123 additions & 0 deletions tests/test_openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -1786,3 +1786,126 @@ async def fake_create(**kwargs):
assert messages[1] == {"role": "user", "content": "again"}
finally:
await provider.terminate()


# ===== MiMo reasoning_content 回传测试 =====

MIMO_REASONING_MODELS = [
"mimo-v2.5-pro",
"mimo-v2.5",
"mimo-v2-pro",
"mimo-v2-omni",
"mimo-v2-flash",
]

MIMO_NON_REASONING_MODELS = [
"mimo-v2-tts",
"mimo-v2.5-tts",
"mimo-v2.5-tts-voicedesign",
]


@pytest.mark.asyncio
@pytest.mark.parametrize("model", MIMO_REASONING_MODELS)
async def test_mimo_reasoning_model_adds_empty_reasoning_content(model: str):
Comment on lines +1808 to +1810
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Extend tests to cover interaction with the assistant-message filtering logic when only reasoning_content is present

MiMo tests currently cover injection/preservation of reasoning_content, but not its interaction with the "empty assistant message" filter. Please add a test where an assistant message has reasoning_content set (possibly injected) and empty/omitted content, run it through the filtering flow, and assert that the message is not dropped. This will guard against regressions where MiMo-related messages are filtered out.

Suggested implementation:

        payloads = {
            "model": model,
            "messages": [
                {"role": "user", "content": "hello"},
                {
                    "role": "assistant",
                    "content": "I will help with that.",
                },
                # assistant 消息只包含 reasoning_content,content 为空,用于验证过滤逻辑不会将其丢弃
                {
                    "role": "assistant",
                    "content": "",
                    "reasoning_content": "Thinking about how to help with that.",
                },
            ],

To fully implement the requested coverage (asserting the message is not dropped by the "empty assistant message" filter), you should:

  1. Locate where this test currently asserts against the serialized / filtered messages (for example, inspecting the outgoing request body captured by an HTTP mock, or the result of an internal normalization/filtering helper).
  2. Extend those assertions to verify that the assistant message with content == "" and non-empty reasoning_content is still present after filtering, e.g.:
    • There is an assistant message in the filtered list whose reasoning_content matches "Thinking about how to help with that.", and
    • That message has been preserved (i.e., not removed by the empty-assistant-message filter), even though content is empty or omitted.
  3. If other tests (e.g. something like test_empty_assistant_messages_are_filtered_out) already exercise the filter via a specific helper or by inspecting a particular mocked client call, mirror that pattern here so that this MiMo-specific test runs the messages through the exact same filtering flow before asserting on them.

"""MiMo 推理模型:assistant 消息缺少 reasoning_content 时自动补空字符串"""
provider = _make_provider()
try:
payloads = {
"model": model,
"messages": [
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": "I will help with that.",
},
],
}

provider._finally_convert_payload(payloads)

assistant = payloads["messages"][1]
assert assistant["reasoning_content"] == ""
finally:
await provider.terminate()


@pytest.mark.asyncio
@pytest.mark.parametrize("model", MIMO_NON_REASONING_MODELS)
async def test_mimo_non_reasoning_model_does_not_add_reasoning_content(model: str):
"""MiMo 非推理模型(TTS 等):不应自动注入 reasoning_content"""
provider = _make_provider()
try:
payloads = {
"model": model,
"messages": [
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": "speaking...",
},
],
}

provider._finally_convert_payload(payloads)

assistant = payloads["messages"][1]
assert "reasoning_content" not in assistant
finally:
await provider.terminate()


@pytest.mark.asyncio
async def test_mimo_reasoning_preserves_existing_reasoning_content():
"""已有 reasoning_content 的 assistant 消息不会被覆盖"""
provider = _make_provider()
try:
payloads = {
"model": "mimo-v2.5-pro",
"messages": [
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": [
{"type": "think", "think": "let me think..."},
{"type": "text", "text": "here is the answer"},
],
},
],
}

provider._finally_convert_payload(payloads)

assistant = payloads["messages"][1]
assert assistant["reasoning_content"] == "let me think..."
assert assistant["content"] == [{"type": "text", "text": "here is the answer"}]
finally:
await provider.terminate()


@pytest.mark.asyncio
@pytest.mark.parametrize("model", MIMO_REASONING_MODELS)
async def test_mimo_filter_preserves_reasoning_only_assistant_message(model: str):
"""仅有 reasoning_content 的 assistant 消息不会被 _sanitize 过滤掉"""
provider = _make_provider()
try:
payloads = {
"model": model,
"messages": [
{"role": "user", "content": "hello"},
{
"role": "assistant",
"content": "",
"reasoning_content": "thinking about the answer...",
},
{"role": "user", "content": "world"},
],
}

provider._sanitize_assistant_messages(payloads)

messages = payloads["messages"]
assert len(messages) == 3, "含 reasoning_content 的消息不应被过滤"
assert messages[1]["reasoning_content"] == "thinking about the answer..."
finally:
await provider.terminate()
Loading