openzoo 0.48.30 → 0.48.32

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.
Files changed (2) hide show
  1. package/lib/proxy.js +66 -4
  2. 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 ? { ...init.headers, 'x-hrr-context': ctxId } : init.headers,
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.30",
3
+ "version": "0.48.32",
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",