openzoo 0.31.0 → 0.32.1
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/cursorbackend.js +80 -0
- package/lib/hosts.js +17 -3
- package/package.json +1 -1
package/lib/cursorbackend.js
CHANGED
|
@@ -118,6 +118,80 @@ function stripeProfile() {
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Pull the user's prompt out of a StreamUnifiedChatRequest without the full
|
|
123
|
+
* schema: walk the protobuf, collect plausible UTF-8 string fields, take the
|
|
124
|
+
* longest natural-language one (the new user turn). The captured .bin refines it.
|
|
125
|
+
*/
|
|
126
|
+
function extractPromptText(buf) {
|
|
127
|
+
const strings = [];
|
|
128
|
+
const walk = (b, depth) => {
|
|
129
|
+
let i = 0;
|
|
130
|
+
while (i < b.length) {
|
|
131
|
+
let key = 0, shift = 0, byte;
|
|
132
|
+
do { if (i >= b.length) return; byte = b[i++]; key |= (byte & 0x7f) << shift; shift += 7; } while (byte & 0x80);
|
|
133
|
+
const wire = key & 7;
|
|
134
|
+
if (wire === 2) {
|
|
135
|
+
let len = 0; shift = 0;
|
|
136
|
+
do { if (i >= b.length) return; byte = b[i++]; len |= (byte & 0x7f) << shift; shift += 7; } while (byte & 0x80);
|
|
137
|
+
const sub = b.subarray(i, i + len); i += len;
|
|
138
|
+
const txt = sub.toString('utf8');
|
|
139
|
+
if (len > 1 && /[a-zA-Z]/.test(txt) && !/[\x00-\x08\x0e-\x1f]/.test(txt)) strings.push(txt);
|
|
140
|
+
else if (depth < 6) walk(sub, depth + 1);
|
|
141
|
+
} else if (wire === 0) { while (i < b.length && (b[i++] & 0x80)) { /* skip */ } }
|
|
142
|
+
else if (wire === 5) { i += 4; } else if (wire === 1) { i += 8; } else { return; }
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
try { walk(buf, 0); } catch { /* best effort */ }
|
|
146
|
+
const cand = strings.filter((x) => /\s/.test(x.trim()) || x.length > 12);
|
|
147
|
+
return ((cand.length ? cand : strings).sort((a, b) => b.length - a.length)[0] || '').trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** StreamUnifiedChatResponse{1: text}. */
|
|
151
|
+
function encodeChatResponse(text) {
|
|
152
|
+
const t = Buffer.from(text, 'utf8');
|
|
153
|
+
const head = [0x0a];
|
|
154
|
+
let n = t.length; do { let x = n & 0x7f; n = Math.floor(n / 128); if (n) x |= 0x80; head.push(x); } while (n);
|
|
155
|
+
return Buffer.concat([Buffer.from(head), t]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* ROUTE AUTO THROUGH THE ZOO. Cursor's chat inference is ChatService/
|
|
160
|
+
* StreamUnifiedChat to the agent host (now impersonated). Parse the prompt, pay
|
|
161
|
+
* the zoo via the local proxy, stream the answer back as StreamUnifiedChatResponse
|
|
162
|
+
* frames. Legitimate: Auto is free-allowed; we redirect the operator's OWN
|
|
163
|
+
* inference to their OWN paid proxy.
|
|
164
|
+
*/
|
|
165
|
+
async function handleStreamChat(req, res, body, log) {
|
|
166
|
+
const ct = String(req.headers['content-type'] || '');
|
|
167
|
+
const isGrpcWeb = ct.includes('grpc-web');
|
|
168
|
+
const prompt = extractPromptText(body) || 'hello';
|
|
169
|
+
log(`cursor-backend: >> StreamUnifiedChat prompt: ${JSON.stringify(prompt.slice(0, 80))}`);
|
|
170
|
+
let text = '';
|
|
171
|
+
try {
|
|
172
|
+
const zoo = await fetch('http://127.0.0.1:8402/v1/chat/completions', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'content-type': 'application/json', authorization: 'Bearer sk-openzoo' },
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
model: process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
|
|
177
|
+
messages: [{ role: 'user', content: prompt }],
|
|
178
|
+
max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 2048),
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
const data = await zoo.json();
|
|
182
|
+
text = data.choices?.[0]?.message?.content || '(no content)';
|
|
183
|
+
log(`cursor-backend: << zoo replied ${text.length} chars (paid x402)`);
|
|
184
|
+
} catch (e) { text = `openzoo error: ${e.message}`; log(`cursor-backend: zoo call failed: ${e.message}`); }
|
|
185
|
+
|
|
186
|
+
res.writeHead(200, {
|
|
187
|
+
'content-type': isGrpcWeb ? 'application/grpc-web+proto' : 'application/connect+proto',
|
|
188
|
+
'grpc-status': '0', ...CORS,
|
|
189
|
+
});
|
|
190
|
+
res.write(envelope(encodeChatResponse(text)));
|
|
191
|
+
if (isGrpcWeb) { res.end(grpcWebTrailer()); }
|
|
192
|
+
else { const end = Buffer.from('{}'); const h = Buffer.alloc(5); h.writeUInt8(0x02, 0); h.writeUInt32BE(end.length, 1); res.end(Buffer.concat([h, end])); }
|
|
193
|
+
}
|
|
194
|
+
|
|
121
195
|
/**
|
|
122
196
|
* Answer one Connect/gRPC-web call. `method` is the trailing method name,
|
|
123
197
|
* `models` the catalog to publish. Non-catalog methods get an empty-OK body.
|
|
@@ -199,6 +273,12 @@ export function startCursorBackend({ port = 8443, models, log = () => {} } = {})
|
|
|
199
273
|
const ct = req.headers['content-type'] || '?';
|
|
200
274
|
const body = await readBody(req);
|
|
201
275
|
log(`cursor-backend: #${conns} ${req.method} ${full} ct=${ct} body=${body.length}b`);
|
|
276
|
+
// CAPTURE the chat inference request so its schema can be decoded from
|
|
277
|
+
// REAL bytes (Auto's StreamUnifiedChat). Written once; inspect then build.
|
|
278
|
+
if (/StreamUnifiedChat/.test(full)) {
|
|
279
|
+
await handleStreamChat(req, res, body, log);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
202
282
|
try {
|
|
203
283
|
respond(req, res, method, models);
|
|
204
284
|
const populated = ['GetPlanInfo', 'GetMe', 'GetDefaultModel', 'IsOnNewPricing'];
|
package/lib/hosts.js
CHANGED
|
@@ -36,7 +36,16 @@ const BACKUP = `${HOSTS}.openzoo-backup`;
|
|
|
36
36
|
const MARK = '# openzoo: force the editor onto the local proxy';
|
|
37
37
|
|
|
38
38
|
/** Hosts the editor uses for model sync + its own inference proxy. */
|
|
39
|
-
export const BACKEND_HOSTS = [
|
|
39
|
+
export const BACKEND_HOSTS = [
|
|
40
|
+
'api2.cursor.sh',
|
|
41
|
+
// Chat inference (StreamUnifiedChat) — agent.api5 (privacy) / agentn.api5
|
|
42
|
+
// (non-privacy) and the gcpp REGIONAL variants; Auto uses one per region +
|
|
43
|
+
// privacy mode, so all must point at us or the chat escapes.
|
|
44
|
+
'agent.api5.cursor.sh', 'agentn.api5.cursor.sh',
|
|
45
|
+
'agentn-gcpp-uswest.api5.cursor.sh',
|
|
46
|
+
'agentn-gcpp-eucentral.api5.cursor.sh',
|
|
47
|
+
'agentn-gcpp-apsoutheast.api5.cursor.sh',
|
|
48
|
+
];
|
|
40
49
|
|
|
41
50
|
export function isBlocked() {
|
|
42
51
|
try {
|
|
@@ -67,8 +76,13 @@ function flushDnsCmd() {
|
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export function blockBackend() {
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
// Add only the hosts NOT already present, so a prior api2-only block still gets
|
|
80
|
+
// the agent/chat hosts appended (early-returning on isBlocked left them out).
|
|
81
|
+
let current = '';
|
|
82
|
+
try { current = fs.readFileSync(HOSTS, 'utf8'); } catch { /* new */ }
|
|
83
|
+
const missing = BACKEND_HOSTS.filter((h) => !new RegExp(`^\\s*127\\.0\\.0\\.1\\s+${h.replace(/\./g, '\\.')}\\b`, 'm').test(current));
|
|
84
|
+
if (!missing.length) return { already: true };
|
|
85
|
+
const entries = missing.map((h) => `127.0.0.1 ${h}`).join('\\n');
|
|
72
86
|
console.log('');
|
|
73
87
|
console.log('blocking the editor\'s backend so it cannot re-sync over your model list.');
|
|
74
88
|
console.log(` hosts : ${BACKEND_HOSTS.join(', ')} -> 127.0.0.1`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.1",
|
|
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",
|