openzoo 0.51.22 → 0.51.24

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.
@@ -1,3 +1,3 @@
1
1
  <!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'self'"><title>OpenZoo savings</title><style>
2
2
  *{box-sizing:border-box}body{max-height:100vh;overflow-y:auto;margin:0;padding:8px;background:transparent;font:13px system-ui;color:#fff}.pill{border-radius:20px;background:#18181bf5;border:1px solid #ffffff25;overflow:hidden;box-shadow:0 4px 14px #0004}header{background:#e85d1c;padding:12px 16px;font-weight:650;-webkit-app-region:drag;display:flex;justify-content:space-between;align-items:center}button{-webkit-app-region:no-drag;border:0;background:none;color:inherit;font:18px system-ui;cursor:pointer;padding:0 5px}main{padding:12px 16px}dl{display:grid;grid-template-columns:1fr auto;margin:0;gap:7px}dd{margin:0;font-variant-numeric:tabular-nums}small{color:#bbb;display:block;margin-top:12px;font-size:11px}#state{color:#d8ff76}.actions{display:flex;align-items:center;gap:8px}#deposit,.fund-button{font-size:12px;border:1px solid #ffffff60;border-radius:7px;padding:5px 10px}.fund-button{display:block;width:100%;text-align:left;margin:9px 0;overflow-wrap:anywhere;line-height:1.5}.fund-button span{display:block;font-size:11px;color:#ccc;user-select:text}[hidden]{display:none!important}#welcome p{font-size:12px;line-height:1.5;color:#ccc}#funding{border-top:1px solid #ffffff25;margin-top:12px}#funding p{font-size:12px;color:#ccc}
3
- </style></head><body><div class="pill"><header><span>OpenZoo · costs & savings</span><div class="actions"><button id="deposit">Deposit</button><button id="collapse" aria-label="Collapse savings" aria-expanded="true" title="Collapse">−</button><button id="close" aria-label="Close savings pill">×</button></div></header><main><section id="welcome" hidden><h3>Welcome to OpenZoo</h3><p>Pay per request from your OpenZoo wallet. Add funds before your first chat.</p><p>1. Choose card or crypto below.<br>2. Add funds to this wallet.<br>3. Once your balance appears, start chatting.</p></section><div id="totals"><dl><dt>Wallet balance</dt><dd id="wallet">—</dd><dt>Prepaid credit</dt><dd id="credit">—</dd><dt>Charged · verified calls</dt><dd id="spent">—</dd><dt>Direct cost · verified calls</dt><dd id="direct">—</dd><dt id="saving-label">Saved</dt><dd id="saved">—</dd></dl><small id="state" role="status">Checking your balance…</small><small>Savings cover matching verified calls only. Drag the orange bar to move.</small></div><section id="funding" hidden><h3>Add funds</h3><p id="fund-status" role="status">Loading your wallet…</p><button class="fund-button" id="copy-solana" disabled>Solana · USDC / KISS / LEOS / TOKEN<span id="solana-address"></span><span>Click to copy address</span></button><button class="fund-button" id="copy-evm" disabled>Base · USDC<span id="evm-address"></span><span>Click to copy address</span></button><button class="fund-button" id="card">Pay by card · Whop ↗</button><p>At checkout, paste your Solana address to link the purchase to this wallet. Your balance updates automatically after confirmation.</p><button class="fund-button" id="back">Back to savings</button><button class="fund-button" id="later" hidden>Set up later</button></section></main></div><script src="savings-renderer.js"></script></body></html>
3
+ </style></head><body><div class="pill"><header><span>OpenZoo · costs & savings</span><div class="actions"><button id="deposit">Deposit</button><button id="collapse" aria-label="Collapse savings" aria-expanded="true" title="Collapse">−</button><button id="close" aria-label="Close savings pill">×</button></div></header><main><section id="welcome" hidden><h3>Welcome to OpenZoo</h3><p>Pay per request from your OpenZoo wallet. Add funds before your first chat.</p><p>1. Choose card or crypto below.<br>2. Add funds to this wallet.<br>3. Once your balance appears, start chatting.</p></section><div id="totals"><dl><dt>Wallet balance</dt><dd id="wallet">—</dd><dt>Prepaid credit</dt><dd id="credit">—</dd><dt>Charged · verified calls</dt><dd id="spent">—</dd><dt>Direct cost · verified calls</dt><dd id="direct">—</dd><dt id="saving-label">Saved</dt><dd id="saved">—</dd></dl><small id="state" role="status">Checking your balance…</small><small>Savings cover matching verified calls only. Drag the orange bar to move.</small></div><section id="funding" hidden><h3>Add funds</h3><p>Use native USDC or eat.ag cooked tokens. Unwrapped TOKEN, KISS and LEOS are not accepted. Cook first at eat.ag.</p><p id="fund-status" role="status">Loading your wallet…</p><button class="fund-button" id="copy-solana" disabled>Solana · native USDC / cooked KISS / LEOS / TOKEN<span id="solana-address"></span><span>Click to copy address</span></button><button class="fund-button" id="copy-evm" disabled>Base · USDC<span id="evm-address"></span><span>Click to copy address</span></button><button class="fund-button" id="card">Pay by card · Whop ↗</button><p>At checkout, paste your Solana address to link the purchase to this wallet. Your balance updates automatically after confirmation.</p><button class="fund-button" id="back">Back to savings</button><button class="fund-button" id="later" hidden>Set up later</button></section></main></div><script src="savings-renderer.js"></script></body></html>
package/lib/config.js CHANGED
@@ -38,12 +38,12 @@ export const config = {
38
38
  */
39
39
  export const FUNDING_ASSETS = [
40
40
  { symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' },
41
- { symbol: 'KISS', mint: '7K2iAPzHddrghwBF7S7oA9qHr7dDR4QvFmdD1sZgRJxF' },
42
- // LEOS is NINE decimals, and a LEGACY SPL mint rather than Token-2022 —
41
+ { symbol: 'KISS', mint: '9TVSmJj6UbZHMoqdYwjTwTdr6AfT9tjTzT27hvyZxsbX' },
42
+ // Cooked LEOS is NINE decimals and uses Token-2022 —
43
43
  // balance reads must not assume either, so nothing here hardcodes them;
44
44
  // decimals come off the mint on chain.
45
- { symbol: 'LEOS', mint: '5xgsnby6P9zqGK71J7H4yJLxzqPvNbC7rDZxNzjHmj7e' },
46
- { symbol: 'TOKEN', mint: 'EVULoNF4DeMBN4dGiZiDfpiiTfNZgoCvXWWgaV3epump' },
45
+ { symbol: 'LEOS', mint: '2tXdVsqrMYZhXGQSErTL5FmF2qCiL9tsQQ9pVafYmzf3' },
46
+ { symbol: 'TOKEN', mint: '4JSgUyRRgrNSK5pA3wQMp61vP5RRuLeynmPbRwMkvrex' },
47
47
  ];
48
48
 
49
49
  export const USDC_MINT = FUNDING_ASSETS[0].mint;
@@ -134,7 +134,7 @@ export function evmRpcFor(rail) {
134
134
  * also has an EVM address for the Base / Robinhood rails.
135
135
  */
136
136
  export function fundingLine(address) {
137
- return `send a few cents of USDC (${USDC_MINT}) or KISS (${TOKEN_MINT}) on Solana to ${address}`;
137
+ return `Send native USDC or eat.ag cooked TOKEN, KISS, or LEOS on Solana to ${address}. Unwrapped project tokens are not accepted. Cook first: ${FUNDING_ASSETS.filter(a => a.symbol !== "USDC").map(a => `${a.symbol}: https://eat.ag/cook/${a.mint}`).join(" · ")}`;
138
138
  }
139
139
 
140
140
  /**
package/lib/pay.js CHANGED
@@ -258,6 +258,7 @@ export class PayClient {
258
258
  async buildPaymentFor(accept, onStage, challenge) {
259
259
  const rail = railOf(accept);
260
260
  if (rail === 'solana') {
261
+ if (['7K2iAPzHddrghwBF7S7oA9qHr7dDR4QvFmdD1sZgRJxF','EVULoNF4DeMBN4dGiZiDfpiiTfNZgoCvXWWgaV3epump','5xgsnby6P9zqGK71J7H4yJLxzqPvNbC7rDZxNzjHmj7e'].includes(accept.asset)) throw new Error('Unwrapped project tokens are no longer accepted. Cook your tokens at eat.ag first.');
261
262
  const need = BigInt(accept.maxAmountRequired);
262
263
  const bal = await cachedTokenBalance(this.connection, this.keypair.publicKey, accept.asset, { force: true }).catch(() => { throw new BalanceUnavailableError(); });
263
264
  // SHORT IS SHORT. The 402 quotes the raw native mint, so there is nothing
@@ -267,7 +268,7 @@ export class PayClient {
267
268
  if (bal.raw < need) {
268
269
  const usd = Number(accept?.extra?.billedUsd);
269
270
  const line = await withOnrampLink(
270
- `Send USDC (or KISS/LEOS) on Solana to ${this.address}.`,
271
+ fundingLine(this.address),
271
272
  { solana: this.address, usd },
272
273
  );
273
274
  throw new UnderfundedError(accept, bal.ui, this.address, { line });
package/lib/proxy.js CHANGED
@@ -27,16 +27,27 @@ import { detectModelCommand, applyModelCommand, serveModelCommand, readOverride
27
27
  * Free read, but harnesses probe on every /model keystroke confirmation —
28
28
  * without a cache each probe is an upstream round trip.
29
29
  */
30
- let catalogIdsMemo = { at: 0, ids: [] };
30
+ let catalogIdsMemo = { at: 0, ids: [], house: new Set() };
31
31
  async function catalogIdsCached(modelsUrl, headers) {
32
32
  if (Date.now() - catalogIdsMemo.at < 5 * 60_000 && catalogIdsMemo.ids.length) return catalogIdsMemo.ids;
33
33
  const r = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });
34
34
  if (!r.ok) throw new Error(`catalog ${r.status}`);
35
35
  const payload = await r.json();
36
- const ids = quoteableRows(payload?.data).map((m) => m.id);
37
- if (ids.length) catalogIdsMemo = { at: Date.now(), ids };
36
+ const rows = quoteableRows(payload?.data);
37
+ const ids = rows.map((m) => m.id);
38
+ // House doors (`owned_by: "openzoo"` — abliterated-model*, openzoo/auto)
39
+ // take plain `function` tools only; see responsestools.js.
40
+ const house = new Set(rows.filter((m) => m.owned_by === 'openzoo').map((m) => m.id));
41
+ if (ids.length) catalogIdsMemo = { at: Date.now(), ids, house };
38
42
  return ids;
39
43
  }
44
+ /** Models we have SEEN reject Responses tool groups with `invalid_tools`,
45
+ * so the next request flattens before paying instead of after. */
46
+ const flattenLearned = new Set();
47
+ function wantsFlatTools(model) {
48
+ const id = String(model || '');
49
+ return flattenLearned.has(id) || catalogIdsMemo.house.has(id) || /^abliterated-model/.test(id);
50
+ }
40
51
  import { withNamespace } from './namespace.js';
41
52
  import { loadSessionSpend, saveSessionSpend, sessionSpendFile } from './session.js';
42
53
  import { creditBalance, quotedPrices } from './info.js';
@@ -44,6 +55,7 @@ import { priceHoldings } from './livestatus.js';
44
55
  import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettle.js';
45
56
  import { fetchHeaders } from './fetch.js';
46
57
  import { attachX402Proof } from './spendProof.js';
58
+ import { flattenResponsesTools, restoreResponsesPayload, sseNamespaceRestorer, isInvalidToolsError } from './responsestools.js';
47
59
 
48
60
  /** Kill whatever is LISTEN on this port except this process.
49
61
  * lsof first (macOS), then fuser, then /proc (Omarchy/Arch with neither).
@@ -224,7 +236,7 @@ async function readBody(req) {
224
236
  *
225
237
  * Sniffing NEVER delays a byte: each chunk is written to the client first and
226
238
  * only then scanned. */
227
- function relay(res, upstream, onReceipt) {
239
+ function relay(res, upstream, onReceipt, rewriter = null) {
228
240
  const headers = {};
229
241
  upstream.headers.forEach((v, k) => {
230
242
  if (!['transfer-encoding', 'connection', 'content-encoding', 'content-length'].includes(k)) headers[k] = v;
@@ -237,13 +249,20 @@ function relay(res, upstream, onReceipt) {
237
249
  body.on('error', () => res.destroy());
238
250
  res.on('close', () => body.destroy());
239
251
  body.on('end', resolve);
240
- if (!sse || typeof onReceipt !== 'function') { body.pipe(res); return; }
241
- const observe = billingObserver(onReceipt);
252
+ if (!sse || (typeof onReceipt !== 'function' && !rewriter)) { body.pipe(res); return; }
253
+ const observe = typeof onReceipt === 'function' ? billingObserver(onReceipt) : () => {};
254
+ // A rewriter (Responses tool un-flattening) needs whole `data:` lines, so
255
+ // it re-chunks on newlines; the billing sniff reads the rewritten text,
256
+ // which leaves the trailing receipt comment untouched.
242
257
  body.on('data', (c) => {
243
- res.write(c);
244
- observe(c.toString('utf8'));
258
+ const text = rewriter ? rewriter.push(c.toString('utf8')) : c;
259
+ if (text.length) res.write(text);
260
+ observe(text.toString('utf8'));
261
+ });
262
+ body.on('end', () => {
263
+ if (rewriter) { const rest = rewriter.flush(); if (rest) { res.write(rest); observe(rest); } }
264
+ res.end();
245
265
  });
246
- body.on('end', () => res.end());
247
266
  });
248
267
  }
249
268
 
@@ -769,9 +788,13 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
769
788
  // The BODY IS NOT TOUCHED — the parse below is read-only, to learn the
770
789
  // client's streaming intent for the JSON→SSE compatibility path.
771
790
  const isChat = req.method === 'POST' && rawPath.includes('/chat/completions');
791
+ const isResponses = req.method === 'POST' && /\/responses$/.test(rawPath);
772
792
  const isPaidPost = req.method === 'POST'
773
793
  && /\/(chat\/completions|completions|messages|responses)$/.test(rawPath);
774
794
  let wantsStream = false;
795
+ // Flat-name → {namespace, name} for this request's Responses tools; set
796
+ // only when the body was flattened, so the reply can be un-flattened.
797
+ let nsMap = null;
775
798
  // RECEIPTS ARE OPT-OUT. `disableStats: true` in the body (or the
776
799
  // x-openzoo-disable-stats header) means this caller does not want the x402
777
800
  // block, so we neither attach our settle proof nor pass the gateway's
@@ -843,6 +866,23 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
843
866
  }
844
867
  }
845
868
  } catch { /* not JSON */ }
869
+ // RESPONSES TOOL GROUPS vs HOUSE DOORS. Codex sends `namespace` tool
870
+ // groups + `web_search`; abliterated-model* rejects both with a PAID
871
+ // 400 `invalid_tools`. Flatten before the wire for models known to
872
+ // need it (catalog house doors, or learned from a prior 400 below).
873
+ if (isResponses) {
874
+ try {
875
+ const parsed = JSON.parse(bodyBuf.toString('utf8'));
876
+ if (parsed && wantsFlatTools(parsed.model)) {
877
+ const flat = flattenResponsesTools(parsed);
878
+ if (flat.map) {
879
+ nsMap = flat.map;
880
+ bodyBuf = Buffer.from(JSON.stringify(flat.body));
881
+ say(` tools flattened for ${parsed.model}: ${flat.map.size} namespaced → function${flat.dropped.length ? `, dropped ${flat.dropped.join(', ')}` : ''}`);
882
+ }
883
+ }
884
+ } catch { /* not JSON */ }
885
+ }
846
886
  }
