@polderlabs/bizar 10.20.0 → 10.20.1
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 +214 -0
- package/package.json +1 -1
- package/packages/sdk/package.json +1 -1
package/cli/commands/models.mjs
CHANGED
|
@@ -1473,6 +1473,168 @@ async function pickModelsInteractive({ ordered, candidates, selected, lastOrder,
|
|
|
1473
1473
|
}
|
|
1474
1474
|
}
|
|
1475
1475
|
|
|
1476
|
+
// ── 10.19.9 Phase 3: post-confirm status screen ──────────────────────────────
|
|
1477
|
+
//
|
|
1478
|
+
// After the operator confirms a picker selection, `bizar models` prints a
|
|
1479
|
+
// status screen showing one row per picked ID with a ✔ / ✖ / ⤳ icon. The
|
|
1480
|
+
// shape mirrors `cli/doctor.mjs#runDoctor` (per-row ✔ / ✖ pattern) so
|
|
1481
|
+
// operators see a familiar surface. The renderer is CLI-only — the
|
|
1482
|
+
// SessionStart hook has no TTY and no outbound HTTP and must NOT import
|
|
1483
|
+
// these helpers.
|
|
1484
|
+
|
|
1485
|
+
const STATUS_ICON = Object.freeze({
|
|
1486
|
+
fresh: '✔', // ✔
|
|
1487
|
+
refreshed: '✔', // ✔
|
|
1488
|
+
unavailable: '✖', // ✖
|
|
1489
|
+
preexisting: '⤳', // ⤳
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Classify one picked id into the four status states the renderer can
|
|
1494
|
+
* print. Pure: no I/O, no side effects.
|
|
1495
|
+
*
|
|
1496
|
+
* 'preexisting' — the operator-confirmed pick already lived in
|
|
1497
|
+
* userSelected.models BEFORE this run. Wins over every
|
|
1498
|
+
* other branch (the operator explicitly re-selected
|
|
1499
|
+
* a known-good pick).
|
|
1500
|
+
* 'unavailable' — the post-confirm Models.dev enrichment returned
|
|
1501
|
+
* profile === null AND no _gateway.name fallback was
|
|
1502
|
+
* available. Surface as ✖.
|
|
1503
|
+
* 'refreshed' — profile exists with metadata.source === 'gateway-
|
|
1504
|
+
* fallback' (Phase 1 contract). Models.dev missed but
|
|
1505
|
+
* the gateway's name field rescued the row. Surface
|
|
1506
|
+
* as ✔ (refreshed).
|
|
1507
|
+
* 'fresh' — profile exists with metadata.source === 'models.dev'
|
|
1508
|
+
* (the Phase 2 enrichment succeeded). Surface as ✔.
|
|
1509
|
+
*
|
|
1510
|
+
* @param {string} id the picked id (e.g. 'anthropic/claude-3-5-sonnet')
|
|
1511
|
+
* @param {object|null} profile the Phase 2 enrichment result for that id
|
|
1512
|
+
* (null when both Models.dev AND _gateway.name missed)
|
|
1513
|
+
* @param {Set<string>} [preExisting] snapshot of userSelected.models BEFORE applyModels
|
|
1514
|
+
* overwrote the block; undefined is treated as empty
|
|
1515
|
+
* @returns {'fresh'|'refreshed'|'unavailable'|'preexisting'}
|
|
1516
|
+
*/
|
|
1517
|
+
export function classifyPickStatus(id, profile, preExisting) {
|
|
1518
|
+
if (preExisting instanceof Set && preExisting.has(id)) return 'preexisting';
|
|
1519
|
+
if (!profile) return 'unavailable';
|
|
1520
|
+
if (profile.metadata && profile.metadata.source === 'gateway-fallback') return 'refreshed';
|
|
1521
|
+
return 'fresh';
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Resolve the profile object from the heterogeneous shapes Phase 2 hands
|
|
1526
|
+
* us: a `Map<string, object|null>` when called from `run()`, a plain
|
|
1527
|
+
* object when called from a fixture, or `undefined` for "the enrichment
|
|
1528
|
+
* step never ran for this id".
|
|
1529
|
+
*/
|
|
1530
|
+
function resolveProfile(profiles, id) {
|
|
1531
|
+
if (!profiles) return null;
|
|
1532
|
+
if (profiles instanceof Map) {
|
|
1533
|
+
return profiles.has(id) ? profiles.get(id) : null;
|
|
1534
|
+
}
|
|
1535
|
+
if (typeof profiles === 'object') {
|
|
1536
|
+
return Object.prototype.hasOwnProperty.call(profiles, id) ? profiles[id] : null;
|
|
1537
|
+
}
|
|
1538
|
+
return null;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function formatStatusRow({ id, profile, status }) {
|
|
1542
|
+
const icon = STATUS_ICON[status];
|
|
1543
|
+
if (status === 'preexisting') {
|
|
1544
|
+
return chalk.dim(` ${icon} ${id} (already in userSelected)`);
|
|
1545
|
+
}
|
|
1546
|
+
if (status === 'unavailable') {
|
|
1547
|
+
// Profile is null AND no _gateway.name fallback. The renderer never
|
|
1548
|
+
// reaches this branch when Phase 2's _gateway.name plumbing survived.
|
|
1549
|
+
return chalk.red(` ${icon} ${id} (metadata unavailable)`);
|
|
1550
|
+
}
|
|
1551
|
+
// 'fresh' or 'refreshed' — both render as ✔ with the model name.
|
|
1552
|
+
// For 'refreshed' the only label available is the Phase 1 _gateway.name;
|
|
1553
|
+
// for 'fresh' we prefer Models.dev's name and fall back to _gateway.name.
|
|
1554
|
+
const label = (profile && profile.name)
|
|
1555
|
+
|| (profile && profile._gateway && profile._gateway.name)
|
|
1556
|
+
|| id;
|
|
1557
|
+
return chalk.green(` ${icon} ${id} (${label})`);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Render the post-confirm status screen for one picker run.
|
|
1562
|
+
*
|
|
1563
|
+
* CLI-only surface — do NOT import from the SessionStart hook (no TTY,
|
|
1564
|
+
* no outbound HTTP). The helper writes to `out` based on `isTTY`:
|
|
1565
|
+
* - isTTY=true → one ` <icon> <id> (<label>)` line per picked id
|
|
1566
|
+
* plus a footer `N passed, M failed, K skipped`.
|
|
1567
|
+
* - isTTY=false → one collapsed line ` v N passed, M failed, K skipped`
|
|
1568
|
+
* appended to the existing "Saved" block (so piped
|
|
1569
|
+
* callers see one summary line, not a flood of rows).
|
|
1570
|
+
*
|
|
1571
|
+
* Always returns `{ perPick, totals, exitCode }` so `--json` callers can
|
|
1572
|
+
* embed the data in the JSON envelope without re-implementing the
|
|
1573
|
+
* classification:
|
|
1574
|
+
* - status.perPick = [{ id, status, hasProfile }]
|
|
1575
|
+
* - status.totals = { passed, failed, skipped }
|
|
1576
|
+
* - exitCode = 0 when at least one ✔; 2 when EVERY row is ✖;
|
|
1577
|
+
* 0 otherwise (mixed picks, or every pick ⤳).
|
|
1578
|
+
*
|
|
1579
|
+
* The caller decides whether to call `process.exit(exitCode)`; the
|
|
1580
|
+
* renderer never exits on its own.
|
|
1581
|
+
*
|
|
1582
|
+
* @param {object} opts
|
|
1583
|
+
* @param {string[]} opts.picked ids the operator confirmed
|
|
1584
|
+
* @param {Map<string, object|null>|object} [opts.profiles] Phase 2 enrichment map
|
|
1585
|
+
* @param {Set<string>} [opts.preExisting] userSelected.models snapshot BEFORE applyModels
|
|
1586
|
+
* @param {object} [opts.fetchSummary] unused today; reserved for Phase 4 disable surfacing
|
|
1587
|
+
* @param {NodeJS.WritableStream} [opts.out] defaults to process.stdout (test fixtures inject a stub)
|
|
1588
|
+
* @param {boolean} [opts.isTTY=true] when false, print the single-line collapse
|
|
1589
|
+
* @returns {{ perPick: Array<{id:string, status:string, hasProfile:boolean}>, totals: {passed:number, failed:number, skipped:number}, exitCode: 0|2 }}
|
|
1590
|
+
*/
|
|
1591
|
+
export function renderPickStatusScreen({
|
|
1592
|
+
picked = [],
|
|
1593
|
+
profiles,
|
|
1594
|
+
preExisting,
|
|
1595
|
+
fetchSummary = null,
|
|
1596
|
+
out = process.stdout,
|
|
1597
|
+
isTTY = true,
|
|
1598
|
+
} = {}) {
|
|
1599
|
+
const perPick = [];
|
|
1600
|
+
let passed = 0;
|
|
1601
|
+
let failed = 0;
|
|
1602
|
+
let skipped = 0;
|
|
1603
|
+
for (const id of picked) {
|
|
1604
|
+
const profile = resolveProfile(profiles, id);
|
|
1605
|
+
const status = classifyPickStatus(id, profile, preExisting);
|
|
1606
|
+
const hasProfile = !!profile;
|
|
1607
|
+
perPick.push({ id, status, hasProfile });
|
|
1608
|
+
if (status === 'preexisting') skipped += 1;
|
|
1609
|
+
else if (status === 'unavailable') failed += 1;
|
|
1610
|
+
else passed += 1;
|
|
1611
|
+
}
|
|
1612
|
+
const totals = { passed, failed, skipped };
|
|
1613
|
+
|
|
1614
|
+
if (isTTY) {
|
|
1615
|
+
for (const entry of perPick) {
|
|
1616
|
+
const profile = resolveProfile(profiles, entry.id);
|
|
1617
|
+
out.write(formatStatusRow({ id: entry.id, profile, status: entry.status }) + '\n');
|
|
1618
|
+
}
|
|
1619
|
+
const summary = chalk.dim(` ${passed} passed, ${failed} failed, ${skipped} skipped`);
|
|
1620
|
+
out.write(summary + '\n');
|
|
1621
|
+
} else {
|
|
1622
|
+
const summary = chalk.dim(` v ${passed} passed, ${failed} failed, ${skipped} skipped`);
|
|
1623
|
+
out.write(summary + '\n');
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Exit-code contract: 0 when at least one ✔; 2 only when EVERY row is ✖.
|
|
1627
|
+
// Mixed picks and all-⤳ picks both exit 0 (the operator got a usable
|
|
1628
|
+
// confirmation, even if some picks couldn't be enriched).
|
|
1629
|
+
const exitCode = (passed > 0)
|
|
1630
|
+
? 0
|
|
1631
|
+
: (failed === picked.length && picked.length > 0)
|
|
1632
|
+
? 2
|
|
1633
|
+
: 0;
|
|
1634
|
+
|
|
1635
|
+
return { perPick, totals, exitCode };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1476
1638
|
/**
|
|
1477
1639
|
* Build a small async iterator over `stdin` lines.
|
|
1478
1640
|
* Returns null from `next()` when the stream ends.
|
|
@@ -1582,6 +1744,24 @@ function showHelp() {
|
|
|
1582
1744
|
userSelected. If userSelected is empty, every dispatch inherits the
|
|
1583
1745
|
active session model.
|
|
1584
1746
|
|
|
1747
|
+
Post-confirm status screen (interactive only): after the picker saves a
|
|
1748
|
+
selection, every confirmed id is reported on its own row with one of:
|
|
1749
|
+
|
|
1750
|
+
✔ Models.dev profile retrieved (or carried over via the gateway
|
|
1751
|
+
_gateway.name fallback). The label shows the profile name.
|
|
1752
|
+
✖ Models.dev miss AND no _gateway.name fallback. The row prints
|
|
1753
|
+
\`(metadata unavailable)\`.
|
|
1754
|
+
⤳ Id was already in userSelected.models before this run
|
|
1755
|
+
(re-confirmed pick). Label: \`(already in userSelected)\`.
|
|
1756
|
+
|
|
1757
|
+
The screen ends with a footer \`N passed, M failed, K skipped\`. In a
|
|
1758
|
+
TTY the screen is multi-row; in a pipe it collapses to one summary
|
|
1759
|
+
line appended to the existing \"Saved N model(s)\" block. \`--json\`
|
|
1760
|
+
carries the equivalent data in \`status.perPick\` (one entry per
|
|
1761
|
+
picked id) and \`status.totals\` ({passed, failed, skipped}). Exit
|
|
1762
|
+
code is 0 when at least one ✔ was reported, 2 when every row is ✖;
|
|
1763
|
+
mixed ✔+✖ still exits 0.
|
|
1764
|
+
|
|
1585
1765
|
Endpoint resolution order: $BIZAR_MODEL_ROUTER_URL / $ANTHROPIC_BASE_URL
|
|
1586
1766
|
-> ~/.claude/settings.json#env.BIZAR_MODEL_ROUTER_URL
|
|
1587
1767
|
-> model-router.json#endpoint
|
|
@@ -1841,6 +2021,12 @@ export async function run(name, args, isHelpRequest, deps = {}) {
|
|
|
1841
2021
|
|
|
1842
2022
|
const router = loadRouter(routerPath);
|
|
1843
2023
|
const { models: current } = currentSelection(router);
|
|
2024
|
+
// 10.19.9 Phase 3: capture a snapshot of userSelected.models BEFORE
|
|
2025
|
+
// applyModels overwrites the block, so the post-confirm status screen
|
|
2026
|
+
// can classify re-confirmed picks as `preexisting` (⤳) instead of
|
|
2027
|
+
// `fresh` (✔). `current` is read once and shared with pickModelsFn;
|
|
2028
|
+
// we wrap it in a Set so classifyPickStatus can use `.has(id)`.
|
|
2029
|
+
const preExisting = new Set(current);
|
|
1844
2030
|
const picked = await pickModelsFn({ candidates, current });
|
|
1845
2031
|
|
|
1846
2032
|
// Phase 2 (10.19.8): the Models.dev catalog fetch moves HERE — only
|
|
@@ -1907,6 +2093,18 @@ export async function run(name, args, isHelpRequest, deps = {}) {
|
|
|
1907
2093
|
// Phase 2 the array equals the picks list (no filtering); Phase 4
|
|
1908
2094
|
// will filter to only the IDs that actually received a profile.
|
|
1909
2095
|
const enriched = picked.slice();
|
|
2096
|
+
// 10.19.9 Phase 3: --json gains `status.perPick` + `status.totals`.
|
|
2097
|
+
// The renderer writes nothing to stdout in --json mode (out is a
|
|
2098
|
+
// no-op writable; isTTY=false keeps the single-line collapse from
|
|
2099
|
+
// leaking into the JSON stream). The JSON envelope carries the
|
|
2100
|
+
// equivalent data shape.
|
|
2101
|
+
const statusResult = renderPickStatusScreen({
|
|
2102
|
+
picked,
|
|
2103
|
+
profiles: profilesMap,
|
|
2104
|
+
preExisting,
|
|
2105
|
+
out: { write: () => true },
|
|
2106
|
+
isTTY: false,
|
|
2107
|
+
});
|
|
1910
2108
|
process.stdout.write(JSON.stringify({
|
|
1911
2109
|
applied: block,
|
|
1912
2110
|
endpoint,
|
|
@@ -1916,7 +2114,9 @@ export async function run(name, args, isHelpRequest, deps = {}) {
|
|
|
1916
2114
|
modelsDev: modelsDevStatus,
|
|
1917
2115
|
sync,
|
|
1918
2116
|
picker,
|
|
2117
|
+
status: { perPick: statusResult.perPick, totals: statusResult.totals, exitCode: statusResult.exitCode },
|
|
1919
2118
|
}, null, 2) + '\n');
|
|
2119
|
+
if (statusResult.exitCode !== 0) process.exitCode = statusResult.exitCode;
|
|
1920
2120
|
} else {
|
|
1921
2121
|
console.log(chalk.green(`\n v Saved ${block.models.length} model(s) to ${routerPath}:`));
|
|
1922
2122
|
console.log(chalk.dim(` Models.dev profiles: ${Object.keys(block.profiles || {}).length}/${block.models.length}`));
|
|
@@ -1930,6 +2130,20 @@ export async function run(name, args, isHelpRequest, deps = {}) {
|
|
|
1930
2130
|
if (picker.wrote) {
|
|
1931
2131
|
console.log(chalk.dim(` /model picker populated with ${picker.options.length} entr${picker.options.length === 1 ? 'y' : 'ies'}`));
|
|
1932
2132
|
}
|
|
2133
|
+
// 10.19.9 Phase 3: post-confirm status screen. Prints one ✔ / ✖ / ⤳
|
|
2134
|
+
// row per picked id + an `N passed, M failed, K skipped` footer when
|
|
2135
|
+
// stdout is a TTY, or a single collapsed line when piped. Empty-pick
|
|
2136
|
+
// (the `picked.length === 0` branch above) intentionally skips the
|
|
2137
|
+
// screen — the chalk.yellow "No models selected" line is the only
|
|
2138
|
+
// operator feedback there.
|
|
2139
|
+
const statusResult = renderPickStatusScreen({
|
|
2140
|
+
picked,
|
|
2141
|
+
profiles: profilesMap,
|
|
2142
|
+
preExisting,
|
|
2143
|
+
out: process.stdout,
|
|
2144
|
+
isTTY: !!process.stdout.isTTY,
|
|
2145
|
+
});
|
|
2146
|
+
if (statusResult.exitCode !== 0) process.exitCode = statusResult.exitCode;
|
|
1933
2147
|
}
|
|
1934
2148
|
return true;
|
|
1935
2149
|
}
|
package/package.json
CHANGED