@goodandready/dsh-model-sync 0.3.12 → 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.
package/lib/index.js CHANGED
@@ -1,163 +1,172 @@
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
- let saveQueue = Promise.resolve()
119
- saveConfigImpl = (patch) => {
120
- const nextTask = saveQueue.then(async () => {
121
- const result = await scope.update(patch)
122
- liveConfig = resolveConfig(scope.get() || baseConfig)
123
- return result
124
- })
125
- saveQueue = nextTask.catch(() => {})
126
- return nextTask
127
- }
128
- ownerCtx.effect(() => () => {
129
- settingsScope = undefined
130
- saveConfigImpl = undefined
131
- stop()
132
- }, 'dsh-model-sync: settings')
133
- return true
134
- }
135
-
136
- let settings
137
- try { settings = ctx.get('settings') } catch {}
138
- if (!bindSettings(settings, ctx)) {
139
- ctx.inject(['settings'], (sctx) => {
140
- bindSettings((typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? sctx.settings, sctx)
141
- })
142
- }
143
-
144
-
145
- const service = createProviderService(ctx)
146
- const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
147
- const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
148
- reconfigure = () => scheduler.reconfigure()
149
- scheduler.start()
150
- ctx.effect(() => () => scheduler.stop(), 'dsh-model-sync: scheduler cleanup')
151
- ctx.effect(() => {
152
- const registrations = registerHttpApi(ctx, synchronizer, { getSchedulerStatus: () => scheduler.status() })
153
- return () => {
154
- for (const dispose of registrations) if (typeof dispose === 'function') dispose()
155
- }
156
- }, 'dsh-model-sync: http api')
157
- ctx.effect(() => {
158
- if (!liveConfig.enabled) return () => {}
159
- ctx.provide('modelSync', service)
160
- ctx.provide('modelSyncRunner', synchronizer)
161
- return () => {}
162
- }, 'dsh-model-sync: service')
163
- }
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
+ aliases: Schema.dict(Schema.object({
67
+ provider: Schema.string().required(),
68
+ model: Schema.string().required(),
69
+ updatedAt: Schema.number().default(0),
70
+ }).loose()).default({}),
71
+ modelSelections: Schema.dict(Schema.array(Schema.string())).default({}),
72
+ adapterRegistry: Schema.dict(Schema.object({
73
+ enabled: Schema.boolean().default(true),
74
+ endpoint: Schema.string().default(''),
75
+ auth: Schema.string().default('bearer'),
76
+ parser: Schema.string().default('openai'),
77
+ modelsPath: Schema.string().default('data'),
78
+ fields: Schema.any().default({}),
79
+ capabilityMap: Schema.any().default({}),
80
+ headers: Schema.dict(Schema.string()).default({}),
81
+ }).loose()).default({}),
82
+ modelPolicies: Schema.dict(Schema.object({
83
+ include: Schema.array(Schema.string()).default([]),
84
+ exclude: Schema.array(Schema.string()).default([]),
85
+ requireCapabilities: Schema.dict(Schema.boolean()).default({}),
86
+ denyCapabilities: Schema.dict(Schema.boolean()).default({}),
87
+ enableCostFilter: Schema.boolean().default(false),
88
+ maxPricePerMillion: Schema.number(),
89
+ enableContextFilter: Schema.boolean().default(false),
90
+ minContextTokens: Schema.number(),
91
+ }).loose()).default({}),
92
+ // Full catalogs are cached separately from the allowlist applied to DSH.
93
+ // This keeps the model picker useful after a restart without widening the
94
+ // active llm-pi-ai catalog.
95
+ modelCatalogs: Schema.dict(Schema.array(Schema.object({
96
+ provider: Schema.string().required(),
97
+ id: Schema.string().required(),
98
+ name: Schema.string().required(),
99
+ }).loose())).default({}),
100
+ })
101
+
102
+ export function apply(ctx, config) {
103
+ const baseConfig = structuredClone(config || {})
104
+ const resolveConfig = (value) => Config(structuredClone(value || {}))
105
+ let liveConfig = resolveConfig(baseConfig)
106
+ let reconfigure = () => {}
107
+ let saveConfigImpl
108
+ // ctx.inject starts a child fiber; keep a stable function reference for the
109
+ // synchronizer while that fiber connects to the Settings service.
110
+ const saveConfig = async (patch) => {
111
+ if (!saveConfigImpl) {
112
+ throw Object.assign(new Error('dsh-model-sync settings are not writable'), { code: 'CONFIG_UNAVAILABLE' })
113
+ }
114
+ return saveConfigImpl(patch)
115
+ }
116
+
117
+ let settingsScope
118
+ const bindSettings = (settings, ownerCtx) => {
119
+ if (settingsScope || !settings?.register) return false
120
+ const scope = settings.register(NS, Config, { base: baseConfig })
121
+ settingsScope = scope
122
+ liveConfig = resolveConfig(scope.get() || baseConfig)
123
+ const stop = scope.watch((next) => {
124
+ liveConfig = resolveConfig(next || baseConfig)
125
+ reconfigure()
126
+ })
127
+ let saveQueue = Promise.resolve()
128
+ saveConfigImpl = (patch) => {
129
+ const nextTask = saveQueue.then(async () => {
130
+ const result = await scope.update(patch)
131
+ liveConfig = resolveConfig(scope.get() || baseConfig)
132
+ return result
133
+ })
134
+ saveQueue = nextTask.catch(() => {})
135
+ return nextTask
136
+ }
137
+ ownerCtx.effect(() => () => {
138
+ settingsScope = undefined
139
+ saveConfigImpl = undefined
140
+ stop()
141
+ }, 'dsh-model-sync: settings')
142
+ return true
143
+ }
144
+
145
+ let settings
146
+ try { settings = ctx.get('settings') } catch {}
147
+ if (!bindSettings(settings, ctx)) {
148
+ ctx.inject(['settings'], (sctx) => {
149
+ bindSettings((typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? sctx.settings, sctx)
150
+ })
151
+ }
152
+
153
+
154
+ const service = createProviderService(ctx)
155
+ const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
156
+ const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
157
+ reconfigure = () => scheduler.reconfigure()
158
+ scheduler.start()
159
+ ctx.effect(() => () => scheduler.stop(), 'dsh-model-sync: scheduler cleanup')
160
+ ctx.effect(() => {
161
+ const registrations = registerHttpApi(ctx, synchronizer, { getSchedulerStatus: () => scheduler.status() })
162
+ return () => {
163
+ for (const dispose of registrations) if (typeof dispose === 'function') dispose()
164
+ }
165
+ }, 'dsh-model-sync: http api')
166
+ ctx.effect(() => {
167
+ if (!liveConfig.enabled) return () => {}
168
+ ctx.provide('modelSync', service)
169
+ ctx.provide('modelSyncRunner', synchronizer)
170
+ return () => {}
171
+ }, 'dsh-model-sync: service')
172
+ }