squeezr-ai 1.90.0 → 1.99.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/compressor.test.js +5 -1
- package/dist/__tests__/contentRouter.test.d.ts +1 -0
- package/dist/__tests__/contentRouter.test.js +76 -0
- package/dist/__tests__/controlEndpointGuard.test.d.ts +1 -0
- package/dist/__tests__/controlEndpointGuard.test.js +61 -0
- package/dist/__tests__/doctor.test.js +17 -1
- package/dist/__tests__/jsonCrush.test.js +59 -1
- package/dist/__tests__/outputSavings.test.d.ts +1 -0
- package/dist/__tests__/outputSavings.test.js +49 -0
- package/dist/__tests__/relevance.test.d.ts +1 -0
- package/dist/__tests__/relevance.test.js +66 -0
- package/dist/__tests__/secureKeyFile.test.d.ts +1 -0
- package/dist/__tests__/secureKeyFile.test.js +27 -0
- package/dist/__tests__/serverBinding.test.d.ts +1 -0
- package/dist/__tests__/serverBinding.test.js +18 -0
- package/dist/__tests__/statsOutput.test.d.ts +1 -0
- package/dist/__tests__/statsOutput.test.js +46 -0
- package/dist/__tests__/textCrusher.test.js +63 -1
- package/dist/codexMitm.d.ts +1 -0
- package/dist/codexMitm.js +26 -2
- package/dist/compressor.js +31 -3
- package/dist/contentRouter.d.ts +30 -0
- package/dist/contentRouter.js +112 -0
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.js +26 -7
- package/dist/deterministic.d.ts +1 -1
- package/dist/deterministic.js +14 -13
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +11 -0
- package/dist/index.js +4 -1
- package/dist/jsonCrush.d.ts +4 -0
- package/dist/jsonCrush.js +83 -18
- package/dist/outputSavings.d.ts +20 -0
- package/dist/outputSavings.js +52 -0
- package/dist/relevance.d.ts +27 -0
- package/dist/relevance.js +78 -0
- package/dist/server.js +84 -11
- package/dist/stats.d.ts +17 -0
- package/dist/stats.js +55 -0
- package/dist/systemPrompt.js +3 -2
- package/dist/textCrusher.d.ts +4 -0
- package/dist/textCrusher.js +69 -7
- package/package.json +69 -69
package/dist/dashboard.js
CHANGED
|
@@ -430,7 +430,7 @@ code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
|
|
|
430
430
|
<div class="hero-card">
|
|
431
431
|
<div class="hc-label">Requests</div>
|
|
432
432
|
<div class="hc-val" id="h-reqs">—</div>
|
|
433
|
-
<div class="hc-sub"><span id="h-comp">—</span> AI-compressed · det. always on
|
|
433
|
+
<div class="hc-sub"><span id="h-comp">—</span> AI-compressed · det. always on<span id="h-bypassed"></span></div>
|
|
434
434
|
</div>
|
|
435
435
|
</div>
|
|
436
436
|
|
|
@@ -498,16 +498,16 @@ code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
|
|
|
498
498
|
<div style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">0 here is normal when AI compression is off</div>
|
|
499
499
|
</div>
|
|
500
500
|
</div>
|
|
501
|
-
<!-- AI
|
|
501
|
+
<!-- Output shaping (output-side token reduction) — pinned in the old AI-Compression slot -->
|
|
502
502
|
<div class="section" style="margin:0">
|
|
503
|
-
<div class="section-head"><span class="section-title">
|
|
503
|
+
<div class="section-head"><span class="section-title">Output</span><span style="font-size:11px;color:var(--text3)" id="out-note">what the model writes back · needs [output] enabled</span></div>
|
|
504
504
|
<div class="section-body">
|
|
505
505
|
<div class="cache-row">
|
|
506
|
-
<div class="cache-card"><div class="cache-label">
|
|
507
|
-
<div class="cache-card"><div class="cache-label">
|
|
508
|
-
<div class="cache-card"><div class="cache-label">
|
|
506
|
+
<div class="cache-card"><div class="cache-label">Echo (restated)</div><div class="cache-val" id="out-echo">—</div></div>
|
|
507
|
+
<div class="cache-card"><div class="cache-label">Steered</div><div class="cache-val" id="out-steered">—</div></div>
|
|
508
|
+
<div class="cache-card"><div class="cache-label">Effort lowered</div><div class="cache-val" id="out-effort">—</div></div>
|
|
509
509
|
</div>
|
|
510
|
-
<div
|
|
510
|
+
<div style="margin-top:8px;font-size:11px;color:var(--text3);text-align:center">Echo = % of the model's output that merely restated context it was already given (lower is better).</div>
|
|
511
511
|
</div>
|
|
512
512
|
</div>
|
|
513
513
|
</div>
|
|
@@ -1022,6 +1022,9 @@ function render(d) {
|
|
|
1022
1022
|
document.getElementById('h-cost').textContent = fmtUsd(tCost);
|
|
1023
1023
|
document.getElementById('h-reqs').textContent = fmt(tReqs);
|
|
1024
1024
|
document.getElementById('h-comp').textContent = fmt(tComps);
|
|
1025
|
+
var byp = (d.bypassed_requests != null) ? d.bypassed_requests : 0;
|
|
1026
|
+
var bypEl = document.getElementById('h-bypassed');
|
|
1027
|
+
if (bypEl) bypEl.textContent = byp > 0 ? ' · ' + fmt(byp) + ' bypassed (not counted)' : '';
|
|
1025
1028
|
var perEl = document.getElementById('overview-period');
|
|
1026
1029
|
if (perEl && today.date) perEl.textContent = 'today · ' + today.date;
|
|
1027
1030
|
// (No quality banner in the Overview — only genuine quality issues, high expand
|
|
@@ -1118,6 +1121,22 @@ var eff = (today.efficiency_pct != null) ? today.efficiency_pct : null;
|
|
|
1118
1121
|
}
|
|
1119
1122
|
}
|
|
1120
1123
|
}
|
|
1124
|
+
// Output shaping card (all-time; only meaningful when [output] enabled)
|
|
1125
|
+
var out = d.output || {};
|
|
1126
|
+
var setOut = function(id, v){ var e = document.getElementById(id); if(e) e.textContent = v; };
|
|
1127
|
+
var echoEl = document.getElementById('out-echo');
|
|
1128
|
+
if (echoEl) {
|
|
1129
|
+
if (out.echo_samples > 0) {
|
|
1130
|
+
echoEl.textContent = out.avg_echo_pct + '%';
|
|
1131
|
+
// lower echo is better: green under 15%, amber 15-35%, red above
|
|
1132
|
+
echoEl.style.color = out.avg_echo_pct <= 15 ? 'var(--brand2)' : out.avg_echo_pct <= 35 ? '#fbbf24' : 'var(--red, #e5484d)';
|
|
1133
|
+
} else {
|
|
1134
|
+
echoEl.textContent = '—';
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
setOut('out-steered', out.steered > 0 ? fmt(out.steered) : '—');
|
|
1138
|
+
setOut('out-effort', out.effort_lowered > 0 ? fmt(out.effort_lowered) : '—');
|
|
1139
|
+
|
|
1121
1140
|
// Cost Comparison is OVERVIEW = TODAY: use today's tokens (not all-time), so the
|
|
1122
1141
|
// sub-lines match the today money. (modelCosts already comes from today's models.)
|
|
1123
1142
|
var cmpIn = today.original_tokens || 0;
|
package/dist/deterministic.d.ts
CHANGED
|
@@ -34,4 +34,4 @@ export declare function preprocessAssistant(text: string): string;
|
|
|
34
34
|
export type CodeLang = 'ts' | 'py' | 'go' | 'rs' | 'java' | 'c' | 'cpp';
|
|
35
35
|
export declare function detectCodeLanguage(text: string): CodeLang | null;
|
|
36
36
|
export declare function extractCodeStructure(text: string, lang: CodeLang): string;
|
|
37
|
-
export declare function preprocessForTool(text: string, toolName: string, pressure?: number): string;
|
|
37
|
+
export declare function preprocessForTool(text: string, toolName: string, pressure?: number, query?: string): string;
|
package/dist/deterministic.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* Glob — compact large file listings into directory summary
|
|
28
28
|
*/
|
|
29
29
|
import { storeOriginal, storeSegments } from './expand.js';
|
|
30
|
-
import {
|
|
30
|
+
import { crushEmbeddedJson } from './contentRouter.js';
|
|
31
31
|
import { crushText } from './textCrusher.js';
|
|
32
32
|
// ── Pattern hit tracking (for squeezr discover) ───────────────────────────────
|
|
33
33
|
export const detPatternHits = {};
|
|
@@ -928,13 +928,13 @@ function extractGenericErrors(text) {
|
|
|
928
928
|
// (collapse near-duplicate spam, keep head/tail + high-signal lines) over blind
|
|
929
929
|
// tail-keep — deterministic and cache-safe. Falls back to tail-keep if the crusher
|
|
930
930
|
// can't beat it (e.g. all lines unique and low-signal).
|
|
931
|
-
function truncateLongOutput(text, pressure = 0) {
|
|
931
|
+
function truncateLongOutput(text, pressure = 0, query = '') {
|
|
932
932
|
const threshold = pressure >= 0.9 ? 50 : 80;
|
|
933
933
|
const keepLines = pressure >= 0.9 ? 30 : 50;
|
|
934
934
|
const lines = text.split('\n');
|
|
935
935
|
if (lines.length <= threshold)
|
|
936
936
|
return text;
|
|
937
|
-
const crushed = crushText(text, { maxLines: keepLines, headKeep: 5, tailKeep: 8 });
|
|
937
|
+
const crushed = crushText(text, { maxLines: keepLines, headKeep: 5, tailKeep: 8, query });
|
|
938
938
|
if (crushed.dropped > 0 && crushed.text.length < text.length) {
|
|
939
939
|
hit('textCrush');
|
|
940
940
|
return crushed.text;
|
|
@@ -959,7 +959,7 @@ function compactFileListing(text) {
|
|
|
959
959
|
const summary = Object.entries(byDir).sort(([, a], [, b]) => b - a).map(([d, n]) => `${d}/ (${n} files)`).join('\n');
|
|
960
960
|
return `${lines.length} files total:\n${summary}`;
|
|
961
961
|
}
|
|
962
|
-
function applyBashPatterns(text, pressure = 0) {
|
|
962
|
+
function applyBashPatterns(text, pressure = 0, query = '') {
|
|
963
963
|
if (looksLikeGitDiff(text)) {
|
|
964
964
|
hit('gitDiff');
|
|
965
965
|
return compactGitDiff(text, pressure);
|
|
@@ -1083,7 +1083,7 @@ function applyBashPatterns(text, pressure = 0) {
|
|
|
1083
1083
|
return errExtracted;
|
|
1084
1084
|
}
|
|
1085
1085
|
hit('truncated');
|
|
1086
|
-
return truncateLongOutput(text, pressure);
|
|
1086
|
+
return truncateLongOutput(text, pressure, query);
|
|
1087
1087
|
}
|
|
1088
1088
|
// ── Grep tool ─────────────────────────────────────────────────────────────────
|
|
1089
1089
|
// Group matches by file, cap at MAX_PER_FILE lines each, truncate if too many files
|
|
@@ -1344,7 +1344,7 @@ function makeRecoverable(original, compacted) {
|
|
|
1344
1344
|
const id = storeOriginal(original);
|
|
1345
1345
|
return `${compacted}\n[squeezr_expand("${id}") — full untruncated output]`;
|
|
1346
1346
|
}
|
|
1347
|
-
export function preprocessForTool(text, toolName, pressure = 0) {
|
|
1347
|
+
export function preprocessForTool(text, toolName, pressure = 0, query = '') {
|
|
1348
1348
|
const tool = toolName.toLowerCase();
|
|
1349
1349
|
// NEVER compress the result of an expand call. The whole point of squeezr_expand
|
|
1350
1350
|
// is to deliver the FULL original back to the model; re-compacting it here (or
|
|
@@ -1354,19 +1354,20 @@ export function preprocessForTool(text, toolName, pressure = 0) {
|
|
|
1354
1354
|
if (tool === 'read') {
|
|
1355
1355
|
return makeRecoverable(text, compactReadOutput(preprocessRead(text)));
|
|
1356
1356
|
}
|
|
1357
|
-
// JSON array crush (deterministic, cache-safe, reversible).
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
// (its marker
|
|
1362
|
-
|
|
1357
|
+
// JSON array crush (deterministic, cache-safe, reversible). Now via the content router,
|
|
1358
|
+
// so it fires on arrays of homogeneous objects whether they are the WHOLE result or
|
|
1359
|
+
// EMBEDDED inside a larger one (gh api with a header, MCP envelopes, bash echo + json…).
|
|
1360
|
+
// Repeated per-row keys collapse to a single header; each crushed span is self-recoverable
|
|
1361
|
+
// (its marker carries squeezr_expand). Runs BEFORE the base pipeline so line-dedup/JSON-minify
|
|
1362
|
+
// never touch the structured data; returned directly.
|
|
1363
|
+
const crushed = crushEmbeddedJson(text);
|
|
1363
1364
|
if (crushed.savedChars > 0) {
|
|
1364
1365
|
hit('jsonTableCrush');
|
|
1365
1366
|
return crushed.text;
|
|
1366
1367
|
}
|
|
1367
1368
|
let t = preprocess(text);
|
|
1368
1369
|
if (tool === 'bash') {
|
|
1369
|
-
t = applyBashPatterns(t, pressure);
|
|
1370
|
+
t = applyBashPatterns(t, pressure, query);
|
|
1370
1371
|
}
|
|
1371
1372
|
else if (tool === 'grep') {
|
|
1372
1373
|
const before = t;
|
package/dist/doctor.d.ts
CHANGED
|
@@ -18,11 +18,16 @@ type Health = {
|
|
|
18
18
|
version?: string;
|
|
19
19
|
bypassed?: boolean;
|
|
20
20
|
port?: number;
|
|
21
|
+
compression?: {
|
|
22
|
+
requests?: number;
|
|
23
|
+
savings_pct?: number;
|
|
24
|
+
};
|
|
21
25
|
} | null;
|
|
22
26
|
export declare function checkProxy(health: Health): CheckResult;
|
|
23
27
|
export declare function checkVersion(installed: string, running: string | null): CheckResult;
|
|
24
28
|
export declare function checkEnv(baseUrl: string | undefined, port: number): CheckResult;
|
|
25
29
|
export declare function checkBypass(bypassed: boolean | undefined): CheckResult;
|
|
30
|
+
export declare function checkSavings(requests: number | undefined, savingsPct: number | undefined): CheckResult;
|
|
26
31
|
export declare function computeExitCode(checks: CheckResult[]): number;
|
|
27
32
|
export declare function formatDoctor(checks: CheckResult[], exitCode: number): string;
|
|
28
33
|
export declare function runDoctor(): Promise<{
|
package/dist/doctor.js
CHANGED
|
@@ -42,6 +42,16 @@ export function checkBypass(bypassed) {
|
|
|
42
42
|
return { name: 'Bypass', status: 'warn', detail: 'bypass is ON — compression is disabled (squeezr bypass --off to resume)' };
|
|
43
43
|
return { name: 'Bypass', status: 'pass', detail: 'compression active' };
|
|
44
44
|
}
|
|
45
|
+
export function checkSavings(requests, savingsPct) {
|
|
46
|
+
if (requests === undefined)
|
|
47
|
+
return { name: 'Savings', status: 'skip', detail: 'proxy not running' };
|
|
48
|
+
if (requests === 0)
|
|
49
|
+
return { name: 'Savings', status: 'skip', detail: 'no traffic yet' };
|
|
50
|
+
if (!savingsPct || savingsPct <= 0) {
|
|
51
|
+
return { name: 'Savings', status: 'warn', detail: `${requests} requests seen but ~0% saved — bypass on? stale build? all content incompressible?` };
|
|
52
|
+
}
|
|
53
|
+
return { name: 'Savings', status: 'pass', detail: `~${Math.round(savingsPct)}% saved over ${requests} requests` };
|
|
54
|
+
}
|
|
45
55
|
export function computeExitCode(checks) {
|
|
46
56
|
if (checks.some(c => c.status === 'fail'))
|
|
47
57
|
return 2;
|
|
@@ -96,6 +106,7 @@ export async function runDoctor() {
|
|
|
96
106
|
checkVersion(installedVersion(), runningVersion),
|
|
97
107
|
checkEnv(process.env.ANTHROPIC_BASE_URL, health?.port ?? port),
|
|
98
108
|
checkBypass(health?.bypassed),
|
|
109
|
+
checkSavings(health?.compression?.requests, health?.compression?.savings_pct),
|
|
99
110
|
];
|
|
100
111
|
const exitCode = computeExitCode(checks);
|
|
101
112
|
return { checks, exitCode, report: formatDoctor(checks, exitCode) };
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,10 @@ if (PORT !== config.port) {
|
|
|
46
46
|
const httpServer = createAdaptorServer({ fetch: app.fetch });
|
|
47
47
|
// Persist caches every 60s so a crash doesn't lose more than a minute of work
|
|
48
48
|
setInterval(() => { persistSessionCache(); persistExpandStore(); persistHistory(); }, 60_000).unref();
|
|
49
|
-
|
|
49
|
+
// Bind to loopback only: the proxy holds the user's API keys and exposes
|
|
50
|
+
// state-changing control endpoints, so it must never be reachable from the LAN.
|
|
51
|
+
// Matches the desktop proxy / MITM listeners, which already bind 127.0.0.1.
|
|
52
|
+
httpServer.listen(PORT, '127.0.0.1', () => {
|
|
50
53
|
console.log(`Squeezr v${VERSION} listening on http://localhost:${PORT}`);
|
|
51
54
|
console.log(`Mode: ${config.dryRun ? 'dry-run' : 'active'}`);
|
|
52
55
|
if (config.disabled)
|
package/dist/jsonCrush.d.ts
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* (the lossy SmartCrusher path) is intentionally NOT done here.
|
|
21
21
|
*/
|
|
22
22
|
export declare const TABLE_MARKER_RE: RegExp;
|
|
23
|
+
/** 32-bit SimHash fingerprint of a text (bit-voting over its word tokens). Two texts
|
|
24
|
+
* that share most tokens get fingerprints a small Hamming distance apart. */
|
|
25
|
+
export declare function simhash32(text: string): number;
|
|
26
|
+
export declare function hammingDistance(a: number, b: number): number;
|
|
23
27
|
/**
|
|
24
28
|
* If `text` is (entirely) a crushable JSON array of objects, return the table form plus
|
|
25
29
|
* the char count saved. Otherwise return the input unchanged with savedChars = 0.
|
package/dist/jsonCrush.js
CHANGED
|
@@ -23,11 +23,73 @@ import { storeOriginal } from './expand.js';
|
|
|
23
23
|
const MIN_ITEMS = 5;
|
|
24
24
|
const MIN_OBJECT_FRACTION = 0.8; // ≥80% of elements must be plain objects
|
|
25
25
|
const MIN_SAVINGS_RATIO = 0.15; // only crush if it shaves ≥15% of the chars
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const LOSSY_ROW_THRESHOLD = 50; // above this many rows, drop near-duplicates (SimHash)
|
|
27
|
+
const HAMMING_NEAR_DUP = 3; // rows within this 32-bit Hamming distance are "the same"
|
|
28
|
+
const ROW_SIGNAL_RE = /\b(error|err|fail|failed|failure|exception|fatal|panic|denied|refused|timeout|critical|crash|oom|unhealthy|degraded)\b/i;
|
|
29
|
+
// Recognises the table header this module emits (anywhere in the text, since the table
|
|
30
|
+
// may be embedded in a larger tool result). Group 1 = the expand id.
|
|
31
|
+
export const TABLE_MARKER_RE = /\[squeezr:table ([0-9a-f]{6,}(?:~\d+)?) —/;
|
|
28
32
|
function isPlainObject(v) {
|
|
29
33
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
30
34
|
}
|
|
35
|
+
// ── SimHash near-duplicate detection (deterministic, dependency-free) ─────────
|
|
36
|
+
/** FNV-1a 32-bit hash of a token. */
|
|
37
|
+
function fnv1a32(s) {
|
|
38
|
+
let h = 0x811c9dc5;
|
|
39
|
+
for (let i = 0; i < s.length; i++) {
|
|
40
|
+
h ^= s.charCodeAt(i);
|
|
41
|
+
h = Math.imul(h, 0x01000193);
|
|
42
|
+
}
|
|
43
|
+
return h >>> 0;
|
|
44
|
+
}
|
|
45
|
+
/** 32-bit SimHash fingerprint of a text (bit-voting over its word tokens). Two texts
|
|
46
|
+
* that share most tokens get fingerprints a small Hamming distance apart. */
|
|
47
|
+
export function simhash32(text) {
|
|
48
|
+
const tokens = text.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
|
|
49
|
+
if (tokens.length === 0)
|
|
50
|
+
return 0;
|
|
51
|
+
const bits = new Array(32).fill(0);
|
|
52
|
+
for (const t of tokens) {
|
|
53
|
+
const h = fnv1a32(t);
|
|
54
|
+
for (let b = 0; b < 32; b++)
|
|
55
|
+
bits[b] += ((h >>> b) & 1) ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
let f = 0;
|
|
58
|
+
for (let b = 0; b < 32; b++)
|
|
59
|
+
if (bits[b] > 0)
|
|
60
|
+
f |= (1 << b);
|
|
61
|
+
return f >>> 0;
|
|
62
|
+
}
|
|
63
|
+
export function hammingDistance(a, b) {
|
|
64
|
+
let x = (a ^ b) >>> 0;
|
|
65
|
+
let c = 0;
|
|
66
|
+
while (x) {
|
|
67
|
+
c += x & 1;
|
|
68
|
+
x >>>= 1;
|
|
69
|
+
}
|
|
70
|
+
return c;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Given the per-row rendered strings, return the indices to KEEP: one representative per
|
|
74
|
+
* SimHash cluster (near-duplicates dropped), plus every row that carries an error/anomaly
|
|
75
|
+
* signal (always kept). Order preserved. Deterministic.
|
|
76
|
+
*/
|
|
77
|
+
function selectRepresentativeRows(rowStrings) {
|
|
78
|
+
const reps = []; // simhash fingerprints of chosen representatives
|
|
79
|
+
const kept = [];
|
|
80
|
+
for (let i = 0; i < rowStrings.length; i++) {
|
|
81
|
+
if (ROW_SIGNAL_RE.test(rowStrings[i])) {
|
|
82
|
+
kept.push(i);
|
|
83
|
+
continue;
|
|
84
|
+
} // anomaly → always keep
|
|
85
|
+
const sig = simhash32(rowStrings[i]);
|
|
86
|
+
if (reps.some(r => hammingDistance(r, sig) <= HAMMING_NEAR_DUP))
|
|
87
|
+
continue; // near-dup → drop
|
|
88
|
+
reps.push(sig);
|
|
89
|
+
kept.push(i);
|
|
90
|
+
}
|
|
91
|
+
return kept;
|
|
92
|
+
}
|
|
31
93
|
/** Union of keys across rows, in first-seen order (deterministic). */
|
|
32
94
|
function collectColumns(rows) {
|
|
33
95
|
const cols = [];
|
|
@@ -86,27 +148,30 @@ export function crushJsonArrays(text) {
|
|
|
86
148
|
if (cols.length === 0)
|
|
87
149
|
return { text, savedChars: 0 };
|
|
88
150
|
const header = cols.join(CELL_DELIM);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
const rowLines = rows.map(row => isPlainObject(row)
|
|
152
|
+
? cols.map(c => cell(row[c], c in row)).join(CELL_DELIM)
|
|
153
|
+
// Non-object element inside a mostly-object array: keep it verbatim as JSON so
|
|
154
|
+
// nothing is lost (recoverable in full via expand anyway).
|
|
155
|
+
: JSON.stringify(row));
|
|
156
|
+
// Lossy row-drop for LARGE arrays: collapse near-duplicate rows (SimHash), always
|
|
157
|
+
// keeping anomaly/error rows. Deterministic → cache-safe. The full original is in the
|
|
158
|
+
// expand store, so dropped rows are recoverable. Small arrays keep every row.
|
|
159
|
+
let bodyLines = rowLines;
|
|
160
|
+
let omitted = 0;
|
|
161
|
+
if (rows.length > LOSSY_ROW_THRESHOLD) {
|
|
162
|
+
const kept = selectRepresentativeRows(rowLines);
|
|
163
|
+
if (kept.length < rowLines.length) {
|
|
164
|
+
bodyLines = kept.map(i => rowLines[i]);
|
|
165
|
+
omitted = rowLines.length - kept.length;
|
|
98
166
|
}
|
|
99
167
|
}
|
|
100
|
-
// Build the (id-less) body first to measure real savings before we pay for an id.
|
|
101
|
-
const provisional = `${header}\n${bodyLines.join('\n')}`;
|
|
102
|
-
const saved = text.length - provisional.length;
|
|
103
|
-
if (saved <= 0 || saved / text.length < MIN_SAVINGS_RATIO)
|
|
104
|
-
return { text, savedChars: 0 };
|
|
105
168
|
const id = storeOriginal(text);
|
|
106
|
-
const marker =
|
|
169
|
+
const marker = omitted > 0
|
|
170
|
+
? `[squeezr:table ${id} — showing ${bodyLines.length} of ${rows.length} rows × ${cols.length} cols; ${omitted} near-duplicate rows omitted; squeezr_expand("${id}") for original JSON]`
|
|
171
|
+
: `[squeezr:table ${id} — ${rows.length} rows × ${cols.length} cols; squeezr_expand("${id}") for original JSON]`;
|
|
107
172
|
const out = `${marker}\n${header}\n${bodyLines.join('\n')}`;
|
|
108
173
|
const savedChars = text.length - out.length;
|
|
109
|
-
if (savedChars <= 0)
|
|
174
|
+
if (savedChars <= 0 || savedChars / text.length < MIN_SAVINGS_RATIO)
|
|
110
175
|
return { text, savedChars: 0 };
|
|
111
176
|
return { text: out, savedChars };
|
|
112
177
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-savings signals — Squeezr's honest, no-counterfactual take on headroom's output
|
|
3
|
+
* measurement. The fully honest number (headroom's holdout A/B) needs a control arm; that
|
|
4
|
+
* is deferred. This ships the tier that needs NO counterfactual: the ECHO RATIO — how much
|
|
5
|
+
* of what the model just wrote merely restated context it was already given.
|
|
6
|
+
*
|
|
7
|
+
* High echo = wasted output tokens = exactly what verbosity steering (1.83) targets. Reading
|
|
8
|
+
* the response to measure this never changes the bytes we forward, so it's cache-safe.
|
|
9
|
+
*/
|
|
10
|
+
/** Word n-grams (set) of a text. */
|
|
11
|
+
export declare function wordNgrams(text: string, n?: number): Set<string>;
|
|
12
|
+
/**
|
|
13
|
+
* Fraction of the OUTPUT's n-grams that also appear in the CONTEXT (0..1). 1 = the model
|
|
14
|
+
* only restated what it was already shown; 0 = fully novel output.
|
|
15
|
+
*/
|
|
16
|
+
export declare function echoRatio(output: string, context: string, n?: number): number;
|
|
17
|
+
/** Pull the assistant's text out of an Anthropic SSE stream (concatenated text_delta). */
|
|
18
|
+
export declare function extractAssistantTextFromSse(sse: string): string;
|
|
19
|
+
/** Assistant text from a non-streaming Anthropic response body's content blocks. */
|
|
20
|
+
export declare function extractAssistantTextFromContent(content: unknown): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-savings signals — Squeezr's honest, no-counterfactual take on headroom's output
|
|
3
|
+
* measurement. The fully honest number (headroom's holdout A/B) needs a control arm; that
|
|
4
|
+
* is deferred. This ships the tier that needs NO counterfactual: the ECHO RATIO — how much
|
|
5
|
+
* of what the model just wrote merely restated context it was already given.
|
|
6
|
+
*
|
|
7
|
+
* High echo = wasted output tokens = exactly what verbosity steering (1.83) targets. Reading
|
|
8
|
+
* the response to measure this never changes the bytes we forward, so it's cache-safe.
|
|
9
|
+
*/
|
|
10
|
+
/** Word n-grams (set) of a text. */
|
|
11
|
+
export function wordNgrams(text, n = 3) {
|
|
12
|
+
const words = text.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
|
|
13
|
+
const set = new Set();
|
|
14
|
+
for (let i = 0; i + n <= words.length; i++)
|
|
15
|
+
set.add(words.slice(i, i + n).join(' '));
|
|
16
|
+
return set;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fraction of the OUTPUT's n-grams that also appear in the CONTEXT (0..1). 1 = the model
|
|
20
|
+
* only restated what it was already shown; 0 = fully novel output.
|
|
21
|
+
*/
|
|
22
|
+
export function echoRatio(output, context, n = 3) {
|
|
23
|
+
const out = wordNgrams(output, n);
|
|
24
|
+
if (out.size === 0)
|
|
25
|
+
return 0;
|
|
26
|
+
const ctx = wordNgrams(context, n);
|
|
27
|
+
let echoed = 0;
|
|
28
|
+
for (const g of out)
|
|
29
|
+
if (ctx.has(g))
|
|
30
|
+
echoed++;
|
|
31
|
+
return echoed / out.size;
|
|
32
|
+
}
|
|
33
|
+
/** Pull the assistant's text out of an Anthropic SSE stream (concatenated text_delta). */
|
|
34
|
+
export function extractAssistantTextFromSse(sse) {
|
|
35
|
+
let out = '';
|
|
36
|
+
for (const m of sse.matchAll(/"type":"text_delta","text":"((?:[^"\\]|\\.)*)"/g)) {
|
|
37
|
+
try {
|
|
38
|
+
out += JSON.parse(`"${m[1]}"`);
|
|
39
|
+
}
|
|
40
|
+
catch { /* skip malformed delta */ }
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Assistant text from a non-streaming Anthropic response body's content blocks. */
|
|
45
|
+
export function extractAssistantTextFromContent(content) {
|
|
46
|
+
if (!Array.isArray(content))
|
|
47
|
+
return '';
|
|
48
|
+
return content
|
|
49
|
+
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
|
50
|
+
.map(b => b.text)
|
|
51
|
+
.join('');
|
|
52
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 relevance scorer — a shared primitive so Squeezr's compressors can keep content
|
|
3
|
+
* by RELEVANCE to the current task, not just by position (head/tail) or recency.
|
|
4
|
+
*
|
|
5
|
+
* This is the single highest-leverage idea copied from headroom: their SmartCrusher,
|
|
6
|
+
* TextCrusher and CodeCompressor all route "what to keep" through one relevance scorer.
|
|
7
|
+
* Squeezr, until now, kept blindly. This module is that scorer — pure TS, zero deps,
|
|
8
|
+
* deterministic (→ cache-safe when used to drive byte-stable compression).
|
|
9
|
+
*
|
|
10
|
+
* BM25 (Okapi) is the standard, well-understood ranking function: term frequency with
|
|
11
|
+
* saturation (k1) and document-length normalization (b), weighted by inverse document
|
|
12
|
+
* frequency so rare query terms (an error code, a filename) count far more than common
|
|
13
|
+
* words. The idf uses +0.5 smoothing so it is always positive.
|
|
14
|
+
*/
|
|
15
|
+
export interface BM25Options {
|
|
16
|
+
k1?: number;
|
|
17
|
+
b?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Lowercase, split on non-alphanumerics, keep tokens of length >= 2. */
|
|
20
|
+
export declare function tokenize(text: string): string[];
|
|
21
|
+
/**
|
|
22
|
+
* BM25 score of each doc in `docs` against `query`. One score per doc, in order.
|
|
23
|
+
* A doc with no query-term overlap scores exactly 0.
|
|
24
|
+
*/
|
|
25
|
+
export declare function bm25Scores(query: string, docs: string[], opts?: BM25Options): number[];
|
|
26
|
+
/** Document indices sorted by descending relevance (ties keep original order). */
|
|
27
|
+
export declare function rankByRelevance(query: string, docs: string[], opts?: BM25Options): number[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 relevance scorer — a shared primitive so Squeezr's compressors can keep content
|
|
3
|
+
* by RELEVANCE to the current task, not just by position (head/tail) or recency.
|
|
4
|
+
*
|
|
5
|
+
* This is the single highest-leverage idea copied from headroom: their SmartCrusher,
|
|
6
|
+
* TextCrusher and CodeCompressor all route "what to keep" through one relevance scorer.
|
|
7
|
+
* Squeezr, until now, kept blindly. This module is that scorer — pure TS, zero deps,
|
|
8
|
+
* deterministic (→ cache-safe when used to drive byte-stable compression).
|
|
9
|
+
*
|
|
10
|
+
* BM25 (Okapi) is the standard, well-understood ranking function: term frequency with
|
|
11
|
+
* saturation (k1) and document-length normalization (b), weighted by inverse document
|
|
12
|
+
* frequency so rare query terms (an error code, a filename) count far more than common
|
|
13
|
+
* words. The idf uses +0.5 smoothing so it is always positive.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_K1 = 1.5;
|
|
16
|
+
const DEFAULT_B = 0.75;
|
|
17
|
+
/** Lowercase, split on non-alphanumerics, keep tokens of length >= 2. */
|
|
18
|
+
export function tokenize(text) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const m of text.toLowerCase().matchAll(/[a-z0-9_]+/g)) {
|
|
21
|
+
if (m[0].length >= 2)
|
|
22
|
+
out.push(m[0]);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* BM25 score of each doc in `docs` against `query`. One score per doc, in order.
|
|
28
|
+
* A doc with no query-term overlap scores exactly 0.
|
|
29
|
+
*/
|
|
30
|
+
export function bm25Scores(query, docs, opts = {}) {
|
|
31
|
+
const k1 = opts.k1 ?? DEFAULT_K1;
|
|
32
|
+
const b = opts.b ?? DEFAULT_B;
|
|
33
|
+
const queryTerms = [...new Set(tokenize(query))];
|
|
34
|
+
const N = docs.length;
|
|
35
|
+
if (queryTerms.length === 0 || N === 0)
|
|
36
|
+
return new Array(N).fill(0);
|
|
37
|
+
// Tokenize docs once; compute lengths and average length.
|
|
38
|
+
const docTokens = docs.map(tokenize);
|
|
39
|
+
const docLen = docTokens.map(t => t.length);
|
|
40
|
+
const avgdl = docLen.reduce((s, l) => s + l, 0) / N || 1;
|
|
41
|
+
// Per-doc term-frequency maps.
|
|
42
|
+
const tf = docTokens.map(tokens => {
|
|
43
|
+
const m = new Map();
|
|
44
|
+
for (const t of tokens)
|
|
45
|
+
m.set(t, (m.get(t) ?? 0) + 1);
|
|
46
|
+
return m;
|
|
47
|
+
});
|
|
48
|
+
// Document frequency + idf per query term.
|
|
49
|
+
const idf = new Map();
|
|
50
|
+
for (const term of queryTerms) {
|
|
51
|
+
let df = 0;
|
|
52
|
+
for (const m of tf)
|
|
53
|
+
if (m.has(term))
|
|
54
|
+
df++;
|
|
55
|
+
// Always-positive smoothed idf.
|
|
56
|
+
idf.set(term, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
|
|
57
|
+
}
|
|
58
|
+
return docTokens.map((_, i) => {
|
|
59
|
+
let score = 0;
|
|
60
|
+
const len = docLen[i];
|
|
61
|
+
for (const term of queryTerms) {
|
|
62
|
+
const f = tf[i].get(term) ?? 0;
|
|
63
|
+
if (f === 0)
|
|
64
|
+
continue;
|
|
65
|
+
const denom = f + k1 * (1 - b + (b * len) / avgdl);
|
|
66
|
+
score += (idf.get(term) ?? 0) * ((f * (k1 + 1)) / denom);
|
|
67
|
+
}
|
|
68
|
+
return score;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Document indices sorted by descending relevance (ties keep original order). */
|
|
72
|
+
export function rankByRelevance(query, docs, opts) {
|
|
73
|
+
const scores = bm25Scores(query, docs, opts);
|
|
74
|
+
return scores
|
|
75
|
+
.map((score, i) => ({ score, i }))
|
|
76
|
+
.sort((a, b) => (b.score - a.score) || (a.i - b.i))
|
|
77
|
+
.map(x => x.i);
|
|
78
|
+
}
|