Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

360 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""YAML config loader + integrity-aware validation.
The benchmark framework is YAML-driven (Yauhen's stated requirement: easy
to configure, parallel by default). Configs live under
``tests/benchmarks/cloudopsbench/configs/*.yml``. Loading a config goes through these
validation layers:
1. Pydantic — types and field constraints (always-on, fast).
2. ``BenchmarkConfig.lint()`` — anti-pattern checks (Gregg Ch 2 § 2.5
applied to benchmark configs): no replication, missing model pins,
missing cost budget, etc.
The framework refuses to start a run on a config that fails layer 2.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Literal
import yaml
from pydantic import BaseModel, Field, model_validator
from tests.benchmarks._framework.adapters import (
AdapterCapabilities,
Mode,
capabilities_for,
)
def _default_report_formats() -> list[Literal["json", "markdown", "html"]]:
"""Module-level factory keeps mypy happy with the Literal element type."""
return ["json", "markdown"]
def _resolve_capabilities_or_default(benchmark_name: str) -> AdapterCapabilities:
"""Look up an adapter's capabilities; fall back to the all-False default.
Unknown adapters return the all-False default — every gated feature
is refused. This is intentional: a typo in ``config.benchmark`` (e.g.
``cloudopsbnech``) must NOT silently bypass capability-based guards
just because the adapter cannot be looked up. The user gets a clear
"this feature requires the adapter to declare X" error for each
gated knob, plus the underlying unknown-benchmark error when the
runner subsequently tries to build the adapter via ``build_adapter``.
"""
try:
return capabilities_for(benchmark_name)
except (KeyError, ImportError):
return AdapterCapabilities()
# --------------------------------------------------------------------------- #
# Config schema #
# --------------------------------------------------------------------------- #
class FiltersConfig(BaseModel):
"""User-supplied case filters. Empty list = no filter on that dim."""
systems: list[str] = Field(default_factory=list)
fault_categories: list[str] = Field(default_factory=list)
difficulty: list[Literal["easy", "medium", "hard"]] = Field(default_factory=list)
seen_shape: list[bool] = Field(default_factory=list)
case_ids: list[str] = Field(default_factory=list)
limit: int | None = None
class BenchmarkConfig(BaseModel):
"""Top-level config for one benchmark run.
Example minimal YAML::
benchmark: cloudopsbench
modes: [opensre+llm]
llms: [claude-4-sonnet]
model_versions:
claude-4-sonnet: claude-sonnet-4-5-20250929
runs_per_case: 3
workers: 4
cost_budget_usd: 100
seed: 42
output_dir: .bench-results/test-run/
"""
# Which adapter to use
benchmark: str
# Which modes to run (typically both for paired comparison)
modes: list[Mode] = Field(min_length=1)
# LLMs to test (one row per LLM × mode × case × run)
llms: list[str] = Field(min_length=1)
# Locked model versions — refused at runtime if a model resolves
# to a different snapshot (integrity Mechanism: standardization)
model_versions: dict[str, str]
# Replication count per cell — required for honest variance estimate
# (Box-Hunter-Hunter Ch 3.4)
runs_per_case: int = Field(ge=1, default=3)
# Parallel workers — default sized to laptop; bump on AWS
workers: int = Field(ge=1, default=4)
# Hard cap on API spend — framework aborts cleanly when exceeded
# (Principle 11: cost as first-class metric)
cost_budget_usd: float = Field(gt=0)
# Seeded random case selection — Mechanism 6 (no cherry-picking)
seed: int
# Where artifacts land
output_dir: Path
# Optional case filtering
filters: FiltersConfig = Field(default_factory=FiltersConfig)
# Required for integrity Phase 0: pre-registration path
# If unset, framework refuses to start the run.
pre_registration_path: Path | None = None
# Required formats — at least one
report_formats: list[Literal["json", "markdown", "html"]] = Field(
default_factory=_default_report_formats, min_length=1
)
# Adapter-specific termination floor (currently honored only by the
# CloudOpsBench adapter's ``BenchInvestigationAgent``). When set, the
# CLI overrides ``BenchInvestigationAgent.MIN_TOOL_CALLS`` to this
# value before the run starts — keeping the floor as part of the
# experiment definition rather than a launch-time env var. Leave
# ``None`` to inherit the agent's default (which itself can be
# overridden by the ``BENCH_MIN_TOOL_CALLS`` env var at import time).
# Required for floor-ablation experiments so the floor is reproducible
# from the config file alone — see ``cloudopsbench_floor_ablation_v2_openai.yml``.
min_tool_calls: int | None = Field(ge=0, default=None)
# Adapter-specific bench agent variant. ``"default"`` (the default) keeps
# ``BenchInvestigationAgent`` and its full opensre system prompt — the
# apples-to-apples comparison with production behavior. ``"trimmed_prompt"``
# swaps in ``BenchInvestigationAgentTrimmedPrompt``, which keeps tool
# filtering + tool-output citation but drops the multi-stage / validation /
# hedging scaffolding the full opensre prompt carries. Honored only by the
# CloudOpsBench adapter; other adapters ignore the field.
#
# Predictor-drift mode (60% of opensre+llm losses on the floor=0 full-N
# run) is upstream of the predictor — opensre's investigation TEXT itself
# is biased toward adjacent vocabulary, which the predictor faithfully
# formalizes. This field exists to test whether a less structured prompt
# produces less adjacent-token bias.
agent_variant: Literal["default", "trimmed_prompt"] = "default"
# Predictor variant for adapters with a paper-format predictor stage.
# ``"default"`` (the default) uses the text-emit predictor in
# ``predictor/llm_call.py`` — fed back through opensre's LLM client wrapper.
# ``"structured"`` swaps in the OpenAI structured-outputs variant in
# ``predictor/llm_call_structured_openai.py`` — grammar-constrained sampling at
# the API level, so ``root_cause`` and ``fault_taxonomy`` are emitted
# from the closed vocabulary by construction (no off-vocab fallout).
#
# OpenAI-only (gpt-4o-2024-08-06+ or gpt-5). Honored only by the
# CloudOpsBench adapter — cross-field lint refuses ``"structured"`` on
# other adapters or with non-OpenAI llms.
predictor_variant: Literal["default", "structured"] = "default"
# ----------------------------------------------------------------------- #
# Pydantic-level validation #
# ----------------------------------------------------------------------- #
@model_validator(mode="after")
def _model_versions_cover_all_llms(self) -> BenchmarkConfig:
"""Every LLM in ``llms`` must have a pinned version."""
missing = set(self.llms) - set(self.model_versions.keys())
if missing:
raise ValueError(
f"model_versions missing pinned snapshot for: {sorted(missing)}. "
f"Pin every LLM in ``llms`` for reproducibility "
f"(integrity Mechanism: standardization)."
)
return self
# ----------------------------------------------------------------------- #
# Anti-pattern lint — refuses configs that would produce dishonest results #
# (Principle 8 + Gregg Ch 2 § 2.5 applied to benchmark configs) #
# ----------------------------------------------------------------------- #
def lint(self) -> list[str]:
"""Return list of anti-pattern errors. Empty list = config is honest.
Refuses configs exhibiting these anti-patterns:
- **Streetlight**: no validity metric declared by chosen adapter
(caught later by adapter MetricSchema)
- **Premature Conclusion**: ``runs_per_case < 3``
(single-run is statistical foot-gun for stochastic LLMs)
- **No Variance Reporting**: framework default reports median+IQR;
configurable here in future
- **Ad Hoc Checklist**: missing pre_registration_path
(Phase 0 integrity gate)
- **Marketing Narrative**: no negative_results requirement
(framework default — flagged here for awareness)
- **Random Change** signals: too many LLMs × modes × cases for one
cycle (recommend breaking into sub-runs)
"""
errors: list[str] = []
if self.runs_per_case < 3:
errors.append(
f"runs_per_case={self.runs_per_case} < 3 — single runs of "
"stochastic LLMs are unreliable. Set runs_per_case >= 3 "
"(Box-Hunter-Hunter Ch 3.4)."
)
if self.pre_registration_path is None:
errors.append(
"pre_registration_path is unset — integrity Phase 0 requires "
"expected_deltas committed to disk BEFORE the run starts. "
"Set pre_registration_path to a .yml file committed to git."
)
# Crude size check — warns rather than blocks
# Estimate: 452 (cloudopsbench full) × len(llms) × len(modes) × runs_per_case
estimated_runs = 452 * len(self.llms) * len(self.modes) * self.runs_per_case
if estimated_runs > 20000:
errors.append(
f"Estimated {estimated_runs} runs in one cycle — too large "
"for variance attribution. Split into multiple sub-runs."
)
if self.cost_budget_usd > 10_000:
errors.append(
f"cost_budget_usd=${self.cost_budget_usd:,.0f} is unusually "
"large for a single run. Confirm intent in pre-registration."
)
# Cross-field guard: agent_variant is silently ignored by adapters
# that don't declare ``supports_agent_variant=True`` in their
# ``AdapterCapabilities``. Setting it on such a config would run
# the wrong agent without warning — refuse the config so the
# intent is explicit. ``"default"`` is always allowed.
#
# Looking up capabilities by adapter (not by hardcoded
# ``benchmark == "cloudopsbench"``) means a new adapter that opts
# in to ``supports_agent_variant=True`` is automatically accepted
# by the framework without changes here.
adapter_caps = _resolve_capabilities_or_default(self.benchmark)
if self.agent_variant != "default" and not adapter_caps.supports_agent_variant:
errors.append(
f"agent_variant={self.agent_variant!r} requires the "
f"benchmark adapter to declare "
f"``supports_agent_variant=True`` in its "
f"``AdapterCapabilities``, but benchmark={self.benchmark!r} "
f"does not. The field would be silently ignored, producing "
f"an experiment that measures the default agent. Set "
f"agent_variant: default or use an adapter that supports it."
)
# Cross-field guard: predictor_variant="structured" requires an
# adapter that declares a predictor stage AND an OpenAI-compatible
# LLM (structured outputs is OpenAI-only on the predictor side).
if self.predictor_variant == "structured":
if not adapter_caps.supports_predictor_variant:
errors.append(
f"predictor_variant=structured requires the benchmark "
f"adapter to declare ``supports_predictor_variant=True`` "
f"in its ``AdapterCapabilities``, but "
f"benchmark={self.benchmark!r} does not. "
f"Set predictor_variant: default or use an adapter "
f"that has a predictor stage."
)
# Prefixes for OpenAI models that support structured outputs.
# Includes the o-series (o1, o3, o4-mini) and gpt-series. Other
# providers may add structured-output support — when they do, a
# peer ``llm_call_structured_<provider>.py`` module lands and
# the dispatcher routes by LLM provider. Until then, this guard
# refuses non-OpenAI llms with a clear error.
openai_prefixes = ("gpt-", "openai", "o1", "o3", "o4")
non_openai_llms = [llm for llm in self.llms if not llm.startswith(openai_prefixes)]
if non_openai_llms:
errors.append(
f"predictor_variant=structured currently supports OpenAI "
f"models only (gpt-4o-2024-08-06+, gpt-5, o-series). "
f"Found non-OpenAI llms: {non_openai_llms}. Either set "
"predictor_variant: default or restrict llms to OpenAI "
"models. Other-provider peer variants "
"(llm_call_structured_anthropic.py, "
"llm_call_structured_deepseek.py) are planned follow-ups."
)
# Output dir must not be a managed system path. Compare BOTH the lexical
# form and the resolved form (on macOS /etc → /private/etc symlink would
# bypass a check against only one). The narrow prefix list intentionally
# excludes user-writable temp paths like /var/folders (pytest tmpdir) and
# /var/tmp.
lexical = str(self.output_dir)
resolved = str(self.output_dir.resolve()) if self.output_dir.is_absolute() else lexical
system_prefixes = (
"/etc/",
"/usr/",
"/var/log/",
"/var/lib/",
"/var/run/",
"/private/etc/",
"/private/var/log/",
"/private/var/lib/",
"/private/var/run/",
)
system_exacts = {"/", "/etc", "/usr", "/var", "/private/etc", "/private/var"}
if any(s in system_exacts or s.startswith(system_prefixes) for s in (lexical, resolved)):
errors.append(f"output_dir={self.output_dir} would write to a system path — refuse.")
return errors
# --------------------------------------------------------------------------- #
# Loader #
# --------------------------------------------------------------------------- #
def load_config(path: Path) -> BenchmarkConfig:
"""Read YAML, parse via Pydantic, leave linting to caller.
The two-step (parse → lint) lets callers decide whether to abort or
warn on lint failures. The framework's runner refuses to start on
any lint failure.
"""
if not path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
with path.open("r", encoding="utf-8") as fh:
raw = yaml.safe_load(fh)
if not isinstance(raw, dict):
raise ValueError(f"Config file {path} must be a YAML mapping; got {type(raw).__name__}")
# Honor a few env-var overrides used in CI (override workers + budget
# without editing the file)
if env_workers := os.environ.get("OPENSRE_BENCH_WORKERS"):
raw["workers"] = int(env_workers)
if env_budget := os.environ.get("OPENSRE_BENCH_COST_BUDGET_USD"):
raw["cost_budget_usd"] = float(env_budget)
return BenchmarkConfig.model_validate(raw)
def validate_config_or_raise(path: Path) -> BenchmarkConfig:
"""Load + lint + raise on either failure. Use this from the runner's
pre-flight stage; use ``load_config`` + manual ``.lint()`` from tooling
that wants to inspect errors without raising.
"""
config = load_config(path)
errors = config.lint()
if errors:
raise ValueError(
"Benchmark config failed integrity lint:\n" + "\n".join(f" - {e}" for e in errors)
)
return config