@polderlabs/bizar 10.19.2 → 10.19.3
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
|
@@ -592,6 +592,109 @@ export function applyModelOverrides({ settingsJsonPath, pickedIds, liveIds = []
|
|
|
592
592
|
return { wrote: true, syncedIds: synced, skippedStale: skipped, settingsPath: path };
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Derive a human-readable label for a model ID. Used to populate the
|
|
597
|
+
* `modelPicker` array in settings.json so Claude Code's `/model` picker
|
|
598
|
+
* displays picked models with something nicer than the raw `provider/name`
|
|
599
|
+
* string.
|
|
600
|
+
*
|
|
601
|
+
* minimax/MiniMax-M3 → "MiniMax M3"
|
|
602
|
+
* codex/gpt-5.6-sol → "GPT 5.6 Sol"
|
|
603
|
+
* qct/qwen3.8-max-preview → "Qwen3.8 Max Preview"
|
|
604
|
+
* openrouter/nvidia/foo:free → "nvidia/foo:free"
|
|
605
|
+
*
|
|
606
|
+
* The gateway-reported `name` (when available on the picked profile) always
|
|
607
|
+
* wins. Falls back to a title-cased rendering of the model segment.
|
|
608
|
+
*
|
|
609
|
+
* @param {string} modelId
|
|
610
|
+
* @param {object} [profile] Optional profile with `name` or `displayName`
|
|
611
|
+
* @returns {string}
|
|
612
|
+
*/
|
|
613
|
+
export function deriveModelLabel(modelId, profile) {
|
|
614
|
+
const name = profile && typeof profile.name === 'string' && profile.name.trim();
|
|
615
|
+
if (name) return name.trim();
|
|
616
|
+
const displayName = profile && typeof profile.displayName === 'string' && profile.displayName.trim();
|
|
617
|
+
if (displayName) return displayName.trim();
|
|
618
|
+
const id = String(modelId || '').trim();
|
|
619
|
+
if (!id) return '';
|
|
620
|
+
// Drop the leading provider segment (`minimax/MiniMax-M3` → `MiniMax-M3`)
|
|
621
|
+
// so the operator sees the model name, not the namespace.
|
|
622
|
+
const slash = id.indexOf('/');
|
|
623
|
+
const tail = slash >= 0 ? id.slice(slash + 1) : id;
|
|
624
|
+
// Split on word boundaries (hyphens / underscores / dots / colons / path
|
|
625
|
+
// separators) and join with spaces. Case is preserved verbatim — `gpt`
|
|
626
|
+
// stays `gpt`, `MiniMax` stays `MiniMax`, `M2.7` stays `M2.7`. Brand
|
|
627
|
+
// casing belongs to the gateway (`name` field), not us.
|
|
628
|
+
return tail
|
|
629
|
+
.replace(/[\\/]+/g, ' ')
|
|
630
|
+
.replace(/[-_]+/g, ' ')
|
|
631
|
+
.replace(/:/g, ' ')
|
|
632
|
+
.replace(/\s+/g, ' ')
|
|
633
|
+
.trim();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Sync `userSelected.models` into Claude Code's `modelPicker` array
|
|
638
|
+
* (settings.json). The picker is what populates `/model` — `modelOverrides`
|
|
639
|
+
* alone only silences diagnostics, it does NOT add entries to the picker.
|
|
640
|
+
*
|
|
641
|
+
* Per Claude Code's settings reference: `modelPicker` is an array of
|
|
642
|
+
* `{ id, label }` entries that replaces the gateway-discovered picker
|
|
643
|
+
* contents. Scope is User-or-managed (settings.json is in scope). Each
|
|
644
|
+
* entry preserves the operator's pick order from `userSelected.models`.
|
|
645
|
+
*
|
|
646
|
+
* Behavior:
|
|
647
|
+
* - Reads settings.json; preserves every other field (env, mcpServers,
|
|
648
|
+
* permissions, hooks, etc.).
|
|
649
|
+
* - Writes `modelPicker` as an array of `{ id, label }`.
|
|
650
|
+
* - Filters out picks the live gateway rejects (same stale-ID contract as
|
|
651
|
+
* `applyModelOverrides`); the surviving picks fill the picker.
|
|
652
|
+
* - Empty pick list → writes `modelPicker: []` so the picker falls back
|
|
653
|
+
* to whatever Claude Code's defaults surface.
|
|
654
|
+
* - Atomic replace via temp-file + rename (matches `applyModels`).
|
|
655
|
+
* - Refuses to overwrite a corrupt settings.json.
|
|
656
|
+
* - When `settingsJsonPath === null`, returns a no-op (tests).
|
|
657
|
+
*
|
|
658
|
+
* @param {{
|
|
659
|
+
* settingsJsonPath?: string|null,
|
|
660
|
+
* pickedIds: string[],
|
|
661
|
+
* profiles?: Record<string, object>,
|
|
662
|
+
* liveIds?: string[],
|
|
663
|
+
* }} opts
|
|
664
|
+
* @returns {{
|
|
665
|
+
* wrote: boolean,
|
|
666
|
+
* entries: Array<{id: string, label: string}>,
|
|
667
|
+
* skippedStale: string[],
|
|
668
|
+
* settingsPath: string|null,
|
|
669
|
+
* }}
|
|
670
|
+
*/
|
|
671
|
+
export function applyModelPicker({ settingsJsonPath, pickedIds, profiles = {}, liveIds = [] }) {
|
|
672
|
+
const path = settingsJsonPath === undefined
|
|
673
|
+
? join(homedir(), '.claude', 'settings.json')
|
|
674
|
+
: settingsJsonPath;
|
|
675
|
+
if (path === null) {
|
|
676
|
+
return { wrote: false, entries: [], skippedStale: [], settingsPath: null };
|
|
677
|
+
}
|
|
678
|
+
const live = new Set(Array.isArray(liveIds) ? liveIds : []);
|
|
679
|
+
const picks = Array.isArray(pickedIds) ? pickedIds.filter((id) => typeof id === 'string' && id.trim()) : [];
|
|
680
|
+
const surviving = live.size === 0 ? picks : picks.filter((id) => live.has(id));
|
|
681
|
+
const skipped = live.size === 0 ? [] : picks.filter((id) => !live.has(id));
|
|
682
|
+
const entries = surviving.map((id) => ({ id, label: deriveModelLabel(id, profiles?.[id]) }));
|
|
683
|
+
|
|
684
|
+
let settings = {};
|
|
685
|
+
if (existsSync(path)) {
|
|
686
|
+
try {
|
|
687
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
688
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) settings = parsed;
|
|
689
|
+
} catch {
|
|
690
|
+
return { wrote: false, entries: [], skippedStale: skipped, settingsPath: path };
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
settings.modelPicker = entries;
|
|
694
|
+
writeAtomic(path, JSON.stringify(settings, null, 2) + '\n');
|
|
695
|
+
return { wrote: true, entries, skippedStale: skipped, settingsPath: path };
|
|
696
|
+
}
|
|
697
|
+
|
|
595
698
|
// ── F-190 / IMP-017 refresh + operator-override preservation ────────────────
|
|
596
699
|
|
|
597
700
|
/**
|
|
@@ -1409,8 +1512,9 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1409
1512
|
// `[claude-code:unrecognized_model]` for any ID the gateway later
|
|
1410
1513
|
// rejects; the operator can re-run `bizar models` to drop them.
|
|
1411
1514
|
const sync = applyModelOverrides({ pickedIds: ids, liveIds: [] });
|
|
1515
|
+
const picker = applyModelPicker({ pickedIds: ids, profiles: block.profiles || {}, liveIds: [] });
|
|
1412
1516
|
if (wantJson) {
|
|
1413
|
-
process.stdout.write(JSON.stringify({ applied: block, sync }, null, 2) + '\n');
|
|
1517
|
+
process.stdout.write(JSON.stringify({ applied: block, sync, picker }, null, 2) + '\n');
|
|
1414
1518
|
} else {
|
|
1415
1519
|
console.log(chalk.green(` v ${block.models.length} model(s) saved to userSelected`));
|
|
1416
1520
|
for (const id of block.models) {
|
|
@@ -1504,9 +1608,10 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1504
1608
|
const picked = await pickModels({ candidates, current });
|
|
1505
1609
|
if (picked.length === 0) {
|
|
1506
1610
|
const block = applyModels({ routerPath, models: [], source: 'live-pick' });
|
|
1507
|
-
// Clear Claude Code modelOverrides when the picker is
|
|
1508
|
-
// session no longer claims to recognise removed IDs.
|
|
1611
|
+
// Clear Claude Code modelOverrides + modelPicker when the picker is
|
|
1612
|
+
// emptied so the session no longer claims to recognise removed IDs.
|
|
1509
1613
|
applyModelOverrides({ pickedIds: [], liveIds: [] });
|
|
1614
|
+
applyModelPicker({ pickedIds: [], liveIds: [] });
|
|
1510
1615
|
if (wantJson) {
|
|
1511
1616
|
process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource }, null, 2) + '\n');
|
|
1512
1617
|
} else {
|
|
@@ -1533,8 +1638,11 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1533
1638
|
block.staleIds = partition.staleIds;
|
|
1534
1639
|
}
|
|
1535
1640
|
const sync = applyModelOverrides({ pickedIds: picked, liveIds });
|
|
1641
|
+
// Sync the /model picker contents (`modelPicker` setting) so the user's
|
|
1642
|
+
// picks drive the picker without relying on gateway discovery.
|
|
1643
|
+
const picker = applyModelPicker({ pickedIds: picked, profiles, liveIds });
|
|
1536
1644
|
if (wantJson) {
|
|
1537
|
-
process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync }, null, 2) + '\n');
|
|
1645
|
+
process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync, picker }, null, 2) + '\n');
|
|
1538
1646
|
} else {
|
|
1539
1647
|
console.log(chalk.green(`\n v Saved ${block.models.length} model(s) to ${routerPath}:`));
|
|
1540
1648
|
console.log(chalk.dim(` Models.dev profiles: ${Object.keys(block.profiles || {}).length}/${block.models.length}`));
|
|
@@ -1545,6 +1653,9 @@ export async function run(name, args, isHelpRequest) {
|
|
|
1545
1653
|
if (sync.wrote && sync.skippedStale.length > 0) {
|
|
1546
1654
|
console.log(chalk.yellow(` Settings sync skipped ${sync.skippedStale.length} stale id(s): ${sync.skippedStale.join(', ')}`));
|
|
1547
1655
|
}
|
|
1656
|
+
if (picker.wrote) {
|
|
1657
|
+
console.log(chalk.dim(` /model picker populated with ${picker.entries.length} entr${picker.entries.length === 1 ? 'y' : 'ies'}`));
|
|
1658
|
+
}
|
|
1548
1659
|
}
|
|
1549
1660
|
return true;
|
|
1550
1661
|
}
|
package/package.json
CHANGED