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/README.md +20 -1
- package/lib/boxes.js +1 -1
- package/lib/grokui.mjs +29 -2
- package/lib/launch.js +31 -68
- package/lib/livestatus.js +2 -2
- package/lib/models.js +113 -5
- package/lib/pay.js +67 -3
- package/lib/podagent.mjs +11 -1
- package/lib/proxy.js +96 -35
- package/lib/racesettle.js +127 -0
- package/lib/spill.js +285 -24
- package/package.json +1 -1
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,
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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 =
|
|
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({
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (!
|
|
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
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|