openzoo 0.48.61 → 0.48.63
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/lib/proxy.js +98 -13
- package/package.json +1 -1
package/lib/proxy.js
CHANGED
|
@@ -119,6 +119,9 @@ const spillMemo = new Map();
|
|
|
119
119
|
// path:mtime of every file already bound — a file is bound once per version,
|
|
120
120
|
// never re-uploaded because the agent read it again.
|
|
121
121
|
const boundFiles = new Set();
|
|
122
|
+
// context_id -> total chars BOUND (turns + files appended). The counterfactual
|
|
123
|
+
// basis must reflect what the corpus actually holds, not just this turn's slice.
|
|
124
|
+
const boundChars = new Map();
|
|
122
125
|
|
|
123
126
|
/**
|
|
124
127
|
* Every fundable balance across all three chains, for the startup line and
|
|
@@ -519,10 +522,56 @@ async function spillTranscript(body, log, req) {
|
|
|
519
522
|
return out.join('\n\n');
|
|
520
523
|
};
|
|
521
524
|
|
|
525
|
+
// FILES RIDE THE BACKGROUND, NEVER THE CRITICAL PATH.
|
|
526
|
+
//
|
|
527
|
+
// The FIRST bind of a session is necessarily synchronous — the request cannot
|
|
528
|
+
// go until the context_id exists, because it travels as x-hrr-context. Folding
|
|
529
|
+
// file bytes into that bind put a 400KB upload in front of the caller's turn,
|
|
530
|
+
// which is the cold-bind stall this whole exercise was meant to remove: the
|
|
531
|
+
// bind endpoint measures 0.34-0.48s on a 613KB corpus, and that is 0.34-0.48s
|
|
532
|
+
// the user waits before a single token appears.
|
|
533
|
+
//
|
|
534
|
+
// Nothing recalls a file during the turn that read it — the model already has
|
|
535
|
+
// the tool result in its window. Files are only worth having bound for the
|
|
536
|
+
// NEXT ask. So the conversation binds inline and the files are appended after
|
|
537
|
+
// the fact, off the clock.
|
|
522
538
|
const turns = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
|
|
523
539
|
const files = filesForCorpus();
|
|
524
|
-
const corpus =
|
|
525
|
-
|
|
540
|
+
const corpus = turns;
|
|
541
|
+
|
|
542
|
+
// FILES BIND EVEN WHEN THE CONVERSATION IS TOO SMALL TO SPILL.
|
|
543
|
+
//
|
|
544
|
+
// The threshold exists to stop us binding a two-line chat — it was never
|
|
545
|
+
// meant to gate FILES. But bailing here skipped them entirely, so a fresh
|
|
546
|
+
// session that reads a 200KB file bound nothing and scored 1.00x forever:
|
|
547
|
+
// OBSERVED on a live session that read files all turn and never produced a
|
|
548
|
+
// corpus, because its conversation stayed under the threshold the whole time.
|
|
549
|
+
//
|
|
550
|
+
// A file is worth binding on its own merit. So when the turns are too small
|
|
551
|
+
// to spill but files exist, bind the files anyway — in the background,
|
|
552
|
+
// against this session's context — and let this turn go unspilled. The corpus
|
|
553
|
+
// is then waiting for the next ask.
|
|
554
|
+
const sessionId = req?.headers?.['x-claude-code-session-id']
|
|
555
|
+
|| req?.headers?.['x-session-id']
|
|
556
|
+
|| req?.headers?.['x-claude-session-id']
|
|
557
|
+
|| (typeof body?.metadata?.user_id === 'string' ? body.metadata.user_id : null);
|
|
558
|
+
const sessionKey = sessionId ? `sid:${sessionId}` : corpus.slice(0, 2048);
|
|
559
|
+
if (corpus.length <= BIND_MIN_CHARS) {
|
|
560
|
+
if (files) {
|
|
561
|
+
const known = spillMemo.get(sessionKey);
|
|
562
|
+
void bindCorpus(files, {
|
|
563
|
+
appendTo: known?.contextId || null,
|
|
564
|
+
onStage: (stage, info) => {
|
|
565
|
+
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of FILES (conversation under spill threshold, background)`);
|
|
566
|
+
},
|
|
567
|
+
}).then((b) => {
|
|
568
|
+
if (!b?.contextId) return;
|
|
569
|
+
boundChars.set(b.contextId, (boundChars.get(b.contextId) || 0) + files.length);
|
|
570
|
+
if (!known) spillMemo.set(sessionKey, { corpus, contextId: b.contextId, hash: b.hash });
|
|
571
|
+
}).catch((e) => log(`file bind failed: ${e.message}`));
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
526
575
|
|
|
527
576
|
// CONTINUE THE CONTEXT, BIND ONLY THE DELTA.
|
|
528
577
|
//
|
|
@@ -554,11 +603,7 @@ async function spillTranscript(body, log, req) {
|
|
|
554
603
|
// The real header list is:
|
|
555
604
|
// anthropic-beta, anthropic-version, x-app, x-claude-code-session-id,
|
|
556
605
|
// x-stainless-*
|
|
557
|
-
const
|
|
558
|
-
|| req?.headers?.['x-session-id']
|
|
559
|
-
|| req?.headers?.['x-claude-session-id']
|
|
560
|
-
|| (typeof body?.metadata?.user_id === 'string' ? body.metadata.user_id : null);
|
|
561
|
-
const anchor = sessionId ? `sid:${sessionId}` : corpus.slice(0, 2048);
|
|
606
|
+
const anchor = sessionKey;
|
|
562
607
|
const prior = spillMemo.get(anchor);
|
|
563
608
|
let bind;
|
|
564
609
|
if (prior && corpus.startsWith(prior.corpus) && corpus.length > prior.corpus.length) {
|
|
@@ -588,6 +633,19 @@ async function spillTranscript(body, log, req) {
|
|
|
588
633
|
},
|
|
589
634
|
});
|
|
590
635
|
}
|
|
636
|
+
// APPEND THE FILES AFTER, off the clock. Fire-and-forget against the context
|
|
637
|
+
// we just secured: this turn is already answerable without them, and the next
|
|
638
|
+
// ask gets them for free. `boundFiles` already deduped by path:mtime, so this
|
|
639
|
+
// uploads each version exactly once no matter how often the agent re-reads it.
|
|
640
|
+
if (files) {
|
|
641
|
+
boundChars.set(bind.contextId, (boundChars.get(bind.contextId) || corpus.length) + files.length);
|
|
642
|
+
void bindCorpus(files, {
|
|
643
|
+
appendTo: bind.contextId,
|
|
644
|
+
onStage: (stage, info) => {
|
|
645
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB of FILES to ${bind.contextId} (background)`);
|
|
646
|
+
},
|
|
647
|
+
}).catch((e) => log(`file append failed (corpus lags one turn): ${e.message}`));
|
|
648
|
+
}
|
|
591
649
|
spillMemo.set(anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
|
|
592
650
|
if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
|
|
593
651
|
const sent = msgs.length - cut;
|
|
@@ -771,6 +829,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
771
829
|
// cost can.
|
|
772
830
|
let sessionActual = 0;
|
|
773
831
|
let actualCalls = 0;
|
|
832
|
+
// billed for ONLY those calls whose real cost we learned — the honest
|
|
833
|
+
// numerator for markupX. See the comment at the usage.cost site.
|
|
834
|
+
let billedWithActual = 0;
|
|
774
835
|
// CREDIT, CACHED. Users cannot tell prepaid credit from wallet balance and
|
|
775
836
|
// have to guess whether a call was even paid for ("I don't think x402 made me
|
|
776
837
|
// pay this at all"). The status line runs EVERY turn, so this is refreshed at
|
|
@@ -922,9 +983,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
922
983
|
actual: {
|
|
923
984
|
calls: actualCalls,
|
|
924
985
|
upstreamUsd: Number(sessionActual.toFixed(6)),
|
|
925
|
-
billedUsd: Number(
|
|
926
|
-
marginUsd: Number((
|
|
927
|
-
markupX: sessionActual > 0 ? Number((
|
|
986
|
+
billedUsd: Number(billedWithActual.toFixed(6)),
|
|
987
|
+
marginUsd: Number((billedWithActual - sessionActual).toFixed(6)),
|
|
988
|
+
markupX: sessionActual > 0 ? Number((billedWithActual / sessionActual).toFixed(2)) : null,
|
|
928
989
|
},
|
|
929
990
|
mcp: `${self.replace(/\/v1$/, '')}/mcp`,
|
|
930
991
|
upstream: config.apiBase,
|
|
@@ -1210,6 +1271,21 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1210
1271
|
// Retry of a body we answered seconds ago? Serve the cached completion —
|
|
1211
1272
|
// never pay twice for a harness's reconnect loop.
|
|
1212
1273
|
const isChat = req.method === 'POST' && (req.url || '').includes('/chat/completions');
|
|
1274
|
+
// GROUND TRUTH ON THE OUTGOING BODY. Three sessions have now reported "no
|
|
1275
|
+
// actual question or task from you" while the proxy log showed a healthy
|
|
1276
|
+
// forward, and two rounds of reasoning about the cut were wrong. Log what
|
|
1277
|
+
// is actually in messages[] on the way out — roles, and the tail of the
|
|
1278
|
+
// last user turn — so the question stops being a matter of opinion.
|
|
1279
|
+
if (process.env.OPENZOO_LOG_BODY === '1' && isChat) {
|
|
1280
|
+
try {
|
|
1281
|
+
const b = JSON.parse(bodyBuf.toString('utf8'));
|
|
1282
|
+
const ms = Array.isArray(b?.messages) ? b.messages : [];
|
|
1283
|
+
const roles = ms.map((m) => (m.role || '?')[0]).join('');
|
|
1284
|
+
const lastUser = [...ms].reverse().find((m) => m.role === 'user');
|
|
1285
|
+
const txt = lastUser ? String(msgText(lastUser)).slice(-160).replace(/\s+/g, ' ') : '(NO USER MESSAGE)';
|
|
1286
|
+
log(` OUT roles=${roles} n=${ms.length} lastUser="${txt}"`);
|
|
1287
|
+
} catch { /* not json */ }
|
|
1288
|
+
}
|
|
1213
1289
|
const rKey = isChat ? replayKey(bodyBuf, req.headers) : null;
|
|
1214
1290
|
if (rKey) {
|
|
1215
1291
|
const hit = replayGet(rKey);
|
|
@@ -1295,7 +1371,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1295
1371
|
didSpill = true;
|
|
1296
1372
|
spilledChars += cached.corpus?.length || 0;
|
|
1297
1373
|
if (cached.reused) spillReuses += 1;
|
|
1298
|
-
result = await send(cached.body, cached.contextId, cached.topK, cached.corpus?.length);
|
|
1374
|
+
result = await send(cached.body, cached.contextId, cached.topK, boundChars.get(cached.contextId) || cached.corpus?.length);
|
|
1299
1375
|
// Sidecar wiped between runs: the gateway 404s BEFORE the 402 (nothing
|
|
1300
1376
|
// paid). Never fail on a stale manifest — re-bind once and retry.
|
|
1301
1377
|
if (result.response.status === 404) {
|
|
@@ -1304,7 +1380,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1304
1380
|
log('bound context is gone on the zoo — re-binding once...');
|
|
1305
1381
|
forgetContext(config.apiBase, cached.hash);
|
|
1306
1382
|
const rebound = await bindCorpus(cached.corpus, { force: true });
|
|
1307
|
-
result = await send(cached.body, rebound.contextId, cached.topK, cached.corpus?.length);
|
|
1383
|
+
result = await send(cached.body, rebound.contextId, cached.topK, boundChars.get(rebound.contextId) || cached.corpus?.length);
|
|
1308
1384
|
} else {
|
|
1309
1385
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
1310
1386
|
res.end(text);
|
|
@@ -1378,6 +1454,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1378
1454
|
if (typeof data?.usage?.cost === 'number' && data.usage.cost >= 0) {
|
|
1379
1455
|
sessionActual += data.usage.cost;
|
|
1380
1456
|
actualCalls += 1;
|
|
1457
|
+
// PAIR THE NUMERATOR WITH THE DENOMINATOR. sessionSpent is summed on
|
|
1458
|
+
// three paths and sessionActual on two, so markupX divided ALL billed
|
|
1459
|
+
// by the SUBSET that reported a real cost — a 402-receipt call added
|
|
1460
|
+
// to billed and nothing to real, and the ratio read 12.55x on a stack
|
|
1461
|
+
// running at ~1.0x. Track the billed side of exactly the calls whose
|
|
1462
|
+
// cost we actually learned.
|
|
1463
|
+
// Both figures ride the SAME response object, so read them together
|
|
1464
|
+
// rather than carrying one across sites and hoping the order holds.
|
|
1465
|
+
billedWithActual += Number(data?.x402?.billedUsd) || 0;
|
|
1381
1466
|
}
|
|
1382
1467
|
// PREPAID CALLS STILL COST MONEY. The block above only meters calls
|
|
1383
1468
|
// where THIS proxy answered a 402 and paid. When prepaid credit covers
|
|
@@ -1455,7 +1540,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1455
1540
|
sessionSpent += x.billedUsd;
|
|
1456
1541
|
sessionCogs += typeof x.cogsUsd === 'number' ? x.cogsUsd : x.billedUsd / MARKUP;
|
|
1457
1542
|
sessionDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
|
|
1458
|
-
if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; }
|
|
1543
|
+
if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; billedWithActual += x.billedUsd || 0; }
|
|
1459
1544
|
if (didSpill) {
|
|
1460
1545
|
const lc = x.lecore || {};
|
|
1461
1546
|
log(`spill priced (streamed): ${x.pricing} · basis ${x.counterfactualTokensUsed ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x.billedUsd ?? 0).toFixed(5)} direct ${(x.directUsd ?? 0).toFixed(5)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.63",
|
|
4
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",
|