feat: preserve governed OpenAI tool messages in 1.4.0
Add faithful single-choice chat results, tool stream activity, error-frame retries, and isolated protocol caching. Reject empty cached completions with the same value predicate as transport, add regression coverage, and adopt Apache-2.0.
This commit is contained in:
@@ -22,7 +22,13 @@ from polygateway.client import (
|
||||
)
|
||||
from polygateway.providers import get_provider
|
||||
from polygateway.transports.openai_compat import OpenAICompatTransport
|
||||
from polygateway.types import Effort, EmbeddingTransportResult, SourceConfig, TransportResult
|
||||
from polygateway.types import (
|
||||
Effort,
|
||||
EmbeddingTransportResult,
|
||||
ResponseMode,
|
||||
SourceConfig,
|
||||
TransportResult,
|
||||
)
|
||||
from tests.live_evidence import (
|
||||
AttemptEvidence,
|
||||
HttpEvidence,
|
||||
@@ -293,6 +299,7 @@ class ObservedTransport:
|
||||
reasoning_effort: Effort | None,
|
||||
first_token_event: asyncio.Event | None,
|
||||
content_event: asyncio.Event | None,
|
||||
response_mode: ResponseMode,
|
||||
) -> TransportResult:
|
||||
"""与生产端口逐参数同签名。"""
|
||||
with self._capture.attempt_context(call_id):
|
||||
@@ -306,6 +313,7 @@ class ObservedTransport:
|
||||
reasoning_effort=reasoning_effort,
|
||||
first_token_event=first_token_event,
|
||||
content_event=content_event,
|
||||
response_mode=response_mode,
|
||||
)
|
||||
|
||||
async def embed(
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""真实网关工具双轮冒烟;复用安全 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,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user