openzoo 0.49.6 → 0.49.8

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 CHANGED
@@ -14,11 +14,13 @@ import { evmTokenBalance } from './evm.js';
14
14
  import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
15
15
  import {
16
16
  loadBoundChars, noteCorpusLedger, filesForCorpus, readFilesForCorpus, boundAbsFromKeys,
17
- createSpillStats, corpusCharsForSend, applySpillCut, msgText, hudDollarX,
17
+ createSpillStats, corpusCharsForSend, msgText, hudDollarX,
18
18
  spillPricedLine,
19
19
  planConversationBind, rememberSpillMemo, SPILL_CONTENT_ANCHOR_CHARS,
20
+ corpusRecall,
21
+ decideChatSpill, isOneShotCorpusAsk,
20
22
  } from './spill.js';
21
- import { rewritablePath, augmentModelList, ALIAS_IDS, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS } from './models.js';
23
+ import { rewritablePath, modelsListForRequest, ALIAS_IDS, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS } from './models.js';
22
24
  import { forgetContext } from './contexts.js';
23
25
  import { injectBrief } from './brief.js';
24
26
  import { withNamespace } from './namespace.js';
@@ -28,8 +30,9 @@ import { loadSessionSpend, saveSessionSpend } from './session.js';
28
30
  import { creditBalance, quotedPrices } from './info.js';
29
31
  import { subscriptionPublicView } from './subscription.js';
30
32
  import { priceHoldings } from './livestatus.js';
31
- import { receiptUsedCogs, receiptDirectUsd } from './racesettle.js';
33
+ import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettle.js';
32
34
  import { rewriteWrapClientError } from './wrap.js';
35
+ import { fetchHeaders } from './fetch.js';
33
36
 
