@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 +6 -0
- package/lib/index.js +74 -67
- package/package.json +2 -2
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', '
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
}
|