openzoo 0.49.7 → 0.49.9
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/README.md +39 -1
- package/bin/openzoo.js +3 -2
- package/lib/claudecode.js +847 -0
- package/lib/grokui.mjs +1351 -226
- package/lib/launch.js +244 -152
- package/lib/livestatus.js +6 -4
- package/lib/modelroute/README.md +1 -0
- package/lib/modelroute/catalog.json +1 -0
- package/lib/modelroute/outcomes.json +1566 -0
- package/lib/modelroute/router.json +1 -0
- package/lib/modelroute.js +737 -0
- package/lib/models.js +221 -44
- package/lib/package.json +3 -0
- package/lib/pay.js +67 -3
- package/lib/podagent.mjs +84 -28
- package/lib/proxy.js +144 -56
- package/lib/racesettle.js +127 -0
- package/lib/relay.js +275 -0
- package/lib/runguard.js +31 -0
- package/lib/spill.js +9 -1
- package/lib/think.js +126 -0
- package/package.json +5 -4
- package/vendor/modelroute/CURRENT_STATE.md +132 -0
- package/vendor/modelroute/HANDOFF.md +159 -0
- package/vendor/modelroute/catalog.json +1 -0
- package/vendor/modelroute/holographic_modelroute.py +809 -0
- package/vendor/modelroute/outcomes.json +1566 -0
- package/vendor/modelroute/router.json +1 -0
package/lib/racesettle.js
CHANGED
|
@@ -180,6 +180,133 @@ export function receiptDirectUsd(x) {
|
|
|
180
180
|
return billedOk ? billed : 0;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** Zoo's share of (direct − OpenRouter) when the 402 has real savings. */
|
|
184
|
+
export const SAVINGS_SHARE = 0.33;
|
|
185
|
+
/**
|
|
186
|
+
* billed / usage.cost above this, without settled house cogs, is the
|
|
187
|
+
* max_tokens quote reserve — not the charge after completion.
|
|
188
|
+
* MEASURED 2026-08-19: $0.9858 reserved / $0.007962 usage.cost ≈ 124× on a ~1× call.
|
|
189
|
+
*/
|
|
190
|
+
export const QUOTE_RESERVE_X = 2;
|
|
191
|
+
|
|
192
|
+
function money(v) {
|
|
193
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function closeRatio(a, b, rel = 0.25) {
|
|
197
|
+
const den = Math.max(b, 1e-12);
|
|
198
|
+
return Math.abs(a - b) / den <= rel;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* After-completion billed fields the gateway may already put on x402 / usage.
|
|
203
|
+
* Read only if present — do not invent them on the wire.
|
|
204
|
+
*/
|
|
205
|
+
function explicitSettledBilled(x, usage) {
|
|
206
|
+
const bags = [x, usage, x?.used, x?.settled, x?.receipt, usage?.used, usage?.settled]
|
|
207
|
+
.filter((o) => o && typeof o === 'object' && !Array.isArray(o));
|
|
208
|
+
const keys = [
|
|
209
|
+
'billedActual', 'billedActualUsd', 'settledUsd', 'settledBilledUsd',
|
|
210
|
+
'chargedUsd', 'usedUsd', 'actualBilledUsd',
|
|
211
|
+
];
|
|
212
|
+
for (const bag of bags) {
|
|
213
|
+
for (const k of keys) {
|
|
214
|
+
const n = money(bag[k]);
|
|
215
|
+
if (n != null) return n;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** tokens actually used × unit prices, only when both sides are already on the object. */
|
|
222
|
+
function billedFromUsedTokens(x, usage) {
|
|
223
|
+
const prompt = money(usage?.prompt_tokens ?? usage?.promptTokens ?? x?.prompt_tokens);
|
|
224
|
+
const completion = money(usage?.completion_tokens ?? usage?.completionTokens ?? x?.completion_tokens);
|
|
225
|
+
const inPrice = money(x?.promptPriceUsd ?? x?.inputPriceUsd ?? x?.priceInUsd);
|
|
226
|
+
const outPrice = money(x?.completionPriceUsd ?? x?.outputPriceUsd ?? x?.priceOutUsd);
|
|
227
|
+
if (prompt != null && completion != null && inPrice != null && outPrice != null) {
|
|
228
|
+
return prompt * inPrice + completion * outPrice;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* True when `billed` is the quote-time max_tokens ceiling, not the settled charge.
|
|
235
|
+
* A large billed/cost is honest when cogs already matches usage.cost (33% of
|
|
236
|
+
* real savings). The 124× lie is billed >> cost with at-cost / reserved cogs.
|
|
237
|
+
*/
|
|
238
|
+
export function isQuoteReserveBilled(billed, cost, x = {}) {
|
|
239
|
+
if (money(billed) == null || money(cost) == null) return false;
|
|
240
|
+
if (cost === 0) return billed > 0;
|
|
241
|
+
if (billed <= cost * QUOTE_RESERVE_X) return false;
|
|
242
|
+
const cogs = money(x?.cogsUsd);
|
|
243
|
+
if (cogs != null && closeRatio(cogs, cost)) return false;
|
|
244
|
+
const saved = money(x?.savedUsd);
|
|
245
|
+
if (saved == null || saved <= cost * 0.5) return true;
|
|
246
|
+
if (cogs != null && cogs > cost * QUOTE_RESERVE_X) return true;
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Post-completion billed USD to pair with usage.cost / x.actualUsd.
|
|
252
|
+
* `x.billedUsd` is often the quote reserve (max_tokens × catalog), which made
|
|
253
|
+
* HUD markupX read 124× on a ~1× call. Prefer a settled field; otherwise
|
|
254
|
+
* reconstruct from tokens used × price or from usage.cost (+ 33% of settled
|
|
255
|
+
* savings). Never return the reserve when we learned the real upstream cost.
|
|
256
|
+
*/
|
|
257
|
+
export function receiptSettledBilled(x, usage) {
|
|
258
|
+
const cost = money(usage?.cost) ?? money(x?.actualUsd) ?? money(usage?.actualUsd);
|
|
259
|
+
const explicit = explicitSettledBilled(x, usage);
|
|
260
|
+
if (explicit != null) return explicit;
|
|
261
|
+
|
|
262
|
+
const fromTokens = billedFromUsedTokens(x, usage);
|
|
263
|
+
if (fromTokens != null) {
|
|
264
|
+
const billed = money(x?.billedUsd);
|
|
265
|
+
if (billed == null || isQuoteReserveBilled(billed, fromTokens, x)
|
|
266
|
+
|| (cost != null && isQuoteReserveBilled(billed, cost, x))) {
|
|
267
|
+
return fromTokens;
|
|
268
|
+
}
|
|
269
|
+
return billed;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const billed = money(x?.billedUsd);
|
|
273
|
+
if (billed != null && (cost == null || !isQuoteReserveBilled(billed, cost, x))) {
|
|
274
|
+
return billed;
|
|
275
|
+
}
|
|
276
|
+
if (cost != null) {
|
|
277
|
+
const saved = settledSavedUsd(x, cost);
|
|
278
|
+
return cost + SAVINGS_SHARE * saved;
|
|
279
|
+
}
|
|
280
|
+
return billed ?? 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function settledSavedUsd(x, cost) {
|
|
284
|
+
const saved = money(x?.savedUsd);
|
|
285
|
+
if (saved == null || saved <= 0) return 0;
|
|
286
|
+
const cogs = money(x?.cogsUsd);
|
|
287
|
+
// Quote-time savedUsd rides the same max_tokens reserve. Only keep it when
|
|
288
|
+
// house cost already matches the metered upstream (settled cogs).
|
|
289
|
+
if (cogs != null && closeRatio(cogs, cost)) return saved;
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Pair the HUD denominator (real upstream) with the post-completion billed
|
|
295
|
+
* twin. Null when this call did not report a real cost — do not mix populations.
|
|
296
|
+
*/
|
|
297
|
+
export function pairActualBilled(x402, usage) {
|
|
298
|
+
const fromUsage = money(usage?.cost);
|
|
299
|
+
const fromX = money(x402?.actualUsd) ?? money(x402?.usage?.cost);
|
|
300
|
+
const upstreamUsd = fromUsage ?? fromX;
|
|
301
|
+
if (upstreamUsd == null) return null;
|
|
302
|
+
const bag = x402 && typeof x402 === 'object' ? x402 : {};
|
|
303
|
+
const usageBag = usage && typeof usage === 'object' ? usage : {};
|
|
304
|
+
return {
|
|
305
|
+
upstreamUsd,
|
|
306
|
+
billedUsd: receiptSettledBilled(bag, { ...usageBag, cost: upstreamUsd }),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
183
310
|
/**
|
|
184
311
|
* Session meter. spent/direct are the receipt totals as billed — never a
|
|
185
312
|
* first-call rewrite, never a race_unused user refund.
|
package/lib/relay.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipe an upstream fetch Response to the client.
|
|
3
|
+
*
|
|
4
|
+
* Fly (and other CDNs) gzip error bodies. Node fetch does not always inflate
|
|
5
|
+
* those — MEASURED: a 400 from x402-tokens.fly.dev arrived still gzip-wrapped
|
|
6
|
+
* with `Content-Encoding: gzip`. The old relay stripped that header and
|
|
7
|
+
* `Content-Length`, then piped `response.body` RAW. Claude Code printed
|
|
8
|
+
* `API Error: 400` plus diamond/binary mojibake.
|
|
9
|
+
*
|
|
10
|
+
* Contract: never emit a compressed body without Content-Encoding. Prefer
|
|
11
|
+
* decompressing so the client sees application/json or text (Claude CLI can
|
|
12
|
+
* parse the error). Unknown encodings keep the header and the bytes together.
|
|
13
|
+
*/
|
|
14
|
+
import zlib from 'node:zlib';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
16
|
+
import { Readable, PassThrough } from 'node:stream';
|
|
17
|
+
|
|
18
|
+
const gunzipAsync = promisify(zlib.gunzip);
|
|
19
|
+
const inflateAsync = promisify(zlib.inflate);
|
|
20
|
+
const inflateRawAsync = promisify(zlib.inflateRaw);
|
|
21
|
+
const brotliAsync = promisify(zlib.brotliDecompress);
|
|
22
|
+
|
|
23
|
+
const HOP = new Set(['transfer-encoding', 'connection', 'content-length']);
|
|
24
|
+
|
|
25
|
+
export function looksGzip(buf) {
|
|
26
|
+
return Buffer.isBuffer(buf) && buf.length >= 2 && buf[0] === 0x1f && buf[1] === 0x8b;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function headerEncoding(headers) {
|
|
30
|
+
return String(headers?.get?.('content-encoding') || '').trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Single known codec, or null if identity / absent / unknown / stacked. */
|
|
34
|
+
export function knownCodec(encoding) {
|
|
35
|
+
const parts = String(encoding || '')
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((s) => s.trim().toLowerCase())
|
|
38
|
+
.filter((s) => s && s !== 'identity');
|
|
39
|
+
if (parts.length !== 1) return null;
|
|
40
|
+
if (parts[0] === 'gzip' || parts[0] === 'x-gzip') return 'gzip';
|
|
41
|
+
if (parts[0] === 'deflate') return 'deflate';
|
|
42
|
+
if (parts[0] === 'br') return 'br';
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function inflateEncoded(buf, encoding) {
|
|
47
|
+
const codec = knownCodec(encoding) || (looksGzip(buf) ? 'gzip' : null);
|
|
48
|
+
if (!codec) return buf;
|
|
49
|
+
// Leftover Content-Encoding after fetch already inflated: do not gunzip JSON.
|
|
50
|
+
if (codec === 'gzip' && !looksGzip(buf)) return buf;
|
|
51
|
+
try {
|
|
52
|
+
if (codec === 'gzip') return await gunzipAsync(buf);
|
|
53
|
+
if (codec === 'br') return await brotliAsync(buf);
|
|
54
|
+
try {
|
|
55
|
+
return await inflateAsync(buf);
|
|
56
|
+
} catch {
|
|
57
|
+
return await inflateRawAsync(buf);
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (!looksBinary(buf)) return buf;
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function looksBinary(buf) {
|
|
66
|
+
if (!buf || !buf.length) return false;
|
|
67
|
+
if (looksGzip(buf)) return true;
|
|
68
|
+
const n = Math.min(buf.length, 256);
|
|
69
|
+
let ctrl = 0;
|
|
70
|
+
for (let i = 0; i < n; i++) {
|
|
71
|
+
const c = buf[i];
|
|
72
|
+
if (c === 0 || c < 8 || (c > 13 && c < 32)) ctrl += 1;
|
|
73
|
+
}
|
|
74
|
+
return ctrl >= 4;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function copyHeaders(upstream, { decoded }) {
|
|
78
|
+
const headers = {};
|
|
79
|
+
upstream.headers.forEach((v, k) => {
|
|
80
|
+
const key = String(k).toLowerCase();
|
|
81
|
+
if (HOP.has(key)) return;
|
|
82
|
+
// Drop Content-Encoding only when the bytes we send are already inflated.
|
|
83
|
+
if (decoded && key === 'content-encoding') return;
|
|
84
|
+
headers[k] = v;
|
|
85
|
+
});
|
|
86
|
+
return headers;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function preferReadableType(headers, buf) {
|
|
90
|
+
const ct = String(headers['content-type'] || '');
|
|
91
|
+
if (ct && !/octet-stream|(?:x-)?gzip|br\b/i.test(ct)) return;
|
|
92
|
+
const c = buf[0];
|
|
93
|
+
headers['content-type'] = (c === 0x7b || c === 0x5b)
|
|
94
|
+
? 'application/json; charset=utf-8'
|
|
95
|
+
: 'text/plain; charset=utf-8';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function jsonUpstreamErr(res, status) {
|
|
99
|
+
res.writeHead(status || 502, { 'content-type': 'application/json' });
|
|
100
|
+
res.end(JSON.stringify({ error: { message: `upstream HTTP ${status || 502}` } }));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readBody(upstream) {
|
|
104
|
+
if (!upstream.body) return Buffer.alloc(0);
|
|
105
|
+
const chunks = [];
|
|
106
|
+
for await (const c of Readable.fromWeb(upstream.body)) chunks.push(c);
|
|
107
|
+
return Buffer.concat(chunks);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Non-OK: buffer, inflate gzip/br/deflate (or gzip-magic with no header),
|
|
112
|
+
* send uncompressed JSON/text. Never forward gzip bytes with the encoding
|
|
113
|
+
* header stripped. Un-decodable compressed bodies become a short JSON error
|
|
114
|
+
* so Claude / grokui do not paint diamond mojibake — the canvas fold to
|
|
115
|
+
* `upstream HTTP 400` lives in #65; we just refuse to emit the binary.
|
|
116
|
+
*/
|
|
117
|
+
async function relayError(res, upstream) {
|
|
118
|
+
const encoding = headerEncoding(upstream.headers);
|
|
119
|
+
const codec = knownCodec(encoding);
|
|
120
|
+
let buf;
|
|
121
|
+
try {
|
|
122
|
+
buf = await readBody(upstream);
|
|
123
|
+
} catch {
|
|
124
|
+
jsonUpstreamErr(res, upstream.status);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (codec || looksGzip(buf)) {
|
|
129
|
+
try {
|
|
130
|
+
buf = await inflateEncoded(buf, encoding);
|
|
131
|
+
} catch {
|
|
132
|
+
jsonUpstreamErr(res, upstream.status);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (looksBinary(buf)) {
|
|
136
|
+
jsonUpstreamErr(res, upstream.status);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const headers = copyHeaders(upstream, { decoded: true });
|
|
140
|
+
preferReadableType(headers, buf);
|
|
141
|
+
res.writeHead(upstream.status, headers);
|
|
142
|
+
res.end(buf);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (encoding && !codec) {
|
|
147
|
+
// Unknown / stacked coding: keep the label with the bytes.
|
|
148
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: false }));
|
|
149
|
+
res.end(buf);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (looksBinary(buf)) {
|
|
154
|
+
jsonUpstreamErr(res, upstream.status);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const headers = copyHeaders(upstream, { decoded: true });
|
|
158
|
+
preferReadableType(headers, buf);
|
|
159
|
+
res.writeHead(upstream.status, headers);
|
|
160
|
+
res.end(buf);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createDecoder(codec) {
|
|
164
|
+
if (codec === 'gzip') return zlib.createGunzip();
|
|
165
|
+
if (codec === 'deflate') return zlib.createInflate();
|
|
166
|
+
if (codec === 'br') return zlib.createBrotliDecompress();
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function prependChunk(chunk, rest) {
|
|
171
|
+
const out = new PassThrough();
|
|
172
|
+
out.write(chunk);
|
|
173
|
+
rest.pipe(out);
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function attachBody(res, body, onReceipt, sse, resolve) {
|
|
178
|
+
body.on('error', () => res.destroy());
|
|
179
|
+
res.on('close', () => body.destroy());
|
|
180
|
+
if (!sse || typeof onReceipt !== 'function') {
|
|
181
|
+
body.pipe(res);
|
|
182
|
+
body.on('end', resolve);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
let pending = '';
|
|
186
|
+
body.on('data', (c) => {
|
|
187
|
+
res.write(c);
|
|
188
|
+
pending += c.toString('utf8');
|
|
189
|
+
const lines = pending.split('\n');
|
|
190
|
+
pending = lines.pop() ?? '';
|
|
191
|
+
for (const line of lines) {
|
|
192
|
+
if (!line.startsWith(': x402 ')) continue;
|
|
193
|
+
try { onReceipt(JSON.parse(line.slice(7))); } catch { /* not our frame */ }
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
body.on('end', () => { res.end(); resolve(); });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 2xx stream. Uncompressed (the cheap Fly 200) is the old pipe. Encoded
|
|
201
|
+
* bodies are inflated when we know the codec, otherwise the encoding
|
|
202
|
+
* header stays with the bytes.
|
|
203
|
+
*/
|
|
204
|
+
function relayOk(res, upstream, onReceipt) {
|
|
205
|
+
const encoding = headerEncoding(upstream.headers);
|
|
206
|
+
const codec = knownCodec(encoding);
|
|
207
|
+
const sse = (upstream.headers.get('content-type') || '').includes('text/event-stream');
|
|
208
|
+
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
const raw = Readable.fromWeb(upstream.body);
|
|
211
|
+
raw.on('error', () => res.destroy());
|
|
212
|
+
res.on('close', () => raw.destroy());
|
|
213
|
+
|
|
214
|
+
if (!encoding) {
|
|
215
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: true }));
|
|
216
|
+
attachBody(res, raw, onReceipt, sse, resolve);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Peek so a leftover Content-Encoding on already-plain bytes is stripped,
|
|
221
|
+
// and a real gzip body is never sent naked.
|
|
222
|
+
let decided = false;
|
|
223
|
+
raw.once('data', (chunk) => {
|
|
224
|
+
if (decided) return;
|
|
225
|
+
decided = true;
|
|
226
|
+
const head = Buffer.from(chunk);
|
|
227
|
+
const gzip = looksGzip(head);
|
|
228
|
+
if (gzip || (codec && codec !== 'gzip')) {
|
|
229
|
+
const dec = codec ? createDecoder(codec) : zlib.createGunzip();
|
|
230
|
+
if (!dec) {
|
|
231
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: false }));
|
|
232
|
+
attachBody(res, prependChunk(head, raw), onReceipt, sse, resolve);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
dec.write(head);
|
|
236
|
+
raw.pipe(dec);
|
|
237
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: true }));
|
|
238
|
+
attachBody(res, dec, onReceipt, sse, resolve);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Header leftover on plain JSON/SSE — drop encoding, replay the peek.
|
|
242
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: true }));
|
|
243
|
+
attachBody(res, prependChunk(head, raw), onReceipt, sse, resolve);
|
|
244
|
+
});
|
|
245
|
+
raw.once('end', () => {
|
|
246
|
+
if (decided) return;
|
|
247
|
+
decided = true;
|
|
248
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: true }));
|
|
249
|
+
res.end();
|
|
250
|
+
resolve();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Pipe an upstream fetch Response to the client, unbuffered (SSE-safe).
|
|
256
|
+
*
|
|
257
|
+
* `onReceipt` is called with the gateway's x402 block when it arrives. On a
|
|
258
|
+
* STREAMED call there is no JSON body to carry that block, so the gateway
|
|
259
|
+
* emits it as an SSE COMMENT (`: x402 {...}`) after the last frame —
|
|
260
|
+
* comments are discarded by every compliant client, so nothing downstream
|
|
261
|
+
* sees it, but without reading it here every spend and savings figure on
|
|
262
|
+
* the status line would silently read zero the moment real streaming was
|
|
263
|
+
* switched on.
|
|
264
|
+
*
|
|
265
|
+
* Sniffing NEVER delays a byte: each chunk is written to the client first
|
|
266
|
+
* and only then scanned. */
|
|
267
|
+
export function relay(res, upstream, onReceipt) {
|
|
268
|
+
if (!upstream.body) {
|
|
269
|
+
res.writeHead(upstream.status, copyHeaders(upstream, { decoded: true }));
|
|
270
|
+
res.end();
|
|
271
|
+
return Promise.resolve();
|
|
272
|
+
}
|
|
273
|
+
if (!upstream.ok) return relayError(res, upstream);
|
|
274
|
+
return relayOk(res, upstream, onReceipt);
|
|
275
|
+
}
|
package/lib/runguard.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Pin find(1) to the thread cwd. Unbounded `find /` is a full-disk walk. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const FIND_MAXDEPTH = Number(process.env.OZ_FIND_MAXDEPTH || 8);
|
|
5
|
+
|
|
6
|
+
export function guardFindCwd(command, cwd = '.') {
|
|
7
|
+
if (typeof command !== 'string' || !command) return command;
|
|
8
|
+
if (!/(?:^|[\s;|&(`])find(?:\s|$)/.test(command)) return command;
|
|
9
|
+
const root = path.resolve(cwd || '.');
|
|
10
|
+
return command.replace(/(^|[\s;|&(`])((?:sudo\s+)?)find(\s+)([^;&|`\n]*)/g, (m, pre, sudo, sp, args) => {
|
|
11
|
+
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g) || [];
|
|
12
|
+
const paths = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < tokens.length && !String(tokens[i]).startsWith('-')) {
|
|
15
|
+
paths.push(tokens[i]);
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
const rest = tokens.slice(i);
|
|
19
|
+
const hasMax = rest.some((t) => t === '-maxdepth');
|
|
20
|
+
const pin = (raw) => {
|
|
21
|
+
const unquoted = String(raw).replace(/^['"]|['"]$/g, '');
|
|
22
|
+
if (!unquoted || unquoted === '.' || unquoted === './') return '.';
|
|
23
|
+
const abs = path.resolve(root, unquoted);
|
|
24
|
+
const inside = abs === root || abs.startsWith(root + path.sep);
|
|
25
|
+
return inside ? raw : '.';
|
|
26
|
+
};
|
|
27
|
+
const newPaths = (paths.length ? paths.map(pin) : ['.']);
|
|
28
|
+
const depth = hasMax ? [] : ['-maxdepth', String(FIND_MAXDEPTH)];
|
|
29
|
+
return `${pre}${sudo}find${sp}${[...newPaths, ...depth, ...rest].join(' ')}`;
|
|
30
|
+
});
|
|
31
|
+
}
|
package/lib/spill.js
CHANGED
|
@@ -1916,7 +1916,15 @@ function emptySpillDecision(msgs, reason) {
|
|
|
1916
1916
|
export function decideChatSpill(body, opts = {}) {
|
|
1917
1917
|
const msgs = Array.isArray(body?.messages) ? body.messages : null;
|
|
1918
1918
|
if (!msgs?.length) return emptySpillDecision(msgs, 'no-messages');
|
|
1919
|
-
|
|
1919
|
+
let minChars = opts.minPrefixChars ?? SPILL_MIN_PREFIX_CHARS;
|
|
1920
|
+
const dx = Number(opts.dollarX);
|
|
1921
|
+
const model = String(body?.model || '').trim().toLowerCase();
|
|
1922
|
+
const auto = model === 'openzoo/auto' || model === 'openzoo-auto' || model === 'auto';
|
|
1923
|
+
// Thin HUD / Auto: bind way earlier than 16k. 2.1x session is cheap-model
|
|
1924
|
+
// vs cheap-model because the prefix never cleared the gate.
|
|
1925
|
+
if (auto || (Number.isFinite(dx) && dx > 0 && dx < 5)) {
|
|
1926
|
+
minChars = Math.min(minChars, 2048);
|
|
1927
|
+
}
|
|
1920
1928
|
|
|
1921
1929
|
if (isOneShotCorpusAsk(msgs, minChars)) {
|
|
1922
1930
|
const last = msgs[msgs.length - 1];
|
package/lib/think.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model reasoning arrives on several wires:
|
|
3
|
+
* - leaked `<think>…</think>` / `<thinking>…` in the visible reply
|
|
4
|
+
* - `reasoning_content` / `reasoning` / `thinking` / `thought` on the delta
|
|
5
|
+
* - provider objects with a plaintext summary next to encrypted blobs
|
|
6
|
+
*
|
|
7
|
+
* Encrypted reasoning is not a thought we can show — never dump ciphertext
|
|
8
|
+
* into the canvas. Empty plaintext means no thinking chip.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const THINK_BLOCK = /<think(?:ing)?\b[^>]*>[\s\S]*?<\/think(?:ing)?>/gi;
|
|
12
|
+
const THINK_OPEN = /<think(?:ing)?\b[^>]*>[\s\S]*$/i;
|
|
13
|
+
const THINK_CLOSE = /<\/think(?:ing)?>/gi;
|
|
14
|
+
|
|
15
|
+
const REASON_KEYS = [
|
|
16
|
+
'reasoning_content',
|
|
17
|
+
'reasoning',
|
|
18
|
+
'reasoning_details',
|
|
19
|
+
'thinking',
|
|
20
|
+
'thought',
|
|
21
|
+
'thoughts',
|
|
22
|
+
'reasoning_text',
|
|
23
|
+
'summary',
|
|
24
|
+
'text',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function looksEncryptedReasoning(s) {
|
|
28
|
+
const t = String(s || '').trim();
|
|
29
|
+
if (t.length < 48) return false;
|
|
30
|
+
if (/\s/.test(t)) return false;
|
|
31
|
+
return /^[A-Za-z0-9+/_=-]+$/.test(t);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function keyLooksEncrypted(key) {
|
|
35
|
+
return /encrypt/i.test(String(key || ''));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pull displayable reasoning out of a provider field. Strings that look like
|
|
40
|
+
* ciphertext, and objects typed/keyed as encrypted, become ''.
|
|
41
|
+
*/
|
|
42
|
+
export function reasoningPlaintext(value, depth = 0) {
|
|
43
|
+
if (value == null || depth > 6) return '';
|
|
44
|
+
if (typeof value === 'string') return looksEncryptedReasoning(value) ? '' : value;
|
|
45
|
+
if (typeof value !== 'object') return '';
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.map((x) => reasoningPlaintext(x, depth + 1)).filter(Boolean).join('');
|
|
48
|
+
}
|
|
49
|
+
const type = String(value.type || value.kind || '');
|
|
50
|
+
if (/encrypt/i.test(type)) return '';
|
|
51
|
+
const parts = [];
|
|
52
|
+
for (const key of REASON_KEYS) {
|
|
53
|
+
if (!(key in value) || keyLooksEncrypted(key)) continue;
|
|
54
|
+
const inner = reasoningPlaintext(value[key], depth + 1);
|
|
55
|
+
if (inner) parts.push(inner);
|
|
56
|
+
}
|
|
57
|
+
return parts.join('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when a delta/message carried a reasoning field, including encrypted. */
|
|
61
|
+
export function reasoningPresent(delta, message) {
|
|
62
|
+
for (const src of [delta, message]) {
|
|
63
|
+
if (!src || typeof src !== 'object') continue;
|
|
64
|
+
if (src.reasoning_content != null || src.reasoning != null
|
|
65
|
+
|| src.reasoning_details != null || src.thinking != null
|
|
66
|
+
|| src.thought != null || src.thoughts != null
|
|
67
|
+
|| src.reasoning_text != null || src.encrypted_content != null
|
|
68
|
+
|| src.reasoning_encrypted_content != null) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reasoning on a chat message or stream delta — never `message.content`. */
|
|
74
|
+
export function messageReasoning(msg, delta) {
|
|
75
|
+
const blobs = [];
|
|
76
|
+
for (const src of [delta, msg]) {
|
|
77
|
+
if (!src || typeof src !== 'object') continue;
|
|
78
|
+
blobs.push(reasoningPlaintext(src.reasoning_content));
|
|
79
|
+
blobs.push(reasoningPlaintext(src.reasoning));
|
|
80
|
+
blobs.push(reasoningPlaintext(src.reasoning_details));
|
|
81
|
+
blobs.push(reasoningPlaintext(src.thinking));
|
|
82
|
+
blobs.push(reasoningPlaintext(src.thought));
|
|
83
|
+
blobs.push(reasoningPlaintext(src.thoughts));
|
|
84
|
+
blobs.push(reasoningPlaintext(src.reasoning_text));
|
|
85
|
+
}
|
|
86
|
+
return blobs.filter((s) => String(s || '').trim()).join('');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function splitThink(text) {
|
|
90
|
+
const thoughts = [];
|
|
91
|
+
let s = String(text ?? '');
|
|
92
|
+
s = s.replace(THINK_BLOCK, (m) => {
|
|
93
|
+
const a = m.indexOf('>');
|
|
94
|
+
const b = m.toLowerCase().lastIndexOf('</think');
|
|
95
|
+
if (a >= 0 && b > a) thoughts.push(m.slice(a + 1, b));
|
|
96
|
+
return '';
|
|
97
|
+
});
|
|
98
|
+
s = s.replace(THINK_OPEN, (m) => {
|
|
99
|
+
const a = m.indexOf('>');
|
|
100
|
+
thoughts.push(a >= 0 ? m.slice(a + 1) : '');
|
|
101
|
+
return '';
|
|
102
|
+
});
|
|
103
|
+
s = s.replace(THINK_CLOSE, '');
|
|
104
|
+
const visible = s.replace(/^\n+|\n+$/g, '').trim();
|
|
105
|
+
const thinking = thoughts.join('\n').replace(/^\n+|\n+$/g, '').trim();
|
|
106
|
+
return { visible, thinking };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function stripThinkTags(text) {
|
|
110
|
+
return splitThink(text).visible;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function wrapThink(thinking, visible) {
|
|
114
|
+
const t = String(thinking || '').trim();
|
|
115
|
+
const v = String(visible || '');
|
|
116
|
+
if (!t) return v;
|
|
117
|
+
return `<think>${t}</think>\n${v}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Persist thinking on a history row; never leave tags in the visible text. */
|
|
121
|
+
export function takeThink(raw, extraThink) {
|
|
122
|
+
const parts = splitThink(raw);
|
|
123
|
+
const extra = String(extraThink || '').trim();
|
|
124
|
+
const thinking = [extra, parts.thinking].filter(Boolean).join('\n').trim();
|
|
125
|
+
return { text: parts.visible, thinking: thinking || undefined };
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.49.
|
|
4
|
-
"description": "Local x402-paying proxy + MCP server for openzoo.fun
|
|
3
|
+
"version": "0.49.9",
|
|
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",
|
|
7
7
|
"bin": {
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
12
|
"lib",
|
|
13
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
"vendor/modelroute"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=18"
|
|
17
18
|
},
|
|
18
19
|
"scripts": {
|
|
19
|
-
"test": "node --test test/*.test.js && node scripts/assert-grokui-pin.mjs"
|
|
20
|
+
"test": "node --test test/*.test.js && node scripts/assert-grokui-pin.mjs && node scripts/assert-app-html-script.mjs && node scripts/assert-overlaid-openzoo.mjs"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "^1.12.0",
|