项目文件夹

文件
wehub-resource-sync 23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:19 +08:00

390 行
17 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
#!/usr/bin/env node
/**
* benchmark-models.mjs — Real measured benchmark of cheap-tier model
* alternatives via OpenRouter (ADR-148 phase 2 follow-up).
*
* What this measures, per model, on the machine it runs on:
* - latency: per-query mean, p50, p95 (wall-clock, includes network)
* - quality: pass rate against a hand-crafted pattern check
* - cost: USD per query from OpenRouter's `usage` field (tokens × price)
*
* The test corpus is hand-crafted to be representative of cheap-tier work
* (single-file structural edits, naming, adds/removes) — the kind of task
* that #2334's hybrid router should be confident on. Each row has a
* `check` regex that the model's response must contain to count as a pass.
*
* USAGE
* # Dry run — print what would be called + cost estimate, no API calls
* node scripts/benchmark-models.mjs
*
* # Live run — REAL OpenRouter API calls, spends real money
* OPENROUTER_API_KEY=sk-or-... node scripts/benchmark-models.mjs --live
*
* # Custom model list
* node scripts/benchmark-models.mjs --live --models google/gemini-flash-1.5,openai/gpt-4o-mini
*
* # Custom max-cost cap (default $0.50 — refuses to run if estimate exceeds)
* node scripts/benchmark-models.mjs --live --max-cost 1.00
*
* OUTPUT: markdown to stdout + JSON after `===BENCH_JSON===`. Writes a
* timestamped copy under docs/benchmarks/runs/cheap-models-*.{txt,json}.
*
* Co-Authored-By: RuFlo <ruv@ruv.net>
*/
import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolvePath(__dirname, '..');
// ============================================================================
// Models under test (cheap-tier focus)
// ============================================================================
/**
* Default cheap-tier candidate list. Prices are quoted public list prices in
* USD per million tokens (input / output) as of 2026-06-15 — re-verify
* before relying on them for cost projections.
*/
const DEFAULT_MODELS = [
// Anthropic baseline
{ id: 'anthropic/claude-haiku-4.5', in_per_m: 1.00, out_per_m: 5.00, family: 'anthropic' },
// Google (current OpenRouter slugs as of 2026-06-15)
{ id: 'google/gemini-2.5-flash-lite', in_per_m: 0.10, out_per_m: 0.40, family: 'google' },
{ id: 'google/gemini-2.5-flash', in_per_m: 0.30, out_per_m: 2.50, family: 'google' },
// OpenAI
{ id: 'openai/gpt-4o-mini', in_per_m: 0.15, out_per_m: 0.60, family: 'openai' },
// Meta
{ id: 'meta-llama/llama-3.3-70b-instruct', in_per_m: 0.13, out_per_m: 0.40, family: 'meta' },
{ id: 'meta-llama/llama-3.1-8b-instruct', in_per_m: 0.02, out_per_m: 0.03, family: 'meta' },
// Mistral
{ id: 'mistralai/ministral-3b-2512', in_per_m: 0.10, out_per_m: 0.10, family: 'mistral' },
// Qwen
{ id: 'qwen/qwen-2.5-7b-instruct', in_per_m: 0.05, out_per_m: 0.10, family: 'qwen' },
// InclusionAI — extreme cheap
{ id: 'inclusionai/ling-2.6-flash', in_per_m: 0.01, out_per_m: 0.03, family: 'inclusionai' },
// NVIDIA — free Nemotron tier (cost=$0 but rate-limited; useful as a fallback / budget tier)
{ id: 'nvidia/nemotron-nano-9b-v2:free', in_per_m: 0.00, out_per_m: 0.00, family: 'nvidia' },
{ id: 'nvidia/nemotron-3-super-120b-a12b:free', in_per_m: 0.00, out_per_m: 0.00, family: 'nvidia' },
];
// ============================================================================
// Hand-crafted cheap-tier test corpus
// ============================================================================
const CORPUS = [
{
id: 'rename-1',
task: 'Rename the variable `count` to `total` in this code. Return ONLY the corrected JavaScript, no explanation:\n\nlet count = 0;\nfor (const x of items) { count += x; }\nreturn count;',
check: /\btotal\b[^]*\bcount\b\s*\+=|\btotal\s*\+=/, // total appears + assignment uses it
bannedCheck: /count/, // and the old name shouldn't dominate
},
{
id: 'console-log-1',
task: 'Add a console.log("debug:", value) on the line before the return. Return ONLY the JavaScript, no explanation:\n\nfunction f(value) {\n return value * 2;\n}',
check: /console\.log\(\s*['"`]debug:?['"`]\s*,\s*value\s*\)/,
},
{
id: 'var-to-const-1',
task: 'Convert this `var` declaration to `const`. Return ONLY the JavaScript, no explanation:\n\nvar name = "alice";',
check: /^\s*const\s+name\s*=\s*['"`]alice['"`]\s*;?\s*$/m,
},
{
id: 'add-types-1',
task: 'Add TypeScript type annotations to the parameter and return value. The function adds two numbers. Return ONLY the corrected TS, no explanation:\n\nfunction add(a, b) { return a + b; }',
check: /function\s+add\s*\(\s*a\s*:\s*number\s*,\s*b\s*:\s*number\s*\)\s*:\s*number/,
},
{
id: 'try-catch-1',
task: 'Wrap this in a try/catch that logs the error. Return ONLY the JavaScript, no explanation:\n\nconst data = JSON.parse(input);',
check: /try\s*\{[^]*JSON\.parse[^]*\}\s*catch/,
},
{
id: 'typo-fix-1',
task: 'Fix the spelling in the comment. Return ONLY the JavaScript, no explanation:\n\n// Recieves data from the server\nfunction handle() {}',
check: /Receives/,
bannedCheck: /Recieves/,
},
{
id: 'remove-unused-1',
task: 'Remove the unused import `path`. Return ONLY the JavaScript, no explanation:\n\nimport { readFileSync } from "fs";\nimport path from "path";\n\nconsole.log(readFileSync("./x"));',
check: /^(?!.*import\s+path).*/s,
bannedCheck: /import\s+path/,
},
{
id: 'add-return-type-1',
task: 'Add the TypeScript return type annotation. The function returns a string. Return ONLY the TS, no explanation:\n\nfunction greet(name: string) { return `hello ${name}`; }',
check: /function\s+greet\s*\([^)]*\)\s*:\s*string/,
},
{
id: 'kebab-case-1',
task: 'Convert this camelCase variable name to kebab-case (as a string). Return ONLY the string in quotes, nothing else: myHelperFunction',
check: /"my-helper-function"|'my-helper-function'/,
},
{
id: 'increment-1',
task: 'Increment the counter variable by 1. Return ONLY the JavaScript, no explanation:\n\nlet counter = 0;',
check: /counter\s*\+\+|counter\s*\+=\s*1|counter\s*=\s*counter\s*\+\s*1/,
},
{
id: 'simple-json-1',
task: 'Return ONLY the JSON object {"status":"ok","code":200}, nothing else.',
check: /\{\s*"status"\s*:\s*"ok"\s*,\s*"code"\s*:\s*200\s*\}|\{\s*"code"\s*:\s*200\s*,\s*"status"\s*:\s*"ok"\s*\}/,
},
{
id: 'capitalize-1',
task: 'Capitalize the first letter of "hello world" and return ONLY the resulting string in quotes: ',
check: /"Hello world"|'Hello world'/,
},
{
id: 'arrow-fn-1',
task: 'Convert this function expression to an arrow function. Return ONLY the JavaScript, no explanation:\n\nconst double = function(n) { return n * 2; };',
check: /const\s+double\s*=\s*\(?n\)?\s*=>\s*n\s*\*\s*2/,
},
{
id: 'add-default-param-1',
task: 'Add a default value of 10 for parameter `n`. Return ONLY the JavaScript, no explanation:\n\nfunction times(n) { return n * 3; }',
check: /function\s+times\s*\(\s*n\s*=\s*10\s*\)/,
},
{
id: 'snake-to-camel-1',
task: 'Convert the snake_case name to camelCase as a string. Return ONLY the string in quotes, nothing else: get_user_profile',
check: /"getUserProfile"|'getUserProfile'/,
},
];
// ============================================================================
// CLI args
// ============================================================================
function parseArgs(argv) {
const a = { live: false, models: null, maxCost: 0.50, repeat: 1, maxTokens: 256, save: true };
for (let i = 2; i < argv.length; i++) {
const k = argv[i];
if (k === '--live') a.live = true;
else if (k === '--models') a.models = argv[++i].split(',').map(s => s.trim()).filter(Boolean);
else if (k === '--max-cost') a.maxCost = parseFloat(argv[++i]);
else if (k === '--repeat') a.repeat = parseInt(argv[++i], 10) || 1;
else if (k === '--max-tokens') a.maxTokens = parseInt(argv[++i], 10) || 256;
else if (k === '--no-save') a.save = false;
else if (k === '--help' || k === '-h') {
console.log('Usage: node scripts/benchmark-models.mjs [--live] [--models a,b,c] [--max-cost USD] [--repeat N] [--max-tokens N] [--no-save]');
process.exit(0);
}
}
return a;
}
const ARGS = parseArgs(process.argv);
const MODELS = ARGS.models
? DEFAULT_MODELS.filter(m => ARGS.models.includes(m.id)).concat(
ARGS.models.filter(id => !DEFAULT_MODELS.find(m => m.id === id))
.map(id => ({ id, in_per_m: 0, out_per_m: 0, family: 'unknown' }))
)
: DEFAULT_MODELS;
// ============================================================================
// Cost estimation (used in dry-run and as a refuse-to-run gate)
// ============================================================================
/** Rough projection: assume avg 80 input tokens + 60 output tokens per query. */
const AVG_IN_TOK = 80;
const AVG_OUT_TOK = 60;
function projectedCost() {
let total = 0;
for (const m of MODELS) {
const perQuery = (AVG_IN_TOK * m.in_per_m + AVG_OUT_TOK * m.out_per_m) / 1_000_000;
total += perQuery * CORPUS.length * ARGS.repeat;
}
return total;
}
// ============================================================================
// OpenRouter chat-completion call
// ============================================================================
async function callOpenRouter(modelId, userPrompt, apiKey) {
const t0 = performance.now();
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/ruvnet/ruflo',
'X-Title': 'ruflo-benchmark-models',
},
body: JSON.stringify({
model: modelId,
messages: [{ role: 'user', content: userPrompt }],
max_tokens: ARGS.maxTokens,
temperature: 0.0,
}),
});
const dt = performance.now() - t0;
const text = await res.text();
let body;
try { body = JSON.parse(text); } catch { body = { _raw: text }; }
if (!res.ok) {
return { ok: false, status: res.status, error: body?.error?.message ?? text.slice(0, 200), latencyMs: dt };
}
const content = body?.choices?.[0]?.message?.content ?? '';
const usage = body?.usage ?? {};
return {
ok: true,
content,
latencyMs: dt,
promptTokens: usage.prompt_tokens ?? 0,
completionTokens: usage.completion_tokens ?? 0,
totalTokens: usage.total_tokens ?? 0,
};
}
// ============================================================================
// Grading
// ============================================================================
function gradeResponse(row, content) {
if (!content || typeof content !== 'string') return { pass: false, reason: 'empty response' };
const checkPass = row.check.test(content);
const banned = row.bannedCheck ? row.bannedCheck.test(content) : false;
if (!checkPass) return { pass: false, reason: 'check regex did not match' };
if (banned) return { pass: false, reason: 'banned pattern present' };
return { pass: true };
}
// ============================================================================
// Main
// ============================================================================
async function main() {
const apiKey = process.env.OPENROUTER_API_KEY;
console.log('# Cheap-tier model benchmark (ADR-148 phase 2)\n');
console.log(`- ts: ${new Date().toISOString().slice(0, 19)}Z`);
console.log(`- node: ${process.version} platform: ${process.platform}-${process.arch}`);
console.log(`- corpus: ${CORPUS.length} queries × ${ARGS.repeat} repeat`);
console.log(`- models: ${MODELS.length} (${MODELS.map(m => m.id).join(', ')})`);
console.log(`- max-tokens per response: ${ARGS.maxTokens}`);
const projected = projectedCost();
console.log(`- projected total cost (rough): ~$${projected.toFixed(4)} USD (~$${(projected / MODELS.length).toFixed(4)}/model)`);
console.log(`- max-cost gate: $${ARGS.maxCost.toFixed(2)}`);
console.log(`- live mode: ${ARGS.live ? '**YES — real API calls**' : 'no (dry run)'}\n`);
if (!ARGS.live) {
console.log('Dry run — no API calls. To run for real:');
console.log(` OPENROUTER_API_KEY=sk-or-... node scripts/benchmark-models.mjs --live\n`);
console.log('===BENCH_JSON===');
console.log(JSON.stringify({ dryRun: true, projectedCostUSD: projected, models: MODELS.map(m => m.id), corpusSize: CORPUS.length }, null, 2));
return;
}
if (!apiKey) {
console.error('[bench] --live requires OPENROUTER_API_KEY in env.');
process.exit(2);
}
if (projected > ARGS.maxCost) {
console.error(`[bench] projected cost $${projected.toFixed(4)} exceeds --max-cost $${ARGS.maxCost.toFixed(2)}; refusing to run. Override with --max-cost.`);
process.exit(3);
}
// Per-model accumulator
const results = MODELS.map(m => ({
model: m.id, family: m.family,
latencies: [], passes: 0, total: 0, errors: [], usdCost: 0,
promptTokens: 0, completionTokens: 0,
}));
for (let r = 0; r < ARGS.repeat; r++) {
for (const row of CORPUS) {
// Per-row, parallel over models (small fan-out — OR rate limits allowing)
const tasks = MODELS.map((m, mi) => async () => {
try {
const resp = await callOpenRouter(m.id, row.task, apiKey);
const acc = results[mi];
acc.total++;
acc.latencies.push(resp.latencyMs);
if (!resp.ok) {
acc.errors.push({ row: row.id, status: resp.status, error: resp.error });
return;
}
const grade = gradeResponse(row, resp.content);
if (grade.pass) acc.passes++;
acc.promptTokens += resp.promptTokens;
acc.completionTokens += resp.completionTokens;
acc.usdCost += (resp.promptTokens * m.in_per_m + resp.completionTokens * m.out_per_m) / 1_000_000;
} catch (e) {
results[mi].total++;
results[mi].errors.push({ row: row.id, error: e instanceof Error ? e.message : String(e) });
}
});
await Promise.all(tasks.map(t => t()));
}
}
// Aggregate + print
const rows = results.map(r => {
const sorted = r.latencies.slice().sort((a, b) => a - b);
const mean = sorted.length ? sorted.reduce((s, x) => s + x, 0) / sorted.length : 0;
const p50 = sorted.length ? sorted[Math.floor(sorted.length * 0.5)] : 0;
const p95 = sorted.length ? sorted[Math.floor(sorted.length * 0.95)] : 0;
// Sample std-dev of per-call latency; useful when --repeat > 1 to see how
// stable a model's latency is across redundant runs.
let stdev = 0;
if (sorted.length > 1) {
const variance = sorted.reduce((s, x) => s + (x - mean) ** 2, 0) / (sorted.length - 1);
stdev = Math.sqrt(variance);
}
return {
model: r.model, family: r.family,
passRate: r.total ? r.passes / r.total : 0,
passes: r.passes, total: r.total,
latency: { mean, p50, p95, stdev },
usdCost: r.usdCost,
promptTokens: r.promptTokens, completionTokens: r.completionTokens,
errorCount: r.errors.length,
errorSample: r.errors.slice(0, 2),
};
});
// Sort by pass rate desc, then by cost asc — Pareto-friendly view
rows.sort((a, b) => b.passRate - a.passRate || a.usdCost - b.usdCost);
const showStdev = ARGS.repeat > 1;
console.log(`| Model | Family | Pass | Latency mean${showStdev ? ' ± σ' : ''} | p95 | $/run | $/1k passes |`);
console.log(`|---|---|---|---|---|---|---|`);
for (const r of rows) {
const dollarPer1kPasses = r.passes > 0 ? (r.usdCost / r.passes) * 1000 : Infinity;
const lat = showStdev
? `${r.latency.mean.toFixed(0)} ± ${r.latency.stdev.toFixed(0)} ms`
: `${r.latency.mean.toFixed(0)} ms`;
console.log(`| \`${r.model}\` | ${r.family} | **${r.passes}/${r.total} = ${(r.passRate * 100).toFixed(1)}%** | ${lat} | ${r.latency.p95.toFixed(0)} ms | $${r.usdCost.toFixed(5)} | ${dollarPer1kPasses === Infinity ? '∞' : '$' + dollarPer1kPasses.toFixed(4)} |`);
}
console.log('');
console.log(`Total spend: $${rows.reduce((s, r) => s + r.usdCost, 0).toFixed(5)}`);
console.log(`Total errors: ${rows.reduce((s, r) => s + r.errorCount, 0)}`);
console.log(`\nPareto recommendation: pick the model on the upper-left of the (pass-rate, $/run) plane. Higher is better for accuracy; lower is better for cost.\n`);
// Save artifacts
if (ARGS.save) {
const ts = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-') + 'Z';
const outDir = resolvePath(REPO_ROOT, 'docs', 'benchmarks', 'runs');
mkdirSync(outDir, { recursive: true });
const jsonPath = resolvePath(outDir, `cheap-models-${ts}.json`);
writeFileSync(jsonPath, JSON.stringify({
meta: { ts: new Date().toISOString(), node: process.version, platform: `${process.platform}-${process.arch}`, args: ARGS, corpusSize: CORPUS.length },
results: rows,
}, null, 2));
console.log(`Saved: ${jsonPath}`);
}
console.log('\n===BENCH_JSON===');
console.log(JSON.stringify({ rows, totalSpendUSD: rows.reduce((s, r) => s + r.usdCost, 0) }, null, 2));
}
main().catch(e => { console.error('[bench] fatal:', e); process.exit(1); });