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 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
- try { up = (await fetch(`${base}/info`, { signal: AbortSignal.timeout(3000) })).ok; } catch { up = false; }
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, unopenrouter } from './models.js';
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
- // the client-side balance check is advisory, so a wallet that looks
852
- // fundable can still fail on-chain, and the gateway answers the paid
853
- // retry with a fresh 402. Relaying that raw meant Claude Code printed a
854
- // half-kilobyte accepts[] blob at the user instead of the one thing they
855
- // need: the price, what the wallet holds, and where to send funds.
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.90",
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",