openzoo 0.49.9 → 0.49.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/launch.js CHANGED
@@ -1,15 +1,13 @@
1
1
  /**
2
- * `npx openzoo claude [args...]` — install and/or launch `openzoo-claude`
3
- * (the staccDOTsol fork of open-claude-code) already pointed at the local zoo.
2
+ * `npx openzoo claude [args...]` — launch Claude Code (or any Anthropic-shaped
3
+ * harness) already pointed at the local zoo, so its inference is paid per turn
4
+ * over x402 instead of hitting Anthropic directly.
4
5
  *
5
- * This is the supported front door. We exec that package's binary
6
- * (`openzoo-claude` / `occ` / its `claude` bin), never Anthropic's bun
7
- * `~/.local/bin/claude`. First run prefers `npx -y openzoo-claude` so there
8
- * is no global-install dance and no official Anthropic installer.
9
- *
10
- * ANTHROPIC_BASE_URL is the env the harness reads at startup. The proxy
11
- * serves POST /v1/messages (see lib/anthropic.js). We boot :8402 first if
12
- * needed, then spawn with the same zoo env pair — no second key/url pair.
6
+ * This is the supported front door: ANTHROPIC_BASE_URL is an official env var
7
+ * Claude Code reads at startup. No DNS games, no TLS interception — the proxy
8
+ * serves POST /v1/messages (see lib/anthropic.js) and this just spawns the
9
+ * harness with the two env vars set. The proxy must already be running
10
+ * (`npx openzoo` in another terminal); we check first and say so if not.
13
11
  */
14
12
  import { spawn, spawnSync } from 'node:child_process';
15
13
  import fs from 'node:fs';
@@ -60,9 +58,8 @@ function resolveClaudeDesktop() {
60
58
  return null;
61
59
  }
62
60
 
