openzoo 0.45.0 → 0.46.0
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 +26 -2
- package/lib/info.js +43 -2
- package/lib/pay.js +14 -1
- package/lib/podagent.mjs +7 -0
- package/lib/proxy.js +41 -11
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -293,6 +293,8 @@ function newGroupThread(names) {
|
|
|
293
293
|
// chunk after the first carries the PREVIOUS chunk's context_id so the
|
|
294
294
|
// sidecar appends to the same bound context instead of starting fresh.
|
|
295
295
|
const BIND_CHUNK_BYTES = 512 * 1024;
|
|
296
|
+
// Chained auto-run commands per user message. Each hop is a paid call.
|
|
297
|
+
const AUTO_MAX_STEPS = Number(process.env.OZ_AUTO_MAX_STEPS || 8);
|
|
296
298
|
async function bindThread(t) {
|
|
297
299
|
// Only bind what's NEW since the last successful bind, continuing the
|
|
298
300
|
// existing context_id — previously this rebuilt and re-sent the WHOLE
|
|
@@ -520,6 +522,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
520
522
|
bindThread(t).catch(() => {});
|
|
521
523
|
return;
|
|
522
524
|
}
|
|
525
|
+
if (!/^\(command output\)/.test(userText)) t.autoSteps = 0;
|
|
523
526
|
t.messages.push({ role: 'user', content: contentFor(userText, images) });
|
|
524
527
|
t.status = 'thinking';
|
|
525
528
|
let reply = '';
|
|
@@ -538,10 +541,31 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
538
541
|
if (t.runMode === 'auto') {
|
|
539
542
|
const output = await execCommand(command, dirFor(t.id));
|
|
540
543
|
const shown = `$ ${command}\n${output}`;
|
|
541
|
-
t.messages.push({ role: 'user', content: `output:\n${output}` });
|
|
542
544
|
t.history.push({ who: 'bot', text: shown });
|
|
543
545
|
onEvent?.({ type: 'final', name: t.name, color: t.color, text: shown });
|
|
544
|
-
|
|
546
|
+
// FEED THE OUTPUT BACK. The 'ask' path already does this on approve, so
|
|
547
|
+
// auto mode was strictly LESS capable than the gated one: the command
|
|
548
|
+
// ran, the result was shown, and the model never saw it — no diagnosis,
|
|
549
|
+
// no follow-up, no next step. It looked like "auto mode does nothing".
|
|
550
|
+
//
|
|
551
|
+
// Bounded, because this is a loop that spends real money on every hop:
|
|
552
|
+
// AUTO_MAX_STEPS chained commands per user message, reset whenever the
|
|
553
|
+
// user speaks again.
|
|
554
|
+
t.autoSteps = (t.autoSteps || 0) + 1;
|
|
555
|
+
t.status = 'idle';
|
|
556
|
+
t.lastActivityAt = Date.now();
|
|
557
|
+
saveThreads();
|
|
558
|
+
if (t.autoSteps < AUTO_MAX_STEPS) {
|
|
559
|
+
runTurn(threadId, `(command output)\n${output}`, onEvent).catch(() => {});
|
|
560
|
+
} else {
|
|
561
|
+
const note = `(auto-run stopped after ${AUTO_MAX_STEPS} chained commands — say "continue" to keep going)`;
|
|
562
|
+
t.history.push({ who: 'bot', text: note });
|
|
563
|
+
onEvent?.({ type: 'final', name: t.name, color: t.color, text: note });
|
|
564
|
+
saveThreads();
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
{
|
|
545
569
|
const runId = randomUUID();
|
|
546
570
|
t.pendingRun = { runId, command, cwd: dirFor(t.id) };
|
|
547
571
|
t.history.push({ who: 'bot', text: command, runId, runStatus: 'pending' });
|
package/lib/info.js
CHANGED
|
@@ -86,10 +86,51 @@ export async function printBalance() {
|
|
|
86
86
|
* Credit is keyed by the signed namespace, so it belongs to this wallet and
|
|
87
87
|
* cannot be spent by anyone else.
|
|
88
88
|
*/
|
|
89
|
+
/**
|
|
90
|
+
* How much credit this wallet can actually afford, in USD.
|
|
91
|
+
*
|
|
92
|
+
* Derived from a LIVE quote rather than our own price table: ask the gateway
|
|
93
|
+
* to price $1 of credit, read what each rail wants in raw units, and divide
|
|
94
|
+
* our balance by it. That way TOKEN (whose USD price we do not carry) is
|
|
95
|
+
* valued exactly as the gateway values it at settlement time.
|
|
96
|
+
*/
|
|
97
|
+
export async function affordableUsd() {
|
|
98
|
+
const { PayClient } = await import('./pay.js');
|
|
99
|
+
const { withNamespace } = await import('./namespace.js');
|
|
100
|
+
const client = new PayClient();
|
|
101
|
+
const r = await fetch(`${config.apiBase}/v1/credits/topup`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
104
|
+
body: JSON.stringify({ usd: 1 }),
|
|
105
|
+
});
|
|
106
|
+
if (r.status !== 402) return 0;
|
|
107
|
+
const ch = await r.json().catch(() => ({}));
|
|
108
|
+
let best = 0;
|
|
109
|
+
for (const row of ch.accepts || []) {
|
|
110
|
+
const perUsd = BigInt(row.maxAmountRequired || '0');
|
|
111
|
+
if (perUsd <= 0n) continue;
|
|
112
|
+
try {
|
|
113
|
+
const bal = await client.balanceForAccept?.(row);
|
|
114
|
+
const raw = typeof bal === 'bigint' ? bal : BigInt(bal?.raw ?? 0);
|
|
115
|
+
const usd = Number(raw * 1000n / perUsd) / 1000;
|
|
116
|
+
if (usd > best) best = usd;
|
|
117
|
+
} catch { /* a rail we cannot read is simply not a candidate */ }
|
|
118
|
+
}
|
|
119
|
+
return best;
|
|
120
|
+
}
|
|
121
|
+
|
|
89
122
|
export async function topUp(usdArg) {
|
|
90
|
-
|
|
123
|
+
// "all" spends everything the wallet can cover, minus a small margin so a
|
|
124
|
+
// price tick between quote and settle does not fail the payment outright.
|
|
125
|
+
let usd = Number(usdArg);
|
|
126
|
+
if (String(usdArg).toLowerCase() === 'all' || usdArg === undefined) {
|
|
127
|
+
const max = await affordableUsd();
|
|
128
|
+
usd = Math.floor(max * 0.97 * 100) / 100;
|
|
129
|
+
if (!(usd >= 1)) throw new Error(`wallet covers only $${max.toFixed(4)} of credit — fund it first (openzoo balance)`);
|
|
130
|
+
console.log(`wallet covers ~$${max.toFixed(2)} — buying $${usd.toFixed(2)}`);
|
|
131
|
+
}
|
|
91
132
|
if (!Number.isFinite(usd) || usd < 1 || usd > 500) {
|
|
92
|
-
throw new Error('usage: openzoo topup <usd> (1-500)');
|
|
133
|
+
throw new Error('usage: openzoo topup <usd|all> (1-500)');
|
|
93
134
|
}
|
|
94
135
|
const { PayClient } = await import('./pay.js');
|
|
95
136
|
const client = new PayClient();
|
package/lib/pay.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Connection, PublicKey } from '@solana/web3.js';
|
|
2
2
|
import { getAssociatedTokenAddressSync } from '@solana/spl-token';
|
|
3
|
-
import { config, fundingLine } from './config.js';
|
|
3
|
+
import { config, fundingLine, evmRpcFor } from './config.js';
|
|
4
4
|
import { loadOrCreateWallet } from './wallet.js';
|
|
5
5
|
import {
|
|
6
6
|
parse402, orderAccepts, railOf, buildPaymentOnline, tokenBalance,
|
|
@@ -137,6 +137,19 @@ export class PayClient {
|
|
|
137
137
|
try { return privateKeyToAccount(this.evmPrivateKey).address; } catch { return null; }
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Raw balance this wallet holds of the asset a 402 row asks for. */
|
|
141
|
+
async balanceForAccept(accept) {
|
|
142
|
+
const rail = railOf(accept);
|
|
143
|
+
if (rail === 'solana') {
|
|
144
|
+
const b = await tokenBalance(this.connection, this.keypair.publicKey, accept.asset);
|
|
145
|
+
return BigInt(b.raw || 0);
|
|
146
|
+
}
|
|
147
|
+
const raw = await evmTokenBalance({
|
|
148
|
+
rpcUrl: evmRpcFor(rail), token: accept.asset, owner: this.evmAddress,
|
|
149
|
+
});
|
|
150
|
+
return BigInt(raw || 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
async buildPaymentFor(accept, onStage) {
|
|
141
154
|
const rail = railOf(accept);
|
|
142
155
|
if (rail === 'solana') {
|
package/lib/podagent.mjs
CHANGED
|
@@ -513,6 +513,13 @@ for (const port of PORTS) {
|
|
|
513
513
|
record({ port, method: 'UPGRADE', path: req.url, bodyBytes: 0 });
|
|
514
514
|
socket.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n');
|
|
515
515
|
});
|
|
516
|
+
// NEVER FATAL. These are the Grok-Bot-shaped agent ports, a nicety — but an
|
|
517
|
+
// unhandled 'error' event takes the whole process down, and the process that
|
|
518
|
+
// dies is the one also serving the CHAT on :4173. A stale instance holding
|
|
519
|
+
// :1337 therefore black-screened the app: Electron loaded :4173 and got
|
|
520
|
+
// ERR_CONNECTION_REFUSED, with the real cause (EADDRINUSE on a port nobody
|
|
521
|
+
// cares about) buried in a log the user never sees.
|
|
522
|
+
server.on('error', (e) => console.log(`[agent] :${port} unavailable (${e.code}) — continuing without it`));
|
|
516
523
|
server.listen(port, '0.0.0.0', () => console.log(`[agent] on :${port}`));
|
|
517
524
|
}
|
|
518
525
|
|
package/lib/proxy.js
CHANGED
|
@@ -722,6 +722,22 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
722
722
|
if (isChat && response.ok && upCt.includes('application/json')) {
|
|
723
723
|
let data = null;
|
|
724
724
|
try { data = await response.clone().json(); } catch { /* not JSON after all */ }
|
|
725
|
+
// PREPAID CALLS STILL COST MONEY. The block above only meters calls
|
|
726
|
+
// where THIS proxy answered a 402 and paid. When prepaid credit covers
|
|
727
|
+
// the quote the gateway serves 200 on the FIRST request, so there is
|
|
728
|
+
// no 402, no payment and no receipt — and the session read $0.05 / 2
|
|
729
|
+
// calls while the credit balance had actually fallen $3.017 -> $1.395
|
|
730
|
+
// over a 30-question run. The receipt still rides the response body,
|
|
731
|
+
// so meter it from there.
|
|
732
|
+
if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
|
|
733
|
+
const x = data.x402;
|
|
734
|
+
sessionSpent += x.billedUsd;
|
|
735
|
+
sessionCogs += typeof x.cogsUsd === 'number' ? x.cogsUsd : x.billedUsd / MARKUP;
|
|
736
|
+
sessionDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
|
|
737
|
+
paidCalls += 1;
|
|
738
|
+
if (viaTunnel) tunnelSpent += x.billedUsd;
|
|
739
|
+
say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
|
|
740
|
+
}
|
|
725
741
|
if (data?.object === 'chat.completion') {
|
|
726
742
|
if (rKey) replayPut(rKey, data, response.headers.get('x-payment-response'));
|
|
727
743
|
// Anthropic-shaped caller gets an Anthropic-shaped answer, streamed
|
|
@@ -789,22 +805,36 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
789
805
|
// when this wallet actually has funds, so a fresh/empty wallet is untouched.
|
|
790
806
|
// Opt out with OPENZOO_NO_AUTOTOPUP=1; size it with OPENZOO_AUTOTOPUP_USD.
|
|
791
807
|
if (!process.env.OPENZOO_NO_AUTOTOPUP) {
|
|
792
|
-
|
|
808
|
+
// Keep credit topped up, forever, from whatever the wallet holds.
|
|
809
|
+
//
|
|
810
|
+
// The first version ran ONCE at startup and bought a fixed $5, so funding
|
|
811
|
+
// the wallet later did nothing at all — the user sent TOKEN and kept
|
|
812
|
+
// paying on-chain per call. This checks on an interval and spends what the
|
|
813
|
+
// wallet can actually cover, priced by the gateway's own live quote (so
|
|
814
|
+
// TOKEN is valued exactly as it settles).
|
|
815
|
+
const FLOOR = Number(process.env.OPENZOO_AUTOTOPUP_FLOOR || 2);
|
|
816
|
+
const EVERY = Number(process.env.OPENZOO_AUTOTOPUP_EVERY_MS || 60_000);
|
|
817
|
+
let topping = false;
|
|
818
|
+
const tick = async () => {
|
|
819
|
+
if (topping) return;
|
|
820
|
+
topping = true;
|
|
793
821
|
try {
|
|
794
|
-
const { creditBalance, topUp } = await import('./info.js');
|
|
822
|
+
const { creditBalance, topUp, affordableUsd } = await import('./info.js');
|
|
795
823
|
const have = await creditBalance();
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
await topUp(want);
|
|
824
|
+
if (have >= FLOOR) return;
|
|
825
|
+
const can = await affordableUsd();
|
|
826
|
+
if (can < 1) return; // nothing to convert; stay quiet
|
|
827
|
+
say(`credit $${have.toFixed(4)} below $${FLOOR} — wallet covers ~$${can.toFixed(2)}, topping up`);
|
|
828
|
+
await topUp('all');
|
|
802
829
|
} catch (e) {
|
|
803
|
-
// A wallet with no funds, or a gateway that refuses, must never stop
|
|
804
|
-
// the proxy from serving: calls just fall back to paying per call.
|
|
805
830
|
say(`auto top-up skipped: ${String(e.message || e).slice(0, 120)}`);
|
|
831
|
+
} finally {
|
|
832
|
+
topping = false;
|
|
806
833
|
}
|
|
807
|
-
}
|
|
834
|
+
};
|
|
835
|
+
tick();
|
|
836
|
+
const timer = setInterval(tick, EVERY);
|
|
837
|
+
timer.unref?.(); // never hold the process open just for this
|
|
808
838
|
}
|
|
809
839
|
|
|
810
840
|
if (!silent) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun — 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",
|