openzoo 0.48.30 → 0.48.31
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 +66 -4
- package/package.json +1 -1
package/lib/proxy.js
CHANGED
|
@@ -331,8 +331,29 @@ async function spillTranscript(body, log) {
|
|
|
331
331
|
? `transcript prefix already bound (${bind.contextId}) — sending ${sent}/${msgs.length} turns`
|
|
332
332
|
: `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}) — sending ${sent}/${msgs.length} turns`);
|
|
333
333
|
|
|
334
|
+
// ADAPTIVE TOP-K. A fixed 32 chunks is what was actually eating the saving:
|
|
335
|
+
// MEASURED on a 56,265-token corpus, top_k 32 handed 9,990 tokens back and
|
|
336
|
+
// scored 2.45x, while 8 handed back 2,574 and scored 4.73x — same answer,
|
|
337
|
+
// same corpus, nearly double the saving. Recall breadth, not markup, is the
|
|
338
|
+
// lever, and spilling 34k tokens only to recall 22k of them back is not a
|
|
339
|
+
// saving, it is a round trip.
|
|
340
|
+
//
|
|
341
|
+
// So budget the recall in TOKENS and derive k from it, rather than fixing the
|
|
342
|
+
// chunk count and letting the token cost fall where it may. ~320 tokens per
|
|
343
|
+
// chunk measured. The budget scales with the ask — a one-line question needs
|
|
344
|
+
// far less context than a detailed one — and is clamped so a huge ask cannot
|
|
345
|
+
// drag the whole corpus back in.
|
|
346
|
+
const askChars = msgText(msgs[msgs.length - 1] || {}).length;
|
|
347
|
+
const budget = Math.min(
|
|
348
|
+
Number(process.env.OPENZOO_RECALL_MAX_TOKENS || 6000),
|
|
349
|
+
Math.max(Number(process.env.OPENZOO_RECALL_MIN_TOKENS || 1500),
|
|
350
|
+
Math.round(askChars / 2)),
|
|
351
|
+
);
|
|
352
|
+
const topK = Math.max(4, Math.min(32, Math.round(budget / 320)));
|
|
353
|
+
|
|
334
354
|
return {
|
|
335
355
|
body: Buffer.from(JSON.stringify({ ...body, messages: [...head, ...msgs.slice(cut)] })),
|
|
356
|
+
topK,
|
|
336
357
|
contextId: bind.contextId,
|
|
337
358
|
hash: bind.hash,
|
|
338
359
|
corpus,
|
|
@@ -767,6 +788,40 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
767
788
|
try {
|
|
768
789
|
const parsed = JSON.parse(bodyBuf.toString('utf8'));
|
|
769
790
|
wantsStream = parsed?.stream === true;
|
|
791
|
+
// REASONING MODELS SPEND max_tokens ON THINKING FIRST.
|
|
792
|
+
//
|
|
793
|
+
// The budget covers hidden reasoning AND the visible answer, so a
|
|
794
|
+
// caller that asks for 40 tokens because it wants a short answer often
|
|
795
|
+
// gets ZERO — the whole allowance went to reasoning and the completion
|
|
796
|
+
// truncated to an empty string. Measured across three families in one
|
|
797
|
+
// day: deepseek returned 0 chars at 8k and was fine at 24k; grok-4.6
|
|
798
|
+
// pinned ct at exactly its 16,000 budget with no visible output;
|
|
799
|
+
// sonnet-5 truncated a 600-token file mid-function because Anthropic's
|
|
800
|
+
// max_tokens covers thinking too.
|
|
801
|
+
//
|
|
802
|
+
// An empty completion is not an error — it bills normally and renders
|
|
803
|
+
// as a blank reply — so this fails silently and looks like the retrieval
|
|
804
|
+
// broke. It cost real debugging time tonight for exactly that reason.
|
|
805
|
+
// Multiply the allowance for known reasoning families and let callers
|
|
806
|
+
// keep asking for what they actually want back.
|
|
807
|
+
const REASONING = /(deepseek|grok|o[134](-|$)|reasoner|thinking|-pro\b|sol-pro|qwq)/i;
|
|
808
|
+
const mult = Number(process.env.OPENZOO_REASONING_MAX_TOKENS_X || 4);
|
|
809
|
+
const cap = Number(process.env.OPENZOO_REASONING_MAX_TOKENS_CAP || 32000);
|
|
810
|
+
const mdl = String(parsed?.model || '');
|
|
811
|
+
const mt = Number(parsed?.max_tokens);
|
|
812
|
+
// A MULTIPLIER ALONE IS NOT ENOUGH. 4x on a caller's 40 is 160, which is
|
|
813
|
+
// still nothing for a model that thinks first — measured, 2 of 3 runs
|
|
814
|
+
// still returned empty at 160. Reasoning needs an absolute floor, not a
|
|
815
|
+
// relative bump, so take whichever is larger.
|
|
816
|
+
const floor = Number(process.env.OPENZOO_REASONING_MIN_TOKENS || 4000);
|
|
817
|
+
if (mult > 1 && REASONING.test(mdl) && Number.isFinite(mt) && mt > 0 && mt < cap) {
|
|
818
|
+
const raised = Math.min(cap, Math.max(floor, Math.round(mt * mult)));
|
|
819
|
+
if (raised > mt) {
|
|
820
|
+
parsed.max_tokens = raised;
|
|
821
|
+
bodyBuf = Buffer.from(JSON.stringify(parsed));
|
|
822
|
+
log(`reasoning model ${mdl}: max_tokens ${mt} -> ${raised} (thinking shares the budget; OPENZOO_REASONING_MAX_TOKENS_X=1 disables)`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
770
825
|
// Tell the agent what it is actually connected to — in band, where it
|
|
771
826
|
// will read it, instead of leaving it to guess (and to chunk corpora
|
|
772
827
|
// it could bind whole). See lib/brief.js.
|
|
@@ -842,17 +897,24 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
842
897
|
} catch (err) {
|
|
843
898
|
log(`context cache skipped for this call: ${err.message}`);
|
|
844
899
|
}
|
|
845
|
-
const send = (buf, ctxId) => client.fetch(url, {
|
|
900
|
+
const send = (buf, ctxId, topK) => client.fetch(url, {
|
|
846
901
|
...init,
|
|
847
902
|
body: buf,
|
|
848
|
-
headers: ctxId
|
|
903
|
+
headers: ctxId
|
|
904
|
+
? {
|
|
905
|
+
...init.headers,
|
|
906
|
+
'x-hrr-context': ctxId,
|
|
907
|
+
// Only on the spill path — a caller that set its own top-k keeps it.
|
|
908
|
+
...(topK ? { 'x-hrr-top-k': String(topK) } : {}),
|
|
909
|
+
}
|
|
910
|
+
: init.headers,
|
|
849
911
|
});
|
|
850
912
|
let result;
|
|
851
913
|
if (cached) {
|
|
852
914
|
spillCalls += 1;
|
|
853
915
|
spilledChars += cached.corpus?.length || 0;
|
|
854
916
|
if (cached.reused) spillReuses += 1;
|
|
855
|
-
result = await send(cached.body, cached.contextId);
|
|
917
|
+
result = await send(cached.body, cached.contextId, cached.topK);
|
|
856
918
|
// Sidecar wiped between runs: the gateway 404s BEFORE the 402 (nothing
|
|
857
919
|
// paid). Never fail on a stale manifest — re-bind once and retry.
|
|
858
920
|
if (result.response.status === 404) {
|
|
@@ -861,7 +923,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
861
923
|
log('bound context is gone on the zoo — re-binding once...');
|
|
862
924
|
forgetContext(config.apiBase, cached.hash);
|
|
863
925
|
const rebound = await bindCorpus(cached.corpus, { force: true });
|
|
864
|
-
result = await send(cached.body, rebound.contextId);
|
|
926
|
+
result = await send(cached.body, rebound.contextId, cached.topK);
|
|
865
927
|
} else {
|
|
866
928
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
867
929
|
res.end(text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.31",
|
|
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",
|