openzoo 0.48.27 → 0.48.29
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 +28 -1
- package/lib/proxy.js +46 -0
- package/package.json +1 -1
package/lib/launch.js
CHANGED
|
@@ -164,7 +164,16 @@ export async function launchClaude(argv) {
|
|
|
164
164
|
+ `${JSON.stringify(process.execPath)} -e `
|
|
165
165
|
+ '\'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{'
|
|
166
166
|
+ 'try{const j=JSON.parse(s);'
|
|
167
|
-
|
|
167
|
+
// Spend alone is the COST with none of the benefit. The spill figure is
|
|
168
|
+
// the context that did NOT ride upstream — the thing being paid for.
|
|
169
|
+
+ 'const sp=j.spilled||{};'
|
|
170
|
+
+ 'const tk=Number(sp.tokensApprox)||0;'
|
|
171
|
+
+ 'const ht=tk>=1e6?(tk/1e6).toFixed(1)+"M":tk>=1e3?Math.round(tk/1e3)+"k":String(tk);'
|
|
172
|
+
+ 'const spill=tk?(" \\u00b7 "+ht+" tok offloaded"):"";'
|
|
173
|
+
// "how can I easily see what credit i'm left" — asked by a user who
|
|
174
|
+
// could not tell whether x402 had charged them at all.
|
|
175
|
+
+ 'const cr=(j.creditUsd==null)?"":(" \\u00b7 $"+Number(j.creditUsd).toFixed(2)+" credit");'
|
|
176
|
+
+ '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")+spill+cr+" \\u00b7 x402")}'
|
|
168
177
|
+ 'catch{process.stdout.write("\\x1b[38;5;208m\\u25cf\\x1b[0m openzoo \\u00b7 x402")}})\'\n');
|
|
169
178
|
fs.chmodSync(scriptPath, 0o755);
|
|
170
179
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
@@ -172,12 +181,30 @@ export async function launchClaude(argv) {
|
|
|
172
181
|
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) || {}; } catch { settings = {}; }
|
|
173
182
|
const prev = settings.statusLine;
|
|
174
183
|
settings.statusLine = { type: 'command', command: `sh ${scriptPath}` };
|
|
184
|
+
// AUTO-COMPACT OFF — and only correct BECAUSE the proxy spills.
|
|
185
|
+
//
|
|
186
|
+
// Claude Code counts the WHOLE local transcript and compacts on its own
|
|
187
|
+
// ceiling; it cannot know the proxy binds the old prefix into leCore and
|
|
188
|
+
// forwards only the system block plus the recent tail. So it was
|
|
189
|
+
// destroying history to stay under a limit the upstream request never
|
|
190
|
+
// approached — on a product whose pitch is that it does not have to.
|
|
191
|
+
//
|
|
192
|
+
// This would have been RECKLESS before spillTranscript existed: with the
|
|
193
|
+
// full body going upstream, disabling compaction just moves the failure
|
|
194
|
+
// from a lossy summary to a hard context error. It is safe now precisely
|
|
195
|
+
// because OPENZOO_KEEP_TAIL_MSGS bounds what is actually sent.
|
|
196
|
+
// OPENZOO_KEEP_COMPACT=1 restores stock behaviour.
|
|
197
|
+
const prevCompact = settings.autoCompactEnabled;
|
|
198
|
+
if (process.env.OPENZOO_KEEP_COMPACT !== '1') settings.autoCompactEnabled = false;
|
|
175
199
|
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
176
200
|
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
177
201
|
restoreStatus = () => {
|
|
178
202
|
try {
|
|
179
203
|
const cur = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
180
204
|
if (prev === undefined) delete cur.statusLine; else cur.statusLine = prev;
|
|
205
|
+
// Never leave a global setting mutated after we exit.
|
|
206
|
+
if (prevCompact === undefined) delete cur.autoCompactEnabled;
|
|
207
|
+
else cur.autoCompactEnabled = prevCompact;
|
|
181
208
|
fs.writeFileSync(settingsPath, `${JSON.stringify(cur, null, 2)}\n`);
|
|
182
209
|
} catch { /* leave as-is */ }
|
|
183
210
|
};
|
package/lib/proxy.js
CHANGED
|
@@ -304,6 +304,17 @@ async function spillTranscript(body, log) {
|
|
|
304
304
|
for (let i = msgs.length - keepTail; i > firstSpillable; i--) {
|
|
305
305
|
if (msgs[i]?.role === 'user') { cut = i; break; }
|
|
306
306
|
}
|
|
307
|
+
// FALL BACK TO THE LAST SEVERABLE TURN. The keepTail window is a preference,
|
|
308
|
+
// not a requirement: a transcript can be enormous and still have very few
|
|
309
|
+
// user turns (one huge document, then tool traffic), and on those the window
|
|
310
|
+
// contained no `user` message at all — so nothing spilled and the whole body
|
|
311
|
+
// went upstream while the counter honestly reported 0. Keep at least the
|
|
312
|
+
// final turn; anything earlier that is severable is better than not spilling.
|
|
313
|
+
if (cut <= firstSpillable) {
|
|
314
|
+
for (let i = msgs.length - 2; i > firstSpillable; i--) {
|
|
315
|
+
if (msgs[i]?.role === 'user') { cut = i; break; }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
307
318
|
if (cut <= firstSpillable) return null; // nothing safely severable
|
|
308
319
|
|
|
309
320
|
const head = msgs.slice(0, firstSpillable); // system block, always kept
|
|
@@ -430,6 +441,27 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
430
441
|
// "is the editor really routing through us?" — an editor that silently keeps
|
|
431
442
|
// using its own backend leaves this at 0 while looking perfectly healthy.
|
|
432
443
|
let servedRequests = 0;
|
|
444
|
+
// SPILL ACCOUNTING. The product's whole claim is that context is offloaded
|
|
445
|
+
// instead of re-sent, and nothing measured it — the status line showed spend
|
|
446
|
+
// and call count, which is the cost side with none of the benefit.
|
|
447
|
+
let spillCalls = 0;
|
|
448
|
+
let spilledChars = 0;
|
|
449
|
+
let spillReuses = 0;
|
|
450
|
+
// CREDIT, CACHED. Users cannot tell prepaid credit from wallet balance and
|
|
451
|
+
// have to guess whether a call was even paid for ("I don't think x402 made me
|
|
452
|
+
// pay this at all"). The status line runs EVERY turn, so this is refreshed at
|
|
453
|
+
// most every 20s and served stale in between — a status line must never add
|
|
454
|
+
// latency to the thing it is describing.
|
|
455
|
+
let creditUsd = null;
|
|
456
|
+
let creditAt = 0;
|
|
457
|
+
const refreshCredit = () => {
|
|
458
|
+
if (Date.now() - creditAt < 20000) return;
|
|
459
|
+
creditAt = Date.now();
|
|
460
|
+
fetch(`${config.apiBase}/v1/credits`, { headers: withNamespace({}), signal: AbortSignal.timeout(4000) })
|
|
461
|
+
.then((r) => r.json())
|
|
462
|
+
.then((j) => { creditUsd = Number(j.balanceUsd) || 0; })
|
|
463
|
+
.catch(() => { /* advisory only */ });
|
|
464
|
+
};
|
|
433
465
|
let tunnelError = null;
|
|
434
466
|
|
|
435
467
|
const server = http.createServer(async (req, res) => {
|
|
@@ -523,6 +555,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
523
555
|
{
|
|
524
556
|
const p0 = (req.url || '').split('?')[0];
|
|
525
557
|
if (req.method === 'GET' && (p0 === '/v1/info' || p0 === '/info')) {
|
|
558
|
+
refreshCredit();
|
|
526
559
|
const self = viaTunnel && tunnelGate?.publicUrl
|
|
527
560
|
? `${tunnelGate.publicUrl}/v1`
|
|
528
561
|
: `http://localhost:${config.port}/v1`;
|
|
@@ -533,7 +566,17 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
533
566
|
reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
|
|
534
567
|
publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
|
|
535
568
|
servedRequests,
|
|
569
|
+
spilled: {
|
|
570
|
+
calls: spillCalls,
|
|
571
|
+
chars: spilledChars,
|
|
572
|
+
// ~4 chars/token is the usual rough rule; this is the context that
|
|
573
|
+
// did NOT ride upstream on those calls, which is the number the
|
|
574
|
+
// saving is actually made of.
|
|
575
|
+
tokensApprox: Math.round(spilledChars / 4),
|
|
576
|
+
reusedBinds: spillReuses,
|
|
577
|
+
},
|
|
536
578
|
spendUsd: sessionSpent,
|
|
579
|
+
creditUsd,
|
|
537
580
|
paidCalls,
|
|
538
581
|
mcp: `${self.replace(/\/v1$/, '')}/mcp`,
|
|
539
582
|
upstream: config.apiBase,
|
|
@@ -798,6 +841,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
798
841
|
});
|
|
799
842
|
let result;
|
|
800
843
|
if (cached) {
|
|
844
|
+
spillCalls += 1;
|
|
845
|
+
spilledChars += cached.corpus?.length || 0;
|
|
846
|
+
if (cached.reused) spillReuses += 1;
|
|
801
847
|
result = await send(cached.body, cached.contextId);
|
|
802
848
|
// Sidecar wiped between runs: the gateway 404s BEFORE the 402 (nothing
|
|
803
849
|
// paid). Never fail on a stale manifest — re-bind once and retry.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.29",
|
|
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",
|