"""真实网关工具双轮冒烟;复用安全 Markdown 报告,不运行工具副作用。""" import dataclasses import json import os from pathlib import Path from uuid import uuid4 import pytest from dotenv import dotenv_values from polygateway import GatewayClient, GatewaySettings from tests.live_evidence import write_live_round _ENV = {k: v for k, v in {**dotenv_values(".env"), **os.environ}.items() if v is not None} pytestmark = [pytest.mark.slow, pytest.mark.requires_llm] @pytest.mark.parametrize("stream", [True, False]) async def test_tools_two_rounds(stream): """已配置源中选首源,只改变测试隔离与 raw-only 意图,不收紧生产超时。""" if not any(k.startswith("LLM__") and k.endswith("__API_KEY") for k in _ENV): pytest.skip("未配置真实源,未覆盖") settings = GatewaySettings.from_env("LLM", env=_ENV) source = dataclasses.replace(settings.sources[0], enable_thinking=None, reasoning_effort=None) settings = dataclasses.replace( settings, sources=(source,), cache_backend="none", cache_namespace=None, cache_ttl_s=None, telemetry_backend="none", telemetry_auto_migrate=False, limiter_backend="memory", breaker_backend="memory", hedge_after_s=None, hedge_stall_after_s=None, ) tools = [ { "type": "function", "function": { "name": "echo_value", "description": "Return the supplied value", "parameters": { "type": "object", "properties": {"value": {"type": "string"}}, "required": ["value"], }, }, } ] messages = [ { "role": "user", "content": "Call echo_value with value hello. After receiving the tool result, reply with hello.", } ] status, error_type, counts = "FAIL", None, {} try: async with GatewayClient.from_settings(settings) as client: first = await client.chat( messages, stream=stream, response_mode="openai", overlay={ "tools": tools, "tool_choice": {"type": "function", "function": {"name": "echo_value"}}, }, ) completion = first.completion assert completion is not None and completion.message["role"] == "assistant" assert completion.response_id and completion.finish_reason == "tool_calls" calls = completion.message["tool_calls"] counts["tools"] = len(calls) assert calls messages.append(completion.message) for call in calls: assert call["function"]["name"] == "echo_value" arguments = json.loads(call["function"]["arguments"]) assert arguments["value"] == "hello" messages.append( {"role": "tool", "tool_call_id": call["id"], "content": arguments["value"]} ) second = await client.chat( messages, stream=stream, response_mode="openai", overlay={"tools": tools, "tool_choice": "none"}, ) assert ( second.completion is not None and second.completion.message["role"] == "assistant" ) assert "hello" in (second.completion.message.get("content") or "").lower() counts["attempts"] = first.call_stats.attempts + second.call_stats.attempts status = "PASS" except Exception as exc: error_type = type(exc).__name__ raise finally: write_live_round( Path("tests/outputs/issue29/live"), run_id=uuid4().hex, matrix_id="tools", round_index=1, safe_fields={ "status": status, "stream": stream, "error_type": error_type, "counts": counts, "planned_rounds": 2, "completed_rounds": 2 if status == "PASS" else 0, }, )