Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

612 lines
19 KiB
JavaScript

#!/usr/bin/env node
/**
* guided_decode_token_bench.mjs — Eliza-1 guided-structured-decode token-savings
* benchmark.
*
* Measures how many decode tokens the *eliza harness schema* removes from a
* structured generation. When the agent's job is to emit a structured response
* (action selection / tool call / typed object), the lazy GBNF forces the JSON
* scaffold and the closed enum set, and the *prefill plan* lets the server
* splice the deterministically-implied byte runs as token ids *without a
* forward pass* (see
* `packages/app-core/src/services/local-inference/structured-output.ts`
* `ElizaPrefillPlan` / `reports/porting/2026-05-11/guided-structured-decoding.md`).
*
* Two modes:
*
* - **static** (default — always runs, no model needed): for each
* representative skeleton it reports the bytes/estimated-tokens of the
* deterministic runs (the prefill plan's forced spans), i.e. the tokens the
* model never has to generate, vs. the total envelope size. The token
* estimate uses ~3.6 bytes/token (a llama-family BPE average for JSON-ish
* text); pass `--bpt N` to override. This is the "guaranteed" floor of the
* saving — independent of which words the model picks for the free spans.
*
* - **live** (`--bin PATH` to a fork `llama-server`, `--model PATH` to a
* GGUF): runs each prompt twice — once unguided, once with the harness
* schema (grammar + prefill plan) on the request body — and reports the
* measured `completion_tokens` and wall-time delta. (Today's fork ignores
* `eliza_prefill_plan` and only honours the grammar, so the live delta is
* the *grammar-only* saving — the scaffold tokens are still generated but
* constrained; the prefill-plan saving lands when a fork build consumes the
* field. The static mode reports that future floor.) When no binary / model
* is available it writes `status: "skipped"` and exits 0 — it does NOT
* fabricate numbers (AGENTS.md §3 / §8).
*
* Usage:
* node packages/inference/verify/guided_decode_token_bench.mjs \
* [--bin PATH --model PATH] [--bpt 3.6] [--report PATH] [--json]
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function parseArgs(argv) {
const out = { bin: null, model: null, bpt: 3.6, report: null, json: false };
for (let i = 2; i < argv.length; i += 1) {
const a = argv[i];
if (a === "--bin") out.bin = argv[++i];
else if (a === "--model") out.model = argv[++i];
else if (a === "--bpt") out.bpt = Number(argv[++i]);
else if (a === "--report") out.report = argv[++i];
else if (a === "--json") out.json = true;
}
return out;
}
// ---------------------------------------------------------------------------
// Representative eliza-harness skeletons (the structured outputs the agent loop
// actually emits). Built the same way `buildResponseGrammar` /
// `buildPlannerActionGrammar` do, inlined here so the bench has no app-core
// import (it runs from `packages/inference/`).
// ---------------------------------------------------------------------------
/** Build a planner action-selection skeleton over a closed action-id set. */
function plannerSkeleton(actionIds) {
return {
id: `planner#${actionIds.join(",")}`,
spans: [
{ kind: "literal", value: '{"action":' },
actionIds.length === 1
? {
kind: "literal",
key: "action",
value: JSON.stringify(actionIds[0]),
}
: { kind: "enum", key: "action", enumValues: actionIds },
{ kind: "literal", value: ',"parameters":' },
{ kind: "free-json", key: "parameters" },
{ kind: "literal", value: ',"thought":' },
{ kind: "free-string", key: "thought" },
{ kind: "literal", value: "}" },
],
};
}
/** Build the Stage-1 response-handler envelope skeleton (non-direct channel). */
function stage1Skeleton() {
return {
id: "stage1#full",
spans: [
{ kind: "literal", value: '{"shouldRespond":' },
{
kind: "enum",
key: "shouldRespond",
enumValues: ["RESPOND", "IGNORE", "STOP"],
},
{ kind: "literal", value: ',"thought":' },
{ kind: "free-string", key: "thought" },
{ kind: "literal", value: ',"replyText":' },
{ kind: "free-string", key: "replyText" },
{ kind: "literal", value: ',"contexts":' },
{ kind: "free-json", key: "contexts" },
{ kind: "literal", value: ',"requiresTool":' },
{ kind: "free-json", key: "requiresTool" },
{ kind: "literal", value: ',"extract":' },
{ kind: "free-json", key: "extract" },
{ kind: "literal", value: "}" },
],
};
}
/** A typed-field object (e.g. a SETTINGS action's parameters). */
function typedFieldsSkeleton() {
return {
id: "settings#params",
spans: [
{ kind: "literal", value: '{"action":"' },
{ kind: "enum", key: "action", enumValues: ["set", "get", "list"] },
{ kind: "literal", value: '","provider":"' },
{
kind: "enum",
key: "provider",
enumValues: ["openai", "anthropic", "local"],
},
{ kind: "literal", value: '","capability":"' },
{ kind: "free-string", key: "capability" },
{ kind: "literal", value: '","enabled":' },
{ kind: "free-json", key: "enabled" },
{ kind: "literal", value: "}" },
],
};
}
/** Single-action turn (every scaffold byte AND the action id are forced). */
function singleActionSkeleton() {
return plannerSkeleton(["IGNORE"]);
}
const SKELETONS = [
{
name: "planner-action-select (8 actions)",
skeleton: plannerSkeleton([
"REPLY",
"SEND_MESSAGE",
"IGNORE",
"MUTE_ROOM",
"FOLLOW_ROOM",
"UNFOLLOW_ROOM",
"UPDATE_SETTINGS",
"GENERATE_IMAGE",
]),
},
{
name: "planner-action-select (single action)",
skeleton: singleActionSkeleton(),
},
{ name: "stage1-response-envelope", skeleton: stage1Skeleton() },
{ name: "typed-fields-object (SETTINGS)", skeleton: typedFieldsSkeleton() },
];
// ---------------------------------------------------------------------------
// collapseSkeleton + compilePrefillPlan — inlined mirror of structured-output.ts
// ---------------------------------------------------------------------------
function collapseSkeleton(skeleton) {
const out = [];
for (const span of skeleton.spans) {
if (
span.kind === "enum" &&
Array.isArray(span.enumValues) &&
span.enumValues.length <= 1
) {
out.push({
kind: "literal",
key: span.key,
value: span.enumValues[0] ?? span.value ?? "",
});
continue;
}
out.push(span);
}
return { spans: out, id: skeleton.id };
}
function compilePrefillPlan(skeletonInput) {
const skeleton = collapseSkeleton(skeletonInput);
const runs = [];
let freeCount = 0;
let pending = "";
const flush = (after) => {
if (pending.length === 0) return;
runs.push({ afterFreeSpan: after, text: pending });
pending = "";
};
for (const span of skeleton.spans) {
if (span.kind === "literal") {
pending += span.value ?? "";
continue;
}
if (
span.kind === "enum" &&
Array.isArray(span.enumValues) &&
span.enumValues.length === 1
) {
pending += JSON.stringify(String(span.enumValues[0]));
continue;
}
flush(freeCount - 1);
freeCount += 1;
}
flush(freeCount - 1);
if (runs.length === 0) return null;
return {
prefix: runs[0].afterFreeSpan === -1 ? runs[0].text : "",
runs,
freeCount,
id: skeleton.id,
};
}
/** Bytes the prefill plan forces (the deterministic runs the model never generates). */
function forcedBytes(plan) {
return plan
? plan.runs.reduce((n, r) => n + Buffer.byteLength(r.text, "utf8"), 0)
: 0;
}
/** A representative "free" content size in bytes for token estimates, per skeleton. */
function estimateFreeBytes(skeleton) {
// Rough: thought ~80 chars, replyText ~120, parameters ~40, contexts ~12,
// requiresTool ~5, extract ~20, a free-string field ~20, enum-with-N ~ name len.
let total = 0;
for (const span of skeleton.spans) {
if (span.kind === "literal") continue;
if (span.kind === "enum") {
const vals = span.enumValues ?? [];
if (vals.length <= 1) continue; // collapsed
// The model emits one of the (JSON-quoted) values — average length.
total += Math.round(
vals.reduce((n, v) => n + v.length + 2, 0) / vals.length,
);
continue;
}
const k = span.key ?? "";
if (k === "thought") total += 80;
else if (k === "replyText") total += 120;
else if (k === "parameters") total += 40;
else if (k === "contexts") total += 12;
else if (k === "extract") total += 20;
else total += 12;
}
return total;
}
// ---------------------------------------------------------------------------
// Static mode
// ---------------------------------------------------------------------------
function runStatic(bpt) {
const rows = SKELETONS.map(({ name, skeleton }) => {
const plan = compilePrefillPlan(skeleton);
const forced = forcedBytes(plan);
const free = estimateFreeBytes(skeleton);
const totalBytes = forced + free;
const forcedTokens = Math.round(forced / bpt);
const freeTokens = Math.round(free / bpt);
const totalTokens = forcedTokens + freeTokens;
return {
name,
freeSpans: plan?.freeCount ?? 0,
forcedBytes: forced,
forcedTokensEst: forcedTokens,
freeBytesEst: free,
freeTokensEst: freeTokens,
totalTokensEst: totalTokens,
tokenReductionPct:
totalTokens > 0 ? Math.round((forcedTokens / totalTokens) * 100) : 0,
prefixBytes: plan ? Buffer.byteLength(plan.prefix, "utf8") : 0,
};
});
const agg = {
forcedTokensEst: rows.reduce((n, r) => n + r.forcedTokensEst, 0),
totalTokensEst: rows.reduce((n, r) => n + r.totalTokensEst, 0),
};
agg.tokenReductionPct =
agg.totalTokensEst > 0
? Math.round((agg.forcedTokensEst / agg.totalTokensEst) * 100)
: 0;
return { mode: "static", bytesPerToken: bpt, rows, aggregate: agg };
}
// ---------------------------------------------------------------------------
// Live mode (optional)
// ---------------------------------------------------------------------------
const PROMPTS = [
"User: schedule a meeting tomorrow at 3pm with the design team.\nPick an action and parameters.",
"User: hey what's up?\nDecide whether to respond and draft a reply.",
"User: turn on the openai provider for image generation.\nFill in the settings object.",
];
async function fetchJson(url, body, timeoutMs) {
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), timeoutMs);
try {
const res = await fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
signal: ac.signal,
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} finally {
clearTimeout(t);
}
}
function waitHealth(baseUrl, timeoutMs) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve, reject) => {
const tick = async () => {
try {
const res = await fetch(`${baseUrl}/health`);
if (res.ok) return resolve();
} catch {}
if (Date.now() > deadline)
return reject(new Error("server health timeout"));
setTimeout(tick, 250);
};
tick();
});
}
async function runLive(opts) {
if (!fs.existsSync(opts.bin) || !fs.existsSync(opts.model)) {
return null;
}
const port = 18900 + (process.pid % 200);
const baseUrl = `http://127.0.0.1:${port}`;
const child = spawn(
opts.bin,
[
"-m",
opts.model,
"--host",
"127.0.0.1",
"--port",
String(port),
"-c",
"4096",
"-np",
"1",
"--no-warmup",
],
{ stdio: ["ignore", "ignore", "ignore"] },
);
try {
await waitHealth(baseUrl, 60_000);
const rows = [];
for (let i = 0; i < PROMPTS.length; i += 1) {
const prompt = PROMPTS[i];
const skeleton =
i === 1
? stage1Skeleton()
: i === 2
? typedFieldsSkeleton()
: plannerSkeleton([
"REPLY",
"SEND_MESSAGE",
"IGNORE",
"UPDATE_SETTINGS",
]);
const plan = compilePrefillPlan(skeleton);
const grammar = compileSkeletonToGbnfLike(skeleton);
const base = {
model: "x",
messages: [{ role: "user", content: prompt }],
max_tokens: 256,
temperature: 0.2,
cache_prompt: true,
};
const t0 = Date.now();
const r0 = await fetchJson(
`${baseUrl}/v1/chat/completions`,
base,
60_000,
);
const dtUnguided = Date.now() - t0;
const guidedBody = {
...base,
grammar: grammar.source,
...(grammar.lazy
? {
grammar_lazy: true,
grammar_triggers: grammar.triggers.map((v) => ({
type: "word",
value: v,
})),
}
: {}),
eliza_prefill_plan: plan
? {
prefix: plan.prefix,
runs: plan.runs.map((r) => ({
after_free_span: r.afterFreeSpan,
text: r.text,
})),
free_count: plan.freeCount,
id: plan.id,
}
: undefined,
messages:
plan && plan.prefix
? [
{ role: "user", content: prompt },
{ role: "assistant", content: plan.prefix },
]
: base.messages,
continue_final_message: !!(plan && plan.prefix),
};
const t1 = Date.now();
const r1 = await fetchJson(
`${baseUrl}/v1/chat/completions`,
guidedBody,
60_000,
);
const dtGuided = Date.now() - t1;
rows.push({
prompt: prompt.slice(0, 40) + "…",
unguided: {
completionTokens: r0?.usage?.completion_tokens ?? null,
wallMs: dtUnguided,
},
guided: {
completionTokens: r1?.usage?.completion_tokens ?? null,
wallMs: dtGuided,
prefillPlanForcedBytes: forcedBytes(plan),
},
tokenDelta:
(r0?.usage?.completion_tokens ?? 0) -
(r1?.usage?.completion_tokens ?? 0),
wallDeltaMs: dtUnguided - dtGuided,
});
}
return { mode: "live", bin: opts.bin, model: opts.model, rows };
} finally {
child.kill("SIGKILL");
}
}
// Minimal GBNF compiler for the live mode (mirror of compileSkeletonToGbnf).
function gbnfEsc(s) {
let o = "";
for (const ch of s) {
const c = ch.codePointAt(0) ?? 0;
if (ch === "\\") o += "\\\\";
else if (ch === '"') o += '\\"';
else if (ch === "\n") o += "\\n";
else if (ch === "\r") o += "\\r";
else if (ch === "\t") o += "\\t";
else if (c < 0x20) o += `\\x${c.toString(16).padStart(2, "0")}`;
else o += ch;
}
return o;
}
function compileSkeletonToGbnfLike(skeletonInput) {
const skeleton = collapseSkeleton(skeletonInput);
const JSON_STRING = '"\\"" ( [^"\\\\] | "\\\\" . )* "\\""';
const JSON_VALUE = [
'jsonvalue ::= jsonobject | jsonarray | jsonstring | jsonnumber | "true" | "false" | "null"',
'jsonobject ::= "{" ws ( jsonstring ws ":" ws jsonvalue ( ws "," ws jsonstring ws ":" ws jsonvalue )* )? ws "}"',
'jsonarray ::= "[" ws ( jsonvalue ( ws "," ws jsonvalue )* )? ws "]"',
`jsonstring ::= ${JSON_STRING}`,
'jsonnumber ::= "-"? ( [0-9] | [1-9] [0-9]* ) ( "." [0-9]+ )? ( [eE] [-+]? [0-9]+ )?',
"ws ::= [ \\t\\n\\r]*",
].join("\n");
const rules = new Map();
const root = [];
let freeIdx = 0,
needsJson = false,
trigger = null;
skeleton.spans.forEach((span, i) => {
if (span.kind === "literal") {
const t = span.value ?? "";
if (i === 0 && t.length) trigger = t;
root.push(`"${gbnfEsc(t)}"`);
} else if (span.kind === "enum") {
const vals = span.enumValues ?? [];
const rn = `enum${freeIdx++}`;
rules.set(rn, vals.map((v) => `"${gbnfEsc(`"${v}"`)}"`).join(" | "));
root.push(rn);
} else if (span.kind === "free-string") {
const rn = `freestr${freeIdx++}`;
rules.set(rn, JSON_STRING);
root.push(rn);
} else {
needsJson = true;
root.push("jsonvalue");
}
});
const lines = [`root ::= ${root.join(" ")}`];
for (const [n, b] of rules) lines.push(`${n} ::= ${b}`);
if (needsJson) lines.push(JSON_VALUE);
return {
source: lines.join("\n"),
lazy: !!trigger,
triggers: trigger ? [trigger] : [],
};
}
// ---------------------------------------------------------------------------
async function main() {
const opts = parseArgs(process.argv);
const reportPath = opts.report
? path.resolve(opts.report)
: path.join(
__dirname,
"bench_results",
`guided_decode_${new Date().toISOString().slice(0, 10)}.json`,
);
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
const result = {
benchmark: "guided-structured-decode-token-savings",
generatedAt: new Date().toISOString(),
host: {
platform: process.platform,
arch: process.arch,
cpus: os.cpus().length,
},
static: runStatic(opts.bpt),
live: null,
};
if (opts.bin && opts.model) {
try {
result.live = await runLive(opts);
} catch (err) {
result.live = {
mode: "live",
status: "failed",
reason: String(err && err.message ? err.message : err),
};
}
if (result.live === null) {
result.live = {
mode: "live",
status: "skipped",
reason: "binary or model not found",
};
}
} else {
result.live = {
mode: "live",
status: "skipped",
reason: "pass --bin PATH --model PATH for a live run",
};
}
fs.writeFileSync(reportPath, JSON.stringify(result, null, 2));
if (opts.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(
"[guided-decode-bench] static token savings (estimated, bpt=" +
opts.bpt +
"):",
);
for (const r of result.static.rows) {
console.log(
` ${r.name.padEnd(40)} free-spans=${String(r.freeSpans).padStart(2)} ` +
`forced≈${String(r.forcedTokensEst).padStart(3)}tok free≈${String(r.freeTokensEst).padStart(3)}tok ` +
`→ ${String(r.tokenReductionPct).padStart(2)}% fewer generated`,
);
}
console.log(
` AGGREGATE: ${result.static.aggregate.forcedTokensEst}/${result.static.aggregate.totalTokensEst} tokens forced ≈ ${result.static.aggregate.tokenReductionPct}% reduction`,
);
if (
result.live &&
result.live.mode === "live" &&
Array.isArray(result.live.rows)
) {
console.log(
"[guided-decode-bench] live (grammar-only today; prefill-plan when the fork consumes it):",
);
for (const r of result.live.rows) {
console.log(
` ${r.prompt.padEnd(42)} Δtokens=${r.tokenDelta} Δwall=${r.wallDeltaMs}ms`,
);
}
} else {
console.log(
`[guided-decode-bench] live: ${result.live?.reason ?? "skipped"}`,
);
}
console.log(`[guided-decode-bench] report → ${reportPath}`);
}
}
main().catch((err) => {
console.error("[guided-decode-bench] error:", err);
process.exit(1);
});