// Issue 78851 Model Resolution script supports OpenClaw repository automation. import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import * as inspector from "node:inspector"; import { tmpdir } from "node:os"; import path from "node:path"; import { monitorEventLoopDelay, performance } from "node:perf_hooks"; import { resolveModelAsync } from "../../src/agents/embedded-agent-runner/model.js"; import { ensureOpenClawModelsJson, resetModelsJsonReadyCacheForTest, } from "../../src/agents/models-config.js"; import type { OpenClawConfig } from "../../src/config/types.openclaw.js"; import { Issue78851CliArgumentError, issue78851ModelResolutionHelpRequested, issue78851ModelResolutionUsage, parseIssue78851ModelResolutionOptions, type Issue78851ModelResolutionOptions as Options, } from "./issue-78851-model-resolution-cli.js"; type PhaseSample = { ensureMs: number; resolveMs: number; totalMs: number; wrote: boolean; }; type RunSample = { cold: PhaseSample; eventLoopDelayMaxMs: number; eventLoopDelayMeanMs: number; index: number; rssMb: number; warm: PhaseSample; }; type SummaryStats = { avg: number; max: number; min: number; p50: number; p95: number; }; type Report = { scenario: string; options: Omit; samples: RunSample[]; summary: { coldEnsureMs: SummaryStats; coldResolveMs: SummaryStats; coldTotalMs: SummaryStats; warmEnsureMs: SummaryStats; warmResolveMs: SummaryStats; warmTotalMs: SummaryStats; eventLoopDelayMaxMs: SummaryStats; rssMb: SummaryStats; }; tempRoot: string; cpuProfilePath?: string; }; function round(value: number): number { return Math.round(value * 100) / 100; } function percentile(values: number[], p: number): number { if (values.length === 0) { return 0; } const sorted = values.toSorted((a, b) => a - b); const index = Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * p)); return round(sorted[index] ?? 0); } function stats(values: number[]): SummaryStats { if (values.length === 0) { return { avg: 0, max: 0, min: 0, p50: 0, p95: 0 }; } const total = values.reduce((sum, value) => sum + value, 0); return { avg: round(total / values.length), max: round(Math.max(...values)), min: round(Math.min(...values)), p50: percentile(values, 0.5), p95: percentile(values, 0.95), }; } function modelRef(providerIndex: number, modelIndex: number): string { return `perf-${providerIndex}/perf-model-${modelIndex}`; } function buildConfig(options: Options, workspaceDir: string): OpenClawConfig { const providers: NonNullable["providers"]> = {}; for (let providerIndex = 0; providerIndex < options.providers; providerIndex += 1) { providers[`perf-${providerIndex}`] = { api: providerIndex % 2 === 0 ? "openai-responses" : "openai-completions", apiKey: "perf-key", baseUrl: `http://127.0.0.1:${20_000 + providerIndex}/v1`, models: Array.from({ length: options.modelsPerProvider }, (_, modelIndex) => ({ api: modelIndex % 2 === 0 ? "openai-responses" : "openai-completions", baseUrl: `http://127.0.0.1:${20_000 + providerIndex}/v1`, contextWindow: 128_000 + modelIndex, cost: { cacheRead: 0, cacheWrite: 0, input: 0, output: 0 }, id: `perf-model-${modelIndex}`, input: modelIndex % 5 === 0 ? ["text", "image"] : ["text"], maxTokens: 8192, name: `Perf Model ${providerIndex}.${modelIndex}`, params: { cacheRetention: modelIndex % 3 === 0 ? "ephemeral" : undefined, syntheticRank: providerIndex * options.modelsPerProvider + modelIndex, }, reasoning: modelIndex % 3 === 0, })), params: { syntheticProviderRank: providerIndex, }, }; } const fallbacks = Array.from({ length: Math.min(12, options.providers) }, (_, index) => modelRef(index, index % options.modelsPerProvider), ); return { browser: { enabled: false }, agents: { defaults: { contextInjection: "never", model: { primary: modelRef(0, 0), fallbacks, }, skipBootstrap: true, workspace: workspaceDir, }, list: Array.from({ length: options.agentCount }, (_, index) => ({ default: index === 0, id: `agent-${index}`, model: { primary: modelRef(index % options.providers, index % options.modelsPerProvider), fallbacks: fallbacks.toReversed(), }, workspace: path.join(workspaceDir, `agent-${index}`), })), }, gateway: { auth: { mode: "none" }, bind: "loopback", controlUi: { enabled: false }, mode: "local", }, models: { mode: "replace", providers, }, plugins: { enabled: true, entries: { browser: { enabled: false }, }, }, }; } async function startCpuProfile(params: { dir?: string; output?: string }): Promise<{ stop: () => Promise; }> { const fallbackDir = ".artifacts/perf/issue-78851/cpu"; const cpuProfDir = params.dir ?? path.dirname(params.output ?? fallbackDir); await mkdir(cpuProfDir, { recursive: true }); const session = new inspector.Session(); session.connect(); const post = (method: string, paramsLocal?: Record) => new Promise((resolve, reject) => { session.post(method, paramsLocal ?? {}, (error, result) => { if (error) { reject(error); } else { resolve(result as T); } }); }); await post("Profiler.enable"); await post("Profiler.start"); return { async stop() { const result = await post<{ profile: unknown }>("Profiler.stop"); session.disconnect(); const profilePath = params.output ?? path.join(cpuProfDir, `issue-78851-${process.pid}-${Date.now()}.cpuprofile`); await mkdir(path.dirname(profilePath), { recursive: true }); await writeFile(profilePath, JSON.stringify(result.profile)); return profilePath; }, }; } async function measurePhase(params: { agentDir: string; config: OpenClawConfig; lookups: number; modelIndexOffset: number; providerCount: number; modelsPerProvider: number; workspaceDir: string; runtimeHooks: boolean; }): Promise { const started = performance.now(); const ensureStarted = performance.now(); const ensureResult = await ensureOpenClawModelsJson(params.config, params.agentDir, { // Keep this harness deterministic by measuring configured-model scale. // Live provider catalog timing belongs in a separate Crabbox lane with secrets. providerDiscoveryProviderIds: [], providerDiscoveryTimeoutMs: 5_000, workspaceDir: params.workspaceDir, }); const ensureMs = performance.now() - ensureStarted; const resolveStarted = performance.now(); for (let lookupIndex = 0; lookupIndex < params.lookups; lookupIndex += 1) { const providerIndex = lookupIndex % params.providerCount; const modelIndex = (lookupIndex + params.modelIndexOffset) % params.modelsPerProvider; const resolved = await resolveModelAsync( `perf-${providerIndex}`, `perf-model-${modelIndex}`, params.agentDir, params.config, { skipProviderRuntimeHooks: !params.runtimeHooks, workspaceDir: params.workspaceDir, }, ); if (!resolved.model) { throw new Error(resolved.error ?? `failed to resolve ${modelRef(providerIndex, modelIndex)}`); } } const resolveMs = performance.now() - resolveStarted; return { ensureMs: round(ensureMs), resolveMs: round(resolveMs), totalMs: round(performance.now() - started), wrote: ensureResult.wrote, }; } async function runOne(params: { config: OpenClawConfig; index: number; options: Options; tempRoot: string; workspaceDir: string; }): Promise { const agentDir = path.join(params.tempRoot, `agent-state-${params.index}`); await mkdir(agentDir, { recursive: true }); resetModelsJsonReadyCacheForTest(); const histogram = monitorEventLoopDelay({ resolution: 10 }); histogram.enable(); const cold = await measurePhase({ agentDir, config: params.config, lookups: params.options.lookupsPerRun, modelIndexOffset: params.index, modelsPerProvider: params.options.modelsPerProvider, providerCount: params.options.providers, workspaceDir: params.workspaceDir, runtimeHooks: params.options.runtimeHooks, }); const warm = await measurePhase({ agentDir, config: params.config, lookups: params.options.lookupsPerRun, modelIndexOffset: params.index + 1, modelsPerProvider: params.options.modelsPerProvider, providerCount: params.options.providers, workspaceDir: params.workspaceDir, runtimeHooks: params.options.runtimeHooks, }); histogram.disable(); return { cold, eventLoopDelayMaxMs: round(histogram.max / 1_000_000), eventLoopDelayMeanMs: round(histogram.mean / 1_000_000), index: params.index, rssMb: round(process.memoryUsage().rss / 1024 / 1024), warm, }; } function summarize(samples: RunSample[]): Report["summary"] { return { coldEnsureMs: stats(samples.map((sample) => sample.cold.ensureMs)), coldResolveMs: stats(samples.map((sample) => sample.cold.resolveMs)), coldTotalMs: stats(samples.map((sample) => sample.cold.totalMs)), eventLoopDelayMaxMs: stats(samples.map((sample) => sample.eventLoopDelayMaxMs)), rssMb: stats(samples.map((sample) => sample.rssMb)), warmEnsureMs: stats(samples.map((sample) => sample.warm.ensureMs)), warmResolveMs: stats(samples.map((sample) => sample.warm.resolveMs)), warmTotalMs: stats(samples.map((sample) => sample.warm.totalMs)), }; } function printHuman(report: Report, cpuProfilePath?: string): void { const lines = [ `scenario: ${report.scenario}`, `providers: ${report.options.providers}`, `modelsPerProvider: ${report.options.modelsPerProvider}`, `agents: ${report.options.agentCount}`, `lookups: ${report.options.lookupsPerRun}`, `runs: ${report.options.runs}`, `runtimeHooks: ${report.options.runtimeHooks}`, `coldTotalMs: avg=${report.summary.coldTotalMs.avg} p50=${report.summary.coldTotalMs.p50} p95=${report.summary.coldTotalMs.p95} max=${report.summary.coldTotalMs.max}`, `coldEnsureMs: avg=${report.summary.coldEnsureMs.avg} p50=${report.summary.coldEnsureMs.p50} p95=${report.summary.coldEnsureMs.p95} max=${report.summary.coldEnsureMs.max}`, `coldResolveMs: avg=${report.summary.coldResolveMs.avg} p50=${report.summary.coldResolveMs.p50} p95=${report.summary.coldResolveMs.p95} max=${report.summary.coldResolveMs.max}`, `warmTotalMs: avg=${report.summary.warmTotalMs.avg} p50=${report.summary.warmTotalMs.p50} p95=${report.summary.warmTotalMs.p95} max=${report.summary.warmTotalMs.max}`, `warmEnsureMs: avg=${report.summary.warmEnsureMs.avg} p50=${report.summary.warmEnsureMs.p50} p95=${report.summary.warmEnsureMs.p95} max=${report.summary.warmEnsureMs.max}`, `warmResolveMs: avg=${report.summary.warmResolveMs.avg} p50=${report.summary.warmResolveMs.p50} p95=${report.summary.warmResolveMs.p95} max=${report.summary.warmResolveMs.max}`, `eventLoopDelayMaxMs: avg=${report.summary.eventLoopDelayMaxMs.avg} max=${report.summary.eventLoopDelayMaxMs.max}`, `rssMb: avg=${report.summary.rssMb.avg} max=${report.summary.rssMb.max}`, ]; if (report.options.output) { lines.push(`output: ${report.options.output}`); } if (report.cpuProfilePath ?? cpuProfilePath) { lines.push(`cpuProfile: ${report.cpuProfilePath ?? cpuProfilePath}`); } process.stdout.write(`${lines.join("\n")}\n`); } async function main(): Promise { const args = process.argv.slice(2); const options = parseIssue78851ModelResolutionOptions(args); if (issue78851ModelResolutionHelpRequested(args)) { process.stdout.write(issue78851ModelResolutionUsage()); return; } const tempRoot = await mkdtemp(path.join(tmpdir(), "openclaw-issue-78851-")); const workspaceDir = path.join(tempRoot, "workspace"); await mkdir(workspaceDir, { recursive: true }); const config = buildConfig(options, workspaceDir); let profiler: Awaited> | undefined; let cpuProfilePath: string | undefined; try { if (options.cpuProfDir ?? options.cpuProfOutput) { profiler = await startCpuProfile({ dir: options.cpuProfDir, output: options.cpuProfOutput, }); } for (let index = 0; index < options.warmup; index += 1) { await runOne({ config, index: -index - 1, options, tempRoot, workspaceDir }); } const samples: RunSample[] = []; for (let index = 0; index < options.runs; index += 1) { samples.push(await runOne({ config, index, options, tempRoot, workspaceDir })); } if (profiler) { cpuProfilePath = await profiler.stop(); profiler = undefined; } const report: Report = { options: { agentCount: options.agentCount, cpuProfDir: options.cpuProfDir, cpuProfOutput: options.cpuProfOutput, lookupsPerRun: options.lookupsPerRun, modelsPerProvider: options.modelsPerProvider, output: options.output, providers: options.providers, runs: options.runs, runtimeHooks: options.runtimeHooks, warmup: options.warmup, }, samples, scenario: "issue-78851-model-resolution", summary: summarize(samples), tempRoot, ...(cpuProfilePath ? { cpuProfilePath } : {}), }; if (options.output) { await mkdir(path.dirname(options.output), { recursive: true }); await writeFile(options.output, `${JSON.stringify(report, null, 2)}\n`); } if (options.json) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); } else { printHuman(report, cpuProfilePath); } } finally { if (profiler) { await profiler.stop().catch(() => undefined); } if (!options.keepTemp) { await rm(tempRoot, { recursive: true, force: true }); } } } main().catch((error: unknown) => { if (error instanceof Issue78851CliArgumentError) { process.stderr.write(`${error.message}\n`); process.exit(1); } const message = error instanceof Error ? (error.stack ?? error.message) : String(error); process.stderr.write(`${message}\n`); process.exit(1); });