@mmmbuto/nexuscrew 0.9.75 → 0.9.76

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-BURtPwUi.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.76"}
@@ -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,50 @@ 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
+ }
1774
1826
  async function discoverEngineModels(engineId, opts = {}) {
1775
1827
  const id = String(engineId || '');
1776
1828
  if (!id || id.length > 64 || !/^[A-Za-z0-9._-]+$/.test(id)) {
@@ -1812,10 +1864,12 @@ async function createBuiltinFleet(cfg = {}) {
1812
1864
  id: m.id,
1813
1865
  ...(Number.isFinite(m.contextWindow) ? { contextWindow: m.contextWindow } : {}),
1814
1866
  ...(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).
1867
+ // definitions.js parses the three flags as typed booleans: `false`
1868
+ // is an explicit operator answer, not an absence. Keep it exactly
1869
+ // like that, or a provider's `true` would override the user layer
1870
+ // here (the merge itself already handles booleans correctly).
1871
+ ...(typeof m.tools === 'boolean' ? { tools: m.tools } : {}),
1872
+ ...(typeof m.vision === 'boolean' ? { vision: m.vision } : {}),
1819
1873
  ...(typeof m.reasoning === 'boolean' ? { reasoning: m.reasoning } : {}),
1820
1874
  });
1821
1875
  }
@@ -1856,6 +1910,7 @@ async function createBuiltinFleet(cfg = {}) {
1856
1910
  status: result.status,
1857
1911
  stale: result.stale === true,
1858
1912
  error: result.error || '',
1913
+ auth: modelListingAuthDiagnosis(plan, result),
1859
1914
  ...(result.fetchedAt ? { fetchedAt: result.fetchedAt } : {}),
1860
1915
  ...(result.publicCatalog ? { publicCatalog: result.publicCatalog } : {}),
1861
1916
  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
 
@@ -1334,10 +1334,30 @@ function engineModelDiscoveryPlan(profile, spec, cfg, home, engineId) {
1334
1334
  }
1335
1335
  if (!baseUrl) return null;
1336
1336
  const plan = { engine: engineId || profile.id, collector, baseUrl };
1337
+ // The credential STATE travels with the plan, from the same resolution the
1338
+ // launcher uses (the caller never re-reads the credential sources):
1339
+ // none / configured / missing / unavailable, plus the env NAME when one
1340
+ // exists and the editor already shows it. A file that exists but cannot be
1341
+ // read is "unavailable", not "missing" — the same distinction describeManaged
1342
+ // makes for launching. The value, its provenance paths/hashes and any
1343
+ // exception text stay here: the plan hands the key to the collector in
1344
+ // memory and nothing else leaves.
1337
1345
  try {
1338
- const cred = credential(profile, spec || {}, cfg, home);
1346
+ const cred = credential(profile, spec || {}, cfg, home, { blocked: [] });
1347
+ plan.credentialState = !cred ? 'unavailable'
1348
+ : cred.value ? 'configured'
1349
+ : cred.source === 'unreadable' ? 'unavailable'
1350
+ : cred.source === 'missing' ? 'missing'
1351
+ : 'none';
1339
1352
  if (cred && cred.value) plan.apiKey = cred.value;
1340
- } catch (_) { /* discovery without a key: key-less local endpoints still list */ }
1353
+ const envKey = cred && typeof cred.envKey === 'string' ? cred.envKey : '';
1354
+ // 'login'/'none' resolve to a pseudo key name: never invent one for them.
1355
+ if (plan.credentialState !== 'none' && ENV_KEY_RE.test(envKey || '')) plan.envKey = envKey;
1356
+ } catch (_) {
1357
+ // Resolution itself failed: the key is not known to be missing — it is
1358
+ // unavailable, and an anonymous rejection will say exactly that.
1359
+ plan.credentialState = 'unavailable';
1360
+ }
1341
1361
  return plan;
1342
1362
  }
1343
1363
  async function discoverOllamaModels(opts = {}) {