openzoo 0.49.4 → 0.49.6
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/README.md +7 -7
- package/lib/demo.js +4 -1
- package/lib/grokui.mjs +449 -26
- package/lib/gui.html +4 -1
- package/lib/livestatus.js +173 -4
- package/lib/mcp.js +3 -1
- package/lib/pay.js +3 -0
- package/lib/podagent.mjs +50 -12
- package/lib/proxy.js +114 -97
- package/lib/racesettle.js +78 -8
- package/lib/spill.js +168 -9
- package/lib/x402.js +13 -4
- package/package.json +3 -3
package/lib/spill.js
CHANGED
|
@@ -118,6 +118,31 @@ export function corpusCharsForSend(boundChars, contextId, thisTurn) {
|
|
|
118
118
|
return Math.max(boundChars.get(contextId) || 0, thisTurn || 0);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Pricing / unspilled basis: the unspilled size, never the shrunken sent
|
|
123
|
+
* (tokensAfter) size. Same unit on both args — tokens or chars, not mixed.
|
|
124
|
+
*/
|
|
125
|
+
export function unspilledBasis({ tokensBefore, corpus } = {}) {
|
|
126
|
+
return Math.max(Number(tokensBefore) || 0, Number(corpus) || 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gateway counterfactual the tell-line must print. Missing / non-finite /
|
|
131
|
+
* non-positive → null (`basis ?`). Never reads lecore.corpusTokens.
|
|
132
|
+
*/
|
|
133
|
+
export function spillPricedTellBasis(x) {
|
|
134
|
+
const n = Number(x?.counterfactualTokensUsed);
|
|
135
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Exact `spill priced:` / `spill priced (streamed):` line the proxy logs. */
|
|
139
|
+
export function spillPricedLine(x, { streamed = false } = {}) {
|
|
140
|
+
const lc = x?.lecore || {};
|
|
141
|
+
const basis = spillPricedTellBasis(x);
|
|
142
|
+
const prefix = streamed ? 'spill priced (streamed):' : 'spill priced:';
|
|
143
|
+
return `${prefix} ${x?.pricing} · basis ${basis ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x?.billedUsd ?? 0).toFixed(5)} direct ${(x?.directUsd ?? 0).toFixed(5)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
/** Expand ~ and resolve relative paths against cwd. Returns null if unusable. */
|
|
122
147
|
export function resolveReadablePath(p, cwd = process.cwd()) {
|
|
123
148
|
if (typeof p !== 'string') return null;
|
|
@@ -1256,9 +1281,9 @@ function lastUserAskIndex(msgs, firstSpillable) {
|
|
|
1256
1281
|
return -1;
|
|
1257
1282
|
}
|
|
1258
1283
|
|
|
1259
|
-
function countRealTurns(msgs, from) {
|
|
1284
|
+
function countRealTurns(msgs, from, to = msgs.length) {
|
|
1260
1285
|
let n = 0;
|
|
1261
|
-
for (let i = from; i <
|
|
1286
|
+
for (let i = from; i < to; i++) {
|
|
1262
1287
|
const r = msgs[i]?.role;
|
|
1263
1288
|
if (r === 'user' || r === 'assistant') n += 1;
|
|
1264
1289
|
}
|
|
@@ -1356,7 +1381,9 @@ export function trimUnseverablePairs(msgs, {
|
|
|
1356
1381
|
|
|
1357
1382
|
/**
|
|
1358
1383
|
* Pick a severable cut: keep a recent tail, honour the byte budget, floor
|
|
1359
|
-
* at minTurns of user/assistant, and never drop the last
|
|
1384
|
+
* at minTurns of user/assistant on a LONG thread, and never drop the last
|
|
1385
|
+
* user ask. minTurns must not empty the bind prefix — a short AUTO thread
|
|
1386
|
+
* binds early turns and may forward a tail with fewer than minTurns.
|
|
1360
1387
|
*/
|
|
1361
1388
|
export function cutTranscript(msgs, knobs = {}) {
|
|
1362
1389
|
const k = sanitizeKnobs({ ...envKnobs(), ...knobs });
|
|
@@ -1398,21 +1425,38 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1398
1425
|
}
|
|
1399
1426
|
if (tailStart > cut) cut = tailStart;
|
|
1400
1427
|
|
|
1428
|
+
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1429
|
+
|
|
1430
|
+
// minTurns may still size the tail on a long thread. If walking earlier
|
|
1431
|
+
// would empty the bind prefix (the old firstSpillable+1 fallback), keep a
|
|
1432
|
+
// non-empty prefix and allow fewer than minTurns in the tail. On a short
|
|
1433
|
+
// thread that cannot satisfy minTurns at all, pin the last ask so early
|
|
1434
|
+
// user/assistant turns bind instead of riding in the forwarded tail.
|
|
1401
1435
|
if (countRealTurns(msgs, cut) < minTurns) {
|
|
1436
|
+
let moved = false;
|
|
1402
1437
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1403
|
-
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1404
|
-
|
|
1438
|
+
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1439
|
+
cut = i;
|
|
1440
|
+
moved = true;
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (!moved && lastUser > firstSpillable && isSeverable(msgs, lastUser, firstSpillable)) {
|
|
1445
|
+
cut = lastUser;
|
|
1405
1446
|
}
|
|
1406
1447
|
}
|
|
1407
1448
|
|
|
1408
|
-
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1409
1449
|
if (lastUser > firstSpillable && cut > lastUser) cut = lastUser;
|
|
1410
1450
|
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1451
|
+
// 2-real-turn floor protects the last user ask: never drop it, and expand
|
|
1452
|
+
// the tail to two turns on a LONG thread. Do not steal the first
|
|
1453
|
+
// user+assistant pair from the prefix just to pad a short tail.
|
|
1413
1454
|
if (lastUser > firstSpillable && countRealTurns(msgs, cut) < 2) {
|
|
1414
1455
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1415
|
-
if (isSeverable(msgs, i, firstSpillable)
|
|
1456
|
+
if (!isSeverable(msgs, i, firstSpillable) || countRealTurns(msgs, i) < 2) continue;
|
|
1457
|
+
if (countRealTurns(msgs, firstSpillable, i) < 2) continue;
|
|
1458
|
+
cut = i;
|
|
1459
|
+
break;
|
|
1416
1460
|
}
|
|
1417
1461
|
if (cut > lastUser) cut = lastUser;
|
|
1418
1462
|
}
|
|
@@ -1420,6 +1464,121 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1420
1464
|
return { cut, firstSpillable, lastUser, knobs: k };
|
|
1421
1465
|
}
|
|
1422
1466
|
|
|
1467
|
+
/** Opening slice used as a content-anchor when no session header is sent. */
|
|
1468
|
+
export const SPILL_CONTENT_ANCHOR_CHARS = 2048;
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Find a memoized / ledger bind for this request. Prefers an explicit
|
|
1472
|
+
* session key; otherwise matches a stored prefix so a growing content-anchor
|
|
1473
|
+
* (grokui AUTO sends no session header) still recalls the same contextId.
|
|
1474
|
+
*/
|
|
1475
|
+
export function lookupSpillMemo(spillMemo, sessionLedger, { sessionKey, corpus } = {}) {
|
|
1476
|
+
if (sessionKey && spillMemo?.has(sessionKey)) {
|
|
1477
|
+
return { key: sessionKey, source: 'memo', ...spillMemo.get(sessionKey) };
|
|
1478
|
+
}
|
|
1479
|
+
if (sessionKey && sessionLedger?.has(sessionKey)) {
|
|
1480
|
+
const led = sessionLedger.get(sessionKey);
|
|
1481
|
+
if (led?.contextId) return { key: sessionKey, source: 'ledger', restored: !led.corpus, ...led };
|
|
1482
|
+
}
|
|
1483
|
+
if (corpus && spillMemo) {
|
|
1484
|
+
for (const [k, v] of spillMemo) {
|
|
1485
|
+
if (typeof v?.corpus === 'string' && v.corpus.length && corpus.startsWith(v.corpus)) {
|
|
1486
|
+
return { key: k, source: 'memo-prefix', ...v };
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const opening = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
|
|
1490
|
+
for (const [k, v] of spillMemo) {
|
|
1491
|
+
if (typeof k !== 'string' || k.startsWith('sid:')) continue;
|
|
1492
|
+
if (opening.startsWith(k) || (k && k.startsWith(opening))) {
|
|
1493
|
+
return { key: k, source: 'memo-anchor', ...v };
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
export function rememberSpillMemo(spillMemo, key, entry, { max = 32 } = {}) {
|
|
1501
|
+
if (!spillMemo || !key) return;
|
|
1502
|
+
spillMemo.set(key, entry);
|
|
1503
|
+
while (spillMemo.size > max) spillMemo.delete(spillMemo.keys().next().value);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Decide first-bind vs later-turn recall. No I/O — bindCorpus is injected
|
|
1508
|
+
* by the caller (or mocked in tests).
|
|
1509
|
+
*
|
|
1510
|
+
* cold-bind — fire-and-forget first bind; this turn may go unspilled
|
|
1511
|
+
* await-pending — later turn while that bind is in flight; await, then tail
|
|
1512
|
+
* recall — contextId known; send tail (+ optional delta append)
|
|
1513
|
+
*/
|
|
1514
|
+
export function planConversationBind({ sessionKey, corpus, spillMemo, sessionLedger } = {}) {
|
|
1515
|
+
const key = sessionKey || (corpus ? corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS) : null);
|
|
1516
|
+
const prior = lookupSpillMemo(spillMemo, sessionLedger, { sessionKey: key, corpus });
|
|
1517
|
+
if (!prior) {
|
|
1518
|
+
return { action: 'cold-bind', send: 'full', key, corpus };
|
|
1519
|
+
}
|
|
1520
|
+
if (prior.pending && !prior.contextId) {
|
|
1521
|
+
return {
|
|
1522
|
+
action: 'await-pending',
|
|
1523
|
+
send: 'tail',
|
|
1524
|
+
key: prior.key || key,
|
|
1525
|
+
corpus,
|
|
1526
|
+
ready: prior.ready,
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
if (prior.contextId) {
|
|
1530
|
+
let delta = '';
|
|
1531
|
+
let append = false;
|
|
1532
|
+
if (prior.restored) {
|
|
1533
|
+
append = true;
|
|
1534
|
+
} else if (typeof prior.corpus === 'string' && corpus.startsWith(prior.corpus) && corpus.length > prior.corpus.length) {
|
|
1535
|
+
delta = corpus.slice(prior.corpus.length);
|
|
1536
|
+
append = true;
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
action: 'recall',
|
|
1540
|
+
send: 'tail',
|
|
1541
|
+
key: prior.key || key,
|
|
1542
|
+
corpus,
|
|
1543
|
+
contextId: prior.contextId,
|
|
1544
|
+
hash: prior.hash,
|
|
1545
|
+
reused: true,
|
|
1546
|
+
append,
|
|
1547
|
+
delta,
|
|
1548
|
+
restored: Boolean(prior.restored),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
return { action: 'cold-bind', send: 'full', key, corpus };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Cut the transcript and plan the conversation bind. Tests use this instead
|
|
1556
|
+
* of standing up the proxy or a live gateway.
|
|
1557
|
+
*/
|
|
1558
|
+
export function planTranscriptSpill(msgs, {
|
|
1559
|
+
knobs,
|
|
1560
|
+
sessionKey,
|
|
1561
|
+
spillMemo,
|
|
1562
|
+
sessionLedger,
|
|
1563
|
+
corpusChars = 0,
|
|
1564
|
+
adapt = false,
|
|
1565
|
+
persist = false,
|
|
1566
|
+
...cutOpts
|
|
1567
|
+
} = {}) {
|
|
1568
|
+
const adapted = applySpillCut(msgs, {
|
|
1569
|
+
knobs,
|
|
1570
|
+
corpusChars,
|
|
1571
|
+
adapt,
|
|
1572
|
+
persist,
|
|
1573
|
+
...cutOpts,
|
|
1574
|
+
});
|
|
1575
|
+
const empty = { ...adapted, corpus: '', bindPlan: null };
|
|
1576
|
+
if (adapted.cut <= adapted.firstSpillable) return empty;
|
|
1577
|
+
const corpus = msgs.slice(adapted.firstSpillable, adapted.cut).map(msgText).filter(Boolean).join('\n\n');
|
|
1578
|
+
const bindPlan = planConversationBind({ sessionKey, corpus, spillMemo, sessionLedger });
|
|
1579
|
+
return { ...adapted, corpus, bindPlan };
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1423
1582
|
function stubForCut(msgs, cut, opts) {
|
|
1424
1583
|
return stubBoundFileResults(msgs, {
|
|
1425
1584
|
boundFiles: opts.boundFiles,
|
package/lib/x402.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
* payTo: "<wallet>", resource, description, maxTimeoutSeconds,
|
|
23
23
|
* extra: { facilitator, feePayer, symbol, billedUsd, tokenUsd,
|
|
24
24
|
* pricedAt, pricing: "markup"|"counterfactual",
|
|
25
|
-
*
|
|
25
|
+
* billedUsd, directUsd?, savedUsd?, savesVsDirect?,
|
|
26
26
|
* acquire?: { method: "spl-token-wrap", steps: WRAP_ACQUIRE_STEPS } } } ],
|
|
27
27
|
* error: "payment required",
|
|
28
28
|
* help: "Wrap ix has 9 accounts. Program CPIs the deposit — do not send a separate TransferChecked. 0x6a = NotEnoughAccounts (old 5-account wrap is dead)." }
|
|
@@ -232,12 +232,21 @@ export function receiptLine(accept, settle) {
|
|
|
232
232
|
// reaches the spill threshold, so there is nothing to compress and nothing
|
|
233
233
|
// to save — which is worth saying out loud, because the fix on the caller's
|
|
234
234
|
// side is to BIND a corpus, not to change models.
|
|
235
|
-
const
|
|
235
|
+
const billed = Number(x.billedUsd);
|
|
236
|
+
const direct = x.directUsd != null ? Number(x.directUsd) : null;
|
|
237
|
+
const saved = x.savedUsd != null ? Number(x.savedUsd)
|
|
238
|
+
: (Number.isFinite(billed) && Number.isFinite(direct) ? Math.max(0, direct - billed) : null);
|
|
239
|
+
const ratio = x.savesVsDirect != null ? Number(x.savesVsDirect)
|
|
240
|
+
: (Number.isFinite(billed) && billed > 0 && Number.isFinite(direct) ? direct / billed : null);
|
|
241
|
+
// Wallet path: OpenRouter price, plus 33% of savings vs direct when any.
|
|
242
|
+
// Never print extra.markup — that field is leftover 3× and is not the quote.
|
|
236
243
|
const saves = ratio != null
|
|
237
244
|
? (ratio >= 1.05
|
|
238
245
|
? ` (${ratio.toFixed(1)}× cheaper than direct)`
|
|
239
|
-
: ' (at
|
|
240
|
-
: (
|
|
246
|
+
: ' (at OpenRouter price — nothing to compress; bind a corpus to save)')
|
|
247
|
+
: (Number.isFinite(saved) && saved > 0
|
|
248
|
+
? ` ($${saved.toFixed(4)} saved vs direct)`
|
|
249
|
+
: ' (at OpenRouter price — short body)');
|
|
241
250
|
const tx = settle?.transaction || settle?.txHash || settle?.signature;
|
|
242
251
|
const rail = railOf(accept);
|
|
243
252
|
return `paid ${usd}${saves}${rail ? ` · rail ${rail}` : ''}${tx ? ` · tx ${tx}` : ''}`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
4
|
-
"description": "Local x402-paying proxy + MCP server for openzoo.fun
|
|
3
|
+
"version": "0.49.6",
|
|
4
|
+
"description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"node": ">=18"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node --test test/*.test.js"
|
|
19
|
+
"test": "node --test test/*.test.js && node scripts/assert-grokui-pin.mjs"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|