openzoo 0.36.0 β 0.37.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/gui.html +15 -2
- package/lib/mcp.js +126 -0
- package/package.json +1 -1
package/lib/gui.html
CHANGED
|
@@ -172,10 +172,23 @@ async function send() {
|
|
|
172
172
|
if (state.ctx) headers["x-hrr-context"] = state.ctx;
|
|
173
173
|
const r = await fetch("/v1/chat/completions", {
|
|
174
174
|
method: "POST", headers,
|
|
175
|
-
|
|
175
|
+
// reasoning models bill their thinking inside max_tokens β 2048 bought
|
|
176
|
+
// a whole essay of invisible reasoning and zero visible words (measured
|
|
177
|
+
// live: deepseek, finish_reason "length", empty content, $0.0172)
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
model: $("model").value,
|
|
180
|
+
messages: state.messages,
|
|
181
|
+
max_tokens: /deepseek|grok|thinking|fable|sonnet-5|-r1|reason/i.test($("model").value) ? 16384 : 4096,
|
|
182
|
+
}),
|
|
176
183
|
});
|
|
177
184
|
const d = await r.json();
|
|
178
|
-
const
|
|
185
|
+
const ch = d.choices?.[0];
|
|
186
|
+
let content = ch?.message?.content || "";
|
|
187
|
+
if (!content && ch?.finish_reason === "length") {
|
|
188
|
+
content = "π¦₯ the animal thought so hard it ran out of room before saying anything. try again β the cage just got bigger.";
|
|
189
|
+
} else if (!content) {
|
|
190
|
+
content = d.error?.message || "π¦ the zoo returned something unusual: " + JSON.stringify(d).slice(0, 200);
|
|
191
|
+
}
|
|
179
192
|
thinking.textContent = content;
|
|
180
193
|
state.messages.push({ role: "assistant", content });
|
|
181
194
|
const x = d.x402 || {};
|
package/lib/mcp.js
CHANGED
|
@@ -41,6 +41,17 @@ export function buildMcpServer() {
|
|
|
41
41
|
'Never search the machine for API keys or endpoints β there are none to find. zoo_wallet shows funding',
|
|
42
42
|
'state, zoo_models lists what is served with live pricing.',
|
|
43
43
|
'',
|
|
44
|
+
// Agents previously probed zoo_ask, got text back, and concluded the zoo
|
|
45
|
+
// could not do media at all β the text upstream serves NO video models.
|
|
46
|
+
'IMAGE AND VIDEO ARE SERVED, on their own tools β zoo_ask cannot emit them.',
|
|
47
|
+
' zoo_image synchronous, returns hosted URLs (FLUX, Imagen, Seedream, Qwen-Image, gpt-imageβ¦)',
|
|
48
|
+
' zoo_video asynchronous, returns a job id (Sora 2, Veo 3.1, Kling, Seedance, Wan, pixverseβ¦)',
|
|
49
|
+
' zoo_video_result poll the job id β free, the render was paid for at submit',
|
|
50
|
+
'In zoo_models these carry kind="image" / kind="video" and are priced per image or per clip, not per token.',
|
|
51
|
+
'leCore does NOT participate in a render: a diffusion prompt never reaches the spill threshold, so do not',
|
|
52
|
+
'claim holographic memory improved the picture. To ground a render in your own material, use zoo_ask to',
|
|
53
|
+
'WRITE the prompt from a bound corpus, then pass that prompt to zoo_image / zoo_video.',
|
|
54
|
+
'',
|
|
44
55
|
// ROUTING, STATED UNCONDITIONALLY. An agent whose OWN inference is not
|
|
45
56
|
// routed through the proxy never receives lib/brief.js, so it answers
|
|
46
57
|
// "I have no visibility into the routing layer" while this very server
|
|
@@ -168,6 +179,121 @@ export function buildMcpServer() {
|
|
|
168
179
|
}
|
|
169
180
|
});
|
|
170
181
|
|
|
182
|
+
/* ------------------------------------------------------------------ *
|
|
183
|
+
* MEDIA β image and video.
|
|
184
|
+
*
|
|
185
|
+
* These exist because the honest answer to "can openzoo make me a picture"
|
|
186
|
+
* used to be no: zoo_ask rides /v1/chat/completions, whose upstream is
|
|
187
|
+
* text-only (of OpenRouter's 413 models, ZERO emit video). Agents worked
|
|
188
|
+
* that out the hard way and concluded the zoo was a dead end for media.
|
|
189
|
+
*
|
|
190
|
+
* DO NOT route these through leCore. A diffusion prompt is a few hundred
|
|
191
|
+
* characters and the spill threshold is 8,000 tokens, so binding would
|
|
192
|
+
* never engage β and implying otherwise would sell holographic memory as
|
|
193
|
+
* doing something it demonstrably does not do here. leCore's real use for
|
|
194
|
+
* media is upstream: ask zoo_ask (grounded in a bound corpus) to WRITE the
|
|
195
|
+
* prompt, then pass that prompt here.
|
|
196
|
+
* ------------------------------------------------------------------ */
|
|
197
|
+
server.registerTool('zoo_image', {
|
|
198
|
+
description:
|
|
199
|
+
'Generate an IMAGE through openzoo.fun, paid per call from the local burner wallet via x402 β no '
|
|
200
|
+
+ 'Together/OpenAI account or API key needed. Returns hosted image URLs plus the payment receipt. '
|
|
201
|
+
+ 'Pick a model with zoo_models (image models carry kind="image"); the default is a fast, cheap FLUX. '
|
|
202
|
+
+ 'This is a RENDER call, not a reasoning call: the prompt is used verbatim, no corpus is bound, and '
|
|
203
|
+
+ 'leCore holographic memory does not participate. To ground a prompt in your own material, call '
|
|
204
|
+
+ 'zoo_ask first to write it, then pass the result here.',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
prompt: z.string().describe('What to draw. Used verbatim by the diffusion model.'),
|
|
207
|
+
model: z.string().optional().describe('Image model id from zoo_models (kind="image"). Defaults to a fast cheap FLUX.'),
|
|
208
|
+
width: z.number().int().positive().optional().describe('Pixels. Default 1024. Priced per megapixel on some models.'),
|
|
209
|
+
height: z.number().int().positive().optional().describe('Pixels. Default 1024.'),
|
|
210
|
+
n: z.number().int().positive().optional().describe('How many images. Default 1. Multiplies the price.'),
|
|
211
|
+
},
|
|
212
|
+
}, async ({ prompt, model, width, height, n }) => {
|
|
213
|
+
try {
|
|
214
|
+
const { response, receipt } = await client.fetch(`${config.apiBase}/v1/images/generations`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: { 'content-type': 'application/json' },
|
|
217
|
+
body: JSON.stringify({ prompt, ...(model ? { model } : {}), ...(width ? { width } : {}),
|
|
218
|
+
...(height ? { height } : {}), ...(n ? { n } : {}) }),
|
|
219
|
+
});
|
|
220
|
+
const data = await response.json();
|
|
221
|
+
if (!response.ok) throw new Error(`zoo returned HTTP ${response.status}: ${JSON.stringify(data).slice(0, 300)}`);
|
|
222
|
+
return text({
|
|
223
|
+
images: (data?.data ?? []).map((d) => d.url ?? (d.b64_json ? '<base64 inline>' : null)).filter(Boolean),
|
|
224
|
+
model: data?.model ?? model ?? null,
|
|
225
|
+
receipt: receipt ? { billedUsd: receipt.billedUsd, rail: receipt.rail, line: receipt.line } : 'free (no 402 issued)',
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
return text({ error: err?.message || String(err), hint: railFundingHint() });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
server.registerTool('zoo_video', {
|
|
233
|
+
description:
|
|
234
|
+
'Generate a VIDEO through openzoo.fun, paid per call from the local burner wallet via x402. '
|
|
235
|
+
+ 'ASYNCHRONOUS: this returns a job id immediately β poll zoo_video_result with that id until status '
|
|
236
|
+
+ 'is "completed", then read outputs.video_url. Rendering typically takes tens of seconds to minutes. '
|
|
237
|
+
+ 'Payment happens HERE, at submit; polling is free. Pick a model with zoo_models (kind="video"): '
|
|
238
|
+
+ 'Sora 2, Veo 3.1, Kling, Seedance, Wan and others are served. Video is materially more expensive '
|
|
239
|
+
+ 'than image β check the quote in the receipt.',
|
|
240
|
+
inputSchema: {
|
|
241
|
+
prompt: z.string().describe('What the clip should show.'),
|
|
242
|
+
model: z.string().optional().describe('Video model id from zoo_models (kind="video").'),
|
|
243
|
+
seconds: z.number().int().positive().optional().describe('Clip length in seconds. Default 5. Longer costs proportionally more.'),
|
|
244
|
+
resolution: z.enum(['720P', '1080P']).optional().describe('720P (default) or 1080P.'),
|
|
245
|
+
generate_audio: z.boolean().optional().describe('Ask the model for audio too, where supported.'),
|
|
246
|
+
negative_prompt: z.string().optional().describe('What to avoid.'),
|
|
247
|
+
},
|
|
248
|
+
}, async ({ prompt, model, seconds, resolution, generate_audio, negative_prompt }) => {
|
|
249
|
+
try {
|
|
250
|
+
const { response, receipt } = await client.fetch(`${config.apiBase}/v1/videos/generations`, {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: { 'content-type': 'application/json' },
|
|
253
|
+
body: JSON.stringify({ prompt, ...(model ? { model } : {}), ...(seconds ? { seconds } : {}),
|
|
254
|
+
...(resolution ? { resolution } : {}), ...(generate_audio !== undefined ? { generate_audio } : {}),
|
|
255
|
+
...(negative_prompt ? { negative_prompt } : {}) }),
|
|
256
|
+
});
|
|
257
|
+
const data = await response.json();
|
|
258
|
+
if (!response.ok) throw new Error(`zoo returned HTTP ${response.status}: ${JSON.stringify(data).slice(0, 300)}`);
|
|
259
|
+
return text({
|
|
260
|
+
jobId: data?.id ?? null,
|
|
261
|
+
status: data?.status ?? null,
|
|
262
|
+
next: data?.id ? `Call zoo_video_result with id="${data.id}" until status is "completed".` : null,
|
|
263
|
+
receipt: receipt ? { billedUsd: receipt.billedUsd, rail: receipt.rail, line: receipt.line } : 'free (no 402 issued)',
|
|
264
|
+
});
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return text({ error: err?.message || String(err), hint: railFundingHint() });
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
server.registerTool('zoo_video_result', {
|
|
271
|
+
description:
|
|
272
|
+
'Fetch the state of a zoo_video job. FREE β the render was already paid for at submit, so poll as often '
|
|
273
|
+
+ 'as you like. status is "in_progress", "completed" or "failed"; on completion outputs.video_url holds '
|
|
274
|
+
+ 'the clip and outputs.cost the real upstream charge. A failed job reports why in error.message β note '
|
|
275
|
+
+ 'that a bad parameter fails the job AFTER submit, so a failure here is not a payment failure.',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
id: z.string().describe('The jobId returned by zoo_video.'),
|
|
278
|
+
},
|
|
279
|
+
}, async ({ id }) => {
|
|
280
|
+
try {
|
|
281
|
+
const r = await fetch(`${config.apiBase}/v1/videos/${encodeURIComponent(id)}`, {
|
|
282
|
+
headers: withNamespace({}),
|
|
283
|
+
});
|
|
284
|
+
const data = await r.json();
|
|
285
|
+
if (!r.ok) throw new Error(`zoo returned HTTP ${r.status}: ${JSON.stringify(data).slice(0, 300)}`);
|
|
286
|
+
return text({
|
|
287
|
+
status: data?.status ?? null,
|
|
288
|
+
videoUrl: data?.outputs?.video_url ?? null,
|
|
289
|
+
upstreamCost: data?.outputs?.cost ?? null,
|
|
290
|
+
error: data?.error ?? undefined,
|
|
291
|
+
});
|
|
292
|
+
} catch (err) {
|
|
293
|
+
return text({ error: err?.message || String(err) });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
171
297
|
// WHY A DEDICATED TOOL: MCP server `instructions` are advisory and several
|
|
172
298
|
// clients (Cursor among them) never surface them to the model β so an agent
|
|
173
299
|
// with this server attached still answers "I have no visibility into the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
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",
|