openzoo 0.48.74 → 0.48.75
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 +9 -2
- package/lib/spill.js +109 -16
- package/package.json +1 -1
package/lib/proxy.js
CHANGED
|
@@ -749,6 +749,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
749
749
|
mkdirSync(path.dirname(sayFile), { recursive: true });
|
|
750
750
|
} catch { sayFile = null; }
|
|
751
751
|
}
|
|
752
|
+
// Spill/adapt/classifier lines must use this channel: `log` is a no-op when
|
|
753
|
+
// `openzoo claude` starts us with silent:true, and printing them on stdout
|
|
754
|
+
// corrupts the Claude Code TTY. say() writes ~/.openzoo/proxy.log then.
|
|
752
755
|
const say = (...a) => {
|
|
753
756
|
const line = a.join(' ');
|
|
754
757
|
if (sayFile) { try { appendFileSync(sayFile, line + '\n'); return; } catch { /* fall through */ } }
|
|
@@ -1187,7 +1190,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1187
1190
|
const policy = rewriteChatModel(parsed, ids, { bodyLen: bodyBuf.length });
|
|
1188
1191
|
parsed = policy.parsed;
|
|
1189
1192
|
if (policy.tiny) {
|
|
1190
|
-
|
|
1193
|
+
// `openzoo claude` starts us silent � `log` is a no-op then. say()
|
|
1194
|
+
// is the proxy.log channel and never the Claude Code TTY.
|
|
1195
|
+
say(`classifier tiny max_tokens=${Number(parsed?.max_tokens)} "${policy.from}" -> ${policy.to} (no reasoning floor)`);
|
|
1191
1196
|
} else {
|
|
1192
1197
|
// SAY WHETHER THE OVERRIDE IS ACTUALLY SET, and name it.
|
|
1193
1198
|
//
|
|
@@ -1372,7 +1377,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1372
1377
|
try {
|
|
1373
1378
|
let cached = null;
|
|
1374
1379
|
try {
|
|
1375
|
-
|
|
1380
|
+
// Spill/adapt diagnostics (adapt, file-stub, sending N/M) must hit
|
|
1381
|
+
// ~/.openzoo/proxy.log when we are silent. `log` is a no-op then.
|
|
1382
|
+
cached = await maybeCacheCorpus(req, bodyBuf, say, spill);
|
|
1376
1383
|
} catch (err) {
|
|
1377
1384
|
log(`context cache skipped for this call: ${err.message}`);
|
|
1378
1385
|
}
|
package/lib/spill.js
CHANGED
|
@@ -492,9 +492,26 @@ export function looksLikeFileView(command) {
|
|
|
492
492
|
return sawView;
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
-
|
|
495
|
+
/** Tool result larger than this is "fat" — stub it in the forwarded tail. */
|
|
496
|
+
export const FAT_TOOL_CHARS = 400;
|
|
497
|
+
|
|
498
|
+
export function fileBoundStub(paths, n) {
|
|
496
499
|
const list = [...new Set((paths || []).filter(Boolean))].join(' ');
|
|
497
|
-
|
|
500
|
+
const mark = Number.isFinite(n) && n > 0 ? `[bound, ${n} chars]` : '[bound]';
|
|
501
|
+
return list ? `FILE ${list} ${mark}` : `FILE ${mark}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Generic stub for WebSearch / Fetch / Bash / any fat tool_result. */
|
|
505
|
+
export function toolResultStub(n, paths) {
|
|
506
|
+
const list = [...new Set((paths || []).filter(Boolean))].join(' ');
|
|
507
|
+
const mark = `[bound, ${Number(n) || 0} chars]`;
|
|
508
|
+
return list ? `FILE ${list} ${mark}` : mark;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function isStubText(content) {
|
|
512
|
+
if (typeof content === 'string') return /\[bound(?:, \d+ chars)?\]/.test(content);
|
|
513
|
+
if (Array.isArray(content)) return content.some((b) => isStubText(typeof b === 'string' ? b : b?.text ?? b?.content));
|
|
514
|
+
return false;
|
|
498
515
|
}
|
|
499
516
|
|
|
500
517
|
function toolContentLength(content) {
|
|
@@ -524,10 +541,15 @@ function resolveBoundPath(raw, cwd, boundAbs) {
|
|
|
524
541
|
* (live: 5MB filebind, lastSend 13/107, savingX 1.22 instead of ~7x).
|
|
525
542
|
*
|
|
526
543
|
* Cheap rewrite — no disk I/O. First-read results (not yet in boundAbs) and
|
|
527
|
-
* non-file tool output
|
|
544
|
+
* non-file tool output stay verbatim UNLESS `aggressive` / `stubMore` or a
|
|
545
|
+
* tail `budget` is set. Those two are what beat a WebSearch/Fetch/Bash
|
|
546
|
+
* storm: the 800-byte floor cannot move `cut` inside a tool chain
|
|
547
|
+
* (`isSeverable` is false), so the byte budget has to win by stubbing
|
|
548
|
+
* bodies instead of deleting the tool_use / tool_result pairing.
|
|
528
549
|
*
|
|
529
550
|
* `fromIndex` limits the rewrite to the forwarded tail so the spilled prefix
|
|
530
|
-
* that becomes the conversation corpus is unchanged.
|
|
551
|
+
* that becomes the conversation corpus is unchanged. The last user ask is
|
|
552
|
+
* never rewritten.
|
|
531
553
|
*/
|
|
532
554
|
export function stubBoundFileResults(msgs, {
|
|
533
555
|
boundFiles,
|
|
@@ -536,10 +558,16 @@ export function stubBoundFileResults(msgs, {
|
|
|
536
558
|
fromIndex = 0,
|
|
537
559
|
// When the live tuner is below target, stub file-view results even if
|
|
538
560
|
// this turn has not yet recorded them in boundAbs (first-read bodies).
|
|
561
|
+
// Also stubs fat non-file tools (WebSearch / Fetch / Bash).
|
|
539
562
|
aggressive = false,
|
|
563
|
+
// When the forwarded tail is over this many chars, stub older tool_result
|
|
564
|
+
// bodies (oldest first) until it fits. Pairing stays; the ask stays.
|
|
565
|
+
budget = null,
|
|
566
|
+
fatChars = FAT_TOOL_CHARS,
|
|
540
567
|
} = {}) {
|
|
541
568
|
const absSet = boundAbs || boundAbsFromKeys(boundFiles);
|
|
542
|
-
|
|
569
|
+
const wantBudget = budget != null && Number.isFinite(Number(budget));
|
|
570
|
+
if (!Array.isArray(msgs) || (!absSet.size && !aggressive && !wantBudget)) {
|
|
543
571
|
return { messages: msgs, stubbed: 0, dropped: 0 };
|
|
544
572
|
}
|
|
545
573
|
|
|
@@ -585,32 +613,91 @@ export function stubBoundFileResults(msgs, {
|
|
|
585
613
|
}
|
|
586
614
|
}
|
|
587
615
|
|
|
588
|
-
|
|
616
|
+
const lastUser = lastUserAskIndex(msgs, firstSpillableIndex(msgs));
|
|
617
|
+
const fatLimit = Number.isFinite(Number(fatChars)) ? Number(fatChars) : FAT_TOOL_CHARS;
|
|
618
|
+
|
|
619
|
+
const stubFor = (id, n) => {
|
|
620
|
+
const paths = idPaths.get(id);
|
|
621
|
+
return paths?.length ? fileBoundStub(paths, n) : toolResultStub(n);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const shouldStubBody = (id, n) => {
|
|
625
|
+
if (!n || isStubText(typeof n === 'number' ? '' : n)) return false;
|
|
626
|
+
if (stubIds.has(id)) return true;
|
|
627
|
+
if (aggressive && n >= fatLimit) return true;
|
|
628
|
+
return false;
|
|
629
|
+
};
|
|
589
630
|
|
|
590
631
|
let stubbed = 0;
|
|
591
632
|
let dropped = 0;
|
|
592
|
-
|
|
593
|
-
if (i < fromIndex || !m) return m;
|
|
594
|
-
if (m.role === 'tool'
|
|
633
|
+
let messages = msgs.map((m, i) => {
|
|
634
|
+
if (i < fromIndex || !m || i === lastUser) return m;
|
|
635
|
+
if (m.role === 'tool') {
|
|
595
636
|
const n = toolContentLength(m.content);
|
|
596
|
-
if (!n) return m;
|
|
637
|
+
if (!shouldStubBody(m.tool_call_id, n) || isStubText(m.content)) return m;
|
|
597
638
|
dropped += n;
|
|
598
639
|
stubbed += 1;
|
|
599
|
-
return { ...m, content:
|
|
640
|
+
return { ...m, content: stubFor(m.tool_call_id, n) };
|
|
600
641
|
}
|
|
601
642
|
if (!Array.isArray(m.content)) return m;
|
|
602
643
|
let changed = false;
|
|
603
644
|
const blocks = m.content.map((b) => {
|
|
604
|
-
if (b?.type !== 'tool_result'
|
|
645
|
+
if (b?.type !== 'tool_result') return b;
|
|
605
646
|
const n = toolContentLength(b.content);
|
|
606
|
-
if (!n) return b;
|
|
647
|
+
if (!shouldStubBody(b.tool_use_id, n) || isStubText(b.content)) return b;
|
|
607
648
|
dropped += n;
|
|
608
649
|
stubbed += 1;
|
|
609
650
|
changed = true;
|
|
610
|
-
return { ...b, content:
|
|
651
|
+
return { ...b, content: stubFor(b.tool_use_id, n) };
|
|
611
652
|
});
|
|
612
653
|
return changed ? { ...m, content: blocks } : m;
|
|
613
654
|
});
|
|
655
|
+
|
|
656
|
+
// Byte budget wins inside a tool chain. cutTranscript cannot move tailStart
|
|
657
|
+
// past assistant(tool_calls) / role:tool (pairing 400s the provider), so a
|
|
658
|
+
// 15-result storm used to ride in full at the 800-byte floor. Stub older
|
|
659
|
+
// bodies first; never drop the ask or the assistant tool_calls themselves.
|
|
660
|
+
if (wantBudget) {
|
|
661
|
+
const cap = Number(budget);
|
|
662
|
+
let used = sliceChars(messages, fromIndex);
|
|
663
|
+
if (used > cap) {
|
|
664
|
+
const next = messages.slice();
|
|
665
|
+
for (let i = fromIndex; i < next.length && used > cap; i++) {
|
|
666
|
+
if (i === lastUser) continue;
|
|
667
|
+
const m = next[i];
|
|
668
|
+
if (!m) continue;
|
|
669
|
+
if (m.role === 'tool') {
|
|
670
|
+
if (isStubText(m.content)) continue;
|
|
671
|
+
const n = toolContentLength(m.content);
|
|
672
|
+
if (!n) continue;
|
|
673
|
+
const stub = stubFor(m.tool_call_id, n);
|
|
674
|
+
if (stub.length >= n) continue;
|
|
675
|
+
used = used - n + stub.length;
|
|
676
|
+
next[i] = { ...m, content: stub };
|
|
677
|
+
stubbed += 1;
|
|
678
|
+
dropped += n;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (!Array.isArray(m.content)) continue;
|
|
682
|
+
let changed = false;
|
|
683
|
+
const blocks = m.content.map((b) => {
|
|
684
|
+
if (b?.type !== 'tool_result' || used <= cap || isStubText(b.content)) return b;
|
|
685
|
+
const n = toolContentLength(b.content);
|
|
686
|
+
if (!n) return b;
|
|
687
|
+
const stub = stubFor(b.tool_use_id, n);
|
|
688
|
+
if (stub.length >= n) return b;
|
|
689
|
+
used = used - n + stub.length;
|
|
690
|
+
stubbed += 1;
|
|
691
|
+
dropped += n;
|
|
692
|
+
changed = true;
|
|
693
|
+
return { ...b, content: stub };
|
|
694
|
+
});
|
|
695
|
+
if (changed) next[i] = { ...m, content: blocks };
|
|
696
|
+
}
|
|
697
|
+
messages = next;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
614
701
|
return { messages, stubbed, dropped };
|
|
615
702
|
}
|
|
616
703
|
|
|
@@ -940,6 +1027,11 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
940
1027
|
return { cut: -1, firstSpillable, lastUser: lastUserAskIndex(msgs, firstSpillable), knobs: k };
|
|
941
1028
|
}
|
|
942
1029
|
|
|
1030
|
+
// Only moves the cut at a severable index. A current-turn tool storm
|
|
1031
|
+
// (assistant(tool_calls) + N tool results + user ask) has no severable
|
|
1032
|
+
// index inside the chain, so this walk is a no-op — the byte budget is
|
|
1033
|
+
// applied by stubbing bodies in stubBoundFileResults, not by orphaning
|
|
1034
|
+
// a tool_result.
|
|
943
1035
|
let tailStart = cut;
|
|
944
1036
|
{
|
|
945
1037
|
let used = 0;
|
|
@@ -979,6 +1071,7 @@ function stubForCut(msgs, cut, opts) {
|
|
|
979
1071
|
cwd: opts.cwd,
|
|
980
1072
|
fromIndex: cut,
|
|
981
1073
|
aggressive: Boolean(opts.aggressive),
|
|
1074
|
+
budget: opts.budget,
|
|
982
1075
|
});
|
|
983
1076
|
}
|
|
984
1077
|
|
|
@@ -1033,7 +1126,7 @@ export function applySpillCut(msgs, {
|
|
|
1033
1126
|
};
|
|
1034
1127
|
};
|
|
1035
1128
|
|
|
1036
|
-
let stubbed = stubForCut(msgs, plan.cut, { boundFiles, boundAbs, cwd, aggressive: k.stubMore });
|
|
1129
|
+
let stubbed = stubForCut(msgs, plan.cut, { boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget });
|
|
1037
1130
|
let stats = measure(plan.cut, stubbed, k);
|
|
1038
1131
|
let action = 'hold';
|
|
1039
1132
|
|
|
@@ -1049,7 +1142,7 @@ export function applySpillCut(msgs, {
|
|
|
1049
1142
|
if (decision.recut) {
|
|
1050
1143
|
plan = cutTranscript(msgs, k);
|
|
1051
1144
|
if (plan.cut > plan.firstSpillable) {
|
|
1052
|
-
stubbed = stubForCut(msgs, plan.cut, { boundFiles, boundAbs, cwd, aggressive: k.stubMore });
|
|
1145
|
+
stubbed = stubForCut(msgs, plan.cut, { boundFiles, boundAbs, cwd, aggressive: k.stubMore, budget: k.budget });
|
|
1053
1146
|
stats = measure(plan.cut, stubbed, k);
|
|
1054
1147
|
}
|
|
1055
1148
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.75",
|
|
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",
|