openzoo 0.49.18 → 0.50.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/lib/proxy.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import fs from 'node:fs';
5
4
  import http from 'node:http';
6
5
  import crypto from 'node:crypto';
7
6
  import { Readable } from 'node:stream';
@@ -11,32 +10,27 @@ import {
11
10
  import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
12
11
  import { tokenBalance } from './x402.js';
13
12
  import { evmTokenBalance } from './evm.js';
14
- import { bindCorpus, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
15
- import {
16
- loadBoundChars, noteCorpusLedger, filesForCorpus, readFilesForCorpus, boundAbsFromKeys,
17
- createSpillStats, corpusCharsForSend, msgText, hudDollarX,
18
- spillPricedLine,
19
- planConversationBind, rememberSpillMemo, SPILL_CONTENT_ANCHOR_CHARS,
20
- corpusRecall,
21
- decideChatSpill, isOneShotCorpusAsk,
22
- } from './spill.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';
28
- import { forgetContext } from './contexts.js';
29
- import { injectBrief } from './brief.js';
13
+ import { modelsListForRequest, isHarnessAliasId } from './models.js';
30
14
  import { withNamespace } from './namespace.js';
31
- import { anthropicToOpenAI, openAIToAnthropic, streamOpenAIToAnthropic, writeAnthropicSse } from './anthropic.js';
32
- import { responsesToChat, chatToResponses, writeResponsesSse } from './responses.js';
33
- import { streamOpenAIToResponses } from './responses-stream.js';
34
15
  import { loadSessionSpend, saveSessionSpend } from './session.js';
35
16
  import { creditBalance, quotedPrices } from './info.js';
36
17
  import { priceHoldings } from './livestatus.js';
37
18
  import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettle.js';
38
19
  import { fetchHeaders } from './fetch.js';
39
20
 
21
+ // THE SHIM IS A FACILITATOR, NOT A MIDDLEBOX.
22
+ //
23
+ // Everything that used to rewrite the request on the way through — model-id
24
+ // rewriting, the tiny-classify pin, the reasoning max_tokens floor, transcript
25
+ // spilling / corpus binding, brief injection, system-message hoisting,
26
+ // Anthropic /v1/messages and Responses translation, AUTO routing — now lives
27
+ // on the backend. This proxy does exactly three jobs:
28
+ // 1. x402: answer the gateway's 402 with a signed payment (PayClient),
29
+ // 2. tunnel: publish + gate the public URL,
30
+ // 3. meter: read receipts so the operator can see spend vs direct.
31
+ // The request body is forwarded byte-for-byte. If a body looks wrong upstream,
32
+ // the fix belongs on the backend, never here.
33
+
40
34
  const HOP_BY_HOP = new Set([
41
35
  'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
42
36
  'proxy-authorization', 'proxy-connection', 'te', 'trailer', 'content-length',
@@ -54,14 +48,12 @@ const HOP_BY_HOP = new Set([
54
48
  * Be forgiving about the path, the same way we are about model ids.
55
49
  * Harnesses are configured with a base_url that ALREADY ends in /v1, so an
56
50
  * agent building "{base}/v1/hrr/bind" sends /v1/v1/hrr/bind and gets a 404 it
57
- * cannot diagnose (observed: agent concluded the bind endpoint "is not
58
- * functioning as advertised" and fell back to stuffing the corpus inline).
59
- * Collapse repeated /v1 and add a missing one.
51
+ * cannot diagnose. Collapse repeated /v1 and add a missing one.
60
52
  */
61
53
  function normalizePath(url) {
62
54
  const [path, query] = (url || '/').split(/(?=\?)/);
63
55
  let p = path.replace(/^(?:\/v1)+(?=\/v1\/)/, ''); // /v1/v1/x -> /v1/x
64
- if (!/^\/v1(\/|$)/.test(p) && /^\/(hrr|chat|models|completions|embeddings|usage|responses)/.test(p)) p = `/v1${p}`;
56
+ if (!/^\/v1(\/|$)/.test(p) && /^\/(hrr|chat|models|completions|embeddings|usage|responses|messages)/.test(p)) p = `/v1${p}`;
65
57
  return p === path ? url : `${p}${query || ''}`;
66
58
  }
67
59
 
@@ -70,12 +62,8 @@ function upstreamHeaders(req) {
70
62
  for (const [k, v] of Object.entries(req.headers)) {
71
63
  if (!HOP_BY_HOP.has(k.toLowerCase())) out[k] = v;
72
64
  }
73
- // EVERY forwarded request carries this wallet's context namespace, not just
74
- // the ones PayClient builds itself. Without it a bind sent THROUGH the proxy
75
- // (an agent posting to /v1/hrr/bind) landed in the shared tenant while the
76
- // chat that referenced it looked in the wallet's tenant — the context was
77
- // unreachable and every spill bind came back 400, silently forwarding the
78
- // whole body at full price.
65
+ // EVERY forwarded request carries this wallet's context namespace — binds
66
+ // and the chats that reference them must land in the same tenant.
79
67
  return withNamespace(out);
80
68
  }
81
69
 
@@ -130,66 +118,6 @@ function jsonErr(res, status, message, extraFields = {}) {
130
118
  res.end(JSON.stringify({ error: { message }, ...extraFields }));
131
119
  }
132
120
 
133
- const mb = (n) => (n / 1048576).toFixed(1);
134
-
135
- // anchor -> { corpus, contextId, hash } for append-only transcript spills
136
- const spillMemo = new Map();
137
- // path:mtime of every file already bound — a file is bound once per version,
138
- // never re-uploaded because the agent read it again.
139
- const boundFiles = new Set();
140
- // context_id -> total chars BOUND (turns + files appended). The counterfactual
141
- // basis must reflect what the corpus actually holds, not just this turn's slice.
142
- const boundChars = new Map();
143
- // sessionKey -> { contextId, chars } so a sidecar restart can keep appending
144
- // to the same context and still send the accumulated x-hrr-corpus-chars.
145
- const sessionLedger = new Map();
146
- loadBoundChars(boundChars, { sessions: sessionLedger, boundFiles });
147
-
148
- // THIS MAP SURVIVES A RESTART, OR THE COUNTERFACTUAL DOES NOT.
149
- //
150
- // boundChars is the only record of how large a bound corpus has GROWN. It feeds
151
- // `x-hrr-corpus-chars`, which is what lets the gateway price the counterfactual
152
- // against the whole corpus instead of against the turn in front of it. Held
153
- // purely in memory, every restart silently reset that basis to the live body and
154
- // savings fell to exactly 1.00x until a session re-accumulated — with nothing in
155
- // the log to say why, because the corpus in the DAEMON was still there. Only our
156
- // accounting of it was gone.
157
- //
158
- // MEASURED minutes after a restart: `basis 16112 tok vs sent 16112 -> 4238 ·
159
- // billed 1.14063 direct 1.14063`. The body spilled 3.8x and the bill did not
160
- // move, because basis fell back to `cached.corpus?.length`.
161
- //
162
- // Advisory, never load-bearing: a corrupt or missing file just means we start
163
- // counting again, which is exactly today's behaviour.
164
- const BOUND_CHARS_FILE = path.join(os.homedir(), '.openzoo', 'bound-chars.json');
165
- const BOUND_CHARS_MAX = 500; // newest contexts only; this is a ledger, not a log
166
-
167
- let boundCharsRestored = 0;
168
- try {
169
- const saved = JSON.parse(fs.readFileSync(BOUND_CHARS_FILE, 'utf8'));
170
- for (const [ctx, chars] of Object.entries(saved)) {
171
- if (typeof chars === 'number' && chars > 0) boundChars.set(ctx, chars);
172
- }
173
- boundCharsRestored = boundChars.size;
174
- } catch { /* absent or unreadable — start empty, same as before */ }
175
-
176
- let boundCharsTimer = null;
177
- /** Debounced: appends land in bursts, and the basis only has to survive a
178
- * restart, not every keystroke. Never rejects — persistence is a nicety. */
179
- const persistBoundChars = () => {
180
- if (boundCharsTimer) return;
181
- boundCharsTimer = setTimeout(() => {
182
- boundCharsTimer = null;
183
- try {
184
- // Map preserves insertion order, so the tail is the newest.
185
- const entries = [...boundChars.entries()].slice(-BOUND_CHARS_MAX);
186
- mkdirSync(path.dirname(BOUND_CHARS_FILE), { recursive: true });
187
- fs.writeFileSync(BOUND_CHARS_FILE, JSON.stringify(Object.fromEntries(entries)));
188
- } catch { /* advisory */ }
189
- }, 2000);
190
- boundCharsTimer.unref?.(); // must never hold the process open
191
- };
192
-
193
121
  /**
194
122
  * Every fundable balance across all three chains, for the startup line and
195
123
  * the live refresh. Each read is independent and advisory — one lagging RPC
@@ -227,22 +155,19 @@ function balanceLine(snap) {
227
155
  }
228
156
 
229
157
  /**
230
- * The zoo answers chat completions as ONE JSON object (it settles payment
231
- * before serving — there is nothing to stream until generation is done).
232
- * Harnesses that sent `stream: true` expect SSE and treat a JSON body as a
233
- * dead connection: Cursor shows "Reconnecting…", RETRIES, and every retry is
234
- * a fresh payment. So the proxy honours the contract itself — the finished
235
- * completion is re-emitted as spec-shaped chat.completion.chunk events.
158
+ * When the zoo answers a chat completion as ONE JSON object but the harness
159
+ * sent `stream: true`, honour the streaming contract ourselves — a JSON body
160
+ * on an expected SSE socket reads as a dead connection (Cursor shows
161
+ * "Reconnecting…", RETRIES, and every retry is a fresh payment). An upstream
162
+ * that truly streams passes straight through relay(), untouched.
236
163
  */
237
164
  function serveAsSse(res, data, upstream) {
238
165
  const headers = {
239
166
  'content-type': 'text/event-stream; charset=utf-8',
240
167
  'cache-control': 'no-cache',
241
168
  // Tell any proxy in front of us (cloudflare quick tunnel, nginx) NOT to
242
- // buffer the stream. Without this the tunnel accumulates the whole SSE
243
- // body and releases it at once, which reorders/merges the tool_call frames
244
- // an agent parses incrementally — observed as "provider-side tool-call
245
- // protocol error" over the tunnel while localhost (no proxy) is fine.
169
+ // buffer the stream — buffering reorders/merges tool_call frames an agent
170
+ // parses incrementally.
246
171
  'x-accel-buffering': 'no',
247
172
  connection: 'keep-alive',
248
173
  };
@@ -259,9 +184,8 @@ function serveAsSse(res, data, upstream) {
259
184
  ev({ ...base, choices: [{ index: c.index ?? 0, delta: { content: c.message.content }, finish_reason: null }] });
260
185
  }
261
186
  // Agent mode lives or dies here: a finish_reason of "tool_calls" with the
262
- // calls themselves dropped strands the harness mid-turn (observed: Cursor
263
- // agent hangs). Streaming spec: tool_calls ride the delta with an index,
264
- // arguments as a string chunk — one full chunk per call is valid SSE.
187
+ // calls themselves dropped strands the harness mid-turn. Streaming spec:
188
+ // tool_calls ride the delta with an index, arguments as a string chunk.
265
189
  if (Array.isArray(c.message?.tool_calls) && c.message.tool_calls.length) {
266
190
  ev({
267
191
  ...base,
@@ -290,24 +214,15 @@ function serveAsSse(res, data, upstream) {
290
214
  * body within seconds, and each retry used to be a fresh payment (observed:
291
215
  * six identical $0.06 settles for one Cursor message). An identical POST body
292
216
  * arriving within the window is served the cached completion, not re-paid.
293
- * Window is deliberately short: a genuinely new turn always differs (harnesses
294
- * resend the whole conversation), so only true retries can hit.
217
+ * Window is deliberately short: a genuinely new turn always differs.
295
218
  */
296
219
  const REPLAY_TTL_MS = 30_000;
297
- const replayCache = new Map(); // sha256(body) -> { at, data, settle }
220
+ const replayCache = new Map(); // sha256(body+headers) -> { at, data, settle }
298
221
  /**
299
- * The key MUST include the routing headers, not just the body.
300
- *
301
- * Keying on the body alone was a correctness bug, not merely a caching one:
302
- * N shards asking the SAME question of N DIFFERENT bound corpora send
303
- * byte-identical bodies and differ only in X-HRR-Context. They collided on one
304
- * key, so shards 2..N were served shard 1's answer — REPRODUCED in the field:
305
- * 10 shards, 7 byte-identical replies across corpora known to differ, and the
306
- * batch finished in 12s where a single uncached call took ~7s.
307
- *
308
- * Wrong answers attributed to the wrong corpus is a far worse failure than the
309
- * double-billing this cache exists to prevent, so every header that can change
310
- * the ANSWER joins the key.
222
+ * The key MUST include the routing headers, not just the body: N shards asking
223
+ * the SAME question of N DIFFERENT bound corpora send byte-identical bodies and
224
+ * differ only in X-HRR-Context. Wrong answers attributed to the wrong corpus is
225
+ * a far worse failure than the double-billing this cache exists to prevent.
311
226
  */
312
227
  const REPLAY_KEY_HEADERS = ['x-hrr-context', 'x-hrr-top-k', 'x-hrr-gate', 'x-openzoo-namespace'];
313
228
 
@@ -333,483 +248,6 @@ function replayPut(key, data, settle) {
333
248
  }
334
249
  }
335
250
 
336
- /**
337
- * "The body never ships twice" at the proxy. A chat body whose LAST message
338
- * carries a huge pasted corpus gets split at its last blank line — corpus vs
339
- * ask — so the corpus can be bound ONCE on the zoo and every later call ships
340
- * only the ask plus X-HRR-Context. The split point is deterministic, which is
341
- * what makes the sha256 manifest hit on run 2 even when the question changed.
342
- *
343
- * Conservative on purpose: only a single big STRING content on the final
344
- * message, only when a blank-line boundary exists, and any failure falls back
345
- * to sending the original body untouched — caching must never break a call.
346
- * Returns null (send as-is) or { body, contextId, hash, corpus, reused, savedBytes }.
347
- */
348
- /**
349
- * Spill the OLD prefix of a long TRANSCRIPT into leCore.
350
- *
351
- * WHY THIS EXISTS. The only spill was the corpus+question shape below, which
352
- * requires the last message to be one big string ending in `\n\n<question>` —
353
- * true for zoo_ask, never true for an agent. `npx openzoo claude` therefore
354
- * spilled NOTHING, hit Claude Code's own context ceiling, and auto-compacted,
355
- * on a product whose pitch is that it does not have to. Compaction was honest
356
- * given nothing was offloaded; this is what makes the claim true.
357
- *
358
- * Runs on the OpenAI shape ON PURPOSE. /v1/messages is translated by
359
- * anthropicToOpenAI and rewritten to /v1/chat/completions BEFORE this is
360
- * reached, so operating here covers Claude Code, Cursor and the raw API with
361
- * one implementation instead of three that can drift.
362
- *
363
- * THE CUT POINT IS NOT NEGOTIABLE. An assistant `tool_calls` must be answered
364
- * by role:"tool" messages or the upstream 400s, so the transcript may only be
365
- * severed at a plain `user` message — everything before one is self-contained.
366
- * A system message is never spilled: it is the operating contract, not history.
367
- */
368
- async function spillTranscript(body, log, req, stats, extra = {}) {
369
- const msgs = Array.isArray(body?.messages) ? body.messages : null;
370
- if (!msgs?.length) return null;
371
-
372
- // PATHS FIRST, BYTES LATER. The cut/length gates below used to run before
373
- // filesForCorpus, so a short agent turn that Read a file never bound it �
374
- // and when the extract itself returned empty, nothing logged. Collect
375
- // unconditionally, but only paths + cheap stat/mtime: a 2MB Read must not
376
- // stall this turn. readdir + readFile + bindCorpus run after we return,
377
- // via setImmediate, so the chat request goes first.
378
- //
379
- // Snapshot bound paths BEFORE collect so this turn's first-read files stay
380
- // verbatim in the tail (not yet in the corpus for recall). Previously
381
- // bound files before the last ask may stub; remaining post-ask Read/Edit/
382
- // Write / Bash-file bodies stay real (drop older rounds instead).
383
- const previouslyBoundAbs = boundAbsFromKeys(boundFiles);
384
- const fileCollect = filesForCorpus(msgs, { boundFiles });
385
- const sessionId = req?.headers?.['x-claude-code-session-id']
386
- || req?.headers?.['x-session-id']
387
- || req?.headers?.['x-claude-session-id']
388
- || (typeof body?.metadata?.user_id === 'string' ? body.metadata.user_id : null);
389
- let sessionKey = sessionId ? `sid:${sessionId}` : null;
390
-
391
- const ledgerOpts = () => ({ sessionKey, sessions: sessionLedger, boundFiles });
392
- const bindFilesInBackground = (label, { appendTo: forcedAppend, asAppend = false } = {}) => {
393
- if (!fileCollect.pending.length) return;
394
- const known = (sessionKey && spillMemo.get(sessionKey))
395
- || (sessionKey && sessionLedger.get(sessionKey))
396
- || null;
397
- const appendTo = forcedAppend !== undefined ? forcedAppend : (known?.contextId || null);
398
- setImmediate(() => {
399
- let read;
400
- try {
401
- read = readFilesForCorpus(fileCollect, { boundFiles, log });
402
- } catch (e) {
403
- log(`${asAppend ? 'file append failed (corpus lags one turn)' : 'file bind failed'}: ${e.message}`);
404
- return;
405
- }
406
- if (!read.text) return;
407
- void bindCorpus(read.text, {
408
- appendTo,
409
- onStage: (stage, info) => {
410
- if (stage !== 'binding') return;
411
- if (asAppend && appendTo) {
412
- log(`appending ${info.bytes} bytes (${mb(info.bytes)}MB) of FILES to ${appendTo} (background)`);
413
- } else {
414
- log(`binding ${info.bytes} bytes (${mb(info.bytes)}MB) of FILES (${label})`);
415
- }
416
- },
417
- }).then((b) => {
418
- if (!b?.contextId) return;
419
- noteCorpusLedger(boundChars, {
420
- contextId: b.contextId,
421
- reused: Boolean(appendTo),
422
- corpusChars: 0,
423
- fileChars: read.bytes,
424
- ...ledgerOpts(),
425
- });
426
- stats?.noteFileBind(read.files, read.bytes);
427
- if (sessionKey && !spillMemo.has(sessionKey)) {
428
- spillMemo.set(sessionKey, { corpus: '', contextId: b.contextId, hash: b.hash });
429
- }
430
- }).catch((e) => log(`${asAppend ? 'file append failed (corpus lags one turn)' : 'file bind failed'}: ${e.message}`));
431
- });
432
- };
433
-
434
- // LIVE SELF-TUNER. Env knobs seed the first cut; after cut+stub the proxy
435
- // scores the HUD dollar multiple (spill direct/billed) and retunes
436
- // keep/min-turns/budget (stubMore for SEARCH, not live file bodies)
437
- // in process memory so this request recuts when the green x is under 10.
438
- // No restart. OPENZOO_ADAPT=0 freezes the env defaults. The ask always
439
- // stays; we never drop below 2 real user/assistant turns to delete it.
440
- //
441
- // 1-model AND raced grokui AUTO both land here (same POST /chat/completions
442
- // door). The old `msgs.length < 6` bail skipped fat 1-model hops � a 40k
443
- // command-output turn with 4�5 messages never bound, so spent?direct.
444
- // decideChatSpill is the shared gate: oversized ? bind prefix + system/tail
445
- // + x-hrr-context; small ? passthrough. Race fields do not change it.
446
- const knownLedger = (sessionKey && spillMemo.get(sessionKey))
447
- || (sessionKey && sessionLedger.get(sessionKey))
448
- || null;
449
- const knownChars = knownLedger?.contextId
450
- ? (boundChars.get(knownLedger.contextId) || 0)
451
- : 0;
452
- const decision = decideChatSpill(body, {
453
- corpusChars: knownChars,
454
- boundAbs: previouslyBoundAbs,
455
- recall: corpusRecall(typeof knownLedger?.corpus === 'string' ? knownLedger.corpus : ''),
456
- log,
457
- persist: true,
458
- dollarX: extra.dollarX ?? hudDollarX(stats || {}),
459
- lastSend: extra.lastSend,
460
- minPrefixChars: BIND_MIN_CHARS,
461
- });
462
- if (decision.mode === 'passthrough') {
463
- bindFilesInBackground(decision.reason === 'no-cut'
464
- ? 'no severable cut, files only'
465
- : 'conversation under spill threshold, background');
466
- return null;
467
- }
468
- if (decision.mode === 'stub-only') {
469
- bindFilesInBackground('no severable cut, files only');
470
- // Still forward stubbed/trimmed bodies so an un-severable storm is not
471
- // shipped at full size just because cutTranscript could not move.
472
- return {
473
- body: Buffer.from(JSON.stringify({ ...body, messages: decision.forwarded })),
474
- corpus: '',
475
- reused: false,
476
- savedBytes: 0,
477
- sent: decision.forwarded.length,
478
- msgs: msgs.length,
479
- };
480
- }
481
- // oneshot is handled in maybeCacheCorpus; if we reach it here, bind the
482
- // extracted corpus the same way as a transcript prefix.
483
- if (decision.mode === 'oneshot' && !decision.adapted) {
484
- decision.adapted = {
485
- cut: msgs.length - 1,
486
- firstSpillable: Math.max(0, msgs.findIndex((m) => m?.role !== 'system')),
487
- stubbed: { messages: decision.forwarded, stubbed: 0, dropped: 0 },
488
- };
489
- }
490
- const adapted = decision.adapted;
491
- const { cut, firstSpillable } = adapted;
492
- const stubbed = adapted.stubbed;
493
-
494
- const head = decision.head.length ? decision.head : msgs.slice(0, firstSpillable); // system block, always kept
495
- // EVERY FILE THE AGENT TOUCHED, AT FULL SIZE.
496
- //
497
- // The saving ratio is corpus/sent, so on a fresh session — where the corpus
498
- // IS the conversation — it starts near 1x and only climbs as you talk. That
499
- // is exactly what the live agent numbers showed: 1.0-1.3x early, 8x once a
500
- // 149k-token history existed. MEASURED the same day: prompt compressed 2.67x
501
- // but billed savings was 1.13x, because output does not compress and a small
502
- // corpus leaves nothing to compress on the other side either.
503
- //
504
- // Files are the fix. An agent reads far more bytes than it discusses, the
505
- // harness has usually TRUNCATED them on the way in, and the full text is
506
- // sitting on this machine. Binding it makes the corpus large immediately
507
- // instead of eventually, and makes the truncated read whole again.
508
- //
509
- // Read-only, bounded, deduped by path+mtime. Path collection already ran at
510
- // the top of this function (fileCollect.pending). Bytes + dir expansion
511
- // happen in bindFilesInBackground after this turn is forwarded.
512
- //
513
- // FILES RIDE THE BACKGROUND, NEVER THE CRITICAL PATH.
514
- //
515
- // The first conversation bind is also fire-and-forget: turn 1 may go out
516
- // unspilled while the bind runs, then later turns recall a tail + contextId.
517
- // Folding file bytes into a synchronous first bind used to put a 400KB
518
- // upload in front of the caller's turn (0.34-0.48s on a 613KB corpus).
519
- //
520
- // Nothing recalls a file during the turn that read it � the model already has
521
- // the tool result in its window. Files are only worth having bound for the
522
- // NEXT ask, so they append after the conversation bind completes.
523
- const turns = decision.prefix
524
- || msgs.slice(firstSpillable, cut).map(msgText).filter(Boolean).join('\n\n');
525
- const corpus = turns;
526
- if (!sessionKey) sessionKey = corpus.slice(0, SPILL_CONTENT_ANCHOR_CHARS);
527
-
528
- // Conversation prefix binds at any size. BIND_MIN_CHARS only gates the
529
- // one-shot corpus+question path in maybeCacheCorpus � a real but small
530
- // early-turn prefix used to be discarded here ("only large ones").
531
- if (!corpus) {
532
- bindFilesInBackground('empty conversation prefix, files only');
533
- return null;
534
- }
535
-
536
- // CONTINUE THE CONTEXT, BIND ONLY THE DELTA.
537
- //
538
- // A transcript grows by one message per turn, so the whole-corpus hash misses
539
- // every time and the old code re-uploaded the ENTIRE prefix on every single
540
- // turn � OBSERVED live: 0.4MB bound three turns running, a fresh context id
541
- // each time, while only a few KB was actually new. Bind cost grew with
542
- // conversation length and was re-paid per message.
543
- //
544
- // The corpus is append-only: each turn's corpus starts with the previous
545
- // one. So when it does, send just the tail and keep the same context_id.
546
- // Anchored on the FIRST 2KB, which is stable for the life of a conversation
547
- // and distinguishes concurrent ones.
548
- // KEY ON THE SESSION, NOT ON THE CONTENT.
549
- //
550
- // The anchor was the first 2KB of corpus, which works only because a
551
- // transcript's opening never changes. It is fragile in exactly the cases that
552
- // matter: two sessions that open identically (same system block, same first
553
- // instruction � the norm for an agent) collide onto ONE bound context and
554
- // interleave their histories, and any edit near the top of a transcript
555
- // silently orphans the binding and re-uploads the whole thing.
556
- //
557
- // Claude Code identifies its session, so use that when it is offered and fall
558
- // back to the content anchor when it is not. Same memo, better key.
559
- // CAPTURED FROM A LIVE claude-cli/2.1.232 REQUEST, not guessed. The first
560
- // version of this checked x-session-id / x-claude-session-id /
561
- // metadata.user_id � none of which Claude Code sends, so it silently fell
562
- // back to the content anchor on every request and the feature did nothing.
563
- // The real header list is:
564
- // anthropic-beta, anthropic-version, x-app, x-claude-code-session-id,
565
- // x-stainless-*
566
- //
567
- // COLD-BIND. Turn 1 may go unspilled (full messages, no x-hrr-context) while
568
- // the first bind runs in the background � same spirit as file-bind, so the
569
- // opening ask is not stalled. The in-flight/completed bind is memoized on
570
- // the session key; later turns recall a tail + contextId. Subsequent
571
- // appends stay fire-and-forget deltas.
572
- let bindPlan = planConversationBind({
573
- sessionKey,
574
- corpus,
575
- spillMemo,
576
- sessionLedger,
577
- });
578
- const anchor = bindPlan.key || sessionKey;
579
-
580
- if (bindPlan.action === 'cold-bind') {
581
- const ready = bindCorpus(corpus, {
582
- onStage: (stage, info) => {
583
- if (stage === 'binding') log(`binding ${mb(info.bytes)}MB of transcript to holographic memory (background)...`);
584
- },
585
- }).then((b) => {
586
- if (!b?.contextId) return b;
587
- noteCorpusLedger(boundChars, {
588
- contextId: b.contextId,
589
- reused: false,
590
- corpusChars: corpus.length,
591
- deltaChars: 0,
592
- fileChars: 0,
593
- ...ledgerOpts(),
594
- });
595
- rememberSpillMemo(spillMemo, anchor, { corpus, contextId: b.contextId, hash: b.hash });
596
- bindFilesInBackground('background', { appendTo: b.contextId, asAppend: true });
597
- return b;
598
- }).catch((e) => {
599
- log(`bind failed (turn went unspilled): ${e.message}`);
600
- const cur = spillMemo.get(anchor);
601
- if (cur?.pending) spillMemo.delete(anchor);
602
- return null;
603
- });
604
- rememberSpillMemo(spillMemo, anchor, { corpus, pending: true, ready });
605
- return null;
606
- }
607
-
608
- if (bindPlan.action === 'await-pending') {
609
- const finished = await bindPlan.ready.catch((e) => {
610
- log(`bind failed (history may lag one turn): ${e.message}`);
611
- return null;
612
- });
613
- if (!finished?.contextId) {
614
- bindFilesInBackground('first bind failed, files only');
615
- return null;
616
- }
617
- bindPlan = planConversationBind({
618
- sessionKey: anchor,
619
- corpus,
620
- spillMemo,
621
- sessionLedger,
622
- });
623
- }
624
-
625
- if (bindPlan.action !== 'recall' || !bindPlan.contextId) {
626
- bindFilesInBackground('no context yet, files only');
627
- return null;
628
- }
629
-
630
- let bind;
631
- let deltaChars = 0;
632
- let appended = false;
633
- if (bindPlan.restored && bindPlan.contextId) {
634
- // Sidecar came back up: we still know the context_id and the accumulated
635
- // char count, but not the prior prefix string, so we cannot slice a delta.
636
- // Re-append the current prefix (some overlap is harmless) and keep the
637
- // restored ledger � do not add corpus.length again.
638
- void bindCorpus(corpus, {
639
- appendTo: bindPlan.contextId,
640
- onStage: (stage, info) => {
641
- if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (restored session, background)`);
642
- },
643
- }).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
644
- appended = true;
645
- bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
646
- } else if (bindPlan.append && bindPlan.delta) {
647
- const delta = bindPlan.delta;
648
- deltaChars = delta.length;
649
- // FIRE AND FORGET. This delta is history for FUTURE turns � the answer
650
- // being generated right now is served from the tail plus what is already
651
- // bound, so waiting on the upload buys nothing and costs the user the
652
- // round trip on every single turn. The context id is already known, so
653
- // nothing is lost by not waiting for it.
654
- void bindCorpus(delta, {
655
- appendTo: bindPlan.contextId,
656
- onStage: (stage, info) => {
657
- if (stage === 'binding') log(`appending ${mb(info.bytes)}MB to ${bindPlan.contextId} (delta, background)`);
658
- },
659
- }).catch((e) => log(`append failed (history may lag one turn): ${e.message}`));
660
- appended = true;
661
- bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: delta.length };
662
- } else {
663
- bind = { contextId: bindPlan.contextId, hash: bindPlan.hash, reused: true, bytes: 0 };
664
- }
665
- // CONVERSATION LEDGER � every successful bind AND append, not only when
666
- // files exist. First bind initializes to the bound corpus size; each append
667
- // adds the delta; file bytes ride on top. This is what makes x-hrr-corpus-chars
668
- // the accumulated bound corpus instead of this-turn's prefix.
669
- noteCorpusLedger(boundChars, {
670
- contextId: bind.contextId,
671
- reused: appended,
672
- corpusChars: corpus.length,
673
- deltaChars,
674
- fileChars: 0,
675
- ...ledgerOpts(),
676
- });
677
- // APPEND THE FILES AFTER, off the clock. Fire-and-forget against the context
678
- // we just secured: this turn is already answerable without them, and the next
679
- // ask gets them for free. `boundFiles` already deduped by path:mtime, so this
680
- // uploads each version exactly once no matter how often the agent re-reads it.
681
- // Read + readdir are inside setImmediate � they must not run before send().
682
- bindFilesInBackground('background', { appendTo: bind.contextId, asAppend: true });
683
- rememberSpillMemo(spillMemo, anchor, { corpus, contextId: bind.contextId, hash: bind.hash });
684
- // Count the tail that is actually forwarded after stub/trim � older
685
- // continue-turn rounds after the ask may have been dropped, so
686
- // msgs.length - cut would keep lastSend growing with the raw pile.
687
- const sent = Math.max(0, stubbed.messages.length - cut);
688
- // NAME THE KEY. A memo keyed on the wrong thing fails silently — it just
689
- // re-binds forever and collides sessions — so the log says which key was used.
690
- const keyKind = sessionId ? `sid ${String(sessionId).slice(0, 8)}` : 'content-anchor';
691
- // WHAT ACTUALLY REACHES THE MODEL. Inference about this cut has been wrong
692
- // twice; the roles of the forwarded tail settle it in one line.
693
- if (process.env.OPENZOO_LOG_TAIL === '1') {
694
- const roles = msgs.slice(cut).map((m) => (m.role || '?')[0]).join('');
695
- const lastU = msgs.slice(cut).some((m) => m.role === 'user' && msgText(m).trim());
696
- log(` tail roles=${roles} firstSpillable=${firstSpillable} cut=${cut} hasUserText=${lastU}`);
697
- }
698
- log(bind.reused
699
- ? `transcript prefix already bound (${bind.contextId}, ${keyKind}) — sending ${sent}/${msgs.length} turns`
700
- : `transcript prefix bound (${mb(bind.bytes)}MB → ${bind.contextId}, ${keyKind}) — sending ${sent}/${msgs.length} turns`);
701
-
702
- // applySpillCut cut/stubbed the tail (post-ask file bodies stay real;
703
- // search may stub). The adapt line is already logged there.
704
- if (stubbed.dropped) {
705
- log(`file-stub stubbed=${stubbed.stubbed} dropped=${stubbed.dropped}`);
706
- }
707
-
708
- // ADAPTIVE TOP-K. A fixed 32 chunks is what was actually eating the saving:
709
- // MEASURED on a 56,265-token corpus, top_k 32 handed 9,990 tokens back and
710
- // scored 2.45x, while 8 handed back 2,574 and scored 4.73x — same answer,
711
- // same corpus, nearly double the saving. Recall breadth, not markup, is the
712
- // lever, and spilling 34k tokens only to recall 22k of them back is not a
713
- // saving, it is a round trip.
714
- //
715
- // So budget the recall in TOKENS and derive k from it, rather than fixing the
716
- // chunk count and letting the token cost fall where it may. ~320 tokens per
717
- // chunk measured. The budget scales with the ask — a one-line question needs
718
- // far less context than a detailed one — and is clamped so a huge ask cannot
719
- // drag the whole corpus back in.
720
- const askChars = msgText(msgs[msgs.length - 1] || {}).length;
721
- // BREADTH IS THE LEVER, AND MORE OF IT IS WORSE. MEASURED on a 56,265-token
722
- // corpus: top_k 32 handed back 9,990 tokens and scored 2.45x, top_k 8 handed
723
- // back 2,574 and scored 4.73x — same question, same answer, nearly double the
724
- // saving. Spilling 34k tokens to recall 22k of them back is a round trip, not
725
- // a saving. So budget the recall tighter and cap k where the measurement says
726
- // the value is, rather than at the largest number that still fits.
727
- const budget = Math.min(
728
- Number(process.env.OPENZOO_RECALL_MAX_TOKENS || 3000),
729
- Math.max(Number(process.env.OPENZOO_RECALL_MIN_TOKENS || 1200),
730
- Math.round(askChars / 3)),
731
- );
732
- const topK = Math.max(4, Math.min(12, Math.round(budget / 320)));
733
-
734
- return {
735
- body: Buffer.from(JSON.stringify({
736
- ...body,
737
- messages: decision.forwarded || [...head, ...stubbed.messages.slice(cut)],
738
- })),
739
- topK,
740
- contextId: bind.contextId,
741
- hash: bind.hash,
742
- corpus,
743
- reused: bind.reused,
744
- savedBytes: bind.bytes,
745
- sent,
746
- msgs: msgs.length,
747
- };
748
- }
749
-
750
- async function maybeCacheCorpus(req, bodyBuf, log, stats, extra = {}) {
751
- if (contextCacheDisabled()) return null;
752
- if (req.method !== 'POST' || !(req.url || '').includes('/chat/completions')) return null;
753
- if (req.headers['x-hrr-context']) return null; // harness manages its own context
754
- if (bodyBuf.length <= BIND_MIN_CHARS) {
755
- // Still walk the transcript: a short agent turn that Read a file should
756
- // bind it even when there is nothing large enough to spill.
757
- try {
758
- const body = JSON.parse(bodyBuf.toString('utf8'));
759
- if (Array.isArray(body?.messages) && body.messages.length) {
760
- return spillTranscript(body, log, req, stats, extra);
761
- }
762
- } catch { /* not json */ }
763
- return null;
764
- }
765
- let body;
766
- try { body = JSON.parse(bodyBuf.toString('utf8')); } catch { return null; }
767
- const msgs = Array.isArray(body?.messages) ? body.messages : null;
768
- if (!msgs?.length) return null;
769
- // CORPUS+QUESTION first — one huge final message ending in \n\n<ask>. That is
770
- // what zoo_ask and the chat surface send, and binding exactly that body keeps
771
- // the ask verbatim. Anything else (an agent transcript) falls through to the
772
- // transcript spill, which used to be a silent no-op.
773
- const last = msgs[msgs.length - 1];
774
- // Same gate decideChatSpill uses � 1-model and race share it. zoo_ask
775
- // stays on the corpus+question bind; everything else (including grokui
776
- // AUTO, raced or not) falls through to spillTranscript ? decideChatSpill.
777
- if (!isOneShotCorpusAsk(msgs, BIND_MIN_CHARS)) return spillTranscript(body, log, req, stats, extra);
778
- const cut = last.content.lastIndexOf('\n\n');
779
- const corpus = last.content.slice(0, cut);
780
- const ask = last.content.slice(cut + 2).trim();
781
- if (!ask || ask.length > 8000) return spillTranscript(body, log, req, stats, extra);
782
-
783
- const bind = await bindCorpus(corpus, {
784
- onStage: (stage, info) => {
785
- if (stage === 'binding') log(`binding ${mb(info.bytes)}MB corpus to holographic memory (one-time)...`);
786
- },
787
- });
788
- noteCorpusLedger(boundChars, {
789
- contextId: bind.contextId,
790
- reused: false,
791
- corpusChars: corpus.length,
792
- sessions: sessionLedger,
793
- boundFiles,
794
- });
795
- if (bind.reused) {
796
- log(`corpus already bound (${bind.hash.slice(0, 12)}… → ${bind.contextId}) — skipped ${mb(bind.bytes)}MB upload`);
797
- } else {
798
- log(`corpus bound once (${mb(bind.bytes)}MB → ${bind.contextId}) — repeats of this body are near-free`);
799
- }
800
- const rewritten = { ...body, messages: [...msgs.slice(0, -1), { ...last, content: ask }] };
801
- return {
802
- body: Buffer.from(JSON.stringify(rewritten)),
803
- contextId: bind.contextId,
804
- hash: bind.hash,
805
- corpus,
806
- reused: bind.reused,
807
- savedBytes: bind.bytes,
808
- sent: 1,
809
- msgs: msgs.length,
810
- };
811
- }
812
-
813
251
  /**
814
252
  * `requireToken` / `sessionMaxUsd` are TUNNEL MODE (see lib/tunnel.js): once the
815
253
  * proxy is reachable from the internet, the api key stops being decorative and
@@ -819,16 +257,10 @@ async function maybeCacheCorpus(req, bodyBuf, log, stats, extra = {}) {
819
257
  export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
820
258
  const client = new PayClient();
821
259
  const log = silent ? () => {} : (...a) => console.log(...a);
822
- // ALWAYS-ON. `silent: true` is used by the editor path to keep startup tidy,
823
- // but it also swallowed the per-request lines and the payment receipts — so the
824
- // terminal sat blank and there was no way to tell a working setup from an editor
825
- // quietly answering from its own backend. Traffic and receipts are the whole
826
- // point of watching this window; they are never silenced.
827
260
  // ALWAYS-ON, BUT NEVER INTO A HARNESS'S TERMINAL. `silent` means another
828
261
  // process (openzoo claude, the editor launcher) owns stdio — printing request
829
- // lines / payment receipts there corrupts that program's output (observed: the
830
- // Solana receipt leaking into the Claude Code CLI). When silent, route this
831
- // channel to a log file instead; only print to the console when we own it.
262
+ // lines / payment receipts there corrupts that program's output. When silent,
263
+ // route this channel to a log file instead; only print when we own the console.
832
264
  const restored = loadSessionSpend();
833
265
  let paidCalls = restored.paidCalls;
834
266
  let sayFile = null;
@@ -838,9 +270,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
838
270
  mkdirSync(path.dirname(sayFile), { recursive: true });
839
271
  } catch { sayFile = null; }
840
272
  }
841
- // Spill/adapt/classifier lines must use this channel: `log` is a no-op when
842
- // `openzoo claude` starts us with silent:true, and printing them on stdout
843
- // corrupts the Claude Code TTY. say() writes ~/.openzoo/proxy.log then.
844
273
  const say = (...a) => {
845
274
  const line = a.join(' ');
846
275
  if (sayFile) { try { appendFileSync(sayFile, line + '\n'); return; } catch { /* fall through */ } }
@@ -854,7 +283,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
854
283
  saveSessionSpend({ spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls });
855
284
  };
856
285
  if (restored.ok && (sessionSpent > 0 || paidCalls > 0)) {
857
- say(`session restored: $${sessionSpent.toFixed(6)} � ${paidCalls} paid call${paidCalls === 1 ? '' : 's'}`);
286
+ say(`session restored: $${sessionSpent.toFixed(6)} · ${paidCalls} paid call${paidCalls === 1 ? '' : 's'}`);
858
287
  }
859
288
  process.on('exit', rememberSpend);
860
289
  const noteQuote = (x) => {
@@ -880,44 +309,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
880
309
  // where public traffic slips through ungated.
881
310
  let tunnelGate = null;
882
311
  // How many chat requests actually ARRIVED. The single number that answers
883
- // "is the editor really routing through us?" — an editor that silently keeps
884
- // using its own backend leaves this at 0 while looking perfectly healthy.
312
+ // "is the editor really routing through us?"
885
313
  let servedRequests = 0;
886
- // SPILL ACCOUNTING. The product's whole claim is that context is offloaded
887
- // instead of re-sent, and nothing measured it — the status line showed spend
888
- // and call count, which is the cost side with none of the benefit.
889
- const spill = createSpillStats();
890
- let lastSpillSend = null;
891
- // Spend/direct for ONLY the calls that spilled. The session-wide savingX
892
- // averages these with every small turn that had nothing to offload, so it
893
- // slides toward 1.0 as a conversation grows — which reads as the mechanism
894
- // degrading when it is just the mix changing. OBSERVED: 1.3166 -> 1.1823
895
- // while spilled calls and offloaded tokens both sat completely still.
896
- // spillSpend / spillDirect live on `spill` (createSpillStats).
897
314
  // ACTUAL UPSTREAM SPEND, FROM THE PROVIDER — not our estimate of it.
898
- //
899
- // Every other dollar figure here is derived from OpenRouter's CATALOG price
900
- // times a token count we guessed, and the guess is bad in a specific
901
- // direction: output is priced on `max_tokens`, which callers set as a ceiling.
902
- // MEASURED 2026-08-19: a call we quoted at $0.9858 (32,000 reserved output
903
- // tokens) actually cost $0.007962 — OpenRouter's own number, 124x smaller.
904
- //
905
- // OpenRouter returns `usage.cost` on every completion, so the real figure is
906
- // already in the response body and needs no extra request and no account-level
907
- // lookup. That last part matters: /api/v1/credits reports the WHOLE key's
908
- // lifetime usage, and this key also pays for ttfx direct runs and other work,
909
- // so the account total can never attribute a dollar to this proxy. Per-call
910
- // cost can.
315
+ // OpenRouter returns `usage.cost` on every completion; per-call cost is the
316
+ // only figure attributable to THIS proxy.
911
317
  let sessionActual = 0;
912
318
  let actualCalls = 0;
913
319
  // billed for ONLY those calls whose real cost we learned — the honest
914
- // numerator for markupX. See the comment at the usage.cost site.
320
+ // numerator for markupX.
915
321
  let billedWithActual = 0;
916
- // CREDIT, CACHED. Users cannot tell prepaid credit from wallet balance and
917
- // have to guess whether a call was even paid for ("I don't think x402 made me
918
- // pay this at all"). The status line runs EVERY turn, so this is refreshed at
919
- // most every 20s and served stale in between — a status line must never add
920
- // latency to the thing it is describing.
322
+ // CREDIT, CACHED. Refreshed at most every 20s and served stale in between —
323
+ // a status line must never add latency to the thing it is describing.
921
324
  let creditUsd = null;
922
325
  let creditAt = 0;
923
326
  let creditInflight = null;
@@ -949,23 +352,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
949
352
 
950
353
  const server = http.createServer(async (req, res) => {
951
354
  // MCP on the SAME port as the proxy. One `npx openzoo` gives a harness
952
- // both surfaces: point base_url at /v1 for transparent context spilling,
953
- // or add /mcp for tools (zoo_bind, zoo_ask...). Running two commands to
954
- // get both was friction nobody should pay.
955
- // This wallet's own running total for THIS proxy process — every paid
956
- // call through this port counts (GUI, MCP, CLI, any harness), not just
957
- // whichever surface happens to be asking. Local-only, no auth needed:
958
- // it's a number, not a capability.
355
+ // both surfaces: point base_url at /v1, or add /mcp for tools.
959
356
  if (req.method === 'GET' && (req.url || '').split('?')[0] === '/v1/session') {
960
- // NEVER await fly.dev. Serve last-known; a hung keep-alive on the same
961
- // undici pool as brainRace used to wedge this handler (LISTEN up, HTTP 000).
962
- // refreshCredit() is fire-and-forget so HUD "continue" cannot sit on HTTP 000.
357
+ // NEVER await fly.dev. Serve last-known; refreshCredit() is
358
+ // fire-and-forget so HUD "continue" cannot sit on HTTP 000.
963
359
  refreshCredit().catch(() => {});
964
360
  refreshPrices();
965
361
  const money = walletMoney();
966
362
  res.writeHead(200, { 'content-type': 'application/json' });
967
- // Same package.json the startup banner reads � grokui-app refuses to
968
- // attach to a leftover :8402 whose version is older than it shipped with.
969
363
  const { version } = JSON.parse(
970
364
  readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
971
365
  );
@@ -977,9 +371,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
977
371
  return;
978
372
  }
979
373
 
980
- // Public addresses only — never the private key. Exists so a caller (the
981
- // grokui error path, in particular) can print REAL funding instructions
982
- // inline instead of telling the user to go look somewhere else.
374
+ // Public addresses only — never the private key. Exists so a caller can
375
+ // print REAL funding instructions inline.
983
376
  if (req.method === 'GET' && (req.url || '').split('?')[0] === '/v1/wallet') {
984
377
  refreshCredit().catch(() => {});
985
378
  refreshPrices();
@@ -989,8 +382,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
989
382
  solana: client.address,
990
383
  evm: client.evmAddress,
991
384
  funding: fundingLine(client.address),
992
- // from the same background poll the startup/refresh lines use, so a
993
- // caller can tell a genuinely empty wallet from a transient 402
994
385
  balances: balanceLine(lastSnap || []) || null,
995
386
  funded: (lastSnap || []).some((b) => b.ui > 0),
996
387
  creditUsd,
@@ -1010,10 +401,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1010
401
  return;
1011
402
  }
1012
403
 
1013
- // THE CUTESY GUI. Local browsers hitting GET / get a little chat app —
1014
- // model zoo, bind-a-corpus drawer, live spent/saved ticker off the x402
1015
- // receipts. Tunnel traffic (cf headers) keeps the JSON discovery below:
1016
- // the GUI is the operator's, not the public's.
404
+ // THE CUTESY GUI. Local browsers hitting GET / get a little chat app.
405
+ // Tunnel traffic (cf headers) keeps the JSON discovery below.
1017
406
  {
1018
407
  const p0 = (req.url || '').split('?')[0];
1019
408
  const local = !(req.headers['cf-connecting-ip'] || req.headers['cf-ray']);
@@ -1032,7 +421,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1032
421
  log(`path ${req.url} -> ${normalized} (base_url already ends in /v1)`);
1033
422
  req.url = normalized;
1034
423
  }
1035
- let url = `${config.apiBase}${req.url}`;
424
+ const url = `${config.apiBase}${req.url}`;
1036
425
  // Requests that arrived over the public quick-tunnel URL carry cloudflared's
1037
426
  // headers; nothing dialing 127.0.0.1 directly does. That distinction is what
1038
427
  // lets localhost stay keyless while the SAME port is safely public.
@@ -1051,11 +440,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1051
440
  return;
1052
441
  }
1053
442
  }
1054
- // ROUTING TRUTH, SERVED FROM WHATEVER URL YOU REACHED US ON. A cloud agent
1055
- // only ever touches the tunnel, so asking a local MCP process "what is my
1056
- // routing" is the wrong question — the answer has to come from the tunnel
1057
- // itself, and name the tunnel. Free and unauthenticated: discovery must
1058
- // never be the thing that is gated.
443
+ // ROUTING TRUTH, SERVED FROM WHATEVER URL YOU REACHED US ON. Free and
444
+ // unauthenticated: discovery must never be the thing that is gated.
1059
445
  {
1060
446
  const p0 = (req.url || '').split('?')[0];
1061
447
  if (req.method === 'GET' && (p0 === '/v1/info' || p0 === '/info')) {
@@ -1070,30 +456,16 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1070
456
  reachedVia: viaTunnel ? 'public tunnel' : 'localhost',
1071
457
  publicTunnel: tunnelGate?.publicUrl ? `${tunnelGate.publicUrl}/v1` : null,
1072
458
  servedRequests,
1073
- spilled: (() => {
1074
- const ledgerTotal = [...boundChars.values()].reduce((a, b) => a + b, 0);
1075
- return {
1076
- ...spill.snapshot({ boundChars: ledgerTotal }),
1077
- boundChars: ledgerTotal,
1078
- lastSend: lastSpillSend,
1079
- };
1080
- })(),
1081
459
  spendUsd: sessionSpent,
1082
460
  creditUsd,
1083
461
  // WHAT THE SAME CALLS WOULD HAVE COST DIRECT. Spend on its own is a
1084
- // bill; spend beside the counterfactual is the product. The receipt
1085
- // already carries directUsd per call — it simply never reached the
1086
- // status line, so the one number that justifies the tool was the one
1087
- // the user could not see.
462
+ // bill; spend beside the counterfactual is the product.
1088
463
  directUsd: sessionDirect,
1089
464
  savedUsd: Math.max(0, sessionDirect - sessionSpent),
1090
465
  savingX: sessionSpent > 0 ? Number((sessionDirect / sessionSpent).toFixed(4)) : null,
1091
466
  paidCalls,
1092
- // WHAT THE INFERENCE ACTUALLY COST, as reported by OpenRouter on each
1093
- // completion (`usage.cost`). Every other figure above is a catalog
1094
- // estimate built on `max_tokens`; this is metered. `margin` is the
1095
- // honest gross on this session — the number that says whether the
1096
- // pricing is sane, which no estimate can.
467
+ // WHAT THE INFERENCE ACTUALLY COST, as reported by the provider on
468
+ // each completion (`usage.cost`). `margin` is the honest gross.
1097
469
  actual: {
1098
470
  calls: actualCalls,
1099
471
  upstreamUsd: Number(sessionActual.toFixed(6)),
@@ -1107,12 +479,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1107
479
  auth: viaTunnel
1108
480
  ? 'this public URL requires the oz_… bearer for paid endpoints; /v1/models and /v1/hrr/bind are free'
1109
481
  : 'localhost is keyless',
1110
- context: {
1111
- yourAttentionWindow: 'unchanged — openzoo does not enlarge it',
1112
- boundCeiling: '~128M tokens client-usable via bind + retrieval',
1113
- singleRequestLimit: '~8MB per request; larger corpora bind in parts',
1114
- retrieval: 'lossy top-k retrieval, NOT lossless compression',
1115
- },
482
+ shim: 'pure passthrough — request bodies are forwarded byte-for-byte; all message/model handling is on the backend',
1116
483
  tools: ['zoo_bind', 'zoo_ask', 'zoo_status', 'zoo_models', 'zoo_wallet', 'zoo_contexts'],
1117
484
  docs: 'https://openzoo.fun',
1118
485
  }, null, 2));
@@ -1123,18 +490,9 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1123
490
  if (viaTunnel) {
1124
491
  const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
1125
492
  // TRUST ON FIRST USE, so a stale key still works without weakening the
1126
- // tunnel to "any key forever".
1127
- //
1128
- // Tunnel tokens are minted per session, so a client holding a key from an
1129
- // earlier run silently failed — and the only fixes were re-pasting by
1130
- // hand or writing into the editor's OS-encrypted credential store, which
1131
- // would mean prompting for keychain access to install a value the user
1132
- // never chose. Instead: the printed token always works, and the FIRST
1133
- // other key to present itself claims the tunnel for the rest of the
1134
- // session. Your editor (which reaches the URL first, from this machine)
1135
- // adopts it; anyone who finds the URL afterwards is refused because the
1136
- // slot is taken. The URL is unguessable and ephemeral, the spend ceiling
1137
- // still applies, and OPENZOO_TUNNEL_STRICT=1 restores exact-match only.
493
+ // tunnel to "any key forever". The printed token always works, and the
494
+ // FIRST other key to present itself claims the tunnel for the session.
495
+ // OPENZOO_TUNNEL_STRICT=1 restores exact-match only.
1138
496
  const strict = process.env.OPENZOO_TUNNEL_STRICT === '1';
1139
497
  let authed = got === tunnelGate.token;
1140
498
  if (!authed && !strict && got.length >= 8) {
@@ -1147,11 +505,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1147
505
  }
1148
506
  }
1149
507
  const p = (req.url || '').split('?')[0];
1150
- // DISCOVERY IS FREE. An agent probing this URL cold should be able to
1151
- // work out what it is and what to ask its operator for — a bare 401 on
1152
- // every path just sends it spelunking through the operator's machine.
1153
- // Reads that cost nothing and leak nothing (the catalog is public on
1154
- // the zoo anyway) go through without the key; money paths stay gated.
508
+ // DISCOVERY IS FREE. Reads that cost nothing and leak nothing go through
509
+ // without the key; money paths stay gated.
1155
510
  if (!authed) {
1156
511
  if (req.method === 'GET' && (p === '/' || p === '/v1' || p === '/v1/info')) {
1157
512
  res.writeHead(200, { 'content-type': 'application/json' });
@@ -1162,18 +517,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1162
517
  endpoints: {
1163
518
  'GET /v1/models': 'model catalog with pricing + context_length — no key needed',
1164
519
  'GET /v1/models/{id}': 'single-model probe — no key needed',
1165
- 'POST /v1/chat/completions': 'chat (streaming supported, any model id — unknown ids are matched to the nearest served model) — key required',
520
+ 'POST /v1/chat/completions': 'chat (streaming supported) — key required',
1166
521
  },
1167
522
  docs: 'https://openzoo.fun · https://www.npmjs.com/package/openzoo',
1168
523
  }, null, 2));
1169
524
  return;
1170
525
  }
1171
- // Binding COSTS NOTHING — no 402, no wallet, no settlement. Gating it
1172
- // behind the key only stopped agents from using the one endpoint that
1173
- // makes a big corpus workable: observed in the wild, an agent wrote a
1174
- // correct multi-part bind script, got 401 on the final append, and
1175
- // fell back to stuffing the corpus inline. The money paths below stay
1176
- // gated; the worst a stranger can do here is spend our sidecar's disk.
526
+ // Binding COSTS NOTHING — no 402, no wallet, no settlement. The money
527
+ // paths below stay gated; the worst a stranger can do here is spend
528
+ // the sidecar's disk.
1177
529
  const freeRead = req.method === 'GET' && (p === '/v1/models' || p.startsWith('/v1/models/'));
1178
530
  const freeBind = req.method === 'POST' && p === '/v1/hrr/bind';
1179
531
  if (freeBind) log(`public url: unauthenticated bind allowed (free endpoint) from ${req.socket.remoteAddress}`);
@@ -1196,348 +548,28 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1196
548
  return;
1197
549
  }
1198
550
 
1199
- // Local decision � no upstream, no payment. Same keys as Python route().
1200
- {
1201
- const routePath = (req.url || '').split('?')[0];
1202
- if (req.method === 'POST' && (routePath === '/route' || routePath === '/v1/route')) {
1203
- let payload;
1204
- try { payload = JSON.parse(bodyBuf.toString('utf8') || '{}'); } catch {
1205
- jsonErr(res, 400, 'invalid /route body');
1206
- return;
1207
- }
1208
- try {
1209
- const r = routeTask(payload.text ?? '', {
1210
- allow_free: payload.allow_free ?? false,
1211
- bindable: payload.bindable ?? true,
1212
- context: payload.context,
1213
- input_tokens: payload.input_tokens,
1214
- has_image: payload.has_image,
1215
- needs_tools: payload.needs_tools,
1216
- needs_json: payload.needs_json,
1217
- ...(payload.constraints && typeof payload.constraints === 'object' ? payload.constraints : {}),
1218
- });
1219
- res.writeHead(200, { 'content-type': 'application/json' });
1220
- res.end(JSON.stringify(r));
1221
- } catch (err) {
1222
- jsonErr(res, 500, `openzoo /route: ${err.message}`);
1223
- }
1224
- return;
1225
- }
1226
- }
1227
-
1228
- // ANTHROPIC MESSAGES SHAPE. A harness pointed here via ANTHROPIC_BASE_URL
1229
- // (Claude Code, the Anthropic SDKs) speaks POST /v1/messages, not chat
1230
- // completions — this is how such a harness routes its inference through
1231
- // x402 without any DNS or TLS trickery. Translate the body to OpenAI shape
1232
- // and rewrite the path so EVERYTHING downstream (model rewrite, brief,
1233
- // corpus cache, payment, replay, streaming) runs unchanged; translate the
1234
- // answer back on the way out. See lib/anthropic.js.
1235
- let anthropicMode = false;
1236
- let anthropicModel = null;
1237
- // The CLIENT's streaming intent, kept separate from the body we send
1238
- // upstream. The Anthropic lane asks the gateway for a complete message (we
1239
- // can only translate a finished one) while still owing the caller SSE.
1240
- let clientWantsStream = false;
1241
- let responsesMode = false;
1242
- let responsesModel = null;
1243
- let responsesCustom = null; // names of freeform tools needing custom_tool_call on the way back
1244
551
  const rawPath = (req.url || '').split('?')[0];
1245
-
1246
- // RESPONSES API. Some harnesses speak only this wire format — OpenAI's
1247
- // Codex Security CLI pins `wire_api: "responses"` in its provider table, so
1248
- // a bare 404 here made it fall back to wss://api.openai.com and bypass the
1249
- // proxy entirely while reporting the failure as an auth error. Translate
1250
- // in, translate out; everything between stays on the chat path.
1251
- if (req.method === 'POST' && (rawPath === '/v1/responses' || rawPath === '/responses')) {
1252
- try {
1253
- const inbound = JSON.parse(bodyBuf.toString('utf8'));
1254
- // TEMP CAPTURE: dump the first few Responses requests so the agent's
1255
- // actual wire usage (store / previous_response_id / tool shapes) can be
1256
- // read rather than inferred. Guarded by an env var so it is off unless
1257
- // asked for.
1258
- if (process.env.OZ_CAPTURE_RESPONSES) {
1259
- try {
1260
- const fs = await import('node:fs');
1261
- const dir = process.env.OZ_CAPTURE_RESPONSES;
1262
- fs.mkdirSync(dir, { recursive: true });
1263
- const n = fs.readdirSync(dir).length;
1264
- // Capture the LATER turns too. Turn 1 is already understood; the
1265
- // unknown is what codex sends back after running a tool, so bias
1266
- // the capture toward requests that carry a tool result.
1267
- const hasResult = Array.isArray(inbound.input)
1268
- && inbound.input.some((i) => i && String(i.type || '').endsWith('_call_output'));
1269
- const tag = hasResult ? 'result' : 'plain';
1270
- if (n < 24) fs.writeFileSync(`${dir}/${tag}-${n}.json`, JSON.stringify(inbound, null, 2));
1271
- } catch { /* capture must never break a paid call */ }
1272
- }
1273
- responsesModel = inbound.model;
1274
- const meta = {};
1275
- const translated = responsesToChat(inbound, meta);
1276
- // Codex's watchdog is ~10s to first SSE byte. Force stream so we can
1277
- // emit response.created immediately instead of buffering Opus.
1278
- translated.stream = true;
1279
- bodyBuf = Buffer.from(JSON.stringify(translated));
1280
- responsesCustom = meta.custom;
1281
- responsesMode = true;
1282
- req.url = '/v1/chat/completions';
1283
- url = `${config.apiBase}${req.url}`;
1284
- } catch {
1285
- jsonErr(res, 400, 'invalid responses body');
1286
- return;
1287
- }
1288
- }
1289
-
1290
- if (req.method === 'POST' && (rawPath === '/v1/messages' || rawPath === '/messages')) {
1291
- try {
1292
- const inbound = JSON.parse(bodyBuf.toString('utf8'));
1293
- anthropicModel = inbound.model;
1294
- // ANTHROPIC CLIENTS CANNOT READ AN OPENAI STREAM.
1295
- //
1296
- // The gateway now streams for real, and `relay()` pipes those frames
1297
- // through untouched — which is correct for an OpenAI client and
1298
- // unreadable to Claude Code, which speaks the Anthropic SSE grammar
1299
- // (message_start / content_block_delta / message_stop). It surfaced as
1300
- // "API returned an empty or malformed response (HTTP 200)": a 200 whose
1301
- // body the client cannot parse.
1302
- //
1303
- // The translation we have (openAIToAnthropic + writeAnthropicSse) works
1304
- // on a COMPLETE message, so this lane asks the gateway not to stream and
1305
- // keeps the buffered translation. That costs Claude Code the
1306
- // time-to-first-byte win until an incremental OpenAI->Anthropic frame
1307
- // translator exists; a readable answer late beats an unreadable one now.
1308
- const converted = anthropicToOpenAI(inbound);
1309
- clientWantsStream = converted.stream === true || inbound.stream === true;
1310
- // STREAM AGAIN. This forced `stream:false` for a few hours because
1311
- // relay() piped the gateway's OpenAI frames straight to a client that
1312
- // speaks message_start / content_block_delta, producing a 200 nobody
1313
- // could parse. Buffering fixed the parse and cost the whole point:
1314
- // Claude Code sends max_tokens=32000 against a 600-turn transcript, so
1315
- // every turn became minutes of zero bytes and read as a hang.
1316
- // streamOpenAIToAnthropic() translates the grammar frame by frame, so
1317
- // the lane can be fast AND readable.
1318
- converted.stream = clientWantsStream;
1319
- bodyBuf = Buffer.from(JSON.stringify(converted));
1320
- anthropicMode = true;
1321
- req.url = '/v1/chat/completions';
1322
- url = `${config.apiBase}${req.url}`;
1323
- } catch {
1324
- jsonErr(res, 400, 'invalid anthropic messages body');
1325
- return;
1326
- }
1327
- }
1328
-
1329
- // Harness model ids ("gpt-5.6-sol", "claude-…") are rewritten onto the
1330
- // NEAREST zoo model BEFORE anything else sees the body — any POST that
1331
- // carries a model field, not just chat/completions, so /completions,
1332
- // /responses and future shapes all work. Never silent.
552
+ // Paid inference paths: chat completions, Anthropic messages, Responses.
553
+ // The BODY IS NOT TOUCHED — the parse below is read-only, to learn the
554
+ // client's streaming intent for the JSON→SSE compatibility path.
555
+ const isChat = req.method === 'POST' && rawPath.includes('/chat/completions');
556
+ const isPaidPost = req.method === 'POST'
557
+ && /\/(chat\/completions|completions|messages|responses)$/.test(rawPath);
1333
558
  let wantsStream = false;
1334
- let autoRoute = null;
1335
- if ((req.url || '').includes('/chat/completions') && req.method === 'POST') {
1336
- servedRequests += 1;
1337
- say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
1338
- // TEMPORARY: name the headers (not values) so we can see whether the
1339
- // client offers a session id at all. Values are never logged — several
1340
- // of these carry auth.
1341
- if (process.env.OPENZOO_LOG_HEADERS === '1') {
1342
- log(` headers: ${Object.keys(req.headers).sort().join(', ')}`);
1343
- }
1344
- }
1345
- if (rewritablePath(req.method, req.url)) {
1346
- try {
1347
- let parsed = JSON.parse(bodyBuf.toString('utf8'));
1348
- wantsStream = parsed?.stream === true || clientWantsStream;
1349
- // TINY CLASSIFY FIRST, ON THE ORIGINAL BODY.
1350
- //
1351
- // Claude Code auto-mode sends a 16-token yes/no to claude-sonnet-5
1352
- // before Bash/WebSearch. Two things used to eat that call:
1353
- // 1. OPENZOO_DEFAULT_MODEL rewrote it onto deepseek/grok;
1354
- // 2. the reasoning floor then raised 16 -> 4000.
1355
- // Measured: 11.5s, past the caller's timeout, "claude-sonnet-5 is
1356
- // temporarily unavailable (timed out), so auto mode cannot determine
1357
- // the safety of WebSearch". Even staying on sonnet-5 is too slow
1358
- // (402 handshake behind a long Grok stream). Pin to a fast
1359
- // non-reasoning catalog id and leave max_tokens alone. Real
1360
- // Grok/DeepSeek chats (max_tokens 2000+ AND a long transcript)
1361
- // still get the 4000 floor � those still go blank without it.
1362
- // 0.48.75 missed grok nubs at max_tokens 128/2000 on a 1�2
1363
- // message body (~3� from the floor, no classifier log).
1364
- let ids = [];
1365
- try { ids = await zooModelIds(); } catch { /* catalog miss: still skip the floor on a tiny classify */ }
1366
- const policy = rewriteChatModel(parsed, ids, { bodyLen: bodyBuf.length });
1367
- parsed = policy.parsed;
1368
- if (!policy.tiny && (policy.auto || isAutoModel(parsed?.model))) {
1369
- autoRoute = routeChatBody(parsed, {
1370
- allow_free: false,
1371
- bindable: true,
1372
- // Live quoteable ids only � Auto must never emit :batch / $0 /
1373
- // missing-price rows that 500 `bad openrouter price` on Fly.
1374
- ...(ids.length ? { allow_ids: ids } : {}),
1375
- });
1376
- if (!autoRoute.model) {
1377
- jsonErr(res, 422, autoRoute.reason || 'openzoo/auto: no feasible model', { route: autoRoute });
1378
- return;
1379
- }
1380
- parsed = { ...parsed, model: autoRoute.model };
1381
- const bump = raiseReasoningMaxTokens(parsed);
1382
- parsed = bump.parsed;
1383
- say(`openzoo/auto -> ${autoRoute.model} p=${autoRoute.p_success} cleared=${autoRoute.cleared_bar} ${autoRoute.task_class}${autoRoute.bind_first ? ' bind_first' : ''}`);
1384
- if (!autoRoute.cleared_bar) {
1385
- say(`openzoo/auto cleared_bar=false � strongest fallback, not a normal pick`);
1386
- }
1387
- }
1388
- if (policy.tiny) {
1389
- // `openzoo claude` starts us silent � `log` is a no-op then. say()
1390
- // is the proxy.log channel and never the Claude Code TTY.
1391
- say(`classifier tiny max_tokens=${Number(parsed?.max_tokens)} "${policy.from}" -> ${policy.to} (no reasoning floor)`);
1392
- } else {
1393
- // SAY WHETHER THE OVERRIDE IS ACTUALLY SET, and name it.
1394
- //
1395
- // This used to print "(OPENZOO_DEFAULT_MODEL overrides)" on EVERY
1396
- // rewrite whether or not the variable existed � a hint about a knob,
1397
- // phrased as a statement about this request. It cost a real incident:
1398
- // the proxy was restarted from a shell carrying
1399
- // OPENZOO_DEFAULT_MODEL=deepseek/deepseek-v4-pro-0813, so every
1400
- // claude-sonnet-5 ask was served by deepseek, and the log line looked
1401
- // exactly the same as it always had. deepseek matches the reasoning
1402
- // regex, so a 16-token safety classification became a 4,000-token
1403
- // reasoning generation � 11.5s, past the caller's timeout, and Claude
1404
- // Code reported "claude-sonnet-5 is temporarily unavailable".
1405
- // Tiny classify is pinned above and never reaches this path.
1406
- if (policy.to && policy.to !== policy.from) {
1407
- const forced = process.env.OPENZOO_DEFAULT_MODEL;
1408
- log(forced
1409
- ? `model "${policy.from}" -> FORCED to ${forced} by OPENZOO_DEFAULT_MODEL (nearest match would have been ${policy.to})`
1410
- : `model "${policy.from}" is not on the zoo � nearest match ${policy.to}`);
1411
- }
1412
- // REASONING MODELS SPEND max_tokens ON THINKING FIRST.
1413
- //
1414
- // The budget covers hidden reasoning AND the visible answer, so a
1415
- // caller that asks for 40 tokens because it wants a short answer often
1416
- // gets ZERO � the whole allowance went to reasoning and the completion
1417
- // truncated to an empty string. Measured across three families in one
1418
- // day: deepseek returned 0 chars at 8k and was fine at 24k; grok-4.6
1419
- // pinned ct at exactly its 16,000 budget with no visible output;
1420
- // sonnet-5 truncated a 600-token file mid-function because Anthropic's
1421
- // max_tokens covers thinking too.
1422
- //
1423
- // An empty completion is not an error � it bills normally and renders
1424
- // as a blank reply � so this fails silently and looks like the retrieval
1425
- // broke. It cost real debugging time tonight for exactly that reason.
1426
- // Multiply the allowance for known reasoning families and let callers
1427
- // keep asking for what they actually want back.
1428
- //
1429
- // A MULTIPLIER ALONE IS NOT ENOUGH. 4x on a caller's 40 is 160, which is
1430
- // still nothing for a model that thinks first � measured, 2 of 3 runs
1431
- // still returned empty at 160. Reasoning needs an absolute floor, not a
1432
- // relative bump, so take whichever is larger.
1433
- if (policy.raised) {
1434
- log(`reasoning model ${parsed.model}: max_tokens ${policy.raisedFrom} -> ${policy.raisedTo} (thinking shares the budget; OPENZOO_REASONING_MAX_TOKENS_X=1 disables)`);
1435
- }
1436
- }
1437
- if (policy.tiny || policy.raised || autoRoute || (policy.to && policy.to !== policy.from)) {
1438
- bodyBuf = Buffer.from(JSON.stringify(parsed));
1439
- }
1440
- // Tell the agent what it is actually connected to — in band, where it
1441
- // will read it, instead of leaving it to guess (and to chunk corpora
1442
- // it could bind whole). See lib/brief.js.
1443
- if ((req.url || '').includes('/chat/completions')) {
1444
- // Tell it the URL it actually reached us on — the public tunnel for
1445
- // a remote harness, localhost for a local one. An agent that has to
1446
- // guess its own endpoint guesses a website.
1447
- const selfUrl = viaTunnel && tunnelGate?.publicUrl
1448
- ? `${tunnelGate.publicUrl}/v1`
1449
- : `http://localhost:${config.port}/v1`;
1450
- // NOT ON A YES/NO. The brief is ~2.2KB describing corpus binding,
1451
- // request-size limits and payment — none of which a tiny call can
1452
- // use. Claude Code's auto-mode safety classifier asks a 16-token
1453
- // question before it will run Bash, and it has a short timeout:
1454
- // MEASURED, that call takes 3.5s cold through here against a 0.09s
1455
- // gateway 402, and it times out on a machine paying on-chain. Adding
1456
- // 2.2KB of prose to a body that small is latency and tokens spent on
1457
- // advice nobody will read.
1458
- //
1459
- // Threshold is the same one the spill uses: below it there is no
1460
- // corpus and nothing the brief could help with. policy.tiny is
1461
- // that check on the ORIGINAL body, before the reasoning floor.
1462
- const tiny = policy.tiny
1463
- || (bodyBuf.length < BIND_MIN_CHARS
1464
- && Number(parsed?.max_tokens ?? 0) > 0
1465
- && Number(parsed?.max_tokens) <= CLASSIFY_MAX_TOKENS);
1466
- const briefed = tiny ? null : injectBrief(parsed, selfUrl);
1467
- if (briefed) parsed = briefed;
1468
- // SYSTEM MESSAGES BELONG AT THE FRONT, OR GOOGLE 400s.
1469
- //
1470
- // Claude Code emits <system-reminder> blocks mid-conversation, which
1471
- // is legal for Anthropic natively. Several upstreams behind OpenRouter
1472
- // are not: fable-5 is served by GOOGLE, whose API takes a system
1473
- // instruction only before the conversation starts and rejects one
1474
- // after. CAPTURED live — provider_error code 400,
1475
- // roles="sssusatatus", 311KB body: two system messages sitting after
1476
- // user turns, on a model that answers a simple call fine.
1477
- //
1478
- // So fold every later system message into the leading block, in
1479
- // order. The content survives and its position moves; the alternative
1480
- // is a 400 that ends the turn and tells the caller nothing.
1481
- const nm = Array.isArray(parsed?.messages) ? parsed.messages : null;
1482
- if (nm && nm.length > 1) {
1483
- let lead = 0;
1484
- while (lead < nm.length && nm[lead]?.role === 'system') lead += 1;
1485
- const strays = [];
1486
- const kept = [];
1487
- nm.forEach((m, i) => {
1488
- if (i >= lead && m?.role === 'system') strays.push(m);
1489
- else kept.push(m);
1490
- });
1491
- if (strays.length) {
1492
- const merged = strays.map((m) => (typeof m.content === 'string' ? m.content : msgText(m))).filter(Boolean).join('\n\n');
1493
- const head = kept.slice(0, lead);
1494
- const tailMsgs = kept.slice(lead);
1495
- if (head.length) head[head.length - 1] = { ...head[head.length - 1], content: `${typeof head[head.length - 1].content === 'string' ? head[head.length - 1].content : msgText(head[head.length - 1])}\n\n${merged}` };
1496
- else head.push({ role: 'system', content: merged });
1497
- parsed = { ...parsed, messages: [...head, ...tailMsgs] };
1498
- log(`hoisted ${strays.length} interleaved system message(s) to the leading block (some providers 400 otherwise)`);
1499
- }
1500
- }
1501
- bodyBuf = Buffer.from(JSON.stringify(parsed));
1502
- }
1503
- } catch { /* not JSON */ }
559
+ if (isPaidPost) {
560
+ servedRequests += 1;
561
+ say(`\n<- request #${servedRequests} from ${(req.headers['user-agent'] || 'unknown').slice(0, 40)}`);
562
+ try { wantsStream = JSON.parse(bodyBuf.toString('utf8'))?.stream === true; } catch { /* not JSON */ }
1504
563
  }
1505
564
 
1506
565
  // Retry of a body we answered seconds ago? Serve the cached completion —
1507
- // never pay twice for a harness's reconnect loop.
1508
- const isChat = req.method === 'POST' && (req.url || '').includes('/chat/completions');
1509
- // GROUND TRUTH ON THE OUTGOING BODY. Three sessions have now reported "no
1510
- // actual question or task from you" while the proxy log showed a healthy
1511
- // forward, and two rounds of reasoning about the cut were wrong. Log what
1512
- // is actually in messages[] on the way out — roles, and the tail of the
1513
- // last user turn — so the question stops being a matter of opinion.
1514
- if (process.env.OPENZOO_LOG_BODY === '1' && isChat) {
1515
- try {
1516
- const b = JSON.parse(bodyBuf.toString('utf8'));
1517
- const ms = Array.isArray(b?.messages) ? b.messages : [];
1518
- const roles = ms.map((m) => (m.role || '?')[0]).join('');
1519
- const lastUser = [...ms].reverse().find((m) => m.role === 'user');
1520
- const txt = lastUser ? String(msgText(lastUser)).slice(-160).replace(/\s+/g, ' ') : '(NO USER MESSAGE)';
1521
- log(` OUT roles=${roles} n=${ms.length} lastUser="${txt}"`);
1522
- } catch { /* not json */ }
1523
- }
566
+ // never pay twice for a harness's reconnect loop. Chat-completions JSON
567
+ // only: it is the one shape we know how to re-emit (including as SSE).
1524
568
  const rKey = isChat ? replayKey(bodyBuf, req.headers) : null;
1525
569
  if (rKey) {
1526
570
  const hit = replayGet(rKey);
1527
571
  if (hit) {
1528
572
  log('identical request within 30s — served the cached completion, NOT re-paid');
1529
- // The cache stores the CHAT shape. A Responses caller must get its own
1530
- // wire format back, or the replay path silently answers in a format the
1531
- // client cannot parse — a bug that only appears on the SECOND identical
1532
- // request, which is exactly when nobody is watching.
1533
- if (responsesMode) {
1534
- if (wantsStream) { writeResponsesSse(res, hit.data, responsesModel, null, responsesCustom); return; }
1535
- const rh = { 'content-type': 'application/json' };
1536
- if (hit.settle) rh['x-payment-response'] = hit.settle;
1537
- res.writeHead(200, rh);
1538
- res.end(JSON.stringify(chatToResponses(hit.data, responsesModel, responsesCustom)));
1539
- return;
1540
- }
1541
573
  if (wantsStream) { serveAsSse(res, hit.data, null); return; }
1542
574
  const h = { 'content-type': 'application/json' };
1543
575
  if (hit.settle) h['x-payment-response'] = hit.settle;
@@ -1551,8 +583,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1551
583
 
1552
584
  // Harnesses validate their configured model BEFORE ever POSTing — some
1553
585
  // list /v1/models, some probe /v1/models/<id>. Both must succeed for the
1554
- // ids we know how to rewrite, or the harness refuses upfront and the
1555
- // rewrite never gets its chance.
586
+ // alias ids the launchers write into configs.
1556
587
  const path = (req.url || '').split('?')[0];
1557
588
  if (req.method === 'GET' && path === '/v1/models') {
1558
589
  // Catalog is chrome, not a paid call. Paying the list would put a 402
@@ -1561,25 +592,18 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1561
592
  const response = await fetchHeaders(url, init);
1562
593
  if (response.ok) {
1563
594
  const payload = await response.json();
1564
- // Quoteable catalog only. Raw OpenRouter dump includes :batch,
1565
- // $0 / missing prices, and we used to mint openzoo-* twins of each
1566
- // � Claude Code /model then showed 63 clones and Auto 500'd.
595
+ // Quoteable catalog only: no :batch twins, no $0 rows, no clones.
1567
596
  res.writeHead(200, { 'content-type': 'application/json' });
1568
597
  res.end(JSON.stringify(modelsListForRequest(payload, req.headers)));
1569
598
  return;
1570
599
  }
1571
600
  await response.text().catch(() => {});
1572
- } catch { /* gateway 402/down � serve aliases so chrome still paints */ }
601
+ } catch { /* gateway 402/down — serve aliases so chrome still paints */ }
1573
602
  res.writeHead(200, { 'content-type': 'application/json' });
1574
603
  res.end(JSON.stringify(modelsListForRequest({ object: 'list', data: [] }, req.headers)));
1575
604
  return;
1576
605
  }
1577
606
  const probe = req.method === 'GET' && /^\/v1\/models\/(.+)$/.exec(path);
1578
- if (probe && isAutoModel(decodeURIComponent(probe[1]))) {
1579
- res.writeHead(200, { 'content-type': 'application/json' });
1580
- res.end(JSON.stringify(autoModelListEntry()));
1581
- return;
1582
- }
1583
607
  if (probe && isHarnessAliasId(decodeURIComponent(probe[1]))) {
1584
608
  res.writeHead(200, { 'content-type': 'application/json' });
1585
609
  res.end(JSON.stringify({ id: decodeURIComponent(probe[1]), object: 'model', owned_by: 'openzoo-alias' }));
@@ -1587,127 +611,24 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1587
611
  }
1588
612
 
1589
613
  try {
1590
- let cached = null;
1591
- try {
1592
- // Spill/adapt diagnostics (adapt, file-stub, sending N/M) must hit
1593
- // ~/.openzoo/proxy.log when we are silent. `log` is a no-op then.
1594
- cached = await maybeCacheCorpus(req, bodyBuf, say, spill, {
1595
- lastSend: lastSpillSend?.sent,
1596
- dollarX: hudDollarX({
1597
- spillDirect: spill.spillDirect,
1598
- spillSpend: spill.spillSpend,
1599
- sessionDirect,
1600
- sessionSpent,
1601
- }),
1602
- });
1603
- } catch (err) {
1604
- log(`context cache skipped for this call: ${err.message}`);
1605
- }
1606
- const send = (buf, ctxId, topK, corpusChars) => client.fetch(url, {
1607
- ...init,
1608
- body: buf,
1609
- headers: ctxId
1610
- ? {
1611
- ...init.headers,
1612
- 'x-hrr-context': ctxId,
1613
- // Only on the spill path — a caller that set its own top-k keeps it.
1614
- ...(topK ? { 'x-hrr-top-k': String(topK) } : {}),
1615
- // TELL THE GATEWAY HOW BIG THE CORPUS IS. It has been guessing:
1616
- // `contextChars` is populated only when a bind passes through the
1617
- // gateway itself, so on an APPEND — and on every corpus that
1618
- // includes files the agent read, which never appear in a request
1619
- // body at all — it has no idea and falls back to the body size.
1620
- // That is why the counterfactual logged `corpus ?` all day and why
1621
- // a call whose corpus held 40,777 tokens priced as if it held
1622
- // 4,303. The proxy assembled the corpus; it is the only party that
1623
- // knows.
1624
- ...(corpusChars ? { 'x-hrr-corpus-chars': String(corpusChars) } : {}),
1625
- }
1626
- : init.headers,
1627
- });
1628
- let didSpill = false;
1629
- let result;
1630
- if (cached) {
1631
- spill.noteSpill({ corpusChars: cached.corpus?.length || 0, reused: cached.reused });
1632
- didSpill = true;
1633
- if (cached.sent != null) lastSpillSend = { sent: cached.sent, msgs: cached.msgs };
1634
- result = await send(cached.body, cached.contextId, cached.topK, corpusCharsForSend(boundChars, cached.contextId, cached.corpus?.length));
1635
- // Sidecar wiped between runs: the gateway 404s BEFORE the 402 (nothing
1636
- // paid). Never fail on a stale manifest — re-bind once and retry.
1637
- if (result.response.status === 404) {
1638
- const text = await result.response.text();
1639
- if (/context_not_found/.test(text)) {
1640
- log('bound context is gone on the zoo — re-binding once...');
1641
- forgetContext(config.apiBase, cached.hash);
1642
- const rebound = await bindCorpus(cached.corpus, { force: true });
1643
- result = await send(cached.body, rebound.contextId, cached.topK, corpusCharsForSend(boundChars, rebound.contextId, cached.corpus?.length));
1644
- } else {
1645
- res.writeHead(404, { 'content-type': 'application/json' });
1646
- res.end(text);
1647
- return;
1648
- }
1649
- }
1650
- } else {
1651
- result = await client.fetch(url, init);
1652
- }
1653
- // Walk the auto shortlist on 429/5xx � cheapest-first among models that
1654
- // cleared the bar. Finite: one pass over fallbackChain(), never a loop.
1655
- if (autoRoute?.cleared_bar && isRetryableStatus(result.response.status)) {
1656
- const chain = fallbackChain(autoRoute);
1657
- for (const next of chain) {
1658
- const failed = result.response.status;
1659
- try { await result.response.arrayBuffer(); } catch { /* drain */ }
1660
- say(`openzoo/auto fallback HTTP ${failed} -> ${next}`);
1661
- const rewriteModel = (buf) => {
1662
- try {
1663
- const b = JSON.parse(buf.toString('utf8'));
1664
- b.model = next;
1665
- return Buffer.from(JSON.stringify(b));
1666
- } catch { return buf; }
1667
- };
1668
- bodyBuf = rewriteModel(bodyBuf);
1669
- init.body = bodyBuf;
1670
- if (cached) {
1671
- cached = { ...cached, body: rewriteModel(cached.body) };
1672
- result = await send(cached.body, cached.contextId, cached.topK, corpusCharsForSend(boundChars, cached.contextId, cached.corpus?.length));
1673
- } else {
1674
- result = await client.fetch(url, { ...init, body: bodyBuf });
1675
- }
1676
- if (!isRetryableStatus(result.response.status)) break;
1677
- }
1678
- }
614
+ const result = await client.fetch(url, init);
1679
615
  const { response, paid, receipt } = result;
1680
616
  if (paid && receipt) {
1681
617
  if (receipt.ok && typeof receipt.billedUsd === 'number') {
1682
618
  sessionSpent += receipt.billedUsd;
1683
- // Wallet path: no 3x. Prefer extra.cogsUsd / billedUsd / directUsd /
1684
- // savedUsd from the 402. billedUsd is the OpenRouter price (plus
1685
- // zoo's 33% of savings when the caller beat direct).
1686
619
  sessionCogs += receiptUsedCogs(receipt);
1687
620
  noteQuote(receipt);
1688
- // direct = what answering this WITHOUT the zoo would have cost. On an
1689
- // attach call that is the whole bound corpus, which is why it can be
1690
- // orders of magnitude above what was billed. Read extra.directUsd /
1691
- // extra.savedUsd; do not invent billed * 3.
1692
- if (didSpill) {
1693
- spill.spillSpend += receipt.billedUsd || 0;
1694
- spill.spillDirect += receiptDirectUsd(receipt);
1695
- }
1696
621
  sessionDirect += receiptDirectUsd(receipt);
1697
622
  // The public-URL ceiling meters only public-origin spend — your own
1698
623
  // local calls never eat into it.
1699
624
  if (viaTunnel) tunnelSpent += receipt.billedUsd;
1700
625
  }
1701
626
  const line = receipt.ok ? receipt.line : `paid retry -> HTTP ${receipt.status}`;
1702
- // Wherever a running total is the thing to watch, it rides the receipt.
1703
627
  if (requireToken) say(`${line} · session $${sessionSpent.toFixed(6)}`);
1704
628
  else if (viaTunnel) say(`${line} · public-url session $${tunnelSpent.toFixed(6)}`);
1705
629
  else say(line);
1706
- // ALWAYS-ON SPEND, TUI-SAFE. When a harness owns the terminal (silent),
1707
- // the receipt lines go to a file (they corrupt a TUI). But the running
1708
- // total should still be visible — so write it to the terminal TITLE via
1709
- // an OSC escape, which updates the window/tab title without touching the
1710
- // TUI's content. `openzoo ◝ $0.0042 · 12 calls` in the title bar, live.
630
+ // ALWAYS-ON SPEND, TUI-SAFE: running total in the terminal TITLE via
631
+ // an OSC escape, never in a TUI's content.
1711
632
  if (receipt.ok && typeof receipt.billedUsd === 'number') { paidCalls += 1; }
1712
633
  if (receipt.ok && typeof receipt.billedUsd === 'number') rememberSpend();
1713
634
  if (sayFile) {
@@ -1715,39 +636,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1715
636
  }
1716
637
  scheduleRefresh(4000); // settlement lands on-chain in a few seconds
1717
638
  }
1718
- // Chat completions come back as one JSON object (settle-before-serve).
1719
- // Cache it against retries, and if the harness asked to stream, honour
1720
- // that contract ourselves. An upstream that someday truly streams (SSE
1721
- // content-type) passes straight through the relay below, untouched.
1722
639
  const upCt = response.headers.get('content-type') || '';
1723
- const noteAutoOutcome = (status, data, streamed = false) => {
1724
- if (!autoRoute) return;
1725
- let ok = outcomeFromResponse(status, data);
1726
- if (ok == null) return;
1727
- if (streamed && status >= 200 && status < 300) ok = true;
1728
- recordRouteOutcome(autoRoute, ok);
1729
- };
1730
- if (isChat && response.ok && upCt.includes('application/json')) {
640
+ if (isPaidPost && response.ok && upCt.includes('application/json')) {
1731
641
  let data = null;
1732
642
  try { data = await response.clone().json(); } catch { /* not JSON after all */ }
1733
- // WHAT IT REALLY COST, straight from the provider. This rides every
1734
- // completion already — no extra call, and unlike the account-level
1735
- // /api/v1/credits total it is attributable to THIS proxy even though the
1736
- // same OpenRouter key also pays for ttfx and everything else.
643
+ // WHAT IT REALLY COST, straight from the provider — rides every
644
+ // completion already; no extra call.
1737
645
  {
1738
- // PAIR THE NUMERATOR WITH THE DENOMINATOR. sessionSpent is summed on
1739
- // three paths and sessionActual on two, so markupX divided ALL billed
1740
- // by the SUBSET that reported a real cost � a 402-receipt call added
1741
- // to billed and nothing to real, and the ratio read 12.55x on a stack
1742
- // running at ~1.0x. Track the billed side of exactly the calls whose
1743
- // cost we actually learned.
1744
- // Both figures ride the SAME response object, so read them together
1745
- // rather than carrying one across sites and hoping the order holds.
1746
- //
1747
- // x402.billedUsd is often the QUOTE reserve (max_tokens � catalog),
1748
- // not the settled charge. MEASURED: $0.9858 reserved vs $0.007962
1749
- // usage.cost -> markupX lied at 124x on a ~1x call. Pair usage.cost
1750
- // with post-completion billed, never the reserve.
646
+ // PAIR THE NUMERATOR WITH THE DENOMINATOR: usage.cost with the
647
+ // post-completion billed twin, never the quote reserve.
1751
648
  const pair = pairActualBilled(data?.x402, data?.usage);
1752
649
  if (pair) {
1753
650
  sessionActual += pair.upstreamUsd;
@@ -1755,64 +652,22 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1755
652
  billedWithActual += pair.billedUsd;
1756
653
  }
1757
654
  }
1758
- // PREPAID CALLS STILL COST MONEY. The block above only meters calls
1759
- // where THIS proxy answered a 402 and paid. When prepaid credit covers
1760
- // the quote the gateway serves 200 on the FIRST request, so there is
1761
- // no 402, no payment and no receipt — and the session read $0.05 / 2
1762
- // calls while the credit balance had actually fallen $3.017 -> $1.395
1763
- // over a 30-question run. The receipt still rides the response body,
1764
- // so meter it from there.
655
+ // PREPAID CALLS STILL COST MONEY. When prepaid credit covers the quote
656
+ // the gateway serves 200 on the FIRST request — no 402, no payment, no
657
+ // receipt. The receipt still rides the response body, so meter it there.
1765
658
  if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
1766
659
  const x = data.x402;
1767
660
  sessionSpent += x.billedUsd;
1768
661
  sessionCogs += receiptUsedCogs(x);
1769
662
  noteQuote(x);
1770
663
  sessionDirect += receiptDirectUsd(x);
1771
- if (didSpill) {
1772
- // THE number that settles why a spilled call did or did not save:
1773
- // the gateway only prices a counterfactual when corpusTokens >
1774
- // promptTokens, so a tail that rivals the corpus silently falls
1775
- // back to at-cost and direct collapses onto billed.
1776
- // Tell-line prints the gateway's actual counterfactual only.
1777
- // Never fall back to lecore.corpusTokens (often == tokensBefore).
1778
- log(spillPricedLine(x));
1779
- spill.spillSpend += x.billedUsd;
1780
- spill.spillDirect += receiptDirectUsd(x);
1781
- }
1782
664
  paidCalls += 1;
1783
665
  if (viaTunnel) tunnelSpent += x.billedUsd;
1784
666
  rememberSpend();
1785
667
  say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
1786
668
  }
1787
669
  if (data?.object === 'chat.completion') {
1788
- noteAutoOutcome(response.status, data);
1789
670
  if (rKey) replayPut(rKey, data, response.headers.get('x-payment-response'));
1790
- // Anthropic-shaped caller gets an Anthropic-shaped answer, streamed
1791
- // or not, so Claude Code and the SDKs parse it natively.
1792
- if (anthropicMode) {
1793
- const msg = openAIToAnthropic(data, anthropicModel);
1794
- if (wantsStream) { writeAnthropicSse(res, msg, response); return; }
1795
- const h = { 'content-type': 'application/json' };
1796
- const settleHdr = response.headers.get('x-payment-response');
1797
- if (settleHdr) h['x-payment-response'] = settleHdr;
1798
- res.writeHead(200, h);
1799
- res.end(JSON.stringify(msg));
1800
- return;
1801
- }
1802
- if (responsesMode) {
1803
- // A Responses client that asked to stream is WAITING for
1804
- // `response.completed`; handing it a JSON body closes the socket
1805
- // mid-stream and it reports "stream disconnected before
1806
- // completion". Honour the streaming contract when it asked for it.
1807
- if (wantsStream) { writeResponsesSse(res, data, responsesModel, response, responsesCustom); return; }
1808
- const out = chatToResponses(data, responsesModel, responsesCustom);
1809
- const h = { 'content-type': 'application/json' };
1810
- const settleHdr = response.headers.get('x-payment-response');
1811
- if (settleHdr) h['x-payment-response'] = settleHdr;
1812
- res.writeHead(200, h);
1813
- res.end(JSON.stringify(out));
1814
- return;
1815
- }
1816
671
  if (wantsStream) { serveAsSse(res, data, response); return; }
1817
672
  const h = { 'content-type': 'application/json' };
1818
673
  const settleHdr = response.headers.get('x-payment-response');
@@ -1826,9 +681,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1826
681
  // same figures the JSON path reads out of `data.x402`, same counters, so
1827
682
  // the status line does not care which transport served the answer.
1828
683
  const meterStreamed = (x) => {
1829
- // Same pairing rule as the JSON path: actualUsd / usage.cost with the
1830
- // settled billed twin, even on a wallet-paid stream (do not skip just
1831
- // because `paid` already recorded the quote-time receipt).
1832
684
  const pair = pairActualBilled(x, x?.usage);
1833
685
  if (pair) {
1834
686
  sessionActual += pair.upstreamUsd;
@@ -1840,86 +692,11 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1840
692
  sessionCogs += receiptUsedCogs(x);
1841
693
  noteQuote(x);
1842
694
  sessionDirect += receiptDirectUsd(x);
1843
- if (didSpill) {
1844
- log(spillPricedLine(x, { streamed: true }));
1845
- spill.spillSpend += x.billedUsd;
1846
- spill.spillDirect += receiptDirectUsd(x);
1847
- }
1848
695
  paidCalls += 1;
1849
696
  if (viaTunnel) tunnelSpent += x.billedUsd;
1850
697
  rememberSpend();
1851
698
  say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
1852
699
  };
1853
-
1854
- // RESPONSES CLIENTS (Codex Security) GET THE SAME STREAM, IN THEIR GRAMMAR.
1855
- // Without this, relay() pipes OpenAI SSE frames at a client waiting for
1856
- // response.created, and the 10s watchdog fires ("connection interrupted").
1857
- if (responsesMode && wantsStream
1858
- && (response.headers.get('content-type') || '').includes('text/event-stream')
1859
- && response.ok && response.body) {
1860
- const tr = streamOpenAIToResponses(res, response, responsesModel, responsesCustom);
1861
- let pending = '';
1862
- const body = Readable.fromWeb(response.body);
1863
- res.on('close', () => body.destroy());
1864
- try {
1865
- for await (const c of body) {
1866
- pending += c.toString('utf8');
1867
- const lines = pending.split('\n');
1868
- pending = lines.pop() ?? '';
1869
- for (const line of lines) {
1870
- if (line.startsWith(': x402 ')) {
1871
- try { meterStreamed(JSON.parse(line.slice(7))); } catch { /* not ours */ }
1872
- continue;
1873
- }
1874
- if (!line.startsWith('data:')) continue;
1875
- const payload = line.slice(5).trim();
1876
- if (!payload || payload === '[DONE]') continue;
1877
- try { tr.onChunk(JSON.parse(payload)); } catch { /* frame split */ }
1878
- }
1879
- }
1880
- } catch (e) {
1881
- log(`responses stream aborted: ${e.message}`);
1882
- }
1883
- tr.finish();
1884
- return;
1885
- }
1886
-
1887
- // ANTHROPIC CLIENTS GET THE SAME STREAM, IN THEIR OWN GRAMMAR.
1888
- // Translated frame by frame rather than buffered, so Claude Code sees
1889
- // tokens as they are produced instead of nothing until the turn ends.
1890
- if (anthropicMode && wantsStream
1891
- && (response.headers.get('content-type') || '').includes('text/event-stream')
1892
- && response.ok && response.body) {
1893
- const tr = streamOpenAIToAnthropic(res, response, anthropicModel, meterStreamed);
1894
- let pending = '';
1895
- const body = Readable.fromWeb(response.body);
1896
- res.on('close', () => body.destroy());
1897
- try {
1898
- for await (const c of body) {
1899
- pending += c.toString('utf8');
1900
- const lines = pending.split('\n');
1901
- pending = lines.pop() ?? '';
1902
- for (const line of lines) {
1903
- if (line.startsWith(': x402 ')) {
1904
- try { meterStreamed(JSON.parse(line.slice(7))); } catch { /* not ours */ }
1905
- continue;
1906
- }
1907
- if (!line.startsWith('data:')) continue;
1908
- const payload = line.slice(5).trim();
1909
- if (!payload || payload === '[DONE]') continue;
1910
- try { tr.onChunk(JSON.parse(payload)); } catch { /* frame split across chunks */ }
1911
- }
1912
- }
1913
- } catch (e) {
1914
- log(`anthropic stream aborted: ${e.message}`);
1915
- }
1916
- tr.finish();
1917
- return;
1918
- }
1919
-
1920
- if (autoRoute) {
1921
- noteAutoOutcome(response.status, null, (upCt.includes('text/event-stream') && response.ok));
1922
- }
1923
700
  await relay(res, response, meterStreamed);
1924
701
  } catch (err) {
1925
702
  if (err instanceof QuoteTooHighError) {
@@ -1930,8 +707,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1930
707
  jsonErr(res, 402, err.message);
1931
708
  } else {
1932
709
  // "fetch failed" alone is undiagnosable — undici hides the real
1933
- // network error in `cause`. Surface it (and log the stack) or every
1934
- // transport hiccup looks identical to a payment bug.
710
+ // network error in `cause`. Surface it or every transport hiccup looks
711
+ // identical to a payment bug.
1935
712
  const cause = err.cause?.message || err.cause?.code || err.cause;
1936
713
  const raw = cause ? `${err.message} (${cause})` : err.message;
1937
714
  const detail = raw;
@@ -1943,22 +720,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1943
720
  });
1944
721
 
1945
722
  // BIND HOST. Default 127.0.0.1 — the keyless localhost path must never be
1946
- // world-reachable on an ordinary machine. A RunPod box is the exception: its
1947
- // HTTP proxy reaches the container over the pod network, so a localhost bind
1948
- // shows "Initializing…" forever (MEASURED: podagent's 0.0.0.0 ports went
1949
- // Ready, the 127.0.0.1 proxy never did). The box sets OPENZOO_BIND=0.0.0.0
1950
- // AND a tunnel token, so the RunPod-fronted port stays gated exactly like the
1951
- // public tunnel path.
723
+ // world-reachable on an ordinary machine. A RunPod box sets
724
+ // OPENZOO_BIND=0.0.0.0 AND a tunnel token, so that port stays gated exactly
725
+ // like the public tunnel path.
1952
726
  const bindHost = process.env.OPENZOO_BIND || '127.0.0.1';
1953
- // SELF-HEAL A TAKEN PORT. A killed-but-not-reaped run, a second terminal, or
1954
- // anything else already on 8402 made listen() reject and took the whole start
1955
- // down — and because `openzoo claude` starts us with silent:true, the user saw
1956
- // only "starting the proxy in the background..." and no reason. Walk up to the
1957
- // next free port instead of dying; the caller reads config.port back out, so
1958
- // every URL printed afterwards is the one we actually bound.
1959
- //
1960
- // EXCEPTION: if the thing already on the port is a HEALTHY openzoo proxy,
1961
- // reuse it rather than starting a rival that splits spend across two wallets.
727
+ // SELF-HEAL A TAKEN PORT. Walk up to the next free port instead of dying;
728
+ // the caller reads config.port back out, so every URL printed afterwards is
729
+ // the one we actually bound. EXCEPTION: if the thing already on the port is
730
+ // a HEALTHY openzoo proxy, reuse it rather than starting a rival that splits
731
+ // spend across two wallets.
1962
732
  const wanted = config.port;
1963
733
  for (let attempt = 0; ; attempt++) {
1964
734
  try {
@@ -1985,23 +755,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1985
755
  }
1986
756
  if (config.port !== wanted) say(`openzoo: listening on :${config.port} (:${wanted} was busy)`);
1987
757
 
1988
- // AUTO-PREPAY. Paying on-chain per call is where the latency lives: the
1989
- // gateway answers its 402 challenge in ~0.12s while a full settled call
1990
- // MEASURED 9-37s end to end. Credit is applied automatically server-side
1991
- // whenever a balance covers the quote, so buying it once makes every later
1992
- // call skip verify+settle entirely.
1993
- //
1994
- // Runs in the background — never block the listener on a payment — and only
1995
- // when this wallet actually has funds, so a fresh/empty wallet is untouched.
758
+ // AUTO-PREPAY. Paying on-chain per call is where the latency lives: credit
759
+ // is applied automatically server-side whenever a balance covers the quote,
760
+ // so buying it once makes every later call skip verify+settle entirely.
761
+ // Runs in the background and only when this wallet actually has funds.
1996
762
  // Opt out with OPENZOO_NO_AUTOTOPUP=1; size it with OPENZOO_AUTOTOPUP_USD.
1997
763
  if (!process.env.OPENZOO_NO_AUTOTOPUP) {
1998
- // Keep credit topped up, forever, from whatever the wallet holds.
1999
- //
2000
- // The first version ran ONCE at startup and bought a fixed $5, so funding
2001
- // the wallet later did nothing at all — the user sent TOKEN and kept
2002
- // paying on-chain per call. This checks on an interval and spends what the
2003
- // wallet can actually cover, priced by the gateway's own live quote (so
2004
- // TOKEN is valued exactly as it settles).
2005
764
  const FLOOR = Number(process.env.OPENZOO_AUTOTOPUP_FLOOR || 2);
2006
765
  const EVERY = Number(process.env.OPENZOO_AUTOTOPUP_EVERY_MS || 60_000);
2007
766
  let topping = false;
@@ -2029,19 +788,14 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
2029
788
 
2030
789
  if (!silent) {
2031
790
  // VERSION IN THE BANNER, deliberately. `npx openzoo` can serve a STALE
2032
- // cached copy — npx reuses a cache entry that matches the bare spec, so a
2033
- // user running the newest published version still gets old behaviour and
2034
- // no clue why (observed: a missing tunnel and a missing token row, both
2035
- // "fixed" releases ago). Printing the version makes that one glance.
791
+ // cached copy; printing the version makes that one glance.
2036
792
  const { version } = JSON.parse(
2037
793
  readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
2038
794
  );
2039
795
  console.log(`openzoo v${version} -> ${config.apiBase}`);
2040
796
  console.log(`listening on http://localhost:${config.port}/v1`);
2041
- // LAND THEM IN THE APP. `npx openzoo` in a human terminal opens the chat
2042
- // GUI — a stranger's first 8 seconds should be a working chat, not a URL
2043
- // to notice. Never in CI/agents (no TTY), never twice, opt out with
2044
- // OPENZOO_NO_OPEN=1.
797
+ // LAND THEM IN THE APP. Never in CI/agents (no TTY), never twice, opt out
798
+ // with OPENZOO_NO_OPEN=1.
2045
799
  if (process.stdout.isTTY && !process.env.OPENZOO_NO_OPEN) {
2046
800
  const opener = process.platform === 'darwin' ? 'open'
2047
801
  : process.platform === 'win32' ? 'start' : 'xdg-open';
@@ -2061,10 +815,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
2061
815
  } catch { /* RPC hiccup: balance is advisory */ }
2062
816
  refreshCredit();
2063
817
  refreshPrices();
2064
- // LIVE REFRESH: the startup line goes stale the moment a call settles or
2065
- // the user funds mid-session. Poll on an interval (and shortly after each
2066
- // paid call), print ONLY on change, and call out arrivals explicitly so
2067
- // "did my top-up land?" answers itself in the running log.
818
+ // LIVE REFRESH: poll on an interval (and shortly after each paid call),
819
+ // print ONLY on change, and call out arrivals explicitly.
2068
820
  refreshBalances = async () => {
2069
821
  try {
2070
822
  const snap = await snapshotBalances(client);
@@ -2094,12 +846,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
2094
846
  const rails = await liveRails();
2095
847
  if (rails) {
2096
848
  console.log(`rails live now: ${rails.live.join(' · ')}`);
2097
- // Funding advice is derived from those rails, never hardcoded — the
2098
- // zoo can add a chain without this package shipping again.
2099
849
  const hint = railFundingHint(rails.live);
2100
850
  if (hint) console.log(`fund with: ${hint}`);
2101
- // The exact contracts those symbols mean — every chain has impersonator
2102
- // mints, so a symbol without its CA is an invitation to fund the wrong one.
2103
851
  for (const row of railFundingAddresses(rails.live)) {
2104
852
  row.assets.forEach((a, i) => {
2105
853
  const label = i === 0 ? row.label : '';
@@ -2143,10 +891,6 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
2143
891
  process.once('SIGTERM', () => { bye(); process.exit(0); });
2144
892
  process.once('exit', bye);
2145
893
  log('');
2146
- if (boundCharsRestored) {
2147
- const tot = [...boundChars.values()].reduce((a, b) => a + b, 0);
2148
- log(`corpus ledger restored: ${boundCharsRestored} context(s), ${mb(tot)}MB bound — counterfactual survives restarts`);
2149
- }
2150
894
  log('cloud IDE / remote harness? use the public URL (they cannot reach localhost):');
2151
895
  log(` base_url = ${url}/v1`);
2152
896
  log(` api_key = ${token}`);
@@ -2154,17 +898,15 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
2154
898
  log(' OPENZOO_NO_TUNNEL=1 for localhost-only)');
2155
899
  } catch (err) {
2156
900
  // Record it: a caller polling for publicUrl (openzoo cursor) would
2157
- // otherwise spin the full timeout on a tunnel that already died, with
2158
- // silent:true swallowing this very message.
901
+ // otherwise spin the full timeout on a tunnel that already died.
2159
902
  tunnelError = err.message;
2160
903
  log(`public URL unavailable (${err.message}) — localhost still works; OPENZOO_NO_TUNNEL=1 hides this line`);
2161
904
  }
2162
905
  })();
2163
906
  }
2164
907
  // Expose live tunnel details so a caller that starts the proxy in-process
2165
- // (openzoo cursor/vscode) can surface the public URL + key instead of the
2166
- // user hunting for them. Getters, because the tunnel resolves ASYNC after
2167
- // this returns — a snapshot would always be null.
908
+ // (openzoo cursor/vscode) can surface the public URL + key. Getters, because
909
+ // the tunnel resolves ASYNC after this returns.
2168
910
  return {
2169
911
  server,
2170
912
  client,