openzoo 0.50.90 → 0.50.92
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/claude-zoo.js +28 -3
- package/bin/openzoo.js +2 -26
- package/lib/aoe.js +8 -2
- package/lib/cursorbackend.js +2 -0
- package/lib/launch.js +18 -24
- package/lib/models.js +0 -30
- package/lib/proxy.js +111 -52
- package/lib/setup.js +12 -13
- package/package.json +2 -2
- package/lib/websearch.js +0 -31
package/bin/claude-zoo.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the local openzoo proxy on :8402 (x402 per-call payment, no Anthropic
|
|
7
7
|
* subscription, no login):
|
|
8
8
|
*
|
|
9
|
-
* - starts the proxy if it is not already
|
|
9
|
+
* - starts the proxy if it is not already THIS version on :8402 (steals stale)
|
|
10
10
|
* - applies claudeZooEnv(): ANTHROPIC_BASE_URL=localhost:PORT/v1,
|
|
11
11
|
* AUTH_TOKEN=sk-openzoo, ANTHROPIC_API_KEY deleted, compaction disabled
|
|
12
12
|
* (the proxy binds the prefix), 1M-token ceiling restored
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* `claude-code-cli` (same directory, symlinked to occ).
|
|
17
17
|
*/
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
|
-
import { existsSync, readdirSync as fsReaddir } from 'node:fs';
|
|
19
|
+
import { existsSync, readdirSync as fsReaddir, readFileSync } from 'node:fs';
|
|
20
|
+
import { execSync } from 'node:child_process';
|
|
20
21
|
import { homedir, platform } from 'node:os';
|
|
21
22
|
import { dirname, join, sep } from 'node:path';
|
|
22
23
|
import { fileURLToPath } from 'node:url';
|
|
@@ -50,10 +51,34 @@ if (!occ) {
|
|
|
50
51
|
const PROXY_PORT = Number(process.env.OPENZOO_PROXY_PORT || 8402);
|
|
51
52
|
const PROXY_URL = `http://localhost:${PROXY_PORT}/v1`;
|
|
52
53
|
|
|
54
|
+
function mineVersion() {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readFileSync(join(shimRoot, 'package.json'), 'utf8')).version;
|
|
57
|
+
} catch { return ''; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stealPort(port) {
|
|
61
|
+
const n = Number(port);
|
|
62
|
+
for (const cmd of [
|
|
63
|
+
`lsof -t -iTCP:${n} -sTCP:LISTEN | xargs kill -9`,
|
|
64
|
+
`fuser -k ${n}/tcp`,
|
|
65
|
+
]) {
|
|
66
|
+
try { execSync(cmd, { stdio: 'ignore', timeout: 2000, shell: true }); } catch { /* missing */ }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
async function proxyUp() {
|
|
54
71
|
try {
|
|
55
72
|
const r = await fetch(`http://localhost:${PROXY_PORT}/v1/info`, { signal: AbortSignal.timeout(1500) });
|
|
56
|
-
|
|
73
|
+
if (!r.ok) return false;
|
|
74
|
+
const j = await r.json().catch(() => ({}));
|
|
75
|
+
const mine = mineVersion();
|
|
76
|
+
if (mine && String(j.version || '') !== mine) {
|
|
77
|
+
console.error(`claude: stale proxy v${j.version || '?'} on :${PROXY_PORT} — stealing for v${mine}`);
|
|
78
|
+
stealPort(PROXY_PORT);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
57
82
|
} catch { return false; }
|
|
58
83
|
}
|
|
59
84
|
|
package/bin/openzoo.js
CHANGED
|
@@ -330,7 +330,7 @@ async function main() {
|
|
|
330
330
|
}
|
|
331
331
|
case 'ask': {
|
|
332
332
|
const question = process.argv[3];
|
|
333
|
-
if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>]
|
|
333
|
+
if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>]');
|
|
334
334
|
const ci = process.argv.indexOf('--context');
|
|
335
335
|
const mi = process.argv.indexOf('--model');
|
|
336
336
|
// A BARE QUESTION IS A DIFFERENT PRODUCT FROM A BRIEFED ONE.
|
|
@@ -344,20 +344,7 @@ async function main() {
|
|
|
344
344
|
// DHH, Hyprland and theming. Same gateway, same product, one had context.
|
|
345
345
|
// A caller that knows where it is running can now say so.
|
|
346
346
|
const si = process.argv.indexOf('--system');
|
|
347
|
-
|
|
348
|
-
// --web: a keyless DuckDuckGo search, top results injected into THIS
|
|
349
|
-
// call's system prompt. The x402 rail strips OpenRouter's `plugins`
|
|
350
|
-
// field, so search-then-inject has to happen here, on the caller's
|
|
351
|
-
// side. EGRESS: the question text goes to duckduckgo.com. Also on with
|
|
352
|
-
// OPENZOO_ASK_WEB=1; --web-results N caps the count (default 5).
|
|
353
|
-
const wantWeb = process.argv.includes('--web') || process.env.OPENZOO_ASK_WEB === '1';
|
|
354
|
-
if (wantWeb) {
|
|
355
|
-
const wi = process.argv.indexOf('--web-results');
|
|
356
|
-
const n = wi !== -1 ? Number(process.argv[wi + 1]) || 5 : 5;
|
|
357
|
-
const { webSearch, formatWebResults } = await import('../lib/websearch.js');
|
|
358
|
-
const hits = await webSearch(question, n).catch((e) => { console.error(`web search failed: ${e.message}`); return []; });
|
|
359
|
-
if (hits.length) system = (system ? system + '\n\n' : '') + formatWebResults(question, hits);
|
|
360
|
-
}
|
|
347
|
+
const system = si !== -1 ? process.argv[si + 1] : '';
|
|
361
348
|
const { PayClient } = await import('../lib/pay.js');
|
|
362
349
|
const { config } = await import('../lib/config.js');
|
|
363
350
|
const client = new PayClient();
|
|
@@ -402,17 +389,6 @@ async function main() {
|
|
|
402
389
|
case '-h':
|
|
403
390
|
console.log(HELP);
|
|
404
391
|
break;
|
|
405
|
-
case 'version':
|
|
406
|
-
case '--version':
|
|
407
|
-
case '-v':
|
|
408
|
-
case '-V': {
|
|
409
|
-
// Every installer, mise shim and shell script that probes a CLI asks
|
|
410
|
-
// this first; answering "unknown command" to it failed real installs.
|
|
411
|
-
const { readFileSync } = await import('fs');
|
|
412
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
413
|
-
console.log(pkg.version);
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
392
|
default:
|
|
417
393
|
console.error(`unknown command: ${cmd}\n`);
|
|
418
394
|
console.log(HELP);
|
package/lib/aoe.js
CHANGED
|
@@ -287,9 +287,15 @@ export async function setupAoe(argv = []) {
|
|
|
287
287
|
|
|
288
288
|
const base = `http://localhost:${config.port}/v1`;
|
|
289
289
|
if (!flags.has('--no-proxy')) {
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
const { oursOn, packageVersion, killListen } = await import('./proxy.js');
|
|
291
|
+
if (await oursOn(config.port)) {
|
|
292
|
+
console.error(`openzoo: proxy v${packageVersion()} already on ${base}`);
|
|
292
293
|
} else {
|
|
294
|
+
if (await proxyUp(base)) {
|
|
295
|
+
console.error(`openzoo: stale proxy on ${base} — stealing :${config.port}`);
|
|
296
|
+
killListen(config.port);
|
|
297
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
298
|
+
}
|
|
293
299
|
const { pid, logPath } = startDetachedProxy({ tunnel: flags.has('--tunnel') });
|
|
294
300
|
let up = false;
|
|
295
301
|
for (let i = 0; i < 40 && !up; i++) {
|
package/lib/cursorbackend.js
CHANGED
|
@@ -3080,6 +3080,8 @@ async function zooComplete(prompt, log, agentId, parsed = {}, opts = {}) {
|
|
|
3080
3080
|
data.error.message = raw;
|
|
3081
3081
|
try {
|
|
3082
3082
|
const { withOnrampLink, isFundInstruction } = await import('./stripeOnramp.js');
|
|
3083
|
+
// Settle/upstream 402s keep the real message. Whop copy-paste only
|
|
3084
|
+
// when this is a genuine empty-wallet / fund-me instruction.
|
|
3083
3085
|
if (isFundInstruction(raw)) {
|
|
3084
3086
|
const { loadOrCreateWallet } = await import('./wallet.js');
|
|
3085
3087
|
const w = loadOrCreateWallet();
|
package/lib/launch.js
CHANGED
|
@@ -190,18 +190,21 @@ export function claudeZooEnv(baseEnv = process.env, { base, port } = {}) {
|
|
|
190
190
|
* DEFAULT is the desktop app; `--terminal` (or `-t`) runs the Claude Code CLI.
|
|
191
191
|
* Both get ANTHROPIC_BASE_URL so inference pays x402.
|
|
192
192
|
*/
|
|
193
|
+
|
|
194
|
+
|
|
193
195
|
export async function launchClaude(argv) {
|
|
194
|
-
//
|
|
195
|
-
// below must follow the port we actually bound.
|
|
196
|
+
// NEVER hop. :8402 is the product. startProxy steals a stale listener.
|
|
196
197
|
let base = `http://localhost:${config.port}/v1`;
|
|
197
|
-
// AUTO-START THE PROXY. One command should just work — if nothing is listening
|
|
198
|
-
//
|
|
199
|
-
// foreground below), rather than making the user run `npx openzoo` first.
|
|
198
|
+
// AUTO-START THE PROXY. One command should just work — if nothing is listening
|
|
199
|
+
// (or a leftover npx cache is), boot THIS version on :8402 in this process.
|
|
200
200
|
let up = false;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
|
|
202
|
+
const mine = packageVersion();
|
|
203
|
+
if (await oursOn(config.port)) {
|
|
204
|
+
up = true;
|
|
205
|
+
} else {
|
|
206
|
+
process.stderr.write(`openzoo: claiming :${config.port} for v${mine}\n`);
|
|
207
|
+
}
|
|
205
208
|
if (!up) {
|
|
206
209
|
// NEVER GO SILENT DURING STARTUP. silent:true routes the proxy's own lines
|
|
207
210
|
// to ~/.openzoo/proxy.log so payment receipts cannot corrupt Claude Code's
|
|
@@ -218,7 +221,6 @@ export async function launchClaude(argv) {
|
|
|
218
221
|
tick.unref?.();
|
|
219
222
|
const done = (msg) => { clearInterval(tick); process.stderr.write(`\r\x1b[2Kopenzoo: ${msg}\n`); };
|
|
220
223
|
try {
|
|
221
|
-
const { startProxy } = await import('./proxy.js');
|
|
222
224
|
await startProxy({ silent: true, autoTunnel: true });
|
|
223
225
|
} catch (err) {
|
|
224
226
|
// An exception here used to surface as an eternal spinner. Say what broke.
|
|
@@ -227,9 +229,6 @@ export async function launchClaude(argv) {
|
|
|
227
229
|
console.error(' try: OPENZOO_NO_TUNNEL=1 npx openzoo claude (skips the cloudflared download)');
|
|
228
230
|
process.exit(1);
|
|
229
231
|
}
|
|
230
|
-
// The proxy may have healed onto a different port (8402 busy). config.port
|
|
231
|
-
// is the one it ACTUALLY bound, so re-derive every URL from it — the old
|
|
232
|
-
// code kept polling the port it wished for and timed out on a live proxy.
|
|
233
232
|
base = `http://localhost:${config.port}/v1`;
|
|
234
233
|
// PROBE /v1/info, NOT /v1/models. `models` is PROXIED UPSTREAM, so on a
|
|
235
234
|
// network with a bad path to the gateway the local proxy is listening and
|
|
@@ -580,17 +579,12 @@ export async function launchClaude(argv) {
|
|
|
580
579
|
}
|
|
581
580
|
|
|
582
581
|
export async function launchHarness(cmd, args) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (!r.ok) throw new Error(String(r.status));
|
|
590
|
-
} catch {
|
|
591
|
-
console.error(`openzoo: no proxy reachable at ${base}`);
|
|
592
|
-
console.error('start it first in another terminal: npx openzoo');
|
|
593
|
-
process.exit(1);
|
|
582
|
+
let base = `http://localhost:${config.port}/v1`;
|
|
583
|
+
const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
|
|
584
|
+
if (!(await oursOn(config.port))) {
|
|
585
|
+
process.stderr.write(`openzoo: claiming :${config.port} for v${packageVersion()}\n`);
|
|
586
|
+
await startProxy({ silent: true, autoTunnel: true });
|
|
587
|
+
base = `http://localhost:${config.port}/v1`;
|
|
594
588
|
}
|
|
595
589
|
|
|
596
590
|
const env = claudeZooEnv(process.env, { base });
|
package/lib/models.js
CHANGED
|
@@ -168,39 +168,9 @@ const tokensOf = (id) => id.toLowerCase().split(/[^a-z0-9.]+/).filter((t) => t &
|
|
|
168
168
|
* the id is already servable (no rewrite), otherwise the closest zoo id.
|
|
169
169
|
* OPENZOO_DEFAULT_MODEL is an explicit user override, not a fallback tier.
|
|
170
170
|
*/
|
|
171
|
-
/**
|
|
172
|
-
* UNOPENROUTER. OpenRouter is not an upstream any more (gateway, 2026-09-02):
|
|
173
|
-
* every completion is bought from an x402 door, and doors publish BARE ids
|
|
174
|
-
* (`grok-4.3`, `claude-sonnet-5`, `gemini-2.5-flash`). A harness that still
|
|
175
|
-
* sends the OpenRouter spelling (`x-ai/grok-4.3`, `anthropic/claude-sonnet-5`)
|
|
176
|
-
* is rewritten to the bare id whenever the live catalog serves that bare id.
|
|
177
|
-
*
|
|
178
|
-
* OPENZOO_UNOPENROUTER=1 strip the vendor prefix ALWAYS, catalog or not
|
|
179
|
-
* OPENZOO_UNOPENROUTER=0 never strip
|
|
180
|
-
* unset strip when the bare id is in the catalog
|
|
181
|
-
*
|
|
182
|
-
* Router aliases and `openzoo-` twins are never touched: their slash is not a
|
|
183
|
-
* vendor.
|
|
184
|
-
*/
|
|
185
|
-
export function unopenrouter(requested, ids) {
|
|
186
|
-
const mode = process.env.OPENZOO_UNOPENROUTER;
|
|
187
|
-
if (mode === '0') return null;
|
|
188
|
-
const s = String(requested || '');
|
|
189
|
-
if (!s || isAutoModel(s) || /^openzoo[-/]/i.test(s)) return null;
|
|
190
|
-
const slash = s.indexOf('/');
|
|
191
|
-
if (slash <= 0) return null;
|
|
192
|
-
const bare = s.slice(slash + 1);
|
|
193
|
-
if (!bare || bare.includes('/')) return null;
|
|
194
|
-
if (mode === '1') return bare;
|
|
195
|
-
return Array.isArray(ids) && ids.includes(bare) ? bare : null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
171
|
export function resolveModel(requested, ids) {
|
|
199
172
|
// Virtual router id — never family-match, never steal via OPENZOO_DEFAULT_MODEL.
|
|
200
173
|
if (isAutoModel(requested)) return null;
|
|
201
|
-
// Vendor-prefixed OpenRouter spelling → the bare id the doors serve.
|
|
202
|
-
const bareVendor = unopenrouter(requested, ids);
|
|
203
|
-
if (bareVendor) return bareVendor;
|
|
204
174
|
// Bare Anthropic / Claude Code ids are never live on Fly/OpenRouter
|
|
205
175
|
// (`claude-opus-5` → 500 unknown model). Rewrite even on a catalog miss
|
|
206
176
|
// or if a gateway row lists the bare name — the request must not leave
|
package/lib/proxy.js
CHANGED
|
@@ -13,7 +13,7 @@ import { withOnrampLink, settleFailCopy, isFundInstruction } from './stripeOnram
|
|
|
13
13
|
import { tokenBalance } from './x402.js';
|
|
14
14
|
import { evmTokenBalance } from './evm.js';
|
|
15
15
|
import { autoContext } from './autobind.js';
|
|
16
|
-
import { modelsListForRequest, isHarnessAliasId, resolveModel, quoteableRows
|
|
16
|
+
import { modelsListForRequest, isHarnessAliasId, resolveModel, quoteableRows } from './models.js';
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Quoteable catalog ids, cached 5 minutes, for the fuzzy /v1/models/<id> probe.
|
|
@@ -38,19 +38,89 @@ import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettl
|
|
|
38
38
|
import { fetchHeaders } from './fetch.js';
|
|
39
39
|
import { attachX402Proof } from './spendProof.js';
|
|
40
40
|
|
|
41
|
-
/** Kill whatever is LISTEN on this port except this process.
|
|
41
|
+
/** Kill whatever is LISTEN on this port except this process.
|
|
42
|
+
* lsof first (macOS), then fuser, then /proc (Omarchy/Arch with neither).
|
|
43
|
+
* Every openzoo subcommand that binds :8402 goes through this. Hopping to
|
|
44
|
+
* 8403 was the second-burner bug. */
|
|
42
45
|
export function killListen(port, run = execSync) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const n = Number(port);
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const addFrom = (out) => {
|
|
49
|
+
for (const tok of String(out || '').split(/[^\d]+/)) {
|
|
50
|
+
const pid = Number(tok);
|
|
51
|
+
if (Number.isInteger(pid) && pid > 0 && pid !== process.pid) seen.add(pid);
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
+
};
|
|
54
|
+
const tryRun = (cmd, opts = {}) => {
|
|
55
|
+
try {
|
|
56
|
+
return run(cmd, { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
57
|
+
} catch { return ''; }
|
|
58
|
+
};
|
|
59
|
+
addFrom(tryRun(`lsof -nP -iTCP:${n} -sTCP:LISTEN -t`));
|
|
60
|
+
addFrom(tryRun(`fuser -n tcp ${n}`));
|
|
61
|
+
if (process.platform === 'linux' && seen.size === 0) {
|
|
62
|
+
addFrom(tryRun(`python3 - ${n}`, {
|
|
63
|
+
timeout: 2500,
|
|
64
|
+
input: `import os, glob, sys
|
|
65
|
+
port=int(sys.argv[1]); hx=f'{port:04X}'
|
|
66
|
+
inodes=set(); pids=set()
|
|
67
|
+
for path in ('/proc/net/tcp','/proc/net/tcp6'):
|
|
68
|
+
try:
|
|
69
|
+
for line in open(path):
|
|
70
|
+
p=line.split()
|
|
71
|
+
if len(p)<10: continue
|
|
72
|
+
if p[1].split(':')[-1].upper()==hx: inodes.add(p[9])
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
pass
|
|
75
|
+
for fd in glob.glob('/proc/[0-9]*/fd/[0-9]*'):
|
|
76
|
+
try: t=os.readlink(fd)
|
|
77
|
+
except OSError: continue
|
|
78
|
+
if any(ino and ino!='0' and ino in t for ino in inodes):
|
|
79
|
+
try: pids.add(int(fd.split('/')[2]))
|
|
80
|
+
except (OSError, ValueError): pass
|
|
81
|
+
print('\\n'.join(str(p) for p in pids if p != os.getpid()))
|
|
82
|
+
`,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
for (const pid of seen) {
|
|
86
|
+
try { run(`kill ${pid}`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
87
|
+
try { run(`kill -9 ${pid}`, { stdio: 'ignore', timeout: 2000 }); } catch { /* already gone */ }
|
|
53
88
|
}
|
|
89
|
+
return [...seen];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** This package.json version. Same one /v1/info and /v1/session publish. */
|
|
93
|
+
export function packageVersion() {
|
|
94
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True when the listener on this port is THIS openzoo version or newer.
|
|
98
|
+
* Missing / unparseable version is stale (0.49.x answered with no version).
|
|
99
|
+
* Older than us is stale. Newer we leave alone so we never downgrade. */
|
|
100
|
+
export async function oursOn(port = config.port) {
|
|
101
|
+
const mine = packageVersion();
|
|
102
|
+
const parse = (v) => {
|
|
103
|
+
const m = String(v || '').trim().match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
104
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
105
|
+
};
|
|
106
|
+
const attachable = (theirs) => {
|
|
107
|
+
const a = parse(theirs), b = parse(mine);
|
|
108
|
+
if (!a || !b) return false;
|
|
109
|
+
for (let i = 0; i < 3; i++) {
|
|
110
|
+
if (a[i] > b[i]) return true;
|
|
111
|
+
if (a[i] < b[i]) return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
for (const path of ['/v1/info', '/v1/session']) {
|
|
116
|
+
try {
|
|
117
|
+
const r = await fetch(`http://127.0.0.1:${Number(port)}${path}`, { signal: AbortSignal.timeout(1500) });
|
|
118
|
+
if (!r.ok) continue;
|
|
119
|
+
const j = await r.json().catch(() => ({}));
|
|
120
|
+
if (attachable(j.version)) return true;
|
|
121
|
+
} catch { /* try next */ }
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
54
124
|
}
|
|
55
125
|
|
|
56
126
|
// THE SHIM IS A FACILITATOR, NOT A MIDDLEBOX.
|
|
@@ -512,8 +582,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
512
582
|
? `${tunnelGate.publicUrl}/v1`
|
|
513
583
|
: `http://localhost:${config.port}/v1`;
|
|
514
584
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
585
|
+
const { version: ozVersion } = JSON.parse(
|
|
586
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
587
|
+
);
|
|
515
588
|
res.end(JSON.stringify({
|
|
516
589
|
youAreTalkingTo: 'openzoo proxy',
|
|
590
|
+
version: ozVersion,
|
|
591
|
+
solana: client.address,
|
|
517
592
|
yourEndpoint: self,
|
|
518
593
|
reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
|
|
519
594
|
publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
|
|
@@ -622,24 +697,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
622
697
|
servedRequests += 1;
|
|
623
698
|
say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
|
|
624
699
|
try { wantsStream = JSON.parse(bodyBuf.toString('utf8'))?.stream === true; } catch { /* not JSON */ }
|
|
625
|
-
// UNOPENROUTER THE MODEL ID before anything downstream sees the body —
|
|
626
|
-
// the replay key, the outage gate, the wire. A vendor-prefixed
|
|
627
|
-
// OpenRouter spelling becomes the bare id the doors serve when the
|
|
628
|
-
// catalog lists it (OPENZOO_UNOPENROUTER=1 forces it, =0 disables).
|
|
629
|
-
// This is the one place the body is rewritten; see models.js.
|
|
630
|
-
try {
|
|
631
|
-
const parsed = JSON.parse(bodyBuf.toString('utf8'));
|
|
632
|
-
if (parsed && typeof parsed.model === 'string') {
|
|
633
|
-
let ids = [];
|
|
634
|
-
try { ids = await catalogIdsCached(`${config.apiBase}/v1/models`, upstreamHeaders(req)); } catch { /* catalog unreachable: only the forced mode rewrites */ }
|
|
635
|
-
const bare = unopenrouter(parsed.model, ids);
|
|
636
|
-
if (bare) {
|
|
637
|
-
say(` model ${parsed.model} -> ${bare} (bare id: doors, not OpenRouter)`);
|
|
638
|
-
parsed.model = bare;
|
|
639
|
-
bodyBuf = Buffer.from(JSON.stringify(parsed));
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
} catch { /* not JSON */ }
|
|
643
700
|
}
|
|
644
701
|
|
|
645
702
|
// Retry of a body we answered seconds ago? Serve the cached completion —
|
|
@@ -847,12 +904,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
847
904
|
rememberSpend();
|
|
848
905
|
say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
|
|
849
906
|
};
|
|
850
|
-
// A 402 AFTER we attempted payment is a SETTLEMENT failure, not a quote
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
// retry with a fresh 402. Relaying that
|
|
854
|
-
//
|
|
855
|
-
//
|
|
907
|
+
// A 402 AFTER we attempted payment is a SETTLEMENT failure, not a quote.
|
|
908
|
+
// The client-side balance check is advisory, so a funded wallet can still
|
|
909
|
+
// fail on-chain / at the facilitator, and the gateway answers the paid
|
|
910
|
+
// retry with a fresh 402. Relaying that as "wallet underfunded" + Whop
|
|
911
|
+
// copy-paste blamed burners that already paid. Surface the gateway's
|
|
912
|
+
// real reason; only prepend fund-me copy on genuine insufficient_funds.
|
|
856
913
|
if (response.status === 402) {
|
|
857
914
|
let usd;
|
|
858
915
|
let q402 = null;
|
|
@@ -874,6 +931,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
874
931
|
if (!Number.isFinite(usd)) usd = undefined;
|
|
875
932
|
} catch { usd = undefined; }
|
|
876
933
|
const copy = settleFailCopy(q402);
|
|
934
|
+
// paid:true → never "wallet underfunded", never Whop unless the
|
|
935
|
+
// gateway itself named insufficient_funds. UnderfundedError (empty
|
|
936
|
+
// wallet preflight) is handled in the catch below.
|
|
877
937
|
let msg = copy.message;
|
|
878
938
|
const wantOnramp = copy.code === 'insufficient_funds'
|
|
879
939
|
|| (!paid && isFundInstruction(copy.reason, copy));
|
|
@@ -914,11 +974,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
914
974
|
// OPENZOO_BIND=0.0.0.0 AND a tunnel token, so that port stays gated exactly
|
|
915
975
|
// like the public tunnel path.
|
|
916
976
|
const bindHost = process.env.OPENZOO_BIND || '127.0.0.1';
|
|
917
|
-
// SELF-HEAL
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
|
|
977
|
+
// SELF-HEAL :8402. Kill whoever is on THIS port, then bind it. Never hop to
|
|
978
|
+
// 8403 — that was the second-burner. Every subcommand that calls startProxy
|
|
979
|
+
// (openzoo, claude, bot, web, cursor, aoe, grok, tunnel) goes through here.
|
|
980
|
+
// Reusing a leftover listener left a stale PayClient serving $0 after a
|
|
981
|
+
// TOKEN top-up, and an old npx cache answering /v1 with no version.
|
|
982
|
+
{
|
|
983
|
+
const pids = killListen(config.port);
|
|
984
|
+
if (pids.length) {
|
|
985
|
+
say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
986
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
922
989
|
for (let attempt = 0; ; attempt++) {
|
|
923
990
|
try {
|
|
924
991
|
await new Promise((resolve, reject) => {
|
|
@@ -928,20 +995,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
928
995
|
});
|
|
929
996
|
break;
|
|
930
997
|
} catch (e) {
|
|
931
|
-
if (e?.code !== 'EADDRINUSE' || attempt >=
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
936
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
config.port += 1;
|
|
941
|
-
say(`openzoo: :${config.port - 1} busy — trying :${config.port}`);
|
|
998
|
+
if (e?.code !== 'EADDRINUSE' || attempt >= 8) throw e;
|
|
999
|
+
const pids = killListen(config.port);
|
|
1000
|
+
if (pids.length) say(`openzoo: killed proxy on :${config.port} (pids ${pids.join(',')})`);
|
|
1001
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
942
1002
|
}
|
|
943
1003
|
}
|
|
944
|
-
if (config.port !== wanted) say(`openzoo: listening on :${config.port} (:${wanted} was busy)`);
|
|
945
1004
|
|
|
946
1005
|
// AUTO-PREPAY. Paying on-chain per call is where the latency lives: credit
|
|
947
1006
|
// is applied automatically server-side whenever a balance covers the quote,
|
package/lib/setup.js
CHANGED
|
@@ -214,7 +214,7 @@ async function printStartupDiagnostic(base, which) {
|
|
|
214
214
|
|
|
215
215
|
console.log('openzoo diagnostic');
|
|
216
216
|
console.log(` version : ${version} node ${process.version} ${process.platform}/${process.arch}`);
|
|
217
|
-
console.log(` port ${config.port} : ${portBusy ? '
|
|
217
|
+
console.log(` port ${config.port} : ${portBusy ? 'occupied (steal unless this exact version)' : 'free'}`);
|
|
218
218
|
console.log(` editor : ${picked ? `${picked.which} @ ${picked.cmd}` : 'NONE FOUND'}`);
|
|
219
219
|
console.log(` running : ${picked ? (q(() => editorRunning(picked.which), false) ? 'yes — will be quit so settings stick' : 'no') : '-'}`);
|
|
220
220
|
console.log(` backend : ${hosts}`);
|
|
@@ -245,9 +245,17 @@ export async function setupEditor(which, target) {
|
|
|
245
245
|
// Declared out here: the tunnel-rebind hook is registered further down, well
|
|
246
246
|
// outside the block that starts the proxy.
|
|
247
247
|
let started = null;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
const { startProxy, oursOn, packageVersion } = await import('./proxy.js');
|
|
249
|
+
if (await oursOn(config.port)) {
|
|
250
|
+
console.log(`proxy v${packageVersion()} already on ${base} — keeping it`);
|
|
251
|
+
try {
|
|
252
|
+
const info = await (await fetch(`${base}/info`)).json();
|
|
253
|
+
publicUrl = (info?.publicTunnel || '').replace(/\/v1$/, '') || null;
|
|
254
|
+
tunnelKey = info?.tunnelToken ?? tunnelKey;
|
|
255
|
+
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 (from the running proxy)`);
|
|
256
|
+
} catch { /* no /info */ }
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`${(await proxyUp(base)) ? 'stale proxy — stealing' : 'starting proxy on'} ${base} (+ public tunnel)...`);
|
|
251
259
|
started = await startProxy({ silent: true, autoTunnel: true });
|
|
252
260
|
publicUrl = started?.publicUrl ?? null;
|
|
253
261
|
tunnelKey = started?.tunnelToken ?? null;
|
|
@@ -298,15 +306,6 @@ export async function setupEditor(which, target) {
|
|
|
298
306
|
console.log(' most common cause here: no working IPv6 route (we already force');
|
|
299
307
|
console.log(' --edge-ip-version 4). check: npx openzoo tunnel for the raw log.');
|
|
300
308
|
}
|
|
301
|
-
} else {
|
|
302
|
-
console.log(`proxy already running on ${base}`);
|
|
303
|
-
// A proxy someone else started owns the tunnel; ask it for the public URL.
|
|
304
|
-
try {
|
|
305
|
-
const info = await (await fetch(`${base}/info`)).json();
|
|
306
|
-
publicUrl = (info?.publicTunnel || '').replace(/\/v1$/, '') || null;
|
|
307
|
-
tunnelKey = info?.tunnelToken ?? tunnelKey;
|
|
308
|
-
if (publicUrl) console.log(`tunnel: ${publicUrl}/v1 (from the running proxy)`);
|
|
309
|
-
} catch { /* no /info — fall through to the localhost warning below */ }
|
|
310
309
|
}
|
|
311
310
|
// What the EDITOR is configured with. Localhost only as a last resort, and
|
|
312
311
|
// said out loud, because it will fail with the private-networks error.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
4
|
-
"description": "Local x402-paying proxy + MCP server for openzoo.fun
|
|
3
|
+
"version": "0.50.92",
|
|
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": {
|
package/lib/websearch.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// Keyless web search for `openzoo ask --web`: DuckDuckGo's HTML endpoint,
|
|
2
|
-
// scraped for title / url / snippet. No API key, no account, one GET. The
|
|
3
|
-
// only thing that leaves is the question text, to duckduckgo.com.
|
|
4
|
-
const strip = (s) => String(s || '')
|
|
5
|
-
.replace(/<[^>]+>/g, '')
|
|
6
|
-
.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>')
|
|
7
|
-
.replace(/\s+/g, ' ').trim();
|
|
8
|
-
|
|
9
|
-
export async function webSearch(query, max = 5) {
|
|
10
|
-
const res = await fetch('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), {
|
|
11
|
-
headers: { 'user-agent': 'Mozilla/5.0 openzoo-ask/1.0' },
|
|
12
|
-
signal: AbortSignal.timeout(12_000),
|
|
13
|
-
});
|
|
14
|
-
if (!res.ok) throw new Error(`duckduckgo HTTP ${res.status}`);
|
|
15
|
-
const html = await res.text();
|
|
16
|
-
const out = [];
|
|
17
|
-
const re = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
|
|
18
|
-
let m;
|
|
19
|
-
while ((m = re.exec(html)) && out.length < Math.max(1, Math.min(10, max))) {
|
|
20
|
-
let url = m[1];
|
|
21
|
-
const redirected = url.match(/uddg=([^&]+)/);
|
|
22
|
-
if (redirected) url = decodeURIComponent(redirected[1]);
|
|
23
|
-
out.push({ title: strip(m[2]), url, snippet: strip(m[3]).slice(0, 400) });
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function formatWebResults(query, hits) {
|
|
29
|
-
const lines = hits.map((h, i) => `${i + 1}. ${h.title} — ${h.url}\n ${h.snippet}`);
|
|
30
|
-
return `Web search results for "${query}" (DuckDuckGo, fetched just now; cite the url when you rely on one):\n${lines.join('\n')}`;
|
|
31
|
-
}
|