|
@@ -5,6 +5,7 @@ import os
|
|
|
import re
|
|
import re
|
|
|
from collections import defaultdict
|
|
from collections import defaultdict
|
|
|
from collections.abc import Iterable, Sequence
|
|
from collections.abc import Iterable, Sequence
|
|
|
|
|
+from dataclasses import dataclass
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from typing import Literal
|
|
from typing import Literal
|
|
@@ -54,6 +55,8 @@ class BenchmarkAggregate(_StrictReportModel):
|
|
|
initial_provider_ttft_ms: BenchmarkLatencyAggregate
|
|
initial_provider_ttft_ms: BenchmarkLatencyAggregate
|
|
|
visible_ttft_ms: BenchmarkLatencyAggregate
|
|
visible_ttft_ms: BenchmarkLatencyAggregate
|
|
|
turn_wall_time_ms: BenchmarkLatencyAggregate
|
|
turn_wall_time_ms: BenchmarkLatencyAggregate
|
|
|
|
|
+ model_elapsed_ms: BenchmarkLatencyAggregate
|
|
|
|
|
+ tool_latency_ms: BenchmarkLatencyAggregate
|
|
|
prompt_tokens: BenchmarkValueAggregate
|
|
prompt_tokens: BenchmarkValueAggregate
|
|
|
completion_tokens: BenchmarkValueAggregate
|
|
completion_tokens: BenchmarkValueAggregate
|
|
|
total_tokens: BenchmarkValueAggregate
|
|
total_tokens: BenchmarkValueAggregate
|
|
@@ -81,6 +84,7 @@ class BenchmarkOverall(_StrictReportModel):
|
|
|
passed: int = Field(ge=0)
|
|
passed: int = Field(ge=0)
|
|
|
failed: int = Field(ge=0)
|
|
failed: int = Field(ge=0)
|
|
|
warnings: list[str]
|
|
warnings: list[str]
|
|
|
|
|
+ system_errors: list[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
class BenchmarkReport(_StrictReportModel):
|
|
class BenchmarkReport(_StrictReportModel):
|
|
@@ -97,6 +101,14 @@ class BenchmarkReportWriteError(RuntimeError):
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class _ReportReservation:
|
|
|
|
|
+ json_path: Path
|
|
|
|
|
+ markdown_path: Path
|
|
|
|
|
+ lock_path: Path
|
|
|
|
|
+ lock_fd: int
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def percentile(values: Sequence[int | float], quantile: float) -> float | None:
|
|
def percentile(values: Sequence[int | float], quantile: float) -> float | None:
|
|
|
if not 0.0 <= quantile <= 1.0:
|
|
if not 0.0 <= quantile <= 1.0:
|
|
|
raise ValueError("quantile must be between 0 and 1")
|
|
raise ValueError("quantile must be between 0 and 1")
|
|
@@ -146,7 +158,11 @@ def redact_text(value: str, *, secrets: Iterable[str] = ()) -> str:
|
|
|
def sanitize_error(value: str | None, *, secrets: Iterable[str] = ()) -> str | None:
|
|
def sanitize_error(value: str | None, *, secrets: Iterable[str] = ()) -> str | None:
|
|
|
if value is None:
|
|
if value is None:
|
|
|
return None
|
|
return None
|
|
|
- return redact_text(value, secrets=secrets)[:ERROR_LIMIT]
|
|
|
|
|
|
|
+ redacted = redact_text(value, secrets=secrets)
|
|
|
|
|
+ return redacted.encode("utf-8")[:ERROR_LIMIT].decode(
|
|
|
|
|
+ "utf-8",
|
|
|
|
|
+ errors="ignore",
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_benchmark_report(
|
|
def build_benchmark_report(
|
|
@@ -156,8 +172,13 @@ def build_benchmark_report(
|
|
|
mock: bool,
|
|
mock: bool,
|
|
|
generated_at: datetime | None = None,
|
|
generated_at: datetime | None = None,
|
|
|
secrets: Iterable[str] = (),
|
|
secrets: Iterable[str] = (),
|
|
|
|
|
+ system_errors: Sequence[str] = (),
|
|
|
) -> BenchmarkReport:
|
|
) -> BenchmarkReport:
|
|
|
secret_values = tuple(secret for secret in secrets if secret)
|
|
secret_values = tuple(secret for secret in secrets if secret)
|
|
|
|
|
+ sanitized_system_errors = [
|
|
|
|
|
+ sanitize_error(error, secrets=secret_values) or ""
|
|
|
|
|
+ for error in system_errors
|
|
|
|
|
+ ]
|
|
|
sanitized_runs = [
|
|
sanitized_runs = [
|
|
|
_sanitize_run(run, secrets=secret_values) for run in runs
|
|
_sanitize_run(run, secrets=secret_values) for run in runs
|
|
|
]
|
|
]
|
|
@@ -169,6 +190,7 @@ def build_benchmark_report(
|
|
|
]
|
|
]
|
|
|
passed = sum(run.status == "passed" for run in sanitized_runs)
|
|
passed = sum(run.status == "passed" for run in sanitized_runs)
|
|
|
failed = len(sanitized_runs) - passed
|
|
failed = len(sanitized_runs) - passed
|
|
|
|
|
+ system_failure_count = len(sanitized_system_errors)
|
|
|
resolved_generated_at = generated_at or datetime.now(timezone.utc)
|
|
resolved_generated_at = generated_at or datetime.now(timezone.utc)
|
|
|
if resolved_generated_at.tzinfo is None:
|
|
if resolved_generated_at.tzinfo is None:
|
|
|
resolved_generated_at = resolved_generated_at.replace(tzinfo=timezone.utc)
|
|
resolved_generated_at = resolved_generated_at.replace(tzinfo=timezone.utc)
|
|
@@ -188,10 +210,11 @@ def build_benchmark_report(
|
|
|
runs=sanitized_runs,
|
|
runs=sanitized_runs,
|
|
|
aggregates=aggregates,
|
|
aggregates=aggregates,
|
|
|
overall=BenchmarkOverall(
|
|
overall=BenchmarkOverall(
|
|
|
- attempted=len(sanitized_runs),
|
|
|
|
|
|
|
+ attempted=len(sanitized_runs) + system_failure_count,
|
|
|
passed=passed,
|
|
passed=passed,
|
|
|
- failed=failed,
|
|
|
|
|
|
|
+ failed=failed + system_failure_count,
|
|
|
warnings=warnings,
|
|
warnings=warnings,
|
|
|
|
|
+ system_errors=sanitized_system_errors,
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
@@ -270,11 +293,23 @@ def _aggregate_group(
|
|
|
]
|
|
]
|
|
|
for field_name in latency_fields
|
|
for field_name in latency_fields
|
|
|
}
|
|
}
|
|
|
|
|
+ latency_values["model_elapsed_ms"] = [
|
|
|
|
|
+ call.elapsed_ms
|
|
|
|
|
+ for run in passed_runs
|
|
|
|
|
+ for call in run.model_calls
|
|
|
|
|
+ if call.elapsed_ms is not None
|
|
|
|
|
+ ]
|
|
|
|
|
+ latency_values["tool_latency_ms"] = [
|
|
|
|
|
+ latency
|
|
|
|
|
+ for run in passed_runs
|
|
|
|
|
+ for latency in run.tool_latencies_ms
|
|
|
|
|
+ if latency is not None
|
|
|
|
|
+ ]
|
|
|
warnings = [
|
|
warnings = [
|
|
|
f"{case_id.value}/{mode.value} {field_name} p95 low confidence: "
|
|
f"{case_id.value}/{mode.value} {field_name} p95 low confidence: "
|
|
|
f"{len(values)} samples; fewer than 20"
|
|
f"{len(values)} samples; fewer than 20"
|
|
|
for field_name, values in latency_values.items()
|
|
for field_name, values in latency_values.items()
|
|
|
- if 0 < len(values) < 20
|
|
|
|
|
|
|
+ if len(values) < 20
|
|
|
]
|
|
]
|
|
|
value_fields = (
|
|
value_fields = (
|
|
|
"prompt_tokens",
|
|
"prompt_tokens",
|
|
@@ -310,6 +345,12 @@ def _aggregate_group(
|
|
|
turn_wall_time_ms=_latency_aggregate(
|
|
turn_wall_time_ms=_latency_aggregate(
|
|
|
latency_values["turn_wall_time_ms"]
|
|
latency_values["turn_wall_time_ms"]
|
|
|
),
|
|
),
|
|
|
|
|
+ model_elapsed_ms=_latency_aggregate(
|
|
|
|
|
+ latency_values["model_elapsed_ms"]
|
|
|
|
|
+ ),
|
|
|
|
|
+ tool_latency_ms=_latency_aggregate(
|
|
|
|
|
+ latency_values["tool_latency_ms"]
|
|
|
|
|
+ ),
|
|
|
prompt_tokens=value_aggregates["prompt_tokens"],
|
|
prompt_tokens=value_aggregates["prompt_tokens"],
|
|
|
completion_tokens=value_aggregates["completion_tokens"],
|
|
completion_tokens=value_aggregates["completion_tokens"],
|
|
|
total_tokens=value_aggregates["total_tokens"],
|
|
total_tokens=value_aggregates["total_tokens"],
|
|
@@ -368,8 +409,13 @@ def render_markdown(report: BenchmarkReport) -> str:
|
|
|
"",
|
|
"",
|
|
|
"## Aggregates",
|
|
"## Aggregates",
|
|
|
"",
|
|
"",
|
|
|
- "| Case | Mode | Attempted | Passed | Failed | Provider TTFT p50/p95 ms | Visible TTFT p50/p95 ms | Turn wall p50/p95 ms | Tokens total/mean | Model calls total/mean | Fallbacks total/mean | Tools total/mean |",
|
|
|
|
|
- "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
|
|
|
|
|
+ "| Case | Mode | Attempted | Passed | Failed | "
|
|
|
|
|
+ "Provider TTFT p50/p95 ms | Visible TTFT p50/p95 ms | "
|
|
|
|
|
+ "Turn wall p50/p95 ms | Model elapsed p50/p95 ms | "
|
|
|
|
|
+ "Tool latency p50/p95 ms | Tokens total/mean | "
|
|
|
|
|
+ "Model calls total/mean | Fallbacks total/mean | Tools total/mean |",
|
|
|
|
|
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | "
|
|
|
|
|
+ "---: | ---: | ---: | ---: | ---: | ---: |",
|
|
|
]
|
|
]
|
|
|
for aggregate in payload["aggregates"]:
|
|
for aggregate in payload["aggregates"]:
|
|
|
lines.append(
|
|
lines.append(
|
|
@@ -384,6 +430,8 @@ def render_markdown(report: BenchmarkReport) -> str:
|
|
|
_percentile_pair(aggregate["initial_provider_ttft_ms"]),
|
|
_percentile_pair(aggregate["initial_provider_ttft_ms"]),
|
|
|
_percentile_pair(aggregate["visible_ttft_ms"]),
|
|
_percentile_pair(aggregate["visible_ttft_ms"]),
|
|
|
_percentile_pair(aggregate["turn_wall_time_ms"]),
|
|
_percentile_pair(aggregate["turn_wall_time_ms"]),
|
|
|
|
|
+ _percentile_pair(aggregate["model_elapsed_ms"]),
|
|
|
|
|
+ _percentile_pair(aggregate["tool_latency_ms"]),
|
|
|
_total_mean(aggregate["total_tokens"]),
|
|
_total_mean(aggregate["total_tokens"]),
|
|
|
_total_mean(aggregate["model_call_count"]),
|
|
_total_mean(aggregate["model_call_count"]),
|
|
|
_total_mean(aggregate["fallback_count"]),
|
|
_total_mean(aggregate["fallback_count"]),
|
|
@@ -402,6 +450,15 @@ def render_markdown(report: BenchmarkReport) -> str:
|
|
|
else:
|
|
else:
|
|
|
lines.append("- None")
|
|
lines.append("- None")
|
|
|
|
|
|
|
|
|
|
+ lines.extend(["", "## System Errors", ""])
|
|
|
|
|
+ if overall["system_errors"]:
|
|
|
|
|
+ lines.extend(
|
|
|
|
|
+ f"- {_escape_markdown(str(error))}"
|
|
|
|
|
+ for error in overall["system_errors"]
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ lines.append("- None")
|
|
|
|
|
+
|
|
|
lines.extend(
|
|
lines.extend(
|
|
|
[
|
|
[
|
|
|
"",
|
|
"",
|
|
@@ -455,12 +512,15 @@ def write_benchmark_report(
|
|
|
directory = Path(output_dir)
|
|
directory = Path(output_dir)
|
|
|
temp_paths: list[Path] = []
|
|
temp_paths: list[Path] = []
|
|
|
published_paths: list[Path] = []
|
|
published_paths: list[Path] = []
|
|
|
|
|
+ reservation: _ReportReservation | None = None
|
|
|
try:
|
|
try:
|
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
|
stem = timestamp or report.generated_at.astimezone(timezone.utc).strftime(
|
|
stem = timestamp or report.generated_at.astimezone(timezone.utc).strftime(
|
|
|
"%Y%m%dT%H%M%SZ"
|
|
"%Y%m%dT%H%M%SZ"
|
|
|
)
|
|
)
|
|
|
- json_path, markdown_path = _unique_report_paths(directory, stem)
|
|
|
|
|
|
|
+ reservation = _reserve_report_paths(directory, stem)
|
|
|
|
|
+ json_path = reservation.json_path
|
|
|
|
|
+ markdown_path = reservation.markdown_path
|
|
|
nonce = uuid4().hex
|
|
nonce = uuid4().hex
|
|
|
json_temp = directory / f".{json_path.name}.{nonce}.tmp"
|
|
json_temp = directory / f".{json_path.name}.{nonce}.tmp"
|
|
|
markdown_temp = directory / f".{markdown_path.name}.{nonce}.tmp"
|
|
markdown_temp = directory / f".{markdown_path.name}.{nonce}.tmp"
|
|
@@ -481,6 +541,9 @@ def write_benchmark_report(
|
|
|
raise BenchmarkReportWriteError(
|
|
raise BenchmarkReportWriteError(
|
|
|
"failed to write benchmark reports"
|
|
"failed to write benchmark reports"
|
|
|
) from exc
|
|
) from exc
|
|
|
|
|
+ finally:
|
|
|
|
|
+ if reservation is not None:
|
|
|
|
|
+ _release_reservation(reservation)
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write_text(path: Path, content: str) -> None:
|
|
def _write_text(path: Path, content: str) -> None:
|
|
@@ -490,12 +553,55 @@ def _write_text(path: Path, content: str) -> None:
|
|
|
os.fsync(handle.fileno())
|
|
os.fsync(handle.fileno())
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _unique_report_paths(directory: Path, stem: str) -> tuple[Path, Path]:
|
|
|
|
|
|
|
+def _reserve_report_paths(directory: Path, stem: str) -> _ReportReservation:
|
|
|
suffix = 0
|
|
suffix = 0
|
|
|
while True:
|
|
while True:
|
|
|
candidate = stem if suffix == 0 else f"{stem}-{suffix}"
|
|
candidate = stem if suffix == 0 else f"{stem}-{suffix}"
|
|
|
json_path = directory / f"{candidate}.json"
|
|
json_path = directory / f"{candidate}.json"
|
|
|
markdown_path = directory / f"{candidate}.md"
|
|
markdown_path = directory / f"{candidate}.md"
|
|
|
- if not json_path.exists() and not markdown_path.exists():
|
|
|
|
|
- return json_path, markdown_path
|
|
|
|
|
|
|
+ lock_path = directory / f".{candidate}.lock"
|
|
|
|
|
+ try:
|
|
|
|
|
+ lock_fd = os.open(
|
|
|
|
|
+ lock_path,
|
|
|
|
|
+ os.O_CREAT | os.O_EXCL | os.O_WRONLY,
|
|
|
|
|
+ 0o600,
|
|
|
|
|
+ )
|
|
|
|
|
+ except FileExistsError:
|
|
|
|
|
+ suffix += 1
|
|
|
|
|
+ continue
|
|
|
|
|
+ reservation = _ReportReservation(
|
|
|
|
|
+ json_path=json_path,
|
|
|
|
|
+ markdown_path=markdown_path,
|
|
|
|
|
+ lock_path=lock_path,
|
|
|
|
|
+ lock_fd=lock_fd,
|
|
|
|
|
+ )
|
|
|
|
|
+ try:
|
|
|
|
|
+ paths_are_available = (
|
|
|
|
|
+ not json_path.exists() and not markdown_path.exists()
|
|
|
|
|
+ )
|
|
|
|
|
+ except BaseException:
|
|
|
|
|
+ _release_reservation(reservation)
|
|
|
|
|
+ raise
|
|
|
|
|
+ if paths_are_available:
|
|
|
|
|
+ return reservation
|
|
|
|
|
+ _release_reservation(reservation)
|
|
|
suffix += 1
|
|
suffix += 1
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _release_reservation(reservation: _ReportReservation) -> None:
|
|
|
|
|
+ try:
|
|
|
|
|
+ try:
|
|
|
|
|
+ path_stat = reservation.lock_path.stat()
|
|
|
|
|
+ fd_stat = os.fstat(reservation.lock_fd)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ return
|
|
|
|
|
+ if (path_stat.st_dev, path_stat.st_ino) == (
|
|
|
|
|
+ fd_stat.st_dev,
|
|
|
|
|
+ fd_stat.st_ino,
|
|
|
|
|
+ ):
|
|
|
|
|
+ reservation.lock_path.unlink(missing_ok=True)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ try:
|
|
|
|
|
+ os.close(reservation.lock_fd)
|
|
|
|
|
+ except OSError:
|
|
|
|
|
+ pass
|