openzoo 0.48.46 → 0.48.48
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 +68 -6
- package/package.json +1 -1
package/lib/proxy.js
CHANGED
|
@@ -66,20 +66,43 @@ async function readBody(req) {
|
|
|
66
66
|
return Buffer.concat(chunks);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Pipe an upstream fetch Response to the client, unbuffered (SSE-safe).
|
|
70
|
-
|
|
69
|
+
/** Pipe an upstream fetch Response to the client, unbuffered (SSE-safe).
|
|
70
|
+
*
|
|
71
|
+
* `onReceipt` is called with the gateway's x402 block when it arrives. On a
|
|
72
|
+
* STREAMED call there is no JSON body to carry that block, so the gateway
|
|
73
|
+
* emits it as an SSE COMMENT (`: x402 {...}`) after the last frame — comments
|
|
74
|
+
* are discarded by every compliant client, so nothing downstream sees it, but
|
|
75
|
+
* without reading it here every spend and savings figure on the status line
|
|
76
|
+
* would silently read zero the moment real streaming was switched on.
|
|
77
|
+
*
|
|
78
|
+
* Sniffing NEVER delays a byte: each chunk is written to the client first and
|
|
79
|
+
* only then scanned. */
|
|
80
|
+
function relay(res, upstream, onReceipt) {
|
|
71
81
|
const headers = {};
|
|
72
82
|
upstream.headers.forEach((v, k) => {
|
|
73
83
|
if (!['transfer-encoding', 'connection', 'content-encoding', 'content-length'].includes(k)) headers[k] = v;
|
|
74
84
|
});
|
|
75
85
|
res.writeHead(upstream.status, headers);
|
|
76
86
|
if (!upstream.body) { res.end(); return Promise.resolve(); }
|
|
87
|
+
const sse = (upstream.headers.get('content-type') || '').includes('text/event-stream');
|
|
77
88
|
return new Promise((resolve) => {
|
|
78
89
|
const body = Readable.fromWeb(upstream.body);
|
|
79
90
|
body.on('error', () => res.destroy());
|
|
80
91
|
res.on('close', () => body.destroy());
|
|
81
92
|
body.on('end', resolve);
|
|
82
|
-
body.pipe(res);
|
|
93
|
+
if (!sse || typeof onReceipt !== 'function') { body.pipe(res); return; }
|
|
94
|
+
let pending = '';
|
|
95
|
+
body.on('data', (c) => {
|
|
96
|
+
res.write(c);
|
|
97
|
+
pending += c.toString('utf8');
|
|
98
|
+
const lines = pending.split('\n');
|
|
99
|
+
pending = lines.pop() ?? ''; // a comment can straddle two chunks
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (!line.startsWith(': x402 ')) continue;
|
|
102
|
+
try { onReceipt(JSON.parse(line.slice(7))); } catch { /* not our frame */ }
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
body.on('end', () => res.end());
|
|
83
106
|
});
|
|
84
107
|
}
|
|
85
108
|
|
|
@@ -840,6 +863,10 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
840
863
|
// answer back on the way out. See lib/anthropic.js.
|
|
841
864
|
let anthropicMode = false;
|
|
842
865
|
let anthropicModel = null;
|
|
866
|
+
// The CLIENT's streaming intent, kept separate from the body we send
|
|
867
|
+
// upstream. The Anthropic lane asks the gateway for a complete message (we
|
|
868
|
+
// can only translate a finished one) while still owing the caller SSE.
|
|
869
|
+
let clientWantsStream = false;
|
|
843
870
|
let responsesMode = false;
|
|
844
871
|
let responsesModel = null;
|
|
845
872
|
let responsesCustom = null; // names of freeform tools needing custom_tool_call on the way back
|
|
@@ -889,7 +916,24 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
889
916
|
try {
|
|
890
917
|
const inbound = JSON.parse(bodyBuf.toString('utf8'));
|
|
891
918
|
anthropicModel = inbound.model;
|
|
892
|
-
|
|
919
|
+
// ANTHROPIC CLIENTS CANNOT READ AN OPENAI STREAM.
|
|
920
|
+
//
|
|
921
|
+
// The gateway now streams for real, and `relay()` pipes those frames
|
|
922
|
+
// through untouched — which is correct for an OpenAI client and
|
|
923
|
+
// unreadable to Claude Code, which speaks the Anthropic SSE grammar
|
|
924
|
+
// (message_start / content_block_delta / message_stop). It surfaced as
|
|
925
|
+
// "API returned an empty or malformed response (HTTP 200)": a 200 whose
|
|
926
|
+
// body the client cannot parse.
|
|
927
|
+
//
|
|
928
|
+
// The translation we have (openAIToAnthropic + writeAnthropicSse) works
|
|
929
|
+
// on a COMPLETE message, so this lane asks the gateway not to stream and
|
|
930
|
+
// keeps the buffered translation. That costs Claude Code the
|
|
931
|
+
// time-to-first-byte win until an incremental OpenAI->Anthropic frame
|
|
932
|
+
// translator exists; a readable answer late beats an unreadable one now.
|
|
933
|
+
const converted = anthropicToOpenAI(inbound);
|
|
934
|
+
clientWantsStream = converted.stream === true || inbound.stream === true;
|
|
935
|
+
converted.stream = false;
|
|
936
|
+
bodyBuf = Buffer.from(JSON.stringify(converted));
|
|
893
937
|
anthropicMode = true;
|
|
894
938
|
req.url = '/v1/chat/completions';
|
|
895
939
|
url = `${config.apiBase}${req.url}`;
|
|
@@ -916,7 +960,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
916
960
|
}
|
|
917
961
|
try {
|
|
918
962
|
const parsed = JSON.parse(bodyBuf.toString('utf8'));
|
|
919
|
-
wantsStream = parsed?.stream === true;
|
|
963
|
+
wantsStream = parsed?.stream === true || clientWantsStream;
|
|
920
964
|
// REASONING MODELS SPEND max_tokens ON THINKING FIRST.
|
|
921
965
|
//
|
|
922
966
|
// The budget covers hidden reasoning AND the visible answer, so a
|
|
@@ -1197,7 +1241,25 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
1197
1241
|
return;
|
|
1198
1242
|
}
|
|
1199
1243
|
}
|
|
1200
|
-
|
|
1244
|
+
// A STREAMED call is metered from the gateway's trailing SSE comment —
|
|
1245
|
+
// same figures the JSON path reads out of `data.x402`, same counters, so
|
|
1246
|
+
// the status line does not care which transport served the answer.
|
|
1247
|
+
await relay(res, response, (x) => {
|
|
1248
|
+
if (paid || typeof x?.billedUsd !== 'number') return;
|
|
1249
|
+
sessionSpent += x.billedUsd;
|
|
1250
|
+
sessionCogs += typeof x.cogsUsd === 'number' ? x.cogsUsd : x.billedUsd / MARKUP;
|
|
1251
|
+
sessionDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
|
|
1252
|
+
if (typeof x.actualUsd === 'number' && x.actualUsd >= 0) { sessionActual += x.actualUsd; actualCalls += 1; }
|
|
1253
|
+
if (didSpill) {
|
|
1254
|
+
const lc = x.lecore || {};
|
|
1255
|
+
log(`spill priced (streamed): ${x.pricing} · basis ${x.counterfactualTokensUsed ?? '?'} tok vs sent ${lc.tokensBefore ?? '?'} -> ${lc.tokensAfter ?? '?'} · billed ${(x.billedUsd ?? 0).toFixed(5)} direct ${(x.directUsd ?? 0).toFixed(5)}`);
|
|
1256
|
+
spillSpend += x.billedUsd;
|
|
1257
|
+
spillDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
|
|
1258
|
+
}
|
|
1259
|
+
paidCalls += 1;
|
|
1260
|
+
if (viaTunnel) tunnelSpent += x.billedUsd;
|
|
1261
|
+
say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
|
|
1262
|
+
});
|
|
1201
1263
|
} catch (err) {
|
|
1202
1264
|
if (err instanceof QuoteTooHighError) {
|
|
1203
1265
|
log(err.message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.48.
|
|
3
|
+
"version": "0.48.48",
|
|
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",
|