@polderlabs/bizar 10.19.6 → 10.19.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/cli/commands/models.mjs
CHANGED
|
@@ -227,7 +227,15 @@ function normalizedModelIdentity(id) {
|
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Build a per-model profile object from a Models.dev catalog row.
|
|
232
|
+
*
|
|
233
|
+
* Phase 1 (v10.19.7): in addition to the long-standing `name` / `family` /
|
|
234
|
+
* `capabilities` / `limits` / `metadata` fields, propagate Models.dev's
|
|
235
|
+
* `description` and `summary` onto the profile so Phase 3's status screen
|
|
236
|
+
* (10.19.9) can render the description without re-querying the catalog.
|
|
237
|
+
*/
|
|
238
|
+
export function toCapabilityProfile(gatewayId, match, matchType, confidence) {
|
|
231
239
|
const limit = match.limit && typeof match.limit === 'object' ? match.limit : {};
|
|
232
240
|
const modalities = match.modalities && typeof match.modalities === 'object' ? match.modalities : {};
|
|
233
241
|
return {
|
|
@@ -235,6 +243,8 @@ function toCapabilityProfile(gatewayId, match, matchType, confidence) {
|
|
|
235
243
|
baseModel: match.id,
|
|
236
244
|
name: typeof match.name === 'string' ? match.name : match.id,
|
|
237
245
|
family: typeof match.family === 'string' ? match.family : null,
|
|
246
|
+
description: typeof match.description === 'string' ? match.description : null,
|
|
247
|
+
summary: typeof match.summary === 'string' ? match.summary : null,
|
|
238
248
|
capabilities: {
|
|
239
249
|
attachment: match.attachment === true,
|
|
240
250
|
reasoning: match.reasoning === true,
|
|
@@ -265,6 +275,15 @@ function toCapabilityProfile(gatewayId, match, matchType, confidence) {
|
|
|
265
275
|
* Enrich gateway-discovered models with Models.dev profiles. Exact IDs win.
|
|
266
276
|
* A normalized/suffix match is accepted only when unique; ambiguous models
|
|
267
277
|
* remain unmatched rather than receiving guessed capabilities.
|
|
278
|
+
*
|
|
279
|
+
* Phase 1 (v10.19.7): when Models.dev has no row for the candidate AND the
|
|
280
|
+
* candidate carries gateway-supplied `_gateway.name` / `_gateway.description`,
|
|
281
|
+
* build a minimal `profile` so the picker row renderer can read
|
|
282
|
+
* `profile.name` / `profile.description` directly without dereferencing
|
|
283
|
+
* `_gateway`. Candidates whose `normalizeModels` output had no `_gateway`
|
|
284
|
+
* fields keep the legacy `profile === null` contract so `capabilityLabel`
|
|
285
|
+
* still returns `'metadata unavailable'` (Phase 2 owns the rewrite that
|
|
286
|
+
* lets a non-null profile render the `'metadata unavailable'` label).
|
|
268
287
|
*/
|
|
269
288
|
export function enrichModelsWithCapabilities(candidates, catalog) {
|
|
270
289
|
const entries = flattenModelsDevCatalog(catalog);
|
|
@@ -286,8 +305,196 @@ export function enrichModelsWithCapabilities(candidates, catalog) {
|
|
|
286
305
|
const profile = toCapabilityProfile(gatewayId, matches[0], 'unique-normalized-id', 0.7);
|
|
287
306
|
return { ...candidate, profile, contextWindow: profile.limits.contextTokens };
|
|
288
307
|
}
|
|
289
|
-
|
|
308
|
+
// Models.dev miss: if the candidate carries gateway-supplied label /
|
|
309
|
+
// description, build a minimal `profile` so the picker row renderer
|
|
310
|
+
// can read `profile.name` / `profile.description` directly. Candidates
|
|
311
|
+
// that arrived from `normalizeModels` WITHOUT any `_gateway` data keep
|
|
312
|
+
// the legacy `profile === null` contract so `capabilityLabel(null)`
|
|
313
|
+
// still returns `'metadata unavailable'` (Phase 2 owns that rewrite).
|
|
314
|
+
const gw = (candidate && typeof candidate._gateway === 'object' && candidate._gateway) || {};
|
|
315
|
+
const gatewayName = typeof gw.name === 'string' ? gw.name
|
|
316
|
+
: (typeof gw.display_name === 'string' ? gw.display_name : null);
|
|
317
|
+
const gatewayDescription = typeof gw.description === 'string' ? gw.description : null;
|
|
318
|
+
if (gatewayName === null && gatewayDescription === null) {
|
|
319
|
+
return { ...candidate, profile: null, contextWindow: null };
|
|
320
|
+
}
|
|
321
|
+
const fallbackProfile = {
|
|
322
|
+
gatewayId,
|
|
323
|
+
baseModel: gatewayId,
|
|
324
|
+
name: gatewayName,
|
|
325
|
+
family: null,
|
|
326
|
+
description: gatewayDescription,
|
|
327
|
+
summary: null,
|
|
328
|
+
capabilities: {
|
|
329
|
+
attachment: false,
|
|
330
|
+
reasoning: false,
|
|
331
|
+
toolCall: false,
|
|
332
|
+
structuredOutput: false,
|
|
333
|
+
temperature: true,
|
|
334
|
+
inputModalities: ['text'],
|
|
335
|
+
outputModalities: ['text'],
|
|
336
|
+
},
|
|
337
|
+
limits: { contextTokens: null, inputTokens: null, outputTokens: null },
|
|
338
|
+
releaseDate: null,
|
|
339
|
+
lastUpdated: null,
|
|
340
|
+
metadata: {
|
|
341
|
+
source: 'gateway-fallback',
|
|
342
|
+
sourceUrl: null,
|
|
343
|
+
retrievedAt: new Date().toISOString(),
|
|
344
|
+
matchType: 'gateway-fallback',
|
|
345
|
+
confidence: 0,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
return { ...candidate, profile: fallbackProfile, contextWindow: null };
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Strip a `<provider>/` prefix from a model id. Used as a last-ditch catalog
|
|
354
|
+
* lookup key when the gateway reports the model under a wrapper namespace
|
|
355
|
+
* (e.g. `claude-minimax/MiniMax-M3`) and the models.dev catalog uses the bare
|
|
356
|
+
* form (`minimax/MiniMax-M3`).
|
|
357
|
+
*
|
|
358
|
+
* @param {string} id
|
|
359
|
+
* @returns {string}
|
|
360
|
+
*/
|
|
361
|
+
function stripProvider(id) {
|
|
362
|
+
const raw = String(id || '').trim();
|
|
363
|
+
const slash = raw.indexOf('/');
|
|
364
|
+
return slash >= 0 ? raw.slice(slash + 1) : raw;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Phase 2 lazy enrichment. The interactive picker path used to call
|
|
369
|
+
* `fetchModelsDevCatalog` BEFORE the picker opened, paying the round-trip
|
|
370
|
+
* on every `--list`/`--set` (which never even consulted the catalog) and
|
|
371
|
+
* on every interactive session that the user then aborted.
|
|
372
|
+
*
|
|
373
|
+
* After Phase 2 the catalog fetch moves to AFTER picker confirmation:
|
|
374
|
+
* - `--list` and `--set` skip the fetch entirely.
|
|
375
|
+
* - The interactive path only fetches when the user actually picked
|
|
376
|
+
* one or more models, and only the picked IDs are enriched.
|
|
377
|
+
*
|
|
378
|
+
* Concurrency: per-id lookups run in parallel with a bounded worker pool
|
|
379
|
+
* (`concurrency`, default 8) and a per-id timeout (`timeoutMs`, default
|
|
380
|
+
* 3000ms). On per-id timeout or lookup failure, we fall back to the
|
|
381
|
+
* candidate's Phase 1 `_gateway.name` contract (when present); when no
|
|
382
|
+
* `_gateway` block is attached, the profile is `null`.
|
|
383
|
+
*
|
|
384
|
+
* @param {{
|
|
385
|
+
* candidates: Array<{ id: string, owned_by?: string|null, _gateway?: { name?: string|null, display_name?: string|null, description?: string|null } }>,
|
|
386
|
+
* pickedIds: string[],
|
|
387
|
+
* fetchFn?: typeof fetchModelsDevCatalog,
|
|
388
|
+
* timeoutMs?: number,
|
|
389
|
+
* concurrency?: number,
|
|
390
|
+
* now?: () => number,
|
|
391
|
+
* }} opts
|
|
392
|
+
* @returns {Promise<{ profiles: Map<string, object|null>, modelsDev: object }>}
|
|
393
|
+
*/
|
|
394
|
+
export async function enrichPicksByMetadata({
|
|
395
|
+
candidates,
|
|
396
|
+
pickedIds,
|
|
397
|
+
fetchFn = fetchModelsDevCatalog,
|
|
398
|
+
timeoutMs = 3000,
|
|
399
|
+
concurrency = 8,
|
|
400
|
+
} = {}) {
|
|
401
|
+
// Wholesale catalog fetch — best-effort. A network failure on the
|
|
402
|
+
// initial fetch degrades to an empty map; per-id timeouts on the
|
|
403
|
+
// downstream enrichment path fall back to `_gateway.name`.
|
|
404
|
+
// The wholesale fetch inherits `timeoutMs` so a hung stub does not
|
|
405
|
+
// block the picker-confirmation step indefinitely.
|
|
406
|
+
const catalog = await Promise.race([
|
|
407
|
+
fetchFn({}),
|
|
408
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('enrichPicksByMetadata: wholesale fetch timeout')), timeoutMs)),
|
|
409
|
+
]).catch(() => ({}));
|
|
410
|
+
const catalogMap = flattenModelsDevCatalog(catalog);
|
|
411
|
+
|
|
412
|
+
const out = new Map();
|
|
413
|
+
const ids = Array.isArray(pickedIds) ? pickedIds.filter((id) => typeof id === 'string' && id) : [];
|
|
414
|
+
// Stable order so the work array indexes are predictable for the
|
|
415
|
+
// bounded runner below.
|
|
416
|
+
const work = ids.map((id, index) => ({ id, index }));
|
|
417
|
+
if (work.length === 0) return { profiles: out, modelsDev: catalog };
|
|
418
|
+
|
|
419
|
+
const findCandidate = (id) => (Array.isArray(candidates) ? candidates.find((c) => c && c.id === id) : null);
|
|
420
|
+
|
|
421
|
+
// Bounded-concurrency worker pool. Each worker pulls jobs off the
|
|
422
|
+
// queue until empty. Per-id failure (timeout or thrown) falls back to
|
|
423
|
+
// the candidate's `_gateway` block when present, otherwise leaves
|
|
424
|
+
// the profile as `null`.
|
|
425
|
+
const queue = [...work];
|
|
426
|
+
const workerCount = Math.max(1, Math.min(concurrency, queue.length));
|
|
427
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
428
|
+
while (queue.length > 0) {
|
|
429
|
+
const job = queue.shift();
|
|
430
|
+
if (!job) return;
|
|
431
|
+
const { id } = job;
|
|
432
|
+
const candidate = findCandidate(id);
|
|
433
|
+
if (!candidate) {
|
|
434
|
+
out.set(id, null);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
await Promise.race([
|
|
439
|
+
// Each "job" resolves immediately with the profile lookup; the
|
|
440
|
+
// race is a defense-in-depth measure so a misbehaving fetchFn
|
|
441
|
+
// cannot stall the worker past `timeoutMs`.
|
|
442
|
+
Promise.resolve().then(() => {
|
|
443
|
+
const lower = String(id).toLowerCase();
|
|
444
|
+
const match = catalogMap.get(lower)
|
|
445
|
+
|| catalogMap.get(stripProvider(id).toLowerCase());
|
|
446
|
+
let profile;
|
|
447
|
+
if (match) {
|
|
448
|
+
// Re-use the existing capability-profile shape; the new
|
|
449
|
+
// helper differs from `enrichModelsWithCapabilities` only
|
|
450
|
+
// in WHEN the catalog is fetched and WHICH ids are
|
|
451
|
+
// enriched (confirmed picks only).
|
|
452
|
+
profile = toCapabilityProfile(id, match, 'exact-id', 0.9);
|
|
453
|
+
} else if (candidate._gateway) {
|
|
454
|
+
profile = {
|
|
455
|
+
name: candidate._gateway.name ?? candidate._gateway.display_name ?? null,
|
|
456
|
+
description: candidate._gateway.description ?? null,
|
|
457
|
+
summary: null,
|
|
458
|
+
ownedBy: candidate.owned_by ?? null,
|
|
459
|
+
supportsTools: null,
|
|
460
|
+
supportsVision: null,
|
|
461
|
+
contextWindow: null,
|
|
462
|
+
costTier: null,
|
|
463
|
+
confidence: 0,
|
|
464
|
+
metadata: { source: 'gateway-fallback' },
|
|
465
|
+
};
|
|
466
|
+
} else {
|
|
467
|
+
profile = null;
|
|
468
|
+
}
|
|
469
|
+
out.set(id, profile);
|
|
470
|
+
}),
|
|
471
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('enrichPicksByMetadata: per-id timeout')), timeoutMs)),
|
|
472
|
+
]);
|
|
473
|
+
} catch {
|
|
474
|
+
// Per-id failure (timeout or thrown). Fall back to `_gateway.name`
|
|
475
|
+
// when present so the Phase 1 contract still surfaces a name;
|
|
476
|
+
// otherwise the profile is null.
|
|
477
|
+
if (candidate._gateway) {
|
|
478
|
+
out.set(id, {
|
|
479
|
+
name: candidate._gateway.name ?? candidate._gateway.display_name ?? null,
|
|
480
|
+
description: candidate._gateway.description ?? null,
|
|
481
|
+
summary: null,
|
|
482
|
+
ownedBy: candidate.owned_by ?? null,
|
|
483
|
+
supportsTools: null,
|
|
484
|
+
supportsVision: null,
|
|
485
|
+
contextWindow: null,
|
|
486
|
+
costTier: null,
|
|
487
|
+
confidence: 0,
|
|
488
|
+
metadata: { source: 'gateway-fallback' },
|
|
489
|
+
});
|
|
490
|
+
} else {
|
|
491
|
+
out.set(id, null);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
290
495
|
});
|
|
496
|
+
await Promise.all(workers);
|
|
497
|
+
return { profiles: out, modelsDev: catalog };
|
|
291
498
|
}
|
|
292
499
|
|
|
293
500
|
function capabilityLabel(profile) {
|
|
@@ -379,7 +586,24 @@ async function fetchOnce({ doFetch, url, authToken, timeoutMs }) {
|
|
|
379
586
|
}
|
|
380
587
|
}
|
|
381
588
|
|
|
382
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Normalize the gateway `/models` response into the candidate-pool shape
|
|
591
|
+
* consumed by the picker.
|
|
592
|
+
*
|
|
593
|
+
* Phase 1 (v10.19.7): in addition to the existing `id` / `owned_by` / `kind`
|
|
594
|
+
* fields, preserve the gateway's optional `name` / `display_name` /
|
|
595
|
+
* `description` payload under a new `_gateway` sub-object. The picker row
|
|
596
|
+
* renderer reads `profile.name` / `profile.description`, so when the
|
|
597
|
+
* Models.dev enrichment misses (Phase 2) the renderer can still surface a
|
|
598
|
+
* gateway-supplied label or description rather than an id-derived fallback.
|
|
599
|
+
*
|
|
600
|
+
* NOTE: `_gateway` is in-memory only. `applyModels` never writes it to
|
|
601
|
+
* `model-router.json`; the persisted shape stays as it was before this
|
|
602
|
+
* change. See `models-namespace-sync.test.mjs#normalizeModels does not
|
|
603
|
+
* persist _gateway into userSelected on round-trip` for the regression
|
|
604
|
+
* pin.
|
|
605
|
+
*/
|
|
606
|
+
export function normalizeModels(body) {
|
|
383
607
|
if (!body || typeof body !== 'object') return [];
|
|
384
608
|
const list = Array.isArray(body.data) ? body.data : Array.isArray(body) ? body : [];
|
|
385
609
|
const out = [];
|
|
@@ -388,7 +612,11 @@ function normalizeModels(body) {
|
|
|
388
612
|
const id = typeof m.id === 'string' ? m.id.trim() : '';
|
|
389
613
|
if (!id) continue;
|
|
390
614
|
const owned = typeof m.owned_by === 'string' ? m.owned_by : '';
|
|
391
|
-
|
|
615
|
+
const gw = {};
|
|
616
|
+
if (typeof m.name === 'string' && m.name) gw.name = m.name;
|
|
617
|
+
if (typeof m.display_name === 'string' && m.display_name) gw.display_name = m.display_name;
|
|
618
|
+
if (typeof m.description === 'string' && m.description) gw.description = m.description;
|
|
619
|
+
out.push({ id, owned_by: owned, kind: classifyKind(id), _gateway: gw });
|
|
392
620
|
}
|
|
393
621
|
// Stable order — by id — so the picker does not shuffle between runs.
|
|
394
622
|
out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
@@ -1374,7 +1602,16 @@ function parseSet(value) {
|
|
|
1374
1602
|
.filter((s) => s.length > 0);
|
|
1375
1603
|
}
|
|
1376
1604
|
|
|
1377
|
-
export async function run(name, args, isHelpRequest) {
|
|
1605
|
+
export async function run(name, args, isHelpRequest, deps = {}) {
|
|
1606
|
+
// `bizar model` remains a deprecated alias; `bizar models` is the canonical surface.
|
|
1607
|
+
// `deps` is an optional test-injection surface (10.19.8 Phase 2):
|
|
1608
|
+
// - `pickModels` — override the interactive picker (for tests that
|
|
1609
|
+
// auto-confirm without a TTY)
|
|
1610
|
+
// - `fetchModelsDevCatalog` — override the catalog fetch (for tests
|
|
1611
|
+
// that count calls or stub models.dev)
|
|
1612
|
+
// - `listModels` — override the gateway `/models` fetch (for tests)
|
|
1613
|
+
// All three default to the module-level exports; production callers
|
|
1614
|
+
// see no behaviour change.
|
|
1378
1615
|
// `bizar model` remains a deprecated alias; `bizar models` is the canonical surface.
|
|
1379
1616
|
if (name !== 'models' && name !== 'model') return false;
|
|
1380
1617
|
if (isHelpRequest || args.includes('--help') || args.includes('-h')) {
|
|
@@ -1537,32 +1774,21 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1537
1774
|
// We do not short-circuit here — the code below falls into the
|
|
1538
1775
|
// `wantList || isDeprecatedAlias` branch and prints candidate IDs.
|
|
1539
1776
|
|
|
1777
|
+
// Phase 2 (10.19.8): the Models.dev catalog fetch used to happen HERE,
|
|
1778
|
+
// before any flag was inspected. That paid the round-trip on every
|
|
1779
|
+
// `--list`, every `--set`, every aborted picker, and every CI run that
|
|
1780
|
+
// never looked at the result. Move the fetch to AFTER picker
|
|
1781
|
+
// confirmation (see the interactive branch below); `--list` and
|
|
1782
|
+
// `bizar model` now skip the fetch entirely.
|
|
1540
1783
|
let candidates;
|
|
1541
|
-
let modelsDev = { status: '
|
|
1784
|
+
let modelsDev = { status: 'skipped', matched: 0, total: 0, source: MODELS_DEV_CATALOG_URL, note: 'lazy fetch — --list does not contact models.dev' };
|
|
1542
1785
|
try {
|
|
1543
1786
|
// The deprecated `bizar model` alias preserves the legacy
|
|
1544
1787
|
// "retry-without-auth on 401" behavior so existing scripts keep working.
|
|
1545
1788
|
// The new `bizar models` surface fails fast on 401 with an actionable
|
|
1546
1789
|
// error message (the user explicitly picks models — no silent retry).
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
const catalogue = await fetchModelsDevCatalog({});
|
|
1550
|
-
candidates = enrichModelsWithCapabilities(candidates, catalogue);
|
|
1551
|
-
modelsDev = {
|
|
1552
|
-
status: 'ok',
|
|
1553
|
-
matched: candidates.filter((candidate) => candidate.profile).length,
|
|
1554
|
-
total: candidates.length,
|
|
1555
|
-
source: MODELS_DEV_CATALOG_URL,
|
|
1556
|
-
};
|
|
1557
|
-
} catch (metadataError) {
|
|
1558
|
-
modelsDev = {
|
|
1559
|
-
status: 'unavailable',
|
|
1560
|
-
matched: 0,
|
|
1561
|
-
total: candidates.length,
|
|
1562
|
-
source: MODELS_DEV_CATALOG_URL,
|
|
1563
|
-
error: metadataError instanceof Error ? metadataError.message : String(metadataError),
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1790
|
+
const listModelsFn = deps.listModels || listModels;
|
|
1791
|
+
candidates = await listModelsFn({ endpoint, authToken, retryWithoutAuth: isDeprecatedAlias });
|
|
1566
1792
|
} catch (err) {
|
|
1567
1793
|
if (err.name === 'AbortError' || String(err.message).includes('aborted')) {
|
|
1568
1794
|
console.error(chalk.red(` x Request timed out after 3000ms - ${endpoint}/models`));
|
|
@@ -1593,7 +1819,10 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1593
1819
|
// Interactive picker — only reached when explicitly invoked as `bizar models`
|
|
1594
1820
|
// with NO --list / --clear / --set flags. We avoid running the picker when
|
|
1595
1821
|
// stdin is not a TTY (e.g. CI / npm test), because readline.question would block.
|
|
1596
|
-
|
|
1822
|
+
// 10.19.8 Phase 2: when `deps.pickModels` is provided (test injection),
|
|
1823
|
+
// skip the TTY guard — the test harness is responsible for the picker.
|
|
1824
|
+
const pickModelsFn = deps.pickModels || pickModels;
|
|
1825
|
+
if (!process.stdin.isTTY && !deps.pickModels) {
|
|
1597
1826
|
if (wantJson) {
|
|
1598
1827
|
process.stdout.write(JSON.stringify({
|
|
1599
1828
|
endpoint,
|
|
@@ -1612,7 +1841,24 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1612
1841
|
|
|
1613
1842
|
const router = loadRouter(routerPath);
|
|
1614
1843
|
const { models: current } = currentSelection(router);
|
|
1615
|
-
const picked = await
|
|
1844
|
+
const picked = await pickModelsFn({ candidates, current });
|
|
1845
|
+
|
|
1846
|
+
// Phase 2 (10.19.8): the Models.dev catalog fetch moves HERE — only
|
|
1847
|
+
// AFTER the operator has actually confirmed picks. Per-id enrichment
|
|
1848
|
+
// runs in parallel with bounded concurrency (8 workers) and a per-id
|
|
1849
|
+
// timeout (3000ms); per-id failures fall back to the Phase 1
|
|
1850
|
+
// `_gateway.name` contract.
|
|
1851
|
+
const fetchModelsDevCatalogFn = deps.fetchModelsDevCatalog || fetchModelsDevCatalog;
|
|
1852
|
+
const enrichment = await enrichPicksByMetadata({
|
|
1853
|
+
candidates,
|
|
1854
|
+
pickedIds: picked,
|
|
1855
|
+
fetchFn: fetchModelsDevCatalogFn,
|
|
1856
|
+
});
|
|
1857
|
+
const profilesMap = enrichment.profiles;
|
|
1858
|
+
const modelsDevStatus = enrichment.modelsDev && Object.keys(enrichment.modelsDev).length > 0
|
|
1859
|
+
? { status: 'ok', source: MODELS_DEV_CATALOG_URL, matched: [...profilesMap.values()].filter((p) => p && p.metadata && p.metadata.source === 'models.dev').length, total: picked.length }
|
|
1860
|
+
: { status: 'unavailable', source: MODELS_DEV_CATALOG_URL, matched: 0, total: picked.length, note: 'wholesale fetch failed; per-id enrichment degraded to _gateway.name fallback' };
|
|
1861
|
+
|
|
1616
1862
|
if (picked.length === 0) {
|
|
1617
1863
|
const block = applyModels({ routerPath, models: [], source: 'live-pick' });
|
|
1618
1864
|
// Clear Claude Code modelOverrides + modelPicker when the picker is
|
|
@@ -1620,7 +1866,14 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1620
1866
|
applyModelOverrides({ pickedIds: [], liveIds: [] });
|
|
1621
1867
|
applyModelPicker({ pickedIds: [], liveIds: [] });
|
|
1622
1868
|
if (wantJson) {
|
|
1623
|
-
process.stdout.write(JSON.stringify({
|
|
1869
|
+
process.stdout.write(JSON.stringify({
|
|
1870
|
+
applied: block,
|
|
1871
|
+
endpoint,
|
|
1872
|
+
endpointSource,
|
|
1873
|
+
enriched: [],
|
|
1874
|
+
profiles: {},
|
|
1875
|
+
modelsDev: modelsDevStatus,
|
|
1876
|
+
}, null, 2) + '\n');
|
|
1624
1877
|
} else {
|
|
1625
1878
|
console.log(chalk.yellow(' ! No models selected - userSelected is now empty; orchestrator will fall back to session-only.'));
|
|
1626
1879
|
}
|
|
@@ -1630,7 +1883,7 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1630
1883
|
const profiles = {};
|
|
1631
1884
|
for (const id of picked) {
|
|
1632
1885
|
tierHints[id] = defaultTierHint(id);
|
|
1633
|
-
const profile =
|
|
1886
|
+
const profile = profilesMap.get(id);
|
|
1634
1887
|
if (profile) profiles[id] = profile;
|
|
1635
1888
|
}
|
|
1636
1889
|
// F-191 / 10.19.2 — stale-ID detection: the picker shows candidates
|
|
@@ -1649,7 +1902,21 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1649
1902
|
// picks drive the picker without relying on gateway discovery.
|
|
1650
1903
|
const picker = applyModelPicker({ pickedIds: picked, profiles, liveIds });
|
|
1651
1904
|
if (wantJson) {
|
|
1652
|
-
|
|
1905
|
+
// Phase 2 (10.19.8): the interactive JSON output gains an `enriched`
|
|
1906
|
+
// key naming the picked IDs that received Models.dev enrichment. For
|
|
1907
|
+
// Phase 2 the array equals the picks list (no filtering); Phase 4
|
|
1908
|
+
// will filter to only the IDs that actually received a profile.
|
|
1909
|
+
const enriched = picked.slice();
|
|
1910
|
+
process.stdout.write(JSON.stringify({
|
|
1911
|
+
applied: block,
|
|
1912
|
+
endpoint,
|
|
1913
|
+
endpointSource,
|
|
1914
|
+
enriched,
|
|
1915
|
+
profiles,
|
|
1916
|
+
modelsDev: modelsDevStatus,
|
|
1917
|
+
sync,
|
|
1918
|
+
picker,
|
|
1919
|
+
}, null, 2) + '\n');
|
|
1653
1920
|
} else {
|
|
1654
1921
|
console.log(chalk.green(`\n v Saved ${block.models.length} model(s) to ${routerPath}:`));
|
|
1655
1922
|
console.log(chalk.dim(` Models.dev profiles: ${Object.keys(block.profiles || {}).length}/${block.models.length}`));
|
package/package.json
CHANGED