openzoo 0.7.2 → 0.9.0
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 +6 -5
- package/lib/models.js +175 -0
- package/lib/proxy.js +96 -5
- package/package.json +1 -1
package/bin/openzoo.js
CHANGED
|
@@ -4,10 +4,10 @@ const cmd = process.argv[2] || 'proxy';
|
|
|
4
4
|
const HELP = `openzoo — local x402-paying proxy + MCP server for openzoo.fun
|
|
5
5
|
|
|
6
6
|
usage:
|
|
7
|
-
npx openzoo start the proxy
|
|
7
|
+
npx openzoo start the proxy: http://localhost:8402/v1 (keyless) PLUS a
|
|
8
|
+
public HTTPS url for cloud IDEs (key required, printed at start)
|
|
8
9
|
npx openzoo mcp stdio MCP server (tools: zoo_ask, zoo_models, zoo_wallet)
|
|
9
|
-
npx openzoo tunnel public
|
|
10
|
-
(installs cloudflared itself; mints a required api key)
|
|
10
|
+
npx openzoo tunnel public-url-only mode (everything key-gated, no keyless localhost)
|
|
11
11
|
npx openzoo demo ~1M-token needle demo: direct refuses, the zoo answers
|
|
12
12
|
(run it twice — the second run reuses the bound corpus and is near-free)
|
|
13
13
|
npx openzoo contexts list corpora bound to the zoo (never re-uploaded)
|
|
@@ -35,13 +35,14 @@ env:
|
|
|
35
35
|
OPENZOO_NO_CONTEXT_CACHE (0 — set 1 to always ship the full body)
|
|
36
36
|
OPENZOO_ENABLE_RH (0 — let DEFAULT selection fall through to the Robinhood rail;
|
|
37
37
|
OPENZOO_RAIL=robinhood forces it without this)
|
|
38
|
-
OPENZOO_TUNNEL_MAX_USD (1.00 —
|
|
38
|
+
OPENZOO_TUNNEL_MAX_USD (1.00 — public-url session ceiling) OPENZOO_TUNNEL_TOKEN (pin the api key)
|
|
39
|
+
OPENZOO_NO_TUNNEL (0 — set 1 for localhost-only, no public url)`;
|
|
39
40
|
|
|
40
41
|
async function main() {
|
|
41
42
|
switch (cmd) {
|
|
42
43
|
case 'proxy':
|
|
43
44
|
case 'start':
|
|
44
|
-
await (await import('../lib/proxy.js')).startProxy();
|
|
45
|
+
await (await import('../lib/proxy.js')).startProxy({ autoTunnel: true });
|
|
45
46
|
break;
|
|
46
47
|
case 'mcp':
|
|
47
48
|
await (await import('../lib/mcp.js')).startMcp();
|
package/lib/models.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Model-id rewriting — "any harness, zero model setup".
|
|
5
|
+
*
|
|
6
|
+
* Cursor and friends send THEIR model ids ("gpt-5.6-sol", "gpt-4o",
|
|
7
|
+
* "claude-…") to whatever base URL is configured, and the zoo answers
|
|
8
|
+
* "model not available". Users will not hand-add custom models per harness,
|
|
9
|
+
* so the proxy maps unknown ids onto the zoo's live catalog instead:
|
|
10
|
+
*
|
|
11
|
+
* 1. an id the zoo serves passes through UNTOUCHED — this layer can never
|
|
12
|
+
* hijack an explicit, valid choice;
|
|
13
|
+
* 2. a family hint in the requested id (grok→x-ai/, gemini→google/, …)
|
|
14
|
+
* picks the plain (non-:free/:batch) model of that family;
|
|
15
|
+
* 3. anything else — gpt-*, claude-*, composer, o3 — falls through to
|
|
16
|
+
* OPENZOO_DEFAULT_MODEL, or a preference-ordered pick from the catalog.
|
|
17
|
+
*
|
|
18
|
+
* Every rewrite is logged with both ids. The catalog is fetched live and
|
|
19
|
+
* cached briefly, so new zoo models resolve without shipping this package.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const CATALOG_TTL_MS = 5 * 60 * 1000;
|
|
23
|
+
let cache = { at: 0, ids: null };
|
|
24
|
+
|
|
25
|
+
export async function zooModelIds() {
|
|
26
|
+
if (cache.ids && Date.now() - cache.at < CATALOG_TTL_MS) return cache.ids;
|
|
27
|
+
const r = await fetch(`${config.apiBase}/v1/models`);
|
|
28
|
+
if (!r.ok) throw new Error(`model catalog fetch failed: HTTP ${r.status}`);
|
|
29
|
+
const d = await r.json();
|
|
30
|
+
const ids = (d.data || []).map((m) => m.id).filter(Boolean);
|
|
31
|
+
if (ids.length) cache = { at: Date.now(), ids };
|
|
32
|
+
return ids;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Vendor fingerprints in harness model ids → zoo catalog prefixes. Order
|
|
36
|
+
* matters only for overlapping hints; first match wins. */
|
|
37
|
+
const FAMILIES = [
|
|
38
|
+
[/^gpt|^chatgpt|^o[134]\b|^o[134]-|openai/i, 'openai/'],
|
|
39
|
+
[/claude|anthropic/i, 'anthropic/'],
|
|
40
|
+
[/gemini|google/i, 'google/'],
|
|
41
|
+
[/grok|x-?ai/i, 'x-ai/'],
|
|
42
|
+
[/deepseek/i, 'deepseek/'],
|
|
43
|
+
[/qwen/i, 'qwen/'],
|
|
44
|
+
[/mistral|mixtral|codestral/i, 'mistralai/'],
|
|
45
|
+
[/llama|meta\b/i, 'meta-llama/'],
|
|
46
|
+
[/glm|z-ai|zhipu/i, 'z-ai/'],
|
|
47
|
+
[/kimi|moonshot/i, 'moonshotai/'],
|
|
48
|
+
[/minimax/i, 'minimax/'],
|
|
49
|
+
[/command|cohere/i, 'cohere/'],
|
|
50
|
+
[/nova|amazon/i, 'amazon/'],
|
|
51
|
+
[/sonar|perplexity/i, 'perplexity/'],
|
|
52
|
+
[/nemotron|nvidia/i, 'nvidia/'],
|
|
53
|
+
[/seed|doubao|bytedance/i, 'bytedance-seed/'],
|
|
54
|
+
[/solar|upstage/i, 'upstage/'],
|
|
55
|
+
[/liquid|lfm/i, 'liquid/'],
|
|
56
|
+
[/sakana/i, 'sakana/'],
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Capability-tier fingerprints. The rewrite must land on a LIKE model — a
|
|
61
|
+
* harness asking for a flagship gets the zoo's flagship, "mini"/"flash" gets
|
|
62
|
+
* a light model, a reasoning id gets the heaviest thing available — never a
|
|
63
|
+
* one-size-fits-all default.
|
|
64
|
+
*/
|
|
65
|
+
const LIGHT_RE = /mini|nano|flash|lite|small|tiny|haiku|lightning|turbo/i;
|
|
66
|
+
const HEAVY_RE = /pro\b|pro-|max\b|opus|ultra|large|\bsol\b/i;
|
|
67
|
+
const REASON_RE = /^o[134]\b|^o[134]-|reason|think|r1\b|deepthink/i;
|
|
68
|
+
const CODE_RE = /code|coder|codex|composer|copilot/i;
|
|
69
|
+
|
|
70
|
+
/** "2.6b" → light, "70b"/"2.4t" → heavy; a param count in the id outranks words. */
|
|
71
|
+
function paramTier(id) {
|
|
72
|
+
const m = /(\d+(?:\.\d+)?)([bt])\b/i.exec(id);
|
|
73
|
+
if (!m) return null;
|
|
74
|
+
const n = Number(m[1]) * (m[2].toLowerCase() === 't' ? 1000 : 1);
|
|
75
|
+
return n >= 60 ? 'heavy' : n < 15 ? 'light' : 'mid';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tierOf(id) {
|
|
79
|
+
if (REASON_RE.test(id)) return 'reason';
|
|
80
|
+
return paramTier(id) || (LIGHT_RE.test(id) ? 'light' : HEAVY_RE.test(id) ? 'heavy' : 'mid');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const GENERIC_TOKENS = new Set(['chat', 'model', 'latest', 'preview', 'instruct', 'v1', 'v2', 'v3', 'v4']);
|
|
84
|
+
const tokensOf = (id) => id.toLowerCase().split(/[^a-z0-9.]+/).filter((t) => t && !GENERIC_TOKENS.has(t));
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Map a requested model id onto the catalog by similarity. Returns null when
|
|
88
|
+
* the id is already servable (no rewrite), otherwise the closest zoo id.
|
|
89
|
+
* OPENZOO_DEFAULT_MODEL is an explicit user override, not a fallback tier.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveModel(requested, ids) {
|
|
92
|
+
if (!requested || !ids?.length || ids.includes(requested)) return null;
|
|
93
|
+
const env = process.env.OPENZOO_DEFAULT_MODEL;
|
|
94
|
+
if (env && ids.includes(env)) return env;
|
|
95
|
+
|
|
96
|
+
const reqTier = tierOf(requested);
|
|
97
|
+
const reqCode = CODE_RE.test(requested);
|
|
98
|
+
const reqToks = new Set(tokensOf(requested));
|
|
99
|
+
|
|
100
|
+
let best = null;
|
|
101
|
+
let bestScore = -Infinity;
|
|
102
|
+
for (const id of ids) {
|
|
103
|
+
let score = 0;
|
|
104
|
+
// Same vendor family is the strongest signal there is.
|
|
105
|
+
for (const [re, prefix] of FAMILIES) {
|
|
106
|
+
if (re.test(requested) && id.startsWith(prefix)) { score += 100; break; }
|
|
107
|
+
}
|
|
108
|
+
// Tier: exact match strong; reasoning degrades to heavy (a reasoner's
|
|
109
|
+
// nearest neighbour is a flagship, never a mini); mid borders both.
|
|
110
|
+
const t = tierOf(id);
|
|
111
|
+
if (t === reqTier) score += 40;
|
|
112
|
+
else if (reqTier === 'reason' && t === 'heavy') score += 30;
|
|
113
|
+
else if ((reqTier === 'mid') !== (t === 'mid') && t !== 'light' && reqTier !== 'light') score += 15;
|
|
114
|
+
else if ((reqTier === 'light' && t === 'mid') || (reqTier === 'mid' && t === 'light')) score += 15;
|
|
115
|
+
// Specialisation: code asks want code models; nothing else does.
|
|
116
|
+
if (CODE_RE.test(id)) score += reqCode ? 25 : -8;
|
|
117
|
+
// Shared name tokens ("grok", "4.6", "sonnet") pull toward the namesake.
|
|
118
|
+
for (const tok of tokensOf(id)) if (reqToks.has(tok)) score += 10;
|
|
119
|
+
// Full-strength beats :free/:batch variants at equal similarity.
|
|
120
|
+
if (!id.includes(':')) score += 5;
|
|
121
|
+
if (score > bestScore) { bestScore = score; best = id; }
|
|
122
|
+
}
|
|
123
|
+
return best;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ids harnesses ship as DEFAULTS (Cursor, Continue, Aider, Codex CLI, Cline,
|
|
128
|
+
* OpenClaw, LangChain templates…). Merged into GET /v1/models so a harness
|
|
129
|
+
* that validates its configured model against the list passes validation —
|
|
130
|
+
* the POST is then rewritten by resolveModel. Every one of these resolves.
|
|
131
|
+
*/
|
|
132
|
+
export const ALIAS_IDS = [
|
|
133
|
+
'gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4-turbo', 'gpt-3.5-turbo',
|
|
134
|
+
'gpt-5', 'gpt-5-mini', 'chatgpt-4o-latest', 'o1', 'o3', 'o3-mini', 'o4-mini',
|
|
135
|
+
'claude-3-5-sonnet-latest', 'claude-sonnet-4-0', 'claude-opus-4-1',
|
|
136
|
+
'gemini-2.5-pro', 'gemini-2.5-flash', 'grok-4', 'grok-3',
|
|
137
|
+
'deepseek-chat', 'deepseek-reasoner', 'qwen-max', 'llama-3.3-70b',
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
/** Merge alias rows into a /v1/models payload without duplicating real ids. */
|
|
141
|
+
export function augmentModelList(payload) {
|
|
142
|
+
const data = Array.isArray(payload?.data) ? payload.data : [];
|
|
143
|
+
const have = new Set(data.map((m) => m.id));
|
|
144
|
+
const aliases = ALIAS_IDS.filter((id) => !have.has(id))
|
|
145
|
+
.map((id) => ({ id, object: 'model', owned_by: 'openzoo-alias' }));
|
|
146
|
+
return { ...payload, object: payload?.object || 'list', data: [...data, ...aliases] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Which request paths carry a rewritable model field. POST-only; embeddings /
|
|
151
|
+
* audio / image / moderation models are DIFFERENT model families — rewriting
|
|
152
|
+
* a chat model into those would corrupt the call, so they pass untouched.
|
|
153
|
+
*/
|
|
154
|
+
export function rewritablePath(method, url) {
|
|
155
|
+
if (method !== 'POST') return false;
|
|
156
|
+
const p = (url || '').split('?')[0];
|
|
157
|
+
return !/embed|audio|image|moderation/.test(p);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Rewrite the model field of any request body that has one.
|
|
162
|
+
* Returns null (send as-is) or { body, from, to }. Any failure — bad JSON,
|
|
163
|
+
* unreachable catalog — returns null: this layer must never break a call
|
|
164
|
+
* that would have worked without it.
|
|
165
|
+
*/
|
|
166
|
+
export async function maybeRewriteModel(bodyBuf) {
|
|
167
|
+
let body;
|
|
168
|
+
try { body = JSON.parse(bodyBuf.toString('utf8')); } catch { return null; }
|
|
169
|
+
if (typeof body?.model !== 'string') return null;
|
|
170
|
+
let ids;
|
|
171
|
+
try { ids = await zooModelIds(); } catch { return null; }
|
|
172
|
+
const to = resolveModel(body.model, ids);
|
|
173
|
+
if (!to) return null;
|
|
174
|
+
return { body: Buffer.from(JSON.stringify({ ...body, model: to })), from: body.model, to };
|
|
175
|
+
}
|
package/lib/proxy.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
|
|
7
7
|
import { tokenBalance } from './x402.js';
|
|
8
8
|
import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
|
|
9
|
+
import { maybeRewriteModel, rewritablePath, augmentModelList, ALIAS_IDS } from './models.js';
|
|
9
10
|
import { forgetContext } from './contexts.js';
|
|
10
11
|
|
|
11
12
|
const HOP_BY_HOP = new Set([
|
|
@@ -110,13 +111,23 @@ async function maybeCacheCorpus(req, bodyBuf, log) {
|
|
|
110
111
|
* becomes the only thing between a stranger and your wallet. Both are off by
|
|
111
112
|
* default, so localhost behaviour is unchanged.
|
|
112
113
|
*/
|
|
113
|
-
export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null } = {}) {
|
|
114
|
+
export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
|
|
114
115
|
const client = new PayClient();
|
|
115
116
|
const log = silent ? () => {} : (...a) => console.log(...a);
|
|
116
117
|
let sessionSpent = 0;
|
|
118
|
+
let tunnelSpent = 0;
|
|
119
|
+
// Set once cloudflared is up (see below). Gating keys off the REQUEST's
|
|
120
|
+
// origin, not off whether the URL exists yet, so there is no startup window
|
|
121
|
+
// where public traffic slips through ungated.
|
|
122
|
+
let tunnelGate = null;
|
|
117
123
|
|
|
118
124
|
const server = http.createServer(async (req, res) => {
|
|
119
125
|
const url = `${config.apiBase}${req.url}`;
|
|
126
|
+
// Requests that arrived over the public quick-tunnel URL carry cloudflared's
|
|
127
|
+
// headers; nothing dialing 127.0.0.1 directly does. That distinction is what
|
|
128
|
+
// lets localhost stay keyless while the SAME port is safely public.
|
|
129
|
+
const viaTunnel = !requireToken && tunnelGate
|
|
130
|
+
&& Boolean(req.headers['cf-connecting-ip'] || req.headers['cf-ray']);
|
|
120
131
|
// Auth first: refuse before reading a body, forwarding, quoting or paying.
|
|
121
132
|
if (requireToken) {
|
|
122
133
|
const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
@@ -130,6 +141,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
130
141
|
return;
|
|
131
142
|
}
|
|
132
143
|
}
|
|
144
|
+
if (viaTunnel) {
|
|
145
|
+
const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
146
|
+
if (got !== tunnelGate.token) {
|
|
147
|
+
log(`public url: 401 ${req.method} ${req.url}`);
|
|
148
|
+
jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at startup');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (tunnelSpent >= tunnelGate.sessionMaxUsd) {
|
|
152
|
+
jsonErr(res, 402, `openzoo public-URL session cap reached ($${tunnelGate.sessionMaxUsd}) — restart the proxy or raise OPENZOO_TUNNEL_MAX_USD`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
133
156
|
let bodyBuf;
|
|
134
157
|
try {
|
|
135
158
|
bodyBuf = await readBody(req);
|
|
@@ -137,9 +160,41 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
137
160
|
jsonErr(res, 400, 'bad request body');
|
|
138
161
|
return;
|
|
139
162
|
}
|
|
163
|
+
// Harness model ids ("gpt-5.6-sol", "claude-…") are rewritten onto the
|
|
164
|
+
// NEAREST zoo model BEFORE anything else sees the body — any POST that
|
|
165
|
+
// carries a model field, not just chat/completions, so /completions,
|
|
166
|
+
// /responses and future shapes all work. Never silent.
|
|
167
|
+
if (rewritablePath(req.method, req.url)) {
|
|
168
|
+
const rw = await maybeRewriteModel(bodyBuf);
|
|
169
|
+
if (rw) {
|
|
170
|
+
log(`model "${rw.from}" is not on the zoo — nearest match ${rw.to} (OPENZOO_DEFAULT_MODEL overrides)`);
|
|
171
|
+
bodyBuf = rw.body;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
140
174
|
const init = { method: req.method, headers: upstreamHeaders(req) };
|
|
141
175
|
if (req.method !== 'GET' && req.method !== 'HEAD') init.body = bodyBuf;
|
|
142
176
|
|
|
177
|
+
// Harnesses validate their configured model BEFORE ever POSTing — some
|
|
178
|
+
// list /v1/models, some probe /v1/models/<id>. Both must succeed for the
|
|
179
|
+
// ids we know how to rewrite, or the harness refuses upfront and the
|
|
180
|
+
// rewrite never gets its chance.
|
|
181
|
+
const path = (req.url || '').split('?')[0];
|
|
182
|
+
if (req.method === 'GET' && path === '/v1/models') {
|
|
183
|
+
try {
|
|
184
|
+
const { response } = await client.fetch(url, init);
|
|
185
|
+
const payload = await response.json();
|
|
186
|
+
res.writeHead(response.status, { 'content-type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify(response.ok ? augmentModelList(payload) : payload));
|
|
188
|
+
return;
|
|
189
|
+
} catch { /* fall through to the plain relay below */ }
|
|
190
|
+
}
|
|
191
|
+
const probe = req.method === 'GET' && /^\/v1\/models\/(.+)$/.exec(path);
|
|
192
|
+
if (probe && ALIAS_IDS.includes(decodeURIComponent(probe[1]))) {
|
|
193
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify({ id: decodeURIComponent(probe[1]), object: 'model', owned_by: 'openzoo-alias' }));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
143
198
|
try {
|
|
144
199
|
let cached = null;
|
|
145
200
|
try {
|
|
@@ -175,11 +230,17 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
175
230
|
}
|
|
176
231
|
const { response, paid, receipt } = result;
|
|
177
232
|
if (paid && receipt) {
|
|
178
|
-
if (receipt.ok && typeof receipt.billedUsd === 'number')
|
|
233
|
+
if (receipt.ok && typeof receipt.billedUsd === 'number') {
|
|
234
|
+
sessionSpent += receipt.billedUsd;
|
|
235
|
+
// The public-URL ceiling meters only public-origin spend — your own
|
|
236
|
+
// local calls never eat into it.
|
|
237
|
+
if (viaTunnel) tunnelSpent += receipt.billedUsd;
|
|
238
|
+
}
|
|
179
239
|
const line = receipt.ok ? receipt.line : `paid retry -> HTTP ${receipt.status}`;
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
log(
|
|
240
|
+
// Wherever a running total is the thing to watch, it rides the receipt.
|
|
241
|
+
if (requireToken) log(`${line} · session $${sessionSpent.toFixed(6)}`);
|
|
242
|
+
else if (viaTunnel) log(`${line} · public-url session $${tunnelSpent.toFixed(6)}`);
|
|
243
|
+
else log(line);
|
|
183
244
|
}
|
|
184
245
|
await relay(res, response);
|
|
185
246
|
} catch (err) {
|
|
@@ -252,5 +313,35 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
252
313
|
console.log(` base_url = http://localhost:${config.port}/v1`);
|
|
253
314
|
console.log(' api_key = sk-openzoo (any value works; the zoo takes payment, not keys)');
|
|
254
315
|
}
|
|
316
|
+
|
|
317
|
+
// AUTO-TUNNEL: `npx openzoo` must be end-to-end for cloud IDEs too — their
|
|
318
|
+
// servers cannot dial localhost, so the default command also publishes a
|
|
319
|
+
// quick-tunnel URL. It comes up in the background (never delays localhost),
|
|
320
|
+
// failure degrades to local-only, and public traffic is gated above by
|
|
321
|
+
// token + its own spend ceiling. OPENZOO_NO_TUNNEL=1 opts out.
|
|
322
|
+
if (autoTunnel && process.env.OPENZOO_NO_TUNNEL !== '1') {
|
|
323
|
+
(async () => {
|
|
324
|
+
try {
|
|
325
|
+
const { ensureCloudflared, startCloudflared, mintToken } = await import('./tunnel.js');
|
|
326
|
+
const token = mintToken();
|
|
327
|
+
const cap = Number(process.env.OPENZOO_TUNNEL_MAX_USD || 1);
|
|
328
|
+
const bin = await ensureCloudflared((m) => log(m));
|
|
329
|
+
const { url, proc } = await startCloudflared(bin, config.port, log);
|
|
330
|
+
tunnelGate = { token, sessionMaxUsd: cap };
|
|
331
|
+
const bye = () => { try { proc.kill('SIGTERM'); } catch { /* already gone */ } };
|
|
332
|
+
process.once('SIGINT', () => { bye(); process.exit(0); });
|
|
333
|
+
process.once('SIGTERM', () => { bye(); process.exit(0); });
|
|
334
|
+
process.once('exit', bye);
|
|
335
|
+
log('');
|
|
336
|
+
log('cloud IDE / remote harness? use the public URL (they cannot reach localhost):');
|
|
337
|
+
log(` base_url = ${url}/v1`);
|
|
338
|
+
log(` api_key = ${token}`);
|
|
339
|
+
log(` (key REQUIRED on the public URL — it spends this wallet; capped at $${cap.toFixed(2)}/session,`);
|
|
340
|
+
log(' OPENZOO_TUNNEL_MAX_USD to change, OPENZOO_NO_TUNNEL=1 for localhost-only)');
|
|
341
|
+
} catch (err) {
|
|
342
|
+
log(`public URL unavailable (${err.message}) — localhost still works; OPENZOO_NO_TUNNEL=1 hides this line`);
|
|
343
|
+
}
|
|
344
|
+
})();
|
|
345
|
+
}
|
|
255
346
|
return { server, client, spent: () => sessionSpent };
|
|
256
347
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|