openzoo 0.50.90 → 0.50.91
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/launch.js +52 -2
- package/lib/proxy.js +15 -25
- package/package.json +1 -1
package/lib/launch.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* harness with the two env vars set. The proxy must already be running
|
|
10
10
|
* (`npx openzoo` in another terminal); we check first and say so if not.
|
|
11
11
|
*/
|
|
12
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
12
|
+
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
@@ -190,6 +190,41 @@ 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
|
+
/** Omarchy often has no lsof. Kill whatever is bound to the proxy port. */
|
|
195
|
+
function killPort(port) {
|
|
196
|
+
const n = Number(port);
|
|
197
|
+
for (const cmd of [
|
|
198
|
+
`lsof -t -iTCP:${n} -sTCP:LISTEN | xargs kill -9`,
|
|
199
|
+
`fuser -k ${n}/tcp`,
|
|
200
|
+
]) {
|
|
201
|
+
try { execSync(cmd, { stdio: 'ignore', timeout: 2000, shell: true }); } catch { /* tool missing */ }
|
|
202
|
+
}
|
|
203
|
+
// /proc fallback — no lsof/fuser required (Omarchy).
|
|
204
|
+
try {
|
|
205
|
+
execSync(`python3 - ${n}`, {
|
|
206
|
+
stdio: 'ignore', timeout: 2500, input: `import os, glob, sys
|
|
207
|
+
port=int(sys.argv[1]); hx=f'{port:04X}'
|
|
208
|
+
inodes=set()
|
|
209
|
+
for path in ('/proc/net/tcp','/proc/net/tcp6'):
|
|
210
|
+
try:
|
|
211
|
+
for line in open(path):
|
|
212
|
+
p=line.split()
|
|
213
|
+
if len(p)<10: continue
|
|
214
|
+
if p[1].split(':')[-1].upper()==hx: inodes.add(p[9])
|
|
215
|
+
except FileNotFoundError:
|
|
216
|
+
pass
|
|
217
|
+
for fd in glob.glob('/proc/[0-9]*/fd/[0-9]*'):
|
|
218
|
+
try: t=os.readlink(fd)
|
|
219
|
+
except OSError: continue
|
|
220
|
+
if any(ino and ino!='0' and ino in t for ino in inodes):
|
|
221
|
+
try: os.kill(int(fd.split('/')[2]), 9)
|
|
222
|
+
except OSError: pass
|
|
223
|
+
`,
|
|
224
|
+
});
|
|
225
|
+
} catch { /* no python or not linux */ }
|
|
226
|
+
}
|
|
227
|
+
|
|
193
228
|
export async function launchClaude(argv) {
|
|
194
229
|
// let, not const: startProxy can heal onto a different port and every URL
|
|
195
230
|
// below must follow the port we actually bound.
|
|
@@ -201,7 +236,22 @@ export async function launchClaude(argv) {
|
|
|
201
236
|
// /info, not /models — see the poll below. "Is a proxy already listening" must
|
|
202
237
|
// not be answered by an endpoint that needs the gateway, or a user whose
|
|
203
238
|
// upstream is flaky gets told to start a proxy that is already running.
|
|
204
|
-
|
|
239
|
+
const mine = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
240
|
+
try {
|
|
241
|
+
const r = await fetch(`${base}/info`, { signal: AbortSignal.timeout(3000) });
|
|
242
|
+
if (r.ok) {
|
|
243
|
+
const info = await r.json().catch(() => ({}));
|
|
244
|
+
const theirs = String(info.version || '');
|
|
245
|
+
if (!theirs || theirs !== mine) {
|
|
246
|
+
process.stderr.write(`openzoo: replacing stale proxy ${theirs ? 'v'+theirs : '(no version)'} with v${mine} on :${config.port}\n`);
|
|
247
|
+
killPort(config.port);
|
|
248
|
+
await new Promise((ok) => setTimeout(ok, 300));
|
|
249
|
+
up = false;
|
|
250
|
+
} else {
|
|
251
|
+
up = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch { up = false; }
|
|
205
255
|
if (!up) {
|
|
206
256
|
// NEVER GO SILENT DURING STARTUP. silent:true routes the proxy's own lines
|
|
207
257
|
// to ~/.openzoo/proxy.log so payment receipts cannot corrupt Claude Code's
|
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.
|
|
@@ -512,8 +512,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
512
512
|
? `${tunnelGate.publicUrl}/v1`
|
|
513
513
|
: `http://localhost:${config.port}/v1`;
|
|
514
514
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
515
|
+
const { version: ozVersion } = JSON.parse(
|
|
516
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
517
|
+
);
|
|
515
518
|
res.end(JSON.stringify({
|
|
516
519
|
youAreTalkingTo: 'openzoo proxy',
|
|
520
|
+
version: ozVersion,
|
|
521
|
+
solana: client.address,
|
|
517
522
|
yourEndpoint: self,
|
|
518
523
|
reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
|
|
519
524
|
publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
|
|
@@ -622,24 +627,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
622
627
|
servedRequests += 1;
|
|
623
628
|
say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
|
|
624
629
|
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
630
|
}
|
|
644
631
|
|
|
645
632
|
// Retry of a body we answered seconds ago? Serve the cached completion —
|
|
@@ -847,12 +834,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
847
834
|
rememberSpend();
|
|
848
835
|
say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
|
|
849
836
|
};
|
|
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
|
-
//
|
|
837
|
+
// A 402 AFTER we attempted payment is a SETTLEMENT failure, not a quote.
|
|
838
|
+
// The client-side balance check is advisory, so a funded wallet can still
|
|
839
|
+
// fail on-chain / at the facilitator, and the gateway answers the paid
|
|
840
|
+
// retry with a fresh 402. Relaying that as "wallet underfunded" + Whop
|
|
841
|
+
// copy-paste blamed burners that already paid. Surface the gateway's
|
|
842
|
+
// real reason; only prepend fund-me copy on genuine insufficient_funds.
|
|
856
843
|
if (response.status === 402) {
|
|
857
844
|
let usd;
|
|
858
845
|
let q402 = null;
|
|
@@ -874,6 +861,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
874
861
|
if (!Number.isFinite(usd)) usd = undefined;
|
|
875
862
|
} catch { usd = undefined; }
|
|
876
863
|
const copy = settleFailCopy(q402);
|
|
864
|
+
// paid:true → never "wallet underfunded", never Whop unless the
|
|
865
|
+
// gateway itself named insufficient_funds. UnderfundedError (empty
|
|
866
|
+
// wallet preflight) is handled in the catch below.
|
|
877
867
|
let msg = copy.message;
|
|
878
868
|
const wantOnramp = copy.code === 'insufficient_funds'
|
|
879
869
|
|| (!paid && isFundInstruction(copy.reason, copy));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.91",
|
|
4
4
|
"description": "Local x402-paying proxy + MCP server for openzoo.fun \u2014 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",
|