openzoo 0.49.8 → 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/lib/proxy.js CHANGED
@@ -20,7 +20,11 @@ import {
20
20
  corpusRecall,
21
21
  decideChatSpill, isOneShotCorpusAsk,
22
22
  } from './spill.js';
23
- import { rewritablePath, modelsListForRequest, ALIAS_IDS, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS } from './models.js';
23
+ import { rewritablePath, modelsListForRequest, isHarnessAliasId, rewriteChatModel, zooModelIds, CLASSIFY_MAX_TOKENS, raiseReasoningMaxTokens, isAutoModel } from './models.js';
24
+ import {
25
+ route as routeTask, routeChatBody, fallbackChain, isRetryableStatus,
26
+ outcomeFromResponse, recordRouteOutcome, autoModelListEntry,
27
+ } from './modelroute.js';
24
28
  import { forgetContext } from './contexts.js';
25
29
  import { injectBrief } from './brief.js';
26
30
  import { withNamespace } from './namespace.js';
@@ -33,6 +37,7 @@ import { priceHoldings } from './livestatus.js';
33
37
  import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettle.js';
34
38
  import { rewriteWrapClientError } from './wrap.js';
35
39
  import { fetchHeaders } from './fetch.js';
40
+ import { relay } from './relay.js';
36
41
 
