@mmmbuto/nexuscrew 0.9.75 → 0.9.77

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.
@@ -11,7 +11,7 @@
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-DMfpKI3n.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-Cgd3OQws.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-B9GW2LHm.css">
16
16
  </head>
17
17
  <body>
@@ -1 +1 @@
1
- {"version":"0.9.75"}
1
+ {"version":"0.9.77"}
@@ -41,7 +41,7 @@ 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
+ engineModelDiscoveryPlan, modelListingCredential, ollamaCloudCatalogModels, declaredModelsFor, ALIBABA_PI_MODELS,
45
45
  } = require('./managed.js');
46
46
  const { probeModel, probeCustomEndpoint } = require('./model-probe.js');
47
47
  const { createModelDiscovery, DISCOVERY_PROPOSAL_CAP } = require('./model-discovery.js');
@@ -965,7 +965,7 @@ async function createBuiltinFleet(cfg = {}) {
965
965
  // I campi non-identita' che `parseModel` ammette per una dichiarazione:
966
966
  // id ed engine SONO l'identita' (il chiaro per cui esiste edit-model e' non
967
967
  // doverla ritoccare), tutto il resto e' patchabile con la stessa validazione.
968
- const EDIT_MODEL_FIELDS = new Set(['label', 'contextWindow', 'maxTokens', 'reasoning']);
968
+ const EDIT_MODEL_FIELDS = new Set(['label', 'contextWindow', 'maxTokens', 'tools', 'vision', 'reasoning']);
969
969
 
970
970
  async function editModel(id, engine, patch) {
971
971
  if (readonly()) throw httpError(403, 'READONLY: edit-model bloccato');
@@ -984,12 +984,18 @@ async function createBuiltinFleet(cfg = {}) {
984
984
  }
985
985
  }
986
986
  for (const key of ['contextWindow', 'maxTokens']) {
987
- if (patch[key] !== undefined && (!Number.isSafeInteger(patch[key]) || patch[key] < 1)) {
988
- throw httpError(400, `${key} non valido: intero >= 1`);
987
+ // Stessa scala del parser: un valore che il dialogo accetta e il file
988
+ // rifiuta farebbe fallire l'intero salvataggio, non il solo campo.
989
+ if (patch[key] !== undefined && (!Number.isSafeInteger(patch[key]) || patch[key] < 1024 || patch[key] > 100000000)) {
990
+ throw httpError(400, `${key} non valido: intero fra 1024 e 100000000`);
989
991
  }
990
992
  }
991
- if (patch.reasoning !== undefined && typeof patch.reasoning !== 'boolean') {
992
- throw httpError(400, 'reasoning non valido: booleano');
993
+ // `tools` e `vision` sono booleani tipizzati come `reasoning`: `false` e'
994
+ // una risposta, quindi non si rifiuta e non si scambia per assenza.
995
+ for (const key of ['tools', 'vision', 'reasoning']) {
996
+ if (patch[key] !== undefined && typeof patch[key] !== 'boolean') {
997
+ throw httpError(400, `${key} non valido: booleano`);
998
+ }
993
999
  }
994
1000
  const defs = reloadDefs();
995
1001
  if (!(defs.models || []).some((m) => m.id === wantedId && m.engine === wantedEngine)) {
@@ -1605,6 +1611,8 @@ async function createBuiltinFleet(cfg = {}) {
1605
1611
  // arriverebbe al client come comportamento inspiegabile.
1606
1612
  contextWindow: { type: 'integer', required: false, min: 1024, max: 100000000 },
1607
1613
  maxTokens: { type: 'integer', required: false, min: 1024, max: 100000000 },
1614
+ tools: { type: 'boolean', required: false },
1615
+ vision: { type: 'boolean', required: false },
1608
1616
  reasoning: { type: 'boolean', required: false },
1609
1617
  },
1610
1618
  cell: {
@@ -1771,6 +1779,67 @@ async function createBuiltinFleet(cfg = {}) {
1771
1779
  const base = id.replace(/:[^:/]+$/, '');
1772
1780
  return base !== id && index.has(base) ? index.get(base) : null;
1773
1781
  }
1782
+ // Curated authentication diagnosis of the LISTING attempt. The route sees
1783
+ // only this block: the credential state, the env NAME (already public in
1784
+ // the editor), whether an attempt happened, whether it carried the
1785
+ // credential, and a fixed cause. Never the key value, its provenance, the
1786
+ // endpoint, or any transport exception text. Config (state) and transport
1787
+ // (attempt) stay separate: a present key is not a sent key, and a cached
1788
+ // answer is neither. A fresh cache makes no claim at all about the key
1789
+ // (attempted=false, no cause), and a SUCCESS never claims the endpoint does
1790
+ // not need a key — it only says the listing came back without one.
1791
+ function modelListingAuthDiagnosis(plan, result) {
1792
+ const credentialState = plan && ['none', 'configured', 'missing', 'unavailable'].includes(plan.credentialState)
1793
+ ? plan.credentialState : 'none';
1794
+ const envKey = typeof plan?.envKey === 'string' && plan.envKey ? plan.envKey : '';
1795
+ const attempt = result && result.attempt && typeof result.attempt === 'object' ? result.attempt : null;
1796
+ const attempted = !!attempt;
1797
+ const credentialSent = !!(attempt && attempt.credentialSent === true);
1798
+ let cause = '';
1799
+ if (attempt) {
1800
+ if (result.status === 'api') {
1801
+ // Success: the only outcome worth a line is the anonymous one, stated
1802
+ // as a fact about THIS listing, never about the endpoint in general.
1803
+ if (!credentialSent) cause = 'anonymous-success';
1804
+ } else if (attempt.category === 'network' || attempt.category === 'timeout') {
1805
+ cause = 'endpoint-unreachable';
1806
+ } else if (attempt.category === 'invalid-response') {
1807
+ cause = 'invalid-response';
1808
+ } else if (attempt.category === 'http') {
1809
+ const status = Number.isSafeInteger(attempt.status) ? attempt.status : 0;
1810
+ if (status === 403) cause = 'access-denied'; // with OR without the key: permissions or policy
1811
+ else if (status === 401) {
1812
+ if (credentialSent) cause = 'key-rejected';
1813
+ else if (credentialState === 'unavailable') cause = 'credential-unavailable';
1814
+ else cause = 'missing-key';
1815
+ } else cause = 'provider-error';
1816
+ }
1817
+ }
1818
+ return {
1819
+ credentialState,
1820
+ ...(envKey ? { envKey } : {}),
1821
+ attempted,
1822
+ credentialSent,
1823
+ ...(cause ? { cause } : {}),
1824
+ };
1825
+ }
1826
+ // Backup proposals for fields no layer could fill: the missing-data dialog
1827
+ // offers what the internal catalog knows, and the operator's confirmation
1828
+ // is what turns a proposal into a user value.
1829
+ function withBackupDefaults(records, profile) {
1830
+ const internalIndex = internalModelIndex(profile);
1831
+ return (Array.isArray(records) ? records : []).map((m) => {
1832
+ const backupValues = {};
1833
+ for (const field of ['contextWindow', 'maxTokens']) {
1834
+ if (m[field] === undefined) {
1835
+ const known = internalValueFor(internalIndex, m.id);
1836
+ if (known && known[field] !== undefined) backupValues[field] = known[field];
1837
+ }
1838
+ }
1839
+ return Object.keys(backupValues).length ? { ...m, backupValues } : m;
1840
+ });
1841
+ }
1842
+
1774
1843
  async function discoverEngineModels(engineId, opts = {}) {
1775
1844
  const id = String(engineId || '');
1776
1845
  if (!id || id.length > 64 || !/^[A-Za-z0-9._-]+$/.test(id)) {
@@ -1791,16 +1860,10 @@ async function createBuiltinFleet(cfg = {}) {
1791
1860
  spec = profile ? { client: profile.client, provider: profile.provider, ...(profile.credentialProfile ? { credentialProfile: profile.credentialProfile } : {}) } : null;
1792
1861
  }
1793
1862
  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
1863
  // User layer: models declared in fleet.json for this engine (or its
1802
1864
  // profile) — the values the operator chose, always on top and never
1803
- // rewritten by discovery.
1865
+ // rewritten by discovery. Built BEFORE the plan: an engine without a
1866
+ // listing still has ids worth enriching.
1804
1867
  const extraModels = extraModelsFrom(defs);
1805
1868
  const userModels = [];
1806
1869
  const seenUser = new Set();
@@ -1812,43 +1875,69 @@ async function createBuiltinFleet(cfg = {}) {
1812
1875
  id: m.id,
1813
1876
  ...(Number.isFinite(m.contextWindow) ? { contextWindow: m.contextWindow } : {}),
1814
1877
  ...(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).
1878
+ // definitions.js parses the three flags as typed booleans: `false`
1879
+ // is an explicit operator answer, not an absence. Keep it exactly
1880
+ // like that, or a provider's `true` would override the user layer
1881
+ // here (the merge itself already handles booleans correctly).
1882
+ ...(typeof m.tools === 'boolean' ? { tools: m.tools } : {}),
1883
+ ...(typeof m.vision === 'boolean' ? { vision: m.vision } : {}),
1819
1884
  ...(typeof m.reasoning === 'boolean' ? { reasoning: m.reasoning } : {}),
1820
1885
  });
1821
1886
  }
1822
1887
  }
1823
1888
  const backupModels = backupModelsFor(profile);
1824
1889
  const wanted = [...new Set([...userModels, ...backupModels].map((m) => m.id))].slice(0, 32);
1890
+ // The plan is the HTTP listing, when the profile has one. When it does
1891
+ // not, the same composition still runs with the API layer explicitly
1892
+ // unavailable: known ids are enriched from the public snapshot, with no
1893
+ // invented collector, request or auth attempt.
1894
+ const plan = engineModelDiscoveryPlan(profile, spec, cfg, home, id);
1895
+ // The active model is configuration the operator already chose. It counts
1896
+ // as a known id only when it is the ONLY one: with a declared model or a
1897
+ // profile backup the proposal already has its ids, and adding it would
1898
+ // duplicate a row or change the order.
1899
+ const activeModel = spec && typeof spec.model === 'string' ? spec.model.trim() : '';
1900
+ const knownIds = !userModels.length && !backupModels.length && activeModel ? [activeModel] : [];
1825
1901
  const result = await engineModelDiscovery.discoverEngine({
1826
- ...plan,
1902
+ ...(plan || {}),
1903
+ engine: id,
1827
1904
  home,
1828
1905
  userModels, backupModels,
1906
+ knownIds,
1829
1907
  ...(wanted.length ? { detailIds: wanted } : {}),
1830
- publicCatalog: { provider: profile.provider, baseUrl: plan.baseUrl, ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}) },
1908
+ publicCatalog: {
1909
+ provider: profile.provider,
1910
+ ...(plan && plan.baseUrl ? { baseUrl: plan.baseUrl } : {}),
1911
+ ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
1912
+ },
1831
1913
  ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
1832
1914
  ...(opts.refresh === true ? { noCache: true } : {}),
1915
+ ...(plan ? {} : { apiAvailable: false }),
1833
1916
  });
1834
1917
  // 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
- });
1918
+ // already merged (with provenance) into `models`. `backupValues` rides
1919
+ // along only for fields NO layer could fill.
1920
+ const models = withBackupDefaults(result.models, profile);
1921
+ if (!plan) {
1922
+ const cred = modelListingCredential(profile, spec, cfg, home);
1923
+ return {
1924
+ engine: id,
1925
+ supported: false,
1926
+ source: 'unsupported',
1927
+ status: result.status,
1928
+ stale: false,
1929
+ error: '',
1930
+ auth: {
1931
+ credentialState: cred.credentialState,
1932
+ ...(cred.envKey ? { envKey: cred.envKey } : {}),
1933
+ attempted: false,
1934
+ credentialSent: false,
1935
+ },
1936
+ ...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
1937
+ models,
1938
+ cap: DISCOVERY_PROPOSAL_CAP,
1939
+ };
1940
+ }
1852
1941
  return {
1853
1942
  engine: id,
1854
1943
  supported: true,
@@ -1856,6 +1945,7 @@ async function createBuiltinFleet(cfg = {}) {
1856
1945
  status: result.status,
1857
1946
  stale: result.stale === true,
1858
1947
  error: result.error || '',
1948
+ auth: modelListingAuthDiagnosis(plan, result),
1859
1949
  ...(result.fetchedAt ? { fetchedAt: result.fetchedAt } : {}),
1860
1950
  ...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
1861
1951
  models,
@@ -196,7 +196,7 @@ const MODEL_ID_MAX = 128;
196
196
 
197
197
  function parseModel(m) {
198
198
  if (!m || typeof m !== 'object' || Array.isArray(m)) return null;
199
- const allowed = new Set(['id', 'engine', 'label', 'contextWindow', 'maxTokens', 'reasoning']);
199
+ const allowed = new Set(['id', 'engine', 'label', 'contextWindow', 'maxTokens', 'tools', 'vision', 'reasoning']);
200
200
  if (Object.keys(m).some((k) => !allowed.has(k))) return null;
201
201
  const id = typeof m.id === 'string' ? m.id.trim() : '';
202
202
  // Stessa forma accettata dal gate dei modelli: nessun byte di controllo, un
@@ -221,6 +221,15 @@ function parseModel(m) {
221
221
  if (typeof m.reasoning !== 'boolean') return null;
222
222
  out.reasoning = m.reasoning;
223
223
  }
224
+ // `tools` e `vision`: stessi booleani tipizzati di `reasoning`. `false` e'
225
+ // una RISPOSTA dell'operatore, non un'assenza: si conserva tale, cosi' la
226
+ // scoperta (user > api > public) non lascia salire il `true` di un catalogo
227
+ // sopra la parola esplicita di chi dichiara.
228
+ for (const key of ['tools', 'vision']) {
229
+ if (m[key] === undefined) continue;
230
+ if (typeof m[key] !== 'boolean') return null;
231
+ out[key] = m[key];
232
+ }
224
233
  return out;
225
234
  }
226
235
 
@@ -1313,32 +1313,72 @@ const sharedModelDiscovery = createModelDiscovery();
1313
1313
  // (catalog engines) the profile id IS the engine id.
1314
1314
  function engineModelDiscoveryPlan(profile, spec, cfg, home, engineId) {
1315
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 : '';
1316
+ const httpUrl = (value) => (typeof value === 'string' && /^https?:\/\//i.test(value) ? value : '');
1317
+ // Endpoint precedence follows the launcher: the normalized spec, the
1318
+ // generated provider extension, then the profile's own endpoint. A native
1319
+ // provider has no explicit endpoint here, and is never pointed at another
1320
+ // client's profile.
1321
+ const specBase = httpUrl(spec && spec.baseUrl);
1322
+ const extensionBase = httpUrl(profile.piExtension && profile.piExtension.baseUrl);
1323
+ const profileBase = httpUrl(profile.endpoint);
1324
+ // The wire name may come hyphenated (`openai-completions`, the Pi form) or
1325
+ // underscored (the normalized form). It is the same transport.
1326
+ const rawWire = spec && typeof spec.protocol === 'string' && spec.protocol
1327
+ ? spec.protocol : (typeof profile.protocol === 'string' ? profile.protocol : '');
1328
+ const wire = rawWire.replace(/-/g, '_');
1318
1329
  let collector = null;
1319
1330
  let baseUrl = '';
1320
1331
  if (profile.provider === 'ollama' || profile.provider === 'ollama-cloud') {
1332
+ // Ollama speaks its own listing API, whatever wire a client uses for chat.
1321
1333
  collector = 'ollama';
1322
- baseUrl = specBase || profileBase || 'http://127.0.0.1:11434';
1334
+ baseUrl = specBase || extensionBase || profileBase || 'http://127.0.0.1:11434';
1323
1335
  } else if (profile.provider === 'lmstudio') {
1324
1336
  collector = 'lmstudio';
1325
- baseUrl = specBase || 'http://127.0.0.1:1234';
1326
- } else if (profile.protocol === 'anthropic_messages') {
1337
+ baseUrl = specBase || extensionBase || 'http://127.0.0.1:1234';
1338
+ } else if (wire === 'anthropic_messages') {
1327
1339
  collector = 'anthropic';
1328
- baseUrl = specBase || profileBase;
1329
- } else if (profile.protocol === 'openai_responses' || profile.protocol === 'openai_completions') {
1340
+ baseUrl = specBase || extensionBase || profileBase;
1341
+ } else if (wire === 'openai_responses' || wire === 'openai_completions') {
1330
1342
  collector = 'openai-compat';
1331
- baseUrl = specBase || profileBase;
1343
+ baseUrl = specBase || extensionBase || profileBase;
1332
1344
  } else {
1345
+ // `pi_native`, login flows and any other client-owned wire: no explicit
1346
+ // listing contract, so no collector. An explicit endpoint alone is not
1347
+ // proof of a listing API.
1333
1348
  return null;
1334
1349
  }
1335
1350
  if (!baseUrl) return null;
1336
- const plan = { engine: engineId || profile.id, collector, baseUrl };
1351
+ return { engine: engineId || profile.id, collector, baseUrl, ...modelListingCredential(profile, spec, cfg, home) };
1352
+ }
1353
+
1354
+ // Credential resolution for a model listing, from the same path the launcher
1355
+ // uses (the caller never re-reads the credential sources): none / configured /
1356
+ // missing / unavailable, plus the env NAME when one exists and the editor
1357
+ // already shows it. A file that exists but cannot be read is "unavailable",
1358
+ // not "missing" — the same distinction describeManaged makes for launching.
1359
+ // The value, its provenance paths/hashes and any exception text stay here:
1360
+ // the value is handed to a collector in memory and nothing else leaves. An
1361
+ // engine with no listing still gets the STATE: the diagnosis refers to the
1362
+ // listing only, never to the client's own login.
1363
+ function modelListingCredential(profile, spec, cfg, home) {
1364
+ const out = { credentialState: 'unavailable' };
1337
1365
  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;
1366
+ const cred = credential(profile, spec || {}, cfg, home, { blocked: [] });
1367
+ out.credentialState = !cred ? 'unavailable'
1368
+ : cred.value ? 'configured'
1369
+ : cred.source === 'unreadable' ? 'unavailable'
1370
+ : cred.source === 'missing' ? 'missing'
1371
+ : 'none';
1372
+ if (cred && cred.value) out.apiKey = cred.value;
1373
+ const envKey = cred && typeof cred.envKey === 'string' ? cred.envKey : '';
1374
+ // 'login'/'none' resolve to a pseudo key name: never invent one for them.
1375
+ if (out.credentialState !== 'none' && ENV_KEY_RE.test(envKey || '')) out.envKey = envKey;
1376
+ } catch (_) {
1377
+ // Resolution itself failed: the key is not known to be missing — it is
1378
+ // unavailable, and an anonymous rejection will say exactly that.
1379
+ out.credentialState = 'unavailable';
1380
+ }
1381
+ return out;
1342
1382
  }
1343
1383
  async function discoverOllamaModels(opts = {}) {
1344
1384
  const now = Date.now(); const ttl = opts.ttlMs === undefined ? 30000 : opts.ttlMs;
@@ -3145,7 +3185,7 @@ module.exports = {
3145
3185
  OPENCODE_GO_MESSAGES_MODELS, OPENCODE_GO_RESPONSES_MODELS, OPENCODE_GO_CHAT_MODELS, OPENCODE_GO_LIMITS,
3146
3186
  OPENCODE_GO_ANTHROPIC_ROOT, OPENCODE_GO_API_BASE,
3147
3187
  CLIENT_LABELS, CLIENT_ORDER, engineClient, ordinaEngine, isStandardClient, normalizeManagedSpec, profileFor,
3148
- defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, engineModelDiscoveryPlan, resolveManagedEngine, needsExplicitNode,
3188
+ defaultDefinitions, defaultShellEngine, defaultAgyEngine, defaultKimiEngine, defaultGrokEngine, defaultVlEngine, describeManaged, describeCatalogCredential, discoverOllamaModels, engineModelDiscoveryPlan, modelListingCredential, resolveManagedEngine, needsExplicitNode,
3149
3189
  discoverPiModels, EXTERNAL_DISCOVERY_TIMEOUT_MS, parseEnvFile, parseProviderShellFile, findBinary, publicCatalog, writePiProviderExtension, customCatalogFor,
3150
3190
  codexSurfaceNames, writeCellCodexProfile,
3151
3191
  writeVlCellPrompt, vlSystemAppendGate,