@polderlabs/bizar 10.19.7 → 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
|
@@ -349,6 +349,154 @@ export function enrichModelsWithCapabilities(candidates, catalog) {
|
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
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
|
+
}
|
|
495
|
+
});
|
|
496
|
+
await Promise.all(workers);
|
|
497
|
+
return { profiles: out, modelsDev: catalog };
|
|
498
|
+
}
|
|
499
|
+
|
|
352
500
|
function capabilityLabel(profile) {
|
|
353
501
|
if (!profile) return 'metadata unavailable';
|
|
354
502
|
const caps = [];
|
|
@@ -1454,7 +1602,16 @@ function parseSet(value) {
|
|
|
1454
1602
|
.filter((s) => s.length > 0);
|
|
1455
1603
|
}
|
|
1456
1604
|
|
|
1457
|
-
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.
|
|
1458
1615
|
// `bizar model` remains a deprecated alias; `bizar models` is the canonical surface.
|
|
1459
1616
|
if (name !== 'models' && name !== 'model') return false;
|
|
1460
1617
|
if (isHelpRequest || args.includes('--help') || args.includes('-h')) {
|
|
@@ -1617,32 +1774,21 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1617
1774
|
// We do not short-circuit here — the code below falls into the
|
|
1618
1775
|
// `wantList || isDeprecatedAlias` branch and prints candidate IDs.
|
|
1619
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.
|
|
1620
1783
|
let candidates;
|
|
1621
|
-
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' };
|
|
1622
1785
|
try {
|
|
1623
1786
|
// The deprecated `bizar model` alias preserves the legacy
|
|
1624
1787
|
// "retry-without-auth on 401" behavior so existing scripts keep working.
|
|
1625
1788
|
// The new `bizar models` surface fails fast on 401 with an actionable
|
|
1626
1789
|
// error message (the user explicitly picks models — no silent retry).
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
const catalogue = await fetchModelsDevCatalog({});
|
|
1630
|
-
candidates = enrichModelsWithCapabilities(candidates, catalogue);
|
|
1631
|
-
modelsDev = {
|
|
1632
|
-
status: 'ok',
|
|
1633
|
-
matched: candidates.filter((candidate) => candidate.profile).length,
|
|
1634
|
-
total: candidates.length,
|
|
1635
|
-
source: MODELS_DEV_CATALOG_URL,
|
|
1636
|
-
};
|
|
1637
|
-
} catch (metadataError) {
|
|
1638
|
-
modelsDev = {
|
|
1639
|
-
status: 'unavailable',
|
|
1640
|
-
matched: 0,
|
|
1641
|
-
total: candidates.length,
|
|
1642
|
-
source: MODELS_DEV_CATALOG_URL,
|
|
1643
|
-
error: metadataError instanceof Error ? metadataError.message : String(metadataError),
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1790
|
+
const listModelsFn = deps.listModels || listModels;
|
|
1791
|
+
candidates = await listModelsFn({ endpoint, authToken, retryWithoutAuth: isDeprecatedAlias });
|
|
1646
1792
|
} catch (err) {
|
|
1647
1793
|
if (err.name === 'AbortError' || String(err.message).includes('aborted')) {
|
|
1648
1794
|
console.error(chalk.red(` x Request timed out after 3000ms - ${endpoint}/models`));
|
|
@@ -1673,7 +1819,10 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1673
1819
|
// Interactive picker — only reached when explicitly invoked as `bizar models`
|
|
1674
1820
|
// with NO --list / --clear / --set flags. We avoid running the picker when
|
|
1675
1821
|
// stdin is not a TTY (e.g. CI / npm test), because readline.question would block.
|
|
1676
|
-
|
|
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) {
|
|
1677
1826
|
if (wantJson) {
|
|
1678
1827
|
process.stdout.write(JSON.stringify({
|
|
1679
1828
|
endpoint,
|
|
@@ -1692,7 +1841,24 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1692
1841
|
|
|
1693
1842
|
const router = loadRouter(routerPath);
|
|
1694
1843
|
const { models: current } = currentSelection(router);
|
|
1695
|
-
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
|
+
|
|
1696
1862
|
if (picked.length === 0) {
|
|
1697
1863
|
const block = applyModels({ routerPath, models: [], source: 'live-pick' });
|
|
1698
1864
|
// Clear Claude Code modelOverrides + modelPicker when the picker is
|
|
@@ -1700,7 +1866,14 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1700
1866
|
applyModelOverrides({ pickedIds: [], liveIds: [] });
|
|
1701
1867
|
applyModelPicker({ pickedIds: [], liveIds: [] });
|
|
1702
1868
|
if (wantJson) {
|
|
1703
|
-
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');
|
|
1704
1877
|
} else {
|
|
1705
1878
|
console.log(chalk.yellow(' ! No models selected - userSelected is now empty; orchestrator will fall back to session-only.'));
|
|
1706
1879
|
}
|
|
@@ -1710,7 +1883,7 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1710
1883
|
const profiles = {};
|
|
1711
1884
|
for (const id of picked) {
|
|
1712
1885
|
tierHints[id] = defaultTierHint(id);
|
|
1713
|
-
const profile =
|
|
1886
|
+
const profile = profilesMap.get(id);
|
|
1714
1887
|
if (profile) profiles[id] = profile;
|
|
1715
1888
|
}
|
|
1716
1889
|
// F-191 / 10.19.2 — stale-ID detection: the picker shows candidates
|
|
@@ -1729,7 +1902,21 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1729
1902
|
// picks drive the picker without relying on gateway discovery.
|
|
1730
1903
|
const picker = applyModelPicker({ pickedIds: picked, profiles, liveIds });
|
|
1731
1904
|
if (wantJson) {
|
|
1732
|
-
|
|
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');
|
|
1733
1920
|
} else {
|
|
1734
1921
|
console.log(chalk.green(`\n v Saved ${block.models.length} model(s) to ${routerPath}:`));
|
|
1735
1922
|
console.log(chalk.dim(` Models.dev profiles: ${Object.keys(block.profiles || {}).length}/${block.models.length}`));
|
package/package.json
CHANGED