openzoo 0.49.1 → 0.49.3
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 +89 -32
- package/lib/podagent.mjs +277 -2
- package/lib/proxy.js +15 -6
- package/lib/racesettle.js +129 -0
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -691,6 +691,7 @@ const AUTO_RACE_RETRY = 'AUTO is still on — the last model call failed (race/e
|
|
|
691
691
|
+ 'Do not stop and do not ask the user to type continue. '
|
|
692
692
|
+ 'Emit the next directive now (RUN:/SPAWN:/SEND:/READ:/WRITE:/GLOB:/FETCH:/MCP:/SERVE:), '
|
|
693
693
|
+ 'or DONE: if the job is actually finished.';
|
|
694
|
+
const AUTO_EMPTY_RETRY = 'AUTO_EMPTY_RETRY: the command produced no output, try a different command or a different path, do not stop.';
|
|
694
695
|
// Said it would, without a directive line. "Spawned X" and "working on it" are
|
|
695
696
|
// in here because they are FALSE without a SPAWN: in the same reply — the bot
|
|
696
697
|
// reports success for something the harness never saw.
|
|
@@ -709,13 +710,43 @@ function isTransientModelFail(text) {
|
|
|
709
710
|
function isPaymentFailed(text) {
|
|
710
711
|
return /\b(?:payment failed|HTTP 402|wallet is empty|empty wallet)\b/i.test(String(text || ''));
|
|
711
712
|
}
|
|
713
|
+
// Empty stdout, "(no output)", or a directive that found nothing. That is
|
|
714
|
+
// still a command-output hop today, so AUTO used to chain once and then park
|
|
715
|
+
// as if the job had succeeded. It has not — try another command or path.
|
|
716
|
+
function isEmptyExecOutput(text) {
|
|
717
|
+
const s = String(text ?? '').trim();
|
|
718
|
+
return !s || s === '(no output)';
|
|
719
|
+
}
|
|
720
|
+
function isEmptyDirectiveAck(text) {
|
|
721
|
+
const s = String(text ?? '').trim();
|
|
722
|
+
if (isEmptyExecOutput(s)) return true;
|
|
723
|
+
if (/:\s*\(empty\)$/i.test(s)) return true;
|
|
724
|
+
if (/:\s*no matches\s*$/i.test(s)) return true;
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
function isEmptyToolResult(text) {
|
|
728
|
+
if (text == null) return false;
|
|
729
|
+
const s = String(text).trim();
|
|
730
|
+
if (isEmptyExecOutput(s)) return true;
|
|
731
|
+
const cmd = /^\(command output\)\s*([\s\S]*)$/.exec(s);
|
|
732
|
+
if (cmd) return isEmptyExecOutput(cmd[1]);
|
|
733
|
+
const dir = /^\(directive result\)\s*([\s\S]*)$/.exec(s);
|
|
734
|
+
if (dir) return isEmptyDirectiveAck(dir[1]);
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
function isEmptyShownRun(text) {
|
|
738
|
+
const shown = /^\$ [^\n]*\n([\s\S]*)$/.exec(String(text ?? ''));
|
|
739
|
+
return Boolean(shown && isEmptyExecOutput(shown[1]));
|
|
740
|
+
}
|
|
712
741
|
// Park only: ask mode, pendingRun, DONE:, 402/empty-wallet, or the hard cap.
|
|
713
|
-
|
|
742
|
+
// Empty /(no output) exec is not DONE — keep going with AUTO_EMPTY_RETRY.
|
|
743
|
+
function shouldKeepAuto(t, reply, userText) {
|
|
714
744
|
if (!t || t.runMode !== 'auto') return false;
|
|
715
745
|
if (t.pendingRun) return false;
|
|
716
746
|
if ((t.autoSteps || 0) >= AUTO_MAX_STEPS) return false;
|
|
717
|
-
if (isDoneReply(reply)) return false;
|
|
718
747
|
if (isPaymentFailed(reply)) return false;
|
|
748
|
+
if (isEmptyToolResult(userText) || isEmptyShownRun(reply)) return true;
|
|
749
|
+
if (isDoneReply(reply)) return false;
|
|
719
750
|
return true;
|
|
720
751
|
}
|
|
721
752
|
function enqueueAutoHop(t, threadId, userText, onEvent) {
|
|
@@ -725,7 +756,8 @@ function enqueueAutoHop(t, threadId, userText, onEvent) {
|
|
|
725
756
|
kickTurn(threadId, userText, onEvent).catch(() => {});
|
|
726
757
|
return true;
|
|
727
758
|
}
|
|
728
|
-
function autoHopText(reply) {
|
|
759
|
+
function autoHopText(reply, userText) {
|
|
760
|
+
if (isEmptyToolResult(userText) || isEmptyShownRun(reply)) return AUTO_EMPTY_RETRY;
|
|
729
761
|
return isTransientModelFail(reply) ? AUTO_RACE_RETRY : AUTO_CONTINUE;
|
|
730
762
|
}
|
|
731
763
|
// PING used to be a read: last-line status, no turn. Idle children stayed idle
|
|
@@ -805,7 +837,8 @@ without a fact only the user has.
|
|
|
805
837
|
|
|
806
838
|
When the job is actually finished, emit DONE: as the first line. A status
|
|
807
839
|
sentence is not a stop — the harness keeps this thread working until DONE:,
|
|
808
|
-
a real blocking question, or the step cap
|
|
840
|
+
a real blocking question, or the step cap. Empty output, "(no output)", and
|
|
841
|
+
GLOB/GREP with no matches are not finished — try a different command or path.`;
|
|
809
842
|
async function bindThread(t) {
|
|
810
843
|
// Only bind what's NEW since the last successful bind, continuing the
|
|
811
844
|
// existing context_id — previously this rebuilt and re-sent the WHOLE
|
|
@@ -992,7 +1025,7 @@ function execCommand(command, cwd) {
|
|
|
992
1025
|
exec(command, { cwd, shell: RUN_SHELL, timeout: RUN_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
993
1026
|
let out = (stdout || '') + (stderr ? '\n' + stderr : '');
|
|
994
1027
|
if (err) out += `\n(exit ${err.code ?? 1})`;
|
|
995
|
-
resolve(keepWhole(out) || '(no output)');
|
|
1028
|
+
resolve(keepWhole(out).trim() || '(no output)');
|
|
996
1029
|
});
|
|
997
1030
|
});
|
|
998
1031
|
}
|
|
@@ -1411,7 +1444,7 @@ setInterval(() => {
|
|
|
1411
1444
|
const lastBot = [...(t.history || [])].reverse().find((h) => h.who === 'bot');
|
|
1412
1445
|
const lastText = lastBot?.text || '';
|
|
1413
1446
|
if (shouldKeepAuto(t, lastText)) {
|
|
1414
|
-
kickTurn(t.id,
|
|
1447
|
+
kickTurn(t.id, autoHopText(lastText)).catch(() => {});
|
|
1415
1448
|
} else {
|
|
1416
1449
|
t.status = 'idle';
|
|
1417
1450
|
t.liveStatus = '';
|
|
@@ -1676,7 +1709,9 @@ function isHarnessUserText(text) {
|
|
|
1676
1709
|
return /^\((command output|directive result)\)/.test(String(text || ''))
|
|
1677
1710
|
|| String(text || '') === NUDGE
|
|
1678
1711
|
|| String(text || '') === AUTO_CONTINUE
|
|
1679
|
-
|| String(text || '') === AUTO_RACE_RETRY
|
|
1712
|
+
|| String(text || '') === AUTO_RACE_RETRY
|
|
1713
|
+
|| String(text || '') === AUTO_EMPTY_RETRY
|
|
1714
|
+
|| String(text || '').startsWith('AUTO_EMPTY_RETRY:');
|
|
1680
1715
|
}
|
|
1681
1716
|
|
|
1682
1717
|
function firstUserAsk(t) {
|
|
@@ -2502,8 +2537,8 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2502
2537
|
}
|
|
2503
2538
|
bindThread(t).catch(() => {});
|
|
2504
2539
|
lastReply = memberReply;
|
|
2505
|
-
if (shouldKeepAuto(t, memberReply)) {
|
|
2506
|
-
chained = enqueueAutoHop(t, threadId, autoHopText(memberReply), onEvent);
|
|
2540
|
+
if (shouldKeepAuto(t, memberReply, userText)) {
|
|
2541
|
+
chained = enqueueAutoHop(t, threadId, autoHopText(memberReply, userText), onEvent);
|
|
2507
2542
|
}
|
|
2508
2543
|
return;
|
|
2509
2544
|
}
|
|
@@ -2569,6 +2604,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2569
2604
|
return (await brainRace(callMsgs, emit, t.contextId, models, need, undefined, emitStatus, {
|
|
2570
2605
|
signal: turnAbort.signal,
|
|
2571
2606
|
onArrivals: (arr) => { t.lastRaceFail = summarizeRaceFailures(arr); },
|
|
2607
|
+
tier: t.tier || 'medium',
|
|
2572
2608
|
})).trim();
|
|
2573
2609
|
}
|
|
2574
2610
|
// A retry draws a DIFFERENT model from the tier rather than the same one.
|
|
@@ -2637,7 +2673,11 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2637
2673
|
// most material (command output, GLOB results, MCP tool lists). The
|
|
2638
2674
|
// holographic context stopped growing precisely when it mattered.
|
|
2639
2675
|
lastReply = shown;
|
|
2640
|
-
chained = enqueueAutoHop(
|
|
2676
|
+
chained = enqueueAutoHop(
|
|
2677
|
+
t, threadId,
|
|
2678
|
+
isEmptyExecOutput(output) ? AUTO_EMPTY_RETRY : condense('(command output)', output),
|
|
2679
|
+
onEvent,
|
|
2680
|
+
);
|
|
2641
2681
|
return;
|
|
2642
2682
|
}
|
|
2643
2683
|
{
|
|
@@ -2667,8 +2707,12 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2667
2707
|
// Same budget as RUN (shared t.autoSteps, reset when the user speaks), so
|
|
2668
2708
|
// this cannot spend more than a chained RUN loop already could.
|
|
2669
2709
|
if (t.runMode === 'auto' && ack !== null && ack !== undefined
|
|
2670
|
-
&& !isDoneReply(reply)) {
|
|
2671
|
-
chained = enqueueAutoHop(
|
|
2710
|
+
&& (isEmptyDirectiveAck(ack) || !isDoneReply(reply))) {
|
|
2711
|
+
chained = enqueueAutoHop(
|
|
2712
|
+
t, threadId,
|
|
2713
|
+
isEmptyDirectiveAck(ack) ? AUTO_EMPTY_RETRY : condense('(directive result)', ack),
|
|
2714
|
+
onEvent,
|
|
2715
|
+
);
|
|
2672
2716
|
return;
|
|
2673
2717
|
}
|
|
2674
2718
|
|
|
@@ -2698,17 +2742,18 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2698
2742
|
|
|
2699
2743
|
// After any auto reply that is not DONE: and not waiting on approval,
|
|
2700
2744
|
// kick immediately. Race/empty/error uses AUTO_RACE_RETRY.
|
|
2701
|
-
if (shouldKeepAuto(t, reply)) {
|
|
2702
|
-
chained = enqueueAutoHop(t, threadId, autoHopText(reply), onEvent);
|
|
2745
|
+
if (shouldKeepAuto(t, reply, userText)) {
|
|
2746
|
+
chained = enqueueAutoHop(t, threadId, autoHopText(reply, userText), onEvent);
|
|
2703
2747
|
return;
|
|
2704
2748
|
}
|
|
2705
2749
|
bindThread(t).catch(() => {});
|
|
2706
2750
|
} finally {
|
|
2707
2751
|
// Idle only when this hop should not keep AUTO going: DONE:, pendingRun,
|
|
2708
|
-
// ask mode, 402/empty-wallet, or the hard cap.
|
|
2752
|
+
// ask mode, 402/empty-wallet, or the hard cap. Empty /(no output) is not
|
|
2753
|
+
// DONE — AUTO_EMPTY_RETRY. Otherwise kick again.
|
|
2709
2754
|
if (stillMine() && !chained && !parked) {
|
|
2710
|
-
if (shouldKeepAuto(t, lastReply)) {
|
|
2711
|
-
enqueueAutoHop(t, threadId, autoHopText(lastReply), onEvent);
|
|
2755
|
+
if (shouldKeepAuto(t, lastReply, userText)) {
|
|
2756
|
+
enqueueAutoHop(t, threadId, autoHopText(lastReply, userText), onEvent);
|
|
2712
2757
|
} else if (!t.pendingRun) {
|
|
2713
2758
|
t.status = 'idle';
|
|
2714
2759
|
t.liveStatus = '';
|
|
@@ -4952,9 +4997,14 @@ const APP_HTML = `<!doctype html>
|
|
|
4952
4997
|
const cogs = Number(you.cogsUsd) || 0;
|
|
4953
4998
|
const direct = Number(you.directUsd) || 0;
|
|
4954
4999
|
const margin = spent > 0 ? Math.round((spent - cogs) / spent * 100) + '%' : '—';
|
|
5000
|
+
const cogsOver = cogs > spent;
|
|
4955
5001
|
document.getElementById('hYouSpent').textContent = usd(spent);
|
|
4956
|
-
document.getElementById('hYouCogs')
|
|
4957
|
-
|
|
5002
|
+
const cogsEl = document.getElementById('hYouCogs');
|
|
5003
|
+
cogsEl.textContent = usd(cogs);
|
|
5004
|
+
cogsEl.className = cogsOver ? 'hember' : '';
|
|
5005
|
+
const marginEl = document.getElementById('hYouMargin');
|
|
5006
|
+
marginEl.textContent = margin;
|
|
5007
|
+
marginEl.className = cogsOver ? 'hember' : 'hlime';
|
|
4958
5008
|
document.getElementById('hYouDirect').textContent = usd(direct);
|
|
4959
5009
|
const savedEl = document.getElementById('hYouSaved');
|
|
4960
5010
|
const hintEl = document.getElementById('hHint');
|
|
@@ -4967,19 +5017,26 @@ const APP_HTML = `<!doctype html>
|
|
|
4967
5017
|
// is shipping the WHOLE corpus), so 2dp would read as noise up there.
|
|
4968
5018
|
savedEl.textContent = (mult >= 100 ? Math.round(mult) : mult.toFixed(mult >= 10 ? 1 : 2)) + 'x';
|
|
4969
5019
|
savedEl.className = mult >= 1 ? 'hlime' : 'hember';
|
|
4970
|
-
//
|
|
4971
|
-
//
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5020
|
+
// Session direct/spent (never first-call). Ember when cogs > spent —
|
|
5021
|
+
// house losing. Do not treat race_unused as a user refund.
|
|
5022
|
+
if (cogsOver) {
|
|
5023
|
+
hintEl.className = 'hhint show';
|
|
5024
|
+
hintEl.innerHTML = '<b>cogs above paid.</b> house is losing — our cost exceeded what you were billed. '
|
|
5025
|
+
+ 'you pay for every entrant we actually launched; failures still cost us.';
|
|
5026
|
+
} else {
|
|
5027
|
+
hintEl.className = mult >= 1 ? 'hhint' : 'hhint show';
|
|
5028
|
+
hintEl.innerHTML = '<b>feed it more.</b> you\\'re billed on the slice actually sent, '
|
|
5029
|
+
+ 'not the corpus — so the more you bind, the further ahead this gets. '
|
|
5030
|
+
+ 'small inputs cost more than sending them straight.';
|
|
5031
|
+
}
|
|
4980
5032
|
} else {
|
|
4981
5033
|
savedEl.textContent = '—';
|
|
4982
|
-
|
|
5034
|
+
if (cogsOver) {
|
|
5035
|
+
hintEl.className = 'hhint show';
|
|
5036
|
+
hintEl.innerHTML = '<b>cogs above paid.</b> our cost exceeded what you were billed.';
|
|
5037
|
+
} else {
|
|
5038
|
+
hintEl.className = 'hhint';
|
|
5039
|
+
}
|
|
4983
5040
|
}
|
|
4984
5041
|
document.getElementById('hFoot').textContent = (you.paidCalls || 0) + ' paid calls this session';
|
|
4985
5042
|
} catch (e) {
|
|
@@ -5410,8 +5467,8 @@ export {
|
|
|
5410
5467
|
tryDirective, ensureWorkspacePort, isPreviewableRel, previewAck, workspaceFileUrl,
|
|
5411
5468
|
parseRun, looksLikeMcpAsBash, stripThinkTags, safeResolveIn, inDir, listDir,
|
|
5412
5469
|
handleSlash, newThread, setRunTurnForTest, setBrainAskForTest, runTurn,
|
|
5413
|
-
AUTO_CONTINUE, AUTO_RACE_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
|
|
5414
|
-
isDoneReply, isTransientModelFail, enqueueAutoHop, childKickoff, findByName,
|
|
5470
|
+
AUTO_CONTINUE, AUTO_RACE_RETRY, AUTO_EMPTY_RETRY, pingWakeText, pingCanWake, shouldKeepAuto,
|
|
5471
|
+
isDoneReply, isTransientModelFail, isEmptyToolResult, enqueueAutoHop, childKickoff, findByName,
|
|
5415
5472
|
attachChildDir, finishChildDir,
|
|
5416
5473
|
lockWorktree, unlockWorktree, parsePrRef, fetchSpecsForOrigin, agentSlug,
|
|
5417
5474
|
};
|
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(`${
|
|
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
|
*
|
|
@@ -685,6 +936,7 @@ export async function tierModels(tier, n = 1, random = false) {
|
|
|
685
936
|
*
|
|
686
937
|
* Every entrant is paid for, including the abandoned one — this trades money
|
|
687
938
|
* for latency and quality, which is why it is opt-in and capped.
|
|
939
|
+
* grokui does not grant unused or failed racers back to the user.
|
|
688
940
|
*
|
|
689
941
|
* `hooks` is for tests: `{ stream, classify, pairwise, minScore }`.
|
|
690
942
|
*/
|
|
@@ -693,10 +945,33 @@ export async function brainRace(messages, onDelta, contextId, models, need = 1,
|
|
|
693
945
|
const classify = hooks.classify || classifyRaceAnswer;
|
|
694
946
|
const pairwise = hooks.pairwise || pairwiseTied;
|
|
695
947
|
const minScore = hooks.minScore != null ? Number(hooks.minScore) : RACE_MIN_SCORE;
|
|
696
|
-
|
|
948
|
+
let list = (models || []).filter(Boolean).slice(0, RACE_MAX);
|
|
949
|
+
const budget = await raceBudget(hooks);
|
|
950
|
+
const capped = capRaceByCredit(list.length, budget);
|
|
951
|
+
if (capped.n < 1) {
|
|
952
|
+
onStatus?.('race refused — no credit');
|
|
953
|
+
return RACE_NO_CREDIT;
|
|
954
|
+
}
|
|
955
|
+
if (capped.n < list.length) {
|
|
956
|
+
list = list.slice(0, capped.n);
|
|
957
|
+
onStatus?.(capped.n < 2 ? 'race shrunk to 1 — credit' : `race shrunk to ${capped.n} — credit`);
|
|
958
|
+
}
|
|
697
959
|
if (list.length < 2) return stream(messages, onDelta, contextId, list[0], maxTokens, 0, 0, onStatus);
|
|
698
960
|
const want = Math.max(1, Math.min(Number(need) || 1, list.length));
|
|
699
961
|
|
|
962
|
+
// One Fly settle when the completions door honors `race:`. Custom stream
|
|
963
|
+
// hooks (unit tests of the N-parallel judge) keep the old path. Old
|
|
964
|
+
// sidecar / local mock that does not accept race: also stays N-parallel.
|
|
965
|
+
const customStream = Boolean(hooks.stream);
|
|
966
|
+
let gateway = hooks.gatewayRace;
|
|
967
|
+
if (gateway == null && !customStream) {
|
|
968
|
+
try { gateway = await probeGatewayRace(hooks.proxy || completionsProxy(), hooks.fetch || fetch); }
|
|
969
|
+
catch { gateway = false; }
|
|
970
|
+
}
|
|
971
|
+
if (gateway && !customStream) {
|
|
972
|
+
return brainGatewayRace(messages, onDelta, contextId, list, want, maxTokens, onStatus, hooks);
|
|
973
|
+
}
|
|
974
|
+
|
|
700
975
|
const feed = createRaceFeed(onDelta, onStatus, want);
|
|
701
976
|
feed.start();
|
|
702
977
|
|
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 +=
|
|
1523
|
-
|
|
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 +=
|
|
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 +=
|
|
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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fly gateway race: one POST { race, race_need, tier }.
|
|
3
|
+
* User pays for every entrant we actually launched. Failures still cost us
|
|
4
|
+
* (OpenRouter was paid). race_unused on a receipt is informational — do not
|
|
5
|
+
* treat it as a user refund or shrink HUD cogs to hide a house loss.
|
|
6
|
+
* HUD embers when cogs > spent.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const FLY_GATEWAY_HOST = 'x402-tokens.fly.dev';
|
|
10
|
+
export const RACE_NO_CREDIT = '(race: not enough prepaid credit — shrink N or top up, rather than fire on $0)';
|
|
11
|
+
|
|
12
|
+
const FLY_RE = /x402-tokens\.fly\.dev/i;
|
|
13
|
+
|
|
14
|
+
/** Completions door is the Fly gateway (sidecar → x402-tokens.fly.dev). */
|
|
15
|
+
export function isFlyGatewayUpstream(upstream) {
|
|
16
|
+
return FLY_RE.test(String(upstream || ''));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether this completions door will honor `race:` on POST /chat/completions.
|
|
21
|
+
* Fly (and a test mock that advertises it) → true. Old sidecar / local mock → false.
|
|
22
|
+
*/
|
|
23
|
+
export function doorAcceptsRace(info) {
|
|
24
|
+
if (!info || typeof info !== 'object') return false;
|
|
25
|
+
if (info.race === true || info.gatewayRace === true) return true;
|
|
26
|
+
if (info.race === false || info.gatewayRace === false) return false;
|
|
27
|
+
const features = info.features || info.caps || info.capabilities;
|
|
28
|
+
if (Array.isArray(features) && features.some((f) => String(f).toLowerCase() === 'race')) return true;
|
|
29
|
+
if (features && typeof features === 'object' && (features.race === true || features.gatewayRace === true)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return isFlyGatewayUpstream(info.upstream || info.apiBase || info.gateway);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let probeCache = { at: 0, ok: null, proxy: '' };
|
|
36
|
+
|
|
37
|
+
export function resetGatewayRaceProbe() {
|
|
38
|
+
probeCache = { at: 0, ok: null, proxy: '' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function proxyOrigin(proxy) {
|
|
42
|
+
const raw = String(proxy || '').replace(/\/+$/, '');
|
|
43
|
+
return raw.replace(/\/v1$/i, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Probe the sidecar / mock once. GET /v1/info (and /info) — never a paid
|
|
48
|
+
* completions call. Cached briefly so a race of 4 does not fan out probes.
|
|
49
|
+
*/
|
|
50
|
+
export async function probeGatewayRace(proxy, fetchFn = fetch, ttlMs = 60_000) {
|
|
51
|
+
const key = String(proxy || '');
|
|
52
|
+
if (probeCache.ok != null && probeCache.proxy === key && Date.now() - probeCache.at < ttlMs) {
|
|
53
|
+
return probeCache.ok;
|
|
54
|
+
}
|
|
55
|
+
const origin = proxyOrigin(key);
|
|
56
|
+
if (!origin) {
|
|
57
|
+
probeCache = { at: Date.now(), ok: false, proxy: key };
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const paths = ['/v1/info', '/info'];
|
|
61
|
+
for (const p of paths) {
|
|
62
|
+
try {
|
|
63
|
+
const r = await fetchFn(`${origin}${p}`, { signal: AbortSignal.timeout(1500) });
|
|
64
|
+
if (!r.ok) continue;
|
|
65
|
+
const j = await r.json().catch(() => null);
|
|
66
|
+
const ok = doorAcceptsRace(j);
|
|
67
|
+
probeCache = { at: Date.now(), ok, proxy: key };
|
|
68
|
+
return ok;
|
|
69
|
+
} catch { /* try next */ }
|
|
70
|
+
}
|
|
71
|
+
probeCache = { at: Date.now(), ok: false, proxy: key };
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* If prepaid credit is known and `n × quote > credit`, shrink n (or 0 = refuse)
|
|
77
|
+
* rather than fire 4 groks on $0 credit. Unknown credit/quote → leave n alone.
|
|
78
|
+
*/
|
|
79
|
+
export function capRaceByCredit(n, { creditUsd, quoteUsd } = {}) {
|
|
80
|
+
const want = Math.max(0, Math.floor(Number(n) || 0));
|
|
81
|
+
if (creditUsd == null || !Number.isFinite(Number(creditUsd))) {
|
|
82
|
+
return { n: want, reason: null };
|
|
83
|
+
}
|
|
84
|
+
const credit = Number(creditUsd);
|
|
85
|
+
if (credit <= 0) return { n: 0, reason: 'no-credit' };
|
|
86
|
+
const quote = Number(quoteUsd);
|
|
87
|
+
if (!Number.isFinite(quote) || quote <= 0) {
|
|
88
|
+
// Credit is known and positive but we have no per-entrant quote — do not
|
|
89
|
+
// invent one. A $0 balance already refused above.
|
|
90
|
+
return { n: want, reason: null };
|
|
91
|
+
}
|
|
92
|
+
if (want * quote <= credit) return { n: want, reason: null };
|
|
93
|
+
const maxN = Math.floor(credit / quote);
|
|
94
|
+
if (maxN < 1) return { n: 0, reason: 'no-credit' };
|
|
95
|
+
return { n: Math.min(want, maxN), reason: 'shrunk' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* House cost from the receipt. Do not subtract race_unused — unused
|
|
100
|
+
* grant-back is not a user refund, and shrinking cogs would hide house loss.
|
|
101
|
+
* Does not clamp to billed — HUD embers when cogs exceed what was paid.
|
|
102
|
+
*/
|
|
103
|
+
export function receiptUsedCogs(x, markup = 3) {
|
|
104
|
+
if (!x || typeof x !== 'object') return 0;
|
|
105
|
+
const billedRaw = Number(x.billedUsd);
|
|
106
|
+
const billedOk = Number.isFinite(billedRaw) && billedRaw >= 0;
|
|
107
|
+
if (typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)) return x.cogsUsd;
|
|
108
|
+
return billedOk ? billedRaw / markup : 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Session meter. spent/direct are the receipt totals as billed — never a
|
|
113
|
+
* first-call rewrite, never a race_unused user refund.
|
|
114
|
+
* cogs is the house cost on that receipt (HUD embers when cogs > spent).
|
|
115
|
+
*/
|
|
116
|
+
export function meterRaceReceipt(x, markup = 3) {
|
|
117
|
+
const billed = Number(x?.billedUsd);
|
|
118
|
+
const spentUsd = Number.isFinite(billed) ? billed : 0;
|
|
119
|
+
const usedCogs = receiptUsedCogs(x, markup);
|
|
120
|
+
const direct = typeof x?.directUsd === 'number' ? x.directUsd : spentUsd;
|
|
121
|
+
return { spentUsd, cogsUsd: usedCogs, directUsd: direct };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function inferRaceTier(models, fallback = 'medium') {
|
|
125
|
+
const list = Array.isArray(models) ? models : [];
|
|
126
|
+
const grok = list.filter((m) => /^x-ai\/grok/i.test(String(m)));
|
|
127
|
+
if (list.length && grok.length === list.length) return 'grok4.6';
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.3",
|
|
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",
|