openzoo 0.43.6 → 0.43.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/lib/grokui.mjs +20 -4
- package/lib/podagent.mjs +59 -28
- package/lib/proxy.js +17 -0
- package/package.json +1 -1
package/lib/grokui.mjs
CHANGED
|
@@ -245,10 +245,17 @@ function newGroupThread(names) {
|
|
|
245
245
|
// sidecar appends to the same bound context instead of starting fresh.
|
|
246
246
|
const BIND_CHUNK_BYTES = 512 * 1024;
|
|
247
247
|
async function bindThread(t) {
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
// Only bind what's NEW since the last successful bind, continuing the
|
|
249
|
+
// existing context_id — previously this rebuilt and re-sent the WHOLE
|
|
250
|
+
// history from scratch every turn (discarding t.contextId), so bind cost
|
|
251
|
+
// grew with total conversation length and was re-paid on every message.
|
|
252
|
+
const from = t.boundHistoryCount || 0;
|
|
253
|
+
const delta = t.history.slice(from);
|
|
254
|
+
if (!delta.length) return;
|
|
255
|
+
const corpus = delta.map((h) => (h.who === 'user' ? 'you' : (h.name || t.name)) + ': ' + h.text).join('\n');
|
|
256
|
+
if (!corpus.trim()) { t.boundHistoryCount = t.history.length; return; }
|
|
250
257
|
try {
|
|
251
|
-
let ctx;
|
|
258
|
+
let ctx = t.contextId;
|
|
252
259
|
for (let i = 0; i < corpus.length; i += BIND_CHUNK_BYTES) {
|
|
253
260
|
const part = corpus.slice(i, i + BIND_CHUNK_BYTES);
|
|
254
261
|
const body = ctx ? { corpus: part, context_id: ctx } : { corpus: part };
|
|
@@ -261,7 +268,7 @@ async function bindThread(t) {
|
|
|
261
268
|
if (j?.context_id) ctx = j.context_id;
|
|
262
269
|
else break; // this chunk failed — stop, keep whatever bound so far rather than lose it all
|
|
263
270
|
}
|
|
264
|
-
if (ctx) { t.contextId = ctx; saveThreads(); }
|
|
271
|
+
if (ctx) { t.contextId = ctx; t.boundHistoryCount = t.history.length; saveThreads(); }
|
|
265
272
|
} catch { /* leCore sidecar unreachable — thread still works, just not bound this round */ }
|
|
266
273
|
}
|
|
267
274
|
|
|
@@ -270,6 +277,15 @@ async function bindThread(t) {
|
|
|
270
277
|
// anything runs; 'auto' mode (set via "/mode auto" in chat) runs immediately.
|
|
271
278
|
// Either way this is not sandboxed like WRITE/READ — it can do anything the
|
|
272
279
|
// signed-in user's shell can — so 'ask' is the default, not 'auto'.
|
|
280
|
+
// Some underlying models were tuned with native tool-calling and leak their
|
|
281
|
+
// own control tokens (e.g. DeepSeek's "<||DSML||tool_calls>") as trailing
|
|
282
|
+
// plain text when this harness doesn't wire a `tools` schema. Left in, that
|
|
283
|
+
// text becomes part of the shell command string and breaks /bin/sh's parser
|
|
284
|
+
// ("unexpected token `newline'") — confirmed live. Strip it before exec.
|
|
285
|
+
function sanitizeRunCommand(command) {
|
|
286
|
+
return command.replace(/<\|+[^<>\n]*\|+>/g, '').trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
273
289
|
function execCommand(command, cwd) {
|
|
274
290
|
return new Promise((resolve) => {
|
|
275
291
|
exec(command, { cwd, timeout: 120000, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
package/lib/podagent.mjs
CHANGED
|
@@ -191,53 +191,84 @@ function hasImages(messages) {
|
|
|
191
191
|
// A non-ok response with no usable content used to silently become '', which
|
|
192
192
|
// grokui.mjs then renders as a generic "(no response)" — indistinguishable
|
|
193
193
|
// from a model that genuinely had nothing to say. Callers should know WHY.
|
|
194
|
-
function httpErrorNote(status) {
|
|
195
|
-
if (status === 402)
|
|
194
|
+
async function httpErrorNote(status) {
|
|
195
|
+
if (status === 402) {
|
|
196
|
+
// print the ACTUAL address in the chat, not a link elsewhere to go find it.
|
|
197
|
+
// fetch() does NOT reject on 4xx/5xx, so an older proxy without /wallet
|
|
198
|
+
// returns an error body that parses fine and yields "undefined" fields —
|
|
199
|
+
// check r.ok and the fields themselves before interpolating them.
|
|
200
|
+
try {
|
|
201
|
+
const r = await fetch(`${PROXY}/wallet`);
|
|
202
|
+
const w = r.ok ? await r.json() : null;
|
|
203
|
+
if (w?.funding && w?.evm) {
|
|
204
|
+
// funded === false is the genuinely-empty case; funded === true after
|
|
205
|
+
// the retries above means the rail/quote failed, not the balance
|
|
206
|
+
if (w.funded === false) {
|
|
207
|
+
return `(payment failed — HTTP 402, the wallet is empty. ${w.funding}. EVM (Base/Robinhood): ${w.evm}.)`;
|
|
208
|
+
}
|
|
209
|
+
return `(payment failed — HTTP 402 after ${PAYMENT_RETRIES} retries, though the wallet holds ${w.balances || 'a balance'}. Send it again; if it keeps failing the quoted asset may not be convertible right now. Fund with: ${w.funding})`;
|
|
210
|
+
}
|
|
211
|
+
} catch { /* proxy unreachable — fall through to the generic note */ }
|
|
212
|
+
return `(payment failed — HTTP 402 after ${PAYMENT_RETRIES} retries. Run \`npx openzoo\` to check wallet balances.)`;
|
|
213
|
+
}
|
|
196
214
|
if (status === 429) return '(rate limited — HTTP 429, try again in a moment)';
|
|
197
215
|
if (status >= 500) return `(upstream error — HTTP ${status}, try again)`;
|
|
198
216
|
return status ? `(request failed — HTTP ${status})` : '';
|
|
199
217
|
}
|
|
200
218
|
|
|
219
|
+
// A 402 that reached this layer means the proxy's own x402 retry gave up on
|
|
220
|
+
// this attempt, but the NEXT attempt usually settles (measured: same wallet,
|
|
221
|
+
// same rail, second call pays fine). Surfacing that as a chat message makes
|
|
222
|
+
// the user do the retry by hand — so do it here instead.
|
|
223
|
+
const PAYMENT_RETRIES = 3;
|
|
224
|
+
async function postChat(body, contextId) {
|
|
225
|
+
let r;
|
|
226
|
+
for (let attempt = 0; attempt <= PAYMENT_RETRIES; attempt++) {
|
|
227
|
+
r = await fetch(`${PROXY}/chat/completions`, {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: {
|
|
230
|
+
'content-type': 'application/json', authorization: 'Bearer sk-openzoo',
|
|
231
|
+
// real leCore memory for this thread, bound via POST /v1/hrr/bind — NOT
|
|
232
|
+
// a fabricated mechanism. Retrieval runs automatically once this header
|
|
233
|
+
// is set; nothing more for the model to invent or explain.
|
|
234
|
+
...(contextId ? { 'x-hrr-context': contextId } : {}),
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify(body),
|
|
237
|
+
});
|
|
238
|
+
if (r.status !== 402 || attempt === PAYMENT_RETRIES) return r;
|
|
239
|
+
await new Promise((res) => setTimeout(res, 800 * (attempt + 1)));
|
|
240
|
+
}
|
|
241
|
+
return r;
|
|
242
|
+
}
|
|
243
|
+
|
|
201
244
|
/** One openzoo chat turn. Paid per call by the box's own wallet via the local
|
|
202
245
|
* proxy — no key, no account. */
|
|
203
246
|
export async function brain(messages, contextId) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
},
|
|
213
|
-
// explicit, not relying on the gateway's "inject when caller said nothing"
|
|
214
|
-
// default — an explicit plugins array is always respected as-is, so this
|
|
215
|
-
// guarantees every bot on every model actually has web search, instead of
|
|
216
|
-
// hoping nothing upstream (local proxy, gateway config) already set one.
|
|
217
|
-
// 900 was cutting real (especially web-search-backed) answers off mid-sentence
|
|
218
|
-
body: JSON.stringify({ model: hasImages(messages) ? VISION_MODEL : MODEL, max_tokens: 4096, messages, plugins: [{ id: 'web' }] }),
|
|
219
|
-
});
|
|
247
|
+
// explicit plugins, not relying on the gateway's "inject when caller said
|
|
248
|
+
// nothing" default — an explicit array is always respected as-is, so every
|
|
249
|
+
// bot on every model actually has web search. max_tokens 900 was cutting
|
|
250
|
+
// real (especially web-search-backed) answers off mid-sentence.
|
|
251
|
+
const r = await postChat(
|
|
252
|
+
{ model: hasImages(messages) ? VISION_MODEL : MODEL, max_tokens: 4096, messages, plugins: [{ id: 'web' }] },
|
|
253
|
+
contextId,
|
|
254
|
+
);
|
|
220
255
|
const j = await r.json().catch(() => ({}));
|
|
221
256
|
const content = j?.choices?.[0]?.message?.content;
|
|
222
|
-
return content || (r.ok ? '' : httpErrorNote(r.status));
|
|
257
|
+
return content || (r.ok ? '' : await httpErrorNote(r.status));
|
|
223
258
|
}
|
|
224
259
|
|
|
225
260
|
/** Same call, but streamed — invokes onDelta(text) as tokens arrive (for a
|
|
226
261
|
* live-typing UI) and resolves with the full accumulated text at the end, so
|
|
227
262
|
* callers that need to parse a directive out of the complete reply still can. */
|
|
228
263
|
export async function brainStream(messages, onDelta, contextId) {
|
|
229
|
-
const r = await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
...(contextId ? { 'x-hrr-context': contextId } : {}),
|
|
234
|
-
},
|
|
235
|
-
body: JSON.stringify({ model: hasImages(messages) ? VISION_MODEL : MODEL, max_tokens: 4096, messages, plugins: [{ id: 'web' }], stream: true }),
|
|
236
|
-
});
|
|
264
|
+
const r = await postChat(
|
|
265
|
+
{ model: hasImages(messages) ? VISION_MODEL : MODEL, max_tokens: 4096, messages, plugins: [{ id: 'web' }], stream: true },
|
|
266
|
+
contextId,
|
|
267
|
+
);
|
|
237
268
|
if (!r.ok || !r.body) {
|
|
238
269
|
// fall back to the non-streaming path rather than fail outright
|
|
239
270
|
const content = await r.json().then((j) => j?.choices?.[0]?.message?.content).catch(() => undefined);
|
|
240
|
-
const text = content || (r.ok ? '' : httpErrorNote(r.status));
|
|
271
|
+
const text = content || (r.ok ? '' : await httpErrorNote(r.status));
|
|
241
272
|
if (text) onDelta(text);
|
|
242
273
|
return text;
|
|
243
274
|
}
|
package/lib/proxy.js
CHANGED
|
@@ -355,6 +355,23 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
355
355
|
return;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// Public addresses only — never the private key. Exists so a caller (the
|
|
359
|
+
// grokui error path, in particular) can print REAL funding instructions
|
|
360
|
+
// inline instead of telling the user to go look somewhere else.
|
|
361
|
+
if (req.method === 'GET' && (req.url || '').split('?')[0] === '/v1/wallet') {
|
|
362
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
363
|
+
res.end(JSON.stringify({
|
|
364
|
+
solana: client.address,
|
|
365
|
+
evm: client.evmAddress,
|
|
366
|
+
funding: fundingLine(client.address),
|
|
367
|
+
// from the same background poll the startup/refresh lines use, so a
|
|
368
|
+
// caller can tell a genuinely empty wallet from a transient 402
|
|
369
|
+
balances: balanceLine(lastSnap || []) || null,
|
|
370
|
+
funded: (lastSnap || []).some((b) => b.ui > 0),
|
|
371
|
+
}));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
358
375
|
if ((req.url || '').split('?')[0] === '/mcp') {
|
|
359
376
|
try {
|
|
360
377
|
const { handleMcpRequest } = await import('./mcphttp.js');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.7",
|
|
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",
|