@mmmbuto/nexuscrew 0.9.74 → 0.9.75
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/CHANGELOG.md +10 -0
- package/frontend/dist/assets/{index-C_z6r985.css → index-B9GW2LHm.css} +1 -1
- package/frontend/dist/assets/index-DMfpKI3n.js +169 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +151 -2
- package/lib/fleet/managed.js +62 -8
- package/lib/fleet/model-discovery.js +815 -0
- package/lib/fleet/routes.js +18 -1
- package/package.json +2 -2
- package/frontend/dist/assets/index-DRXfJssx.js +0 -169
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DMfpKI3n.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B9GW2LHm.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.9.
|
|
1
|
+
{"version":"0.9.75"}
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -41,8 +41,10 @@ const {
|
|
|
41
41
|
CATALOG, CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient,
|
|
42
42
|
credential: resolveProfileCredential, extraModelsFrom, knownMcpServerNames, profileFor,
|
|
43
43
|
normalizeManagedSpec, codexSurfaceNames,
|
|
44
|
+
engineModelDiscoveryPlan, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
|
|
44
45
|
} = require('./managed.js');
|
|
45
46
|
const { probeModel, probeCustomEndpoint } = require('./model-probe.js');
|
|
47
|
+
const { createModelDiscovery, DISCOVERY_PROPOSAL_CAP } = require('./model-discovery.js');
|
|
46
48
|
const { sharedProbe: defaultEndpointProbe } = require('./endpoint-probe.js');
|
|
47
49
|
const { validEnvKey } = require('./env-key.js');
|
|
48
50
|
const { isReservedLiveName } = require('../live-host/reserved.js');
|
|
@@ -1648,7 +1650,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1648
1650
|
}
|
|
1649
1651
|
|
|
1650
1652
|
function capabilities() {
|
|
1651
|
-
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'import', 'restore', 'schema', 'definitions', 'credentials', 'model-test', 'edit-model'];
|
|
1653
|
+
return ['status', 'up', 'down', 'restart', 'engine', 'boot', 'define', 'edit', 'remove', 'import', 'restore', 'schema', 'definitions', 'credentials', 'model-test', 'model-discovery', 'edit-model'];
|
|
1652
1654
|
}
|
|
1653
1655
|
|
|
1654
1656
|
// Un engine del documento con un `baseUrl` dichiarato a mano: indirizzo e
|
|
@@ -1714,6 +1716,153 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1714
1716
|
return { engine: profile.id, model: wanted, ...out };
|
|
1715
1717
|
}
|
|
1716
1718
|
|
|
1719
|
+
// Model discovery for the engine editor: the «you can use these» list with
|
|
1720
|
+
// per-field provenance (user > api > public catalog > backup). The engine's
|
|
1721
|
+
// endpoint and credential are resolved on the SAME road the launcher and
|
|
1722
|
+
// testModel use, live in memory only, and never enter the response: the
|
|
1723
|
+
// return shape is curated field by field below and the discovery module's
|
|
1724
|
+
// errors are fixed strings by construction (no key, no URL, ever).
|
|
1725
|
+
const engineModelDiscovery = createModelDiscovery();
|
|
1726
|
+
function backupModelsFor(profile) {
|
|
1727
|
+
if (!profile) return [];
|
|
1728
|
+
// ollama-cloud carries real measured descriptors: context window and
|
|
1729
|
+
// capabilities from the wired catalog, not bare ids.
|
|
1730
|
+
if (profile.provider === 'ollama-cloud') {
|
|
1731
|
+
return ollamaCloudCatalogModels().map((m) => ({
|
|
1732
|
+
id: m.id,
|
|
1733
|
+
...(Number.isFinite(m.contextWindow) ? { contextWindow: m.contextWindow } : {}),
|
|
1734
|
+
...(Array.isArray(m.input) && m.input.includes('image') ? { vision: true } : {}),
|
|
1735
|
+
...(m.reasoning === true ? { reasoning: true } : {}),
|
|
1736
|
+
}));
|
|
1737
|
+
}
|
|
1738
|
+
return (Array.isArray(profile.models) ? profile.models : [])
|
|
1739
|
+
.filter((id) => typeof id === 'string' && id).map((id) => ({ id }));
|
|
1740
|
+
}
|
|
1741
|
+
// Internal-catalog PROPOSALS for the missing-data dialog (lot 3b): what the
|
|
1742
|
+
// wired database knows about a model id, for fields the discovery layers
|
|
1743
|
+
// could NOT fill. These are PROPOSALS only — they never join the merge, so
|
|
1744
|
+
// precedence and discovery stay untouched; the dialog shows them pre-filled
|
|
1745
|
+
// and whatever the operator confirms becomes a user value in fleet.json.
|
|
1746
|
+
// Lookup mirrors the wired ollama rule: exact key first, then the base name
|
|
1747
|
+
// without the ':tag' suffix ('deepseek-v4-flash:0731' -> 'deepseek-v4-flash').
|
|
1748
|
+
function internalModelIndex(profile) {
|
|
1749
|
+
const index = new Map();
|
|
1750
|
+
const add = (id, values) => {
|
|
1751
|
+
if (!id || !values) return;
|
|
1752
|
+
const known = index.get(id) || {};
|
|
1753
|
+
for (const field of ['contextWindow', 'maxTokens']) {
|
|
1754
|
+
if (values[field] !== undefined) known[field] = values[field];
|
|
1755
|
+
}
|
|
1756
|
+
if (Object.keys(known).length) index.set(id, known);
|
|
1757
|
+
};
|
|
1758
|
+
if (profile?.provider === 'ollama-cloud') {
|
|
1759
|
+
for (const m of ollamaCloudCatalogModels()) add(m.id, { contextWindow: m.contextWindow });
|
|
1760
|
+
}
|
|
1761
|
+
if (profile?.provider === 'alibaba-token-plan') {
|
|
1762
|
+
// ALIBABA_PI_MODELS carries the full descriptors (context, output) of
|
|
1763
|
+
// the wired database; the profile's own `models` list is bare ids.
|
|
1764
|
+
for (const m of ALIBABA_PI_MODELS) add(m.id, { contextWindow: m.contextWindow, maxTokens: m.maxTokens });
|
|
1765
|
+
}
|
|
1766
|
+
return index;
|
|
1767
|
+
}
|
|
1768
|
+
function internalValueFor(index, id) {
|
|
1769
|
+
if (!index.size || typeof id !== 'string') return null;
|
|
1770
|
+
if (index.has(id)) return index.get(id);
|
|
1771
|
+
const base = id.replace(/:[^:/]+$/, '');
|
|
1772
|
+
return base !== id && index.has(base) ? index.get(base) : null;
|
|
1773
|
+
}
|
|
1774
|
+
async function discoverEngineModels(engineId, opts = {}) {
|
|
1775
|
+
const id = String(engineId || '');
|
|
1776
|
+
if (!id || id.length > 64 || !/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
1777
|
+
const e = new Error('engine non valido'); e.status = 400; throw e;
|
|
1778
|
+
}
|
|
1779
|
+
const defs = reloadDefs();
|
|
1780
|
+
const declared = findEngine(defs, id);
|
|
1781
|
+
let profile = null;
|
|
1782
|
+
let spec = null;
|
|
1783
|
+
if (declared && declared.managed) {
|
|
1784
|
+
spec = normalizeManagedSpec(declared.managed, { extraModels: extraModelsFrom(defs), engineId: declared.id });
|
|
1785
|
+
profile = spec ? profileFor(spec.client, spec.provider, spec.credentialProfile || '') : null;
|
|
1786
|
+
}
|
|
1787
|
+
if (!profile) {
|
|
1788
|
+
// Not declared (or not resolvable): the catalog profile alone still
|
|
1789
|
+
// knows client, provider, endpoint and credential key.
|
|
1790
|
+
profile = CATALOG.find((p) => p.id === id) || null;
|
|
1791
|
+
spec = profile ? { client: profile.client, provider: profile.provider, ...(profile.credentialProfile ? { credentialProfile: profile.credentialProfile } : {}) } : null;
|
|
1792
|
+
}
|
|
1793
|
+
if (!profile) { const e = new Error(`engine sconosciuto: ${id}`); e.status = 404; throw e; }
|
|
1794
|
+
const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
|
|
1795
|
+
if (!plan) {
|
|
1796
|
+
// No HTTP listing to interrogate (login/native flows, local providers
|
|
1797
|
+
// without a base URL): an honest empty answer, never an error, so the
|
|
1798
|
+
// editor stays usable. The UI hides the section on `supported: false`.
|
|
1799
|
+
return { engine: id, supported: false, source: 'unsupported', models: [] };
|
|
1800
|
+
}
|
|
1801
|
+
// User layer: models declared in fleet.json for this engine (or its
|
|
1802
|
+
// profile) — the values the operator chose, always on top and never
|
|
1803
|
+
// rewritten by discovery.
|
|
1804
|
+
const extraModels = extraModelsFrom(defs);
|
|
1805
|
+
const userModels = [];
|
|
1806
|
+
const seenUser = new Set();
|
|
1807
|
+
for (const key of (id !== profile.id ? [id, profile.id] : [id])) {
|
|
1808
|
+
for (const m of declaredModelsFor(extraModels, key)) {
|
|
1809
|
+
if (seenUser.has(m.id)) continue;
|
|
1810
|
+
seenUser.add(m.id);
|
|
1811
|
+
userModels.push({
|
|
1812
|
+
id: m.id,
|
|
1813
|
+
...(Number.isFinite(m.contextWindow) ? { contextWindow: m.contextWindow } : {}),
|
|
1814
|
+
...(Number.isFinite(m.maxTokens) ? { maxTokens: m.maxTokens } : {}),
|
|
1815
|
+
// definitions.js parses `reasoning` as a boolean: `false` is an
|
|
1816
|
+
// explicit operator answer, not an absence. Keep it exactly like
|
|
1817
|
+
// that, or a provider's `true` would override the user layer here
|
|
1818
|
+
// (the merge itself already handles booleans correctly).
|
|
1819
|
+
...(typeof m.reasoning === 'boolean' ? { reasoning: m.reasoning } : {}),
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const backupModels = backupModelsFor(profile);
|
|
1824
|
+
const wanted = [...new Set([...userModels, ...backupModels].map((m) => m.id))].slice(0, 32);
|
|
1825
|
+
const result = await engineModelDiscovery.discoverEngine({
|
|
1826
|
+
...plan,
|
|
1827
|
+
home,
|
|
1828
|
+
userModels, backupModels,
|
|
1829
|
+
...(wanted.length ? { detailIds: wanted } : {}),
|
|
1830
|
+
publicCatalog: { provider: profile.provider, baseUrl: plan.baseUrl, ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}) },
|
|
1831
|
+
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
1832
|
+
...(opts.refresh === true ? { noCache: true } : {}),
|
|
1833
|
+
});
|
|
1834
|
+
// Curated response: `plan` (baseUrl, apiKey) stays out; apiModels is
|
|
1835
|
+
// already merged (with provenance) into `models`.
|
|
1836
|
+
// `backupValues` rides along only for fields NO layer could fill: the
|
|
1837
|
+
// internal catalog's proposal for the missing-data dialog. A field any
|
|
1838
|
+
// layer answered carries no proposal — the dialog asks for what nobody
|
|
1839
|
+
// knows, and the operator's confirmation is what turns a proposal into a
|
|
1840
|
+
// user value.
|
|
1841
|
+
const internalIndex = internalModelIndex(profile);
|
|
1842
|
+
const models = result.models.map((m) => {
|
|
1843
|
+
const backupValues = {};
|
|
1844
|
+
for (const field of ['contextWindow', 'maxTokens']) {
|
|
1845
|
+
if (m[field] === undefined) {
|
|
1846
|
+
const known = internalValueFor(internalIndex, m.id);
|
|
1847
|
+
if (known && known[field] !== undefined) backupValues[field] = known[field];
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return Object.keys(backupValues).length ? { ...m, backupValues } : m;
|
|
1851
|
+
});
|
|
1852
|
+
return {
|
|
1853
|
+
engine: id,
|
|
1854
|
+
supported: true,
|
|
1855
|
+
source: result.source,
|
|
1856
|
+
status: result.status,
|
|
1857
|
+
stale: result.stale === true,
|
|
1858
|
+
error: result.error || '',
|
|
1859
|
+
...(result.fetchedAt ? { fetchedAt: result.fetchedAt } : {}),
|
|
1860
|
+
...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
|
|
1861
|
+
models,
|
|
1862
|
+
cap: DISCOVERY_PROPOSAL_CAP,
|
|
1863
|
+
};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1717
1866
|
async function close() { await launchBroker.close(); if (leaseManager) leaseManager.close(); }
|
|
1718
1867
|
|
|
1719
1868
|
return {
|
|
@@ -1723,7 +1872,7 @@ async function createBuiltinFleet(cfg = {}) {
|
|
|
1723
1872
|
defineEngine, editEngine, removeEngine,
|
|
1724
1873
|
defineCell, editCell, removeCell, importCell, restoreCells, restoreEngines,
|
|
1725
1874
|
schema, definitions, capabilities,
|
|
1726
|
-
credentialStatus, setLocalCredential, removeLocalCredential, testModel,
|
|
1875
|
+
credentialStatus, setLocalCredential, removeLocalCredential, testModel, discoverEngineModels,
|
|
1727
1876
|
defineModel, editModel, removeModel, close,
|
|
1728
1877
|
// Fetta 2b: il lease Live esposto alle route /api/lease, che il bridge
|
|
1729
1878
|
// MCP delle celle consuma per register/refresh/recovery. Assente se il
|
package/lib/fleet/managed.js
CHANGED
|
@@ -10,6 +10,7 @@ const { execFile } = require('node:child_process');
|
|
|
10
10
|
const { ENV_KEY_RE } = require('./env-key.js');
|
|
11
11
|
const { termuxRuntimePaths } = require('../runtime/env.js');
|
|
12
12
|
const { readCredentialStore, safePrivateDir } = require('./credentials.js');
|
|
13
|
+
const { createModelDiscovery } = require('./model-discovery.js');
|
|
13
14
|
const { argomentiHookCodex, gateVersione, gatePiattaforma, EVENTI_CODEX } = require('./codex-hooks.js');
|
|
14
15
|
|
|
15
16
|
const OLLAMA_CLOUD_MODELS = Object.freeze([
|
|
@@ -1295,6 +1296,50 @@ function applyStoreNeutralization(env, spec, profile) {
|
|
|
1295
1296
|
}
|
|
1296
1297
|
|
|
1297
1298
|
let ollamaCache = { at: 0, models: [] };
|
|
1299
|
+
// Shared model-discovery instance: per-engine HTTP collectors + disk cache.
|
|
1300
|
+
// The engine's key is resolved HERE (credentialSources/credential, the same
|
|
1301
|
+
// path the launcher uses) and handed to the module in memory only.
|
|
1302
|
+
const sharedModelDiscovery = createModelDiscovery();
|
|
1303
|
+
// Map a managed profile (+ normalized spec) to its model-discovery plan:
|
|
1304
|
+
// collector kind, endpoint base URL and the engine's own credential. This is
|
|
1305
|
+
// the per-engine wiring point for the discovery lots: same endpoint and same
|
|
1306
|
+
// key the launcher would use, never a parallel configuration. Returns null
|
|
1307
|
+
// when the profile has no HTTP listing to interrogate (login/native/local
|
|
1308
|
+
// runtimes without an HTTP endpoint, or a custom base URL that is not set).
|
|
1309
|
+
// `engineId` is the DECLARED engine id and becomes the cache key: two custom
|
|
1310
|
+
// engines of the same client resolve to one shared profile, and keying their
|
|
1311
|
+
// model cache by profile id would hand the second engine the first one's
|
|
1312
|
+
// listing without ever interrogating its endpoint. Without a declared id
|
|
1313
|
+
// (catalog engines) the profile id IS the engine id.
|
|
1314
|
+
function engineModelDiscoveryPlan(profile, spec, cfg, home, engineId) {
|
|
1315
|
+
if (!profile) return null;
|
|
1316
|
+
const specBase = spec && typeof spec.baseUrl === 'string' && /^https?:\/\//i.test(spec.baseUrl) ? spec.baseUrl : '';
|
|
1317
|
+
const profileBase = typeof profile.endpoint === 'string' && /^https?:\/\//i.test(profile.endpoint) ? profile.endpoint : '';
|
|
1318
|
+
let collector = null;
|
|
1319
|
+
let baseUrl = '';
|
|
1320
|
+
if (profile.provider === 'ollama' || profile.provider === 'ollama-cloud') {
|
|
1321
|
+
collector = 'ollama';
|
|
1322
|
+
baseUrl = specBase || profileBase || 'http://127.0.0.1:11434';
|
|
1323
|
+
} else if (profile.provider === 'lmstudio') {
|
|
1324
|
+
collector = 'lmstudio';
|
|
1325
|
+
baseUrl = specBase || 'http://127.0.0.1:1234';
|
|
1326
|
+
} else if (profile.protocol === 'anthropic_messages') {
|
|
1327
|
+
collector = 'anthropic';
|
|
1328
|
+
baseUrl = specBase || profileBase;
|
|
1329
|
+
} else if (profile.protocol === 'openai_responses' || profile.protocol === 'openai_completions') {
|
|
1330
|
+
collector = 'openai-compat';
|
|
1331
|
+
baseUrl = specBase || profileBase;
|
|
1332
|
+
} else {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
if (!baseUrl) return null;
|
|
1336
|
+
const plan = { engine: engineId || profile.id, collector, baseUrl };
|
|
1337
|
+
try {
|
|
1338
|
+
const cred = credential(profile, spec || {}, cfg, home);
|
|
1339
|
+
if (cred && cred.value) plan.apiKey = cred.value;
|
|
1340
|
+
} catch (_) { /* discovery without a key: key-less local endpoints still list */ }
|
|
1341
|
+
return plan;
|
|
1342
|
+
}
|
|
1298
1343
|
async function discoverOllamaModels(opts = {}) {
|
|
1299
1344
|
const now = Date.now(); const ttl = opts.ttlMs === undefined ? 30000 : opts.ttlMs;
|
|
1300
1345
|
if (!opts.noCache && ollamaCache.models.length && now - ollamaCache.at < ttl) return [...ollamaCache.models];
|
|
@@ -1307,13 +1352,22 @@ async function discoverOllamaModels(opts = {}) {
|
|
|
1307
1352
|
|| sources.local.OLLAMA_API_KEY || sources.shell.OLLAMA_API_KEY || sources.keys.OLLAMA_API_KEY
|
|
1308
1353
|
|| sources.legacy.OLLAMA_API_KEY;
|
|
1309
1354
|
if (!apiKey) throw new Error('OLLAMA_API_KEY missing');
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1355
|
+
// Generalized on the model-discovery module (ollama collector): the HTTP
|
|
1356
|
+
// call, the sanitized errors and the last-good-on-disk cache live there.
|
|
1357
|
+
// The EXTERNAL behavior stays: the wired list filtered by what the API
|
|
1358
|
+
// declares live, and the full wired list when nothing is known.
|
|
1359
|
+
const layer = await sharedModelDiscovery.fetchEngineModels({
|
|
1360
|
+
engine: 'ollama-cloud', collector: 'ollama', baseUrl: 'https://ollama.com',
|
|
1361
|
+
apiKey, home, fetchImpl, timeoutMs: 2500, ttlMs: ttl, ...(opts.noCache ? { noCache: true } : {}),
|
|
1362
|
+
});
|
|
1363
|
+
// IL CONTRATTO ESTERNO NON CAMBIA (mandato L1: generalizzare «senza
|
|
1364
|
+
// cambiarne il comportamento esterno»). Un valore buono di ieri servito al
|
|
1365
|
+
// posto dell'API non e' una risposta di oggi: vale come "non si sa niente",
|
|
1366
|
+
// e allora si torna alla shortlist cablata intera. Solo `api` (o un valore
|
|
1367
|
+
// fresco, non stantio) dice cosa e' vivo adesso.
|
|
1368
|
+
const conosciuto = layer.status === 'api' || (layer.status === 'cache' && layer.stale === false);
|
|
1369
|
+
if (!conosciuto) throw new Error(layer.error || 'ollama-cloud discovery unavailable');
|
|
1370
|
+
const available = new Set(layer.apiModels.map((m) => m.id));
|
|
1317
1371
|
const models = OLLAMA_CLOUD_MODELS.filter((name) => available.has(name));
|
|
1318
1372
|
if (!models.length) throw new Error('no cloud models');
|
|
1319
1373
|
ollamaCache = { at: now, models }; return [...models];
|
|
@@ -3091,7 +3145,7 @@ module.exports = {
|
|
|
3091
3145
|
OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
|
|
3092
3146
|
OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
|
|
3093
3147
|
CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient, normalizeManagedSpec, profileFor,
|
|
3094
|
-
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, resolveManagedEngine, needsExplicitNode,
|
|
3148
|
+
defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, engineModelDiscoveryPlan, resolveManagedEngine, needsExplicitNode,
|
|
3095
3149
|
discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
|
|
3096
3150
|
codexSurfaceNames, writeCellCodexProfile,
|
|
3097
3151
|
writeVlCellPrompt, vlSystemAppendGate,
|