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:
2026-10-01 14:32:19 -04:00
parent 0e29deac1a
commit 9185eab8c9
42 changed files with 2227 additions and 78 deletions
+9 -1
View File
@@ -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(
+114
View File
@@ -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,
},
)