@goodandready/dsh-model-sync 0.3.13 → 0.3.14

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.
@@ -547,6 +547,34 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
547
547
  ...(entry?.status !== 'ok' ? { error: entry?.message || entry?.status || 'probe failed' } : {}),
548
548
  }
549
549
  }
550
+ async function batchTryModels({ provider, models } = {}) {
551
+ if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
552
+ if (!Array.isArray(models)) throw new Error('models must be an array')
553
+ const list = [...new Set(models.filter((m) => typeof m === 'string' && m.trim()).map((m) => m.trim()))].slice(0, 50)
554
+ const results = []
555
+ const concurrency = 3
556
+ for (let i = 0; i < list.length; i += concurrency) {
557
+ const chunk = list.slice(i, i + concurrency)
558
+ const chunkResults = await Promise.all(
559
+ chunk.map((model) => tryModel({ provider, model }).catch((err) => ({
560
+ provider,
561
+ model,
562
+ ok: false,
563
+ latencyMs: 0,
564
+ status: 'error',
565
+ error: err instanceof Error ? err.message : String(err)
566
+ })))
567
+ )
568
+ results.push(...chunkResults)
569
+ }
570
+ const reachable = results.filter((r) => r.ok).length
571
+ return {
572
+ provider,
573
+ results,
574
+ summary: { total: results.length, reachable, unreachable: results.length - reachable }
575
+ }
576
+ }
577
+
550
578
 
551
579
  async function rollbackHistory({ historyId, provider } = {}) {
552
580
  if (typeof historyId !== 'string' || !historyId) throw Object.assign(new Error('historyId is required'), { code: 'INVALID_HISTORY' })
@@ -632,6 +660,115 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
632
660
  }
633
661
  return { provider, selectedModels, models: nextModels, availableModels: available, selectableModels: safeAvailable, applied: true }
634
662
  }
