|
|
@@ -54,6 +54,33 @@ class ScriptedTimingClient:
|
|
|
self.closed = True
|
|
|
|
|
|
|
|
|
+class WhitespaceTimingClient:
|
|
|
+ def __init__(self, clock: ManualClock) -> None:
|
|
|
+ self.clock = clock
|
|
|
+
|
|
|
+ async def stream_chat(
|
|
|
+ self,
|
|
|
+ messages: list[ChatMessage],
|
|
|
+ tools: list[dict[str, Any]],
|
|
|
+ params: AgentParams,
|
|
|
+ tool_choice: dict[str, Any] | None = None,
|
|
|
+ ) -> AsyncIterator[StreamItem]:
|
|
|
+ del messages, tools, params, tool_choice
|
|
|
+ self.clock.now = 0.011
|
|
|
+ yield StreamItem.raw_response_chunk({"provider": "first"})
|
|
|
+ self.clock.now = 0.019
|
|
|
+ yield StreamItem.message_delta("")
|
|
|
+ self.clock.now = 0.023
|
|
|
+ yield StreamItem.message_delta(" \n")
|
|
|
+ self.clock.now = 0.037
|
|
|
+ yield StreamItem.message_delta("visible")
|
|
|
+ self.clock.now = 0.050
|
|
|
+ yield StreamItem.usage_item(TokenUsage(total_tokens=5))
|
|
|
+
|
|
|
+ async def aclose(self) -> None:
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
class FallbackBenchmarkClient:
|
|
|
def __init__(self) -> None:
|
|
|
self.calls = 0
|
|
|
@@ -269,6 +296,42 @@ async def test_timing_client_keeps_provider_visible_and_elapsed_timings_separate
|
|
|
]
|
|
|
|
|
|
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_timing_client_ignores_blank_deltas_for_visible_ttft():
|
|
|
+ clock = ManualClock()
|
|
|
+ client = benchmark.TimingChatClient(
|
|
|
+ WhitespaceTimingClient(clock),
|
|
|
+ clock=clock,
|
|
|
+ )
|
|
|
+
|
|
|
+ async for _ in client.stream_chat(
|
|
|
+ messages=[],
|
|
|
+ tools=[],
|
|
|
+ params=AgentParams(model="benchmark-model"),
|
|
|
+ ):
|
|
|
+ pass
|
|
|
+
|
|
|
+ assert client.timings[0].provider_ttft_ms == 11
|
|
|
+ assert client.timings[0].visible_ttft_ms == 37
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.asyncio
|
|
|
+async def test_benchmark_output_queue_ignores_blank_deltas_for_visible_ttft():
|
|
|
+ clock = ManualClock()
|
|
|
+ queue = benchmark._BenchmarkOutputQueue(clock)
|
|
|
+
|
|
|
+ clock.now = 0.019
|
|
|
+ await queue.put({"type": "message_delta", "content": ""})
|
|
|
+ clock.now = 0.023
|
|
|
+ await queue.put({"type": "message_delta", "content": " \n\t "})
|
|
|
+ assert queue.first_message_delta_at is None
|
|
|
+
|
|
|
+ clock.now = 0.037
|
|
|
+ await queue.put({"type": "message_delta", "content": "visible"})
|
|
|
+
|
|
|
+ assert queue.first_message_delta_at == 0.037
|
|
|
+
|
|
|
+
|
|
|
@pytest.mark.asyncio
|
|
|
async def test_timing_client_records_failed_fallback_and_closes_inner_client():
|
|
|
clock = ManualClock()
|
|
|
@@ -651,7 +714,7 @@ async def test_runner_reports_argument_fallback_model_and_tool_ledgers(monkeypat
|
|
|
).run()
|
|
|
)[0]
|
|
|
|
|
|
- assert result.status == "passed"
|
|
|
+ assert result.status == "failed"
|
|
|
assert result.model_call_count == 1
|
|
|
assert result.fallback_count == 1
|
|
|
assert result.tool_count == 1
|
|
|
@@ -663,6 +726,10 @@ async def test_runner_reports_argument_fallback_model_and_tool_ledgers(monkeypat
|
|
|
assert result.event_names == ["device.volume.adjust"]
|
|
|
assert result.event_sources == ["text_event"]
|
|
|
assert result.tool_statuses == ["success"]
|
|
|
+ assert result.semantic_failures == [
|
|
|
+ "fallback_count expected 0, ledger got 1",
|
|
|
+ "fallback_count expected 0, timing got 1",
|
|
|
+ ]
|
|
|
assert client.calls == 2
|
|
|
assert client.closed is True
|
|
|
|
|
|
@@ -694,3 +761,93 @@ def test_visible_answer_order_ignores_empty_deltas_before_a_tool_result():
|
|
|
assert answers == ["late answer"]
|
|
|
assert first_answer_index == 2
|
|
|
assert first_tool_index == 1
|
|
|
+
|
|
|
+
|
|
|
+def _semantic_failures_for_counts(
|
|
|
+ *,
|
|
|
+ ledger_model_count: int,
|
|
|
+ timing_model_count: int,
|
|
|
+ ledger_fallback_count: int = 0,
|
|
|
+ timing_fallback_count: int = 0,
|
|
|
+) -> list[str]:
|
|
|
+ config = benchmark.BenchmarkConfig(
|
|
|
+ schema_version=1,
|
|
|
+ base_url="https://provider.example/v1",
|
|
|
+ model="benchmark-model",
|
|
|
+ cases=[benchmark.BenchmarkCaseId.ORDINARY_CHAT],
|
|
|
+ modes=[benchmark.BenchmarkMode.DUAL_AGENT],
|
|
|
+ )
|
|
|
+ runner = benchmark.BenchmarkRunner(config, api_key=None, mock=True)
|
|
|
+ timings = [
|
|
|
+ benchmark.BenchmarkModelCallTiming(
|
|
|
+ call_index=index,
|
|
|
+ call_kind=call_kind,
|
|
|
+ first_item_kind="raw_chunk",
|
|
|
+ provider_ttft_ms=0,
|
|
|
+ visible_ttft_ms=0 if call_kind == "chat_completion" else None,
|
|
|
+ elapsed_ms=0,
|
|
|
+ usage=TokenUsage(),
|
|
|
+ )
|
|
|
+ for index, call_kind in enumerate(
|
|
|
+ ["chat_completion"] * timing_model_count
|
|
|
+ + ["argument_fallback"] * timing_fallback_count,
|
|
|
+ start=1,
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ return runner._semantic_failures(
|
|
|
+ case=benchmark.BENCHMARK_CASE_CATALOG[
|
|
|
+ benchmark.BenchmarkCaseId.ORDINARY_CHAT
|
|
|
+ ],
|
|
|
+ mode=benchmark.BenchmarkMode.DUAL_AGENT,
|
|
|
+ outputs=[{"type": "message_delta", "content": "Ordinary answer."}],
|
|
|
+ audits=[],
|
|
|
+ event_names=[],
|
|
|
+ event_sources=[],
|
|
|
+ tool_statuses=[],
|
|
|
+ model_call_count=ledger_model_count,
|
|
|
+ fallback_count=ledger_fallback_count,
|
|
|
+ tool_count=0,
|
|
|
+ timings=timings,
|
|
|
+ check_mock_text=True,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize("actual_count", [0, 2])
|
|
|
+def test_semantics_reject_missing_or_extra_model_calls_even_when_ledgers_agree(
|
|
|
+ actual_count: int,
|
|
|
+):
|
|
|
+ failures = _semantic_failures_for_counts(
|
|
|
+ ledger_model_count=actual_count,
|
|
|
+ timing_model_count=actual_count,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert failures == [
|
|
|
+ f"model_call_count expected 1, ledger got {actual_count}",
|
|
|
+ f"model_call_count expected 1, timing got {actual_count}",
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def test_semantics_reject_extra_fallback_calls_even_when_ledgers_agree():
|
|
|
+ failures = _semantic_failures_for_counts(
|
|
|
+ ledger_model_count=1,
|
|
|
+ timing_model_count=1,
|
|
|
+ ledger_fallback_count=1,
|
|
|
+ timing_fallback_count=1,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert failures == [
|
|
|
+ "fallback_count expected 0, ledger got 1",
|
|
|
+ "fallback_count expected 0, timing got 1",
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def test_semantics_keep_timing_vs_ledger_consistency_failure():
|
|
|
+ failures = _semantic_failures_for_counts(
|
|
|
+ ledger_model_count=1,
|
|
|
+ timing_model_count=2,
|
|
|
+ )
|
|
|
+
|
|
|
+ assert failures == [
|
|
|
+ "model_call_count expected 1, timing got 2",
|
|
|
+ "model_call_count ledger=1, timing=2",
|
|
|
+ ]
|