@goodandready/dsh-model-sync 0.5.2 → 0.5.4

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/http.js CHANGED
@@ -240,6 +240,12 @@ export function normalizeSelectionRequest(body) {
240
240
  }
241
241
 
242
242
  export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () => null } = {}) {
243
+ if (typeof ctx?.webServer?.register !== 'function') {
244
+ if (typeof ctx?.logger?.warn === 'function') {
245
+ ctx.logger.warn('[dsh-model-sync] webServer service unavailable, HTTP API skipped')
246
+ }
247
+ return []
248
+ }
243
249
  const makeHandler = (route) => async (req, res) => {
244
250
  if (!trusted(req, { requireLoopback: req.method === 'POST' })) return json(res, 403, { error: { code: 'forbidden', message: 'dsh-model-sync: cross-origin or non-loopback request rejected' } })
245
251
  if (route === 'status') {
package/lib/index.js CHANGED
@@ -6,31 +6,31 @@ import { registerHttpApi } from './http.js'
6
6
  import { registerPluginUpdater } from './updater.js'
7
7
 
8
8
  export const name = '@goodandready/dsh-model-sync'
9
- export const inject = ['llm', 'settings', 'webServer', 'credentials']
9
+ export const inject = ['llm', 'webServer', 'credentials']
10
10
 
11
11
  const NS = 'dsh-model-sync'
12
12
 
13
13
  export const Config = Schema.object({
14
- enabled: Schema.boolean().default(true),
15
- intervalMinutes: Schema.number().default(60),
16
- scheduleEnabled: Schema.boolean().default(false),
17
- jitterMinutes: Schema.number().default(0),
18
- ttlMinutes: Schema.number().default(0),
19
- autoApply: Schema.boolean().default(false),
20
- requestTimeoutMs: Schema.number().default(15000),
21
- retryAttempts: Schema.number().default(3),
22
- retryBaseDelayMs: Schema.number().default(250),
23
- retryMaxDelayMs: Schema.number().default(4000),
24
- concurrency: Schema.number().default(4),
25
- circuitBreakerFailures: Schema.number().default(3),
26
- circuitBreakerCooldownMinutes: Schema.number().default(5),
27
- historyLimit: Schema.number().default(50),
28
- notificationLimit: Schema.number().default(50),
29
- diffWebhookUrl: Schema.string().default(),
30
- diffWebhookSecret: Schema.string().default(),
31
- notifyOnModelAdded: Schema.boolean().default(true),
32
- notifyOnModelDeprecated: Schema.boolean().default(true),
33
- notifyOnPriceChanged: Schema.boolean().default(true),
14
+ enabled: Schema.boolean().default(true).volatile(),
15
+ intervalMinutes: Schema.number().default(60).volatile(),
16
+ scheduleEnabled: Schema.boolean().default(false).volatile(),
17
+ jitterMinutes: Schema.number().default(0).volatile(),
18
+ ttlMinutes: Schema.number().default(0).volatile(),
19
+ autoApply: Schema.boolean().default(false).volatile(),
20
+ requestTimeoutMs: Schema.number().default(15000).volatile(),
21
+ retryAttempts: Schema.number().default(3).volatile(),
22
+ retryBaseDelayMs: Schema.number().default(250).volatile(),
23
+ retryMaxDelayMs: Schema.number().default(4000).volatile(),
24
+ concurrency: Schema.number().default(4).volatile(),
25
+ circuitBreakerFailures: Schema.number().default(3).volatile(),
26
+ circuitBreakerCooldownMinutes: Schema.number().default(5).volatile(),
27
+ historyLimit: Schema.number().default(50).volatile(),
28
+ notificationLimit: Schema.number().default(50).volatile(),
29
+ diffWebhookUrl: Schema.string().default().volatile(),
30
+ diffWebhookSecret: Schema.string().default().volatile(),
31
+ notifyOnModelAdded: Schema.boolean().default(true).volatile(),
32
+ notifyOnModelDeprecated: Schema.boolean().default(true).volatile(),
33
+ notifyOnPriceChanged: Schema.boolean().default(true).volatile(),
34
34
  notifications: Schema.array(Schema.object({
35
35
  id: Schema.string().required(),
36
36
  fingerprint: Schema.string().required(),
@@ -44,9 +44,9 @@ export const Config = Schema.object({
44
44
  message: Schema.string().required(),
45
45
  providers: Schema.array(Schema.object({}).loose()).default([]),
46
46
  }).loose()).default([]),
47
- staleGraceRuns: Schema.number().default(2),
48
- lifecycleRetentionRuns: Schema.number().default(20),
49
- lifecycleRevision: Schema.number().default(0),
47
+ staleGraceRuns: Schema.number().default(2).volatile(),
48
+ lifecycleRetentionRuns: Schema.number().default(20).volatile(),
49
+ lifecycleRevision: Schema.number().default(0).volatile(),
50
50
  modelLifecycle: Schema.dict(Schema.dict(Schema.object({
51
51
  status: Schema.string().required(),
52
52
  consecutiveMissing: Schema.number().default(0),
@@ -79,7 +79,7 @@ export const Config = Schema.object({
79
79
  strategy: Schema.string(),
80
80
  updatedAt: Schema.number().default(0),
81
81
  }).loose()).default({}),
82
- modelSelections: Schema.dict(Schema.array(Schema.string())).default({}),
82
+ modelSelections: Schema.dict(Schema.array(Schema.string())).default({}).volatile(),
83
83
  adapterRegistry: Schema.dict(Schema.object({
84
84
  enabled: Schema.boolean().default(true),
85
85
  endpoint: Schema.string().default(''),
@@ -110,12 +110,46 @@ export const Config = Schema.object({
110
110
  }).loose())).default({}),
111
111
  })
112
112
 
113
+ // DSH serves a namespace's settings form from the volatile fields of its profile
114
+ // entry schema, and a volatile field holds a Volatile box rather than its value.
115
+ // Unwrap before any caller reads one, and read lazily: the Loader mutates the
116
+ // boxes in place and re-announces them with loader/volatile-update.
117
+ export function plainConfig(value) {
118
+ if (value === null || typeof value !== 'object') return value
119
+ if (Array.isArray(value)) return value.map(plainConfig)
120
+ if (typeof value.get === 'function') return plainConfig(value.get())
121
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]))
122
+ }
123
+
113
124
  export function apply(ctx, config) {
114
- const baseConfig = structuredClone(config || {})
115
- const resolveConfig = (value) => Config(structuredClone(value || {}))
125
+ const baseConfig = structuredClone(plainConfig(config || {}))
126
+ const resolveConfig = (value) => plainConfig(Config(plainConfig(value) || {}))
116
127
  let liveConfig = resolveConfig(baseConfig)
117
128
  let reconfigure = () => {}
118
- let saveConfigImpl
129
+ let saveConfigImpl = async (patch) => {
130
+ const plainPatch = plainConfig(patch)
131
+ let settings
132
+ try {
133
+ settings = (typeof ctx.get === 'function' ? ctx.get('settings') : null) ?? ctx.settings
134
+ } catch (err) {
135
+ if (typeof ctx.logger?.debug === 'function') {
136
+ ctx.logger.debug('[dsh-model-sync] settings service read failed:', err)
137
+ }
138
+ }
139
+ if (settings && typeof settings.update === 'function') {
140
+ try {
141
+ await settings.update(NS, plainPatch)
142
+ } catch (err) {
143
+ if (typeof ctx.logger?.warn === 'function') {
144
+ ctx.logger.warn('[dsh-model-sync] settings.update failed:', err)
145
+ }
146
+ }
147
+ }
148
+ liveConfig = resolveConfig({ ...liveConfig, ...plainPatch })
149
+ reconfigure()
150
+ return liveConfig
151
+ }
152
+
119
153
  // ctx.inject starts a child fiber; keep a stable function reference for the
120
154
  // synchronizer while that fiber connects to the Settings service.
121
155
  const saveConfig = async (patch) => {
@@ -125,50 +159,23 @@ export function apply(ctx, config) {
125
159
  return saveConfigImpl(patch)
126
160
  }
127
161
 
128
- let settingsScope
129
- const bindSettings = (settings, ownerCtx) => {
130
- if (settingsScope || !settings?.register) return false
131
- const scope = settings.register(NS, Config, { base: baseConfig })
132
- settingsScope = scope
133
- liveConfig = resolveConfig(scope.get() || baseConfig)
134
- const stop = scope.watch((next) => {
135
- liveConfig = resolveConfig(next || baseConfig)
136
- reconfigure()
137
- })
138
- let saveQueue = Promise.resolve()
139
- saveConfigImpl = (patch) => {
140
- const nextTask = saveQueue.then(async () => {
141
- const result = await scope.update(patch)
142
- liveConfig = resolveConfig(scope.get() || baseConfig)
143
- return result
144
- })
145
- saveQueue = nextTask.catch(() => {})
146
- return nextTask
147
- }
148
- ownerCtx.effect(() => () => {
149
- settingsScope = undefined
150
- saveConfigImpl = undefined
151
- stop()
152
- }, 'dsh-model-sync: settings')
153
- return true
154
- }
155
-
156
- let settings
157
- try { settings = ctx.get('settings') } catch (e) {
158
- if (typeof ctx.logger?.debug === 'function') ctx.logger.debug('[dsh-model-sync] settings service not immediately available:', e)
159
- }
160
- if (!bindSettings(settings, ctx)) {
161
- ctx.inject(['settings'], (sctx) => {
162
- bindSettings((typeof sctx.get === 'function' ? sctx.get('settings') : null) ?? sctx.settings, sctx)
163
- })
164
- }
165
-
166
-
167
162
  const service = createProviderService(ctx)
168
163
  const synchronizer = createModelSynchronizer(ctx, { getConfig: () => liveConfig, saveConfig })
169
164
  const scheduler = createSyncScheduler(synchronizer, { getConfig: () => liveConfig })
170
165
  reconfigure = () => scheduler.reconfigure()
171
166
  scheduler.start()
167
+
168
+ if (typeof ctx.on === 'function') {
169
+ ctx.on('loader/volatile-update', () => {
170
+ liveConfig = resolveConfig(config)
171
+ reconfigure()
172
+ })
173
+ }
174
+
175
+ // settings.register was removed before 0.1.7-rc.2. The host hands the profile entry
176
+ // config to apply() directly, and liveConfig above is derived from it; the
177
+ // Loader mutates the boxes in place and re-announces them with
178
+ // loader/volatile-update.
172
179
  ctx.effect(() => () => scheduler.stop(), 'dsh-model-sync: scheduler cleanup')
173
180
  ctx.effect(() => {
174
181
  const registrations = registerHttpApi(ctx, synchronizer, { getSchedulerStatus: () => scheduler.status() })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-model-sync",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Automatic model catalog synchronization for API-key DeepSeek Harness providers.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -53,7 +53,7 @@
53
53
  "license": "MIT",
54
54
  "peerDependencies": {
55
55
  "@deepseek-ai/cordis": "^4.0.1",
56
- "@deepseek-ai/schemastery": "^3.18.1",
56
+ "@deepseek-ai/schemastery": "^3.18.4",
57
57
  "@deepseek-ai/dsh-llm": "^0.1.7-rc.2 || ^0.2.0-rc.1"
58
58
  }
59
59
  }