663
+ function exportConfig() {
664
+ const config = getConfig() ?? {}
665
+ const pmap = {}
666
+ for (const p of providers()) {
667
+ if (p.configured) {
668
+ pmap[p.provider] = {
669
+ policy: config.modelPolicies?.[p.provider] ?? {},
670
+ selectedModels: config.modelSelections?.[p.provider] ?? []
671
+ }
672
+ }
673
+ }
674
+ return {
675
+ version: '1.0',
676
+ exportedAt: new Date().toISOString(),
677
+ plugin: '@goodandready/dsh-model-sync',
678
+ providers: pmap,
679
+ aliases: config.aliases ?? {},
680
+ scheduler: {
681
+ scheduleEnabled: config.scheduleEnabled ?? false,
682
+ intervalMinutes: config.intervalMinutes ?? 60,
683
+ autoApply: config.autoApply ?? false
684
+ }
685
+ }
686
+ }
687
+
688
+ async function importConfig(data) {
689
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
690
+ throw Object.assign(new Error('invalid config payload'), { code: 'INVALID_CONFIG' })
691
+ }
692
+ if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
693
+ const config = getConfig() ?? {}
694
+ const importedProviders = []
695
+ const importedAliases = []
696
+ const patch = {}
697
+
698
+ if (data.providers && typeof data.providers === 'object') {
699
+ const currentPolicies = structuredClone(config.modelPolicies ?? {})
700
+ const currentSelections = structuredClone(config.modelSelections ?? {})
701
+ for (const [pname, pdata] of Object.entries(data.providers)) {
702
+ if (pdata && typeof pdata === 'object') {
703
+ if (pdata.policy) {
704
+ currentPolicies[pname] = validatePolicy(pdata.policy)
705
+ importedProviders.push(pname)
706
+ }
707
+ if (Array.isArray(pdata.selectedModels)) {
708
+ currentSelections[pname] = pdata.selectedModels.filter((m) => typeof m === 'string')
709
+ importedProviders.push(pname)
710
+ }
711
+ }
712
+ }
713
+ patch.modelPolicies = currentPolicies
714
+ patch.modelSelections = currentSelections
715
+ }
716
+
717
+ if (data.aliases && typeof data.aliases === 'object') {
718
+ const currentAliases = structuredClone(config.aliases ?? {})
719
+ for (const [k, v] of Object.entries(data.aliases)) {
720
+ if (typeof k === 'string' && /^[a-zA-Z0-9_\-.]{1,64}$/.test(k.trim()) && v && typeof v === 'object' && typeof v.provider === 'string' && typeof v.model === 'string') {
721
+ currentAliases[k.trim()] = { provider: v.provider, model: v.model, updatedAt: Date.now() }
722
+ importedAliases.push(k.trim())
723
+ }
724
+ }
725
+ patch.aliases = currentAliases
726
+ }
727
+
728
+ if (data.scheduler && typeof data.scheduler === 'object') {
729
+ if (typeof data.scheduler.scheduleEnabled === 'boolean') patch.scheduleEnabled = data.scheduler.scheduleEnabled
730
+ if (typeof data.scheduler.intervalMinutes === 'number') patch.intervalMinutes = data.scheduler.intervalMinutes
731
+ if (typeof data.scheduler.autoApply === 'boolean') patch.autoApply = data.scheduler.autoApply
732
+ }
733
+
734
+ await saveConfig(patch)
735
+ return {
736
+ success: true,
737
+ importedProviders: [...new Set(importedProviders)],
738
+ importedAliases: [...new Set(importedAliases)]
739
+ }
740
+ }
741
+
742
+ function getAliases() {
743
+ const config = getConfig() ?? {}
744
+ return config.aliases ?? {}
745
+ }
746
+
747
+ async function setAlias({ alias, provider, model } = {}) {
748
+ if (typeof alias !== 'string' || !/^[a-zA-Z0-9_\-.]{1,64}$/.test(alias.trim())) {
749
+ throw Object.assign(new Error('alias must be 1-64 alphanumeric characters or _-.'), { code: 'INVALID_ALIAS' })
750
+ }
751
+ const trimmedAlias = alias.trim()
752
+ if (typeof provider !== 'string' || !provider) throw new Error('provider is required')
753
+ if (typeof model !== 'string' || !model) throw new Error('model is required')
754
+ if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
755
+ const config = getConfig() ?? {}
756
+ const currentAliases = structuredClone(config.aliases ?? {})
757
+ currentAliases[trimmedAlias] = { provider, model, updatedAt: Date.now() }
758
+ await saveConfig({ aliases: currentAliases })
759
+ return { alias: trimmedAlias, provider, model, aliases: currentAliases }
760
+ }
761
+
762
+ async function deleteAlias({ alias } = {}) {
763
+ if (typeof alias !== 'string' || !alias) throw new Error('alias is required')
764
+ if (typeof saveConfig !== 'function') throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
765
+ const config = getConfig() ?? {}
766
+ const currentAliases = structuredClone(config.aliases ?? {})
767
+ delete currentAliases[alias.trim()]
768
+ await saveConfig({ aliases: currentAliases })
769
+ return { alias: alias.trim(), deleted: true, aliases: currentAliases }
770
+ }
771
+
635
772
 
636
773
  return Object.freeze({
637
774
  listProviders: providers,
@@ -645,12 +782,18 @@ export function createModelSynchronizer(ctx, { getConfig = () => ({}), saveConfi
645
782
  listPolicies,
646
783
  health,
647
784
  tryModel,
785
+ batchTryModels,
786
+ exportConfig,
787
+ importConfig,
788
+ getAliases,
789
+ setAlias,
790
+ deleteAlias,
648
791
  credentialDiagnostics,
649
792
  history,
650
793
  report: () => structuredClone(lastReport),
651
794
  notifications: notificationLedger,
652
795
  updateNotification: updateNotificationState,
653
796
  rollbackHistory,
654
- status: () => ({ running: Boolean(running), lastRun, lastHealth, lastReport: structuredClone(lastReport), notifications: notificationLedger(), history: history(), policies: listPolicies(), lifecycle: structuredClone(getConfig()?.modelLifecycle ?? {}) }),
797
+ status: () => ({ running: Boolean(running), lastRun, lastHealth, lastReport: structuredClone(lastReport), notifications: notificationLedger(), history: history(), policies: listPolicies(), lifecycle: structuredClone(getConfig()?.modelLifecycle ?? {}), aliases: structuredClone(getConfig()?.aliases ?? {}) }),
655
798
  })
656
799
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-model-sync",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
5
5
  "keywords": [
6
6
  "deepseek-harness",