@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.
@@ -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: 'not-attempted', matched: 0, total: 0 };
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
- candidates = await listModels({ endpoint, authToken, retryWithoutAuth: isDeprecatedAlias });
1628
- try {
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
- if (!process.stdin.isTTY) {
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 pickModels({ candidates, current });
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({ applied: block, endpoint, endpointSource }, null, 2) + '\n');
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 = candidates.find((candidate) => candidate.id === id)?.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
- process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync, picker }, null, 2) + '\n');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.19.7",
3
+ "version": "10.19.8",
4
4
  "description": "Autonomous, human-in-the-loop multi-agent harness for Claude Code with guarded workflows, typed SDK primitives, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export declare const SDK_VERSION: "10.19.7";
4
+ export declare const SDK_VERSION: "10.19.8";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export const SDK_VERSION = "10.19.7";
4
+ export const SDK_VERSION = "10.19.8";
5
5
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-sdk",
3
- "version": "10.19.7",
3
+ "version": "10.19.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",