Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions openclaw_bench/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path
from typing import Protocol

from .container import runtime_kind_target
from .models import BackendResponse, ModelSpec, TaskSpec


Expand Down Expand Up @@ -39,10 +40,19 @@ def __init__(
def _cmd(self, cwd: Path | None = None) -> list[str]:
if not self.container:
return ["openclaw"]
kind, target = runtime_kind_target(self.container)
if kind == "incus":
cmd = ["incus", "exec", target]
if cwd is not None:
cmd.extend(["--cwd", str(cwd)])
cmd.append("--")
cmd.append("openclaw")
return cmd
cmd = ["docker", "exec"]
if cwd is not None:
cmd.extend(["-w", str(cwd)])
cmd.extend([self.container, "openclaw"])
cmd.append(target)
cmd.append("openclaw")
return cmd

def smoke(self, model: ModelSpec, timeout_s: int) -> BackendResponse:
Expand Down Expand Up @@ -437,8 +447,10 @@ def _ensure_workspace_agent(


def _container_openclaw_cmd(container: str) -> list[str]:
return ["docker", "exec", container, "openclaw"]

kind, target = runtime_kind_target(container)
if kind == "incus":
return ["incus", "exec", target, "--", "openclaw"]
return ["docker", "exec", target, "openclaw"]

def _agent_id(session_id: str) -> str:
safe = "".join(char if char.isalnum() or char == "-" else "-" for char in session_id.lower())
Expand Down
44 changes: 37 additions & 7 deletions openclaw_bench/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def main(argv: list[str] | None = None) -> int:
default=60,
help="Seconds to wait for the OpenClaw route smoke before running task attempts",
)
preflight_parser = subparsers.add_parser("preflight", help="Check whether a benchmark run can execute safely")
preflight_parser = subparsers.add_parser("preflight", help="Check benchmark suite/model readiness before a run")
_add_common_run_args(preflight_parser)
preflight_parser.add_argument("--json", action="store_true", help="Print machine-readable preflight output")
preflight_parser.add_argument("--smoke-turn", action="store_true", help="Run a tiny OpenClaw model route turn for eligible models")
Expand All @@ -73,7 +73,7 @@ def main(argv: list[str] | None = None) -> int:
preflight_parser.add_argument("--smoke-timeout", type=int, default=60, help="Seconds to wait for each smoke turn")
provider_preflight_parser = subparsers.add_parser(
"provider-preflight",
help="Run the four verification gates against an OpenClaw profile + provider route.",
help="Verify an OpenClaw provider route: config, models list, health, and route smoke",
)
provider_preflight_parser.add_argument("--profile", required=True)
provider_preflight_parser.add_argument("--provider", required=True, choices=["vllm", "ollama", "llamacpp", "lmstudio"])
Expand Down Expand Up @@ -203,11 +203,17 @@ def _add_quickstart_init_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--no-validate", action="store_true", help="Write files without running openclaw config validate")
parser.add_argument("--no-detect", action="store_true", help="Skip provider auto-detection; use --vllm-* flags or env vars.")
parser.add_argument("--oc-runtime", default=None, help="Override OpenClaw runtime probe target (e.g., ssh:user@host).")
parser.add_argument(
"--probe-hosts",
default="127.0.0.1",
help="Comma-separated host addresses to probe; include bridge/LAN addresses for services not bound to loopback",
)


def _add_quickstart_lifecycle_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--openclaw-profile", default=DEFAULT_PROFILE)
parser.add_argument("--timeout", type=int, default=60)
parser.add_argument("--oc-runtime", default=None, help="Run OpenClaw commands via runtime target (incus:<instance> or docker:<container>)")


def init_command(args: argparse.Namespace) -> int:
Expand All @@ -226,6 +232,7 @@ def init_command(args: argparse.Namespace) -> int:
providers=["vllm", "ollama", "llamacpp", "lmstudio"],
probes=probes,
home=probe_home,
probe_hosts=_parse_csv(args.probe_hosts),
)
for finding in report.findings:
print(f"finding: {finding}")
Expand All @@ -235,7 +242,18 @@ def init_command(args: argparse.Namespace) -> int:
"to specify one explicitly, or start a model server first"
)
return 2
detected = next((c for c in report.candidates if c.provider == "vllm"), report.candidates[0])
detected = next((c for c in report.candidates if c.provider == "vllm"), None)
if detected is None:
detected_summary = ", ".join(
f"{candidate.provider}@{candidate.base_url}"
for candidate in report.candidates
)
print(
"detected local provider(s) that oc-bench init cannot generate yet: "
f"{detected_summary}; supported init provider: vllm. "
"Pass --no-detect with --vllm-base-url/--vllm-model for an explicit vLLM route."
)
return 2

