4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
299 lines
10 KiB
Python
299 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
from config.config import LLMSettings
|
|
from tests.benchmarks.toolcall_model_benchmark.pricing import (
|
|
DEFAULT_REASONING_USD_PER_MTOK,
|
|
DEFAULT_TOOL_USD_PER_MTOK,
|
|
estimate_run_cost_usd,
|
|
)
|
|
|
|
|
|
def configure_split_models() -> None:
|
|
"""No-op placeholder — split-model routing is configured via LLM settings."""
|
|
|
|
|
|
FIXED_SCENARIO_IDS: tuple[str, ...] = (
|
|
"001-replication-lag",
|
|
"002-connection-exhaustion",
|
|
"003-storage-full",
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CaseMetrics:
|
|
"""Per-case benchmark measurements for one scenario run."""
|
|
|
|
scenario_id: str
|
|
run_status: Literal["ok", "error"]
|
|
duration_seconds: float
|
|
input_tokens: int
|
|
output_tokens: int
|
|
total_tokens: int
|
|
estimated_cost_usd: float
|
|
error: str = ""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SummaryMetrics:
|
|
"""Aggregate totals and averages across all executed cases."""
|
|
|
|
case_count: int
|
|
success_count: int
|
|
error_count: int
|
|
total_duration_seconds: float
|
|
avg_duration_seconds: float
|
|
total_input_tokens: int
|
|
total_output_tokens: int
|
|
total_tokens: int
|
|
total_estimated_cost_usd: float
|
|
avg_estimated_cost_usd: float
|
|
|
|
|
|
def _resolve_models() -> tuple[str, str]:
|
|
"""Resolve reasoning/tool model IDs from active provider environment settings."""
|
|
settings = LLMSettings.from_env()
|
|
provider = settings.provider
|
|
reasoning_attr = f"{provider}_reasoning_model"
|
|
tool_attr = f"{provider}_toolcall_model"
|
|
reasoning_model = getattr(settings, reasoning_attr, None)
|
|
tool_model = getattr(settings, tool_attr, None)
|
|
if reasoning_model is None or tool_model is None:
|
|
raise ValueError(
|
|
f"Provider {provider!r} is missing attributes {reasoning_attr!r} "
|
|
f"or {tool_attr!r} on LLMSettings."
|
|
)
|
|
return str(reasoning_model), str(tool_model)
|
|
|
|
|
|
def _summarize(cases: list[CaseMetrics]) -> SummaryMetrics:
|
|
"""Compute benchmark summary totals and averages from per-case data."""
|
|
case_count = len(cases)
|
|
success_count = sum(1 for c in cases if c.run_status == "ok")
|
|
error_count = case_count - success_count
|
|
total_duration = sum(c.duration_seconds for c in cases)
|
|
total_input = sum(c.input_tokens for c in cases)
|
|
total_output = sum(c.output_tokens for c in cases)
|
|
total_tokens = sum(c.total_tokens for c in cases)
|
|
total_cost = sum(c.estimated_cost_usd for c in cases)
|
|
|
|
return SummaryMetrics(
|
|
case_count=case_count,
|
|
success_count=success_count,
|
|
error_count=error_count,
|
|
total_duration_seconds=total_duration,
|
|
avg_duration_seconds=(total_duration / case_count) if case_count else 0.0,
|
|
total_input_tokens=total_input,
|
|
total_output_tokens=total_output,
|
|
total_tokens=total_tokens,
|
|
total_estimated_cost_usd=total_cost,
|
|
avg_estimated_cost_usd=(total_cost / case_count) if case_count else 0.0,
|
|
)
|
|
|
|
|
|
def _sanitize_error_for_markdown(error: str) -> str:
|
|
"""Normalize error text for single-line markdown table rendering."""
|
|
cleaned = error.replace("\n", " ").replace("|", "\\|").strip()
|
|
if len(cleaned) > 140:
|
|
return cleaned[:137] + "..."
|
|
return cleaned
|
|
|
|
|
|
def _scope_line(cases: list[CaseMetrics]) -> str:
|
|
"""Build scope text from executed scenarios to avoid misleading hardcoded output."""
|
|
ids = [c.scenario_id for c in cases]
|
|
if not ids:
|
|
return "Scope: no scenarios executed."
|
|
return f"Scope: {', '.join(ids)}."
|
|
|
|
|
|
def render_markdown(cases: list[CaseMetrics], summary: SummaryMetrics) -> str:
|
|
"""Render a markdown benchmark report with per-case metrics and summary."""
|
|
lines: list[str] = []
|
|
lines.append("# OpenSRE Benchmark")
|
|
lines.append("")
|
|
lines.append(_scope_line(cases))
|
|
lines.append("Metrics reported: duration, token usage, estimated LLM cost.")
|
|
lines.append("Not measured: accuracy, false positives, false negatives.")
|
|
lines.append("")
|
|
lines.append("## Per-case Metrics")
|
|
lines.append("")
|
|
lines.append(
|
|
"| Scenario | Status | Duration (s) | Input Tokens | Output Tokens | Total Tokens | Est. Cost (USD) | Error |"
|
|
)
|
|
lines.append("|---|---|---:|---:|---:|---:|---:|---|")
|
|
for c in cases:
|
|
err = _sanitize_error_for_markdown(c.error) if c.error else ""
|
|
lines.append(
|
|
f"| {c.scenario_id} | {c.run_status} | {c.duration_seconds:.2f} | "
|
|
f"{c.input_tokens} | {c.output_tokens} | {c.total_tokens} | "
|
|
f"{c.estimated_cost_usd:.6f} | {err} |"
|
|
)
|
|
|
|
lines.append("")
|
|
lines.append("## Summary")
|
|
lines.append("")
|
|
lines.append(f"- Cases: {summary.case_count}")
|
|
lines.append(f"- Successful runs: {summary.success_count}")
|
|
lines.append(f"- Failed runs: {summary.error_count}")
|
|
lines.append(f"- Total duration (s): {summary.total_duration_seconds:.2f}")
|
|
lines.append(f"- Avg duration (s): {summary.avg_duration_seconds:.2f}")
|
|
lines.append(f"- Total input tokens: {summary.total_input_tokens}")
|
|
lines.append(f"- Total output tokens: {summary.total_output_tokens}")
|
|
lines.append(f"- Total tokens: {summary.total_tokens}")
|
|
lines.append(f"- Total estimated cost (USD): {summary.total_estimated_cost_usd:.6f}")
|
|
lines.append(f"- Avg estimated cost (USD): {summary.avg_estimated_cost_usd:.6f}")
|
|
lines.append("")
|
|
lines.append("## Notes")
|
|
lines.append("")
|
|
lines.append("- This is an operational benchmark report, not an evaluation scorecard.")
|
|
lines.append("- Accuracy and FP/FN require a separate evaluation workflow.")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def run_benchmark(
|
|
scenario_ids: list[str] | None = None,
|
|
*,
|
|
configure_llm: Callable[[], None] = configure_split_models,
|
|
reasoning_usd_per_mtok: float = DEFAULT_REASONING_USD_PER_MTOK,
|
|
tool_usd_per_mtok: float = DEFAULT_TOOL_USD_PER_MTOK,
|
|
) -> tuple[list[CaseMetrics], SummaryMetrics]:
|
|
"""Execute benchmark cases and collect duration, token, and cost metrics."""
|
|
from tests.benchmarks.toolcall_model_benchmark.pipeline_benchmark import (
|
|
get_fixture_by_id,
|
|
run_investigation_bench,
|
|
)
|
|
|
|
selected = scenario_ids if scenario_ids is not None else list(FIXED_SCENARIO_IDS)
|
|
reasoning_model, tool_model = _resolve_models()
|
|
|
|
cases: list[CaseMetrics] = []
|
|
for sid in selected:
|
|
try:
|
|
fixture = get_fixture_by_id(sid)
|
|
run = run_investigation_bench(
|
|
fixture,
|
|
label=sid,
|
|
configure_llm=configure_llm,
|
|
)
|
|
est_cost_usd, _ = estimate_run_cost_usd(
|
|
run.tokens_by_model,
|
|
reasoning_model=reasoning_model,
|
|
tool_model=tool_model,
|
|
reasoning_usd_per_mtok=reasoning_usd_per_mtok,
|
|
tool_usd_per_mtok=tool_usd_per_mtok,
|
|
)
|
|
cases.append(
|
|
CaseMetrics(
|
|
scenario_id=sid,
|
|
run_status="ok",
|
|
duration_seconds=run.wall_seconds,
|
|
input_tokens=run.tokens.input_tokens,
|
|
output_tokens=run.tokens.output_tokens,
|
|
total_tokens=run.tokens.total,
|
|
estimated_cost_usd=est_cost_usd,
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
logger.exception("[benchmark] failed scenario %s", sid)
|
|
cases.append(
|
|
CaseMetrics(
|
|
scenario_id=sid,
|
|
run_status="error",
|
|
duration_seconds=0.0,
|
|
input_tokens=0,
|
|
output_tokens=0,
|
|
total_tokens=0,
|
|
estimated_cost_usd=0.0,
|
|
error=str(exc),
|
|
)
|
|
)
|
|
|
|
return cases, _summarize(cases)
|
|
|
|
|
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
"""Parse benchmark CLI arguments."""
|
|
parser = argparse.ArgumentParser(description="Run OpenSRE benchmark on fixed synthetic cases.")
|
|
parser.add_argument(
|
|
"--scenario",
|
|
action="append",
|
|
default=[],
|
|
help="Optional scenario id override (repeatable). Default: 001,002,003.",
|
|
)
|
|
parser.add_argument(
|
|
"--md-out",
|
|
default="docs/benchmarks/results.md",
|
|
help="Path for markdown output.",
|
|
)
|
|
parser.add_argument(
|
|
"--reasoning-usd-per-mtok", type=float, default=DEFAULT_REASONING_USD_PER_MTOK
|
|
)
|
|
parser.add_argument("--tool-usd-per-mtok", type=float, default=DEFAULT_TOOL_USD_PER_MTOK)
|
|
parser.add_argument(
|
|
"--no-update-readme",
|
|
action="store_true",
|
|
default=False,
|
|
help="Skip updating the README.md benchmark section.",
|
|
)
|
|
parser.add_argument(
|
|
"--readme-path",
|
|
default=None,
|
|
help="Path to README.md. Default: auto-detect repo root.",
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""Load environment, run benchmark, and write markdown report."""
|
|
load_dotenv(override=False)
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
|
|
args = parse_args(argv)
|
|
selected = list(args.scenario) if args.scenario else list(FIXED_SCENARIO_IDS)
|
|
|
|
cases, summary = run_benchmark(
|
|
selected,
|
|
reasoning_usd_per_mtok=args.reasoning_usd_per_mtok,
|
|
tool_usd_per_mtok=args.tool_usd_per_mtok,
|
|
)
|
|
|
|
md_out = Path(args.md_out)
|
|
md_out.parent.mkdir(parents=True, exist_ok=True)
|
|
md_out.write_text(render_markdown(cases, summary), encoding="utf-8")
|
|
|
|
logger.info("Wrote markdown report: %s", md_out)
|
|
|
|
if not args.no_update_readme:
|
|
from tests.benchmarks.toolcall_model_benchmark.readme_updater import (
|
|
_find_repo_root,
|
|
render_readme_summary,
|
|
update_readme_benchmarks,
|
|
)
|
|
|
|
if args.readme_path:
|
|
readme_path = Path(args.readme_path)
|
|
else:
|
|
readme_path = _find_repo_root() / "README.md"
|
|
snippet = render_readme_summary(cases, summary)
|
|
try:
|
|
update_readme_benchmarks(readme_path, snippet)
|
|
except ValueError as exc:
|
|
logger.warning("Skipped README update: %s", exc)
|
|
|
|
return 0 if summary.error_count == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|