@goodandready/dsh-model-sync 0.3.0 → 0.3.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/lib/index.js CHANGED
@@ -1,157 +1,158 @@
1
- import Schema from '@deepseek-ai/schemastery'
2
- import { createProviderService } from './provider-service.js'
3
- import { createModelSynchronizer } from './synchronizer.js'
4
- import { createSyncScheduler } from './scheduler.js'
5
- import { registerHttpApi } from './http.js'
6
-
7
- export const name = '@goodandready/dsh-model-sync'
8
- export const inject = ['llm', 'settings', 'webServer', 'credentials']
9
-
10
- const NS = 'dsh-model-sync'
11
-
12
- export const Config = Schema.object({
13
- enabled: Schema.boolean().default(true),
14
- intervalMinutes: Schema.number().default(60),
15
- scheduleEnabled: Schema.boolean().default(false),
16
- jitterMinutes: Schema.number().default(0),
17
- ttlMinutes: Schema.number().default(0),
18
- autoApply: Schema.boolean().default(false),
19
- requestTimeoutMs: Schema.number().default(15000),
20
- retryAttempts: Schema.number().default(3),
21
- retryBaseDelayMs: Schema.number().default(250),
22
- retryMaxDelayMs: Schema.number().default(4000),
23
- concurrency: Schema.number().default(4),
24
- circuitBreakerFailures: Schema.number().default(3),
25
- circuitBreakerCooldownMinutes: Schema.number().default(5),
26
- historyLimit: Schema.number().default(50),
27
- notificationLimit: Schema.number().default(50),
28
- notifications: Schema.array(Schema.object({
29
- id: Schema.string().required(),
30
- fingerprint: Schema.string().required(),
31
- createdAt: Schema.number().required(),
32
- updatedAt: Schema.number().required(),
33
- occurrences: Schema.number().default(1),
34
- readAt: Schema.any().default(null),
35
- acknowledgedAt: Schema.any().default(null),
36
- severity: Schema.string().required(),
37
- title: Schema.string().required(),
38
- message: Schema.string().required(),
39
- providers: Schema.array(Schema.object({}).loose()).default([]),
40
- }).loose()).default([]),
41
- staleGraceRuns: Schema.number().default(2),
42
- lifecycleRetentionRuns: Schema.number().default(20),
43
- lifecycleRevision: Schema.number().default(0),
44
- modelLifecycle: Schema.dict(Schema.dict(Schema.object({
45
- status: Schema.string().required(),
46
- consecutiveMissing: Schema.number().default(0),
47
- firstMissingAt: Schema.number().default(0),
48
- lastSeenAt: Schema.number().default(0),
49
- lastObservedRun: Schema.number().default(0),
50
- }).loose())).default({}),
51
- history: Schema.array(Schema.object({
52
- id: Schema.string().required(),
53
- version: Schema.number().required(),
54
- startedAt: Schema.number().required(),
55
- finishedAt: Schema.number().required(),
56
- providers: Schema.array(Schema.object({}).loose()).default([]),
57
- }).loose()).default([]),
58
- providers: Schema.array(Schema.object({
59
- provider: Schema.string().required(),
60
- enabled: Schema.boolean().default(true),
61
- intervalMinutes: Schema.number().default(0),
62
- jitterMinutes: Schema.number().default(0),
63
- ttlMinutes: Schema.number().default(0),
64
- })).default([]),
65
- modelSelections: Schema.dict(Schema.array(Schema.string())).default({}),
66
- adapterRegistry: Schema.dict(Schema.object({
67
- enabled: Schema.boolean().default(true),
68
- endpoint: Schema.string().default(''),
69
- auth: Schema.string().default('bearer'),
70
- parser: Schema.string().default('openai'),
71
- modelsPath: Schema.string().default('data'),
72
- fields: Schema.any().default({}),
73
- capabilityMap: Schema.any().default({}),
74
- headers: Schema.dict(Schema.string()).default({}),
75
- }).loose()).default({}),
76
- modelPolicies: Schema.dict(Schema.object({
77
- include: Schema.array(Schema.string()).default([]),
78
- exclude: Schema.array(Schema.string()).default([]),
79
- requireCapabilities: Schema.dict(Schema.boolean()).default({}),
80
- denyCapabilities: Schema.dict(Schema.boolean()).default({}),
81
- }).loose()).default({}),
82
- // Full catalogs are cached separately from the allowlist applied to DSH.
83
- // This keeps the model picker useful after a restart without widening the
84
- // active llm-pi-ai catalog.
85
- modelCatalogs: Schema.dict(Schema.array(Schema.object({
86
- provider: Schema.string().required(),
87
- id: Schema.string().required(),
88
- name: Schema.string().required(),
89
- }).loose())).default({}),
90
- })
91
-
92
- export function apply(ctx, config) {
93
- const baseConfig = structuredClone(config || {})
94
- const resolveConfig = (value) => Config(structuredClone(value || {}))
95
- let liveConfig = resolveConfig(baseConfig)
96
- let reconfigure = () => {}
97
- let saveConfigImpl
98
- // ctx.inject starts a child fiber; keep a stable function reference for the
99
- // synchronizer while that fiber connects to the Settings service.
100
- const saveConfig = async (patch) => {
101
- if (!saveConfigImpl) {
102
- throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
103
- }
104
- return saveConfigImpl(patch)
105
- }
106
-
107
- let settingsScope
108
- const bindSettings = (settings, ownerCtx) => {
109
- if (settingsScope || !settings?.register) return false
110
- const scope = settings.register(NS, Config, { base: baseConfig })
111
- settingsScope = scope
112
- liveConfig = resolveConfig(scope.get() || baseConfig)
113
- const stop = scope.watch((next) => {
114
- liveConfig = resolveConfig(next || baseConfig)
115
- reconfigure()
116
- })
117
- saveConfigImpl = async (patch) => {
118
- const result = await scope.update(patch)
119
- liveConfig = resolveConfig(scope.get() || baseConfig)
120
- return result
121
- }
122
- ownerCtx.effect(() => () => {
123
- settingsScope = undefined
124
- saveConfigImpl = undefined
125
- stop()
126
- }, 'dsh-model-sync: settings')
127
- return true
128
- }
129
-
130
- let settings
131
- try { settings = ctx.get('settings') } catch {}
132
- if (!bindSettings(settings, ctx)) {
133
- ctx.inject(['settings'], (sctx) => {
134
- bindSettings(sctx.settings, sctx)
135
- })
136
- }
137
-
138
-
139
- const service = createProviderService(ctx)
140
- const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
141
- const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
142
- reconfigure = () => scheduler.reconfigure()
143
- scheduler.start()
144
- ctx.effect(() => {
145
- const registrations = registerHttpApi(ctx, synchronizer, { getSchedulerStatus: () => scheduler.status() })
146
- ctx.effect(() => () => scheduler.stop(), 'dsh-model-sync: scheduler cleanup')
147
- return () => {
148
- for (const dispose of registrations) if (typeof dispose === 'function') dispose()
149
- }
150
- }, 'dsh-model-sync: http api')
151
- ctx.effect(() => {
152
- if (!liveConfig.enabled) return () => {}
153
- ctx.provide('modelSync', service)
154
- ctx.provide('modelSyncRunner', synchronizer)
155
- return () => {}
156
- }, 'dsh-model-sync: service')
157
- }
1
+ import Schema from '@deepseek-ai/schemastery'
2
+ import { createProviderService } from './provider-service.js'
3
+ import { createModelSynchronizer } from './synchronizer.js'
4
+ import { createSyncScheduler } from './scheduler.js'
5
+ import { registerHttpApi } from './http.js'
6
+
7
+ export const name = '@goodandready/dsh-model-sync'
8
+ export const inject = ['llm', 'settings', 'webServer', 'credentials']
9
+
10
+ const NS = 'dsh-model-sync'
11
+
12
+ export const Config = Schema.object({
13
+ enabled: Schema.boolean().default(true),
14
+ intervalMinutes: Schema.number().default(60),
15
+ scheduleEnabled: Schema.boolean().default(false),
16
+ jitterMinutes: Schema.number().default(0),
17
+ ttlMinutes: Schema.number().default(0),
18
+ autoApply: Schema.boolean().default(false),
19
+ requestTimeoutMs: Schema.number().default(15000),
20
+ retryAttempts: Schema.number().default(3),
21
+ retryBaseDelayMs: Schema.number().default(250),
22
+ retryMaxDelayMs: Schema.number().default(4000),
23
+ concurrency: Schema.number().default(4),
24
+ circuitBreakerFailures: Schema.number().default(3),
25
+ circuitBreakerCooldownMinutes: Schema.number().default(5),
26
+ historyLimit: Schema.number().default(50),
27
+ notificationLimit: Schema.number().default(50),
28
+ notifications: Schema.array(Schema.object({
29
+ id: Schema.string().required(),
30
+ fingerprint: Schema.string().required(),
31
+ createdAt: Schema.number().required(),
32
+ updatedAt: Schema.number().required(),
33
+ occurrences: Schema.number().default(1),
34
+ readAt: Schema.any().default(null),
35
+ acknowledgedAt: Schema.any().default(null),
36
+ severity: Schema.string().required(),
37
+ title: Schema.string().required(),
38
+ message: Schema.string().required(),
39
+ providers: Schema.array(Schema.object({}).loose()).default([]),
40
+ }).loose()).default([]),
41
+ staleGraceRuns: Schema.number().default(2),
42
+ lifecycleRetentionRuns: Schema.number().default(20),
43
+ lifecycleRevision: Schema.number().default(0),
44
+ modelLifecycle: Schema.dict(Schema.dict(Schema.object({
45
+ status: Schema.string().required(),
46
+ consecutiveMissing: Schema.number().default(0),
47
+ firstMissingAt: Schema.number().default(0),
48
+ lastSeenAt: Schema.number().default(0),
49
+ lastObservedRun: Schema.number().default(0),
50
+ }).loose())).default({}),
51
+ history: Schema.array(Schema.object({
52
+ id: Schema.string().required(),
53
+ version: Schema.number().required(),
54
+ startedAt: Schema.number().required(),
55
+ finishedAt: Schema.number().required(),
56
+ providers: Schema.array(Schema.object({}).loose()).default([]),
57
+ }).loose()).default([]),
58
+ providers: Schema.array(Schema.object({
59
+ provider: Schema.string().required(),
60
+ enabled: Schema.boolean().default(true),
61
+ intervalMinutes: Schema.number().default(0),
62
+ jitterMinutes: Schema.number().default(0),
63
+ ttlMinutes: Schema.number().default(0),
64
+ autoApply: Schema.boolean().default(false),
65
+ })).default([]),
66
+ modelSelections: Schema.dict(Schema.array(Schema.string())).default({}),
67
+ adapterRegistry: Schema.dict(Schema.object({
68
+ enabled: Schema.boolean().default(true),
69
+ endpoint: Schema.string().default(''),
70
+ auth: Schema.string().default('bearer'),
71
+ parser: Schema.string().default('openai'),
72
+ modelsPath: Schema.string().default('data'),
73
+ fields: Schema.any().default({}),
74
+ capabilityMap: Schema.any().default({}),
75
+ headers: Schema.dict(Schema.string()).default({}),
76
+ }).loose()).default({}),
77
+ modelPolicies: Schema.dict(Schema.object({
78
+ include: Schema.array(Schema.string()).default([]),
79
+ exclude: Schema.array(Schema.string()).default([]),
80
+ requireCapabilities: Schema.dict(Schema.boolean()).default({}),
81
+ denyCapabilities: Schema.dict(Schema.boolean()).default({}),
82
+ }).loose()).default({}),
83
+ // Full catalogs are cached separately from the allowlist applied to DSH.
84
+ // This keeps the model picker useful after a restart without widening the
85
+ // active llm-pi-ai catalog.
86
+ modelCatalogs: Schema.dict(Schema.array(Schema.object({
87
+ provider: Schema.string().required(),
88
+ id: Schema.string().required(),
89
+ name: Schema.string().required(),
90
+ }).loose())).default({}),
91
+ })
92
+
93
+ export function apply(ctx, config) {
94
+ const baseConfig = structuredClone(config || {})
95
+ const resolveConfig = (value) => Config(structuredClone(value || {}))
96
+ let liveConfig = resolveConfig(baseConfig)
97
+ let reconfigure = () => {}
98
+ let saveConfigImpl
99
+ // ctx.inject starts a child fiber; keep a stable function reference for the
100
+ // synchronizer while that fiber connects to the Settings service.
101
+ const saveConfig = async (patch) => {
102
+ if (!saveConfigImpl) {
103
+ throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
104
+ }
105
+ return saveConfigImpl(patch)
106
+ }
107
+
108
+ let settingsScope
109
+ const bindSettings = (settings, ownerCtx) => {
110
+ if (settingsScope || !settings?.register) return false
111
+ const scope = settings.register(NS, Config, { base: baseConfig })
112
+ settingsScope = scope
113
+ liveConfig = resolveConfig(scope.get() || baseConfig)
114
+ const stop = scope.watch((next) => {
115
+ liveConfig = resolveConfig(next || baseConfig)
116
+ reconfigure()
117
+ })
118
+ saveConfigImpl = async (patch) => {
119
+ const result = await scope.update(patch)
120
+ liveConfig = resolveConfig(scope.get() || baseConfig)
121
+ return result
122
+ }
123
+ ownerCtx.effect(() => () => {
124
+ settingsScope = undefined
125
+ saveConfigImpl = undefined
126
+ stop()
127
+ }, 'dsh-model-sync: settings')
128
+ return true
129
+ }
130
+
131
+ let settings
132
+ try { settings = ctx.get('settings') } catch {}
133
+ if (!bindSettings(settings, ctx)) {
134
+ ctx.inject(['settings'], (sctx) => {
135
+ bindSettings(sctx.settings, sctx)
136
+ })
137
+ }
138
+
139
+
140
+ const service = createProviderService(ctx)
141
+ const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
142
+ const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
143
+ reconfigure = () => scheduler.reconfigure()
144
+ scheduler.start()
145
+ ctx.effect(() => {
146
+ const registrations = registerHttpApi(ctx, synchronizer, { getSchedulerStatus: () => scheduler.status() })
147
+ ctx.effect(() => () => scheduler.stop(), 'dsh-model-sync: scheduler cleanup')
148
+ return () => {
149
+ for (const dispose of registrations) if (typeof dispose === 'function') dispose()
150
+ }
151
+ }, 'dsh-model-sync: http api')
152
+ ctx.effect(() => {
153
+ if (!liveConfig.enabled) return () => {}
154
+ ctx.provide('modelSync', service)
155
+ ctx.provide('modelSyncRunner', synchronizer)
156
+ return () => {}
157
+ }, 'dsh-model-sync: service')
158
+ }