# Inherit path: detection found an existing OC profile with the right wiring
# already configured. Clone it verbatim and skip the generation flow that
Expand Down Expand Up @@ -392,13 +410,21 @@ def _init_via_inheritance(


def start_command(args: argparse.Namespace) -> int:
check = start_benchclaw_gateway(args.openclaw_profile, timeout_s=args.timeout)
check = start_benchclaw_gateway(
args.openclaw_profile,
container=getattr(args, "oc_runtime", None),
timeout_s=args.timeout,
)
print(f"{check.name}={check.status} {check.notes}")
return 0 if check.status != "fail" else 1


def stop_command(args: argparse.Namespace) -> int:
check = stop_benchclaw_gateway(args.openclaw_profile, timeout_s=args.timeout)
check = stop_benchclaw_gateway(
args.openclaw_profile,
container=getattr(args, "oc_runtime", None),
timeout_s=args.timeout,
)
print(f"{check.name}={check.status} {check.notes}")
return 0 if check.status != "fail" else 1

Expand Down Expand Up @@ -449,7 +475,11 @@ def quickstart_command(args: argparse.Namespace) -> int:
return run_code
finally:
if args.stop_after and started:
stop_check = stop_benchclaw_gateway(args.openclaw_profile, timeout_s=30)
stop_check = stop_benchclaw_gateway(
args.openclaw_profile,
container=getattr(args, "oc_runtime", None),
timeout_s=30,
)
print(f"{stop_check.name}={stop_check.status} {stop_check.notes}")


Expand Down Expand Up @@ -575,7 +605,7 @@ def run_command(args: argparse.Namespace) -> int:
results = runner.run(config)
finally:
if stop_started_gateway:
stop_check = stop_openclaw_gateway(args.openclaw_profile, timeout_s=args.openclaw_gateway_timeout)
stop_check = stop_openclaw_gateway(args.openclaw_profile, args.openclaw_container, timeout_s=args.openclaw_gateway_timeout)
print(f"{stop_check.name}={stop_check.status} {stop_check.notes}")
failures = sum(1 for result in results if result.status != "pass")
print(f"run_id={run_id}")
Expand Down
18 changes: 18 additions & 0 deletions openclaw_bench/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ def to_row(self) -> dict[str, str]:
return {"name": self.name, "status": self.status, "notes": self.notes}


def runtime_kind_target(runtime: str) -> tuple[str, str]:
if ":" not in runtime:
return "docker", runtime
kind, _, target = runtime.partition(":")
kind = kind.strip().lower()
target = target.strip()
if kind not in {"docker", "incus"} or not target:
raise ValueError(f"unsupported OpenClaw runtime {runtime!r}; use docker:<container> or incus:<instance>")
return kind, target


def runtime_exec_prefix(runtime: str) -> list[str]:
kind, target = runtime_kind_target(runtime)
if kind == "incus":
return ["incus", "exec", target, "--"]
return ["docker", "exec", target]


def ensure_openclaw_container(
*,
container: str,
Expand Down
103 changes: 81 additions & 22 deletions openclaw_bench/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json
import os
import signal
import shlex
import shutil
import subprocess
import tempfile
Expand All @@ -11,7 +12,7 @@
from pathlib import Path

from .backend import OpenClawBackend
from .container import DEFAULT_GATEWAY_PORT, gateway_run_command
from .container import DEFAULT_GATEWAY_PORT, gateway_run_command, runtime_exec_prefix, runtime_kind_target
from .models import ModelSpec, SuiteManifest, TaskSpec
from .workspace import OPENCLAW_SEED_FILES, copy_fixture, seed_openclaw_workspace_files

Expand Down Expand Up @@ -255,7 +256,7 @@ def _check_output_path(path: Path, name: str) -> PreflightCheck:


def _check_container_path(path: Path, container: str, name: str) -> PreflightCheck:
cmd = ["docker", "exec", container, "test", "-d", str(path), "-a", "-w", str(path)]
cmd = [*runtime_exec_prefix(container), "test", "-d", str(path), "-a", "-w", str(path)]
try:
proc = subprocess.run(cmd, text=True, capture_output=True, timeout=10, check=False)
except (OSError, subprocess.TimeoutExpired) as exc:
Expand Down Expand Up @@ -345,13 +346,19 @@ def _check_openclaw(
) -> list[PreflightCheck]:
checks = []
if container:
docker = shutil.which("docker")
if docker is None:
return [PreflightCheck("openclaw_cli", "fail", "docker executable not found for --openclaw-container")]
checks.append(PreflightCheck("openclaw_cli", "pass", f"docker exec {container} openclaw"))
kind, target = runtime_kind_target(container)
executable = shutil.which(kind)
if executable is None:
return [PreflightCheck("openclaw_cli", "fail", f"{kind} executable not found for --openclaw-container/--oc-runtime")]
checks.append(PreflightCheck("openclaw_cli", "pass", f"{kind} exec {target} openclaw"))
else:
executable = shutil.which("openclaw")
if executable is None:
if local:
return [
PreflightCheck("openclaw_cli", "fail", "openclaw executable not found"),
PreflightCheck("openclaw_gateway", "warn", "--openclaw-local skips gateway requirement"),
]
return [PreflightCheck("openclaw_cli", "fail", "openclaw executable not found")]
checks.append(PreflightCheck("openclaw_cli", "pass", executable))
version_check = check_openclaw_version(container)
Expand Down Expand Up @@ -634,21 +641,26 @@ def ensure_openclaw_gateway(profile: str, container: str | None = None, timeout_
)


def stop_openclaw_gateway(profile: str, timeout_s: int = 30) -> PreflightCheck:
foreground = _stop_foreground_gateway(profile, timeout_s)
def stop_openclaw_gateway(profile: str, container: str | None = None, timeout_s: int = 30) -> PreflightCheck:
foreground = _stop_foreground_gateway(profile, timeout_s, container)
if foreground.status == "pass":
return PreflightCheck("openclaw_gateway_stop", "pass", foreground.notes)

cmd = ["openclaw", "--profile", profile, "gateway", "stop"]
cmd = [*_openclaw_cmd(container), "--profile", profile, "gateway", "stop"]
try:
proc = subprocess.run(cmd, text=True, capture_output=True, timeout=timeout_s, check=False)
except (OSError, subprocess.TimeoutExpired) as exc:
return PreflightCheck("openclaw_gateway_stop", "fail", str(exc))
output = f"{proc.stdout}\n{proc.stderr}".strip()
if proc.returncode != 0:
runtime_kill = _stop_runtime_gateway_process(profile, container, timeout_s)
if runtime_kill is not None and runtime_kill.status == "pass":
return PreflightCheck("openclaw_gateway_stop", "pass", runtime_kill.notes)
notes = _trim_output(output)
if foreground.status == "fail":
notes = _join_gateway_notes(notes, foreground.notes)
if runtime_kill is not None and runtime_kill.status == "fail":
notes = _join_gateway_notes(notes, runtime_kill.notes)
return PreflightCheck("openclaw_gateway_stop", "fail", notes)
if foreground.status == "fail":
return PreflightCheck("openclaw_gateway_stop", "fail", foreground.notes)
Expand All @@ -674,7 +686,10 @@ def _join_gateway_notes(prefix: str, notes: str) -> str:
def _openclaw_cmd(container: str | None = None) -> list[str]:
if not container:
return ["openclaw"]
return ["docker", "exec", container, "openclaw"]
kind, target = runtime_kind_target(container)
if kind == "incus":
return ["incus", "exec", target, "--", "openclaw"]
return ["docker", "exec", target, "openclaw"]


def _start_gateway_process(profile: str, container: str | None = None, timeout_s: int = 60) -> PreflightCheck:
Expand All @@ -687,7 +702,7 @@ def _start_gateway_process(profile: str, container: str | None = None, timeout_s
output = f"{start.stdout}\n{start.stderr}".strip()
if start.returncode != 0:
return PreflightCheck("openclaw_gateway_start", "fail", _trim_output(output))
return PreflightCheck("openclaw_gateway_start", "pass", _trim_output(output) or "started detached gateway in container")
return PreflightCheck("openclaw_gateway_start", "pass", _trim_output(output) or "started detached gateway in runtime")

start_cmd = _gateway_start_cmd(profile, None)
try:
Expand All @@ -709,11 +724,34 @@ def _start_gateway_process(profile: str, container: str | None = None, timeout_s

def _gateway_start_cmd(profile: str, container: str | None = None) -> list[str]:
if container:
return ["docker", "exec", "-d", container, "sh", "-lc", gateway_run_command(profile, DEFAULT_GATEWAY_PORT)]
kind, target = runtime_kind_target(container)
command = gateway_run_command(profile, DEFAULT_GATEWAY_PORT)
if kind == "incus":
return [
"incus",
"exec",
target,
"--",
"sh",
"-lc",
f"nohup {command} >{_runtime_gateway_log_path(profile)} 2>&1 </dev/null &",
]
return ["docker", "exec", "-d", target, "sh", "-lc", command]
return ["openclaw", "--profile", profile, "gateway", "--dev", "--verbose", "run"]


def _stop_foreground_gateway(profile: str, timeout_s: int = 30) -> PreflightCheck:
def _runtime_gateway_log_path(profile: str) -> str:
return f"/tmp/oc-bench-gateway-{_safe_path_part(profile)}.log"


def _stop_foreground_gateway(profile: str, timeout_s: int = 30, container: str | None = None) -> PreflightCheck:
if container:
kind, _ = runtime_kind_target(container)
return PreflightCheck(
"openclaw_gateway_foreground_stop",
"warn",
f"{kind} runtime gateways are stopped through the runtime OpenClaw command",
)
pid_path = _gateway_pid_path(profile)
if not pid_path.exists():
return PreflightCheck("openclaw_gateway_foreground_stop", "warn", f"no foreground pid file at {pid_path}")
Expand Down Expand Up @@ -743,6 +781,25 @@ def _stop_foreground_gateway(profile: str, timeout_s: int = 30) -> PreflightChec
return PreflightCheck("openclaw_gateway_foreground_stop", "fail", f"foreground gateway pid {pid} did not stop within {timeout_s}s")


def _stop_runtime_gateway_process(profile: str, container: str | None, timeout_s: int) -> PreflightCheck | None:
if not container:
return None
kind, _ = runtime_kind_target(container)
pattern = f"openclaw.*--profile {shlex.quote(profile)}.*gateway.*run"
cmd = [*runtime_exec_prefix(container), "sh", "-lc", f"pkill -f {shlex.quote(pattern)}"]
try:
proc = subprocess.run(cmd, text=True, capture_output=True, timeout=timeout_s, check=False)
except (OSError, subprocess.TimeoutExpired) as exc:
return PreflightCheck("openclaw_gateway_runtime_stop", "fail", str(exc))
output = f"{proc.stdout}\n{proc.stderr}".strip()
if proc.returncode in {0, 1}:
note = f"stopped {kind} runtime gateway for profile {profile}"
if proc.returncode == 1:
note = f"no {kind} runtime gateway process matched profile {profile}"
return PreflightCheck("openclaw_gateway_runtime_stop", "pass", note)
return PreflightCheck("openclaw_gateway_runtime_stop", "fail", _trim_output(output))


def _gateway_pid_path(profile: str) -> Path:
return Path(tempfile.gettempdir()) / f"openclaw-bench-gateway-{_safe_path_part(profile)}.pid"

Expand Down Expand Up @@ -832,16 +889,18 @@ def _check_provider_health(
url = base_url.rstrip("/") + "/models"
if container is None:
probe = LocalProbe()
elif container_kind == "docker":
probe = DockerExecProbe(container)
elif container_kind == "incus":
probe = IncusExecProbe(container)
else:
return PreflightCheck(
"provider_health",
"fail",
f"unsupported container_kind={container_kind!r}; use incus or docker",
)
container_kind, container = runtime_kind_target(container)
if container_kind == "docker":
probe = DockerExecProbe(container)
elif container_kind == "incus":
probe = IncusExecProbe(container)
else:
return PreflightCheck(
"provider_health",
"fail",
f"unsupported container_kind={container_kind!r}; use incus or docker",
)
try:
result = probe.http_get(
url,
Expand Down
Loading