openzoo 0.48.29 → 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/launch.js +8 -1
- package/lib/proxy.js +74 -4
- package/package.json +1 -1
package/lib/launch.js
CHANGED
|
@@ -173,7 +173,14 @@ export async function launchClaude(argv) {
|
|
|
173
173
|
// "how can I easily see what credit i'm left" — asked by a user who
|
|
174
174
|
// could not tell whether x402 had charged them at all.
|
|
175
175
|
+ 'const cr=(j.creditUsd==null)?"":(" \\u00b7 $"+Number(j.creditUsd).toFixed(2)+" credit");'
|
|
176
|
-
|
|
176
|
+
// The headline: what the same calls would have cost on OpenRouter.
|
|
177
|
+
// ALWAYS shown, 4dp, even at 1.0000. Hiding it when there is no saving
|
|
178
|
+
// reads as "no data"; printing 1.0000x says "measured, and it is level"
|
|
179
|
+
// — which is the difference between an omission and a fact.
|
|
180
|
+
+ 'const sx=Number(j.savingX);const sv=Number(j.savedUsd)||0;'
|
|
181
|
+
+ 'const col=(sx>1)?"\\x1b[32m":"\\x1b[90m";'
|
|
182
|
+
+ 'const save=(sx==null||!isFinite(sx))?"":(" \\u00b7 "+col+sx.toFixed(4)+"x vs direct"+(sv>0?(" ($"+sv.toFixed(4)+" saved)"):"")+"\\x1b[0m");'
|
|
183
|
+
+ 'process.stdout.write("\\x1b[38;5;208m\\u25cf\\x1b[0m openzoo $"+(Number(j.spendUsd)||0).toFixed(4)+" "+(j.paidCalls||0)+" call"+((j.paidCalls||0)===1?"":"s")+save+spill+cr+" \\u00b7 x402")}'
|
|
177
184
|
+ 'catch{process.stdout.write("\\x1b[38;5;208m\\u25cf\\x1b[0m openzoo \\u00b7 x402")}})\'\n');
|
|
178
185
|
fs.chmodSync(scriptPath, 0o755);
|
|
179
186
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
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,
|
|
@@ -577,6 +598,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
577
598
|
},
|
|
578
599
|
spendUsd: sessionSpent,
|
|
579
600
|
creditUsd,
|
|
601
|
+
// WHAT THE SAME CALLS WOULD HAVE COST DIRECT. Spend on its own is a
|
|
602
|
+
// bill; spend beside the counterfactual is the product. The receipt
|
|
603
|
+
// already carries directUsd per call — it simply never reached the
|
|
604
|
+
// status line, so the one number that justifies the tool was the one
|
|
605
|
+
// the user could not see.
|
|
606
|
+
directUsd: sessionDirect,
|
|
607
|
+
savedUsd: Math.max(0, sessionDirect - sessionSpent),
|
|
608
|
+
savingX: sessionSpent > 0 ? Number((sessionDirect / sessionSpent).toFixed(4)) : null,
|
|
580
609
|
paidCalls,
|
|
581
610
|
mcp: `${self.replace(/\/v1$/, '')}/mcp`,
|
|
582
611
|
upstream: config.apiBase,
|
|
@@ -759,6 +788,40 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
759
788
|
try {
|
|
760
789
|
const parsed = JSON.parse(bodyBuf.toString('utf8'));
|
|
761
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
|
+
}
|
|
762
825
|
// Tell the agent what it is actually connected to — in band, where it
|
|
763
826
|
// will read it, instead of leaving it to guess (and to chunk corpora
|
|
764
827
|
// it could bind whole). See lib/brief.js.
|
|
@@ -834,17 +897,24 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
834
897
|
} catch (err) {
|
|
835
898
|
log(`context cache skipped for this call: ${err.message}`);
|
|
836
899
|
}
|
|
837
|
-
const send = (buf, ctxId) => client.fetch(url, {
|
|
900
|
+
const send = (buf, ctxId, topK) => client.fetch(url, {
|
|
838
901
|
...init,
|
|
839
902
|
body: buf,
|
|
840
|
-
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,
|
|
841
911
|
});
|
|
842
912
|
let result;
|
|
843
913
|
if (cached) {
|
|
844
914
|
spillCalls += 1;
|
|
845
915
|
spilledChars += cached.corpus?.length || 0;
|
|
846
916
|
if (cached.reused) spillReuses += 1;
|
|
847
|
-
result = await send(cached.body, cached.contextId);
|
|
917
|
+
result = await send(cached.body, cached.contextId, cached.topK);
|
|
848
918
|
// Sidecar wiped between runs: the gateway 404s BEFORE the 402 (nothing
|
|
849
919
|
// paid). Never fail on a stale manifest — re-bind once and retry.
|
|
850
920
|
if (result.response.status === 404) {
|
|
@@ -853,7 +923,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
853
923
|
log('bound context is gone on the zoo — re-binding once...');
|
|
854
924
|
forgetContext(config.apiBase, cached.hash);
|
|
855
925
|
const rebound = await bindCorpus(cached.corpus, { force: true });
|
|
856
|
-
result = await send(cached.body, rebound.contextId);
|
|
926
|
+
result = await send(cached.body, rebound.contextId, cached.topK);
|
|
857
927
|
} else {
|
|
858
928
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
859
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",
|