openzoo 0.49.5 → 0.49.6
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/README.md +7 -7
- package/lib/demo.js +4 -1
- package/lib/grokui.mjs +177 -5
- package/lib/gui.html +4 -1
- package/lib/mcp.js +3 -1
- package/lib/pay.js +3 -0
- package/lib/proxy.js +114 -97
- package/lib/racesettle.js +22 -8
- package/lib/spill.js +168 -9
- package/lib/x402.js +13 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ claude mcp add openzoo -- npx -y openzoo mcp
|
|
|
79
79
|
**Windsurf / Cline / any MCP host** — same shape: command `npx`, args `["-y", "openzoo", "mcp"]`, stdio transport.
|
|
80
80
|
|
|
81
81
|
Tools:
|
|
82
|
-
- **`zoo_ask`** — `{prompt, corpus?, model?, max_tokens?}`. The flagship: hand it a *huge* `corpus` (hundreds of thousands to ~1M tokens — a body the model itself would refuse) and a question; the zoo's leCore memory spills the corpus so the model reads only a few thousand tokens. Returns the answer plus the receipt (`billedUsd`, `
|
|
82
|
+
- **`zoo_ask`** — `{prompt, corpus?, model?, max_tokens?}`. The flagship: hand it a *huge* `corpus` (hundreds of thousands to ~1M tokens — a body the model itself would refuse) and a question; the zoo's leCore memory spills the corpus so the model reads only a few thousand tokens. Returns the answer plus the receipt (`billedUsd`, `directUsd`, `savedUsd`, tokens actually read). An MCP-capable agent can delegate a giant-context question without touching its own model config.
|
|
83
83
|
- **`zoo_models`** — list the zoo's models and pricing (free).
|
|
84
84
|
- **`zoo_wallet`** — funding address, balances, and this session's receipts.
|
|
85
85
|
|
|
@@ -93,7 +93,7 @@ Builds a ~965k-token document with one planted fact, shows that buying direct re
|
|
|
93
93
|
|
|
94
94
|
```
|
|
95
95
|
bound once in 14.8s: 3.7MB → context ctx_01KZZY8YQE…
|
|
96
|
-
quote for the ask: $0.000480 · pricing=markup
|
|
96
|
+
quote for the ask: $0.000480 · pricing=markup · at OpenRouter price (the 3.7MB corpus is not re-priced)
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
If the wallet is funded with USDC or TOKEN it pays (capped at `OPENZOO_DEMO_MAX_USD`, default $0.01) and prints the answer, the tokens the model actually read, and the receipt. If not, it prints exactly what to fund. Long waits (the one-time upload, pricing, payment, the answer) show a live progress line with stage + elapsed seconds.
|
|
@@ -111,7 +111,7 @@ If the wallet is funded with USDC or TOKEN it pays (capped at `OPENZOO_DEMO_MAX_
|
|
|
111
111
|
The zoo keeps your corpus in leCore holographic memory; the shim keeps a manifest at `~/.openzoo/contexts.json` (chmod 600) mapping `sha256(corpus)` → the zoo's `context_id`, scoped per API base. When a request carries a corpus the manifest already knows:
|
|
112
112
|
|
|
113
113
|
- **nothing big is uploaded** — the ask ships alone with an `X-HRR-Context` header,
|
|
114
|
-
- **the 402 prices the tiny ask** (
|
|
114
|
+
- **the 402 prices the tiny ask** at OpenRouter rates (read `extra.billedUsd`), typically a few hundredths of a cent instead of re-pricing megabytes,
|
|
115
115
|
- **the answer still comes from your corpus** — the zoo recalls the relevant slices server-side.
|
|
116
116
|
|
|
117
117
|
This works in all three fronts: the **proxy** (a big pasted body in the last message is split at its last blank line, bound once, and reused on every later call — even with a different question), the **MCP** `zoo_ask` `corpus` parameter, and the **demo**. If the zoo ever forgets a context (sidecar wipe), the gateway answers 404 *before* any payment and the shim transparently re-binds once and retries — a stale manifest never fails a call.
|
|
@@ -175,11 +175,11 @@ Pin the key with `OPENZOO_TUNNEL_TOKEN` if your IDE stores it. Keys never leave
|
|
|
175
175
|
## Honest pricing note
|
|
176
176
|
|
|
177
177
|
Two bases, reported per call in the 402 (`extra.pricing`):
|
|
178
|
-
- **
|
|
179
|
-
- **Big bodies price at a counterfactual discount** (~10× cheaper than buying the same call direct) — the zoo's leCore memory means it never forwards your whole body upstream, and passes the savings on. Measured numbers at [benches.openzoo.fun](https://benches.openzoo.fun).
|
|
180
|
-
- **Asks against a bound corpus price on the small forwarded body
|
|
178
|
+
- **Wallet path charges OpenRouter prices.** There is no 3× markup. If the caller saves vs sending the same body direct, zoo takes 33% of the savings. Short prompts have nothing to spill, so you pay the OpenRouter figure, reconciled against the provider's own metered cost after it completes.
|
|
179
|
+
- **Big bodies price at a counterfactual discount** (~10× cheaper than buying the same call direct) — the zoo's leCore memory means it never forwards your whole body upstream, and passes most of the savings on. Measured numbers at [benches.openzoo.fun](https://benches.openzoo.fun).
|
|
180
|
+
- **Asks against a bound corpus price on the small forwarded body.** That is not a discount trick: a few hundred tokens at OpenRouter rates is far below the counterfactual price of shipping the corpus, which is the whole point of binding once.
|
|
181
181
|
|
|
182
|
-
The receipt names which base you got; `extra.directUsd` / `extra.
|
|
182
|
+
The receipt names which base you got; `extra.billedUsd` / `extra.directUsd` / `extra.savedUsd` let you check the math. (Card subscription pricing is a different path.)
|
|
183
183
|
|
|
184
184
|
## Payment rails
|
|
185
185
|
|
package/lib/demo.js
CHANGED
|
@@ -120,7 +120,10 @@ export async function runDemo() {
|
|
|
120
120
|
}
|
|
121
121
|
const accept = pickAccept(quote, config.token);
|
|
122
122
|
const x = accept.extra || {};
|
|
123
|
-
|
|
123
|
+
const saved = x.savedUsd != null ? Number(x.savedUsd)
|
|
124
|
+
: (x.directUsd != null ? Math.max(0, Number(x.directUsd) - Number(x.billedUsd)) : 0);
|
|
125
|
+
const saveBit = saved > 0 ? ` · $${saved.toFixed(6)} saved vs direct` : ' · at OpenRouter price';
|
|
126
|
+
console.log(` quote for the ask: $${Number(x.billedUsd).toFixed(6)} · pricing=${x.pricing}${saveBit} (the ${docMb}MB corpus is not re-priced)`);
|
|
124
127
|
console.log('');
|
|
125
128
|
|
|
126
129
|
if (Number(x.billedUsd) > config.demoMaxUsd) {
|
package/lib/grokui.mjs
CHANGED
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
prepareChildDir, finishChildDir, lockWorktree, unlockWorktree,
|
|
25
25
|
parsePrRef, fetchSpecsForOrigin, agentSlug,
|
|
26
26
|
} from './worktree.mjs';
|
|
27
|
+
import {
|
|
28
|
+
filesForCorpus as collectFilesForCorpus,
|
|
29
|
+
readFilesForCorpus,
|
|
30
|
+
looksLikeFileView,
|
|
31
|
+
extractBashPaths,
|
|
32
|
+
} from './spill.js';
|
|
33
|
+
import { BIND_MIN_CHARS } from './hrr.js';
|
|
27
34
|
|
|
28
35
|
const PORT = Number(process.env.OZ_GROKUI_PORT || 4173);
|
|
29
36
|
// BIND HOST. Default 127.0.0.1 so the desktop app never exposes a shell-capable
|
|
@@ -391,8 +398,9 @@ comes back with content:null and finish_reason:"length" (confirmed live) if you
|
|
|
391
398
|
to append to the same bound context, rather than one giant request that silently fails partway.
|
|
392
399
|
|
|
393
400
|
COST ACCOUNTING — do NOT compute this yourself from token counts. Every response carries an
|
|
394
|
-
"x402" object; read the numbers off it: x402.billedUsd (what the user paid
|
|
395
|
-
(our upstream cost), x402.directUsd (what answering
|
|
401
|
+
"x402" object; read the numbers off it: x402.billedUsd (what the user paid — OpenRouter price, plus 33% of savings vs
|
|
402
|
+
direct when any), x402.cogsUsd (our upstream cost), x402.directUsd (what answering
|
|
403
|
+
WITHOUT the zoo would have cost), x402.savedUsd (dollars saved), and
|
|
396
404
|
x402.savesVsDirect (the multiple). Summing usage.cost or usage.prompt_tokens and comparing
|
|
397
405
|
that to a provider's list price is WRONG and understates the saving enormously: against a
|
|
398
406
|
bound context, prompt_tokens counts only the small slice leCore recalled, NOT the corpus
|
|
@@ -401,7 +409,7 @@ the zoo "cost more". MEASURED: a real 21-question run reported 202,238 prompt to
|
|
|
401
409
|
each attach call stood in for a 5,356,546-token corpus — a 556x understatement, and that
|
|
402
410
|
corpus is ~42x larger than the model's own context window, so the "direct" comparison it
|
|
403
411
|
was measured against was not merely pricier but IMPOSSIBLE. When the user asks what they
|
|
404
|
-
saved, quote x402.directUsd and x402.
|
|
412
|
+
saved, quote x402.billedUsd, x402.directUsd and x402.savedUsd. If savedUsd is 0 / savesVsDirect is below 1x, say so
|
|
405
413
|
plainly — that happens on small inputs, where the corpus is too small to save anything.
|
|
406
414
|
For normal questions just answer directly — do not use any of these unless the request
|
|
407
415
|
actually calls for delegation or file work.`;
|
|
@@ -618,7 +626,7 @@ this is roleplay, and never fabricate command output or receipts. A real failure
|
|
|
618
626
|
as real output and an exit code, not as silence.
|
|
619
627
|
|
|
620
628
|
COST ACCOUNTING — read it off the response's "x402" object (billedUsd, cogsUsd, directUsd,
|
|
621
|
-
savesVsDirect). Never derive it by summing usage.cost or usage.prompt_tokens against a
|
|
629
|
+
savedUsd, savesVsDirect). Never derive it by summing usage.cost or usage.prompt_tokens against a
|
|
622
630
|
provider's list price: on a bound context prompt_tokens counts only the slice leCore
|
|
623
631
|
recalled, not the corpus it stands in for, so that math prices the discount against itself
|
|
624
632
|
and wrongly concludes the zoo cost more.
|
|
@@ -1123,6 +1131,159 @@ function condense(label, text) {
|
|
|
1123
1131
|
return `${label}\n${head}\n\n…[${s.length - head.length - tail.length} chars elided from THIS message — the full output is bound to this thread's holographic memory. It is not lost: mention what you need in your next message and the relevant part is retrieved automatically. Do not invent a command to fetch it.]…\n\n${tail}`;
|
|
1124
1132
|
}
|
|
1125
1133
|
|
|
1134
|
+
// Files the grokui agent READ/WRITE/EDIT/MULTIEDIT/NOTEBOOK'd (and RUN
|
|
1135
|
+
// cat/head/type) must land in the HRR corpus as path-prefixed file parts —
|
|
1136
|
+
// not as unlabeled chat text via bindThread, and not as the ~3k condense()
|
|
1137
|
+
// stub the next completion sends. Sidecar maybeCacheCorpus only sees that
|
|
1138
|
+
// stub; bindThread never labels them as files. filesForCorpus (spill.js)
|
|
1139
|
+
// already dedups path:mtime and caps at KEEP_MAX (400KB). We reuse it.
|
|
1140
|
+
const boundFiles = new Set();
|
|
1141
|
+
|
|
1142
|
+
function pathToReadMsgs(paths) {
|
|
1143
|
+
return (Array.isArray(paths) ? paths : [paths]).filter(Boolean).map((file_path) => ({
|
|
1144
|
+
role: 'assistant',
|
|
1145
|
+
tool_calls: [{
|
|
1146
|
+
function: { name: 'Read', arguments: JSON.stringify({ file_path }) },
|
|
1147
|
+
}],
|
|
1148
|
+
}));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function commandToBashMsgs(command) {
|
|
1152
|
+
return [{
|
|
1153
|
+
role: 'assistant',
|
|
1154
|
+
tool_calls: [{
|
|
1155
|
+
function: { name: 'Bash', arguments: JSON.stringify({ command }) },
|
|
1156
|
+
}],
|
|
1157
|
+
}];
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Collect grokui file reads/writes for the HRR corpus.
|
|
1162
|
+
*
|
|
1163
|
+
* Accepts an absolute/relative path, a list of paths, or already-shaped
|
|
1164
|
+
* chat messages (OpenAI tool_calls / Anthropic tool_use). Dedup and the
|
|
1165
|
+
* 400KB cap live in spill.js — this is the grokui entry point so READ
|
|
1166
|
+
* records the same way Claude Code Read does.
|
|
1167
|
+
*/
|
|
1168
|
+
function filesForCorpus(target, opts = {}) {
|
|
1169
|
+
const keys = opts.boundFiles || boundFiles;
|
|
1170
|
+
const cwd = opts.cwd || process.cwd();
|
|
1171
|
+
const cap = opts.cap ?? KEEP_MAX;
|
|
1172
|
+
let msgs = target;
|
|
1173
|
+
if (typeof target === 'string') msgs = pathToReadMsgs(target);
|
|
1174
|
+
else if (Array.isArray(target) && (target.length === 0 || typeof target[0] === 'string')) {
|
|
1175
|
+
msgs = pathToReadMsgs(target);
|
|
1176
|
+
}
|
|
1177
|
+
return collectFilesForCorpus(msgs, { ...opts, boundFiles: keys, cwd, cap });
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function filesForCorpusKeys() {
|
|
1181
|
+
return boundFiles;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function resetFilesForCorpus() {
|
|
1185
|
+
boundFiles.clear();
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function inFlightChars(t) {
|
|
1189
|
+
if (!t) return 0;
|
|
1190
|
+
if (Array.isArray(t.messages)) {
|
|
1191
|
+
let n = 0;
|
|
1192
|
+
for (const m of t.messages) {
|
|
1193
|
+
const c = m?.content;
|
|
1194
|
+
if (typeof c === 'string') n += c.length;
|
|
1195
|
+
else if (c != null) n += JSON.stringify(c).length;
|
|
1196
|
+
}
|
|
1197
|
+
return n;
|
|
1198
|
+
}
|
|
1199
|
+
let n = 0;
|
|
1200
|
+
for (const h of t.history || []) n += String(h?.text || '').length;
|
|
1201
|
+
return n;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* POST file bytes to ${PROXY}/hrr/bind, appending to the project context.
|
|
1206
|
+
* Always fire-and-forget so a READ does not wait on the bind round-trip.
|
|
1207
|
+
* When the in-flight corpus/messages are below BIND_MIN_CHARS the bind
|
|
1208
|
+
* still happens — reuse only "wins" on a big body, but the file must be
|
|
1209
|
+
* recallable next turn. Completions keep omitting x-hrr-context.
|
|
1210
|
+
*/
|
|
1211
|
+
function scheduleFilesForCorpus(t, collected, opts = {}) {
|
|
1212
|
+
if (!collected?.pending?.length) return null;
|
|
1213
|
+
const root = t ? (threads.get(rootOf(t).rootId) || t) : null;
|
|
1214
|
+
const ctx = opts.contextId || root?.contextId || t?.contextId || null;
|
|
1215
|
+
const chars = opts.sentChars ?? inFlightChars(t);
|
|
1216
|
+
const background = chars < BIND_MIN_CHARS;
|
|
1217
|
+
const fetchImpl = opts.fetchImpl || fetch;
|
|
1218
|
+
const run = () => {
|
|
1219
|
+
let read;
|
|
1220
|
+
try { read = readFilesForCorpus(collected, { boundFiles: opts.boundFiles || boundFiles, cap: opts.cap ?? KEEP_MAX }); }
|
|
1221
|
+
catch { return Promise.resolve(null); }
|
|
1222
|
+
if (!read?.text) return Promise.resolve(null);
|
|
1223
|
+
const payload = ctx ? { corpus: read.text, context_id: ctx } : { corpus: read.text };
|
|
1224
|
+
return fetchImpl(`${PROXY}/hrr/bind`, {
|
|
1225
|
+
method: 'POST',
|
|
1226
|
+
headers: { 'content-type': 'application/json' },
|
|
1227
|
+
body: JSON.stringify(payload),
|
|
1228
|
+
}).then(async (r) => {
|
|
1229
|
+
const j = await r.json().catch(() => ({}));
|
|
1230
|
+
if (j?.context_id && t) {
|
|
1231
|
+
const live = threads.get(rootOf(t).rootId) || t;
|
|
1232
|
+
live.contextId = j.context_id;
|
|
1233
|
+
t.contextId = j.context_id;
|
|
1234
|
+
if (Number(j.bound)) live.boundItems = (live.boundItems || 0) + Number(j.bound);
|
|
1235
|
+
}
|
|
1236
|
+
return j;
|
|
1237
|
+
}).catch(() => null);
|
|
1238
|
+
};
|
|
1239
|
+
const job = { pending: collected.pending, background, append: Boolean(ctx), run };
|
|
1240
|
+
const kick = () => { job.promise = run(); };
|
|
1241
|
+
if (opts.defer === false) kick();
|
|
1242
|
+
else setImmediate(kick);
|
|
1243
|
+
return job;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function noteFileForCorpus(originId, relOrAbs, extra = {}) {
|
|
1247
|
+
const t = extra.thread || (originId ? threads.get(originId) : null);
|
|
1248
|
+
const cwd = extra.cwd || (originId ? dirFor(originId) : process.cwd());
|
|
1249
|
+
let abs = relOrAbs;
|
|
1250
|
+
if (typeof abs === 'string' && !path.isAbsolute(abs)) {
|
|
1251
|
+
try { abs = originId ? safeResolveIn(cwd, abs) : path.resolve(cwd, abs); }
|
|
1252
|
+
catch { abs = path.resolve(cwd, abs); }
|
|
1253
|
+
}
|
|
1254
|
+
const collected = filesForCorpus(abs, {
|
|
1255
|
+
cwd,
|
|
1256
|
+
boundFiles: extra.boundFiles || boundFiles,
|
|
1257
|
+
cap: extra.cap ?? KEEP_MAX,
|
|
1258
|
+
...(extra.collectOpts || {}),
|
|
1259
|
+
});
|
|
1260
|
+
return scheduleFilesForCorpus(t, collected, extra);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function noteRunForCorpus(originId, command, extra = {}) {
|
|
1264
|
+
if (!looksLikeFileView(command)) return null;
|
|
1265
|
+
const t = extra.thread || (originId ? threads.get(originId) : null);
|
|
1266
|
+
const cwd = extra.cwd || (originId ? dirFor(originId) : process.cwd());
|
|
1267
|
+
const collected = filesForCorpus(commandToBashMsgs(command), {
|
|
1268
|
+
cwd,
|
|
1269
|
+
boundFiles: extra.boundFiles || boundFiles,
|
|
1270
|
+
cap: extra.cap ?? KEEP_MAX,
|
|
1271
|
+
});
|
|
1272
|
+
if (!collected.pending.length) {
|
|
1273
|
+
const { paths } = extractBashPaths(command, cwd);
|
|
1274
|
+
const abs = paths.map((p) => {
|
|
1275
|
+
const raw = p?.raw;
|
|
1276
|
+
if (!raw) return null;
|
|
1277
|
+
if (path.isAbsolute(raw)) return raw;
|
|
1278
|
+
try { return originId ? safeResolveIn(p.cwd || cwd, raw) : path.resolve(p.cwd || cwd, raw); }
|
|
1279
|
+
catch { return path.resolve(p.cwd || cwd, raw); }
|
|
1280
|
+
}).filter(Boolean);
|
|
1281
|
+
if (!abs.length) return null;
|
|
1282
|
+
return noteFileForCorpus(originId, abs, extra);
|
|
1283
|
+
}
|
|
1284
|
+
return scheduleFilesForCorpus(t, collected, extra);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1126
1287
|
// threadId -> open SSE responses. A Set because the same thread can be open in
|
|
1127
1288
|
// two tabs, and both should see the same tokens.
|
|
1128
1289
|
const streamListeners = new Map();
|
|
@@ -2095,6 +2256,7 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
2095
2256
|
const full = safeResolveIn(dirFor(originId), rel);
|
|
2096
2257
|
mkdirSync(path.dirname(full), { recursive: true });
|
|
2097
2258
|
writeFileSync(full, content);
|
|
2259
|
+
noteFileForCorpus(originId, full);
|
|
2098
2260
|
return `Wrote ${rel} (${Buffer.byteLength(content)} bytes) to ${dirFor(originId)}.${await previewAck(originId, rel)}`;
|
|
2099
2261
|
} catch (e) { return `Couldn't write ${rel}: ${e.message}`; }
|
|
2100
2262
|
}
|
|
@@ -2102,7 +2264,9 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
2102
2264
|
if (readD) {
|
|
2103
2265
|
const rel = readD[1].trim();
|
|
2104
2266
|
try {
|
|
2105
|
-
const
|
|
2267
|
+
const full = safeResolveIn(dirFor(originId), rel);
|
|
2268
|
+
const data = readFileSync(full, 'utf8');
|
|
2269
|
+
noteFileForCorpus(originId, full);
|
|
2106
2270
|
return `${rel}:\n${keepWhole(data)}`;
|
|
2107
2271
|
} catch (e) { return `Couldn't read ${rel}: ${e.message}`; }
|
|
2108
2272
|
}
|
|
@@ -2121,6 +2285,7 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
2121
2285
|
if (hits === 0) return `EDIT ${rel}: that exact text isn't in the file — READ it first, the copy must match byte for byte.`;
|
|
2122
2286
|
if (hits > 1) return `EDIT ${rel}: that text appears ${hits} times — include more surrounding context so it matches exactly once.`;
|
|
2123
2287
|
writeFileSync(full, before.replace(oldStr, newStr));
|
|
2288
|
+
noteFileForCorpus(originId, full);
|
|
2124
2289
|
return `Edited ${rel} (${before.length} -> ${before.replace(oldStr, newStr).length} bytes).${await previewAck(originId, rel)}`;
|
|
2125
2290
|
} catch (e) { return `Couldn't edit ${rel}: ${e.message}`; }
|
|
2126
2291
|
}
|
|
@@ -2147,6 +2312,7 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
2147
2312
|
applied.push(o.slice(0, 40));
|
|
2148
2313
|
}
|
|
2149
2314
|
writeFileSync(full, next);
|
|
2315
|
+
noteFileForCorpus(originId, full);
|
|
2150
2316
|
return `MULTIEDIT ${rel}: ${applied.length} edit(s) applied (${before.length} -> ${next.length} bytes).${await previewAck(originId, rel)}`;
|
|
2151
2317
|
} catch (e) { return `Couldn't multiedit ${rel}: ${e.message}`; }
|
|
2152
2318
|
}
|
|
@@ -2167,6 +2333,7 @@ async function tryDirective(reply, originId, onEvent) {
|
|
|
2167
2333
|
// Stale outputs next to new code are worse than none.
|
|
2168
2334
|
if (doc.cells[idx].cell_type === 'code') { doc.cells[idx].outputs = []; doc.cells[idx].execution_count = null; }
|
|
2169
2335
|
writeFileSync(full, JSON.stringify(doc, null, 1));
|
|
2336
|
+
noteFileForCorpus(originId, full);
|
|
2170
2337
|
return `NOTEBOOK ${rel}: replaced cell ${idx} (${doc.cells[idx].cell_type}); outputs cleared.`;
|
|
2171
2338
|
} catch (e) { return `Couldn't edit ${rel}: ${e.message}`; }
|
|
2172
2339
|
}
|
|
@@ -2520,6 +2687,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2520
2687
|
if (t.runMode === 'auto') {
|
|
2521
2688
|
emitLiveStatus(t.id, paint, peekDirectiveStatus('', command));
|
|
2522
2689
|
const output = await execCommand(command, dirFor(t.id));
|
|
2690
|
+
noteRunForCorpus(t.id, command, { cwd: dirFor(t.id) });
|
|
2523
2691
|
const shown = `$ ${command}\n${output}`;
|
|
2524
2692
|
t.history.push({ who: 'bot', text: shown, name: m.name, color: m.color });
|
|
2525
2693
|
paint({ type: 'final', name: m.name, color: m.color, text: shown });
|
|
@@ -2663,6 +2831,7 @@ async function runTurn(threadId, userText, onEvent, images) {
|
|
|
2663
2831
|
if (t.runMode === 'auto') {
|
|
2664
2832
|
emitLiveStatus(t.id, paint, peekDirectiveStatus('', command));
|
|
2665
2833
|
const output = await execCommand(command, dirFor(t.id));
|
|
2834
|
+
noteRunForCorpus(t.id, command, { cwd: dirFor(t.id) });
|
|
2666
2835
|
if (!stillMine()) return;
|
|
2667
2836
|
const shown = `$ ${command}\n${output}`;
|
|
2668
2837
|
t.history.push({ who: 'bot', text: shown });
|
|
@@ -5614,6 +5783,7 @@ const server = http.createServer((req, res) => {
|
|
|
5614
5783
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
5615
5784
|
res.end('{"ok":true}');
|
|
5616
5785
|
execCommand(command, cwd).then((output) => {
|
|
5786
|
+
noteRunForCorpus(t.id, command, { cwd });
|
|
5617
5787
|
entry.runStatus = 'done';
|
|
5618
5788
|
entry.runOutput = output;
|
|
5619
5789
|
saveThreads();
|
|
@@ -5722,4 +5892,6 @@ export {
|
|
|
5722
5892
|
isDoneReply, isTransientModelFail, isEmptyToolResult, enqueueAutoHop, childKickoff, findByName,
|
|
5723
5893
|
attachChildDir, finishChildDir,
|
|
5724
5894
|
lockWorktree, unlockWorktree, parsePrRef, fetchSpecsForOrigin, agentSlug,
|
|
5895
|
+
filesForCorpus, noteFileForCorpus, noteRunForCorpus, resetFilesForCorpus,
|
|
5896
|
+
filesForCorpusKeys, scheduleFilesForCorpus, inFlightChars, BIND_MIN_CHARS, KEEP_MAX,
|
|
5725
5897
|
};
|
package/lib/gui.html
CHANGED
|
@@ -194,7 +194,10 @@ async function send() {
|
|
|
194
194
|
const x = d.x402 || {};
|
|
195
195
|
state.calls += 1;
|
|
196
196
|
if (typeof x.billedUsd === "number") state.spent += x.billedUsd;
|
|
197
|
-
if (typeof x.
|
|
197
|
+
if (typeof x.savedUsd === "number" && x.savedUsd > 0) state.saved += x.savedUsd;
|
|
198
|
+
else if (typeof x.directUsd === "number" && typeof x.billedUsd === "number") {
|
|
199
|
+
state.saved += Math.max(0, x.directUsd - x.billedUsd);
|
|
200
|
+
}
|
|
198
201
|
const bits = [];
|
|
199
202
|
if (typeof x.billedUsd === "number") bits.push(`$${x.billedUsd.toFixed(4)}`);
|
|
200
203
|
if (x.lecore?.engaged) bits.push(`🧠 ${x.lecore.recalled ?? "?"} slices`);
|
package/lib/mcp.js
CHANGED
|
@@ -80,7 +80,7 @@ export function buildMcpServer() {
|
|
|
80
80
|
+ '(client-usable ceiling ~128M tokens; keep one call under ~9.8M) is bound once and the model reads only '
|
|
81
81
|
+ 'a few thousand tokens of it. Re-asking against an already-bound corpus is near-free and much faster, so '
|
|
82
82
|
+ 'send the corpus once and ask many questions. Returns the answer plus a payment receipt '
|
|
83
|
-
+ '(billedUsd,
|
|
83
|
+
+ '(billedUsd, directUsd, savedUsd, tokens actually read).',
|
|
84
84
|
inputSchema: {
|
|
85
85
|
prompt: z.string().describe('The question or instruction.'),
|
|
86
86
|
corpus: z.string().optional().describe('Optional big context body (document dump, logs, book...). Placed before the prompt.'),
|
|
@@ -128,6 +128,8 @@ export function buildMcpServer() {
|
|
|
128
128
|
receipt: receipt ? {
|
|
129
129
|
line: receipt.line,
|
|
130
130
|
billedUsd: receipt.billedUsd,
|
|
131
|
+
directUsd: receipt.directUsd,
|
|
132
|
+
savedUsd: receipt.savedUsd,
|
|
131
133
|
savesVsDirect: receipt.savesVsDirect,
|
|
132
134
|
pricing: receipt.pricing,
|
|
133
135
|
rail: receipt.rail,
|
package/lib/pay.js
CHANGED
|
@@ -413,6 +413,9 @@ export class PayClient {
|
|
|
413
413
|
line: receiptLine(accept, settle),
|
|
414
414
|
rail: railOf(accept),
|
|
415
415
|
billedUsd: accept.extra?.billedUsd,
|
|
416
|
+
directUsd: accept.extra?.directUsd,
|
|
417
|
+
savedUsd: accept.extra?.savedUsd,
|
|
418
|
+
cogsUsd: accept.extra?.cogsUsd,
|
|
416
419
|
savesVsDirect: accept.extra?.savesVsDirect,
|
|
417
420
|
pricing: accept.extra?.pricing,
|
|
418
421
|
symbol: accept.extra?.symbol,
|
package/lib/proxy.js
CHANGED
|
@@ -15,6 +15,8 @@ import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
|
|
|
15
15
|
import {
|
|
16
16
|
loadBoundChars, noteCorpusLedger, filesForCorpus, readFilesForCorpus, boundAbsFromKeys,
|
|
17
17
|
createSpillStats, corpusCharsForSend, applySpillCut, msgText, hudDollarX,
|
|
18
|
+
spillPricedLine,
|
|
19
|
+
planConversationBind, rememberSpillMemo, SPILL_CONTENT_ANCHOR_CHARS,
|
|
18
20
|
} from './spill.js';
|
|
19
21
|
import { rewritablePath, augmentModelList, ALIAS_IDS, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS } from './models.js';
|
|
20
22
|
import { forgetContext } from './contexts.js';
|
|
@@ -26,7 +28,7 @@ import { loadSessionSpend, saveSessionSpend } from './session.js';
|
|
|
26
28
|
import { creditBalance, quotedPrices } from './info.js';
|
|
27
29
|
import { subscriptionPublicView } from './subscription.js';
|
|
28
30
|
import { priceHoldings } from './livestatus.js';
|
|
29
|
-
import { receiptUsedCogs } from './racesettle.js';
|
|
31
|
+
import { receiptUsedCogs, receiptDirectUsd } from './racesettle.js';
|
|
30
32
|
import { rewriteWrapClientError } from './wrap.js';
|
|
31
33
|
|
|
32
34
|
const HOP_BY_HOP = new Set([
|
|
@@ -423,11 +425,6 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
423
425
|
});
|
|
424
426
|
};
|
|
425
427
|
|
|
426
|
-
if (msgs.length < 6) {
|
|
427
|
-
bindFilesInBackground('conversation under 6 messages, background');
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
428
|
// LIVE SELF-TUNER. Env knobs seed the first cut; after cut+stub the proxy
|
|
432
429
|
// scores the HUD dollar multiple (spill direct/billed) and retunes
|
|
433
430
|
// keep/min-turns/budget (stubMore for SEARCH, not live file bodies)
|
|
@@ -488,35 +485,23 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
488
485
|
//
|
|
489
486
|
// FILES RIDE THE BACKGROUND, NEVER THE CRITICAL PATH.
|
|
490
487
|
//
|
|
491
|
-
// The
|
|
492
|
-
//
|
|
493
|
-
// file bytes into
|
|
494
|
-
//
|
|
495
|
-
// bind endpoint measures 0.34-0.48s on a 613KB corpus, and that is 0.34-0.48s
|
|
496
|
-
// the user waits before a single token appears.
|
|
488
|
+
// The first conversation bind is also fire-and-forget: turn 1 may go out
|
|
489
|
+
// unspilled while the bind runs, then later turns recall a tail + contextId.
|
|
490
|
+
// Folding file bytes into a synchronous first bind used to put a 400KB
|
|
491
|
+
// upload in front of the caller's turn (0.34-0.48s on a 613KB corpus).
|
|
497
492
|
//
|
|
498
|
-
// Nothing recalls a file during the turn that read it � the model already has
|
|
493
|
+
// Nothing recalls a file during the turn that read it � the model already has
|
|
499
494
|
// the tool result in its window. Files are only worth having bound for the
|
|
500
|
-
// NEXT ask
|
|
501
|
-
// the fact, off the clock.
|
|
495
|
+
// NEXT ask, so they append after the conversation bind completes.
|
|
502
496
|
const turns = msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
|
|
503
497
|
const corpus = turns;
|
|
504
|
-
if (!sessionKey) sessionKey = corpus.slice(0,
|
|
498
|
+
if (!sessionKey) sessionKey = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
|
|
505
499
|
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// OBSERVED on a live session that read files all turn and never produced a
|
|
512
|
-
// corpus, because its conversation stayed under the threshold the whole time.
|
|
513
|
-
//
|
|
514
|
-
// A file is worth binding on its own merit. So when the turns are too small
|
|
515
|
-
// to spill but files exist, bind the files anyway � in the background,
|
|
516
|
-
// against this session's context � and let this turn go unspilled. The corpus
|
|
517
|
-
// is then waiting for the next ask.
|
|
518
|
-
if (corpus.length <= BIND_MIN_CHARS) {
|
|
519
|
-
bindFilesInBackground('conversation under spill threshold, background');
|
|
500
|
+
// Conversation prefix binds at any size. BIND_MIN_CHARS only gates the
|
|
501
|
+
// one-shot corpus+question path in maybeCacheCorpus � a real but small
|
|
502
|
+
// early-turn prefix used to be discarded here ("only large ones").
|
|
503
|
+
if (!corpus) {
|
|
504
|
+
bindFilesInBackground('empty conversation prefix, files only');
|
|
520
505
|
return null;
|
|
521
506
|
}
|
|
522
507
|
|
|
@@ -524,7 +509,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
524
509
|
//
|
|
525
510
|
// A transcript grows by one message per turn, so the whole-corpus hash misses
|
|
526
511
|
// every time and the old code re-uploaded the ENTIRE prefix on every single
|
|
527
|
-
// turn
|
|
512
|
+
// turn � OBSERVED live: 0.4MB bound three turns running, a fresh context id
|
|
528
513
|
// each time, while only a few KB was actually new. Bind cost grew with
|
|
529
514
|
// conversation length and was re-paid per message.
|
|
530
515
|
//
|
|
@@ -537,7 +522,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
537
522
|
// The anchor was the first 2KB of corpus, which works only because a
|
|
538
523
|
// transcript's opening never changes. It is fragile in exactly the cases that
|
|
539
524
|
// matter: two sessions that open identically (same system block, same first
|
|
540
|
-
// instruction
|
|
525
|
+
// instruction � the norm for an agent) collide onto ONE bound context and
|
|
541
526
|
// interleave their histories, and any edit near the top of a transcript
|
|
542
527
|
// silently orphans the binding and re-uploads the whole thing.
|
|
543
528
|
//
|
|
@@ -545,60 +530,109 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
545
530
|
// back to the content anchor when it is not. Same memo, better key.
|
|
546
531
|
// CAPTURED FROM A LIVE claude-cli/2.1.232 REQUEST, not guessed. The first
|
|
547
532
|
// version of this checked x-session-id / x-claude-session-id /
|
|
548
|
-
// metadata.user_id
|
|
533
|
+
// metadata.user_id � none of which Claude Code sends, so it silently fell
|
|
549
534
|
// back to the content anchor on every request and the feature did nothing.
|
|
550
535
|
// The real header list is:
|
|
551
536
|
// anthropic-beta, anthropic-version, x-app, x-claude-code-session-id,
|
|
552
537
|
// x-stainless-*
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
538
|
+
//
|
|
539
|
+
// COLD-BIND. Turn 1 may go unspilled (full messages, no x-hrr-context) while
|
|
540
|
+
// the first bind runs in the background � same spirit as file-bind, so the
|
|
541
|
+
// opening ask is not stalled. The in-flight/completed bind is memoized on
|
|
542
|
+
// the session key; later turns recall a tail + contextId. Subsequent
|
|
543
|
+
// appends stay fire-and-forget deltas.
|
|
544
|
+
let bindPlan = planConversationBind({
|
|
545
|
+
sessionKey,
|
|
546
|
+
corpus,
|
|
547
|
+
spillMemo,
|
|
548
|
+
sessionLedger,
|
|
549
|
+
});
|
|
550
|
+
const anchor = bindPlan.key || sessionKey;
|
|
551
|
+
|
|
552
|
+
if (bindPlan.action === 'cold-bind') {
|
|
553
|
+
const ready = bindCorpus(corpus, {
|
|
554
|
+
onStage: (stage, info) => {
|
|
555
|
+
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory (background)...`);
|
|
556
|
+
},
|
|
557
|
+
}).then((b) => {
|
|
558
|
+
if (!b?.contextId) return b;
|
|
559
|
+
noteCorpusLedger(boundChars, {
|
|
560
|
+
contextId: b.contextId,
|
|
561
|
+
reused: false,
|
|
562
|
+
corpusChars: corpus.length,
|
|
563
|
+
deltaChars: 0,
|
|
564
|
+
fileChars: 0,
|
|
565
|
+
...ledgerOpts(),
|
|
566
|
+
});
|
|
567
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, contextId: b.contextId, hash: b.hash });
|
|
568
|
+
bindFilesInBackground('background', { appendTo: b.contextId, asAppend: true });
|
|
569
|
+
return b;
|
|
570
|
+
}).catch((e) => {
|
|
571
|
+
log(`bind failed (turn went unspilled): ${e.message}`);
|
|
572
|
+
const cur = spillMemo.get(anchor);
|
|
573
|
+
if (cur?.pending) spillMemo.delete(anchor);
|
|
574
|
+
return null;
|
|
575
|
+
});
|
|
576
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, pending: true, ready });
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (bindPlan.action === 'await-pending') {
|
|
581
|
+
const finished = await bindPlan.ready.catch((e) => {
|
|
582
|
+
log(`bind failed (history may lag one turn): ${e.message}`);
|
|
583
|
+
return null;
|
|
584
|
+
});
|
|
585
|
+
if (!finished?.contextId) {
|
|
586
|
+
bindFilesInBackground('first bind failed, files only');
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
bindPlan = planConversationBind({
|
|
590
|
+
sessionKey: anchor,
|
|
591
|
+
corpus,
|
|
592
|
+
spillMemo,
|
|
593
|
+
sessionLedger,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (bindPlan.action !== 'recall' || !bindPlan.contextId) {
|
|
598
|
+
bindFilesInBackground('no context yet, files only');
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
558
602
|
let bind;
|
|
559
603
|
let deltaChars = 0;
|
|
560
604
|
let appended = false;
|
|
561
|
-
if (
|
|
605
|
+
if (bindPlan.restored && bindPlan.contextId) {
|
|
562
606
|
// Sidecar came back up: we still know the context_id and the accumulated
|
|
563
607
|
// char count, but not the prior prefix string, so we cannot slice a delta.
|
|
564
608
|
// Re-append the current prefix (some overlap is harmless) and keep the
|
|
565
|
-
// restored ledger � do not add corpus.length again.
|
|
609
|
+
// restored ledger � do not add corpus.length again.
|
|
566
610
|
void bindCorpus(corpus, {
|
|
567
|
-
appendTo:
|
|
611
|
+
appendTo: bindPlan.contextId,
|
|
568
612
|
onStage: (stage, info) => {
|
|
569
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${
|
|
613
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (restored session, background)`);
|
|
570
614
|
},
|
|
571
615
|
}).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
|
|
572
616
|
appended = true;
|
|
573
|
-
bind = { contextId:
|
|
574
|
-
} else if (
|
|
575
|
-
const delta =
|
|
617
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
|
|
618
|
+
} else if (bindPlan.append && bindPlan.delta) {
|
|
619
|
+
const delta = bindPlan.delta;
|
|
576
620
|
deltaChars = delta.length;
|
|
577
|
-
// FIRE AND FORGET. This delta is history for FUTURE turns � the answer
|
|
621
|
+
// FIRE AND FORGET. This delta is history for FUTURE turns � the answer
|
|
578
622
|
// being generated right now is served from the tail plus what is already
|
|
579
623
|
// bound, so waiting on the upload buys nothing and costs the user the
|
|
580
624
|
// round trip on every single turn. The context id is already known, so
|
|
581
625
|
// nothing is lost by not waiting for it.
|
|
582
|
-
//
|
|
583
|
-
// The FIRST bind is deliberately NOT async: the request must carry
|
|
584
|
-
// x-hrr-context, and that id does not exist until the bind returns. Firing
|
|
585
|
-
// that one off would send the opening turn with no context at all � a
|
|
586
|
-
// silently worse answer traded for a shorter pause, which is the wrong way
|
|
587
|
-
// round.
|
|
588
626
|
void bindCorpus(delta, {
|
|
589
|
-
appendTo:
|
|
627
|
+
appendTo: bindPlan.contextId,
|
|
590
628
|
onStage: (stage, info) => {
|
|
591
|
-
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${
|
|
629
|
+
if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (delta, background)`);
|
|
592
630
|
},
|
|
593
631
|
}).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
|
|
594
632
|
appended = true;
|
|
595
|
-
bind = { contextId:
|
|
633
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: delta.length };
|
|
596
634
|
} else {
|
|
597
|
-
bind =
|
|
598
|
-
onStage: (stage, info) => {
|
|
599
|
-
if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory...`);
|
|
600
|
-
},
|
|
601
|
-
});
|
|
635
|
+
bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
|
|
602
636
|
}
|
|
603
637
|
// CONVERSATION LEDGER � every successful bind AND append, not only when
|
|
604
638
|
// files exist. First bind initializes to the bound corpus size; each append
|
|
@@ -618,8 +652,7 @@ async function spillTranscript(body, log, req, stats, extra = {}) {
|
|
|
618
652
|
// uploads each version exactly once no matter how often the agent re-reads it.
|
|
619
653
|
// Read + readdir are inside setImmediate � they must not run before send().
|
|
620
654
|
bindFilesInBackground('background', { appendTo: bind.contextId, asAppend: true });
|
|
621
|
-
spillMemo
|
|
622
|
-
if (spillMemo.size > 32) spillMemo.delete(spillMemo.keys().next().value);
|
|
655
|
+
rememberSpillMemo(spillMemo, anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
|
|
623
656
|
// Count the tail that is actually forwarded after stub/trim � older
|
|
624
657
|
// continue-turn rounds after the ask may have been dropped, so
|
|
625
658
|
// msgs.length - cut would keep lastSend growing with the raw pile.
|
|
@@ -793,7 +826,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
793
826
|
say(`session restored: $${sessionSpent.toFixed(6)} � ${paidCalls} paid call${paidCalls === 1 ? '' : 's'}`);
|
|
794
827
|
}
|
|
795
828
|
process.on('exit', rememberSpend);
|
|
796
|
-
const MARKUP = 3; // confirmed constant, see .claude/wiki.md "Margin needs a like-for-like denominator"
|
|
797
829
|
const noteQuote = (x) => {
|
|
798
830
|
const billed = Number(x?.billedUsd);
|
|
799
831
|
if (!Number.isFinite(billed) || billed < 0) return;
|
|
@@ -1519,30 +1551,20 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1519
1551
|
if (paid && receipt) {
|
|
1520
1552
|
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
|
1521
1553
|
sessionSpent += receipt.billedUsd;
|
|
1522
|
-
//
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
|
|
1526
|
-
// still the ceiling those get capped against.
|
|
1527
|
-
// Prefer the gateway's own cogsUsd. Deriving it as billedUsd/MARKUP
|
|
1528
|
-
// is only correct on a straight-markup call: under counterfactual
|
|
1529
|
-
// pricing billedUsd is min(direct×discount, markupUsd), so the
|
|
1530
|
-
// division understates cost and overstates margin.
|
|
1531
|
-
sessionCogs += receiptUsedCogs(receipt, MARKUP);
|
|
1554
|
+
// Wallet path: no 3x. Prefer extra.cogsUsd / billedUsd / directUsd /
|
|
1555
|
+
// savedUsd from the 402. billedUsd is the OpenRouter price (plus
|
|
1556
|
+
// zoo's 33% of savings when the caller beat direct).
|
|
1557
|
+
sessionCogs += receiptUsedCogs(receipt);
|
|
1532
1558
|
noteQuote(receipt);
|
|
1533
1559
|
// direct = what answering this WITHOUT the zoo would have cost. On an
|
|
1534
1560
|
// attach call that is the whole bound corpus, which is why it can be
|
|
1535
|
-
// orders of magnitude above what was billed. directUsd
|
|
1536
|
-
//
|
|
1561
|
+
// orders of magnitude above what was billed. Read extra.directUsd /
|
|
1562
|
+
// extra.savedUsd; do not invent billed * 3.
|
|
1537
1563
|
if (didSpill) {
|
|
1538
1564
|
spill.spillSpend += receipt.billedUsd || 0;
|
|
1539
|
-
spill.spillDirect +=
|
|
1565
|
+
spill.spillDirect += receiptDirectUsd(receipt);
|
|
1540
1566
|
}
|
|
1541
|
-
sessionDirect +=
|
|
1542
|
-
? receipt.directUsd
|
|
1543
|
-
: typeof receipt.savesVsDirect === 'number'
|
|
1544
|
-
? receipt.savesVsDirect * receipt.billedUsd
|
|
1545
|
-
: receipt.billedUsd;
|
|
1567
|
+
sessionDirect += receiptDirectUsd(receipt);
|
|
1546
1568
|
// The public-URL ceiling meters only public-origin spend — your own
|
|
1547
1569
|
// local calls never eat into it.
|
|
1548
1570
|
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
|
@@ -1599,23 +1621,19 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1599
1621
|
if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
|
|
1600
1622
|
const x = data.x402;
|
|
1601
1623
|
sessionSpent += x.billedUsd;
|
|
1602
|
-
sessionCogs += receiptUsedCogs(x
|
|
1624
|
+
sessionCogs += receiptUsedCogs(x);
|
|
1603
1625
|
noteQuote(x);
|
|
1604
|
-
sessionDirect +=
|
|
1626
|
+
sessionDirect += receiptDirectUsd(x);
|
|
1605
1627
|
if (didSpill) {
|
|
1606
1628
|
// THE number that settles why a spilled call did or did not save:
|
|
1607
1629
|
// the gateway only prices a counterfactual when corpusTokens >
|
|
1608
1630
|
// promptTokens, so a tail that rivals the corpus silently falls
|
|
1609
|
-
// back to
|
|
1610
|
-
|
|
1611
|
-
//
|
|
1612
|
-
|
|
1613
|
-
// 'corpus ?' on calls that were pricing fine — which sent a whole
|
|
1614
|
-
// night's debugging after a number that was never the input.
|
|
1615
|
-
const basis = x.counterfactualTokensUsed ?? lc.corpusTokens;
|
|
1616
|
-
log(`spill priced: ${x.pricing} · basis ${basis ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x.billedUsd ?? 0).toFixed(5)} direct ${(x.directUsd ?? 0).toFixed(5)}`);
|
|
1631
|
+
// back to at-cost and direct collapses onto billed.
|
|
1632
|
+
// Tell-line prints the gateway's actual counterfactual only.
|
|
1633
|
+
// Never fall back to lecore.corpusTokens (often == tokensBefore).
|
|
1634
|
+
log(spillPricedLine(x));
|
|
1617
1635
|
spill.spillSpend += x.billedUsd;
|
|
1618
|
-
spill.spillDirect +=
|
|
1636
|
+
spill.spillDirect += receiptDirectUsd(x);
|
|
1619
1637
|
}
|
|
1620
1638
|
paidCalls += 1;
|
|
1621
1639
|
if (viaTunnel) tunnelSpent += x.billedUsd;
|
|
@@ -1665,15 +1683,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1665
1683
|
const meterStreamed = (x) => {
|
|
1666
1684
|
if (paid || typeof x?.billedUsd !== 'number') return;
|
|
1667
1685
|
sessionSpent += x.billedUsd;
|
|
1668
|
-
sessionCogs += receiptUsedCogs(x
|
|
1686
|
+
sessionCogs += receiptUsedCogs(x);
|
|
1669
1687
|
noteQuote(x);
|
|
1670
|
-
sessionDirect +=
|
|
1688
|
+
sessionDirect += receiptDirectUsd(x);
|
|
1671
1689
|
if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; billedWithActual += x.billedUsd || 0; }
|
|
1672
1690
|
if (didSpill) {
|
|
1673
|
-
|
|
1674
|
-
log(`spill priced (streamed): ${x.pricing} · basis ${x.counterfactualTokensUsed ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x.billedUsd ?? 0).toFixed(5)} direct ${(x.directUsd ?? 0).toFixed(5)}`);
|
|
1691
|
+
log(spillPricedLine(x, { streamed: true }));
|
|
1675
1692
|
spill.spillSpend += x.billedUsd;
|
|
1676
|
-
spill.spillDirect +=
|
|
1693
|
+
spill.spillDirect += receiptDirectUsd(x);
|
|
1677
1694
|
}
|
|
1678
1695
|
paidCalls += 1;
|
|
1679
1696
|
if (viaTunnel) tunnelSpent += x.billedUsd;
|
package/lib/racesettle.js
CHANGED
|
@@ -155,13 +155,29 @@ export function capRaceByCredit(n, { creditUsd, quoteUsd } = {}) {
|
|
|
155
155
|
* House cost from the receipt. Do not subtract race_unused — unused
|
|
156
156
|
* grant-back is not a user refund, and shrinking cogs would hide house loss.
|
|
157
157
|
* Does not clamp to billed — HUD embers when cogs exceed what was paid.
|
|
158
|
+
*
|
|
159
|
+
* Wallet path: there is no 3× markup. Prefer the gateway's cogsUsd; otherwise
|
|
160
|
+
* billedUsd (OpenRouter price, plus zoo's 33% of savings when the 402 has any).
|
|
158
161
|
*/
|
|
159
|
-
export function receiptUsedCogs(x
|
|
162
|
+
export function receiptUsedCogs(x) {
|
|
160
163
|
if (!x || typeof x !== 'object') return 0;
|
|
161
|
-
const billedRaw = Number(x.billedUsd);
|
|
162
|
-
const billedOk = Number.isFinite(billedRaw) && billedRaw >= 0;
|
|
163
164
|
if (typeof x.cogsUsd === 'number' && Number.isFinite(x.cogsUsd)) return x.cogsUsd;
|
|
164
|
-
|
|
165
|
+
const billedRaw = Number(x.billedUsd);
|
|
166
|
+
return Number.isFinite(billedRaw) && billedRaw >= 0 ? billedRaw : 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Counterfactual from the 402: extra.directUsd, else billed + savedUsd. */
|
|
170
|
+
export function receiptDirectUsd(x) {
|
|
171
|
+
if (typeof x?.directUsd === 'number' && Number.isFinite(x.directUsd)) return x.directUsd;
|
|
172
|
+
const billed = Number(x?.billedUsd);
|
|
173
|
+
const billedOk = Number.isFinite(billed) && billed >= 0;
|
|
174
|
+
if (typeof x?.savedUsd === 'number' && Number.isFinite(x.savedUsd) && billedOk) {
|
|
175
|
+
return billed + x.savedUsd;
|
|
176
|
+
}
|
|
177
|
+
if (typeof x?.savesVsDirect === 'number' && Number.isFinite(x.savesVsDirect) && billedOk) {
|
|
178
|
+
return x.savesVsDirect * billed;
|
|
179
|
+
}
|
|
180
|
+
return billedOk ? billed : 0;
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
/**
|
|
@@ -169,12 +185,10 @@ export function receiptUsedCogs(x, markup = 3) {
|
|
|
169
185
|
* first-call rewrite, never a race_unused user refund.
|
|
170
186
|
* cogs is the house cost on that receipt (HUD embers when cogs > spent).
|
|
171
187
|
*/
|
|
172
|
-
export function meterRaceReceipt(x
|
|
188
|
+
export function meterRaceReceipt(x) {
|
|
173
189
|
const billed = Number(x?.billedUsd);
|
|
174
190
|
const spentUsd = Number.isFinite(billed) ? billed : 0;
|
|
175
|
-
|
|
176
|
-
const direct = typeof x?.directUsd === 'number' ? x.directUsd : spentUsd;
|
|
177
|
-
return { spentUsd, cogsUsd: usedCogs, directUsd: direct };
|
|
191
|
+
return { spentUsd, cogsUsd: receiptUsedCogs(x), directUsd: receiptDirectUsd(x) };
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
export function inferRaceTier(models, fallback = 'medium') {
|
package/lib/spill.js
CHANGED
|
@@ -118,6 +118,31 @@ export function corpusCharsForSend(boundChars, contextId, thisTurn) {
|
|
|
118
118
|
return Math.max(boundChars.get(contextId) || 0, thisTurn || 0);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Pricing / unspilled basis: the unspilled size, never the shrunken sent
|
|
123
|
+
* (tokensAfter) size. Same unit on both args — tokens or chars, not mixed.
|
|
124
|
+
*/
|
|
125
|
+
export function unspilledBasis({ tokensBefore, corpus } = {}) {
|
|
126
|
+
return Math.max(Number(tokensBefore) || 0, Number(corpus) || 0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Gateway counterfactual the tell-line must print. Missing / non-finite /
|
|
131
|
+
* non-positive → null (`basis ?`). Never reads lecore.corpusTokens.
|
|
132
|
+
*/
|
|
133
|
+
export function spillPricedTellBasis(x) {
|
|
134
|
+
const n = Number(x?.counterfactualTokensUsed);
|
|
135
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Exact `spill priced:` / `spill priced (streamed):` line the proxy logs. */
|
|
139
|
+
export function spillPricedLine(x, { streamed = false } = {}) {
|
|
140
|
+
const lc = x?.lecore || {};
|
|
141
|
+
const basis = spillPricedTellBasis(x);
|
|
142
|
+
const prefix = streamed ? 'spill priced (streamed):' : 'spill priced:';
|
|
143
|
+
return `${prefix} ${x?.pricing} · basis ${basis ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x?.billedUsd ?? 0).toFixed(5)} direct ${(x?.directUsd ?? 0).toFixed(5)}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
/** Expand ~ and resolve relative paths against cwd. Returns null if unusable. */
|
|
122
147
|
export function resolveReadablePath(p, cwd = process.cwd()) {
|
|
123
148
|
if (typeof p !== 'string') return null;
|
|
@@ -1256,9 +1281,9 @@ function lastUserAskIndex(msgs, firstSpillable) {
|
|
|
1256
1281
|
return -1;
|
|
1257
1282
|
}
|
|
1258
1283
|
|
|
1259
|
-
function countRealTurns(msgs, from) {
|
|
1284
|
+
function countRealTurns(msgs, from, to = msgs.length) {
|
|
1260
1285
|
let n = 0;
|
|
1261
|
-
for (let i = from; i <
|
|
1286
|
+
for (let i = from; i < to; i++) {
|
|
1262
1287
|
const r = msgs[i]?.role;
|
|
1263
1288
|
if (r === 'user' || r === 'assistant') n += 1;
|
|
1264
1289
|
}
|
|
@@ -1356,7 +1381,9 @@ export function trimUnseverablePairs(msgs, {
|
|
|
1356
1381
|
|
|
1357
1382
|
/**
|
|
1358
1383
|
* Pick a severable cut: keep a recent tail, honour the byte budget, floor
|
|
1359
|
-
* at minTurns of user/assistant, and never drop the last
|
|
1384
|
+
* at minTurns of user/assistant on a LONG thread, and never drop the last
|
|
1385
|
+
* user ask. minTurns must not empty the bind prefix — a short AUTO thread
|
|
1386
|
+
* binds early turns and may forward a tail with fewer than minTurns.
|
|
1360
1387
|
*/
|
|
1361
1388
|
export function cutTranscript(msgs, knobs = {}) {
|
|
1362
1389
|
const k = sanitizeKnobs({ ...envKnobs(), ...knobs });
|
|
@@ -1398,21 +1425,38 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1398
1425
|
}
|
|
1399
1426
|
if (tailStart > cut) cut = tailStart;
|
|
1400
1427
|
|
|
1428
|
+
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1429
|
+
|
|
1430
|
+
// minTurns may still size the tail on a long thread. If walking earlier
|
|
1431
|
+
// would empty the bind prefix (the old firstSpillable+1 fallback), keep a
|
|
1432
|
+
// non-empty prefix and allow fewer than minTurns in the tail. On a short
|
|
1433
|
+
// thread that cannot satisfy minTurns at all, pin the last ask so early
|
|
1434
|
+
// user/assistant turns bind instead of riding in the forwarded tail.
|
|
1401
1435
|
if (countRealTurns(msgs, cut) < minTurns) {
|
|
1436
|
+
let moved = false;
|
|
1402
1437
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1403
|
-
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1404
|
-
|
|
1438
|
+
if (isSeverable(msgs, i, firstSpillable) && countRealTurns(msgs, i) >= minTurns) {
|
|
1439
|
+
cut = i;
|
|
1440
|
+
moved = true;
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
if (!moved && lastUser > firstSpillable && isSeverable(msgs, lastUser, firstSpillable)) {
|
|
1445
|
+
cut = lastUser;
|
|
1405
1446
|
}
|
|
1406
1447
|
}
|
|
1407
1448
|
|
|
1408
|
-
const lastUser = lastUserAskIndex(msgs, firstSpillable);
|
|
1409
1449
|
if (lastUser > firstSpillable && cut > lastUser) cut = lastUser;
|
|
1410
1450
|
|
|
1411
|
-
//
|
|
1412
|
-
//
|
|
1451
|
+
// 2-real-turn floor protects the last user ask: never drop it, and expand
|
|
1452
|
+
// the tail to two turns on a LONG thread. Do not steal the first
|
|
1453
|
+
// user+assistant pair from the prefix just to pad a short tail.
|
|
1413
1454
|
if (lastUser > firstSpillable && countRealTurns(msgs, cut) < 2) {
|
|
1414
1455
|
for (let i = cut - 1; i > firstSpillable; i--) {
|
|
1415
|
-
if (isSeverable(msgs, i, firstSpillable)
|
|
1456
|
+
if (!isSeverable(msgs, i, firstSpillable) || countRealTurns(msgs, i) < 2) continue;
|
|
1457
|
+
if (countRealTurns(msgs, firstSpillable, i) < 2) continue;
|
|
1458
|
+
cut = i;
|
|
1459
|
+
break;
|
|
1416
1460
|
}
|
|
1417
1461
|
if (cut > lastUser) cut = lastUser;
|
|
1418
1462
|
}
|
|
@@ -1420,6 +1464,121 @@ export function cutTranscript(msgs, knobs = {}) {
|
|
|
1420
1464
|
return { cut, firstSpillable, lastUser, knobs: k };
|
|
1421
1465
|
}
|
|
1422
1466
|
|
|
1467
|
+
/** Opening slice used as a content-anchor when no session header is sent. */
|
|
1468
|
+
export const SPILL_CONTENT_ANCHOR_CHARS = 2048;
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Find a memoized / ledger bind for this request. Prefers an explicit
|
|
1472
|
+
* session key; otherwise matches a stored prefix so a growing content-anchor
|
|
1473
|
+
* (grokui AUTO sends no session header) still recalls the same contextId.
|
|
1474
|
+
*/
|
|
1475
|
+
export function lookupSpillMemo(spillMemo, sessionLedger, { sessionKey, corpus } = {}) {
|
|
1476
|
+
if (sessionKey && spillMemo?.has(sessionKey)) {
|
|
1477
|
+
return { key: sessionKey, source: 'memo', ...spillMemo.get(sessionKey) };
|
|
1478
|
+
}
|
|
1479
|
+
if (sessionKey && sessionLedger?.has(sessionKey)) {
|
|
1480
|
+
const led = sessionLedger.get(sessionKey);
|
|
1481
|
+
if (led?.contextId) return { key: sessionKey, source: 'ledger', restored: !led.corpus, ...led };
|
|
1482
|
+
}
|
|
1483
|
+
if (corpus && spillMemo) {
|
|
1484
|
+
for (const [k, v] of spillMemo) {
|
|
1485
|
+
if (typeof v?.corpus === 'string' && v.corpus.length && corpus.startsWith(v.corpus)) {
|
|
1486
|
+
return { key: k, source: 'memo-prefix', ...v };
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const opening = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
|
|
1490
|
+
for (const [k, v] of spillMemo) {
|
|
1491
|
+
if (typeof k !== 'string' || k.startsWith('sid:')) continue;
|
|
1492
|
+
if (opening.startsWith(k) || (k && k.startsWith(opening))) {
|
|
1493
|
+
return { key: k, source: 'memo-anchor', ...v };
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
export function rememberSpillMemo(spillMemo, key, entry, { max = 32 } = {}) {
|
|
1501
|
+
if (!spillMemo || !key) return;
|
|
1502
|
+
spillMemo.set(key, entry);
|
|
1503
|
+
while (spillMemo.size > max) spillMemo.delete(spillMemo.keys().next().value);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Decide first-bind vs later-turn recall. No I/O — bindCorpus is injected
|
|
1508
|
+
* by the caller (or mocked in tests).
|
|
1509
|
+
*
|
|
1510
|
+
* cold-bind — fire-and-forget first bind; this turn may go unspilled
|
|
1511
|
+
* await-pending — later turn while that bind is in flight; await, then tail
|
|
1512
|
+
* recall — contextId known; send tail (+ optional delta append)
|
|
1513
|
+
*/
|
|
1514
|
+
export function planConversationBind({ sessionKey, corpus, spillMemo, sessionLedger } = {}) {
|
|
1515
|
+
const key = sessionKey || (corpus ? corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS) : null);
|
|
1516
|
+
const prior = lookupSpillMemo(spillMemo, sessionLedger, { sessionKey: key, corpus });
|
|
1517
|
+
if (!prior) {
|
|
1518
|
+
return { action: 'cold-bind', send: 'full', key, corpus };
|
|
1519
|
+
}
|
|
1520
|
+
if (prior.pending && !prior.contextId) {
|
|
1521
|
+
return {
|
|
1522
|
+
action: 'await-pending',
|
|
1523
|
+
send: 'tail',
|
|
1524
|
+
key: prior.key || key,
|
|
1525
|
+
corpus,
|
|
1526
|
+
ready: prior.ready,
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
if (prior.contextId) {
|
|
1530
|
+
let delta = '';
|
|
1531
|
+
let append = false;
|
|
1532
|
+
if (prior.restored) {
|
|
1533
|
+
append = true;
|
|
1534
|
+
} else if (typeof prior.corpus === 'string' && corpus.startsWith(prior.corpus) && corpus.length > prior.corpus.length) {
|
|
1535
|
+
delta = corpus.slice(prior.corpus.length);
|
|
1536
|
+
append = true;
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
action: 'recall',
|
|
1540
|
+
send: 'tail',
|
|
1541
|
+
key: prior.key || key,
|
|
1542
|
+
corpus,
|
|
1543
|
+
contextId: prior.contextId,
|
|
1544
|
+
hash: prior.hash,
|
|
1545
|
+
reused: true,
|
|
1546
|
+
append,
|
|
1547
|
+
delta,
|
|
1548
|
+
restored: Boolean(prior.restored),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
return { action: 'cold-bind', send: 'full', key, corpus };
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* Cut the transcript and plan the conversation bind. Tests use this instead
|
|
1556
|
+
* of standing up the proxy or a live gateway.
|
|
1557
|
+
*/
|
|
1558
|
+
export function planTranscriptSpill(msgs, {
|
|
1559
|
+
knobs,
|
|
1560
|
+
sessionKey,
|
|
1561
|
+
spillMemo,
|
|
1562
|
+
sessionLedger,
|
|
1563
|
+
corpusChars = 0,
|
|
1564
|
+
adapt = false,
|
|
1565
|
+
persist = false,
|
|
1566
|
+
...cutOpts
|
|
1567
|
+
} = {}) {
|
|
1568
|
+
const adapted = applySpillCut(msgs, {
|
|
1569
|
+
knobs,
|
|
1570
|
+
corpusChars,
|
|
1571
|
+
adapt,
|
|
1572
|
+
persist,
|
|
1573
|
+
...cutOpts,
|
|
1574
|
+
});
|
|
1575
|
+
const empty = { ...adapted, corpus: '', bindPlan: null };
|
|
1576
|
+
if (adapted.cut <= adapted.firstSpillable) return empty;
|
|
1577
|
+
const corpus = msgs.slice(adapted.firstSpillable, adapted.cut).map(msgText).filter(Boolean).join('\n\n');
|
|
1578
|
+
const bindPlan = planConversationBind({ sessionKey, corpus, spillMemo, sessionLedger });
|
|
1579
|
+
return { ...adapted, corpus, bindPlan };
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1423
1582
|
function stubForCut(msgs, cut, opts) {
|
|
1424
1583
|
return stubBoundFileResults(msgs, {
|
|
1425
1584
|
boundFiles: opts.boundFiles,
|
package/lib/x402.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
* payTo: "<wallet>", resource, description, maxTimeoutSeconds,
|
|
23
23
|
* extra: { facilitator, feePayer, symbol, billedUsd, tokenUsd,
|
|
24
24
|
* pricedAt, pricing: "markup"|"counterfactual",
|
|
25
|
-
*
|
|
25
|
+
* billedUsd, directUsd?, savedUsd?, savesVsDirect?,
|
|
26
26
|
* acquire?: { method: "spl-token-wrap", steps: WRAP_ACQUIRE_STEPS } } } ],
|
|
27
27
|
* error: "payment required",
|
|
28
28
|
* help: "Wrap ix has 9 accounts. Program CPIs the deposit — do not send a separate TransferChecked. 0x6a = NotEnoughAccounts (old 5-account wrap is dead)." }
|
|
@@ -232,12 +232,21 @@ export function receiptLine(accept, settle) {
|
|
|
232
232
|
// reaches the spill threshold, so there is nothing to compress and nothing
|
|
233
233
|
// to save — which is worth saying out loud, because the fix on the caller's
|
|
234
234
|
// side is to BIND a corpus, not to change models.
|
|
235
|
-
const
|
|
235
|
+
const billed = Number(x.billedUsd);
|
|
236
|
+
const direct = x.directUsd != null ? Number(x.directUsd) : null;
|
|
237
|
+
const saved = x.savedUsd != null ? Number(x.savedUsd)
|
|
238
|
+
: (Number.isFinite(billed) && Number.isFinite(direct) ? Math.max(0, direct - billed) : null);
|
|
239
|
+
const ratio = x.savesVsDirect != null ? Number(x.savesVsDirect)
|
|
240
|
+
: (Number.isFinite(billed) && billed > 0 && Number.isFinite(direct) ? direct / billed : null);
|
|
241
|
+
// Wallet path: OpenRouter price, plus 33% of savings vs direct when any.
|
|
242
|
+
// Never print extra.markup — that field is leftover 3× and is not the quote.
|
|
236
243
|
const saves = ratio != null
|
|
237
244
|
? (ratio >= 1.05
|
|
238
245
|
? ` (${ratio.toFixed(1)}× cheaper than direct)`
|
|
239
|
-
: ' (at
|
|
240
|
-
: (
|
|
246
|
+
: ' (at OpenRouter price — nothing to compress; bind a corpus to save)')
|
|
247
|
+
: (Number.isFinite(saved) && saved > 0
|
|
248
|
+
? ` ($${saved.toFixed(4)} saved vs direct)`
|
|
249
|
+
: ' (at OpenRouter price — short body)');
|
|
241
250
|
const tx = settle?.transaction || settle?.txHash || settle?.signature;
|
|
242
251
|
const rail = railOf(accept);
|
|
243
252
|
return `paid ${usd}${saves}${rail ? ` · rail ${rail}` : ''}${tx ? ` · tx ${tx}` : ''}`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
4
|
-
"description": "Local x402-paying proxy + MCP server for openzoo.fun
|
|
3
|
+
"version": "0.49.6",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"node": ">=18"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node --test test/*.test.js"
|
|
19
|
+
"test": "node --test test/*.test.js && node scripts/assert-grokui-pin.mjs"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|