63
- /** Extra bin dirs (Homebrew, ~/.local/bin, npm-global-ish). Finder-launched
64
- * grokui has no ~/.zshrc PATH, so PATH-only lookup misses user bins. We
65
- * still scan these for `openzoo-claude` / `occ` — not for Anthropic bun. */
61
+ /** Dirs Claude's install.sh uses, plus Homebrew. Finder-launched grokui
62
+ * has no ~/.zshrc PATH, so PATH-only lookup misses `~/.local/bin/claude`. */
66
63
  export function claudeCodeBinDirs(home = os.homedir()) {
67
64
  return [
68
65
  path.join(home, '.local', 'bin'),
@@ -71,141 +68,19 @@ export function claudeCodeBinDirs(home = os.homedir()) {
71
68
  ];
72
69
  }
73
70
 
74
- export const OPENZOO_CLAUDE_PACKAGE = 'openzoo-claude';
75
-
76
- function exeName(name) {
77
- return name + (process.platform === 'win32' ? '.cmd' : '');
78
- }
79
-
80
- function isExecutable(file) {
81
- try { fs.accessSync(file, fs.constants.X_OK); return true; } catch { return false; }
82
- }
83
-
84
- function readHeadBytes(file, n = 8192) {
85
- const fd = fs.openSync(file, 'r');
86
- try {
87
- const buf = Buffer.alloc(n);
88
- const got = fs.readSync(fd, buf, 0, n, 0);
89
- return buf.subarray(0, got);
90
- } finally {
91
- fs.closeSync(fd);
92
- }
93
- }
94
-
95
- function isNativeBinary(buf) {
96
- if (!buf || buf.length < 2) return false;
97
- if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) return true; // ELF
98
- if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE
99
- if (buf.length >= 4) {
100
- const mag = buf.readUInt32BE(0);
101
- // Mach-O 32/64 + fat (incl. byte-swapped).
102
- if (mag === 0xfeedface || mag === 0xfeedfacf || mag === 0xcafebabe
103
- || mag === 0xcffaedfe || mag === 0xcefaedfe) return true;
104
- }
105
- return false;
106
- }
107
-
108
- /** True for Anthropic's bun `claude` (install.sh standalone or bun shebang). */
109
- export function isAnthropicBunClaude(filePath) {
110
- try {
111
- const buf = readHeadBytes(filePath, 512);
112
- if (isNativeBinary(buf)) return true;
113
- const shebang = buf.toString('utf8').split(/\r?\n/, 1)[0] || '';
114
- return shebang.startsWith('#!') && /\bbun\b/i.test(shebang);
115
- } catch {
116
- return false;
117
- }
118
- }
119
-
120
- /** True when this file is the openzoo-claude package (name, realpath, or shim). */
121
- export function isOpenzooClaudeBin(filePath) {
122
- try {
123
- const real = fs.realpathSync(filePath);
124
- if (/(?:^|[\\/])openzoo-claude(?:[\\/]|$)/i.test(real)) return true;
125
- if (isAnthropicBunClaude(real)) return false;
126
- const buf = readHeadBytes(real, 8192);
127
- if (isNativeBinary(buf)) return false;
128
- return buf.toString('utf8').includes('openzoo-claude');
129
- } catch {
130
- return false;
131
- }
132
- }
133
-
134
- function pathSearchDirs(env = process.env) {
135
- const extras = env.OPENZOO_CLAUDE_PATH_ONLY === '1'
136
- ? []
137
- : claudeCodeBinDirs(env.HOME || os.homedir());
71
+ /** Resolve the Claude Code TERMINAL CLI. */
72
+ export function resolveClaudeCli(env = process.env) {
73
+ const name = 'claude' + (process.platform === 'win32' ? '.cmd' : '');
74
+ const extras = env.OPENZOO_CLAUDE_PATH_ONLY === '1' ? [] : claudeCodeBinDirs();
138
75
  const pathDirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
139
- return [...new Set([...extras, ...pathDirs])].filter(Boolean);
140
- }
141
-
142
- function findNamedBin(name, env, { requireOpenzoo = false } = {}) {
143
- const bin = exeName(name);
144
- for (const dir of pathSearchDirs(env)) {
145
- const f = path.join(dir, bin);
146
- if (!isExecutable(f)) continue;
147
- if (requireOpenzoo && !isOpenzooClaudeBin(f)) continue;
148
- return f;
76
+ for (const dir of [...new Set([...extras, ...pathDirs])]) {
77
+ if (!dir) continue;
78
+ const f = path.join(dir, name);
79
+ try { fs.accessSync(f, fs.constants.X_OK); return f; } catch { /* next */ }
149
80
  }
150
81
  return null;
151
82
  }
152
83
 
153
- export function resolveNpx(env = process.env) {
154
- return findNamedBin('npx', env);
155
- }
156
-
157
- /**
158
- * Resolve the openzoo-claude TERMINAL CLI.
159
- * Prefers `openzoo-claude`, then package `occ`, then that package's `claude`
160
- * bin. Never returns Anthropic bun `~/.local/bin/claude`.
161
- */
162
- export function resolveClaudeCli(env = process.env) {
163
- const named = findNamedBin('openzoo-claude', env);
164
- if (named) return named;
165
- const occ = findNamedBin('occ', env, { requireOpenzoo: true });
166
- if (occ) return occ;
167
- const pkgClaude = findNamedBin('claude', env, { requireOpenzoo: true });
168
- if (pkgClaude) return pkgClaude;
169
- return null;
170
- }
171
-
172
- /**
173
- * Path hit or `npx -y openzoo-claude` fallback. Null only when neither the
174
- * package nor npx is available.
175
- */
176
- export function resolveOpenzooClaude(env = process.env) {
177
- const cli = resolveClaudeCli(env);
178
- if (cli) return { command: cli, prefixArgs: [], via: 'path' };
179
- const npx = resolveNpx(env);
180
- if (npx) return { command: npx, prefixArgs: ['-y', OPENZOO_CLAUDE_PACKAGE], via: 'npx' };
181
- return null;
182
- }
183
-
184
- /** Spawn argv for `openzoo claude [args]` — `--help` execs openzoo-claude. */
185
- export function claudeSpawnSpec(argv = [], env = process.env) {
186
- const rest = argv.filter((a) => !['--terminal', '-t', '--desktop'].includes(a));
187
- const resolved = resolveOpenzooClaude(env);
188
- if (!resolved) return null;
189
- return {
190
- command: resolved.command,
191
- args: [...resolved.prefixArgs, ...rest],
192
- via: resolved.via,
193
- };
194
- }
195
-
196
- function zooAuthToken(baseEnv) {
197
- if (baseEnv.ANTHROPIC_AUTH_TOKEN) return baseEnv.ANTHROPIC_AUTH_TOKEN;
198
- const envKey = String(baseEnv.OPENZOO_SUBSCRIPTION_KEY || '').trim();
199
- if (envKey) return envKey;
200
- const file = baseEnv.OPENZOO_SUBSCRIPTION_PATH
201
- || path.join(baseEnv.HOME || os.homedir(), '.openzoo', 'subscription.json');
202
- try {
203
- const key = String(JSON.parse(fs.readFileSync(file, 'utf8'))?.key || '').trim();
204
- if (key) return key;
205
- } catch { /* no subscription file */ }
206
- return 'sk-openzoo';
207
- }
208
-
209
84
  /**
210
85
  * The same env `openzoo claude` writes: gateway auth at the local proxy,
211
86
  * Anthropic API key UNSET (it would bill api.anthropic.com), catalog
@@ -218,8 +93,8 @@ export function claudeZooEnv(baseEnv = process.env, { base, port } = {}) {
218
93
  const env = { ...baseEnv };
219
94
  delete env.ANTHROPIC_API_KEY;
220
95
  env.ANTHROPIC_BASE_URL = url;
221
- env.ANTHROPIC_AUTH_TOKEN = zooAuthToken(baseEnv);
222
- const extras = claudeCodeBinDirs(baseEnv.HOME || os.homedir()).filter((d) => {
96
+ env.ANTHROPIC_AUTH_TOKEN = baseEnv.ANTHROPIC_AUTH_TOKEN || 'sk-openzoo';
97
+ const extras = claudeCodeBinDirs().filter((d) => {
223
98
  try { return fs.existsSync(d); } catch { return false; }
224
99
  });
225
100
  const pathDirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
@@ -258,9 +133,9 @@ export function claudeZooEnv(baseEnv = process.env, { base, port } = {}) {
258
133
  }
259
134
 
260
135
  /**
261
- * `npx openzoo claude [args]` — launch openzoo-claude on the zoo.
262
- * Terminal (openzoo-claude) is the default; `--desktop` opens the Anthropic app.
263
- * Both get the same zoo env pair so inference pays x402 / subscription.
136
+ * `npx openzoo claude [dir] [--terminal]` — launch Claude on the zoo.
137
+ * DEFAULT is the desktop app; `--terminal` (or `-t`) runs the Claude Code CLI.
138
+ * Both get ANTHROPIC_BASE_URL so inference pays x402.
264
139
  */
265
140
  export async function launchClaude(argv) {
266
141
  // let, not const: startProxy can heal onto a different port and every URL
@@ -356,13 +231,8 @@ export async function launchClaude(argv) {
356
231
  const env = claudeZooEnv(process.env, { base });
357
232
 
358
233
  if (terminal) {
359
- const spec = claudeSpawnSpec(argv, env);
360
- if (!spec) {
361
- console.error('openzoo: openzoo-claude not found and npx is unavailable.');
362
- console.error(' install: npx -y openzoo-claude');
363
- console.error(' or: npm i -g openzoo-claude');
364
- process.exit(1);
365
- }
234
+ const cli = resolveClaudeCli();
235
+ if (!cli) { console.error('openzoo: `claude` CLI not found on PATH — install Claude Code, or drop --terminal for the desktop app'); process.exit(1); }
366
236
  // ALWAYS-ON HUD via Claude Code's NATIVE status line (the title bar is owned
367
237
  // by Claude Code and gets overwritten, so OSC there is useless). We write a
368
238
  // tiny status script that reads the proxy's /v1/info, and merge a statusLine
@@ -472,17 +342,17 @@ export async function launchClaude(argv) {
472
342
  let wallet = '';
473
343
  try { const { PayClient } = await import('./pay.js'); wallet = new PayClient().address; } catch { /* optional */ }
474
344
  console.error('');
475
- console.error(' \x1b[38;5;208m●\x1b[0m openzoo — this openzoo-claude session routes through the zoo');
345
+ console.error(' \x1b[38;5;208m●\x1b[0m openzoo — this Claude Code session routes through the zoo');
476
346
  console.error(` endpoint : ${base}`);
477
- console.error(' models : GET /v1/models (quoteable catalog — Claude /model includes OpenRouter ids + Auto)');
347
+ console.error(' models : GET /v1/models (quoteable ids only — Claude /model is a short picker + Auto)');
478
348
  console.error(' auth : gateway token (ANTHROPIC_API_KEY unset — no api.anthropic.com billing)');
479
349
  if (wallet) console.error(` wallet : ${wallet}`);
480
350
  console.error(' spend : live in the status line below (bottom of screen); receipts in ~/.openzoo/proxy.log');
481
351
  console.error(' every turn pays x402.');
482
352
  console.error('');
483
- const child = spawn(spec.command, spec.args, { stdio: 'inherit', env });
353
+ const child = spawn(cli, rest, { stdio: 'inherit', env });
484
354
  child.on('exit', (c) => { try { restoreStatus?.(); } catch { /* ignore */ } process.exit(c ?? 0); });
485
- child.on('error', (e) => { console.error(`openzoo: could not launch openzoo-claude: ${e.message}`); process.exit(1); });
355
+ child.on('error', (e) => { console.error(`openzoo: could not launch claude: ${e.message}`); process.exit(1); });
486
356
  return;
487
357
  }
488
358
 
package/lib/models.js CHANGED
@@ -366,6 +366,59 @@ export function publishModelList(payload, opts) {
366
366
  };
367
367
  }
368
368
 
369
+ function modelVersionKey(id) {
370
+ const m = String(id || '').match(/(\d+(?:\.\d+)*)/g);
371
+ if (!m) return [0];
372
+ return m[0].split('.').map((n) => Number(n) || 0);
373
+ }
374
+
375
+ function cmpModelVersion(a, b) {
376
+ const ka = modelVersionKey(a);
377
+ const kb = modelVersionKey(b);
378
+ for (let i = 0; i < Math.max(ka.length, kb.length); i++) {
379
+ const d = (kb[i] || 0) - (ka[i] || 0);
380
+ if (d) return d;
381
+ }
382
+ return String(a).localeCompare(String(b));
383
+ }
384
+
385
+ function newestMatching(ids, pred) {
386
+ const hits = ids.filter((id) => pred(id) && !String(id).includes(':') && !/-fast(?:$|:)/i.test(id));
387
+ hits.sort(cmpModelVersion);
388
+ return hits[0] || null;
389
+ }
390
+
391
+ /** Preferred gateway flagships for the Claude Code picker, if quoteable. */
392
+ export const CLAUDE_PICKER_GATEWAY = [
393
+ { prefer: 'x-ai/grok-4.6', re: /^x-ai\/grok-/ },
394
+ { prefer: 'google/gemini-3.7-flash', re: /^google\/gemini-/ },
395
+ { prefer: 'deepseek/deepseek-v4-pro-0813', re: /^deepseek\// },
396
+ { prefer: 'qwen/qwen3.8-2.4t-a95b', re: /^qwen\// },
397
+ ];
398
+
399
+ /**
400
+ * Short honest Claude Code /model list: current Opus/Sonnet/Haiku/Fable
401
+ * plus a few real gateway models plus Auto. Only ids already in `rows`.
402
+ */
403
+ export function pickClaudePickerRows(rows) {
404
+ const list = Array.isArray(rows) ? rows : [];
405
+ const byId = new Map(list.map((m) => [m.id, m]));
406
+ const ids = list.map((m) => m.id);
407
+ const picked = [];
408
+ const add = (id) => {
409
+ if (id && byId.has(id) && !picked.includes(id)) picked.push(id);
410
+ };
411
+ add(newestMatching(ids, (id) => /^anthropic\/claude-opus/.test(id)));
412
+ add(newestMatching(ids, (id) => /^anthropic\/claude-sonnet/.test(id)));
413
+ add(newestMatching(ids, (id) => /claude-(?:3-)?haiku/.test(id)));
414
+ add(newestMatching(ids, (id) => /^anthropic\/claude-fable/.test(id)));
415
+ for (const { prefer, re } of CLAUDE_PICKER_GATEWAY) {
416
+ add(ids.includes(prefer) ? prefer : newestMatching(ids, (id) => re.test(id)));
417
+ }
418
+ add(AUTO_MODEL_ID);
419
+ return picked.map((id) => byId.get(id)).filter(Boolean);
420
+ }
421
+
369
422
  function anthropicRows(rows) {
370
423
  return rows.map((m) => ({
371
424
  type: 'model',
@@ -376,27 +429,14 @@ function anthropicRows(rows) {
376
429
  }));
377
430
  }
378
431
 
379
- /** openzoo/auto first so Claude Code's picker default is the router, not opus. */
380
- function withAutoFirst(rows) {
381
- const list = Array.isArray(rows) ? rows.slice() : [];
382
- const i = list.findIndex((m) => isAutoModel(m?.id));
383
- if (i > 0) {
384
- const [auto] = list.splice(i, 1);
385
- list.unshift(auto);
386
- }
387
- return list;
388
- }
389
-
390
432
  /**
391
433
  * Anthropic GET /v1/models shape (id + display_name + type).
392
434
  * Claude Code gateway discovery reads `data[].id` and optional `display_name`.
393
- * Full quoteable catalog — every priced published id, including OpenRouter
394
- * grok/gemini/gpt rows. Not a 4-id Anthropic cap, not openzoo-* clones.
395
- * Picker ≠ classifier: rewriteChatModel still pins 16-token classify off opus-5.
435
+ * Short picker — not the 300+ OpenRouter dump and not openzoo-* clones.
396
436
  */
397
437
  export function anthropicModelList(payload) {
398
438
  const published = publishModelList(payload, { aliases: false });
399
- const data = anthropicRows(withAutoFirst(published.data || []));
439
+ const data = anthropicRows(pickClaudePickerRows(published.data || []));
400
440
  return {
401
441
  data,
402
442
  has_more: false,
@@ -416,10 +456,8 @@ export function wantsAnthropicModelList(headers = {}) {
416
456
 
417
457
  /**
418
458
  * Body for GET /v1/models. Quoteable catalog only (never opus-5-only, never
419
- * unpriced / :batch / openzoo-* clones). Anthropic-shaped clients get the
420
- * same quoteable ids in Anthropic shape (type / id / display_name) so Claude
421
- * Code can select grok, gemini, gpt, etc. OpenAI clients keep object:list
422
- * of every quoteable id + harness aliases.
459
+ * the raw OpenRouter dump). Anthropic-shaped clients get the short picker;
460
+ * OpenAI clients keep object:list of every quoteable id + harness aliases.
423
461
  */
424
462
  export function modelsListForRequest(payload, headers) {
425
463
  return wantsAnthropicModelList(headers) ? anthropicModelList(payload) : publishModelList(payload);
package/lib/proxy.js CHANGED
@@ -30,6 +30,7 @@ import { injectBrief } from './brief.js';
30
30
  import { withNamespace } from './namespace.js';
31
31
  import { anthropicToOpenAI, openAIToAnthropic, streamOpenAIToAnthropic, writeAnthropicSse } from './anthropic.js';
32
32
  import { responsesToChat, chatToResponses, writeResponsesSse } from './responses.js';
33
+ import { streamOpenAIToResponses } from './responses-stream.js';
33
34
  import { loadSessionSpend, saveSessionSpend } from './session.js';
34
35
  import { creditBalance, quotedPrices } from './info.js';
35
36
  import { subscriptionPublicView } from './subscription.js';
@@ -37,7 +38,6 @@ import { priceHoldings } from './livestatus.js';
37
38
  import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettle.js';
38
39
  import { rewriteWrapClientError } from './wrap.js';
39
40
  import { fetchHeaders } from './fetch.js';
40
- import { relay } from './relay.js';
41
41
 
42
42
  const HOP_BY_HOP = new Set([
43
43
  'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade',
@@ -87,6 +87,46 @@ async function readBody(req) {
87
87
  return Buffer.concat(chunks);
88
88
  }
89
89
 
90
+ /** Pipe an upstream fetch Response to the client, unbuffered (SSE-safe).
91
+ *
92
+ * `onReceipt` is called with the gateway's x402 block when it arrives. On a
93
+ * STREAMED call there is no JSON body to carry that block, so the gateway
94
+ * emits it as an SSE COMMENT (`: x402 {...}`) after the last frame — comments
95
+ * are discarded by every compliant client, so nothing downstream sees it, but
96
+ * without reading it here every spend and savings figure on the status line
97
+ * would silently read zero the moment real streaming was switched on.
98
+ *
99
+ * Sniffing NEVER delays a byte: each chunk is written to the client first and
100
+ * only then scanned. */
101
+ function relay(res, upstream, onReceipt) {
102
+ const headers = {};
103
+ upstream.headers.forEach((v, k) => {
104
+ if (!['transfer-encoding', 'connection', 'content-encoding', 'content-length'].includes(k)) headers[k] = v;
105
+ });
106
+ res.writeHead(upstream.status, headers);
107
+ if (!upstream.body) { res.end(); return Promise.resolve(); }
108
+ const sse = (upstream.headers.get('content-type') || '').includes('text/event-stream');
109
+ return new Promise((resolve) => {
110
+ const body = Readable.fromWeb(upstream.body);
111
+ body.on('error', () => res.destroy());
112
+ res.on('close', () => body.destroy());
113
+ body.on('end', resolve);
114
+ if (!sse || typeof onReceipt !== 'function') { body.pipe(res); return; }
115
+ let pending = '';
116
+ body.on('data', (c) => {
117
+ res.write(c);
118
+ pending += c.toString('utf8');
119
+ const lines = pending.split('\n');
120
+ pending = lines.pop() ?? ''; // a comment can straddle two chunks
121
+ for (const line of lines) {
122
+ if (!line.startsWith(': x402 ')) continue;
123
+ try { onReceipt(JSON.parse(line.slice(7))); } catch { /* not our frame */ }
124
+ }
125
+ });
126
+ body.on('end', () => res.end());
127
+ });
128
+ }
129
+
90
130
  function jsonErr(res, status, message, extraFields = {}) {
91
131
  res.writeHead(status, { 'content-type': 'application/json' });
92
132
  res.end(JSON.stringify({ error: { message }, ...extraFields }));
@@ -779,9 +819,6 @@ async function maybeCacheCorpus(req, bodyBuf, log, stats, extra = {}) {
779
819
  * default, so localhost behaviour is unchanged.
780
820
  */
781
821
  export async function startProxy({ silent = false, requireToken = null, sessionMaxUsd = null, autoTunnel = false } = {}) {
782
- // grokui's packed sidecar spawn sets OPENZOO_SILENT=1 so 400s land in
783
- // ~/.openzoo/proxy.log instead of Electron's inherited /dev/null.
784
- if (process.env.OPENZOO_SILENT === '1') silent = true;
785
822
  const client = new PayClient();
786
823
  const log = silent ? () => {} : (...a) => console.log(...a);
787
824
  // ALWAYS-ON. `silent: true` is used by the editor path to keep startup tidy,
@@ -1241,7 +1278,11 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1241
1278
  }
1242
1279
  responsesModel = inbound.model;
1243
1280
  const meta = {};
1244
- bodyBuf = Buffer.from(JSON.stringify(responsesToChat(inbound, meta)));
1281
+ const translated = responsesToChat(inbound, meta);
1282
+ // Codex's watchdog is ~10s to first SSE byte. Force stream so we can
1283
+ // emit response.created immediately instead of buffering Opus.
1284
+ translated.stream = true;
1285
+ bodyBuf = Buffer.from(JSON.stringify(translated));
1245
1286
  responsesCustom = meta.custom;
1246
1287
  responsesMode = true;
1247
1288
  req.url = '/v1/chat/completions';
@@ -1816,6 +1857,39 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1816
1857
  say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
1817
1858
  };
1818
1859
 
1860
+ // RESPONSES CLIENTS (Codex Security) GET THE SAME STREAM, IN THEIR GRAMMAR.
1861
+ // Without this, relay() pipes OpenAI SSE frames at a client waiting for
1862
+ // response.created, and the 10s watchdog fires ("connection interrupted").
1863
+ if (responsesMode && wantsStream
1864
+ && (response.headers.get('content-type') || '').includes('text/event-stream')
1865
+ && response.ok && response.body) {
1866
+ const tr = streamOpenAIToResponses(res, response, responsesModel, responsesCustom);
1867
+ let pending = '';
1868
+ const body = Readable.fromWeb(response.body);
1869
+ res.on('close', () => body.destroy());
1870
+ try {
1871
+ for await (const c of body) {
1872
+ pending += c.toString('utf8');
1873
+ const lines = pending.split('\n');
1874
+ pending = lines.pop() ?? '';
1875
+ for (const line of lines) {
1876
+ if (line.startsWith(': x402 ')) {
1877
+ try { meterStreamed(JSON.parse(line.slice(7))); } catch { /* not ours */ }
1878
+ continue;
1879
+ }
1880
+ if (!line.startsWith('data:')) continue;
1881
+ const payload = line.slice(5).trim();
1882
+ if (!payload || payload === '[DONE]') continue;
1883
+ try { tr.onChunk(JSON.parse(payload)); } catch { /* frame split */ }
1884
+ }
1885
+ }
1886
+ } catch (e) {
1887
+ log(`responses stream aborted: ${e.message}`);
1888
+ }
1889
+ tr.finish();
1890
+ return;
1891
+ }
1892
+
1819
1893
  // ANTHROPIC CLIENTS GET THE SAME STREAM, IN THEIR OWN GRAMMAR.
1820
1894
  // Translated frame by frame rather than buffered, so Claude Code sees
1821
1895
  // tokens as they are produced instead of nothing until the turn ends.
package/lib/racesettle.js CHANGED
@@ -255,7 +255,16 @@ export function isQuoteReserveBilled(billed, cost, x = {}) {
255
255
  * savings). Never return the reserve when we learned the real upstream cost.
256
256
  */
257
257
  export function receiptSettledBilled(x, usage) {
258
- const cost = money(usage?.cost) ?? money(x?.actualUsd) ?? money(usage?.actualUsd);
258
+ // The gateway now sets `usage.billedUsd` explicitly, and it is exactly the
259
+ // settled figure this function spends the rest of its body reconstructing
260
+ // from tokens and prices. Take it when it is there; the reconstruction below
261
+ // stays for older gateways and for the race path, which has no single
262
+ // billed number of its own.
263
+ const direct = money(usage?.billedUsd);
264
+ if (direct != null) return direct;
265
+ // See pairActualBilled: `usage.cost` is our price now, not upstream's.
266
+ const cost = money(usage?.cost_details?.upstream_inference_cost)
267
+ ?? money(usage?.cost) ?? money(x?.actualUsd) ?? money(usage?.actualUsd);
259
268
  const explicit = explicitSettledBilled(x, usage);
260
269
  if (explicit != null) return explicit;
261
270
 
@@ -295,7 +304,19 @@ function settledSavedUsd(x, cost) {
295
304
  * twin. Null when this call did not report a real cost — do not mix populations.
296
305
  */
297
306
  export function pairActualBilled(x402, usage) {
298
- const fromUsage = money(usage?.cost);
307
+ // `usage.cost` IS NO LONGER THE UPSTREAM FIGURE. The gateway now reports OUR
308
+ // price there, because that is the field every generic OpenAI-compatible
309
+ // client reads and they were all showing what OpenRouter charged us instead
310
+ // of what the caller was charged. The upstream number moved to
311
+ // `cost_details.upstream_inference_cost`, which is where OpenRouter itself
312
+ // publishes it.
313
+ //
314
+ // This is the HUD's DENOMINATOR, so reading the new `usage.cost` here would
315
+ // divide billed by billed and print 1× on every call regardless of markup.
316
+ // Prefer the explicit upstream field; fall back to `usage.cost` only for
317
+ // responses from a gateway old enough to predate the move.
318
+ const fromUpstream = money(usage?.cost_details?.upstream_inference_cost);
319
+ const fromUsage = fromUpstream ?? money(usage?.cost);
299
320
  const fromX = money(x402?.actualUsd) ?? money(x402?.usage?.cost);
300
321
  const upstreamUsd = fromUsage ?? fromX;
301
322
  if (upstreamUsd == null) return null;