openzoo 0.48.26 → 0.48.27
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/proxy.js +94 -3
- package/package.json +1 -1
package/lib/proxy.js
CHANGED
|
@@ -245,6 +245,91 @@ function replayPut(key, data, settle) {
|
|
|
245
245
|
* to sending the original body untouched — caching must never break a call.
|
|
246
246
|
* Returns null (send as-is) or { body, contextId, hash, corpus, reused, savedBytes }.
|
|
247
247
|
*/
|
|
248
|
+
/** Flatten one Anthropic content block to text leCore can index. */
|
|
249
|
+
function blockText(b) {
|
|
250
|
+
if (typeof b === 'string') return b;
|
|
251
|
+
if (!b || typeof b !== 'object') return '';
|
|
252
|
+
if (b.type === 'text') return b.text || '';
|
|
253
|
+
if (b.type === 'tool_use') return `[tool_use ${b.name}] ${JSON.stringify(b.input ?? {})}`;
|
|
254
|
+
if (b.type === 'tool_result') {
|
|
255
|
+
const c = b.content;
|
|
256
|
+
return `[tool_result] ${typeof c === 'string' ? c : (Array.isArray(c) ? c.map(blockText).join('\n') : JSON.stringify(c ?? ''))}`;
|
|
257
|
+
}
|
|
258
|
+
if (b.type === 'thinking') return ''; // never bind reasoning traces
|
|
259
|
+
return '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function msgText(m) {
|
|
263
|
+
const c = m?.content;
|
|
264
|
+
const body = typeof c === 'string' ? c : (Array.isArray(c) ? c.map(blockText).filter(Boolean).join('\n') : '');
|
|
265
|
+
return body ? `${(m.role || '?').toUpperCase()}: ${body}` : '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Spill the OLD prefix of a long TRANSCRIPT into leCore.
|
|
270
|
+
*
|
|
271
|
+
* WHY THIS EXISTS. The only spill was the corpus+question shape below, which
|
|
272
|
+
* requires the last message to be one big string ending in `\n\n<question>` —
|
|
273
|
+
* true for zoo_ask, never true for an agent. `npx openzoo claude` therefore
|
|
274
|
+
* spilled NOTHING, hit Claude Code's own context ceiling, and auto-compacted,
|
|
275
|
+
* on a product whose pitch is that it does not have to. Compaction was honest
|
|
276
|
+
* given nothing was offloaded; this is what makes the claim true.
|
|
277
|
+
*
|
|
278
|
+
* Runs on the OpenAI shape ON PURPOSE. /v1/messages is translated by
|
|
279
|
+
* anthropicToOpenAI and rewritten to /v1/chat/completions BEFORE this is
|
|
280
|
+
* reached, so operating here covers Claude Code, Cursor and the raw API with
|
|
281
|
+
* one implementation instead of three that can drift.
|
|
282
|
+
*
|
|
283
|
+
* THE CUT POINT IS NOT NEGOTIABLE. An assistant `tool_calls` must be answered
|
|
284
|
+
* by role:"tool" messages or the upstream 400s, so the transcript may only be
|
|
285
|
+
* severed at a plain `user` message — everything before one is self-contained.
|
|
286
|
+
* A system message is never spilled: it is the operating contract, not history.
|
|
287
|
+
*/
|
|
288
|
+
async function spillTranscript(body, log) {
|
|
289
|
+
const msgs = Array.isArray(body?.messages) ? body.messages : null;
|
|
290
|
+
if (!msgs || msgs.length < 6) return null;
|
|
291
|
+
|
|
292
|
+
// Keep the recent tail, but never more than half the transcript: a fixed 8 on
|
|
293
|
+
// a 10-message body left only index 2 to search, which is rarely a user turn,
|
|
294
|
+
// so a SHORT-but-huge transcript (one giant tool_result) silently never
|
|
295
|
+
// spilled — the exact case an agent hits first.
|
|
296
|
+
const keepTail = Math.min(
|
|
297
|
+
Number(process.env.OPENZOO_KEEP_TAIL_MSGS || 8),
|
|
298
|
+
Math.max(2, Math.floor(msgs.length / 2)),
|
|
299
|
+
);
|
|
300
|
+
const firstSpillable = msgs.findIndex((m) => m?.role !== 'system');
|
|
301
|
+
if (firstSpillable < 0) return null;
|
|
302
|
+
|
|
303
|
+
let cut = -1;
|
|
304
|
+
for (let i = msgs.length - keepTail; i > firstSpillable; i--) {
|
|
305
|
+
if (msgs[i]?.role === 'user') { cut = i; break; }
|
|
306
|
+
}
|
|
307
|
+
if (cut <= firstSpillable) return null; // nothing safely severable
|
|
308
|
+
|
|
309
|
+
const head = msgs.slice(0, firstSpillable); // system block, always kept
|
|
310
|
+
const corpus = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
|
|
311
|
+
if (corpus.length <= BIND_MIN_CHARS) return null;
|
|
312
|
+
|
|
313
|
+
const bind = await bindCorpus(corpus, {
|
|
314
|
+
onStage: (stage, info) => {
|
|
315
|
+
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const sent = msgs.length - cut;
|
|
319
|
+
log(bind.reused
|
|
320
|
+
? `transcript prefix already bound (${bind.contextId}) — sending ${sent}/${msgs.length} turns`
|
|
321
|
+
: `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}) — sending ${sent}/${msgs.length} turns`);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
body: Buffer.from(JSON.stringify({ ...body, messages: [...head, ...msgs.slice(cut)] })),
|
|
325
|
+
contextId: bind.contextId,
|
|
326
|
+
hash: bind.hash,
|
|
327
|
+
corpus,
|
|
328
|
+
reused: bind.reused,
|
|
329
|
+
savedBytes: bind.bytes,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
248
333
|
async function maybeCacheCorpus(req, bodyBuf, log) {
|
|
249
334
|
if (contextCacheDisabled()) return null;
|
|
250
335
|
if (req.method !== 'POST' || !(req.url || '').includes('/chat/completions')) return null;
|
|
@@ -254,13 +339,19 @@ async function maybeCacheCorpus(req, bodyBuf, log) {
|
|
|
254
339
|
try { body = JSON.parse(bodyBuf.toString('utf8')); } catch { return null; }
|
|
255
340
|
const msgs = Array.isArray(body?.messages) ? body.messages : null;
|
|
256
341
|
if (!msgs?.length) return null;
|
|
342
|
+
// CORPUS+QUESTION first — one huge final message ending in \n\n<ask>. That is
|
|
343
|
+
// what zoo_ask and the chat surface send, and binding exactly that body keeps
|
|
344
|
+
// the ask verbatim. Anything else (an agent transcript) falls through to the
|
|
345
|
+
// transcript spill, which used to be a silent no-op.
|
|
257
346
|
const last = msgs[msgs.length - 1];
|
|
258
|
-
|
|
347
|
+
const oneShot = typeof last?.content === 'string'
|
|
348
|
+
&& last.content.length > BIND_MIN_CHARS
|
|
349
|
+
&& last.content.lastIndexOf('\n\n') >= BIND_MIN_CHARS;
|
|
350
|
+
if (!oneShot) return spillTranscript(body, log);
|
|
259
351
|
const cut = last.content.lastIndexOf('\n\n');
|
|
260
|
-
if (cut < BIND_MIN_CHARS) return null;
|
|
261
352
|
const corpus = last.content.slice(0, cut);
|
|
262
353
|
const ask = last.content.slice(cut + 2).trim();
|
|
263
|
-
if (!ask || ask.length > 8000) return
|
|
354
|
+
if (!ask || ask.length > 8000) return spillTranscript(body, log);
|
|
264
355
|
|
|
265
356
|
const bind = await bindCorpus(corpus, {
|
|
266
357
|
onStage: (stage, info) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.27",
|
|
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",
|