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/proxy.js
CHANGED
|
@@ -15,6 +15,8 @@ import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
|
|
|
15
15
|
import {
|
|
16
16
|
loadBoundChars, noteCorpusLedger, filesForCorpus, readFilesForCorpus, boundAbsFromKeys,
|
|
17
17
|
createSpillStats, corpusCharsForSend, applySpillCut, msgText, hudDollarX,
|
|
18
|
+
spillPricedLine,
|
|
19
|
+
planConversationBind, rememberSpillMemo, SPILL_CONTENT_ANCHOR_CHARS,
|
|
18
20
|
} from './spill.js';
|
|
19
21
|
import { rewritablePath, augmentModelList, ALIAS_IDS, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS } from './models.js';
|
|
20
22
|
import { forgetContext } from './contexts.js';
|
|
@@ -26,7 +28,7 @@ import { loadSessionSpend, saveSessionSpend } from './session.js';
|
|
|
26
28
|
import { creditBalance, quotedPrices } from './info.js';
|
|
27
29
|
import { subscriptionPublicView } from './subscription.js';
|
|
28
30
|
import { priceHoldings } from './livestatus.js';
|
|
29
|
-
import { receiptUsedCogs } from './racesettle.js';
|
|
31
|
+
import { receiptUsedCogs, receiptDirectUsd } from './racesettle.js';
|
|
30
32
|
import { rewriteWrapClientError } from './wrap.js';
|
|
31
33
|
|
|
32
34
|
const HOP_BY_HOP = new Set([
|
|
@@ -423,11 +425,6 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
423
425
|
});
|
|
424
426
|
};
|
|
425
427
|
|
|
426
|
-
if (msgs.length < 6) {
|
|
427
|
-
bindFilesInBackground('conversation under 6 messages, background');
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
428
|
// LIVE SELF-TUNER. Env knobs seed the first cut; after cut+stub the proxy
|
|
432
429
|
// scores the HUD dollar multiple (spill direct/billed) and retunes
|
|
433
430
|
// keep/min-turns/budget (stubMore for SEARCH, not live file bodies)
|
|
@@ -488,35 +485,23 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
488
485
|
//
|
|
489
486
|
// FILES RIDE THE BACKGROUND, NEVER THE CRITICAL PATH.
|
|
490
487
|
//
|
|
491
|
-
// The
|
|
492
|
-
//
|
|
493
|
-
// file bytes into
|
|
494
|
-
//
|
|
495
|
-
// bind endpoint measures 0.34-0.48s on a 613KB corpus, and that is 0.34-0.48s
|
|
496
|
-
// the user waits before a single token appears.
|
|
488
|
+
// The first conversation bind is also fire-and-forget: turn 1 may go out
|
|
489
|
+
// unspilled while the bind runs, then later turns recall a tail + contextId.
|
|
490
|
+
// Folding file bytes into a synchronous first bind used to put a 400KB
|
|
491
|
+
// upload in front of the caller's turn (0.34-0.48s on a 613KB corpus).
|
|
497
492
|
//
|
|
498
|
-
// Nothing recalls a file during the turn that read it � the model already has
|
|
493
|
+
// Nothing recalls a file during the turn that read it � the model already has
|
|
499
494
|
// the tool result in its window. Files are only worth having bound for the
|
|
500
|
-
// NEXT ask
|
|
501
|
-
// the fact, off the clock.
|
|
495
|
+
// NEXT ask, so they append after the conversation bind completes.
|
|
502
496
|
const turns = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
|
|
503
497
|
const corpus = turns;
|
|
504
|
-
if (!sessionKey) sessionKey = corpus.slice(0,
|
|
498
|
+
if (!sessionKey) sessionKey = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
|
|
505
499
|
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// OBSERVED on a live session that read files all turn and never produced a
|
|
512
|
-
// corpus, because its conversation stayed under the threshold the whole time.
|
|
513
|
-
//
|
|
514
|
-
// A file is worth binding on its own merit. So when the turns are too small
|
|
515
|
-
// to spill but files exist, bind the files anyway � in the background,
|
|
516
|
-
// against this session's context � and let this turn go unspilled. The corpus
|
|
517
|
-
// is then waiting for the next ask.
|
|
518
|
-
if (corpus.length <= BIND_MIN_CHARS) {
|
|
519
|
-
bindFilesInBackground('conversation under spill threshold, background');
|
|
500
|
+
// Conversation prefix binds at any size. BIND_MIN_CHARS only gates the
|
|
501
|
+
// one-shot corpus+question path in maybeCacheCorpus � a real but small
|
|
502
|
+
// early-turn prefix used to be discarded here ("only large ones").
|
|
503
|
+
if (!corpus) {
|
|
504
|
+
bindFilesInBackground('empty conversation prefix, files only');
|
|
520
505
|
return null;
|
|
521
506
|
}
|
|
522
507
|
|
|
@@ -524,7 +509,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
524
509
|
//
|
|
525
510
|
// A transcript grows by one message per turn, so the whole-corpus hash misses
|
|
526
511
|
// every time and the old code re-uploaded the ENTIRE prefix on every single
|
|
527
|
-
// turn
|
|
512
|
+
// turn � OBSERVED live: 0.4MB bound three turns running, a fresh context id
|
|
528
513
|
// each time, while only a few KB was actually new. Bind cost grew with
|
|
529
514
|
// conversation length and was re-paid per message.
|
|
530
515
|
//
|
|
@@ -537,7 +522,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
537
522
|
// The anchor was the first 2KB of corpus, which works only because a
|
|
538
523
|
// transcript's opening never changes. It is fragile in exactly the cases that
|
|
539
524
|
// matter: two sessions that open identically (same system block, same first
|
|
540
|
-
// instruction
|
|
525
|
+
// instruction � the norm for an agent) collide onto ONE bound context and
|
|
541
526
|
// interleave their histories, and any edit near the top of a transcript
|
|
542
527
|
// silently orphans the binding and re-uploads the whole thing.
|
|
543
528
|
//
|
|
@@ -545,60 +530,109 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
545
530
|
// back to the content anchor when it is not. Same memo, better key.
|
|
546
531
|
// CAPTURED FROM A LIVE claude-cli/2.1.232 REQUEST, not guessed. The first
|
|
547
532
|
// version of this checked x-session-id / x-claude-session-id /
|
|
548
|
-
// metadata.user_id
|
|
533
|
+
// metadata.user_id � none of which Claude Code sends, so it silently fell
|
|
549
534
|
// back to the content anchor on every request and the feature did nothing.
|
|
550
535
|
// The real header list is:
|
|
551
536
|
// anthropic-beta, anthropic-version, x-app, x-claude-code-session-id,
|
|
552
537
|
// x-stainless-*
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
538
|
+
//
|
|
539
|
+
// COLD-BIND. Turn 1 may go unspilled (full messages, no x-hrr-context) while
|
|
540
|
+
// the first bind runs in the background � same spirit as file-bind, so the
|
|
541
|
+
// opening ask is not stalled. The in-flight/completed bind is memoized on
|
|
542
|
+
// the session key; later turns recall a tail + contextId. Subsequent
|
|
543
|
+
// appends stay fire-and-forget deltas.
|
|
544
|
+
let bindPlan = planConversationBind({
|
|
545
|
+
sessionKey,
|
|
546
|
+
corpus,
|
|
547
|
+
spillMemo,
|
|
548
|
+
sessionLedger,
|
|
549
|
+
});
|
|
550
|
+
const anchor = bindPlan.key || sessionKey;
|
|
551
|
+
|
|
552
|
+
if (bindPlan.action === 'cold-bind') {
|
|
553
|
+
const ready = bindCorpus(corpus, {
|
|
554
|
+
onStage: (stage, info) => {
|
|
555
|
+
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory (background)...`);
|
|
556
|
+
},
|
|
557
|
+
}).then((b) => {
|
|
558
|
+
if (!b?.contextId) return b;
|
|
559
|
+
noteCorpusLedger(boundChars, {
|
|
560
|
+
contextId: b.contextId,
|
|
561
|
+
reused: false,
|
|
562
|
+
corpusChars: corpus.length,
|
|
563
|
+
deltaChars: 0,
|
|
564
|
+
fileChars: 0,
|
|
565
|
+
...ledgerOpts(),
|
|
566
|
+
});
|
|
567
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, contextId: b.contextId, hash: b.hash });
|
|
568
|
+
bindFilesInBackground('background', { appendTo: b.contextId, asAppend: true });
|
|
569
|
+
return b;
|
|
570
|
+
}).catch((e) => {
|
|
571
|
+
log(`bind failed (turn went unspilled): ${e.message}`);
|
|
572
|
+
const cur = spillMemo.get(anchor);
|
|
573
|
+
if (cur?.pending) spillMemo.delete(anchor);
|
|
574
|
+
return null;
|
|
575
|
+
});
|
|
576
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, pending: true, ready });
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (bindPlan.action === 'await-pending') {
|
|
581
|
+
const finished = await bindPlan.ready.catch((e) => {
|
|
582
|
+
log(`bind failed (history may lag one turn): ${e.message}`);
|
|
583
|
+
return null;
|
|
584
|
+
});
|
|
585
|
+
if (!finished?.contextId) {
|
|
586
|
+
bindFilesInBackground('first bind failed, files only');
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
bindPlan = planConversationBind({
|
|
590
|
+
sessionKey: anchor,
|
|
591
|
+
corpus,
|
|
592
|
+
spillMemo,
|
|
593
|
+
sessionLedger,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (bindPlan.action !== 'recall' || !bindPlan.contextId) {
|
|
598
|
+
bindFilesInBackground('no context yet, files only');
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
558
602
|
let bind;
|
|
559
603
|
let deltaChars = 0;
|
|
560
604
|
let appended = false;
|
|
561
|
-
if (
|
|
605
|
+
if (bindPlan.restored && bindPlan.contextId) {
|
|
562
606
|
// Sidecar came back up: we still know the context_id and the accumulated
|
|
563
607
|
// char count, but not the prior prefix string, so we cannot slice a delta.
|
|
564
608
|
// Re-append the current prefix (some overlap is harmless) and keep the
|
|
565
|
-
// restored ledger � do not add corpus.length again.
|
|
609
|
+
// restored ledger � do not add corpus.length again.
|
|
566
610
|
void bindCorpus(corpus, {
|
|
567
|
-
appendTo:
|
|
611
|
+
appendTo: bindPlan.contextId,
|
|
568
612
|
onStage: (stage, info) => {
|
|
569
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${
|
|
613
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (restored session, background)`);
|
|
570
614
|
},
|
|
571
615
|
}).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
|
|
572
616
|
appended = true;
|
|
573
|
-
bind = { contextId:
|
|
574
|
-
} else if (
|
|
575
|
-
const delta =
|
|
617
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
|
|
618
|
+
} else if (bindPlan.append && bindPlan.delta) {
|
|
619
|
+
const delta = bindPlan.delta;
|
|
576
620
|
deltaChars = delta.length;
|
|
577
|
-
// FIRE AND FORGET. This delta is history for FUTURE turns � the answer
|
|
621
|
+
// FIRE AND FORGET. This delta is history for FUTURE turns � the answer
|
|
578
622
|
// being generated right now is served from the tail plus what is already
|
|
579
623
|
// bound, so waiting on the upload buys nothing and costs the user the
|
|
580
624
|
// round trip on every single turn. The context id is already known, so
|
|
581
625
|
// nothing is lost by not waiting for it.
|
|
582
|
-
//
|
|
583
|
-
// The FIRST bind is deliberately NOT async: the request must carry
|
|
584
|
-
// x-hrr-context, and that id does not exist until the bind returns. Firing
|
|
585
|
-
// that one off would send the opening turn with no context at all � a
|
|
586
|
-
// silently worse answer traded for a shorter pause, which is the wrong way
|
|
587
|
-
// round.
|
|
588
626
|
void bindCorpus(delta, {
|
|
589
|
-
appendTo:
|
|
627
|
+
appendTo: bindPlan.contextId,
|
|
590
628
|
onStage: (stage, info) => {
|
|
591
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${
|
|
629
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (delta, background)`);
|
|
592
630
|
},
|
|
593
631
|
}).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
|
|
594
632
|
appended = true;
|
|
595
|
-
bind = { contextId:
|
|
633
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: delta.length };
|
|
596
634
|
} else {
|
|
597
|
-
bind =
|
|
598
|
-
onStage: (stage, info) => {
|
|
599
|
-
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
|
|
600
|
-
},
|
|
601
|
-
});
|
|
635
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
|
|
602
636
|
}
|
|
603
637
|
// CONVERSATION LEDGER � every successful bind AND append, not only when
|
|
604
638
|
// files exist. First bind initializes to the bound corpus size; each append
|
|
@@ -618,8 +652,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
618
652
|
// uploads each version exactly once no matter how often the agent re-reads it.
|
|
619
653
|
// Read + readdir are inside setImmediate � they must not run before send().
|
|
620
654
|
bindFilesInBackground('background', { appendTo: bind.contextId, asAppend: true });
|
|
621
|
-
spillMemo
|
|
622
|
-
if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
|
|
655
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
|
|
623
656
|
// Count the tail that is actually forwarded after stub/trim � older
|
|
624
657
|
// continue-turn rounds after the ask may have been dropped, so
|
|
625
658
|
// msgs.length - cut would keep lastSend growing with the raw pile.
|
|
@@ -793,7 +826,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
793
826
|
say(`session restored: $${sessionSpent.toFixed(6)} � ${paidCalls} paid call${paidCalls === 1 ? '' : 's'}`);
|
|
794
827
|
}
|
|
795
828
|
process.on('exit', rememberSpend);
|
|
796
|
-
const MARKUP = 3; // confirmed constant, see .claude/wiki.md "Margin needs a like-for-like denominator"
|
|
797
829
|
const noteQuote = (x) => {
|
|
798
830
|
const billed = Number(x?.billedUsd);
|
|
799
831
|
if (!Number.isFinite(billed) || billed < 0) return;
|
|
@@ -1519,30 +1551,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1519
1551
|
if (paid && receipt) {
|
|
1520
1552
|
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
|
1521
1553
|
sessionSpent += receipt.billedUsd;
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
// still the ceiling those get capped against.
|
|
1527
|
-
// Prefer the gateway's own cogsUsd. Deriving it as billedUsd/MARKUP
|
|
1528
|
-
// is only correct on a straight-markup call: under counterfactual
|
|
1529
|
-
// pricing billedUsd is min(direct×discount, markupUsd), so the
|
|
1530
|
-
// division understates cost and overstates margin.
|
|
1531
|
-
sessionCogs += receiptUsedCogs(receipt, MARKUP);
|
|
1554
|
+
// Wallet path: no 3x. Prefer extra.cogsUsd / billedUsd / directUsd /
|
|
1555
|
+
// savedUsd from the 402. billedUsd is the OpenRouter price (plus
|
|
1556
|
+
// zoo's 33% of savings when the caller beat direct).
|
|
1557
|
+
sessionCogs += receiptUsedCogs(receipt);
|
|
1532
1558
|
noteQuote(receipt);
|
|
1533
1559
|
// direct = what answering this WITHOUT the zoo would have cost. On an
|
|
1534
1560
|
// attach call that is the whole bound corpus, which is why it can be
|
|
1535
|
-
// orders of magnitude above what was billed. directUsd
|
|
1536
|
-
//
|
|
1561
|
+
// orders of magnitude above what was billed. Read extra.directUsd /
|
|
1562
|
+
// extra.savedUsd; do not invent billed * 3.
|
|
1537
1563
|
if (didSpill) {
|
|
1538
1564
|
spill.spillSpend += receipt.billedUsd || 0;
|
|
1539
|
-
spill.spillDirect +=
|
|
1565
|
+
spill.spillDirect += receiptDirectUsd(receipt);
|
|
1540
1566
|
}
|
|
1541
|
-
sessionDirect +=
|
|
1542
|
-
? receipt.directUsd
|
|
1543
|
-
: typeof receipt.savesVsDirect === 'number'
|
|
1544
|
-
? receipt.savesVsDirect * receipt.billedUsd
|
|
1545
|
-
: receipt.billedUsd;
|
|
1567
|
+
sessionDirect += receiptDirectUsd(receipt);
|
|
1546
1568
|
// The public-URL ceiling meters only public-origin spend — your own
|
|
1547
1569
|
// local calls never eat into it.
|
|
1548
1570
|
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
|
@@ -1599,23 +1621,19 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1599
1621
|
if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
|
|
1600
1622
|
const x = data.x402;
|
|
1601
1623
|
sessionSpent += x.billedUsd;
|
|
1602
|
-
sessionCogs += receiptUsedCogs(x
|
|
1624
|
+
sessionCogs += receiptUsedCogs(x);
|
|
1603
1625
|
noteQuote(x);
|
|
1604
|
-
sessionDirect +=
|
|
1626
|
+
sessionDirect += receiptDirectUsd(x);
|
|
1605
1627
|
if (didSpill) {
|
|
1606
1628
|
// THE number that settles why a spilled call did or did not save:
|
|
1607
1629
|
// the gateway only prices a counterfactual when corpusTokens >
|
|
1608
1630
|
// promptTokens, so a tail that rivals the corpus silently falls
|
|
1609
|
-
// back to
|
|
1610
|
-
|
|
1611
|
-
//
|
|
1612
|
-
|
|
1613
|
-
// 'corpus ?' on calls that were pricing fine — which sent a whole
|
|
1614
|
-
// night's debugging after a number that was never the input.
|
|
1615
|
-
const basis = x.counterfactualTokensUsed ?? lc.corpusTokens;
|
|
1616
|
-
log(`spill priced: ${x.pricing} · basis ${basis ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x.billedUsd ?? 0).toFixed(5)} direct ${(x.directUsd ?? 0).toFixed(5)}`);
|
|
1631
|
+
// back to at-cost and direct collapses onto billed.
|
|
1632
|
+
// Tell-line prints the gateway's actual counterfactual only.
|
|
1633
|
+
// Never fall back to lecore.corpusTokens (often == tokensBefore).
|
|
1634
|
+
log(spillPricedLine(x));
|
|
1617
1635
|
spill.spillSpend += x.billedUsd;
|
|
1618
|
-
spill.spillDirect +=
|
|
1636
|
+
spill.spillDirect += receiptDirectUsd(x);
|
|
1619
1637
|
}
|
|
1620
1638
|
paidCalls += 1;
|
|
1621
1639
|
if (viaTunnel) tunnelSpent += x.billedUsd;
|
|
@@ -1665,15 +1683,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1665
1683
|
const meterStreamed = (x) => {
|
|
1666
1684
|
if (paid || typeof x?.billedUsd !== 'number') return;
|
|
1667
1685
|
sessionSpent += x.billedUsd;
|
|
1668
|
-
sessionCogs += receiptUsedCogs(x
|
|
1686
|
+
sessionCogs += receiptUsedCogs(x);
|
|
1669
1687
|
noteQuote(x);
|
|
1670
|
-
sessionDirect +=
|
|
1688
|
+
sessionDirect += receiptDirectUsd(x);
|
|
1671
1689
|
if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; billedWithActual += x.billedUsd || 0; }
|
|
1672
1690
|
if (didSpill) {
|
|
1673
|
-
|
|
1674
|
-
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)}`);
|
|
1691
|
+
log(spillPricedLine(x, { streamed: true }));
|
|
1675
1692
|
spill.spillSpend += x.billedUsd;
|
|
1676
|
-
spill.spillDirect +=
|
|
1693
|
+
spill.spillDirect += receiptDirectUsd(x);
|
|
1677
1694
|
}
|
|
1678
1695
|
paidCalls += 1;
|
|
1679
1696
|
if (viaTunnel) tunnelSpent += x.billedUsd;
|
package/lib/racesettle.js
CHANGED
|
@@ -8,6 +8,62 @@
|
|
|
8
8
|
|
|
9
9
|
export const FLY_GATEWAY_HOST = 'x402-tokens.fly.dev';
|
|
10
10
|
export const RACE_NO_CREDIT = '(race: not enough prepaid credit — shrink N or top up, rather than fire on $0)';
|
|
11
|
+
/** Low end of the pitched 5–10× vs frontier. Session green HUD below this recuts Y. */
|
|
12
|
+
export const RACE_HUD_TARGET = 5;
|
|
13
|
+
|
|
14
|
+
const TIER_DOWN = {
|
|
15
|
+
'grok4.6': 'medium',
|
|
16
|
+
grok46: 'medium',
|
|
17
|
+
expensive: 'medium',
|
|
18
|
+
medium: 'cheap',
|
|
19
|
+
cheap: 'cheap',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function cheaperRaceTier(tier) {
|
|
23
|
+
const t = String(tier || 'medium');
|
|
24
|
+
return TIER_DOWN[t] || 'cheap';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Same number the HUD green `x` uses: direct/spent. */
|
|
28
|
+
export function sessionDollarX({ dollarX, spentUsd, directUsd } = {}) {
|
|
29
|
+
const given = Number(dollarX);
|
|
30
|
+
if (Number.isFinite(given) && given > 0) return given;
|
|
31
|
+
const spent = Number(spentUsd);
|
|
32
|
+
const direct = Number(directUsd);
|
|
33
|
+
if (spent > 0 && Number.isFinite(direct)) return direct / spent;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Recut launched Y (and maybe drop a band) when session green HUD is thin.
|
|
39
|
+
* Assumes the current multiple already includes this Y tax, so implied
|
|
40
|
+
* single-model x ≈ dollarX × y. Need (X) scales with Y. No user refunds.
|
|
41
|
+
*
|
|
42
|
+
* 2.09x on a 4-racer → implied ~8.4x single → Y=1 (back in the 5–10× band).
|
|
43
|
+
*/
|
|
44
|
+
export function recutRaceByHud({
|
|
45
|
+
y, need = 1, dollarX, tier = 'medium', target = RACE_HUD_TARGET,
|
|
46
|
+
} = {}) {
|
|
47
|
+
const launched = Math.max(1, Math.floor(Number(y) || 1));
|
|
48
|
+
const k = Math.max(1, Math.min(Math.floor(Number(need) || 1), launched));
|
|
49
|
+
const x = Number(dollarX);
|
|
50
|
+
const band = String(tier || 'medium');
|
|
51
|
+
if (!Number.isFinite(x) || x <= 0 || x >= target) {
|
|
52
|
+
return { y: launched, need: k, tier: band, recut: false, reason: null };
|
|
53
|
+
}
|
|
54
|
+
const impliedSingle = x * launched;
|
|
55
|
+
const maxY = Math.max(1, Math.min(launched, Math.floor(impliedSingle / target)));
|
|
56
|
+
const nextTier = impliedSingle < target ? cheaperRaceTier(band) : band;
|
|
57
|
+
const nextNeed = Math.max(1, Math.min(k, maxY));
|
|
58
|
+
const recut = maxY < launched || nextTier !== band;
|
|
59
|
+
return {
|
|
60
|
+
y: maxY,
|
|
61
|
+
need: nextNeed,
|
|
62
|
+
tier: nextTier,
|
|
63
|
+
recut,
|
|
64
|
+
reason: recut ? 'savings' : null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
11
67
|
|
|
12
68
|
const FLY_RE = /x402-tokens\.fly\.dev/i;
|
|
13
69
|
|
|
@@ -99,13 +155,29 @@ export function capRaceByCredit(n, { creditUsd, quoteUsd } = {}) {
|
|
|
99
155
|
* House cost from the receipt. Do not subtract race_unused — unused
|
|
100
156
|
* grant-back is not a user refund, and shrinking cogs would hide house loss.
|
|
101
157
|
* Does not clamp to billed — HUD embers when cogs exceed what was paid.
|
|
158
|
+
*
|
|
159
|
+
* Wallet path: there is no 3× markup. Prefer the gateway's cogsUsd; otherwise
|
|
160
|
+
* billedUsd (OpenRouter price, plus zoo's 33% of savings when the 402 has any).
|
|
102
161
|
*/
|
|
103
|
-
export function receiptUsedCogs(x
|
|
162
|
+
export function receiptUsedCogs(x) {
|
|
104
163
|
if (!x || typeof x !== 'object') return 0;
|
|
105
|
-
const billedRaw = Number(x.billedUsd);
|
|
106
|
-
const billedOk = Number.isFinite(billedRaw) && billedRaw >= 0;
|
|
107
164
|
if (typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)) return x.cogsUsd;
|
|
108
|
-
|
|
165
|
+
const billedRaw = Number(x.billedUsd);
|
|
166
|
+
return Number.isFinite(billedRaw) && billedRaw >= 0 ? billedRaw : 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Counterfactual from the 402: extra.directUsd, else billed + savedUsd. */
|
|
170
|
+
export function receiptDirectUsd(x) {
|
|
171
|
+
if (typeof x?.directUsd === 'number' && Number.isFinite(x.directUsd)) return x.directUsd;
|
|
172
|
+
const billed = Number(x?.billedUsd);
|
|
173
|
+
const billedOk = Number.isFinite(billed) && billed >= 0;
|
|
174
|
+
if (typeof x?.savedUsd === 'number' && Number.isFinite(x.savedUsd) && billedOk) {
|
|
175
|
+
return billed + x.savedUsd;
|
|
176
|
+
}
|
|
177
|
+
if (typeof x?.savesVsDirect === 'number' && Number.isFinite(x.savesVsDirect) && billedOk) {
|
|
178
|
+
return x.savesVsDirect * billed;
|
|
179
|
+
}
|
|
180
|
+
return billedOk ? billed : 0;
|
|
109
181
|
}
|
|
110
182
|
|
|
111
183
|
/**
|
|
@@ -113,12 +185,10 @@ export function receiptUsedCogs(x, markup = 3) {
|
|
|
113
185
|
* first-call rewrite, never a race_unused user refund.
|
|
114
186
|
* cogs is the house cost on that receipt (HUD embers when cogs > spent).
|
|
115
187
|
*/
|
|
116
|
-
export function meterRaceReceipt(x
|
|
188
|
+
export function meterRaceReceipt(x) {
|
|
117
189
|
const billed = Number(x?.billedUsd);
|
|
118
190
|
const spentUsd = Number.isFinite(billed) ? billed : 0;
|
|
119
|
-
|
|
120
|
-
const direct = typeof x?.directUsd === 'number' ? x.directUsd : spentUsd;
|
|
121
|
-
return { spentUsd, cogsUsd: usedCogs, directUsd: direct };
|
|
191
|
+
return { spentUsd, cogsUsd: receiptUsedCogs(x), directUsd: receiptDirectUsd(x) };
|
|
122
192
|
}
|
|
123
193
|
|
|
124
194
|
export function inferRaceTier(models, fallback = 'medium') {
|