@goodandready/dsh-moa 0.2.65 → 0.2.66
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/client.js +2000 -2245
- package/lib/index.js +6 -0
- package/lib/moa-model-validator.js +242 -0
- package/lib/moa-service.js +21 -0
- package/lib/routes/presets.js +11 -0
- package/package.json +4 -2
package/lib/index.js
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic Preset Model Validation via ctx.llm Provider Registry (#283)
|
|
3
|
+
* Inspects active providers and models, detects dead/unregistered slots,
|
|
4
|
+
* and produces diagnostic reports for logging and UI surfacing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { logger } from './logger.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Discovers currently registered / active LLM providers in ctx.
|
|
11
|
+
* Queries ctx.llm.listProviders(), ctx.llm.listConfigurableProviders(),
|
|
12
|
+
* and active provider maps.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} ctx
|
|
15
|
+
* @returns {Promise<Set<string>>}
|
|
16
|
+
*/
|
|
17
|
+
export async function getActiveLlmProviders(ctx) {
|
|
18
|
+
const providers = new Set()
|
|
19
|
+
if (!ctx || !ctx.llm) return providers
|
|
20
|
+
|
|
21
|
+
// 1. ctx.llm.listProviders()
|
|
22
|
+
if (typeof ctx.llm.listProviders === 'function') {
|
|
23
|
+
try {
|
|
24
|
+
const list = await ctx.llm.listProviders()
|
|
25
|
+
if (Array.isArray(list)) {
|
|
26
|
+
for (const p of list) {
|
|
27
|
+
const id = typeof p === 'string' ? p : (p?.id || p?.provider)
|
|
28
|
+
if (id) providers.add(String(id).toLowerCase())
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch (_e) {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. ctx.llm.listConfigurableProviders()
|
|
35
|
+
if (typeof ctx.llm.listConfigurableProviders === 'function') {
|
|
36
|
+
try {
|
|
37
|
+
const list = ctx.llm.listConfigurableProviders()
|
|
38
|
+
if (Array.isArray(list)) {
|
|
39
|
+
for (const p of list) {
|
|
40
|
+
const id = typeof p === 'string' ? p : (p?.id || p?.provider)
|
|
41
|
+
if (id) providers.add(String(id).toLowerCase())
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (_e) {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Fallback to ctx.llm.providers object or map
|
|
48
|
+
if (ctx.llm.providers) {
|
|
49
|
+
if (typeof ctx.llm.providers.keys === 'function') {
|
|
50
|
+
for (const k of ctx.llm.providers.keys()) providers.add(String(k).toLowerCase())
|
|
51
|
+
} else if (typeof ctx.llm.providers === 'object') {
|
|
52
|
+
for (const k of Object.keys(ctx.llm.providers)) providers.add(String(k).toLowerCase())
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return providers
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Discovers registered models per provider if available.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} ctx
|
|
63
|
+
* @param {Set<string>} [providers]
|
|
64
|
+
* @returns {Promise<Map<string, Set<string>>>} Map of provider -> Set of model IDs
|
|
65
|
+
*/
|
|
66
|
+
export async function getActiveLlmModels(ctx, providers) {
|
|
67
|
+
const modelMap = new Map()
|
|
68
|
+
if (!ctx || !ctx.llm) return modelMap
|
|
69
|
+
|
|
70
|
+
const targetProviders = providers || await getActiveLlmProviders(ctx)
|
|
71
|
+
|
|
72
|
+
// 1. ctx.llm.listAvailableModels()
|
|
73
|
+
if (typeof ctx.llm.listAvailableModels === 'function') {
|
|
74
|
+
try {
|
|
75
|
+
const all = await ctx.llm.listAvailableModels()
|
|
76
|
+
if (Array.isArray(all)) {
|
|
77
|
+
for (const item of all) {
|
|
78
|
+
const p = String(item?.provider || '').toLowerCase()
|
|
79
|
+
const m = String(item?.id || item?.model || '').toLowerCase()
|
|
80
|
+
if (p && m) {
|
|
81
|
+
if (!modelMap.has(p)) modelMap.set(p, new Set())
|
|
82
|
+
modelMap.get(p).add(m)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (_e) {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. Query each provider with ctx.llm.listModels(p)
|
|
90
|
+
if (typeof ctx.llm.listModels === 'function') {
|
|
91
|
+
for (const p of targetProviders) {
|
|
92
|
+
try {
|
|
93
|
+
const mList = await Promise.race([
|
|
94
|
+
ctx.llm.listModels(p),
|
|
95
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1500)),
|
|
96
|
+
])
|
|
97
|
+
if (Array.isArray(mList)) {
|
|
98
|
+
if (!modelMap.has(p)) modelMap.set(p, new Set())
|
|
99
|
+
for (const m of mList) {
|
|
100
|
+
const mId = typeof m === 'string' ? m : (m?.id || m?.model || m?.name)
|
|
101
|
+
if (mId) modelMap.get(p).add(String(mId).toLowerCase())
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (_e) {}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return modelMap
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validates a single slot against active providers and models.
|
|
113
|
+
*/
|
|
114
|
+
export function validateSlot(slot, role, activeProviders, modelMap) {
|
|
115
|
+
if (!slot) return { valid: false, role, reason: 'empty_slot' }
|
|
116
|
+
const provider = String(slot.provider || '').toLowerCase().trim()
|
|
117
|
+
const model = String(slot.model || '').toLowerCase().trim()
|
|
118
|
+
const label = `${slot.provider || 'unknown'}:${slot.model || 'unknown'}`
|
|
119
|
+
|
|
120
|
+
if (!provider) {
|
|
121
|
+
return { valid: false, role, provider: slot.provider, model: slot.model, label, reason: 'missing_provider' }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// If we have active providers discovered and provider is not in the list
|
|
125
|
+
if (activeProviders && activeProviders.size > 0 && !activeProviders.has(provider)) {
|
|
126
|
+
return { valid: false, role, provider: slot.provider, model: slot.model, label, reason: 'provider_not_found' }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If we have models list for this provider, check if model exists
|
|
130
|
+
if (modelMap && modelMap.has(provider)) {
|
|
131
|
+
const models = modelMap.get(provider)
|
|
132
|
+
if (models && models.size > 0 && !models.has(model)) {
|
|
133
|
+
return { valid: false, role, provider: slot.provider, model: slot.model, label, reason: 'model_not_found' }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { valid: true, role, provider: slot.provider, model: slot.model, label }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Validates a preset's reference models, aggregator, and fallbacks.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} preset
|
|
144
|
+
* @param {object} [options]
|
|
145
|
+
* @returns {object}
|
|
146
|
+
*/
|
|
147
|
+
export function validatePreset(preset, { activeProviders, modelMap } = {}) {
|
|
148
|
+
if (!preset || typeof preset !== 'object') {
|
|
149
|
+
return {
|
|
150
|
+
valid: false,
|
|
151
|
+
presetName: 'unknown',
|
|
152
|
+
deadModels: [{ role: 'preset', reason: 'invalid_preset_object' }],
|
|
153
|
+
warnings: ['Preset is not a valid object'],
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const presetName = preset.name || preset.id || 'unnamed'
|
|
158
|
+
const deadModels = []
|
|
159
|
+
const warnings = []
|
|
160
|
+
|
|
161
|
+
// Check aggregator
|
|
162
|
+
const aggSlot = preset.aggregator_model || preset.aggregator
|
|
163
|
+
if (aggSlot) {
|
|
164
|
+
const res = validateSlot(aggSlot, 'aggregator', activeProviders, modelMap)
|
|
165
|
+
if (!res.valid) {
|
|
166
|
+
deadModels.push(res)
|
|
167
|
+
warnings.push(`Aggregator model ${res.label} has unreachable provider or model (${res.reason})`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check reference models
|
|
172
|
+
const refs = Array.isArray(preset.references)
|
|
173
|
+
? preset.references
|
|
174
|
+
: (Array.isArray(preset.reference_models) ? preset.reference_models : [])
|
|
175
|
+
|
|
176
|
+
if (refs.length === 0) {
|
|
177
|
+
warnings.push('Preset has no reference models configured')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (let idx = 0; idx < refs.length; idx++) {
|
|
181
|
+
const slot = refs[idx]
|
|
182
|
+
const res = validateSlot(slot, `reference_${idx + 1}`, activeProviders, modelMap)
|
|
183
|
+
if (!res.valid) {
|
|
184
|
+
deadModels.push(res)
|
|
185
|
+
warnings.push(`Reference candidate ${res.label} has unreachable provider or model (${res.reason})`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check slot fallbacks if any
|
|
189
|
+
if (Array.isArray(slot?.fallbacks)) {
|
|
190
|
+
for (const fb of slot.fallbacks) {
|
|
191
|
+
const fbRes = validateSlot(fb, `fallback_for_${res.label}`, activeProviders, modelMap)
|
|
192
|
+
if (!fbRes.valid) {
|
|
193
|
+
deadModels.push(fbRes)
|
|
194
|
+
warnings.push(`Reserve fallback ${fbRes.label} for ${res.label} is unreachable (${fbRes.reason})`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
presetId: preset.id || presetName,
|
|
202
|
+
presetName,
|
|
203
|
+
valid: deadModels.length === 0,
|
|
204
|
+
hasDeadModels: deadModels.length > 0,
|
|
205
|
+
deadModels,
|
|
206
|
+
warnings,
|
|
207
|
+
totalSlotsChecked: (aggSlot ? 1 : 0) + refs.length,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validates an array of presets and logs warnings if any dead models exist.
|
|
213
|
+
*
|
|
214
|
+
* @param {Array<object>} presets
|
|
215
|
+
* @param {object} ctx
|
|
216
|
+
* @returns {Promise<object>}
|
|
217
|
+
*/
|
|
218
|
+
export async function validateAllPresets(presets, ctx) {
|
|
219
|
+
const activeProviders = await getActiveLlmProviders(ctx)
|
|
220
|
+
const modelMap = await getActiveLlmModels(ctx, activeProviders)
|
|
221
|
+
|
|
222
|
+
const presetResults = []
|
|
223
|
+
let totalDeadModels = 0
|
|
224
|
+
|
|
225
|
+
for (const preset of (presets || [])) {
|
|
226
|
+
const result = validatePreset(preset, { activeProviders, modelMap })
|
|
227
|
+
presetResults.push(result)
|
|
228
|
+
if (result.hasDeadModels) {
|
|
229
|
+
totalDeadModels += result.deadModels.length
|
|
230
|
+
const deadLabels = result.deadModels.map((m) => `${m.label} (${m.reason})`).join(', ')
|
|
231
|
+
logger.warn(`[dsh-moa] Preset "${result.presetName}" contains ${result.deadModels.length} dead/unregistered models: ${deadLabels}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
valid: totalDeadModels === 0,
|
|
237
|
+
activeProviders: Array.from(activeProviders),
|
|
238
|
+
totalDeadModels,
|
|
239
|
+
presets: presetResults,
|
|
240
|
+
validatedAt: Date.now(),
|
|
241
|
+
}
|
|
242
|
+
}
|
package/lib/moa-service.js
CHANGED
|
@@ -152,4 +152,25 @@ export class MoaService {
|
|
|
152
152
|
emit(event, payload) {
|
|
153
153
|
emitEvent(this.ctx, event, payload)
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validate presets against active LLM providers in ctx.llm
|
|
158
|
+
* @param {Array<object>} [presets]
|
|
159
|
+
* @returns {Promise<object>}
|
|
160
|
+
*/
|
|
161
|
+
async validatePresets(presets) {
|
|
162
|
+
const list = presets || this.getPresets()
|
|
163
|
+
const { validateAllPresets } = await import('./moa-model-validator.js')
|
|
164
|
+
return validateAllPresets(list, this.ctx)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get list of currently active LLM provider names
|
|
169
|
+
* @returns {Promise<Array<string>>}
|
|
170
|
+
*/
|
|
171
|
+
async getActiveProviders() {
|
|
172
|
+
const { getActiveLlmProviders } = await import('./moa-model-validator.js')
|
|
173
|
+
const set = await getActiveLlmProviders(this.ctx)
|
|
174
|
+
return Array.from(set)
|
|
175
|
+
}
|
|
155
176
|
}
|
package/lib/routes/presets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateAllPresets } from '../moa-model-validator.js'
|
|
1
2
|
import { writeJson, readBody, assertAuthorizedRequest } from '../access.js'
|
|
2
3
|
import { isSafeWriteRequest } from '../updater.js'
|
|
3
4
|
import { resolvePresetForPrompt } from '../moa-router.js'
|
|
@@ -134,10 +135,15 @@ ctx.effect(() => {
|
|
|
134
135
|
if (!assertAuthorizedRequest(req, res, ctx)) return
|
|
135
136
|
const cfg = live()
|
|
136
137
|
if (req.method === 'GET') {
|
|
138
|
+
let validation = null
|
|
139
|
+
try {
|
|
140
|
+
validation = await validateAllPresets(cfg.presets || [], ctx)
|
|
141
|
+
} catch (_e) {}
|
|
137
142
|
writeJson(res, 200, {
|
|
138
143
|
ok: true,
|
|
139
144
|
defaultPreset: cfg.default_preset || 'default',
|
|
140
145
|
presets: cfg.presets || [],
|
|
146
|
+
validation,
|
|
141
147
|
})
|
|
142
148
|
return
|
|
143
149
|
}
|
|
@@ -179,12 +185,17 @@ ctx.effect(() => {
|
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
const unwrapped = typeof plainConfig === 'function' ? plainConfig(validated) : validated
|
|
188
|
+
let validation = null
|
|
189
|
+
try {
|
|
190
|
+
validation = await validateAllPresets(unwrapped.presets || [], ctx)
|
|
191
|
+
} catch (_e) {}
|
|
182
192
|
writeJson(res, 200, {
|
|
183
193
|
ok: true,
|
|
184
194
|
persisted: persistResult?.persisted ?? false,
|
|
185
195
|
enabled: unwrapped.enabled !== false,
|
|
186
196
|
defaultPreset: unwrapped.default_preset,
|
|
187
197
|
presets: unwrapped.presets,
|
|
198
|
+
validation,
|
|
188
199
|
})
|
|
189
200
|
} catch (err) { writeJson(res, 400, { ok: false, error: err?.message || String(err) }) }
|
|
190
201
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-moa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.66",
|
|
4
4
|
"description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
|
-
"test": "node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
22
|
+
"test": "node scripts/build-client.mjs --check && node --import ./test/_setup.mjs --test test/*.test.mjs",
|
|
23
23
|
"build:client": "node scripts/build-client.mjs"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
+
"esbuild": "^0.28.2",
|
|
70
|
+
"eslint": "^10.14.0",
|
|
69
71
|
"react": "^18.3.1",
|
|
70
72
|
"react-dom": "^18.3.1"
|
|
71
73
|
}
|