37
42
  const HOP_BY_HOP = new Set([
38
43
  'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
@@ -82,46 +87,6 @@ async function readBody(req) {
82
87
  return Buffer.concat(chunks);
83
88
  }
84
89
 
85
- /** Pipe an upstream fetch Response to the client, unbuffered (SSE-safe).
86
- *
87
- * `onReceipt` is called with the gateway's x402 block when it arrives. On a
88
- * STREAMED call there is no JSON body to carry that block, so the gateway
89
- * emits it as an SSE COMMENT (`: x402 {...}`) after the last frame — comments
90
- * are discarded by every compliant client, so nothing downstream sees it, but
91
- * without reading it here every spend and savings figure on the status line
92
- * would silently read zero the moment real streaming was switched on.
93
- *
94
- * Sniffing NEVER delays a byte: each chunk is written to the client first and
95
- * only then scanned. */
96
- function relay(res, upstream, onReceipt) {
97
- const headers = {};
98
- upstream.headers.forEach((v, k) => {
99
- if (!['transfer-encoding', 'connection', 'content-encoding', 'content-length'].includes(k)) headers[k] = v;
100
- });
101
- res.writeHead(upstream.status, headers);
102
- if (!upstream.body) { res.end(); return Promise.resolve(); }
103
- const sse = (upstream.headers.get('content-type') || '').includes('text/event-stream');
104
- return new Promise((resolve) => {
105
- const body = Readable.fromWeb(upstream.body);
106
- body.on('error', () => res.destroy());
107
- res.on('close', () => body.destroy());
108
- body.on('end', resolve);
109
- if (!sse || typeof onReceipt !== 'function') { body.pipe(res); return; }
110
- let pending = '';
111
- body.on('data', (c) => {
112
- res.write(c);
113
- pending += c.toString('utf8');
114
- const lines = pending.split('\n');
115
- pending = lines.pop() ?? ''; // a comment can straddle two chunks
116
- for (const line of lines) {
117
- if (!line.startsWith(': x402 ')) continue;
118
- try { onReceipt(JSON.parse(line.slice(7))); } catch { /* not our frame */ }
119
- }
120
- });
121
- body.on('end', () => res.end());
122
- });
123
- }
124
-
125
90
  function jsonErr(res, status, message, extraFields = {}) {
126
91
  res.writeHead(status, { 'content-type': 'application/json' });
127
92
  res.end(JSON.stringify({ error: { message }, ...extraFields }));
@@ -814,6 +779,9 @@ async function maybeCacheCorpus(req, bodyBuf, log, stats, extra = {}) {
814
779
  * default, so localhost behaviour is unchanged.
815
780
  */
816
781
  export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
782
+ // grokui's packed sidecar spawn sets OPENZOO_SILENT=1 so 400s land in
783
+ // ~/.openzoo/proxy.log instead of Electron's inherited /dev/null.
784
+ if (process.env.OPENZOO_SILENT === '1') silent = true;
817
785
  const client = new PayClient();
818
786
  const log = silent ? () => {} : (...a) => console.log(...a);
819
787
  // ALWAYS-ON. `silent: true` is used by the editor path to keep startup tidy,
@@ -1197,6 +1165,35 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1197
1165
  return;
1198
1166
  }
1199
1167
 
1168
+ // Local decision � no upstream, no payment. Same keys as Python route().
1169
+ {
1170
+ const routePath = (req.url || '').split('?')[0];
1171
+ if (req.method === 'POST' && (routePath === '/route' || routePath === '/v1/route')) {
1172
+ let payload;
1173
+ try { payload = JSON.parse(bodyBuf.toString('utf8') || '{}'); } catch {
1174
+ jsonErr(res, 400, 'invalid /route body');
1175
+ return;
1176
+ }
1177
+ try {
1178
+ const r = routeTask(payload.text ?? '', {
1179
+ allow_free: payload.allow_free ?? false,
1180
+ bindable: payload.bindable ?? true,
1181
+ context: payload.context,
1182
+ input_tokens: payload.input_tokens,
1183
+ has_image: payload.has_image,
1184
+ needs_tools: payload.needs_tools,
1185
+ needs_json: payload.needs_json,
1186
+ ...(payload.constraints && typeof payload.constraints === 'object' ? payload.constraints : {}),
1187
+ });
1188
+ res.writeHead(200, { 'content-type': 'application/json' });
1189
+ res.end(JSON.stringify(r));
1190
+ } catch (err) {
1191
+ jsonErr(res, 500, `openzoo /route: ${err.message}`);
1192
+ }
1193
+ return;
1194
+ }
1195
+ }
1196
+
1200
1197
  // ANTHROPIC MESSAGES SHAPE. A harness pointed here via ANTHROPIC_BASE_URL
1201
1198
  // (Claude Code, the Anthropic SDKs) speaks POST /v1/messages, not chat
1202
1199
  // completions — this is how such a harness routes its inference through
@@ -1299,6 +1296,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1299
1296
  // carries a model field, not just chat/completions, so /completions,
1300
1297
  // /responses and future shapes all work. Never silent.
1301
1298
  let wantsStream = false;
1299
+ let autoRoute = null;
1302
1300
  if ((req.url || '').includes('/chat/completions') && req.method === 'POST') {
1303
1301
  servedRequests += 1;
1304
1302
  say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
@@ -1332,6 +1330,26 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1332
1330
  try { ids = await zooModelIds(); } catch { /* catalog miss: still skip the floor on a tiny classify */ }
1333
1331
  const policy = rewriteChatModel(parsed, ids, { bodyLen: bodyBuf.length });
1334
1332
  parsed = policy.parsed;
1333
+ if (!policy.tiny && (policy.auto || isAutoModel(parsed?.model))) {
1334
+ autoRoute = routeChatBody(parsed, {
1335
+ allow_free: false,
1336
+ bindable: true,
1337
+ // Live quoteable ids only � Auto must never emit :batch / $0 /
1338
+ // missing-price rows that 500 `bad openrouter price` on Fly.
1339
+ ...(ids.length ? { allow_ids: ids } : {}),
1340
+ });
1341
+ if (!autoRoute.model) {
1342
+ jsonErr(res, 422, autoRoute.reason || 'openzoo/auto: no feasible model', { route: autoRoute });
1343
+ return;
1344
+ }
1345
+ parsed = { ...parsed, model: autoRoute.model };
1346
+ const bump = raiseReasoningMaxTokens(parsed);
1347
+ parsed = bump.parsed;
1348
+ say(`openzoo/auto -> ${autoRoute.model} p=${autoRoute.p_success} cleared=${autoRoute.cleared_bar} ${autoRoute.task_class}${autoRoute.bind_first ? ' bind_first' : ''}`);
1349
+ if (!autoRoute.cleared_bar) {
1350
+ say(`openzoo/auto cleared_bar=false � strongest fallback, not a normal pick`);
1351
+ }
1352
+ }
1335
1353
  if (policy.tiny) {
1336
1354
  // `openzoo claude` starts us silent � `log` is a no-op then. say()
1337
1355
  // is the proxy.log channel and never the Claude Code TTY.
@@ -1381,7 +1399,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1381
1399
  log(`reasoning model ${parsed.model}: max_tokens ${policy.raisedFrom} -> ${policy.raisedTo} (thinking shares the budget; OPENZOO_REASONING_MAX_TOKENS_X=1 disables)`);
1382
1400
  }
1383
1401
  }
1384
- if (policy.tiny || policy.raised || (policy.to && policy.to !== policy.from)) {
1402
+ if (policy.tiny || policy.raised || autoRoute || (policy.to && policy.to !== policy.from)) {
1385
1403
  bodyBuf = Buffer.from(JSON.stringify(parsed));
1386
1404
  }
1387
1405
  // Tell the agent what it is actually connected to — in band, where it
@@ -1508,9 +1526,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1508
1526
  const response = await fetchHeaders(url, init);
1509
1527
  if (response.ok) {
1510
1528
  const payload = await response.json();
1511
- // Full zoo catalog � never collapse to a single opus-5 or a claude-*
1512
- // allowlist. Claude Code (ANTHROPIC_BASE_URL + gateway discovery) reads
1513
- // data[].id / display_name; OpenAI clients keep object:"list".
1529
+ // Quoteable catalog only. Raw OpenRouter dump includes :batch,
1530
+ // $0 / missing prices, and we used to mint openzoo-* twins of each
1531
+ // � Claude Code /model then showed 63 clones and Auto 500'd.
1514
1532
  res.writeHead(200, { 'content-type': 'application/json' });
1515
1533
  res.end(JSON.stringify(modelsListForRequest(payload, req.headers)));
1516
1534
  return;
@@ -1522,7 +1540,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1522
1540
  return;
1523
1541
  }
1524
1542
  const probe = req.method === 'GET' && /^\/v1\/models\/(.+)$/.exec(path);
1525
- if (probe && ALIAS_IDS.includes(decodeURIComponent(probe[1]))) {
1543
+ if (probe && isAutoModel(decodeURIComponent(probe[1]))) {
1544
+ res.writeHead(200, { 'content-type': 'application/json' });
1545
+ res.end(JSON.stringify(autoModelListEntry()));
1546
+ return;
1547
+ }
1548
+ if (probe && isHarnessAliasId(decodeURIComponent(probe[1]))) {
1526
1549
  res.writeHead(200, { 'content-type': 'application/json' });
1527
1550
  res.end(JSON.stringify({ id: decodeURIComponent(probe[1]), object: 'model', owned_by: 'openzoo-alias' }));
1528
1551
  return;
@@ -1592,6 +1615,32 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1592
1615
  } else {
1593
1616
  result = await client.fetch(url, init);
1594
1617
  }
1618
+ // Walk the auto shortlist on 429/5xx � cheapest-first among models that
1619
+ // cleared the bar. Finite: one pass over fallbackChain(), never a loop.
1620
+ if (autoRoute?.cleared_bar && isRetryableStatus(result.response.status)) {
1621
+ const chain = fallbackChain(autoRoute);
1622
+ for (const next of chain) {
1623
+ const failed = result.response.status;
1624
+ try { await result.response.arrayBuffer(); } catch { /* drain */ }
1625
+ say(`openzoo/auto fallback HTTP ${failed} -> ${next}`);
1626
+ const rewriteModel = (buf) => {
1627
+ try {
1628
+ const b = JSON.parse(buf.toString('utf8'));
1629
+ b.model = next;
1630
+ return Buffer.from(JSON.stringify(b));
1631
+ } catch { return buf; }
1632
+ };
1633
+ bodyBuf = rewriteModel(bodyBuf);
1634
+ init.body = bodyBuf;
1635
+ if (cached) {
1636
+ cached = { ...cached, body: rewriteModel(cached.body) };
1637
+ result = await send(cached.body, cached.contextId, cached.topK, corpusCharsForSend(boundChars, cached.contextId, cached.corpus?.length));
1638
+ } else {
1639
+ result = await client.fetch(url, { ...init, body: bodyBuf });
1640
+ }
1641
+ if (!isRetryableStatus(result.response.status)) break;
1642
+ }
1643
+ }
1595
1644
  const { response, paid, receipt } = result;
1596
1645
  if (paid && receipt) {
1597
1646
  if (receipt.ok && typeof receipt.billedUsd === 'number') {
@@ -1636,6 +1685,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1636
1685
  // that contract ourselves. An upstream that someday truly streams (SSE
1637
1686
  // content-type) passes straight through the relay below, untouched.
1638
1687
  const upCt = response.headers.get('content-type') || '';
1688
+ const noteAutoOutcome = (status, data, streamed = false) => {
1689
+ if (!autoRoute) return;
1690
+ let ok = outcomeFromResponse(status, data);
1691
+ if (ok == null) return;
1692
+ if (streamed && status >= 200 && status < 300) ok = true;
1693
+ recordRouteOutcome(autoRoute, ok);
1694
+ };
1639
1695
  if (isChat && response.ok && upCt.includes('application/json')) {
1640
1696
  let data = null;
1641
1697
  try { data = await response.clone().json(); } catch { /* not JSON after all */ }
@@ -1694,6 +1750,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1694
1750
  say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
1695
1751
  }
1696
1752
  if (data?.object === 'chat.completion') {
1753
+ noteAutoOutcome(response.status, data);
1697
1754
  if (rKey) replayPut(rKey, data, response.headers.get('x-payment-response'));
1698
1755
  // Anthropic-shaped caller gets an Anthropic-shaped answer, streamed
1699
1756
  // or not, so Claude Code and the SDKs parse it natively.
@@ -1792,6 +1849,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1792
1849
  return;
1793
1850
  }
1794
1851
 
1852
+ if (autoRoute) {
1853
+ noteAutoOutcome(response.status, null, (upCt.includes('text/event-stream') && response.ok));
1854
+ }
1795
1855
  await relay(res, response, meterStreamed);
1796
1856
  } catch (err) {
1797
1857
  if (err instanceof QuoteTooHighError) {
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
+ }
@@ -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
- const minChars = opts.minPrefixChars ?? SPILL_MIN_PREFIX_CHARS;
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];