openzoo 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bindpath.js +47 -2
- package/lib/brief.js +2 -1
- package/lib/proxy.js +9 -1
- package/package.json +1 -1
package/lib/bindpath.js
CHANGED
|
@@ -38,6 +38,35 @@ const DEFAULT_EXTS = [
|
|
|
38
38
|
|
|
39
39
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'dist', 'build', '.next', '__pycache__', '.venv']);
|
|
40
40
|
|
|
41
|
+
/** Refuse rather than grind. A directory of exports can be hundreds of MB;
|
|
42
|
+
* binding that takes many minutes and mostly buys markup. Raise deliberately. */
|
|
43
|
+
export const MAX_TOTAL_BYTES = Number(process.env.OPENZOO_BIND_MAX_BYTES || 32 * 1024 * 1024);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* HTML → readable text. Chat/forum exports are ~90% markup: binding the raw
|
|
47
|
+
* pages wastes the corpus on tags and makes retrieval rank div soup against
|
|
48
|
+
* the question. Deliberately crude (no parser dependency) but it keeps the
|
|
49
|
+
* text nodes in document order, which is all retrieval needs.
|
|
50
|
+
*/
|
|
51
|
+
export function stripHtml(html) {
|
|
52
|
+
return html
|
|
53
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
|
|
54
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
55
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
56
|
+
.replace(/<\/(p|div|li|tr|h[1-6])>/gi, '\n')
|
|
57
|
+
.replace(/<[^>]+>/g, ' ')
|
|
58
|
+
.replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<')
|
|
59
|
+
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'")
|
|
60
|
+
.replace(/[ \t]+/g, ' ')
|
|
61
|
+
.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
|
62
|
+
.trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const readAsText = (file) => {
|
|
66
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
67
|
+
return /\.html?$/i.test(file) ? stripHtml(raw) : raw;
|
|
68
|
+
};
|
|
69
|
+
|
|
41
70
|
/** Every bindable file under `root` (or just `root` if it is a file). */
|
|
42
71
|
export function collectFiles(root, { exts = DEFAULT_EXTS, maxFiles = 5000 } = {}) {
|
|
43
72
|
const st = fs.statSync(root);
|
|
@@ -109,11 +138,27 @@ export async function bindPath(target, { exts, onProgress, force = false } = {})
|
|
|
109
138
|
const files = collectFiles(resolved, exts ? { exts } : {});
|
|
110
139
|
if (!files.length) throw new Error(`nothing bindable under ${resolved} (looked for text-like files)`);
|
|
111
140
|
|
|
141
|
+
// Size the job BEFORE reading anything. A 2GB export folder is 100MB of
|
|
142
|
+
// text-like files, and silently grinding through it for minutes reads as a
|
|
143
|
+
// hang — an actionable refusal is better than a spinner.
|
|
144
|
+
const onDisk = files.reduce((n, f) => n + fs.statSync(f).size, 0);
|
|
145
|
+
if (onDisk > MAX_TOTAL_BYTES) {
|
|
146
|
+
const biggest = files
|
|
147
|
+
.map((f) => ({ f, size: fs.statSync(f).size }))
|
|
148
|
+
.sort((a, b) => b.size - a.size).slice(0, 3)
|
|
149
|
+
.map(({ f, size }) => `${path.basename(f)} (${(size / 1048576).toFixed(1)}MB)`);
|
|
150
|
+
throw new Error(
|
|
151
|
+
`${(onDisk / 1048576).toFixed(0)}MB across ${files.length} files exceeds the ${(MAX_TOTAL_BYTES / 1048576).toFixed(0)}MB bind limit. `
|
|
152
|
+
+ `Largest: ${biggest.join(', ')}. Point at one file, narrow with --ext/ext, `
|
|
153
|
+
+ `or raise OPENZOO_BIND_MAX_BYTES if you really want all of it.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
112
157
|
// Each file is prefixed with its path so retrieval can cite where a passage
|
|
113
158
|
// came from — a corpus of concatenated files with no provenance is much
|
|
114
|
-
// less useful to answer from.
|
|
159
|
+
// less useful to answer from. HTML is reduced to its text (see readAsText).
|
|
115
160
|
const text = files
|
|
116
|
-
.map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${
|
|
161
|
+
.map((f) => `===== ${path.relative(path.dirname(resolved), f) || path.basename(f)} =====\n${readAsText(f)}`)
|
|
117
162
|
.join('\n\n');
|
|
118
163
|
|
|
119
164
|
const bytes = Buffer.byteLength(text);
|
package/lib/brief.js
CHANGED
|
@@ -33,7 +33,8 @@ export const briefFor = (selfUrl) => [
|
|
|
33
33
|
' Keep any single request under ~8MB. Bigger bodies are dropped by the network hop before they reach the proxy (an opaque 413 or a dead connection). This is a REQUEST size limit, not a context limit — the bound context can be far larger, which is what parts are for.',
|
|
34
34
|
' Paths: your base_url already ends in /v1, so post to {base_url}/hrr/bind — NOT {base_url}/v1/hrr/bind (that double /v1 404s; the proxy repairs it, but do not rely on that).',
|
|
35
35
|
'',
|
|
36
|
-
'3. PAYMENT IS HANDLED. Every call is paid per-request from the operator\'s wallet via x402 (Solana / Base / Robinhood Chain, whichever is funded). There is no
|
|
36
|
+
'3. PAYMENT IS HANDLED. Every call is paid per-request from the operator\'s wallet via x402 (Solana / Base / Robinhood Chain, whichever is funded). There is no account and no rate limit to negotiate, and you never handle money. Never search the operator\'s machine for credentials — GET / on this proxy describes it.',
|
|
37
|
+
' AUTH, precisely: /hrr/bind and GET /models need NO key, so a script you write can call them directly. Paid endpoints (/chat/completions) need the bearer key your client is already configured with — you cannot read that key, so DO NOT write a standalone script that calls a paid endpoint. Bind from a script if you like, then ask through this conversation, which is already authenticated.',
|
|
37
38
|
'',
|
|
38
39
|
'4. MODEL IDS ARE FORGIVING. Ask for any model id you like; unknown ids are matched to the nearest served model, and /v1/models lists what is real (each alias row carries `served_by`).',
|
|
39
40
|
].join('\n');
|
package/lib/proxy.js
CHANGED
|
@@ -320,8 +320,16 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
320
320
|
}, null, 2));
|
|
321
321
|
return;
|
|
322
322
|
}
|
|
323
|
+
// Binding COSTS NOTHING — no 402, no wallet, no settlement. Gating it
|
|
324
|
+
// behind the key only stopped agents from using the one endpoint that
|
|
325
|
+
// makes a big corpus workable: observed in the wild, an agent wrote a
|
|
326
|
+
// correct multi-part bind script, got 401 on the final append, and
|
|
327
|
+
// fell back to stuffing the corpus inline. The money paths below stay
|
|
328
|
+
// gated; the worst a stranger can do here is spend our sidecar's disk.
|
|
323
329
|
const freeRead = req.method === 'GET' && (p === '/v1/models' || p.startsWith('/v1/models/'));
|
|
324
|
-
|
|
330
|
+
const freeBind = req.method === 'POST' && p === '/v1/hrr/bind';
|
|
331
|
+
if (freeBind) log(`public url: unauthenticated bind allowed (free endpoint) from ${req.socket.remoteAddress}`);
|
|
332
|
+
if (!freeRead && !freeBind) {
|
|
325
333
|
log(`public url: 401 ${req.method} ${req.url}`);
|
|
326
334
|
jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at the operator\'s proxy startup — GET / for discovery, GET /v1/models is open');
|
|
327
335
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
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",
|