@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.
- package/README.md +19 -0
- package/README.ru.md +19 -0
- package/README.zh.md +19 -0
- package/lib/client.js +992 -88
- package/lib/http.js +52 -1
- package/lib/index.js +172 -163
- package/lib/policy.js +24 -1
- package/lib/synchronizer.js +144 -1
- package/package.json +1 -1
package/lib/http.js
CHANGED
|
@@ -97,6 +97,27 @@ export function normalizeTryRequest(body) {
|
|
|
97
97
|
return { provider: body.provider, model: body.model }
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
|
|
101
|
+
export function normalizeBatchTryRequest(body) {
|
|
102
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) throw new Error('request body must be an object')
|
|
103
|
+
if (typeof body.provider !== 'string' || !body.provider) throw new Error('provider is required')
|
|
104
|
+
if (!Array.isArray(body.models)) throw new Error('models must be an array')
|
|
105
|
+
return {
|
|
106
|
+
provider: body.provider.trim(),
|
|
107
|
+
models: body.models.filter((m) => typeof m === 'string' && m.trim()).map((m) => m.trim()),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeAliasRequest(body) {
|
|
112
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) throw new Error('request body must be an object')
|
|
113
|
+
if (typeof body.alias !== 'string' || !body.alias.trim()) throw new Error('alias is required')
|
|
114
|
+
if (!/^[a-zA-Z0-9_\-.]{1,64}$/.test(body.alias.trim())) throw new Error('alias must be 1-64 alphanumeric characters or _-.')
|
|
115
|
+
if (body.action === 'delete') return { action: 'delete', alias: body.alias.trim() }
|
|
116
|
+
if (typeof body.provider !== 'string' || !body.provider.trim() || body.provider.length > 128) throw new Error('provider must be a short string')
|
|
117
|
+
if (typeof body.model !== 'string' || !body.model.trim() || body.model.length > 256) throw new Error('model must be a short string')
|
|
118
|
+
return { action: 'set', alias: body.alias.trim(), provider: body.provider.trim(), model: body.model.trim() }
|
|
119
|
+
}
|
|
120
|
+
|
|
100
121
|
export function normalizeHealthRequest(body) {
|
|
101
122
|
if (body === null || typeof body !== 'object' || Array.isArray(body)) throw new Error('request body must be an object')
|
|
102
123
|
if (body.provider !== undefined && (typeof body.provider !== 'string' || body.provider.length > 128)) throw new Error('provider must be a short string')
|
|
@@ -122,6 +143,10 @@ export function normalizePolicyRequest(body) {
|
|
|
122
143
|
exclude: body.exclude,
|
|
123
144
|
requireCapabilities: body.requireCapabilities,
|
|
124
145
|
denyCapabilities: body.denyCapabilities,
|
|
146
|
+
enableCostFilter: body.enableCostFilter,
|
|
147
|
+
maxPricePerMillion: body.maxPricePerMillion,
|
|
148
|
+
enableContextFilter: body.enableContextFilter,
|
|
149
|
+
minContextTokens: body.minContextTokens,
|
|
125
150
|
}),
|
|
126
151
|
}
|
|
127
152
|
}
|
|
@@ -187,6 +212,24 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
187
212
|
try { return json(res, 200, await synchronizer.credentialDiagnostics(normalizeCredentialRequest(req.url))) }
|
|
188
213
|
catch (error) { return json(res, 400, { error: { code: 'bad-request', message: error instanceof Error ? error.message : String(error) } }) }
|
|
189
214
|
}
|
|
215
|
+
if (route === 'export') {
|
|
216
|
+
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
217
|
+
return json(res, 200, synchronizer.exportConfig())
|
|
218
|
+
}
|
|
219
|
+
if (route === 'aliases' && req.method === 'GET') {
|
|
220
|
+
return json(res, 200, { aliases: synchronizer.getAliases() })
|
|
221
|
+
}
|
|
222
|
+
if (route === 'import') {
|
|
223
|
+
if (req.method !== 'POST') return json(res, 405, { error: { code: 'method', message: 'POST only' } })
|
|
224
|
+
try {
|
|
225
|
+
const body = await readJson(req)
|
|
226
|
+
const result = await synchronizer.importConfig(body)
|
|
227
|
+
return json(res, 200, result)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const code = error?.code === 'INVALID_CONFIG' || error?.code === 'INVALID_POLICY' ? 400 : 500
|
|
230
|
+
return json(res, code, { error: { code: error?.code ?? 'import-failed', message: error instanceof Error ? error.message : String(error) } })
|
|
231
|
+
}
|
|
232
|
+
}
|
|
190
233
|
if (route === 'report') {
|
|
191
234
|
if (req.method !== 'GET') return json(res, 405, { error: { code: 'method', message: 'GET only' } })
|
|
192
235
|
return json(res, 200, { report: synchronizer.report() })
|
|
@@ -203,6 +246,8 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
203
246
|
? normalizeSelectionRequest(body)
|
|
204
247
|
: route === 'policy' ? normalizePolicyRequest(body)
|
|
205
248
|
: route === 'try' ? normalizeTryRequest(body)
|
|
249
|
+
: route === 'batch-try' ? normalizeBatchTryRequest(body)
|
|
250
|
+
: route === 'aliases' ? normalizeAliasRequest(body)
|
|
206
251
|
: route === 'health' ? normalizeHealthRequest(body)
|
|
207
252
|
: route === 'credentials-check' ? normalizeCredentialCheckRequest(body)
|
|
208
253
|
: route === 'notification-read' || route === 'notification-acknowledge' ? normalizeNotificationAction(body)
|
|
@@ -215,6 +260,8 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
215
260
|
? await synchronizer.setModelSelection(options.provider, options.models)
|
|
216
261
|
: route === 'policy' ? await synchronizer.setModelPolicy(options.provider, options.policy)
|
|
217
262
|
: route === 'try' ? await synchronizer.tryModel(options)
|
|
263
|
+
: route === 'batch-try' ? await synchronizer.batchTryModels(options)
|
|
264
|
+
: route === 'aliases' ? (options.action === 'delete' ? await synchronizer.deleteAlias(options) : await synchronizer.setAlias(options))
|
|
218
265
|
: route === 'health' ? await synchronizer.health(options)
|
|
219
266
|
: route === 'credentials-check' ? { health: await synchronizer.health(options), credentials: await synchronizer.credentialDiagnostics(options) }
|
|
220
267
|
: route === 'notification-read' ? await synchronizer.updateNotification(options.id, 'read')
|
|
@@ -224,7 +271,7 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
224
271
|
return json(res, 200, { ...result, providers: synchronizer.listProviders() })
|
|
225
272
|
}
|
|
226
273
|
catch (error) {
|
|
227
|
-
const code = error?.code === 'SETTINGS_CONFLICT' ? 409 : error?.code === 'SETTINGS_UNAVAILABLE' || error?.code === 'CONFIG_UNAVAILABLE' ? 503 : error?.code === 'PROVIDER_NOT_CONFIGURED' || error?.code === 'UNKNOWN_MODEL' || error?.code === 'INVALID_POLICY' || error?.code === 'INVALID_HISTORY' || error?.code === 'HISTORY_NOT_FOUND' || error?.code === 'INVALID_NOTIFICATION' || error?.code === 'NOTIFICATION_NOT_FOUND' ? 400 : 500
|
|
274
|
+
const code = error?.code === 'SETTINGS_CONFLICT' ? 409 : error?.code === 'SETTINGS_UNAVAILABLE' || error?.code === 'CONFIG_UNAVAILABLE' ? 503 : error?.code === 'PROVIDER_NOT_CONFIGURED' || error?.code === 'UNKNOWN_MODEL' || error?.code === 'INVALID_POLICY' || error?.code === 'INVALID_HISTORY' || error?.code === 'HISTORY_NOT_FOUND' || error?.code === 'INVALID_NOTIFICATION' || error?.code === 'NOTIFICATION_NOT_FOUND' || error?.code === 'INVALID_ALIAS' || error?.code === 'INVALID_CONFIG' ? 400 : 500
|
|
228
275
|
return json(res, code, { error: { code: error?.code ?? 'run-failed', message: error instanceof Error ? error.message : String(error) } })
|
|
229
276
|
}
|
|
230
277
|
}
|
|
@@ -232,6 +279,10 @@ export function registerHttpApi(ctx, synchronizer, { getSchedulerStatus = () =>
|
|
|
232
279
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/status`, handler: makeHandler('status') }),
|
|
233
280
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/run`, handler: makeHandler('run') }),
|
|
234
281
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/try`, handler: makeHandler('try') }),
|
|
282
|
+
ctx.webServer.register({ kind: 'exact', path: `${BASE}/batch-try`, handler: makeHandler('batch-try') }),
|
|
283
|
+
ctx.webServer.register({ kind: 'exact', path: `${BASE}/export`, handler: makeHandler('export') }),
|
|
284
|
+
ctx.webServer.register({ kind: 'exact', path: `${BASE}/import`, handler: makeHandler('import') }),
|
|
285
|
+
ctx.webServer.register({ kind: 'exact', path: `${BASE}/aliases`, handler: makeHandler('aliases') }),
|
|
235
286
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/health`, handler: makeHandler('health') }),
|
|
236
287
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/selection`, handler: makeHandler('selection') }),
|
|
237
288
|
ctx.webServer.register({ kind: 'exact', path: `${BASE}/policy`, handler: makeHandler('policy') }),
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
package/lib/policy.js
CHANGED
|
@@ -16,12 +16,21 @@ function capabilities(value) {
|
|
|
16
16
|
|
|
17
17
|
export function normalizePolicy(policy) {
|
|
18
18
|
const value = policy && typeof policy === 'object' && !Array.isArray(policy) ? policy : {}
|
|
19
|
-
|
|
19
|
+
const res = {
|
|
20
20
|
include: list(value.include),
|
|
21
21
|
exclude: list(value.exclude),
|
|
22
22
|
requireCapabilities: capabilities(value.requireCapabilities),
|
|
23
23
|
denyCapabilities: capabilities(value.denyCapabilities),
|
|
24
24
|
}
|
|
25
|
+
if (value.enableCostFilter) res.enableCostFilter = true
|
|
26
|
+
if (typeof value.maxPricePerMillion === 'number' && Number.isFinite(value.maxPricePerMillion) && value.maxPricePerMillion >= 0) {
|
|
27
|
+
res.maxPricePerMillion = value.maxPricePerMillion
|
|
28
|
+
}
|
|
29
|
+
if (value.enableContextFilter) res.enableContextFilter = true
|
|
30
|
+
if (typeof value.minContextTokens === 'number' && Number.isFinite(value.minContextTokens) && value.minContextTokens >= 0) {
|
|
31
|
+
res.minContextTokens = Math.floor(value.minContextTokens)
|
|
32
|
+
}
|
|
33
|
+
return res
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
function compilePatterns(patterns, field) {
|
|
@@ -48,6 +57,8 @@ export function hasPolicy(policy) {
|
|
|
48
57
|
|| normalized.exclude.length > 0
|
|
49
58
|
|| Object.keys(normalized.requireCapabilities).length > 0
|
|
50
59
|
|| Object.keys(normalized.denyCapabilities).length > 0
|
|
60
|
+
|| (normalized.enableCostFilter && normalized.maxPricePerMillion !== undefined)
|
|
61
|
+
|| (normalized.enableContextFilter && normalized.minContextTokens !== undefined)
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export function filterModels(models, policy) {
|
|
@@ -66,6 +77,18 @@ export function filterModels(models, policy) {
|
|
|
66
77
|
for (const [key, denied] of Object.entries(normalized.denyCapabilities)) {
|
|
67
78
|
if (modelCapabilities[key] === denied) return false
|
|
68
79
|
}
|
|
80
|
+
if (normalized.enableCostFilter && normalized.maxPricePerMillion !== undefined) {
|
|
81
|
+
const inputPrice = model?.pricing?.inputPerToken
|
|
82
|
+
if (typeof inputPrice === 'number' && Number.isFinite(inputPrice)) {
|
|
83
|
+
if (inputPrice * 1000000 > normalized.maxPricePerMillion) return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (normalized.enableContextFilter && normalized.minContextTokens !== undefined) {
|
|
87
|
+
const context = model?.contextWindow
|
|
88
|
+
if (typeof context === 'number' && Number.isFinite(context)) {
|
|
89
|
+
if (context < normalized.minContextTokens) return false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
69
92
|
return true
|
|
70
93
|
})
|
|
71
94
|
}
|