openzoo 0.49.5 → 0.49.7
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/boxes.js +1 -1
- 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/models.js +27 -5
- package/lib/pay.js +3 -0
- package/lib/proxy.js +168 -118
- package/lib/racesettle.js +22 -8
- package/lib/spill.js +445 -25
- 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/boxes.js
CHANGED
|
@@ -77,7 +77,7 @@ const ENTRYPOINT = [
|
|
|
77
77
|
// its HTTP port mappings). The app is read-only at runtime; mutable state
|
|
78
78
|
// lives in /root/.openzoo.
|
|
79
79
|
+ 'until OPENZOO_BIND=0.0.0.0 node /opt/openzoo/bin/openzoo.js >> /var/log/openzoo/proxy.log 2>&1; do echo "proxy exited, restarting" >> /var/log/openzoo/proxy.log; sleep 2; done & '
|
|
80
|
-
+ 'until OZ_GROKUI_BIND=0.0.0.0 OZ_GROKUI_PORT=4173 node /opt/
|
|
80
|
+
+ 'until OZ_GROKUI_BIND=0.0.0.0 OZ_GROKUI_PORT=4173 node /opt/openzoo/lib/grokui.mjs >> /var/log/openzoo/grokui.log 2>&1; do echo "grokui exited, restarting" >> /var/log/openzoo/grokui.log; sleep 2; done & '
|
|
81
81
|
// the capture agent answers the ports Grok Bot expects a Cursor sandbox on,
|
|
82
82
|
// logging the protocol we do not yet speak (see lib/podagent.mjs)
|
|
83
83
|
+ 'if [ -n "$OZ_PODAGENT_B64" ]; then printf %s "$OZ_PODAGENT_B64" | base64 -d > /opt/podagent.mjs; node /opt/podagent.mjs > /var/log/openzoo/agent.log 2>&1 & fi; '
|
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/models.js
CHANGED
|
@@ -323,10 +323,24 @@ export function isTinyClassify(body, bodyLen) {
|
|
|
323
323
|
return messages.length <= CLASSIFY_MAX_MSGS && len < CLASSIFY_MAX_BODY;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Ids that must never serve Claude Code / grokui AUTO's tiny yes/no classify.
|
|
328
|
+
* REASONING_MODEL_RE is the thinking floor; HEAVY_RE is the flagship set
|
|
329
|
+
* (opus/pro/max/…). opus-5 is openzoo's default session model and does NOT
|
|
330
|
+
* match the reasoning regex, so a catalog that lists opus before flash — or
|
|
331
|
+
* lists only opus + grok — used to pick opus as "first non-reasoner". That
|
|
332
|
+
* classify is a 402 handshake on a big model; AUTO's timeout then hard-blocks
|
|
333
|
+
* Bash instead of prompting.
|
|
334
|
+
*/
|
|
335
|
+
function isSlowClassifier(id) {
|
|
336
|
+
const s = String(id || '');
|
|
337
|
+
return REASONING_MODEL_RE.test(s) || HEAVY_RE.test(s);
|
|
338
|
+
}
|
|
339
|
+
|
|
326
340
|
/**
|
|
327
341
|
* Fast non-reasoning id that is actually on the zoo. Prefer an explicit
|
|
328
342
|
* OPENZOO_CLASSIFIER_MODEL, then flash, then haiku, then the first catalog
|
|
329
|
-
* id that
|
|
343
|
+
* id that is neither a reasoner nor a heavy/flagship (opus/pro/max/…).
|
|
330
344
|
*/
|
|
331
345
|
export function pickClassifierModel(ids, preferred = process.env.OPENZOO_CLASSIFIER_MODEL) {
|
|
332
346
|
if (!Array.isArray(ids) || !ids.length) return null;
|
|
@@ -334,7 +348,7 @@ export function pickClassifierModel(ids, preferred = process.env.OPENZOO_CLASSIF
|
|
|
334
348
|
for (const id of CLASSIFIER_PREFS) {
|
|
335
349
|
if (ids.includes(id)) return id;
|
|
336
350
|
}
|
|
337
|
-
return ids.find((id) => !
|
|
351
|
+
return ids.find((id) => !isSlowClassifier(id)) || null;
|
|
338
352
|
}
|
|
339
353
|
|
|
340
354
|
/**
|
|
@@ -362,14 +376,22 @@ export function raiseReasoningMaxTokens(parsed, env = process.env) {
|
|
|
362
376
|
* Model + max_tokens policy for one chat body.
|
|
363
377
|
*
|
|
364
378
|
* Tiny classify: pin to a fast non-reasoning catalog id, leave max_tokens
|
|
365
|
-
* alone, ignore OPENZOO_DEFAULT_MODEL.
|
|
366
|
-
*
|
|
379
|
+
* alone, ignore OPENZOO_DEFAULT_MODEL. Never fall back to `from` when that
|
|
380
|
+
* id is a reasoner or a heavy/flagship (the zoo default is opus-5). A
|
|
381
|
+
* catalog miss or an opus-only list used to keep the classify on opus-5
|
|
382
|
+
* and AUTO hard-blocked Bash. Everything else: resolveModel (which honours
|
|
383
|
+
* the default) then the reasoning floor.
|
|
367
384
|
*/
|
|
368
385
|
export function rewriteChatModel(parsed, ids, { bodyLen } = {}) {
|
|
369
386
|
const from = parsed?.model;
|
|
370
387
|
const len = bodyLen ?? (parsed == null ? 0 : Buffer.byteLength(JSON.stringify(parsed)));
|
|
371
388
|
if (isTinyClassify(parsed, len)) {
|
|
372
|
-
const
|
|
389
|
+
const picked = pickClassifierModel(ids);
|
|
390
|
+
// pickClassifierModel returns null on an empty catalog or a zoo that
|
|
391
|
+
// only lists reasoners/heavies. `(picked) || from` left those on
|
|
392
|
+
// anthropic/claude-opus-5 (openzoo's default). Pin to flash instead —
|
|
393
|
+
// never ship a classify body AUTO would time out and hard-block on.
|
|
394
|
+
const to = picked || (typeof from === 'string' && !isSlowClassifier(from) ? from : CLASSIFIER_PREFS[0]);
|
|
373
395
|
return {
|
|
374
396
|
parsed: (to && to !== from) ? { ...parsed, model: to } : parsed,
|
|
375
397
|
tiny: true,
|
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,
|