openzoo 0.50.6 → 0.50.8
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 +6 -1
- package/lib/models.js +35 -1
- package/lib/proxy.js +46 -5
- package/package.json +1 -1
package/lib/launch.js
CHANGED
|
@@ -142,7 +142,12 @@ export function claudeZooEnv(baseEnv = process.env, { base, port } = {}) {
|
|
|
142
142
|
delete env.ANTHROPIC_API_KEY;
|
|
143
143
|
env.ANTHROPIC_BASE_URL = url;
|
|
144
144
|
env.ANTHROPIC_AUTH_TOKEN = baseEnv.ANTHROPIC_AUTH_TOKEN || 'sk-openzoo';
|
|
145
|
-
|
|
145
|
+
// HONOUR OPENZOO_CLAUDE_PATH_ONLY HERE TOO. resolveClaudeCli already skips
|
|
146
|
+
// the well-known bin dirs under this flag, but this function was prepending
|
|
147
|
+
// them onto PATH regardless — so the resolver then found `claude` through the
|
|
148
|
+
// doctored PATH and the flag did nothing. The two must agree, or "search only
|
|
149
|
+
// what I gave you" is not a thing a caller can actually ask for.
|
|
150
|
+
const extras = baseEnv.OPENZOO_CLAUDE_PATH_ONLY === '1' ? [] : claudeCodeBinDirs().filter((d) => {
|
|
146
151
|
try { return fs.existsSync(d); } catch { return false; }
|
|
147
152
|
});
|
|
148
153
|
const pathDirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
|
package/lib/models.js
CHANGED
|
@@ -332,9 +332,43 @@ export function augmentModelList(payload, { aliases: withAliases = true } = {})
|
|
|
332
332
|
};
|
|
333
333
|
})
|
|
334
334
|
: [];
|
|
335
|
+
|
|
336
|
+
// BARE-NAME TWINS, DERIVED FROM THE LIVE CATALOG.
|
|
337
|
+
//
|
|
338
|
+
// ALIAS_IDS is thirty hand-written strings against a ~435-model catalog, so
|
|
339
|
+
// it drifts: `/model deepseek-v4-pro` answered "Model not found" purely
|
|
340
|
+
// because nobody had typed that string into the list, while
|
|
341
|
+
// `deepseek/deepseek-v4-pro-0813` worked. Claude Code validates /model
|
|
342
|
+
// CLIENT-SIDE against this payload, so the shim's similarity rewriter never
|
|
343
|
+
// gets a chance — the id has to be in the list or the picker refuses it.
|
|
344
|
+
//
|
|
345
|
+
// For every `vendor/name` row, publish `name` too. That is the form people
|
|
346
|
+
// actually type, and it needs no maintenance as the catalog moves.
|
|
347
|
+
//
|
|
348
|
+
// FIRST WRITER WINS on a collision: two vendors can ship the same bare name
|
|
349
|
+
// and silently resolving to whichever sorted last would answer from the
|
|
350
|
+
// wrong model. A bare name that is already a real id is never shadowed.
|
|
351
|
+
const bare = [];
|
|
352
|
+
if (withAliases) {
|
|
353
|
+
const taken = new Set([...have, ...aliases.map((a) => a.id)]);
|
|
354
|
+
for (const m of data) {
|
|
355
|
+
const short = String(m.id).includes('/') ? String(m.id).split('/').pop() : null;
|
|
356
|
+
if (!short || taken.has(short)) continue;
|
|
357
|
+
taken.add(short);
|
|
358
|
+
bare.push({
|
|
359
|
+
id: short,
|
|
360
|
+
object: 'model',
|
|
361
|
+
owned_by: 'openzoo-alias',
|
|
362
|
+
...(m.context_length ? { context_length: m.context_length, context_window: m.context_window ?? m.context_length } : {}),
|
|
363
|
+
...(m.pricing ? { pricing: m.pricing } : {}),
|
|
364
|
+
served_by: m.id,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
335
369
|
// Auto is no longer published: routing lives on the backend, and a shim
|
|
336
370
|
// that advertises openzoo/auto would have to route it.
|
|
337
|
-
return { ...payload, object: payload?.object || 'list', data: [...data, ...aliases] };
|
|
371
|
+
return { ...payload, object: payload?.object || 'list', data: [...data, ...aliases, ...bare] };
|
|
338
372
|
}
|
|
339
373
|
|
|
340
374
|
/**
|
package/lib/proxy.js
CHANGED
|
@@ -11,7 +11,23 @@ import { PayClient, QuoteTooHighError, UnderfundedError } from './pay.js';
|
|
|
11
11
|
import { tokenBalance } from './x402.js';
|
|
12
12
|
import { evmTokenBalance } from './evm.js';
|
|
13
13
|
import { autoContext } from './autobind.js';
|
|
14
|
-
import { modelsListForRequest, isHarnessAliasId } from './models.js';
|
|
14
|
+
import { modelsListForRequest, isHarnessAliasId, resolveModel, quoteableRows } from './models.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Quoteable catalog ids, cached 5 minutes, for the fuzzy /v1/models/<id> probe.
|
|
18
|
+
* Free read, but harnesses probe on every /model keystroke confirmation —
|
|
19
|
+
* without a cache each probe is an upstream round trip.
|
|
20
|
+
*/
|
|
21
|
+
let catalogIdsMemo = { at: 0, ids: [] };
|
|
22
|
+
async function catalogIdsCached(modelsUrl, headers) {
|
|
23
|
+
if (Date.now() - catalogIdsMemo.at < 5 * 60_000 && catalogIdsMemo.ids.length) return catalogIdsMemo.ids;
|
|
24
|
+
const r = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(8000) });
|
|
25
|
+
if (!r.ok) throw new Error(`catalog ${r.status}`);
|
|
26
|
+
const payload = await r.json();
|
|
27
|
+
const ids = quoteableRows(payload?.data).map((m) => m.id);
|
|
28
|
+
if (ids.length) catalogIdsMemo = { at: Date.now(), ids };
|
|
29
|
+
return ids;
|
|
30
|
+
}
|
|
15
31
|
import { withNamespace } from './namespace.js';
|
|
16
32
|
import { loadSessionSpend, saveSessionSpend } from './session.js';
|
|
17
33
|
import { creditBalance, quotedPrices } from './info.js';
|
|
@@ -618,10 +634,35 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
618
634
|
return;
|
|
619
635
|
}
|
|
620
636
|
const probe = req.method === 'GET' && /^\/v1\/models\/(.+)$/.exec(path);
|
|
621
|
-
if (probe
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
637
|
+
if (probe) {
|
|
638
|
+
const wanted = decodeURIComponent(probe[1]);
|
|
639
|
+
if (isHarnessAliasId(wanted)) {
|
|
640
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
641
|
+
res.end(JSON.stringify({ id: wanted, object: 'model', owned_by: 'openzoo-alias' }));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
// FUZZY, LIKE THE REST OF THE PIPELINE. The gateway already
|
|
645
|
+
// similarity-rewrites whatever model id lands in a POST body — but a
|
|
646
|
+
// harness never gets that far, because it validates the id CLIENT-SIDE
|
|
647
|
+
// with this exact probe first. Answering 200 only for a hand-typed
|
|
648
|
+
// alias list meant `/model deepseek pro` and `/model
|
|
649
|
+
// deepseek-ai/deepseek-v4-pro` were both refused at the door while the
|
|
650
|
+
// backend would have served either happily. The door must be exactly as
|
|
651
|
+
// forgiving as the room behind it.
|
|
652
|
+
try {
|
|
653
|
+
const ids = await catalogIdsCached(url.replace(/\/v1\/models\/.*$/, '/v1/models'), init.headers);
|
|
654
|
+
if (ids.includes(wanted)) {
|
|
655
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
656
|
+
res.end(JSON.stringify({ id: wanted, object: 'model' }));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const resolved = resolveModel(wanted, ids);
|
|
660
|
+
if (resolved) {
|
|
661
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
662
|
+
res.end(JSON.stringify({ id: wanted, object: 'model', owned_by: 'openzoo-alias', served_by: resolved }));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
} catch { /* catalog unreachable — fall through to the paid path below */ }
|
|
625
666
|
}
|
|
626
667
|
|
|
627
668
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.50.
|
|
3
|
+
"version": "0.50.8",
|
|
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",
|