Files
mai-bot/pytests/test_openai_client_toolless_request.py
Maple_枫溪 9e944f7f85 fix: accept blank tool call arguments
Treat blank OpenAI-compatible tool call arguments as an empty dict so parameterless tools such as finish can execute with providers that return an empty string. Also trim model identifiers during config normalization to avoid leading whitespace leaking into requests and snapshots.
2026-05-05 05:06:26 +08:00

165 lines
5.5 KiB
Python

from types import SimpleNamespace
import pytest
from src.config.model_configs import APIProvider, ReasoningParseMode, ToolArgumentParseMode
from src.llm_models.model_client.openai_client import (
_OpenAIStreamAccumulator,
_build_reasoning_key,
_default_normal_response_parser,
_parse_tool_arguments,
_sanitize_messages_for_toolless_request,
)
from src.llm_models.payload_content.message import Message, RoleType, TextMessagePart
from src.llm_models.payload_content.tool_option import ToolCall
@pytest.mark.parametrize("parse_mode", list(ToolArgumentParseMode))
def test_parse_tool_arguments_treats_blank_arguments_as_empty_dict(parse_mode: ToolArgumentParseMode) -> None:
assert _parse_tool_arguments("", parse_mode, None) == {}
assert _parse_tool_arguments(" ", parse_mode, None) == {}
def test_normal_response_parser_accepts_empty_string_arguments_for_parameterless_tool() -> None:
response = SimpleNamespace(
choices=[
SimpleNamespace(
finish_reason="tool_calls",
message=SimpleNamespace(
content=None,
tool_calls=[
SimpleNamespace(
id="finish-call",
type="function",
function=SimpleNamespace(name="finish", arguments=""),
)
],
),
)
],
usage=None,
model="glm-5.1",
)
api_response, usage_record = _default_normal_response_parser(
response,
reasoning_parse_mode=ReasoningParseMode.AUTO,
tool_argument_parse_mode=ToolArgumentParseMode.AUTO,
reasoning_key=None,
)
assert len(api_response.tool_calls) == 1
assert api_response.tool_calls[0].func_name == "finish"
assert api_response.tool_calls[0].args == {}
assert usage_record is None
def test_sanitize_messages_for_toolless_request_drops_assistant_tool_call_without_parts() -> None:
messages = [
Message(
role=RoleType.Assistant,
tool_calls=[
ToolCall(
call_id="call_1",
func_name="mute_user",
args={"target": "alice"},
)
],
),
Message(
role=RoleType.User,
parts=[TextMessagePart(text="继续")],
),
]
sanitized_messages = _sanitize_messages_for_toolless_request(messages)
assert len(sanitized_messages) == 1
assert sanitized_messages[0].role == RoleType.User
def test_normal_response_parser_ignores_reasoning_field_for_non_openrouter_provider() -> None:
response = SimpleNamespace(
choices=[
SimpleNamespace(
finish_reason="stop",
message=SimpleNamespace(
content="正式回复",
reasoning="推理内容",
tool_calls=None,
),
)
],
usage=None,
model="openrouter/test-model",
)
api_response, usage_record = _default_normal_response_parser(
response,
reasoning_parse_mode=ReasoningParseMode.AUTO,
tool_argument_parse_mode=ToolArgumentParseMode.AUTO,
reasoning_key=_build_reasoning_key(
APIProvider(name="test", base_url="https://openrouter.ai.example.com/api/v1", api_key="test")
),
)
assert api_response.content == "正式回复"
assert api_response.reasoning_content is None
assert usage_record is None
def test_normal_response_parser_reads_provider_reasoning_field_for_reasoning_domains() -> None:
provider_urls = [
"https://openrouter.ai/compatible-api",
"https://api.groq.com/openai/v1",
]
for provider_url in provider_urls:
response = SimpleNamespace(
choices=[
SimpleNamespace(
finish_reason="stop",
message=SimpleNamespace(
content="正式回复",
reasoning="推理内容",
tool_calls=None,
),
)
],
usage=None,
model="test-model",
)
api_response, usage_record = _default_normal_response_parser(
response,
reasoning_parse_mode=ReasoningParseMode.AUTO,
tool_argument_parse_mode=ToolArgumentParseMode.AUTO,
reasoning_key=_build_reasoning_key(
APIProvider(name="reasoning-provider", base_url=provider_url, api_key="test")
),
)
assert api_response.content == "正式回复"
assert api_response.reasoning_content == "推理内容"
assert usage_record is None
def test_stream_accumulator_reads_openrouter_reasoning_delta_field() -> None:
accumulator = _OpenAIStreamAccumulator(
reasoning_parse_mode=ReasoningParseMode.AUTO,
tool_argument_parse_mode=ToolArgumentParseMode.AUTO,
reasoning_key=_build_reasoning_key(
APIProvider(name="openrouter", base_url="https://openrouter.ai/compatible-api", api_key="test")
),
)
try:
accumulator.process_delta(SimpleNamespace(reasoning="流式推理", content=None, tool_calls=None))
accumulator.process_delta(SimpleNamespace(content="正式回复", tool_calls=None))
api_response = accumulator.build_response()
finally:
accumulator.close()
assert api_response.content == "正式回复"
assert api_response.reasoning_content == "流式推理"