34
37
  const HOP_BY_HOP = new Set([
35
38
  'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
@@ -431,40 +434,61 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
431
434
  // in process memory so this request recuts when the green x is under 10.
432
435
  // No restart. OPENZOO_ADAPT=0 freezes the env defaults. The ask always
433
436
  // stays; we never drop below 2 real user/assistant turns to delete it.
437
+ //
438
+ // 1-model AND raced grokui AUTO both land here (same POST /chat/completions
439
+ // door). The old `msgs.length < 6` bail skipped fat 1-model hops � a 40k
440
+ // command-output turn with 4�5 messages never bound, so spent?direct.
441
+ // decideChatSpill is the shared gate: oversized ? bind prefix + system/tail
442
+ // + x-hrr-context; small ? passthrough. Race fields do not change it.
434
443
  const knownLedger = (sessionKey && spillMemo.get(sessionKey))
435
444
  || (sessionKey && sessionLedger.get(sessionKey))
436
445
  || null;
437
446
  const knownChars = knownLedger?.contextId
438
447
  ? (boundChars.get(knownLedger.contextId) || 0)
439
448
  : 0;
440
- const adapted = applySpillCut(msgs, {
449
+ const decision = decideChatSpill(body, {
441
450
  corpusChars: knownChars,
442
451
  boundAbs: previouslyBoundAbs,
452
+ recall: corpusRecall(typeof knownLedger?.corpus === 'string' ? knownLedger.corpus : ''),
443
453
  log,
444
454
  persist: true,
445
455
  dollarX: extra.dollarX ?? hudDollarX(stats || {}),
446
456
  lastSend: extra.lastSend,
457
+ minPrefixChars: BIND_MIN_CHARS,
447
458
  });
448
- if (adapted.cut <= adapted.firstSpillable) {
459
+ if (decision.mode === 'passthrough') {
460
+ bindFilesInBackground(decision.reason === 'no-cut'
461
+ ? 'no severable cut, files only'
462
+ : 'conversation under spill threshold, background');
463
+ return null;
464
+ }
465
+ if (decision.mode === 'stub-only') {
449
466
  bindFilesInBackground('no severable cut, files only');
450
467
  // Still forward stubbed/trimmed bodies so an un-severable storm is not
451
468
  // shipped at full size just because cutTranscript could not move.
452
- if (adapted.stubbed?.dropped || adapted.stubbed?.stubbed) {
453
- return {
454
- body: Buffer.from(JSON.stringify({ ...body, messages: adapted.stubbed.messages })),
455
- corpus: '',
456
- reused: false,
457
- savedBytes: 0,
458
- sent: adapted.stubbed.messages.length,
459
- msgs: msgs.length,
460
- };
461
- }
462
- return null;
469
+ return {
470
+ body: Buffer.from(JSON.stringify({ ...body, messages: decision.forwarded })),
471
+ corpus: '',
472
+ reused: false,
473
+ savedBytes: 0,
474
+ sent: decision.forwarded.length,
475
+ msgs: msgs.length,
476
+ };
463
477
  }
478
+ // oneshot is handled in maybeCacheCorpus; if we reach it here, bind the
479
+ // extracted corpus the same way as a transcript prefix.
480
+ if (decision.mode === 'oneshot' && !decision.adapted) {
481
+ decision.adapted = {
482
+ cut: msgs.length - 1,
483
+ firstSpillable: Math.max(0, msgs.findIndex((m) => m?.role !== 'system')),
484
+ stubbed: { messages: decision.forwarded, stubbed: 0, dropped: 0 },
485
+ };
486
+ }
487
+ const adapted = decision.adapted;
464
488
  const { cut, firstSpillable } = adapted;
465
489
  const stubbed = adapted.stubbed;
466
490
 
467
- const head = msgs.slice(0, firstSpillable); // system block, always kept
491
+ const head = decision.head.length ? decision.head : msgs.slice(0, firstSpillable); // system block, always kept
468
492
  // EVERY FILE THE AGENT TOUCHED, AT FULL SIZE.
469
493
  //
470
494
  // The saving ratio is corpus/sent, so on a fresh session — where the corpus
@@ -493,7 +517,8 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
493
517
  // Nothing recalls a file during the turn that read it � the model already has
494
518
  // the tool result in its window. Files are only worth having bound for the
495
519
  // NEXT ask, so they append after the conversation bind completes.
496
- const turns = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
520
+ const turns = decision.prefix
521
+ || msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
497
522
  const corpus = turns;
498
523
  if (!sessionKey) sessionKey = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
499
524
 
@@ -704,7 +729,10 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
704
729
  const topK = Math.max(4, Math.min(12, Math.round(budget / 320)));
705
730
 
706
731
  return {
707
- body: Buffer.from(JSON.stringify({ ...body, messages: [...head, ...stubbed.messages.slice(cut)] })),
732
+ body: Buffer.from(JSON.stringify({
733
+ ...body,
734
+ messages: decision.forwarded || [...head, ...stubbed.messages.slice(cut)],
735
+ })),
708
736
  topK,
709
737
  contextId: bind.contextId,
710
738
  hash: bind.hash,
@@ -740,10 +768,10 @@ async function maybeCacheCorpus(req, bodyBuf, log, stats, extra = {}) {
740
768
  // the ask verbatim. Anything else (an agent transcript) falls through to the
741
769
  // transcript spill, which used to be a silent no-op.
742
770
  const last = msgs[msgs.length - 1];
743
- const oneShot = typeof last?.content === 'string'
744
- && last.content.length > BIND_MIN_CHARS
745
- && last.content.lastIndexOf('\n\n') >= BIND_MIN_CHARS;
746
- if (!oneShot) return spillTranscript(body, log, req, stats, extra);
771
+ // Same gate decideChatSpill uses 1-model and race share it. zoo_ask
772
+ // stays on the corpus+question bind; everything else (including grokui
773
+ // AUTO, raced or not) falls through to spillTranscript ? decideChatSpill.
774
+ if (!isOneShotCorpusAsk(msgs, BIND_MIN_CHARS)) return spillTranscript(body, log, req, stats, extra);
747
775
  const cut = last.content.lastIndexOf('\n\n');
748
776
  const corpus = last.content.slice(0, cut);
749
777
  const ask = last.content.slice(cut + 2).trim();
@@ -933,7 +961,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
933
961
  refreshPrices();
934
962
  const money = walletMoney();
935
963
  res.writeHead(200, { 'content-type': 'application/json' });
964
+ // Same package.json the startup banner reads � grokui-app refuses to
965
+ // attach to a leftover :8402 whose version is older than it shipped with.
966
+ const { version } = JSON.parse(
967
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
968
+ );
936
969
  res.end(JSON.stringify({
970
+ version,
937
971
  spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls,
938
972
  creditUsd, chainUsd: money.chainUsd, lastQuoteUsd,
939
973
  subscription: subscriptionPublicView(),
@@ -1468,13 +1502,24 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1468
1502
  // rewrite never gets its chance.
1469
1503
  const path = (req.url || '').split('?')[0];
1470
1504
  if (req.method === 'GET' && path === '/v1/models') {
1505
+ // Catalog is chrome, not a paid call. Paying the list would wrap-walk
1506
+ // an empty burner (~4.5s/row) and stall first paint / harness probe.
1471
1507
  try {
1472
- const { response } = await client.fetch(url, init);
1473
- const payload = await response.json();
1474
- res.writeHead(response.status, { 'content-type': 'application/json' });
1475
- res.end(JSON.stringify(response.ok ? augmentModelList(payload) : payload));
1476
- return;
1477
- } catch { /* fall through to the plain relay below */ }
1508
+ const response = await fetchHeaders(url, init);
1509
+ if (response.ok) {
1510
+ const payload = await response.json();
1511
+ // Full zoo catalog � never collapse to a single opus-5 or a claude-*
1512
+ // allowlist. Claude Code (ANTHROPIC_BASE_URL + gateway discovery) reads
1513
+ // data[].id / display_name; OpenAI clients keep object:"list".
1514
+ res.writeHead(200, { 'content-type': 'application/json' });
1515
+ res.end(JSON.stringify(modelsListForRequest(payload, req.headers)));
1516
+ return;
1517
+ }
1518
+ await response.text().catch(() => {});
1519
+ } catch { /* gateway 402/down � serve aliases so chrome still paints */ }
1520
+ res.writeHead(200, { 'content-type': 'application/json' });
1521
+ res.end(JSON.stringify(modelsListForRequest({ object: 'list', data: [] }, req.headers)));
1522
+ return;
1478
1523
  }
1479
1524
  const probe = req.method === 'GET' && /^\/v1\/models\/(.+)$/.exec(path);
1480
1525
  if (probe && ALIAS_IDS.includes(decodeURIComponent(probe[1]))) {
@@ -1598,18 +1643,26 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1598
1643
  // completion already — no extra call, and unlike the account-level
1599
1644
  // /api/v1/credits total it is attributable to THIS proxy even though the
1600
1645
  // same OpenRouter key also pays for ttfx and everything else.
1601
- if (typeof data?.usage?.cost === 'number' && data.usage.cost >= 0) {
1602
- sessionActual += data.usage.cost;
1603
- actualCalls += 1;
1646
+ {
1604
1647
  // PAIR THE NUMERATOR WITH THE DENOMINATOR. sessionSpent is summed on
1605
1648
  // three paths and sessionActual on two, so markupX divided ALL billed
1606
- // by the SUBSET that reported a real cost a 402-receipt call added
1649
+ // by the SUBSET that reported a real cost a 402-receipt call added
1607
1650
  // to billed and nothing to real, and the ratio read 12.55x on a stack
1608
1651
  // running at ~1.0x. Track the billed side of exactly the calls whose
1609
1652
  // cost we actually learned.
1610
1653
  // Both figures ride the SAME response object, so read them together
1611
1654
  // rather than carrying one across sites and hoping the order holds.
1612
- billedWithActual += Number(data?.x402?.billedUsd) || 0;
1655
+ //
1656
+ // x402.billedUsd is often the QUOTE reserve (max_tokens � catalog),
1657
+ // not the settled charge. MEASURED: $0.9858 reserved vs $0.007962
1658
+ // usage.cost -> markupX lied at 124x on a ~1x call. Pair usage.cost
1659
+ // with post-completion billed, never the reserve.
1660
+ const pair = pairActualBilled(data?.x402, data?.usage);
1661
+ if (pair) {
1662
+ sessionActual += pair.upstreamUsd;
1663
+ actualCalls += 1;
1664
+ billedWithActual += pair.billedUsd;
1665
+ }
1613
1666
  }
1614
1667
  // PREPAID CALLS STILL COST MONEY. The block above only meters calls
1615
1668
  // where THIS proxy answered a 402 and paid. When prepaid credit covers
@@ -1681,12 +1734,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1681
1734
  // same figures the JSON path reads out of `data.x402`, same counters, so
1682
1735
  // the status line does not care which transport served the answer.
1683
1736
  const meterStreamed = (x) => {
1737
+ // Same pairing rule as the JSON path: actualUsd / usage.cost with the
1738
+ // settled billed twin, even on a wallet-paid stream (do not skip just
1739
+ // because `paid` already recorded the quote-time receipt).
1740
+ const pair = pairActualBilled(x, x?.usage);
1741
+ if (pair) {
1742
+ sessionActual += pair.upstreamUsd;
1743
+ actualCalls += 1;
1744
+ billedWithActual += pair.billedUsd;
1745
+ }
1684
1746
  if (paid || typeof x?.billedUsd !== 'number') return;
1685
1747
  sessionSpent += x.billedUsd;
1686
1748
  sessionCogs += receiptUsedCogs(x);
1687
1749
  noteQuote(x);
1688
1750
  sessionDirect += receiptDirectUsd(x);
1689
- if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; billedWithActual += x.billedUsd || 0; }
1690
1751
  if (didSpill) {
1691
1752
  log(spillPricedLine(x, { streamed: true }));
1692
1753
  spill.spillSpend += x.billedUsd;
package/lib/racesettle.js CHANGED
@@ -180,6 +180,133 @@ export function receiptDirectUsd(x) {
180
180
  return billedOk ? billed : 0;
181
181
  }
182
182
 
183
+ /** Zoo's share of (direct − OpenRouter) when the 402 has real savings. */
184
+ export const SAVINGS_SHARE = 0.33;
185
+ /**
186
+ * billed / usage.cost above this, without settled house cogs, is the
187
+ * max_tokens quote reserve — not the charge after completion.
188
+ * MEASURED 2026-08-19: $0.9858 reserved / $0.007962 usage.cost ≈ 124× on a ~1× call.
189
+ */
190
+ export const QUOTE_RESERVE_X = 2;
191
+
192
+ function money(v) {
193
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
194
+ }
195
+
196
+ function closeRatio(a, b, rel = 0.25) {
197
+ const den = Math.max(b, 1e-12);
198
+ return Math.abs(a - b) / den <= rel;
199
+ }
200
+
201
+ /**
202
+ * After-completion billed fields the gateway may already put on x402 / usage.
203
+ * Read only if present — do not invent them on the wire.
204
+ */
205
+ function explicitSettledBilled(x, usage) {
206
+ const bags = [x, usage, x?.used, x?.settled, x?.receipt, usage?.used, usage?.settled]
207
+ .filter((o) => o && typeof o === 'object' && !Array.isArray(o));
208
+ const keys = [
209
+ 'billedActual', 'billedActualUsd', 'settledUsd', 'settledBilledUsd',
210
+ 'chargedUsd', 'usedUsd', 'actualBilledUsd',
211
+ ];
212
+ for (const bag of bags) {
213
+ for (const k of keys) {
214
+ const n = money(bag[k]);
215
+ if (n != null) return n;
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /** tokens actually used × unit prices, only when both sides are already on the object. */
222
+ function billedFromUsedTokens(x, usage) {
223
+ const prompt = money(usage?.prompt_tokens ?? usage?.promptTokens ?? x?.prompt_tokens);
224
+ const completion = money(usage?.completion_tokens ?? usage?.completionTokens ?? x?.completion_tokens);
225
+ const inPrice = money(x?.promptPriceUsd ?? x?.inputPriceUsd ?? x?.priceInUsd);
226
+ const outPrice = money(x?.completionPriceUsd ?? x?.outputPriceUsd ?? x?.priceOutUsd);
227
+ if (prompt != null && completion != null && inPrice != null && outPrice != null) {
228
+ return prompt * inPrice + completion * outPrice;
229
+ }
230
+ return null;
231
+ }
232
+
233
+ /**
234
+ * True when `billed` is the quote-time max_tokens ceiling, not the settled charge.
235
+ * A large billed/cost is honest when cogs already matches usage.cost (33% of
236
+ * real savings). The 124× lie is billed >> cost with at-cost / reserved cogs.
237
+ */
238
+ export function isQuoteReserveBilled(billed, cost, x = {}) {
239
+ if (money(billed) == null || money(cost) == null) return false;
240
+ if (cost === 0) return billed > 0;
241
+ if (billed <= cost * QUOTE_RESERVE_X) return false;
242
+ const cogs = money(x?.cogsUsd);
243
+ if (cogs != null && closeRatio(cogs, cost)) return false;
244
+ const saved = money(x?.savedUsd);
245
+ if (saved == null || saved <= cost * 0.5) return true;
246
+ if (cogs != null && cogs > cost * QUOTE_RESERVE_X) return true;
247
+ return true;
248
+ }
249
+
250
+ /**
251
+ * Post-completion billed USD to pair with usage.cost / x.actualUsd.
252
+ * `x.billedUsd` is often the quote reserve (max_tokens × catalog), which made
253
+ * HUD markupX read 124× on a ~1× call. Prefer a settled field; otherwise
254
+ * reconstruct from tokens used × price or from usage.cost (+ 33% of settled
255
+ * savings). Never return the reserve when we learned the real upstream cost.
256
+ */
257
+ export function receiptSettledBilled(x, usage) {
258
+ const cost = money(usage?.cost) ?? money(x?.actualUsd) ?? money(usage?.actualUsd);
259
+ const explicit = explicitSettledBilled(x, usage);
260
+ if (explicit != null) return explicit;
261
+
262
+ const fromTokens = billedFromUsedTokens(x, usage);
263
+ if (fromTokens != null) {
264
+ const billed = money(x?.billedUsd);
265
+ if (billed == null || isQuoteReserveBilled(billed, fromTokens, x)
266
+ || (cost != null && isQuoteReserveBilled(billed, cost, x))) {
267
+ return fromTokens;
268
+ }
269
+ return billed;
270
+ }
271
+
272
+ const billed = money(x?.billedUsd);
273
+ if (billed != null && (cost == null || !isQuoteReserveBilled(billed, cost, x))) {
274
+ return billed;
275
+ }
276
+ if (cost != null) {
277
+ const saved = settledSavedUsd(x, cost);
278
+ return cost + SAVINGS_SHARE * saved;
279
+ }
280
+ return billed ?? 0;
281
+ }
282
+
283
+ function settledSavedUsd(x, cost) {
284
+ const saved = money(x?.savedUsd);
285
+ if (saved == null || saved <= 0) return 0;
286
+ const cogs = money(x?.cogsUsd);
287
+ // Quote-time savedUsd rides the same max_tokens reserve. Only keep it when
288
+ // house cost already matches the metered upstream (settled cogs).
289
+ if (cogs != null && closeRatio(cogs, cost)) return saved;
290
+ return 0;
291
+ }
292
+
293
+ /**
294
+ * Pair the HUD denominator (real upstream) with the post-completion billed
295
+ * twin. Null when this call did not report a real cost — do not mix populations.
296
+ */
297
+ export function pairActualBilled(x402, usage) {
298
+ const fromUsage = money(usage?.cost);
299
+ const fromX = money(x402?.actualUsd) ?? money(x402?.usage?.cost);
300
+ const upstreamUsd = fromUsage ?? fromX;
301
+ if (upstreamUsd == null) return null;
302
+ const bag = x402 && typeof x402 === 'object' ? x402 : {};
303
+ const usageBag = usage && typeof usage === 'object' ? usage : {};
304
+ return {
305
+ upstreamUsd,
306
+ billedUsd: receiptSettledBilled(bag, { ...usageBag, cost: upstreamUsd }),
307
+ };
308
+ }
309
+
183
310
  /**
184
311
  * Session meter. spent/direct are the receipt totals as billed — never a
185
312
  * first-call rewrite, never a race_unused user refund.