openzoo 0.49.1 → 0.49.2

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/grokui.mjs CHANGED
@@ -2569,6 +2569,7 @@ async function runTurn(threadId, userText, onEvent, images) {
2569
2569
  return (await brainRace(callMsgs, emit, t.contextId, models, need, undefined, emitStatus, {
2570
2570
  signal: turnAbort.signal,
2571
2571
  onArrivals: (arr) => { t.lastRaceFail = summarizeRaceFailures(arr); },
2572
+ tier: t.tier || 'medium',
2572
2573
  })).trim();
2573
2574
  }
2574
2575
  // A retry draws a DIFFERENT model from the tier rather than the same one.
@@ -4952,9 +4953,14 @@ const APP_HTML = `<!doctype html>
4952
4953
  const cogs = Number(you.cogsUsd) || 0;
4953
4954
  const direct = Number(you.directUsd) || 0;
4954
4955
  const margin = spent > 0 ? Math.round((spent - cogs) / spent * 100) + '%' : '—';
4956
+ const cogsOver = cogs > spent;
4955
4957
  document.getElementById('hYouSpent').textContent = usd(spent);
4956
- document.getElementById('hYouCogs').textContent = usd(cogs);
4957
- document.getElementById('hYouMargin').textContent = margin;
4958
+ const cogsEl = document.getElementById('hYouCogs');
4959
+ cogsEl.textContent = usd(cogs);
4960
+ cogsEl.className = cogsOver ? 'hember' : '';
4961
+ const marginEl = document.getElementById('hYouMargin');
4962
+ marginEl.textContent = margin;
4963
+ marginEl.className = cogsOver ? 'hember' : 'hlime';
4958
4964
  document.getElementById('hYouDirect').textContent = usd(direct);
4959
4965
  const savedEl = document.getElementById('hYouSaved');
4960
4966
  const hintEl = document.getElementById('hHint');
@@ -4967,19 +4973,26 @@ const APP_HTML = `<!doctype html>
4967
4973
  // is shipping the WHOLE corpus), so 2dp would read as noise up there.
4968
4974
  savedEl.textContent = (mult >= 100 ? Math.round(mult) : mult.toFixed(mult >= 10 ? 1 : 2)) + 'x';
4969
4975
  savedEl.className = mult >= 1 ? 'hlime' : 'hember';
4970
- // Under 1x is real, but on its own it just reads as "this is a bad
4971
- // deal". It isn't a verdict on the product it's a verdict on how
4972
- // little you've given it to work against: you're billed on the tokens
4973
- // actually forwarded, while "direct" is the cost of shipping the whole
4974
- // corpus. The forwarded slice grows barely at all as the corpus grows,
4975
- // so the ratio climbs with corpus size. Say that, and say what to do.
4976
- hintEl.className = mult >= 1 ? 'hhint' : 'hhint show';
4977
- hintEl.innerHTML = '<b>feed it more.</b> you\\'re billed on the slice actually sent, '
4978
- + 'not the corpus — so the more you bind, the further ahead this gets. '
4979
- + 'small inputs cost more than sending them straight.';
4976
+ // Session direct/spent (never first-call). Cogs-over-paid is the
4977
+ // louder warn unused grant-back should have kept used cogs billed.
4978
+ if (cogsOver) {
4979
+ hintEl.className = 'hhint show';
4980
+ hintEl.innerHTML = '<b>cogs above paid.</b> our cost exceeded what you were billed '
4981
+ + '— this line is used racers, not the N+judge ceiling.';
4982
+ } else {
4983
+ hintEl.className = mult >= 1 ? 'hhint' : 'hhint show';
4984
+ hintEl.innerHTML = '<b>feed it more.</b> you\\'re billed on the slice actually sent, '
4985
+ + 'not the corpus — so the more you bind, the further ahead this gets. '
4986
+ + 'small inputs cost more than sending them straight.';
4987
+ }
4980
4988
  } else {
4981
4989
  savedEl.textContent = '—';
4982
- hintEl.className = 'hhint';
4990
+ if (cogsOver) {
4991
+ hintEl.className = 'hhint show';
4992
+ hintEl.innerHTML = '<b>cogs above paid.</b> our cost exceeded what you were billed.';
4993
+ } else {
4994
+ hintEl.className = 'hhint';
4995
+ }
4983
4996
  }