847
887
 
848
888
  // Retry of a body we answered seconds ago? Serve the cached completion —
@@ -958,6 +998,25 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
958
998
  result = await client.fetch(url, init);
959
999
  }
960
1000
  }
1001
+ // A PAID 400 `invalid_tools` on a Responses body we did not flatten:
1002
+ // this model is a function-tools-only door we had not learned yet.
1003
+ // Flatten, remember it, and buy the turn once more — one billed miss
1004
+ // instead of every turn for the rest of the session.
1005
+ if (isResponses && !nsMap && result.response?.status === 400) {
1006
+ let e400 = null;
1007
+ try { e400 = await result.response.clone().json(); } catch { e400 = null; }
1008
+ if (isInvalidToolsError(e400)) {
1009
+ let flat = null;
1010
+ try { flat = flattenResponsesTools(JSON.parse(String(init.body || '{}'))); } catch { flat = null; }
1011
+ if (flat?.map) {
1012
+ flattenLearned.add(outageKey(init));
1013
+ nsMap = flat.map;
1014
+ init = { ...init, body: JSON.stringify(flat.body) };
1015
+ say(` invalid_tools from ${outageKey(init)} — flattened ${flat.map.size} namespaced tools${flat.dropped.length ? `, dropped ${flat.dropped.join(', ')}` : ''}; retrying`);
1016
+ result = await client.fetch(url, init);
1017
+ }
1018
+ }
1019
+ }
961
1020
  const { response, paid, receipt, accept } = result;
