mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
383 lines
14 KiB
Python
383 lines
14 KiB
Python
"""Live request-count assertions for parallel intent-judge batches.
|
|
|
|
These tests run :class:`turnstone.core.judge.IntentJudge` through the real
|
|
OpenAI-compatible backend configured by ``TURNSTONE_TEST_BASE_URL`` (the same
|
|
contract as :mod:`tests.test_server_live`). A local threaded proxy counts the
|
|
HTTP fan-out and fully drains each genuine upstream response. It then returns
|
|
a deterministic, valid Chat Completions SSE verdict so the assertions measure
|
|
Turnstone's scheduler rather than a live model's JSON-formatting reliability.
|
|
|
|
Run explicitly with a live backend::
|
|
|
|
pytest tests/test_judge_parallel_live.py -m live -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
from collections import Counter
|
|
from dataclasses import dataclass
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import httpx
|
|
import pytest
|
|
from openai import OpenAI
|
|
|
|
from tests.test_model_admission_live import (
|
|
_HOP_BY_HOP_HEADERS,
|
|
_LIVE_API_KEY,
|
|
_LIVE_BASE_URL,
|
|
_ConcurrencyCounter,
|
|
)
|
|
from turnstone.core.judge import IntentJudge, IntentVerdict, JudgeConfig
|
|
from turnstone.core.model_registry import ModelConfig, ModelRegistry
|
|
from turnstone.core.model_turn import resolve_model_binding
|
|
|
|
if TYPE_CHECKING:
|
|
from types import TracebackType
|
|
|
|
|
|
_VERDICT_CONTENT = json.dumps(
|
|
{
|
|
"intent_summary": "Live scheduler probe",
|
|
"risk_level": "low",
|
|
"confidence": 0.99,
|
|
"recommendation": "approve",
|
|
"reasoning": "The deterministic proxy verdict confirms one completed judge dispatch.",
|
|
"evidence": ["The genuine upstream response was drained before this verdict."],
|
|
},
|
|
separators=(",", ":"),
|
|
)
|
|
|
|
|
|
def _deterministic_sse() -> bytes:
|
|
chunks = (
|
|
{
|
|
"id": "turnstone-live-judge",
|
|
"object": "chat.completion.chunk",
|
|
"created": 0,
|
|
"model": "turnstone-live-judge",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"delta": {"role": "assistant", "content": _VERDICT_CONTENT},
|
|
"finish_reason": None,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "turnstone-live-judge",
|
|
"object": "chat.completion.chunk",
|
|
"created": 0,
|
|
"model": "turnstone-live-judge",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"delta": {},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
framed = "".join(f"data: {json.dumps(chunk, separators=(',', ':'))}\n\n" for chunk in chunks)
|
|
return (framed + "data: [DONE]\n\n").encode()
|
|
|
|
|
|
class _JudgeCountingProxyServer(ThreadingHTTPServer):
|
|
daemon_threads = True
|
|
|
|
def __init__(self, counter: _ConcurrencyCounter) -> None:
|
|
super().__init__(("127.0.0.1", 0), _JudgeCountingProxyHandler)
|
|
self.counter = counter
|
|
self.upstream_base_url = _LIVE_BASE_URL.rstrip("/")
|
|
self.upstream_authorization = f"Bearer {_LIVE_API_KEY}"
|
|
self.alias_by_authorization: dict[str, str] = {}
|
|
|
|
|
|
class _JudgeCountingProxyHandler(BaseHTTPRequestHandler):
|
|
protocol_version = "HTTP/1.1"
|
|
|
|
@property
|
|
def _proxy(self) -> _JudgeCountingProxyServer:
|
|
if not isinstance(self.server, _JudgeCountingProxyServer):
|
|
raise TypeError("judge counting handler requires _JudgeCountingProxyServer")
|
|
return self.server
|
|
|
|
def do_POST(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
|
|
alias = self._proxy.alias_by_authorization.get(self.headers.get("Authorization", ""))
|
|
if alias is None:
|
|
self._write_json_error(401, "unknown live-test alias credential")
|
|
return
|
|
|
|
try:
|
|
content_length = int(self.headers.get("Content-Length", "0"))
|
|
except ValueError:
|
|
self._write_json_error(400, "invalid content length")
|
|
return
|
|
body = self.rfile.read(content_length)
|
|
# The test measures dispatch, admission, and full upstream drain. Keep
|
|
# the real probe inexpensive even if the configured model tends to
|
|
# deliberate: its content is replaced only after the response completes.
|
|
try:
|
|
upstream_body = json.loads(body)
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
upstream_body = None
|
|
if isinstance(upstream_body, dict):
|
|
if "max_tokens" in upstream_body:
|
|
upstream_body["max_tokens"] = min(int(upstream_body["max_tokens"]), 16)
|
|
if "max_completion_tokens" in upstream_body:
|
|
upstream_body["max_completion_tokens"] = min(
|
|
int(upstream_body["max_completion_tokens"]), 16
|
|
)
|
|
body = json.dumps(upstream_body, separators=(",", ":")).encode()
|
|
|
|
counter = self._proxy.counter
|
|
counter.enter(alias)
|
|
completed = False
|
|
try:
|
|
headers = {
|
|
key: value
|
|
for key, value in self.headers.items()
|
|
if key.lower() not in _HOP_BY_HOP_HEADERS
|
|
and key.lower() not in {"authorization", "host"}
|
|
}
|
|
headers["Authorization"] = self._proxy.upstream_authorization
|
|
headers["Accept-Encoding"] = "identity"
|
|
headers["Content-Length"] = str(len(body))
|
|
counter.begin_forward(alias)
|
|
try:
|
|
upstream = httpx.post(
|
|
f"{self._proxy.upstream_base_url}{self.path}",
|
|
content=body,
|
|
headers=headers,
|
|
timeout=httpx.Timeout(60.0, connect=5.0),
|
|
)
|
|
finally:
|
|
counter.end_forward(alias)
|
|
# ``httpx.post`` has consumed the complete streaming body here.
|
|
# A genuine backend rejection must fail the probe; only a successful
|
|
# response is replaced with deterministic judge output.
|
|
upstream.raise_for_status()
|
|
payload = _deterministic_sse()
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "text/event-stream")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.send_header("Connection", "close")
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
self.wfile.flush()
|
|
completed = True
|
|
except Exception:
|
|
self._write_json_error(502, "live backend judge proxy failure")
|
|
finally:
|
|
counter.leave(alias, completed=completed)
|
|
|
|
def log_message(self, _format: str, *args: Any) -> None:
|
|
"""Keep expected local proxy traffic out of pytest output."""
|
|
|
|
def _write_json_error(self, status: int, detail: str) -> None:
|
|
payload = json.dumps({"error": {"message": detail}}).encode()
|
|
try:
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
self.send_header("Connection", "close")
|
|
self.end_headers()
|
|
self.wfile.write(payload)
|
|
self.wfile.flush()
|
|
except (BrokenPipeError, ConnectionError):
|
|
pass
|
|
|
|
|
|
class _JudgeCountingProxy:
|
|
def __init__(self, counter: _ConcurrencyCounter) -> None:
|
|
self._server = _JudgeCountingProxyServer(counter)
|
|
self._thread = threading.Thread(
|
|
target=self._server.serve_forever,
|
|
name="judge-parallel-live-proxy",
|
|
daemon=True,
|
|
)
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
host, port = self._server.server_address[:2]
|
|
if not isinstance(host, str) or not isinstance(port, int):
|
|
raise TypeError("judge counting proxy did not bind an IPv4 TCP address")
|
|
return f"http://{host}:{port}"
|
|
|
|
def credential_for(self, alias: str) -> str:
|
|
credential = f"turnstone-live-judge-{alias}"
|
|
self._server.alias_by_authorization[f"Bearer {credential}"] = alias
|
|
return credential
|
|
|
|
def __enter__(self) -> _JudgeCountingProxy:
|
|
self._thread.start()
|
|
return self
|
|
|
|
def __exit__(
|
|
self,
|
|
exc_type: type[BaseException] | None,
|
|
exc_value: BaseException | None,
|
|
traceback: TracebackType | None,
|
|
) -> None:
|
|
del exc_type, exc_value, traceback
|
|
self._server.shutdown()
|
|
self._server.server_close()
|
|
self._thread.join(timeout=5.0)
|
|
if self._thread.is_alive():
|
|
raise AssertionError("live judge counting proxy did not stop")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def live_model_id() -> str:
|
|
"""Auto-detect the model using the established live-backend contract."""
|
|
client = OpenAI(base_url=_LIVE_BASE_URL, api_key=_LIVE_API_KEY)
|
|
try:
|
|
models = client.models.list()
|
|
finally:
|
|
client.close()
|
|
ids = [model.id for model in models.data]
|
|
assert ids, "No models found on the live backend"
|
|
return ids[0]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _JudgeRun:
|
|
heuristics: list[IntentVerdict]
|
|
finals: list[IntentVerdict]
|
|
done_calls: int
|
|
|
|
|
|
def _run_live_judge_batch(
|
|
proxy: _JudgeCountingProxy,
|
|
model: str,
|
|
*,
|
|
alias_limit: int,
|
|
parallel_evaluations: int,
|
|
batch_size: int,
|
|
) -> _JudgeRun:
|
|
alias = "live-judge"
|
|
config = ModelConfig(
|
|
alias=alias,
|
|
base_url=proxy.base_url,
|
|
api_key=proxy.credential_for(alias),
|
|
model=model,
|
|
provider="openai-compatible",
|
|
max_concurrency=alias_limit,
|
|
)
|
|
registry = ModelRegistry({alias: config}, default=alias)
|
|
done = threading.Event()
|
|
finals: list[IntentVerdict] = []
|
|
done_calls = 0
|
|
|
|
def _done() -> None:
|
|
nonlocal done_calls
|
|
done_calls += 1
|
|
done.set()
|
|
|
|
try:
|
|
binding = resolve_model_binding(registry, alias)
|
|
judge = IntentJudge(
|
|
JudgeConfig(
|
|
enabled=True,
|
|
read_only_tools=False,
|
|
timeout=60.0,
|
|
parallel_evaluations=parallel_evaluations,
|
|
),
|
|
binding,
|
|
)
|
|
items = [
|
|
{
|
|
"func_name": "bash",
|
|
"func_args": {"command": f"echo live-{index}"},
|
|
"approval_label": "bash",
|
|
"call_id": f"live-call-{index}",
|
|
}
|
|
for index in range(batch_size)
|
|
]
|
|
heuristics = judge.evaluate(
|
|
items,
|
|
[{"role": "user", "content": "Run the independent live scheduler probes."}],
|
|
finals.append,
|
|
done_callback=_done,
|
|
)
|
|
assert done.wait(90.0), "live intent-judge batch did not finish"
|
|
return _JudgeRun(heuristics=heuristics, finals=finals, done_calls=done_calls)
|
|
finally:
|
|
registry.shutdown()
|
|
|
|
|
|
def _assert_exact_batch(run: _JudgeRun, *, batch_size: int) -> None:
|
|
expected_ids = {f"live-call-{index}" for index in range(batch_size)}
|
|
assert len(run.heuristics) == batch_size
|
|
assert len(run.finals) == batch_size
|
|
assert Counter(verdict.call_id for verdict in run.finals) == Counter(
|
|
{call_id: 1 for call_id in expected_ids}
|
|
)
|
|
assert all(verdict.tier == "llm" for verdict in run.finals)
|
|
assert all(verdict.intent_summary == "Live scheduler probe" for verdict in run.finals)
|
|
assert run.done_calls == 1
|
|
|
|
|
|
@pytest.mark.live
|
|
class TestLiveParallelJudgeCounts:
|
|
"""Count genuine upstream calls made by the intent-judge scheduler."""
|
|
|
|
def test_parallel_width_sets_exact_batch_peak(self, live_model_id: str) -> None:
|
|
counter = _ConcurrencyCounter(rendezvous_size=3)
|
|
with _JudgeCountingProxy(counter) as proxy:
|
|
run = _run_live_judge_batch(
|
|
proxy,
|
|
live_model_id,
|
|
alias_limit=0,
|
|
parallel_evaluations=3,
|
|
batch_size=5,
|
|
)
|
|
|
|
_assert_exact_batch(run, batch_size=5)
|
|
counts = counter.snapshot()
|
|
assert counts.requests == 5
|
|
assert counts.forwarded == 5
|
|
assert counts.completed == 5
|
|
assert counts.failed == 0
|
|
assert counts.active == 0
|
|
assert counts.peak == 3
|
|
assert counts.forward_active == 0
|
|
assert counts.forward_peak == 3
|
|
assert counts.requests_by_alias == {"live-judge": 5}
|
|
assert counts.forwarded_by_alias == {"live-judge": 5}
|
|
assert counts.completed_by_alias == {"live-judge": 5}
|
|
assert counts.peak_by_alias == {"live-judge": 3}
|
|
assert counts.forward_peak_by_alias == {"live-judge": 3}
|
|
assert counts.rendezvous_timed_out is False
|
|
|
|
def test_alias_cap_reduces_parallel_judge_peak(self, live_model_id: str) -> None:
|
|
counter = _ConcurrencyCounter(rendezvous_size=2)
|
|
with _JudgeCountingProxy(counter) as proxy:
|
|
run = _run_live_judge_batch(
|
|
proxy,
|
|
live_model_id,
|
|
alias_limit=2,
|
|
parallel_evaluations=4,
|
|
batch_size=4,
|
|
)
|
|
|
|
_assert_exact_batch(run, batch_size=4)
|
|
counts = counter.snapshot()
|
|
assert counts.requests == 4
|
|
assert counts.forwarded == 4
|
|
assert counts.completed == 4
|
|
assert counts.failed == 0
|
|
assert counts.active == 0
|
|
assert counts.peak == 2
|
|
assert counts.forward_active == 0
|
|
assert counts.forward_peak == 2
|
|
assert counts.requests_by_alias == {"live-judge": 4}
|
|
assert counts.forwarded_by_alias == {"live-judge": 4}
|
|
assert counts.completed_by_alias == {"live-judge": 4}
|
|
assert counts.peak_by_alias == {"live-judge": 2}
|
|
assert counts.forward_peak_by_alias == {"live-judge": 2}
|
|
assert counts.rendezvous_timed_out is False
|