4984
4997
  document.getElementById('hFoot').textContent = (you.paidCalls || 0) + ' paid calls this session';
4985
4998
  } catch (e) {
package/lib/podagent.mjs CHANGED
@@ -28,12 +28,19 @@ import {
28
28
  isRaceCountable, raceLastShip, shouldRetryRaceArrival, raceFailKind,
29
29
  summarizeRaceFailures,
30
30
  } from './livestatus.js';
31
+ import {
32
+ probeGatewayRace, capRaceByCredit, inferRaceTier, RACE_NO_CREDIT,
33
+ } from './racesettle.js';
31
34
  import { homedir } from 'node:os';
32
35
 
33
36
  const PORTS = (process.env.OZ_AGENT_PORTS || '1337,6080,1340,6081')
34
37
  .split(',').map((s) => Number(s.trim())).filter(Boolean);
35
38
  const LOG = process.env.OZ_AGENT_LOG || '/var/log/openzoo/agent.jsonl';
36
39
  export const PROXY = process.env.OZ_PROXY || 'http://127.0.0.1:8402/v1';
40
+ /** Live value — tests point OZ_PROXY at a mock after this module loads. */
41
+ function completionsProxy() {
42
+ return process.env.OZ_PROXY || PROXY;
43
+ }
37
44
  export const MODEL = process.env.OZ_BRAIN_MODEL || 'deepseek/deepseek-v4-pro-0813';
38
45
  const MAX_STEPS = Number(process.env.OZ_MAX_STEPS || 10);
39
46
 
@@ -289,7 +296,7 @@ async function postChat(body, contextId, topK, onStatus, signal) {
289
296
  err.name = 'AbortError';
290
297
  throw err;
291
298
  }
292
- r = await fetch(`${PROXY}/chat/completions`, {
299
+ r = await fetch(`${completionsProxy()}/chat/completions`, {
293
300
  method: 'POST',
294
301
  headers: {
295
302
  'content-type': 'application/json', authorization: 'Bearer sk-openzoo',
@@ -649,6 +656,250 @@ export async function tierModels(tier, n = 1, random = false) {
649
656
  return a.slice(0, take);
650
657
  }
651
658
 
659
+ async function readProxySession(proxy = completionsProxy()) {
660
+ try {
661
+ const origin = String(proxy || '').replace(/\/+$/, '').replace(/\/v1$/i, '');
662
+ const r = await fetch(`${origin}/v1/session`, { signal: AbortSignal.timeout(800) });
663
+ if (!r.ok) return null;
664
+ return await r.json();
665
+ } catch { return null; }
666
+ }
667
+
668
+ async function raceBudget(hooks) {
669
+ if (hooks.creditUsd != null || hooks.quoteUsd != null) {
670
+ return { creditUsd: hooks.creditUsd, quoteUsd: hooks.quoteUsd };
671
+ }
672
+ // Injected stream = unit-test N-parallel path. Do not poke :8402.
673
+ if (hooks.stream) return {};
674
+ const s = await readProxySession(hooks.proxy || completionsProxy());
675
+ return {
676
+ creditUsd: s?.creditUsd,
677
+ quoteUsd: s?.lastQuoteUsd ?? s?.quoteUsd,
678
+ };
679
+ }
680
+
681
+ function raceRacerId(obj, fallback) {
682
+ const r = obj?.race || obj?.racer || {};
683
+ if (r.model) return String(r.model);
684
+ if (r.i != null) return `racer-${r.i}`;
685
+ if (obj?.model) return String(obj.model);
686
+ const idx = obj?.choices?.[0]?.index;
687
+ if (idx != null && idx !== 0) return `racer-${idx}`;
688
+ return fallback || 'gateway';
689
+ }
690
+
691
+ function raceEventOf(obj) {
692
+ const r = obj?.race || obj?.racer;
693
+ if (!r || typeof r !== 'object') return null;
694
+ const ev = String(r.event || r.status || '').toLowerCase();
695
+ return { id: raceRacerId(obj, 'gateway'), ev, text: r.text, error: r.error };
696
+ }
697
+
698
+ /**
699
+ * One Fly (or race-capable mock) POST. Streams tokens. First-X-countable:
700
+ * empties / HTTP / pay / fetch-failed do not fill X.
701
+ */
702
+ async function brainGatewayRace(messages, onDelta, contextId, models, need, maxTokens, onStatus, hooks) {
703
+ const n = models.length;
704
+ const want = Math.max(1, Math.min(Number(need) || 1, n));
705
+ const tier = hooks.tier || inferRaceTier(models, 'medium');
706
+ const classify = hooks.classify || classifyRaceAnswer;
707
+ const pairwise = hooks.pairwise || pairwiseTied;
708
+ const minScore = hooks.minScore != null ? Number(hooks.minScore) : RACE_MIN_SCORE;
709
+ const feed = createRaceFeed(onDelta, onStatus, want);
710
+ feed.start();
711
+
712
+ const arrivals = [];
713
+ const done = [];
714
+ const noteRace = (arr) => {
715
+ try {
716
+ hooks.onArrivals?.(arr);
717
+ const line = JSON.stringify({
718
+ at: new Date().toISOString(),
719
+ fail: summarizeRaceFailures(arr),
720
+ n: arr.length,
721
+ kinds: arr.map((a) => raceFailKind(a)),
722
+ door: 'gateway',
723
+ });
724
+ appendFileSync(`${homedir()}/.openzoo/grokui-race.log`, line + '\n');
725
+ } catch { /* diagnostic only */ }
726
+ };
727
+ const ship = (cand) => {
728
+ const out = cand && String(cand.text || '').trim() ? cand : raceLastShip(arrivals);
729
+ feed.settle(out);
730
+ noteRace(arrivals);
731
+ return out.text;
732
+ };
733
+
734
+ const vision = hasImages(messages);
735
+ const model = vision ? VISION_MODEL : (models[0] || MODEL);
736
+ const msgs = vision ? messages : stripImages(messages);
737
+ const budget = maxTokens || MAX_TOKENS;
738
+ const body = {
739
+ model,
740
+ max_tokens: budget,
741
+ messages: withModelId(msgs, model),
742
+ plugins: [{ id: 'web' }],
743
+ stream: true,
744
+ race: n,
745
+ race_need: want,
746
+ tier,
747
+ };
748
+
749
+ let lastFail = { model: 'gateway', text: '', error: 'empty body' };
750
+ for (let attempt = 0; attempt < 2; attempt++) {
751
+ try {
752
+ const r = await postChat(body, contextId, 0, onStatus, hooks.signal);
753
+ if (!r.ok || !r.body) {
754
+ const j = await r.json().catch(() => ({}));
755
+ const content = j?.choices?.[0]?.message?.content;
756
+ const proxied = j?.error?.message;
757
+ const text = content || (r.ok ? '' : (proxied ? `(request failed — HTTP ${r.status}: ${proxied})` : await httpErrorNote(r.status)));
758
+ lastFail = { model: 'gateway', text: text || '', error: r.ok ? undefined : `HTTP ${r.status}` };
759
+ if (isRaceCountable(lastFail)) {
760
+ arrivals.push(lastFail);
761
+ done.push(lastFail);
762
+ feed.onBack();
763
+ if (text) onDelta(text);
764
+ break;
765
+ }
766
+ if (!shouldRetryRaceArrival(lastFail) || attempt === 1) break;
767
+ continue;
768
+ }
769
+
770
+ const parsed = await readGatewayRaceStream(r, feed, hooks.signal);
771
+ for (const a of parsed.arrivals) {
772
+ arrivals.push(a);
773
+ if (isRaceCountable(a)) {
774
+ done.push(a);
775
+ feed.onBack();
776
+ } else {
777
+ feed.onFail(a.model);
778
+ }
779
+ }
780
+ lastFail = parsed.arrivals[parsed.arrivals.length - 1] || lastFail;
781
+ if (done.length >= want || !shouldRetryRaceArrival(lastFail) || attempt === 1) break;
782
+ } catch (e) {
783
+ lastFail = { model: 'gateway', text: '', error: e?.message || 'error' };
784
+ arrivals.push(lastFail);
785
+ if (!shouldRetryRaceArrival(lastFail) || attempt === 1) break;
786
+ }
787
+ }
788
+
789
+ const cands = done.slice(0, want);
790
+ if (!cands.length) return ship(raceLastShip(arrivals));
791
+ if (cands.length === 1) return ship(cands[0]);
792
+
793
+ onStatus?.('judging…');
794
+ const scored = await Promise.all(cands.map(async (c) => {
795
+ let score = 0;
796
+ try { score = Number(await classify(messages, c)) || 0; } catch { score = 0; }
797
+ return { ...c, score };
798
+ }));
799
+ let picked = pickRaceWinner(scored, minScore);
800
+ if (picked.reason === 'tie' && picked.tied.length > 1) {
801
+ let broken = null;
802
+ try { broken = await pairwise(messages, picked.tied); } catch { /* last of the tie */ }
803
+ const usable = broken && String(broken.text || '').trim();
804
+ picked = { winner: usable ? broken : picked.tied[picked.tied.length - 1], reason: 'tiebreak', tied: picked.tied };
805
+ }
806
+ return ship(picked.winner || scored[scored.length - 1] || raceLastShip(arrivals));
807
+ }
808
+
809
+ async function readGatewayRaceStream(r, feed, signal) {
810
+ const reader = r.body.getReader();
811
+ const decoder = new TextDecoder();
812
+ let buf = '';
813
+ const texts = new Map();
814
+ const finished = new Map();
815
+ const live = { id: null };
816
+ const stopWait = startModelWait(() => {});
817
+
818
+ const pushText = (id, chunk) => {
819
+ if (chunk == null || chunk === '') return;
820
+ const key = id || live.id || 'gateway';
821
+ live.id = key;
822
+ texts.set(key, (texts.get(key) || '') + chunk);
823
+ feed.onToken(key, chunk);
824
+ };
825
+ const finishOne = (id, extra = {}) => {
826
+ const key = id || live.id || 'gateway';
827
+ if (finished.has(key)) return;
828
+ const text = extra.text != null ? String(extra.text) : (texts.get(key) || '');
829
+ const row = { model: extra.model || key, text, error: extra.error };
830
+ finished.set(key, row);
831
+ };
832
+
833
+ try {
834
+ for (;;) {
835
+ if (signal?.aborted) break;
836
+ let chunk;
837
+ try {
838
+ chunk = await readWithIdleTimeout(reader, STREAM_IDLE_MS);
839
+ } catch (e) {
840
+ if (e?.code !== 'STREAM_IDLE') throw e;
841
+ try { await reader.cancel(); } catch { /* already */ }
842
+ break;
843
+ }
844
+ const { value, done } = chunk;
845
+ if (done) break;
846
+ buf += decoder.decode(value, { stream: true });
847
+ const lines = buf.split('\n');
848
+ buf = lines.pop();
849
+ for (const line of lines) {
850
+ const s = line.trim();
851
+ if (s.startsWith(': race ')) {
852
+ try {
853
+ const ev = JSON.parse(s.slice(7));
854
+ const id = raceRacerId(ev, 'gateway');
855
+ if (ev.text && ev.event !== 'fail') pushText(id, ev.text);
856
+ if (ev.event === 'token' && ev.delta) pushText(id, ev.delta);
857
+ if (ev.event === 'back' || ev.event === 'done' || ev.finish) {
858
+ finishOne(id, { text: ev.text, model: ev.model });
859
+ }
860
+ if (ev.event === 'fail' || ev.error) {
861
+ finishOne(id, { text: ev.text || '', error: ev.error || 'empty body', model: ev.model });
862
+ }
863
+ } catch { /* keep-alive */ }
864
+ continue;
865
+ }
866
+ if (s.startsWith(': x402 ')) continue; // metered by the sidecar; never log
867
+ if (!s.startsWith('data:')) continue;
868
+ const payload = s.slice(5).trim();
869
+ if (payload === '[DONE]') continue;
870
+ try {
871
+ const obj = JSON.parse(payload);
872
+ const ev = raceEventOf(obj);
873
+ const c = obj?.choices?.[0];
874
+ const d = c?.delta;
875
+ const id = raceRacerId(obj, live.id || 'gateway');
876
+ if (d?.content) pushText(id, d.content);
877
+ else if (d?.reasoning || d?.reasoning_content) { /* thinking — not content */ }
878
+ if (c?.finish_reason) finishOne(id, { model: obj.model });
879
+ if (ev?.ev === 'back' || ev?.ev === 'done') finishOne(ev.id, { text: ev.text, model: ev.id });
880
+ if (ev?.ev === 'fail' || ev?.error) finishOne(ev.id, { text: ev.text || '', error: ev.error || 'empty body' });
881
+ const msg = c?.message?.content || obj?.choices?.[0]?.message?.content;
882
+ if (msg && (c?.finish_reason || obj.object === 'chat.completion')) {
883
+ pushText(id, texts.has(id) ? '' : msg);
884
+ if (!texts.get(id)) texts.set(id, String(msg));
885
+ finishOne(id, { text: texts.get(id) || msg, model: obj.model || id });
886
+ }
887
+ } catch { /* partial JSON */ }
888
+ }
889
+ }
890
+ } finally {
891
+ stopWait();
892
+ try { await reader.cancel(); } catch { /* closed */ }
893
+ }
894
+
895
+ if (!finished.size && texts.size) {
896
+ for (const [id, text] of texts) finishOne(id, { text });
897
+ }
898
+ const arrivals = [...finished.values()];
899
+ if (!arrivals.length) arrivals.push({ model: 'gateway', text: '', error: 'empty body' });
900
+ return { arrivals };
901
+ }
902
+
652
903
  /**
653
904
  * Launch N models at once, judge the FIRST K that come back.
654
905
  *
@@ -693,10 +944,33 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
693
944
  const classify = hooks.classify || classifyRaceAnswer;
694
945
  const pairwise = hooks.pairwise || pairwiseTied;
695
946
  const minScore = hooks.minScore != null ? Number(hooks.minScore) : RACE_MIN_SCORE;
696
- const list = (models || []).filter(Boolean).slice(0, RACE_MAX);
947
+ let list = (models || []).filter(Boolean).slice(0, RACE_MAX);
948
+ const budget = await raceBudget(hooks);
949
+ const capped = capRaceByCredit(list.length, budget);
950
+ if (capped.n < 1) {
951
+ onStatus?.('race refused — no credit');
952
+ return RACE_NO_CREDIT;
953
+ }
954
+ if (capped.n < list.length) {
955
+ list = list.slice(0, capped.n);
956
+ onStatus?.(capped.n < 2 ? 'race shrunk to 1 — credit' : `race shrunk to ${capped.n} — credit`);
957
+ }
697
958
  if (list.length < 2) return stream(messages, onDelta, contextId, list[0], maxTokens, 0, 0, onStatus);
698
959
  const want = Math.max(1, Math.min(Number(need) || 1, list.length));
699
960
 
961
+ // One Fly settle when the completions door honors `race:`. Custom stream
962
+ // hooks (unit tests of the N-parallel judge) keep the old path. Old
963
+ // sidecar / local mock that does not accept race: also stays N-parallel.
964
+ const customStream = Boolean(hooks.stream);
965
+ let gateway = hooks.gatewayRace;
966
+ if (gateway == null && !customStream) {
967
+ try { gateway = await probeGatewayRace(hooks.proxy || completionsProxy(), hooks.fetch || fetch); }
968
+ catch { gateway = false; }
969
+ }
970
+ if (gateway && !customStream) {
971
+ return brainGatewayRace(messages, onDelta, contextId, list, want, maxTokens, onStatus, hooks);
972
+ }
973
+
700
974
  const feed = createRaceFeed(onDelta, onStatus, want);
701
975
  feed.start();
702
976
 
package/lib/proxy.js CHANGED
@@ -26,6 +26,7 @@ import { loadSessionSpend, saveSessionSpend } from './session.js';
26
26
  import { creditBalance, quotedPrices } from './info.js';
27
27
  import { subscriptionPublicView } from './subscription.js';
28
28
  import { priceHoldings } from './livestatus.js';
29
+ import { receiptUsedCogs } from './racesettle.js';
29
30
 
30
31
  const HOP_BY_HOP = new Set([
31
32
  'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
@@ -781,6 +782,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
781
782
  console.log(line);
782
783
  };
783
784
  let sessionSpent = restored.spentUsd;
785
+ let lastQuoteUsd = null;
784
786
  let sessionCogs = restored.cogsUsd;
785
787
  let sessionDirect = restored.directUsd;
786
788
  const rememberSpend = () => {
@@ -791,6 +793,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
791
793
  }
792
794
  process.on('exit', rememberSpend);
793
795
  const MARKUP = 3; // confirmed constant, see .claude/wiki.md "Margin needs a like-for-like denominator"
796
+ const noteQuote = (x) => {
797
+ const billed = Number(x?.billedUsd);
798
+ if (!Number.isFinite(billed) || billed < 0) return;
799
+ const n = Number(x?.race || x?.race_n || 1);
800
+ lastQuoteUsd = n > 1 ? billed / n : billed;
801
+ };
794
802
  let tunnelSpent = 0;
795
803
  // Live balance refresh state — the real implementation is assigned in the
796
804
  // banner section below; the handler only ever calls scheduleRefresh().
@@ -894,7 +902,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
894
902
  res.writeHead(200, { 'content-type': 'application/json' });
895
903
  res.end(JSON.stringify({
896
904
  spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls,
897
- creditUsd, chainUsd: money.chainUsd,
905
+ creditUsd, chainUsd: money.chainUsd, lastQuoteUsd,
898
906
  subscription: subscriptionPublicView(),
899
907
  }));
900
908
  return;
@@ -1519,9 +1527,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1519
1527
  // is only correct on a straight-markup call: under counterfactual
1520
1528
  // pricing billedUsd is min(direct×discount, markupUsd), so the
1521
1529
  // division understates cost and overstates margin.
1522
- sessionCogs += typeof receipt.cogsUsd === 'number'
1523
- ? receipt.cogsUsd
1524
- : receipt.billedUsd / MARKUP;
1530
+ sessionCogs += receiptUsedCogs(receipt, MARKUP);
1531
+ noteQuote(receipt);
1525
1532
  // direct = what answering this WITHOUT the zoo would have cost. On an
1526
1533
  // attach call that is the whole bound corpus, which is why it can be
1527
1534
  // orders of magnitude above what was billed. directUsd is exact and
@@ -1591,7 +1598,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1591
1598
  if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
1592
1599
  const x = data.x402;
1593
1600
  sessionSpent += x.billedUsd;
1594
- sessionCogs += typeof x.cogsUsd === 'number' ? x.cogsUsd : x.billedUsd / MARKUP;
1601
+ sessionCogs += receiptUsedCogs(x, MARKUP);
1602
+ noteQuote(x);
1595
1603
  sessionDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
1596
1604
  if (didSpill) {
1597
1605
  // THE number that settles why a spilled call did or did not save:
@@ -1656,7 +1664,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1656
1664
  const meterStreamed = (x) => {
1657
1665
  if (paid || typeof x?.billedUsd !== 'number') return;
1658
1666
  sessionSpent += x.billedUsd;
1659
- sessionCogs += typeof x.cogsUsd === 'number' ? x.cogsUsd : x.billedUsd / MARKUP;
1667
+ sessionCogs += receiptUsedCogs(x, MARKUP);
1668
+ noteQuote(x);
1660
1669
  sessionDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
1661
1670
  if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; billedWithActual += x.billedUsd || 0; }
1662
1671
  if (didSpill) {
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Fly gateway race: one POST { race, race_need, tier }, unused grant-back,
3
+ * and HUD cogs for the racers that actually ran — never the N+judge ceiling.
4
+ */
5
+
6
+ export const FLY_GATEWAY_HOST = 'x402-tokens.fly.dev';
7
+ export const RACE_NO_CREDIT = '(race: not enough prepaid credit — shrink N or top up, rather than fire on $0)';
8
+
9
+ const FLY_RE = /x402-tokens\.fly\.dev/i;
10
+
11
+ /** Completions door is the Fly gateway (sidecar → x402-tokens.fly.dev). */
12
+ export function isFlyGatewayUpstream(upstream) {
13
+ return FLY_RE.test(String(upstream || ''));
14
+ }
15
+
16
+ /**
17
+ * Whether this completions door will honor `race:` on POST /chat/completions.
18
+ * Fly (and a test mock that advertises it) → true. Old sidecar / local mock → false.
19
+ */
20
+ export function doorAcceptsRace(info) {
21
+ if (!info || typeof info !== 'object') return false;
22
+ if (info.race === true || info.gatewayRace === true) return true;
23
+ if (info.race === false || info.gatewayRace === false) return false;
24
+ const features = info.features || info.caps || info.capabilities;
25
+ if (Array.isArray(features) && features.some((f) => String(f).toLowerCase() === 'race')) return true;
26
+ if (features && typeof features === 'object' && (features.race === true || features.gatewayRace === true)) {
27
+ return true;
28
+ }
29
+ return isFlyGatewayUpstream(info.upstream || info.apiBase || info.gateway);
30
+ }
31
+
32
+ let probeCache = { at: 0, ok: null, proxy: '' };
33
+
34
+ export function resetGatewayRaceProbe() {
35
+ probeCache = { at: 0, ok: null, proxy: '' };
36
+ }
37
+
38
+ function proxyOrigin(proxy) {
39
+ const raw = String(proxy || '').replace(/\/+$/, '');
40
+ return raw.replace(/\/v1$/i, '');
41
+ }
42
+
43
+ /**
44
+ * Probe the sidecar / mock once. GET /v1/info (and /info) — never a paid
45
+ * completions call. Cached briefly so a race of 4 does not fan out probes.
46
+ */
47
+ export async function probeGatewayRace(proxy, fetchFn = fetch, ttlMs = 60_000) {
48
+ const key = String(proxy || '');
49
+ if (probeCache.ok != null && probeCache.proxy === key && Date.now() - probeCache.at < ttlMs) {
50
+ return probeCache.ok;
51
+ }
52
+ const origin = proxyOrigin(key);
53
+ if (!origin) {
54
+ probeCache = { at: Date.now(), ok: false, proxy: key };
55
+ return false;
56
+ }
57
+ const paths = ['/v1/info', '/info'];
58
+ for (const p of paths) {
59
+ try {
60
+ const r = await fetchFn(`${origin}${p}`, { signal: AbortSignal.timeout(1500) });
61
+ if (!r.ok) continue;
62
+ const j = await r.json().catch(() => null);
63
+ const ok = doorAcceptsRace(j);
64
+ probeCache = { at: Date.now(), ok, proxy: key };
65
+ return ok;
66
+ } catch { /* try next */ }
67
+ }
68
+ probeCache = { at: Date.now(), ok: false, proxy: key };
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * If prepaid credit is known and `n × quote > credit`, shrink n (or 0 = refuse)
74
+ * rather than fire 4 groks on $0 credit. Unknown credit/quote → leave n alone.
75
+ */
76
+ export function capRaceByCredit(n, { creditUsd, quoteUsd } = {}) {
77
+ const want = Math.max(0, Math.floor(Number(n) || 0));
78
+ if (creditUsd == null || !Number.isFinite(Number(creditUsd))) {
79
+ return { n: want, reason: null };
80
+ }
81
+ const credit = Number(creditUsd);
82
+ if (credit <= 0) return { n: 0, reason: 'no-credit' };
83
+ const quote = Number(quoteUsd);
84
+ if (!Number.isFinite(quote) || quote <= 0) {
85
+ // Credit is known and positive but we have no per-entrant quote — do not
86
+ // invent one. A $0 balance already refused above.
87
+ return { n: want, reason: null };
88
+ }
89
+ if (want * quote <= credit) return { n: want, reason: null };
90
+ const maxN = Math.floor(credit / quote);
91
+ if (maxN < 1) return { n: 0, reason: 'no-credit' };
92
+ return { n: Math.min(want, maxN), reason: 'shrunk' };
93
+ }
94
+
95
+ function unusedGrant(x) {
96
+ const u = x?.race_unused ?? x?.raceUnused ?? x?.unused;
97
+ if (u == null) return { billed: 0, cogs: 0 };
98
+ if (typeof u === 'number' && Number.isFinite(u)) return { billed: u, cogs: u };
99
+ if (typeof u !== 'object') return { billed: 0, cogs: 0 };
100
+ const billed = Number(u.billedUsd ?? u.refundUsd ?? u.unusedBilledUsd ?? u.usd ?? 0);
101
+ const cogs = Number(u.cogsUsd ?? u.unusedCogsUsd ?? u.refundCogsUsd ?? billed);
102
+ return {
103
+ billed: Number.isFinite(billed) && billed > 0 ? billed : 0,
104
+ cogs: Number.isFinite(cogs) && cogs > 0 ? cogs : 0,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Actual used-racer cogs after unused grant-back.
110
+ * Never the N+judge ceiling. Does not clamp to billed — HUD embers when
111
+ * used cogs still exceed what was paid.
112
+ */
113
+ export function receiptUsedCogs(x, markup = 3) {
114
+ if (!x || typeof x !== 'object') return 0;
115
+ const billedRaw = Number(x.billedUsd);
116
+ const billedOk = Number.isFinite(billedRaw) && billedRaw >= 0;
117
+ let cogs = typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)
118
+ ? x.cogsUsd
119
+ : (billedOk ? billedRaw / markup : 0);
120
+ const grant = unusedGrant(x);
121
+ if (grant.cogs > 0) cogs = Math.max(0, cogs - grant.cogs);
122
+ return cogs;
123
+ }
124
+
125
+ /**
126
+ * Session meter. spent/direct are the receipt totals (already net of unused
127
+ * when the gateway refunds into billedUsd) — never a first-call rewrite.
128
+ * cogs is used racers after unused grant-back.
129
+ */
130
+ export function meterRaceReceipt(x, markup = 3) {
131
+ const billed = Number(x?.billedUsd);
132
+ const spentUsd = Number.isFinite(billed) ? billed : 0;
133
+ const usedCogs = receiptUsedCogs(x, markup);
134
+ const direct = typeof x?.directUsd === 'number' ? x.directUsd : spentUsd;
135
+ return { spentUsd, cogsUsd: usedCogs, directUsd: direct };
136
+ }
137
+
138
+ export function inferRaceTier(models, fallback = 'medium') {
139
+ const list = Array.isArray(models) ? models : [];
140
+ const grok = list.filter((m) => /^x-ai\/grok/i.test(String(m)));
141
+ if (list.length && grok.length === list.length) return 'grok4.6';
142
+ return fallback;
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.49.1",
3
+ "version": "0.49.2",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",