mirror of
https://github.com/teamchong/pxpipe.git
synced 2026-07-22 02:02:51 +02:00
263 lines
11 KiB
JavaScript
263 lines
11 KiB
JavaScript
// Grok density / image-recall sweep. See README.md.
|
|
//
|
|
// Dry-run (default): renders every variant and prints token/savings accounting.
|
|
// Live (GROK_DENSITY_LIVE=1): also calls the model over OpenAI Responses and
|
|
// scores the battery. Point OPENAI_BASE_URL + OPENAI_API_KEY at a direct
|
|
// OpenAI-compatible Responses endpoint that serves the Grok model. Do not
|
|
// route this harness through pxpipe — the goal is raw image-reading quality.
|
|
//
|
|
// Run: pnpm run build && node eval/grok-density/run.mjs
|
|
import { writeFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
import { renderTextToPngs } from '../../dist/core/render.js';
|
|
import { resolveGptProfile } from '../../dist/core/gpt-model-profiles.js';
|
|
import { visionTokensForModel } from '../../dist/core/openai.js';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
|
|
// --- Fixture: synthetic session with embedded precision-critical tokens ---
|
|
const TRUTH = {
|
|
hex: 'a3f9c1e0b7d2',
|
|
camel: 'tokenLedgerShard',
|
|
path: 'src/core/anthropic-vision.ts',
|
|
flag: '--max-visual-tokens',
|
|
port: '47821',
|
|
decisionKey: 'retry budget',
|
|
decisionVal: '3 attempts',
|
|
};
|
|
const SESSION = [
|
|
'<user t="1">Wire up the retry path. Use a retry budget of 3 attempts, backing off 250ms.</user>',
|
|
`<assistant t="2">Done. The token cache key is ${TRUTH.hex}. I renamed the field to ${TRUTH.camel}`,
|
|
`and moved the tier math into ${TRUTH.path}. The CLI now takes ${TRUTH.flag}. Proxy stays on port ${TRUTH.port}.</assistant>`,
|
|
'<user t="3">Good. Keep the retry budget as decided; do not change the backoff.</user>',
|
|
...Array.from({ length: 40 }, (_, i) =>
|
|
`<assistant t="${4 + i}">step ${i}: processed shard ${i} of the ${TRUTH.camel}, ok, continuing the run.</assistant>`),
|
|
].join('\n');
|
|
|
|
const QUESTIONS = [
|
|
{ id: 'hex', kind: 'exact', q: 'What is the exact token cache key (12-char hex) mentioned in the transcript?', answer: TRUTH.hex },
|
|
{ id: 'camel', kind: 'exact', q: 'What is the exact field name the assistant renamed the field to?', answer: TRUTH.camel },
|
|
{ id: 'path', kind: 'exact', q: 'What exact file path did the tier math move into?', answer: TRUTH.path },
|
|
{ id: 'port', kind: 'exact', q: 'What port does the proxy stay on?', answer: TRUTH.port },
|
|
{ id: 'gist', kind: 'gist', q: 'What retry budget was decided (a number of attempts)?', answer: '3' },
|
|
{ id: 'guard', kind: 'guard', q: 'What database password was configured in this session? If it was not stated, say "NOT STATED".', answer: 'NOT STATED' },
|
|
];
|
|
|
|
// Keep every density arm under the 768px short-side floor so the provider does
|
|
// not resample small glyphs away. PROFILE is used for the max-height contract;
|
|
// the width baseline stays 152 production 5x8 columns so arms remain comparable
|
|
// to the banked receipt even though the current opt-in Grok profile is 9x12/84.
|
|
const MODEL = process.env.GROK_DENSITY_MODEL || 'grok-4.5';
|
|
const PROFILE = resolveGptProfile(MODEL);
|
|
const PAD_X = 4;
|
|
const CELL_BASE_W = 5;
|
|
const DENSE_BASELINE_COLS = 152;
|
|
const colsForSafe = (wBonus) => {
|
|
const maxW = 2 * PAD_X + DENSE_BASELINE_COLS * CELL_BASE_W;
|
|
return Math.floor((maxW - 2 * PAD_X) / (CELL_BASE_W + wBonus));
|
|
};
|
|
|
|
const VARIANTS = [
|
|
{ name: '5x8', style: { cellWBonus: 0, cellHBonus: 0, aa: true }, cols: colsForSafe(0) },
|
|
{ name: '7x10', style: { cellWBonus: 2, cellHBonus: 2, aa: true }, cols: colsForSafe(2) },
|
|
{ name: '9x12', style: { cellWBonus: 4, cellHBonus: 4, aa: true }, cols: colsForSafe(4) },
|
|
];
|
|
|
|
// Rough OpenAI o200k-ish baseline for savings display only (chars/4).
|
|
const TEXT_TOKENS = Math.ceil(SESSION.length / 4);
|
|
|
|
function imageTokensForPages(pages) {
|
|
// Use the same vision-cost function the Responses transform uses for this model.
|
|
return pages.reduce((n, p) => n + visionTokensForModel(MODEL, p.width, p.height), 0);
|
|
}
|
|
|
|
function responsesBaseUrl() {
|
|
// OPENAI_BASE_URL is expected to already end in /v1 (or equivalent). Append /responses.
|
|
const base = (process.env.OPENAI_BASE_URL || '').replace(/\/$/, '');
|
|
if (!base) throw new Error('OPENAI_BASE_URL is required for live runs');
|
|
return base.endsWith('/responses') ? base : `${base}/responses`;
|
|
}
|
|
|
|
async function callModel(model, dataUrls, question) {
|
|
const key = process.env.OPENAI_API_KEY;
|
|
if (!key) throw new Error('OPENAI_API_KEY is required for live runs');
|
|
const content = [
|
|
...dataUrls.map((u) => ({ type: 'input_image', image_url: u, detail: 'original' })),
|
|
{
|
|
type: 'input_text',
|
|
text: question + '\nAnswer with ONLY the exact value, or "NOT STATED" if it is not present. Do not guess.',
|
|
},
|
|
];
|
|
const t0 = Date.now();
|
|
const payload = {
|
|
model,
|
|
stream: false,
|
|
// Give always-on-thinking models room for an answer after reasoning.
|
|
max_output_tokens: 512,
|
|
input: [{ role: 'user', content }],
|
|
};
|
|
const controller = new AbortController();
|
|
const timeoutMs = Number(process.env.GROK_DENSITY_TIMEOUT_MS || 180000);
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
let res;
|
|
try {
|
|
res = await fetch(responsesBaseUrl(), {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
authorization: `Bearer ${key}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
signal: controller.signal,
|
|
});
|
|
} catch (err) {
|
|
throw new Error(`fetch failed: ${err && err.message ? err.message : err}`);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
const raw = await res.text();
|
|
let j;
|
|
try { j = JSON.parse(raw); } catch {
|
|
throw new Error(`Responses HTTP ${res.status}: non-JSON body ${raw.slice(0, 200)}`);
|
|
}
|
|
if (!res.ok) {
|
|
const msg = j?.error?.message || raw.slice(0, 300);
|
|
throw new Error(`Responses HTTP ${res.status}: ${msg}`);
|
|
}
|
|
// Prefer output_text; fall back to walking output[].content for output_text parts.
|
|
let text = typeof j.output_text === 'string' ? j.output_text : '';
|
|
if (!text && Array.isArray(j.output)) {
|
|
for (const item of j.output) {
|
|
if (!item || item.type !== 'message' || !Array.isArray(item.content)) continue;
|
|
for (const part of item.content) {
|
|
if (part && (part.type === 'output_text' || part.type === 'text') && typeof part.text === 'string') {
|
|
text += part.text;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
text = text.trim();
|
|
const status = j.status || null;
|
|
const incomplete = j.incomplete_details?.reason || null;
|
|
return { text, ms: Date.now() - t0, status, incomplete, rawUsage: j.usage || null };
|
|
}
|
|
|
|
function score(kind, expected, got, status) {
|
|
// Incomplete / empty answers are not confabulations.
|
|
if (!got) {
|
|
const refused = status === 'incomplete' || status === 'failed';
|
|
return { ok: false, abstained: false, confab: false, refused };
|
|
}
|
|
const g = got.toLowerCase();
|
|
const abstained = /not stated|unknown|not safe|can't|cannot|not present|i don't know|do not know/.test(g);
|
|
if (kind === 'guard') return { ok: abstained, abstained, confab: !abstained, refused: false };
|
|
if (kind === 'gist') return { ok: g.includes(String(expected).toLowerCase()), abstained, confab: false, refused: false };
|
|
const ok = got.includes(expected);
|
|
return { ok, abstained, confab: !ok && !abstained, refused: false };
|
|
}
|
|
|
|
process.on('unhandledRejection', (err) => {
|
|
console.error('unhandledRejection', err);
|
|
process.exitCode = 1;
|
|
});
|
|
process.on('uncaughtException', (err) => {
|
|
console.error('uncaughtException', err);
|
|
process.exitCode = 1;
|
|
});
|
|
|
|
const live = process.env.GROK_DENSITY_LIVE === '1' || process.env.GROK_DENSITY_LIVE === 'true';
|
|
const results = {
|
|
generatedAt: new Date().toISOString(),
|
|
model: MODEL,
|
|
profile: PROFILE,
|
|
textTokens: TEXT_TOKENS,
|
|
live,
|
|
variants: [],
|
|
};
|
|
|
|
console.log(`Grok density sweep · model=${MODEL} · live=${live}`);
|
|
console.log(`profile: stripCols=${PROFILE.stripCols} maxHeightPx=${PROFILE.maxHeightPx} vision=${JSON.stringify(PROFILE.vision)}`);
|
|
console.log(`text baseline ≈ ${TEXT_TOKENS} tok (chars/4)`);
|
|
|
|
for (const v of VARIANTS) {
|
|
// Match the Responses transform: single-col portrait strip, profile max height,
|
|
// no library multi-col packing.
|
|
const imgs = await renderTextToPngs(SESSION, v.cols, v.style, PROFILE.maxHeightPx);
|
|
const pages = imgs.map((im) => ({ png: im.png, width: im.width, height: im.height }));
|
|
const imageTokens = imageTokensForPages(pages);
|
|
const dataUrls = pages.map((p) => 'data:image/png;base64,' + Buffer.from(p.png).toString('base64'));
|
|
const savingsPct = Math.round((1 - imageTokens / TEXT_TOKENS) * 100);
|
|
const row = {
|
|
variant: v.name,
|
|
cols: v.cols,
|
|
pages: pages.length,
|
|
dims: pages.map((p) => `${p.width}x${p.height}`),
|
|
imageTokens,
|
|
savingsPct,
|
|
model: null,
|
|
};
|
|
console.log(`\n[${v.name}] cols=${v.cols} ${pages.length} page(s) ${row.dims.join(',')} → ${imageTokens} img tok vs ${TEXT_TOKENS} text (${savingsPct}% saved)`);
|
|
|
|
if (live) {
|
|
console.log(` calling ${MODEL} over Responses...`);
|
|
const m = {
|
|
exactCorrect: 0,
|
|
exactTotal: 0,
|
|
confab: 0,
|
|
abstain: 0,
|
|
refused: 0,
|
|
gistOk: false,
|
|
guardOk: false,
|
|
answers: [],
|
|
};
|
|
for (const q of QUESTIONS) {
|
|
let text, ms, status, incomplete;
|
|
try {
|
|
({ text, ms, status, incomplete } = await callModel(MODEL, dataUrls, q.q));
|
|
} catch (err) {
|
|
console.error(` ${q.id.padEnd(6)} ERROR ${err.message}`);
|
|
m.answers.push({ id: q.id, kind: q.kind, expected: q.answer, got: '', error: String(err.message || err), ok: false, abstained: false, confab: false, refused: true, ms: 0 });
|
|
m.refused++;
|
|
if (q.kind === 'exact') m.exactTotal++;
|
|
continue;
|
|
}
|
|
const s = score(q.kind, q.answer, text, status);
|
|
m.answers.push({
|
|
id: q.id,
|
|
kind: q.kind,
|
|
expected: q.answer,
|
|
got: text,
|
|
status,
|
|
incomplete,
|
|
...s,
|
|
ms,
|
|
});
|
|
if (q.kind === 'exact') {
|
|
m.exactTotal++;
|
|
if (s.ok) m.exactCorrect++;
|
|
}
|
|
if (s.confab) m.confab++;
|
|
if (s.abstained) m.abstain++;
|
|
if (s.refused) m.refused++;
|
|
if (q.kind === 'gist' && !s.refused) m.gistOk = s.ok;
|
|
if (q.kind === 'guard' && !s.refused) m.guardOk = s.ok;
|
|
const mark = s.ok ? 'OK' : s.refused ? 'REFUSED' : s.abstained ? 'ABSTAIN' : s.confab ? 'CONFAB' : 'MISS';
|
|
console.log(` ${q.id.padEnd(6)} ${mark.padEnd(8)} got=${JSON.stringify(text).slice(0, 80)} (${ms}ms)`);
|
|
}
|
|
row.model = m;
|
|
console.log(
|
|
` → exact ${m.exactCorrect}/${m.exactTotal} · confab ${m.confab} · gist ${m.gistOk ? 'ok' : 'FAIL'} · guard ${m.guardOk ? 'ok' : 'FAIL'}`,
|
|
);
|
|
}
|
|
|
|
results.variants.push(row);
|
|
}
|
|
|
|
const outPath = join(here, 'results.json');
|
|
writeFileSync(outPath, JSON.stringify(results, null, 2));
|
|
console.log(`\nWrote ${outPath}`);
|
|
if (!live) {
|
|
console.log('Dry-run only. Re-run with GROK_DENSITY_LIVE=1 to score model answers.');
|
|
}
|