#!/usr/bin/env python3 """ Shared helpers for edit benchmark scripts. """ from __future__ import annotations import argparse import asyncio import difflib import json import os import shutil import sys import tempfile import threading import time from dataclasses import dataclass from pathlib import Path from typing import Any, Callable REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT / "python/omp-rpc/src")) from omp_rpc import MessageEndEvent, MessageStartEvent, MessageUpdateEvent, RpcClient, ToolExecutionStartEvent # noqa: E402 MODELS = [ "openrouter/moonshotai/kimi-k2.5", "openrouter/anthropic/claude-haiku-4.5", "openrouter/google/gemini-3.1-flash-lite-preview", "openrouter/z-ai/glm-4.7-20251222:nitro" # "openrouter/anthropic/claude-sonnet-4.6", # "openrouter/google/gemini-3-flash-preview", # "openrouter/z-ai/glm-5-turbo", # "openrouter/minimax/minimax-m2.7", ] INITIAL_CONTENT = """\ use std::collections::HashMap; use std::fmt; #[derive(Debug, Clone)] pub struct Config { pub host: String, pub port: u16, pub max_connections: usize, pub timeout_ms: u64, } impl Config { pub fn new(host: &str, port: u16) -> Self { Self { host: host.to_string(), port, max_connections: 100, timeout_ms: 5000, } } pub fn is_local(&self) -> bool { self.host == "localhost" || self.host == "127.0.0.1" } } impl fmt::Display for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", self.host, self.port) } } #[derive(Debug, Clone, PartialEq)] pub enum Status { Active, Inactive, Error(String), } impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Status::Active => write!(f, "active"), Status::Inactive => write!(f, "inactive"), Status::Error(msg) => write!(f, "error: {}", msg), } } } pub struct ConnectionPool { config: Config, connections: Vec, status: Status, } struct Connection { id: u64, active: bool, } impl ConnectionPool { pub fn new(config: Config) -> Self { Self { config, connections: Vec::new(), status: Status::Inactive, } } pub fn connect(&mut self) -> Result<(), String> { if self.connections.len() >= self.config.max_connections { return Err("connection pool is full".to_string()); } let id = self.connections.len() as u64; self.connections.push(Connection { id, active: true }); self.status = Status::Active; Ok(()) } pub fn disconnect(&mut self, id: u64) -> Result<(), String> { let conn = self .connections .iter_mut() .find(|c| c.id == id) .ok_or_else(|| format!("connection {} not found", id))?; conn.active = false; if self.connections.iter().all(|c| !c.active) { self.status = Status::Inactive; } Ok(()) } pub fn active_count(&self) -> usize { self.connections.iter().filter(|c| c.active).count() } pub fn status(&self) -> &Status { &self.status } } pub fn parse_address(addr: &str) -> Result<(String, u16), String> { let parts: Vec<&str> = addr.split(':').collect(); if parts.len() != 2 { return Err("invalid address format".to_string()); } let host = parts[0].to_string(); let port = parts[1] .parse::() .map_err(|e| format!("invalid port: {}", e))?; Ok((host, port)) } pub fn create_pool_from_address(addr: &str) -> Result { let (host, port) = parse_address(addr)?; let config = Config::new(&host, port); Ok(ConnectionPool::new(config)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_new() { let config = Config::new("localhost", 8080); assert_eq!(config.host, "localhost"); assert_eq!(config.port, 8080); assert_eq!(config.max_connections, 100); } #[test] fn test_config_is_local() { let local = Config::new("localhost", 8080); assert!(local.is_local()); let remote = Config::new("example.com", 443); assert!(!remote.is_local()); } #[test] fn test_connection_pool() { let config = Config::new("localhost", 5432); let mut pool = ConnectionPool::new(config); assert_eq!(pool.active_count(), 0); pool.connect().unwrap(); assert_eq!(pool.active_count(), 1); assert_eq!(pool.status(), &Status::Active); pool.disconnect(0).unwrap(); assert_eq!(pool.active_count(), 0); assert_eq!(pool.status(), &Status::Inactive); } #[test] fn test_parse_address() { let (host, port) = parse_address("localhost:8080").unwrap(); assert_eq!(host, "localhost"); assert_eq!(port, 8080); assert!(parse_address("invalid").is_err()); } } """ EXPECTED_CONTENT = """\ use std::collections::HashMap; use std::fmt; use std::time::Instant; #[derive(Debug, Clone)] pub struct Config { pub host: String, pub port: u16, pub max_connections: usize, pub timeout_ms: u64, pub retry_attempts: u32, } impl Config { pub fn new(host: &str, port: u16) -> Self { Self { host: host.to_string(), port, max_connections: 100, timeout_ms: 5000, retry_attempts: 3, } } pub fn with_max_connections(mut self, max: usize) -> Self { self.max_connections = max; self } pub fn is_local(&self) -> bool { self.host == "localhost" || self.host == "127.0.0.1" } } impl fmt::Display for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}:{} (max: {})", self.host, self.port, self.max_connections) } } #[derive(Debug, Clone, PartialEq)] pub enum Status { Active, Inactive, Draining, Error(String), } impl Status { pub fn is_available(&self) -> bool { matches!(self, Status::Active) } } impl fmt::Display for Status { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Status::Active => write!(f, "active"), Status::Inactive => write!(f, "inactive"), Status::Draining => write!(f, "draining"), Status::Error(msg) => write!(f, "error: {}", msg), } } } pub struct ConnectionPool { config: Config, connections: Vec, status: Status, stats: PoolStats, } struct Connection { id: u64, active: bool, created_at: Instant, } #[derive(Default)] struct PoolStats { total_connections: u64, failed_connections: u64, } impl ConnectionPool { pub fn new(config: Config) -> Self { Self { config, connections: Vec::new(), status: Status::Inactive, stats: PoolStats::default(), } } pub fn connect(&mut self) -> Result { if self.connections.len() >= self.config.max_connections { self.stats.failed_connections += 1; return Err("connection pool is full".to_string()); } let id = self.stats.total_connections; self.stats.total_connections += 1; self.connections.push(Connection { id, active: true, created_at: Instant::now(), }); self.status = Status::Active; Ok(id) } pub fn disconnect(&mut self, id: u64) -> Result<(), String> { let conn = self .connections .iter_mut() .find(|c| c.id == id && c.active) .ok_or_else(|| format!("active connection {} not found", id))?; conn.active = false; if self.connections.iter().all(|c| !c.active) { self.status = Status::Inactive; } Ok(()) } pub fn drain(&mut self) { self.status = Status::Draining; for conn in &mut self.connections { conn.active = false; } self.status = Status::Inactive; } pub fn active_count(&self) -> usize { self.connections.iter().filter(|c| c.active).count() } pub fn total_created(&self) -> u64 { self.stats.total_connections } pub fn status(&self) -> &Status { &self.status } } pub fn parse_address(addr: &str) -> Result<(String, u16), String> { let parts: Vec<&str> = addr.splitn(2, ':').collect(); if parts.len() != 2 { return Err(format!("invalid address format: {}", addr)); } let host = parts[0].to_string(); if host.is_empty() { return Err("host cannot be empty".to_string()); } let port = parts[1] .parse::() .map_err(|e| format!("invalid port: {}", e))?; Ok((host, port)) } pub fn create_pool_from_address(addr: &str) -> Result { let (host, port) = parse_address(addr)?; let config = Config::new(&host, port); Ok(ConnectionPool::new(config)) } pub fn create_pool_with_options( addr: &str, max_connections: usize, ) -> Result { let (host, port) = parse_address(addr)?; let config = Config::new(&host, port).with_max_connections(max_connections); Ok(ConnectionPool::new(config)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_new() { let config = Config::new("localhost", 8080); assert_eq!(config.host, "localhost"); assert_eq!(config.port, 8080); assert_eq!(config.max_connections, 100); assert_eq!(config.retry_attempts, 3); } #[test] fn test_config_builder() { let config = Config::new("localhost", 8080).with_max_connections(50); assert_eq!(config.max_connections, 50); } #[test] fn test_config_is_local() { let local = Config::new("localhost", 8080); assert!(local.is_local()); let remote = Config::new("example.com", 443); assert!(!remote.is_local()); } #[test] fn test_connection_pool() { let config = Config::new("localhost", 5432); let mut pool = ConnectionPool::new(config); assert_eq!(pool.active_count(), 0); assert_eq!(pool.total_created(), 0); let id = pool.connect().unwrap(); assert_eq!(pool.active_count(), 1); assert_eq!(pool.total_created(), 1); assert_eq!(pool.status(), &Status::Active); pool.disconnect(id).unwrap(); assert_eq!(pool.active_count(), 0); assert_eq!(pool.status(), &Status::Inactive); } #[test] fn test_pool_drain() { let config = Config::new("localhost", 5432); let mut pool = ConnectionPool::new(config); pool.connect().unwrap(); pool.connect().unwrap(); assert_eq!(pool.active_count(), 2); pool.drain(); assert_eq!(pool.active_count(), 0); assert_eq!(pool.status(), &Status::Inactive); } #[test] fn test_parse_address() { let (host, port) = parse_address("localhost:8080").unwrap(); assert_eq!(host, "localhost"); assert_eq!(port, 8080); assert!(parse_address("invalid").is_err()); assert!(parse_address(":8080").is_err()); } #[test] fn test_status_is_available() { assert!(Status::Active.is_available()); assert!(!Status::Inactive.is_available()); assert!(!Status::Draining.is_available()); } } """ def _compute_edit_diff() -> str: initial_lines = INITIAL_CONTENT.splitlines(keepends=True) expected_lines = EXPECTED_CONTENT.splitlines(keepends=True) diff = difflib.unified_diff(initial_lines, expected_lines, n=3) # Skip the --- and +++ header lines, keep only @@ hunks diff_lines = list(diff) return "".join(diff_lines[2:]) if len(diff_lines) > 2 else "" EDIT_DIFF = _compute_edit_diff() FEEDBACK_PROMPT = """\ STOP. The editing task is complete. Do NOT make any more edits or tool calls. This is a survey. Answer these 6 questions about your experience using the editing tool (2-3 sentences each): 1. Tool input schema: Was the input schema intuitive? What confused you? 2. Tool description: Was the description clear enough? What was missing? 3. Tool behaviour: What would make the tool easier to use? 4. Tool results & errors: Were error messages helpful? What could improve? 5. Bugs: Did anything behave unexpectedly? 6. Other thoughts: Anything else? """ DEFAULT_MAX_TURNS = 5 MAX_TOOL_CALLS_PER_PROMPT = 6 _PRINT_LOCK = threading.Lock() @dataclass(frozen=True) class BenchmarkSpec: description: str workspace_prefix: str tools: tuple[str, ...] env: dict[str, str] initial_prompt: str retry_instruction: str @dataclass class BenchmarkResult: model: str success: bool turns_used: int prompt_attempts: int edit_calls: int token_input: int token_output: int feedback: str error: str | None = None class VerbosePrinter: def __init__(self, model: str): self._label = model.removeprefix("openrouter/") self._open_kind: str | None = None self._seen_block_lengths: dict[tuple[str, int], int] = {} def _prefix(self, kind: str) -> str: return f"[{self._label}] {kind}> " def flush(self) -> None: with _PRINT_LOCK: if self._open_kind is None: return sys.stderr.write("\n") sys.stderr.flush() self._open_kind = None def emit_delta(self, kind: str, delta: str, content_index: int | None = None) -> None: if not delta: return if content_index is not None: key = (kind, content_index) self._seen_block_lengths[key] = self._seen_block_lengths.get(key, 0) + len(delta) with _PRINT_LOCK: if self._open_kind != kind: if self._open_kind is not None: sys.stderr.write("\n") sys.stderr.write(self._prefix(kind)) self._open_kind = kind parts = delta.splitlines(keepends=True) for index, part in enumerate(parts): if index > 0: sys.stderr.write(self._prefix(kind)) sys.stderr.write(part) if delta.endswith("\n"): self._open_kind = None sys.stderr.flush() def emit_tool_call(self, tool_name: str, args: Any) -> None: rendered_args = json.dumps(args, ensure_ascii=False, sort_keys=True, separators=(",", ":")) with _PRINT_LOCK: if self._open_kind is not None: sys.stderr.write("\n") self._open_kind = None sys.stderr.write(f"{self._prefix('tool')}{tool_name} {rendered_args}\n") sys.stderr.flush() def reset_message(self) -> None: self.flush() self._seen_block_lengths.clear() def emit_missing_from_message(self, message: dict[str, Any]) -> None: content = message.get("content") if not isinstance(content, list): return for content_index, block in enumerate(content): if not isinstance(block, dict): continue block_type = block.get("type") if block_type == "text": text = block.get("text") kind = "text" elif block_type == "thinking": text = block.get("thinking") kind = "thinking" else: continue if not isinstance(text, str) or not text: continue key = (kind, content_index) seen = self._seen_block_lengths.get(key, 0) if seen < len(text): self.emit_delta(kind, text[seen:], content_index) def emit_redacted_thinking_notice(self, message: dict[str, Any]) -> None: content = message.get("content") if not isinstance(content, list): return has_redacted = any(isinstance(block, dict) and block.get("type") == "redactedThinking" for block in content) if not has_redacted: return with _PRINT_LOCK: if self._open_kind is not None: sys.stderr.write("\n") self._open_kind = None sys.stderr.write(f"{self._prefix('thinking')}[redacted by provider]\n") sys.stderr.flush() def resolve_repo_omp_bin() -> str | None: cli_path = REPO_ROOT / "packages/coding-agent" / "src/cli.ts" if not cli_path.exists(): return None return str(cli_path) def resolve_omp_bin(raw: str | None) -> str: if raw: return raw repo_bin = resolve_repo_omp_bin() if repo_bin: return repo_bin found = shutil.which("omp") if not found: raise SystemExit("Could not find `omp` on PATH and could not resolve the repo CLI. Set --omp-bin or OMP_BIN.") return found def build_retry_prompt(spec: BenchmarkSpec, current_content: str) -> str: return ( "Previous attempt did not produce the expected result. " "The file has been reset to its original state — start fresh.\n\n" f"Current content:\n```\n{current_content}```\n\n" f"Expected:\n```\n{EXPECTED_CONTENT}```\n\n" f"{spec.retry_instruction}" ) def install_verbose_logging( client: RpcClient, model: str, mode: str | None, thinking: str | None, ) -> Callable[[], None] | None: if mode is None: return None printer = VerbosePrinter(model) include_messages = mode == "verbose" if include_messages and thinking is None: with _PRINT_LOCK: sys.stderr.write( f"[{model.removeprefix('openrouter/')}] verbose> " "no thinking level requested; pass --thinking low|medium|high|xhigh|max if the provider exposes reasoning.\n" ) sys.stderr.flush() def handle_message_start(event: MessageStartEvent) -> None: if not include_messages: return if event.message.get("role") == "assistant": printer.reset_message() def handle_message_update(event: MessageUpdateEvent) -> None: if not include_messages: return if event.message.get("role") != "assistant": return message_event = event.assistant_message_event event_type = message_event["type"] if event_type == "text_delta": printer.emit_delta("text", message_event["delta"], message_event["contentIndex"]) elif event_type == "thinking_delta": printer.emit_delta("thinking", message_event["delta"], message_event["contentIndex"]) def handle_message_end(event: MessageEndEvent) -> None: if not include_messages: return if event.message.get("role") == "assistant": printer.emit_missing_from_message(event.message) printer.emit_redacted_thinking_notice(event.message) printer.flush() printer.reset_message() def handle_tool_start(event: ToolExecutionStartEvent) -> None: printer.emit_tool_call(event.tool_name, event.args) removers = [ client.on_message_start(handle_message_start), client.on_message_update(handle_message_update), client.on_message_end(handle_message_end), client.on_tool_execution_start(handle_tool_start), ] def cleanup() -> None: for remove in reversed(removers): remove() printer.flush() return cleanup def run_benchmark_for_model( *, spec: BenchmarkSpec, model: str, omp_bin: str, workspace: Path, timeout: float, log_mode: str | None, thinking: str | None, max_turns: int, ) -> BenchmarkResult: """Run a single edit benchmark for one model.""" test_file = workspace / "test.rs" test_file.write_text(INITIAL_CONTENT) prompt_attempts = 0 token_input = 0 token_output = 0 turns_used = 0 edit_tool_calls = 0 success = False feedback = "" error_msg: str | None = None counting_edit_turns = True try: with RpcClient( executable=omp_bin, model=model, cwd=workspace, env={**spec.env}, thinking=thinking, tools=spec.tools, no_skills=True, no_rules=True, no_session=True, startup_timeout=30.0, request_timeout=120.0, ) as client: client.install_headless_ui() verbose_cleanup = install_verbose_logging(client, model, log_mode, thinking) def handle_tool_count(event: ToolExecutionStartEvent) -> None: nonlocal edit_tool_calls, turns_used if counting_edit_turns: turns_used += 1 if event.tool_name == "edit": edit_tool_calls += 1 tool_count_remover = client.on_tool_execution_start(handle_tool_count) try: for turn in range(1, max_turns + 1): prompt_attempts = turn if turn == 1: client.prompt(spec.initial_prompt) else: # Reset file to initial state on retry so the model starts fresh # instead of trying to fix a potentially corrupted file. test_file.write_text(INITIAL_CONTENT) client.prompt(build_retry_prompt(spec, INITIAL_CONTENT)) try: client.wait_for_idle(timeout=timeout) except Exception: # Prompt timed out or errored — abort the agent then retry. try: client.abort() time.sleep(1) client.wait_for_idle(timeout=10) except Exception: pass continue current_content = test_file.read_text() if current_content.strip() == EXPECTED_CONTENT.strip(): success = True break stats = client.get_session_stats() token_input = stats.tokens.input token_output = stats.tokens.output counting_edit_turns = False client.prompt(FEEDBACK_PROMPT) client.wait_for_idle(timeout=timeout) feedback = client.get_last_assistant_text() or "" stats = client.get_session_stats() token_input = stats.tokens.input token_output = stats.tokens.output finally: tool_count_remover() if verbose_cleanup is not None: verbose_cleanup() except Exception as exc: error_msg = f"{type(exc).__name__}: {exc}" return BenchmarkResult( model=model, success=success, turns_used=turns_used, prompt_attempts=prompt_attempts, edit_calls=edit_tool_calls, token_input=token_input, token_output=token_output, feedback=feedback.strip(), error=error_msg, ) async def run_all(spec: BenchmarkSpec, args: argparse.Namespace) -> dict[str, dict[str, Any]]: omp_bin = resolve_omp_bin(args.omp_bin) timestamp = time.strftime("%Y%m%d-%H%M%S") workspace_root = Path(tempfile.gettempdir()) / f"{spec.workspace_prefix}-{timestamp}" workspace_root.mkdir(parents=True, exist_ok=True) selected_models = args.models or MODELS tasks = [] for model in selected_models: model_slug = model.replace("/", "_") workspace = workspace_root / model_slug workspace.mkdir(parents=True, exist_ok=True) print(f"Starting benchmark for {model}...", file=sys.stderr) tasks.append( asyncio.to_thread( run_benchmark_for_model, spec=spec, model=model, omp_bin=omp_bin, workspace=workspace, timeout=args.timeout, log_mode="verbose" if args.verbose else ("print" if args.print else None), thinking=args.thinking, max_turns=args.max_turns, ) ) benchmark_results = await asyncio.gather(*tasks, return_exceptions=True) results: dict[str, dict[str, Any]] = {} for model, result in zip(selected_models, benchmark_results): if isinstance(result, Exception): results[model] = { "tokens_in": 0, "tokens_out": 0, "model_feedback": "", "success": False, "turns_used": 0, "prompt_attempts": 0, "edit_calls": 0, "error": f"{type(result).__name__}: {result}", } print(f" {model}: error - {result}", file=sys.stderr) continue results[model] = { "tokens_in": result.token_input, "tokens_out": result.token_output, "model_feedback": result.feedback, "success": result.success, "turns_used": result.turns_used, "edit_calls": result.edit_calls, "prompt_attempts": result.prompt_attempts, "error": result.error, } status = "success" if result.success else "failed" print(f" {model}: {status} in {result.turns_used} turns", file=sys.stderr) return results def parse_args(description: str) -> argparse.Namespace: parser = argparse.ArgumentParser(description=description) parser.add_argument( "--omp-bin", default=os.environ.get("OMP_BIN"), help="Executable to launch. Defaults to the repo checkout CLI, then falls back to `omp` on PATH.", ) parser.add_argument( "--timeout", type=float, default=60.0, help="Per-turn timeout in seconds." ) parser.add_argument( "--max-turns", type=int, default=DEFAULT_MAX_TURNS, help=f"Maximum edit/retry turns before the benchmark gives up (default: {DEFAULT_MAX_TURNS}).", ) parser.add_argument( "--model", dest="models", action="append", help="Repeat to limit execution to specific models.", ) logging_group = parser.add_mutually_exclusive_group() logging_group.add_argument( "--print", action="store_true", help="Print tool calls to stderr while the benchmark runs.", ) logging_group.add_argument( "--verbose", action="store_true", help="Print assistant text, thinking, and tool calls to stderr while the benchmark runs.", ) parser.add_argument( "--thinking", choices=["off", "minimal", "low", "medium", "high", "xhigh", "max"], default="medium", help="Request a specific thinking level for models that support reasoning (default: medium).", ) return parser.parse_args() def run_benchmark_main(spec: BenchmarkSpec) -> int: args = parse_args(spec.description) results = asyncio.run(run_all(spec, args)) print(json.dumps(results, indent=2)) return 0