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 == "流式推理"