962
1021
  if (paid && receipt) {
963
1022
  if (receipt.ok && typeof receipt.billedUsd === 'number') {
@@ -1038,6 +1097,16 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1038
1097
  res.end(JSON.stringify(data));
1039
1098
  return;
1040
1099
  }
1100
+ // Un-flatten a JSON Responses reply so Codex can route the calls.
1101
+ if (nsMap && data?.object === 'response') {
1102
+ restoreResponsesPayload(data, nsMap);
1103
+ const h = { 'content-type': 'application/json' };
1104
+ const settleHdr = response.headers.get('x-payment-response');
1105
+ if (settleHdr) h['x-payment-response'] = settleHdr;
1106
+ res.writeHead(200, h);
1107
+ res.end(JSON.stringify(data));
1108
+ return;
1109
+ }
1041
1110
  }
1042
1111
  // A STREAMED call is metered from the gateway's trailing SSE comment —
1043
1112
  // same figures the JSON path reads out of `data.x402`, same counters, so
@@ -1085,7 +1154,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1085
1154
  paymentError(res, req, init, paid ? copy.status : 402, msg);
1086
1155
  return;
1087
1156
  }
1088
- await relay(res, response, meterStreamed);
1157
+ await relay(res, response, meterStreamed, nsMap ? sseNamespaceRestorer(nsMap) : null);
1089
1158
  } catch (err) {
1090
1159
  if (err instanceof QuoteTooHighError) {
1091
1160
  log(err.message);
Binary file
@@ -101,8 +101,9 @@ export function whopFundBlurb(solana, evm) {
101
101
  'Funding options:',
102
102
  `Card: ${url}`,
103
103
  `At checkout, use this Solana wallet address to link your purchase: ${addr}`,
104
- `Solana deposit wallet (USDC / LEOS / KISS / TOKEN): ${addr}`,
105
- ...FUNDING_ASSETS.filter(a => ['LEOS', 'KISS', 'TOKEN'].includes(a.symbol)).map(a => `${a.symbol} mint (CA): ${a.mint}`),
104
+ `Solana deposit wallet (native USDC / cooked LEOS / cooked KISS / cooked TOKEN): ${addr}`,
105
+ ...FUNDING_ASSETS.filter(a => ['LEOS', 'KISS', 'TOKEN'].includes(a.symbol)).map(a => `Cooked ${a.symbol} mint (CA): ${a.mint} — https://eat.ag/cook/${a.mint}`),
106
+ 'Cook TOKEN, KISS, or LEOS at eat.ag first. Unwrapped versions are not accepted. USDC stays native.',
106
107
  'Send funds to your deposit wallet, not the mint address.',
107
108
  ...(base ? [`Base (USDC): ${base}`] : []),
108
109
  'Your balance refreshes automatically once the payment is confirmed.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.22",
3
+ "version": "0.51.24",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",