openzoo 0.49.5 → 0.49.7
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/boxes.js +1 -1
- package/lib/demo.js +4 -1
- package/lib/grokui.mjs +177 -5
- package/lib/gui.html +4 -1
- package/lib/mcp.js +3 -1
- package/lib/models.js +27 -5
- package/lib/pay.js +3 -0
- package/lib/proxy.js +168 -118
- package/lib/racesettle.js +22 -8
- package/lib/spill.js +445 -25
- package/lib/x402.js +13 -4
- package/package.json +3 -3
package/lib/proxy.js
CHANGED
|
@@ -14,7 +14,11 @@ 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
|
+
spillPricedLine,
|
|
19
|
+
planConversationBind, rememberSpillMemo, SPILL_CONTENT_ANCHOR_CHARS,
|
|
20
|
+
corpusRecall,
|
|
21
|
+
decideChatSpill, isOneShotCorpusAsk,
|
|
18
22
|
} from './spill.js';
|
|
19
23
|
import { rewritablePath, augmentModelList, ALIAS_IDS, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS } from './models.js';
|
|
20
24
|
import { forgetContext } from './contexts.js';
|
|
@@ -26,7 +30,7 @@ import { loadSessionSpend, saveSessionSpend } from './session.js';
|
|
|
26
30
|
import { creditBalance, quotedPrices } from './info.js';
|
|
27
31
|
import { subscriptionPublicView } from './subscription.js';
|
|
28
32
|
import { priceHoldings } from './livestatus.js';
|
|
29
|
-
import { receiptUsedCogs } from './racesettle.js';
|
|
33
|
+
import { receiptUsedCogs, receiptDirectUsd } from './racesettle.js';
|
|
30
34
|
import { rewriteWrapClientError } from './wrap.js';
|
|
31
35
|
|
|
32
36
|
const HOP_BY_HOP = new Set([
|
|
@@ -423,51 +427,67 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
423
427
|
});
|
|
424
428
|
};
|
|
425
429
|
|
|
426
|
-
if (msgs.length < 6) {
|
|
427
|
-
bindFilesInBackground('conversation under 6 messages, background');
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
430
|
// LIVE SELF-TUNER. Env knobs seed the first cut; after cut+stub the proxy
|
|
432
431
|
// scores the HUD dollar multiple (spill direct/billed) and retunes
|
|
433
432
|
// keep/min-turns/budget (stubMore for SEARCH, not live file bodies)
|
|
434
433
|
// in process memory so this request recuts when the green x is under 10.
|
|
435
434
|
// No restart. OPENZOO_ADAPT=0 freezes the env defaults. The ask always
|
|
436
435
|
// stays; we never drop below 2 real user/assistant turns to delete it.
|
|
436
|
+
//
|
|
437
|
+
// 1-model AND raced grokui AUTO both land here (same POST /chat/completions
|
|
438
|
+
// door). The old `msgs.length < 6` bail skipped fat 1-model hops � a 40k
|
|
439
|
+
// command-output turn with 4�5 messages never bound, so spent?direct.
|
|
440
|
+
// decideChatSpill is the shared gate: oversized ? bind prefix + system/tail
|
|
441
|
+
// + x-hrr-context; small ? passthrough. Race fields do not change it.
|
|
437
442
|
const knownLedger = (sessionKey && spillMemo.get(sessionKey))
|
|
438
443
|
|| (sessionKey && sessionLedger.get(sessionKey))
|
|
439
444
|
|| null;
|
|
440
445
|
const knownChars = knownLedger?.contextId
|
|
441
446
|
? (boundChars.get(knownLedger.contextId) || 0)
|
|
442
447
|
: 0;
|
|
443
|
-
const
|
|
448
|
+
const decision = decideChatSpill(body, {
|
|
444
449
|
corpusChars: knownChars,
|
|
445
450
|
boundAbs: previouslyBoundAbs,
|
|
451
|
+
recall: corpusRecall(typeof knownLedger?.corpus === 'string' ? knownLedger.corpus : ''),
|
|
446
452
|
log,
|
|
447
453
|
persist: true,
|
|
448
454
|
dollarX: extra.dollarX ?? hudDollarX(stats || {}),
|
|
449
455
|
lastSend: extra.lastSend,
|
|
456
|
+
minPrefixChars: BIND_MIN_CHARS,
|
|
450
457
|
});
|
|
451
|
-
if (
|
|
458
|
+
if (decision.mode === 'passthrough') {
|
|
459
|
+
bindFilesInBackground(decision.reason === 'no-cut'
|
|
460
|
+
? 'no severable cut, files only'
|
|
461
|
+
: 'conversation under spill threshold, background');
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
if (decision.mode === 'stub-only') {
|
|
452
465
|
bindFilesInBackground('no severable cut, files only');
|
|
453
466
|
// Still forward stubbed/trimmed bodies so an un-severable storm is not
|
|
454
467
|
// shipped at full size just because cutTranscript could not move.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
468
|
+
return {
|
|
469
|
+
body: Buffer.from(JSON.stringify({ ...body, messages: decision.forwarded })),
|
|
470
|
+
corpus: '',
|
|
471
|
+
reused: false,
|
|
472
|
+
savedBytes: 0,
|
|
473
|
+
sent: decision.forwarded.length,
|
|
474
|
+
msgs: msgs.length,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// oneshot is handled in maybeCacheCorpus; if we reach it here, bind the
|
|
478
|
+
// extracted corpus the same way as a transcript prefix.
|
|
479
|
+
if (decision.mode === 'oneshot' && !decision.adapted) {
|
|
480
|
+
decision.adapted = {
|
|
481
|
+
cut: msgs.length - 1,
|
|
482
|
+
firstSpillable: Math.max(0, msgs.findIndex((m) => m?.role !== 'system')),
|
|
483
|
+
stubbed: { messages: decision.forwarded, stubbed: 0, dropped: 0 },
|
|
484
|
+
};
|
|
466
485
|
}
|
|
486
|
+
const adapted = decision.adapted;
|
|
467
487
|
const { cut, firstSpillable } = adapted;
|
|
468
488
|
const stubbed = adapted.stubbed;
|
|
469
489
|
|
|
470
|
-
const head = msgs.slice(0, firstSpillable); // system block, always kept
|
|
490
|
+
const head = decision.head.length ? decision.head : msgs.slice(0, firstSpillable); // system block, always kept
|
|
471
491
|
// EVERY FILE THE AGENT TOUCHED, AT FULL SIZE.
|
|
472
492
|
//
|
|
473
493
|
// The saving ratio is corpus/sent, so on a fresh session — where the corpus
|
|
@@ -488,35 +508,24 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
488
508
|
//
|
|
489
509
|
// FILES RIDE THE BACKGROUND, NEVER THE CRITICAL PATH.
|
|
490
510
|
//
|
|
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.
|
|
511
|
+
// The first conversation bind is also fire-and-forget: turn 1 may go out
|
|
512
|
+
// unspilled while the bind runs, then later turns recall a tail + contextId.
|
|
513
|
+
// Folding file bytes into a synchronous first bind used to put a 400KB
|
|
514
|
+
// upload in front of the caller's turn (0.34-0.48s on a 613KB corpus).
|
|
497
515
|
//
|
|
498
|
-
// Nothing recalls a file during the turn that read it � the model already has
|
|
516
|
+
// Nothing recalls a file during the turn that read it � the model already has
|
|
499
517
|
// the tool result in its window. Files are only worth having bound for the
|
|
500
|
-
// NEXT ask
|
|
501
|
-
|
|
502
|
-
|
|
518
|
+
// NEXT ask, so they append after the conversation bind completes.
|
|
519
|
+
const turns = decision.prefix
|
|
520
|
+
|| msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
|
|
503
521
|
const corpus = turns;
|
|
504
|
-
if (!sessionKey) sessionKey = corpus.slice(0,
|
|
522
|
+
if (!sessionKey) sessionKey = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
|
|
505
523
|
|
|
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');
|
|
524
|
+
// Conversation prefix binds at any size. BIND_MIN_CHARS only gates the
|
|
525
|
+
// one-shot corpus+question path in maybeCacheCorpus � a real but small
|
|
526
|
+
// early-turn prefix used to be discarded here ("only large ones").
|
|
527
|
+
if (!corpus) {
|
|
528
|
+
bindFilesInBackground('empty conversation prefix, files only');
|
|
520
529
|
return null;
|
|
521
530
|
}
|
|
522
531
|
|
|
@@ -524,7 +533,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
524
533
|
//
|
|
525
534
|
// A transcript grows by one message per turn, so the whole-corpus hash misses
|
|
526
535
|
// every time and the old code re-uploaded the ENTIRE prefix on every single
|
|
527
|
-
// turn
|
|
536
|
+
// turn � OBSERVED live: 0.4MB bound three turns running, a fresh context id
|
|
528
537
|
// each time, while only a few KB was actually new. Bind cost grew with
|
|
529
538
|
// conversation length and was re-paid per message.
|
|
530
539
|
//
|
|
@@ -537,7 +546,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
537
546
|
// The anchor was the first 2KB of corpus, which works only because a
|
|
538
547
|
// transcript's opening never changes. It is fragile in exactly the cases that
|
|
539
548
|
// matter: two sessions that open identically (same system block, same first
|
|
540
|
-
// instruction
|
|
549
|
+
// instruction � the norm for an agent) collide onto ONE bound context and
|
|
541
550
|
// interleave their histories, and any edit near the top of a transcript
|
|
542
551
|
// silently orphans the binding and re-uploads the whole thing.
|
|
543
552
|
//
|
|
@@ -545,60 +554,109 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
545
554
|
// back to the content anchor when it is not. Same memo, better key.
|
|
546
555
|
// CAPTURED FROM A LIVE claude-cli/2.1.232 REQUEST, not guessed. The first
|
|
547
556
|
// version of this checked x-session-id / x-claude-session-id /
|
|
548
|
-
// metadata.user_id
|
|
557
|
+
// metadata.user_id � none of which Claude Code sends, so it silently fell
|
|
549
558
|
// back to the content anchor on every request and the feature did nothing.
|
|
550
559
|
// The real header list is:
|
|
551
560
|
// anthropic-beta, anthropic-version, x-app, x-claude-code-session-id,
|
|
552
561
|
// x-stainless-*
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
562
|
+
//
|
|
563
|
+
// COLD-BIND. Turn 1 may go unspilled (full messages, no x-hrr-context) while
|
|
564
|
+
// the first bind runs in the background � same spirit as file-bind, so the
|
|
565
|
+
// opening ask is not stalled. The in-flight/completed bind is memoized on
|
|
566
|
+
// the session key; later turns recall a tail + contextId. Subsequent
|
|
567
|
+
// appends stay fire-and-forget deltas.
|
|
568
|
+
let bindPlan = planConversationBind({
|
|
569
|
+
sessionKey,
|
|
570
|
+
corpus,
|
|
571
|
+
spillMemo,
|
|
572
|
+
sessionLedger,
|
|
573
|
+
});
|
|
574
|
+
const anchor = bindPlan.key || sessionKey;
|
|
575
|
+
|
|
576
|
+
if (bindPlan.action === 'cold-bind') {
|
|
577
|
+
const ready = bindCorpus(corpus, {
|
|
578
|
+
onStage: (stage, info) => {
|
|
579
|
+
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory (background)...`);
|
|
580
|
+
},
|
|
581
|
+
}).then((b) => {
|
|
582
|
+
if (!b?.contextId) return b;
|
|
583
|
+
noteCorpusLedger(boundChars, {
|
|
584
|
+
contextId: b.contextId,
|
|
585
|
+
reused: false,
|
|
586
|
+
corpusChars: corpus.length,
|
|
587
|
+
deltaChars: 0,
|
|
588
|
+
fileChars: 0,
|
|
589
|
+
...ledgerOpts(),
|
|
590
|
+
});
|
|
591
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, contextId: b.contextId, hash: b.hash });
|
|
592
|
+
bindFilesInBackground('background', { appendTo: b.contextId, asAppend: true });
|
|
593
|
+
return b;
|
|
594
|
+
}).catch((e) => {
|
|
595
|
+
log(`bind failed (turn went unspilled): ${e.message}`);
|
|
596
|
+
const cur = spillMemo.get(anchor);
|
|
597
|
+
if (cur?.pending) spillMemo.delete(anchor);
|
|
598
|
+
return null;
|
|
599
|
+
});
|
|
600
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, pending: true, ready });
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (bindPlan.action === 'await-pending') {
|
|
605
|
+
const finished = await bindPlan.ready.catch((e) => {
|
|
606
|
+
log(`bind failed (history may lag one turn): ${e.message}`);
|
|
607
|
+
return null;
|
|
608
|
+
});
|
|
609
|
+
if (!finished?.contextId) {
|
|
610
|
+
bindFilesInBackground('first bind failed, files only');
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
bindPlan = planConversationBind({
|
|
614
|
+
sessionKey: anchor,
|
|
615
|
+
corpus,
|
|
616
|
+
spillMemo,
|
|
617
|
+
sessionLedger,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (bindPlan.action !== 'recall' || !bindPlan.contextId) {
|
|
622
|
+
bindFilesInBackground('no context yet, files only');
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
558
626
|
let bind;
|
|
559
627
|
let deltaChars = 0;
|
|
560
628
|
let appended = false;
|
|
561
|
-
if (
|
|
629
|
+
if (bindPlan.restored && bindPlan.contextId) {
|
|
562
630
|
// Sidecar came back up: we still know the context_id and the accumulated
|
|
563
631
|
// char count, but not the prior prefix string, so we cannot slice a delta.
|
|
564
632
|
// Re-append the current prefix (some overlap is harmless) and keep the
|
|
565
|
-
// restored ledger � do not add corpus.length again.
|
|
633
|
+
// restored ledger � do not add corpus.length again.
|
|
566
634
|
void bindCorpus(corpus, {
|
|
567
|
-
appendTo:
|
|
635
|
+
appendTo: bindPlan.contextId,
|
|
568
636
|
onStage: (stage, info) => {
|
|
569
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${
|
|
637
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (restored session, background)`);
|
|
570
638
|
},
|
|
571
639
|
}).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
|
|
572
640
|
appended = true;
|
|
573
|
-
bind = { contextId:
|
|
574
|
-
} else if (
|
|
575
|
-
const delta =
|
|
641
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
|
|
642
|
+
} else if (bindPlan.append && bindPlan.delta) {
|
|
643
|
+
const delta = bindPlan.delta;
|
|
576
644
|
deltaChars = delta.length;
|
|
577
|
-
// FIRE AND FORGET. This delta is history for FUTURE turns � the answer
|
|
645
|
+
// FIRE AND FORGET. This delta is history for FUTURE turns � the answer
|
|
578
646
|
// being generated right now is served from the tail plus what is already
|
|
579
647
|
// bound, so waiting on the upload buys nothing and costs the user the
|
|
580
648
|
// round trip on every single turn. The context id is already known, so
|
|
581
649
|
// 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
650
|
void bindCorpus(delta, {
|
|
589
|
-
appendTo:
|
|
651
|
+
appendTo: bindPlan.contextId,
|
|
590
652
|
onStage: (stage, info) => {
|
|
591
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${
|
|
653
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (delta, background)`);
|
|
592
654
|
},
|
|
593
655
|
}).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
|
|
594
656
|
appended = true;
|
|
595
|
-
bind = { contextId:
|
|
657
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: delta.length };
|
|
596
658
|
} else {
|
|
597
|
-
bind =
|
|
598
|
-
onStage: (stage, info) => {
|
|
599
|
-
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
|
|
600
|
-
},
|
|
601
|
-
});
|
|
659
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
|
|
602
660
|
}
|
|
603
661
|
// CONVERSATION LEDGER � every successful bind AND append, not only when
|
|
604
662
|
// files exist. First bind initializes to the bound corpus size; each append
|
|
@@ -618,8 +676,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
618
676
|
// uploads each version exactly once no matter how often the agent re-reads it.
|
|
619
677
|
// Read + readdir are inside setImmediate � they must not run before send().
|
|
620
678
|
bindFilesInBackground('background', { appendTo: bind.contextId, asAppend: true });
|
|
621
|
-
spillMemo
|
|
622
|
-
if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
|
|
679
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
|
|
623
680
|
// Count the tail that is actually forwarded after stub/trim � older
|
|
624
681
|
// continue-turn rounds after the ask may have been dropped, so
|
|
625
682
|
// msgs.length - cut would keep lastSend growing with the raw pile.
|
|
@@ -671,7 +728,10 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
671
728
|
const topK = Math.max(4, Math.min(12, Math.round(budget / 320)));
|
|
672
729
|
|
|
673
730
|
return {
|
|
674
|
-
body: Buffer.from(JSON.stringify({
|
|
731
|
+
body: Buffer.from(JSON.stringify({
|
|
732
|
+
...body,
|
|
733
|
+
messages: decision.forwarded || [...head, ...stubbed.messages.slice(cut)],
|
|
734
|
+
})),
|
|
675
735
|
topK,
|
|
676
736
|
contextId: bind.contextId,
|
|
677
737
|
hash: bind.hash,
|
|
@@ -707,10 +767,10 @@ async function maybeCacheCorpus(req, bodyBuf, log, stats, extra = {}) {
|
|
|
707
767
|
// the ask verbatim. Anything else (an agent transcript) falls through to the
|
|
708
768
|
// transcript spill, which used to be a silent no-op.
|
|
709
769
|
const last = msgs[msgs.length - 1];
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (!
|
|
770
|
+
// Same gate decideChatSpill uses � 1-model and race share it. zoo_ask
|
|
771
|
+
// stays on the corpus+question bind; everything else (including grokui
|
|
772
|
+
// AUTO, raced or not) falls through to spillTranscript ? decideChatSpill.
|
|
773
|
+
if (!isOneShotCorpusAsk(msgs, BIND_MIN_CHARS)) return spillTranscript(body, log, req, stats, extra);
|
|
714
774
|
const cut = last.content.lastIndexOf('\n\n');
|
|
715
775
|
const corpus = last.content.slice(0, cut);
|
|
716
776
|
const ask = last.content.slice(cut + 2).trim();
|
|
@@ -793,7 +853,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
793
853
|
say(`session restored: $${sessionSpent.toFixed(6)} � ${paidCalls} paid call${paidCalls === 1 ? '' : 's'}`);
|
|
794
854
|
}
|
|
795
855
|
process.on('exit', rememberSpend);
|
|
796
|
-
const MARKUP = 3; // confirmed constant, see .claude/wiki.md "Margin needs a like-for-like denominator"
|
|
797
856
|
const noteQuote = (x) => {
|
|
798
857
|
const billed = Number(x?.billedUsd);
|
|
799
858
|
if (!Number.isFinite(billed) || billed < 0) return;
|
|
@@ -901,7 +960,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
901
960
|
refreshPrices();
|
|
902
961
|
const money = walletMoney();
|
|
903
962
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
963
|
+
// Same package.json the startup banner reads � grokui-app refuses to
|
|
964
|
+
// attach to a leftover :8402 whose version is older than it shipped with.
|
|
965
|
+
const { version } = JSON.parse(
|
|
966
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
967
|
+
);
|
|
904
968
|
res.end(JSON.stringify({
|
|
969
|
+
version,
|
|
905
970
|
spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls,
|
|
906
971
|
creditUsd, chainUsd: money.chainUsd, lastQuoteUsd,
|
|
907
972
|
subscription: subscriptionPublicView(),
|
|
@@ -1519,30 +1584,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1519
1584
|
if (paid && receipt) {
|
|
1520
1585
|
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
|
1521
1586
|
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);
|
|
1587
|
+
// Wallet path: no 3x. Prefer extra.cogsUsd / billedUsd / directUsd /
|
|
1588
|
+
// savedUsd from the 402. billedUsd is the OpenRouter price (plus
|
|
1589
|
+
// zoo's 33% of savings when the caller beat direct).
|
|
1590
|
+
sessionCogs += receiptUsedCogs(receipt);
|
|
1532
1591
|
noteQuote(receipt);
|
|
1533
1592
|
// direct = what answering this WITHOUT the zoo would have cost. On an
|
|
1534
1593
|
// attach call that is the whole bound corpus, which is why it can be
|
|
1535
|
-
// orders of magnitude above what was billed. directUsd
|
|
1536
|
-
//
|
|
1594
|
+
// orders of magnitude above what was billed. Read extra.directUsd /
|
|
1595
|
+
// extra.savedUsd; do not invent billed * 3.
|
|
1537
1596
|
if (didSpill) {
|
|
1538
1597
|
spill.spillSpend += receipt.billedUsd || 0;
|
|
1539
|
-
spill.spillDirect +=
|
|
1598
|
+
spill.spillDirect += receiptDirectUsd(receipt);
|
|
1540
1599
|
}
|
|
1541
|
-
sessionDirect +=
|
|
1542
|
-
? receipt.directUsd
|
|
1543
|
-
: typeof receipt.savesVsDirect === 'number'
|
|
1544
|
-
? receipt.savesVsDirect * receipt.billedUsd
|
|
1545
|
-
: receipt.billedUsd;
|
|
1600
|
+
sessionDirect += receiptDirectUsd(receipt);
|
|
1546
1601
|
// The public-URL ceiling meters only public-origin spend — your own
|
|
1547
1602
|
// local calls never eat into it.
|
|
1548
1603
|
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
|
@@ -1599,23 +1654,19 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1599
1654
|
if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
|
|
1600
1655
|
const x = data.x402;
|
|
1601
1656
|
sessionSpent += x.billedUsd;
|
|
1602
|
-
sessionCogs += receiptUsedCogs(x
|
|
1657
|
+
sessionCogs += receiptUsedCogs(x);
|
|
1603
1658
|
noteQuote(x);
|
|
1604
|
-
sessionDirect +=
|
|
1659
|
+
sessionDirect += receiptDirectUsd(x);
|
|
1605
1660
|
if (didSpill) {
|
|
1606
1661
|
// THE number that settles why a spilled call did or did not save:
|
|
1607
1662
|
// the gateway only prices a counterfactual when corpusTokens >
|
|
1608
1663
|
// 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)}`);
|
|
1664
|
+
// back to at-cost and direct collapses onto billed.
|
|
1665
|
+
// Tell-line prints the gateway's actual counterfactual only.
|
|
1666
|
+
// Never fall back to lecore.corpusTokens (often == tokensBefore).
|
|
1667
|
+
log(spillPricedLine(x));
|
|
1617
1668
|
spill.spillSpend += x.billedUsd;
|
|
1618
|
-
spill.spillDirect +=
|
|
1669
|
+
spill.spillDirect += receiptDirectUsd(x);
|
|
1619
1670
|
}
|
|
1620
1671
|
paidCalls += 1;
|
|
1621
1672
|
if (viaTunnel) tunnelSpent += x.billedUsd;
|
|
@@ -1665,15 +1716,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1665
1716
|
const meterStreamed = (x) => {
|
|
1666
1717
|
if (paid || typeof x?.billedUsd !== 'number') return;
|
|
1667
1718
|
sessionSpent += x.billedUsd;
|
|
1668
|
-
sessionCogs += receiptUsedCogs(x
|
|
1719
|
+
sessionCogs += receiptUsedCogs(x);
|
|
1669
1720
|
noteQuote(x);
|
|
1670
|
-
sessionDirect +=
|
|
1721
|
+
sessionDirect += receiptDirectUsd(x);
|
|
1671
1722
|
if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; billedWithActual += x.billedUsd || 0; }
|
|
1672
1723
|
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)}`);
|
|
1724
|
+
log(spillPricedLine(x, { streamed: true }));
|
|
1675
1725
|
spill.spillSpend += x.billedUsd;
|
|
1676
|
-
spill.spillDirect +=
|
|
1726
|
+
spill.spillDirect += receiptDirectUsd(x);
|
|
1677
1727
|
}
|
|
1678
1728
|
paidCalls += 1;
|
|
1679
1729
|
if (viaTunnel) tunnelSpent += x.billedUsd;
|
package/lib/racesettle.js
CHANGED
|
@@ -155,13 +155,29 @@ export function capRaceByCredit(n, { creditUsd, quoteUsd } = {}) {
|
|
|
155
155
|
* House cost from the receipt. Do not subtract race_unused — unused
|
|
156
156
|
* grant-back is not a user refund, and shrinking cogs would hide house loss.
|
|
157
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).
|
|
158
161
|
*/
|
|
159
|
-
export function receiptUsedCogs(x
|
|
162
|
+
export function receiptUsedCogs(x) {
|
|
160
163
|
if (!x || typeof x !== 'object') return 0;
|
|
161
|
-
const billedRaw = Number(x.billedUsd);
|
|
162
|
-
const billedOk = Number.isFinite(billedRaw) && billedRaw >= 0;
|
|
163
164
|
if (typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)) return x.cogsUsd;
|
|
164
|
-
|
|
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;
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
/**
|
|
@@ -169,12 +185,10 @@ export function receiptUsedCogs(x, markup = 3) {
|
|
|
169
185
|
* first-call rewrite, never a race_unused user refund.
|
|
170
186
|
* cogs is the house cost on that receipt (HUD embers when cogs > spent).
|
|
171
187
|
*/
|
|
172
|
-
export function meterRaceReceipt(x
|
|
188
|
+
export function meterRaceReceipt(x) {
|
|
173
189
|
const billed = Number(x?.billedUsd);
|
|
174
190
|
const spentUsd = Number.isFinite(billed) ? billed : 0;
|
|
175
|
-
|
|
176
|
-
const direct = typeof x?.directUsd === 'number' ? x.directUsd : spentUsd;
|
|
177
|
-
return { spentUsd, cogsUsd: usedCogs, directUsd: direct };
|
|
191
|
+
return { spentUsd, cogsUsd: receiptUsedCogs(x), directUsd: receiptDirectUsd(x) };
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
export function inferRaceTier(models, fallback = 'medium') {
|