openzoo 0.50.75 → 0.50.77
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/bin/openzoo.js +17 -0
- package/lib/bindpath.js +87 -3
- package/lib/cursorbackend.js +10 -4
- package/lib/mcp.js +4 -0
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -284,6 +284,9 @@ async function main() {
|
|
|
284
284
|
? process.argv[ei + 1].split(',').map((e) => (e.startsWith('.') ? e : `.${e}`))
|
|
285
285
|
: undefined;
|
|
286
286
|
const mb = (n) => (n / 1048576).toFixed(1);
|
|
287
|
+
// Delta uploads are routinely a few KB against a multi-MB corpus; "0.0MB
|
|
288
|
+
// of 3.2MB" reads as a bug. Pick the unit per number.
|
|
289
|
+
const hb = (n) => (n >= 1048576 ? `${(n / 1048576).toFixed(1)}MB` : n >= 1024 ? `${(n / 1024).toFixed(0)}KB` : `${n}B`);
|
|
287
290
|
const out = await bindPath(target, {
|
|
288
291
|
exts,
|
|
289
292
|
force: process.argv.includes('--force'),
|
|
@@ -291,9 +294,23 @@ async function main() {
|
|
|
291
294
|
if (p.stage === 'reused') console.log(`already bound — ${mb(p.bytes)}MB across ${p.files} file(s) reused, nothing uploaded`);
|
|
292
295
|
if (p.stage === 'start') console.log(`binding ${mb(p.bytes)}MB from ${p.files} file(s) in ${p.parts} part(s)...`);
|
|
293
296
|
if (p.stage === 'part') console.log(` part ${p.index}/${p.of} bound (${mb(p.bytes)}MB)`);
|
|
297
|
+
// DELTA: the server already holds most of a re-bound repo, so say
|
|
298
|
+
// how much is actually crossing the wire — that number is the reason
|
|
299
|
+
// re-binding after an edit is cheap, and hiding it makes the feature
|
|
300
|
+
// look like a no-op.
|
|
301
|
+
if (p.stage === 'delta') {
|
|
302
|
+
console.log(p.missing === 0
|
|
303
|
+
? `delta: server already holds all ${p.chunks} chunk(s) of ${hb(p.bytes)} — nothing to upload`
|
|
304
|
+
: `delta: ${p.missing} of ${p.chunks} chunk(s) missing on the server, uploading only those...`);
|
|
305
|
+
}
|
|
306
|
+
if (p.stage === 'delta-fill') console.log(` shipped ${hb(p.shipped)} of ${hb(p.of)}${p.remaining ? ` (${p.remaining} still missing)` : ''}`);
|
|
294
307
|
},
|
|
295
308
|
});
|
|
296
309
|
console.log('');
|
|
310
|
+
if (out.delta) {
|
|
311
|
+
const pct = out.bytes ? ((100 * out.shipped) / out.bytes).toFixed(1) : '0.0';
|
|
312
|
+
console.log(`bound via delta — uploaded ${hb(out.shipped)} of ${hb(out.bytes)} (${pct}%)`);
|
|
313
|
+
}
|
|
297
314
|
console.log(`context: ${out.contextId}`);
|
|
298
315
|
console.log(`ask it: npx openzoo ask "your question" --context ${out.contextId}`);
|
|
299
316
|
console.log('or send X-HRR-Context: <id> with a small body to /v1/chat/completions');
|
package/lib/bindpath.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { config } from './config.js';
|
|
25
|
+
import { createHash } from 'node:crypto';
|
|
25
26
|
import { corpusHash, rememberContext, lookupContext } from './contexts.js';
|
|
26
27
|
import { withNamespace } from './namespace.js';
|
|
27
28
|
|
|
@@ -122,6 +123,77 @@ export function splitIntoParts(text, maxBytes = MAX_PART_BYTES) {
|
|
|
122
123
|
return parts;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* DELTA BIND — ship only the chunks the server does not already hold.
|
|
129
|
+
*
|
|
130
|
+
* The whole-bind path below hashes the CONCATENATED corpus and re-uploads
|
|
131
|
+
* every part on a miss, so editing one file in a repo re-ships the tree.
|
|
132
|
+
* MEASURED here: a 23MB Rust tree in 4MB parts ran 25s -> 30s -> 65s, and a
|
|
133
|
+
* deploy mid-bind threw five good parts away.
|
|
134
|
+
*
|
|
135
|
+
* Here the unit is one file (split further only when a file alone exceeds a
|
|
136
|
+
* part). Probe with sha256 per chunk, get back {missing, known}, ship the
|
|
137
|
+
* missing in batches under MAX_PART_BYTES, and when every hash resolves the
|
|
138
|
+
* corpus binds under an ordinary context_id — through the same chunked path
|
|
139
|
+
* as a whole bind, so recall cannot tell the difference. MEASURED on the
|
|
140
|
+
* sidecar: 6 files / 124,350 chars, editing one file re-ships 22,525 (18%).
|
|
141
|
+
*
|
|
142
|
+
* Any non-200 anywhere returns null and the caller falls back to the whole
|
|
143
|
+
* bind. Delta is an optimisation; it must never be the reason a bind fails.
|
|
144
|
+
*/
|
|
145
|
+
const sha256 = (t) => createHash('sha256').update(t, 'utf8').digest('hex');
|
|
146
|
+
|
|
147
|
+
const ddbg = (...a) => { if (process.env.OPENZOO_BIND_DEBUG) console.error('[delta]', ...a); };
|
|
148
|
+
|
|
149
|
+
async function postDelta(payload) {
|
|
150
|
+
const r = await fetch(`${config.apiBase}/v1/hrr/delta`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: withNamespace({ 'content-type': 'application/json' }),
|
|
153
|
+
body: JSON.stringify(payload),
|
|
154
|
+
});
|
|
155
|
+
if (r.status !== 200) { ddbg('HTTP', r.status, (await r.text().catch(() => '')).slice(0, 200)); return null; }
|
|
156
|
+
const j = await r.json().catch(() => null);
|
|
157
|
+
if (!(j && Array.isArray(j.missing))) ddbg('bad shape', JSON.stringify(j).slice(0, 200));
|
|
158
|
+
return j && Array.isArray(j.missing) ? j : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function bindDelta(fileTexts, shardKey, onProgress) {
|
|
162
|
+
const chunks = [];
|
|
163
|
+
for (const t of fileTexts) for (const part of splitIntoParts(t)) chunks.push(part);
|
|
164
|
+
const hashes = chunks.map(sha256);
|
|
165
|
+
const byHash = new Map(hashes.map((h, i) => [h, chunks[i]]));
|
|
166
|
+
|
|
167
|
+
const probe = await postDelta({ chunk_hashes: hashes, shard_key: shardKey });
|
|
168
|
+
if (!probe) return null;
|
|
169
|
+
let missing = probe.missing;
|
|
170
|
+
const total = chunks.reduce((n, c) => n + Buffer.byteLength(c), 0);
|
|
171
|
+
onProgress?.({ stage: 'delta', chunks: chunks.length, missing: missing.length, bytes: total });
|
|
172
|
+
|
|
173
|
+
let last = probe;
|
|
174
|
+
let shipped = 0;
|
|
175
|
+
while (missing.length) {
|
|
176
|
+
// one fill per batch under the part ceiling; the last fill assembles
|
|
177
|
+
const batch = {};
|
|
178
|
+
let size = 0;
|
|
179
|
+
for (const h of missing) {
|
|
180
|
+
const t = byHash.get(h);
|
|
181
|
+
if (t === undefined) { ddbg('missing hash not in byHash', h); return null; }
|
|
182
|
+
if (size && size + Buffer.byteLength(t) > MAX_PART_BYTES) break;
|
|
183
|
+
batch[h] = t; size += Buffer.byteLength(t);
|
|
184
|
+
}
|
|
185
|
+
if (!Object.keys(batch).length) { ddbg('empty batch'); return null; }
|
|
186
|
+
last = await postDelta({ chunk_hashes: hashes, chunks: batch, shard_key: shardKey });
|
|
187
|
+
if (!last) return null;
|
|
188
|
+
shipped += size;
|
|
189
|
+
onProgress?.({ stage: 'delta-fill', shipped, of: total, remaining: last.missing.length });
|
|
190
|
+
if (last.missing.length >= missing.length) { ddbg('no progress', last.missing.length, missing.length, JSON.stringify(last.refused)); return null; } // no progress: refused chunks
|
|
191
|
+
missing = last.missing;
|
|
192
|
+
}
|
|
193
|
+
if (!last.complete || !last.context_id) { ddbg('incomplete', JSON.stringify(last).slice(0, 200)); return null; }
|
|
194
|
+
return { contextId: last.context_id, chunks: chunks.length, shipped, bytes: total };
|
|
195
|
+
}
|
|
196
|
+
|
|
125
197
|
async function postBind(payload) {
|
|
126
198
|
const r = await fetch(`${config.apiBase}/v1/hrr/bind`, {
|
|
127
199
|
method: 'POST',
|
|
@@ -167,9 +239,9 @@ export async function bindPath(target, { exts, onProgress, force = false } = {})
|
|
|
167
239
|
// Each file is prefixed with its path so retrieval can cite where a passage
|
|
168
240
|
// came from — a corpus of concatenated files with no provenance is much
|
|
169
241
|
// less useful to answer from. HTML is reduced to its text (see readAsText).
|
|
170
|
-
const
|
|
171
|
-
.map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${readAsText(f)}`)
|
|
172
|
-
|
|
242
|
+
const fileTexts = files
|
|
243
|
+
.map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${readAsText(f)}`);
|
|
244
|
+
const text = fileTexts.join('\n\n');
|
|
173
245
|
|
|
174
246
|
const bytes = Buffer.byteLength(text);
|
|
175
247
|
const hash = corpusHash(text);
|
|
@@ -181,6 +253,18 @@ export async function bindPath(target, { exts, onProgress, force = false } = {})
|
|
|
181
253
|
}
|
|
182
254
|
}
|
|
183
255
|
|
|
256
|
+
// Delta first: on a re-bind after an edit this ships one file, not the tree.
|
|
257
|
+
// OPENZOO_BIND_DELTA=0 forces the whole-bind path.
|
|
258
|
+
if (process.env.OPENZOO_BIND_DELTA !== '0') {
|
|
259
|
+
try {
|
|
260
|
+
const d = await bindDelta(fileTexts, resolved, onProgress);
|
|
261
|
+
if (d) {
|
|
262
|
+
rememberContext(config.apiBase, hash, d.contextId);
|
|
263
|
+
return { contextId: d.contextId, files, parts: d.chunks, bytes, reused: false, delta: true, shipped: d.shipped };
|
|
264
|
+
}
|
|
265
|
+
} catch (e) { ddbg('threw', e?.message); /* fall through to the whole bind */ }
|
|
266
|
+
}
|
|
267
|
+
|
|
184
268
|
const parts = splitIntoParts(text);
|
|
185
269
|
onProgress?.({ stage: 'start', files: files.length, parts: parts.length, bytes });
|
|
186
270
|
|
package/lib/cursorbackend.js
CHANGED
|
@@ -1872,9 +1872,15 @@ const MODEL_ALIASES = {
|
|
|
1872
1872
|
flash: 'zai-org/glm-5.3-flash',
|
|
1873
1873
|
// NEVER OPENROUTER for Grok Bot: `auto` is the gateway router over
|
|
1874
1874
|
// OpenRouter's catalog, so it lands on the door-only id instead.
|
|
1875
|
-
auto
|
|
1876
|
-
|
|
1877
|
-
'openzoo/auto'
|
|
1875
|
+
// openzoo/auto is the gateway router; the gateway keeps Auto on door-served
|
|
1876
|
+
// models only, so it never lands on OpenRouter.
|
|
1877
|
+
auto: 'openzoo/auto',
|
|
1878
|
+
'openrouter/auto': 'openzoo/auto',
|
|
1879
|
+
'openzoo/auto': 'openzoo/auto',
|
|
1880
|
+
'fable-5.1': 'anthropic/claude-fable-5.1',
|
|
1881
|
+
'claude-fable-5.1': 'anthropic/claude-fable-5.1',
|
|
1882
|
+
'anthropic/fable-5.1': 'anthropic/claude-fable-5.1',
|
|
1883
|
+
'anthropic/fable-5': 'anthropic/claude-fable-5',
|
|
1878
1884
|
deepseek: 'deepseek/deepseek-v4-pro',
|
|
1879
1885
|
'deepseek-pro': 'deepseek/deepseek-v4-pro',
|
|
1880
1886
|
'deepseek-flash': 'deepseek/deepseek-v4-flash',
|
|
@@ -1888,7 +1894,7 @@ const MODEL_ALIASES = {
|
|
|
1888
1894
|
* — the bazaar (x402 upstream) row, not OpenRouter's `x-ai/grok-4.6`. The
|
|
1889
1895
|
* gateway serves the bare id off an x402 door with an on-chain cogs receipt,
|
|
1890
1896
|
* so an OpenRouter credit outage cannot take it down. */
|
|
1891
|
-
export const DEFAULT_ZOO_MODEL = '
|
|
1897
|
+
export const DEFAULT_ZOO_MODEL = 'openzoo/auto';
|
|
1892
1898
|
async function resolveModelId(raw) {
|
|
1893
1899
|
const s = String(raw || '').trim();
|
|
1894
1900
|
if (!s) return null;
|
package/lib/mcp.js
CHANGED
|
@@ -413,6 +413,10 @@ export function buildMcpServer() {
|
|
|
413
413
|
files: out.files.length,
|
|
414
414
|
parts: out.parts,
|
|
415
415
|
reused: out.reused,
|
|
416
|
+
// A delta bind uploaded only the chunks the server lacked. `uploaded_bytes`
|
|
417
|
+
// against `bound_bytes` is the saving; an agent re-binding a repo after
|
|
418
|
+
// one edit should see a small number here, not the whole tree.
|
|
419
|
+
...(out.delta ? { delta: true, uploaded_bytes: out.shipped } : {}),
|
|
416
420
|
next: `zoo_ask with context_id="${out.contextId}" and a question answers from this corpus; it is already bound, so pasting it into the prompt would send it twice.`,
|
|
417
421
|
});
|
|
418
422
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.77",
|
|
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",
|