@goodandready/dsh-moa 0.2.39 → 0.2.41
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/history.js +168 -35
- package/lib/index.js +8 -3
- package/lib/moa-multi-judge.js +151 -59
- package/lib/moa-report.js +41 -20
- package/lib/moa-runner.js +5 -3
- package/lib/pricing.js +10 -6
- package/lib/routes.js +2 -2
- package/package.json +1 -1
package/lib/history.js
CHANGED
|
@@ -41,7 +41,6 @@ export function getDefaultHistoryFile() {
|
|
|
41
41
|
return path.join(getDefaultHistoryDir(), 'moa-history.jsonl')
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
/** Max history file size before rotation (10 MB) */
|
|
46
45
|
export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
|
|
47
46
|
|
|
@@ -77,6 +76,7 @@ export function rotateHistoryFileIfNeeded(filePath = getDefaultHistoryFile(), ma
|
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* Prepares a normalized history record entry.
|
|
79
|
+
* Preserves candidate persona, temperature, fallback, testResult, and slot details (#169).
|
|
80
80
|
*/
|
|
81
81
|
function normalizeRunRecord(record) {
|
|
82
82
|
return {
|
|
@@ -87,18 +87,31 @@ function normalizeRunRecord(record) {
|
|
|
87
87
|
isRefinement: Boolean(record.isRefinement),
|
|
88
88
|
isFastMode: Boolean(record.isFastMode),
|
|
89
89
|
candidates: Array.isArray(record.candidates)
|
|
90
|
-
? record.candidates.map((c) => ({
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
? record.candidates.map((c, idx) => ({
|
|
91
|
+
index: typeof c.index === 'number' ? c.index : (idx + 1),
|
|
92
|
+
provider: c.provider || c.slot?.provider || '',
|
|
93
|
+
model: c.model || c.slot?.model || '',
|
|
94
|
+
slot: c.slot ? { provider: c.slot.provider, model: c.slot.model } : (c.provider && c.model ? { provider: c.provider, model: c.model } : undefined),
|
|
95
|
+
role: c.role || c.role_persona || 'general',
|
|
96
|
+
temperature: typeof c.temperature === 'number' ? c.temperature : undefined,
|
|
97
|
+
wasFallback: Boolean(c.wasFallback || c.was_fallback),
|
|
98
|
+
originalModel: c.originalModel || c.original_model || null,
|
|
93
99
|
filesCount: Array.isArray(c.files) ? c.files.length : (c.filesCount || 0),
|
|
100
|
+
files: Array.isArray(c.files) ? c.files : undefined,
|
|
94
101
|
usage: c.usage || { inputTokens: 0, outputTokens: 0 },
|
|
95
102
|
costUsd: Number(c.costUsd || 0),
|
|
103
|
+
testResult: c.testResult ? {
|
|
104
|
+
passed: Boolean(c.testResult.passed),
|
|
105
|
+
exitCode: c.testResult.exitCode ?? 0,
|
|
106
|
+
summary: c.testResult.summary || '',
|
|
107
|
+
durationMs: c.testResult.durationMs ?? 0,
|
|
108
|
+
} : null,
|
|
96
109
|
}))
|
|
97
110
|
: [],
|
|
98
111
|
aggregator: record.aggregator
|
|
99
112
|
? {
|
|
100
|
-
provider: record.aggregator.provider || '',
|
|
101
|
-
model: record.aggregator.model || '',
|
|
113
|
+
provider: record.aggregator.provider || record.aggregator.slot?.provider || '',
|
|
114
|
+
model: record.aggregator.model || record.aggregator.slot?.model || '',
|
|
102
115
|
usage: record.aggregator.usage || { inputTokens: 0, outputTokens: 0 },
|
|
103
116
|
costUsd: Number(record.aggregator.costUsd || 0),
|
|
104
117
|
}
|
|
@@ -162,6 +175,7 @@ export async function recordMoaRunAsync(record, filePath = getDefaultHistoryFile
|
|
|
162
175
|
/**
|
|
163
176
|
* Reads lines from the end of a file in reverse order in chunks.
|
|
164
177
|
* Avoids loading multi-megabyte JSONL files into V8 heap strings.
|
|
178
|
+
* Buffer-safe: never splits multi-byte UTF-8 sequences at chunk boundaries (#184).
|
|
165
179
|
*/
|
|
166
180
|
export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
167
181
|
const CHUNK_SIZE = 64 * 1024
|
|
@@ -169,7 +183,7 @@ export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
|
169
183
|
try {
|
|
170
184
|
const stat = fs.fstatSync(fd)
|
|
171
185
|
let pos = stat.size
|
|
172
|
-
let
|
|
186
|
+
let remainderBuf = Buffer.alloc(0)
|
|
173
187
|
const collectedLines = []
|
|
174
188
|
|
|
175
189
|
while (pos > 0 && collectedLines.length < countNeeded) {
|
|
@@ -177,29 +191,107 @@ export function readTailLinesSync(filePath, countNeeded = 50) {
|
|
|
177
191
|
pos -= bytesToRead
|
|
178
192
|
const buf = Buffer.alloc(bytesToRead)
|
|
179
193
|
fs.readSync(fd, buf, 0, bytesToRead, pos)
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
const totalBuf = remainderBuf.length > 0 ? Buffer.concat([buf, remainderBuf]) : buf
|
|
195
|
+
|
|
196
|
+
let endIdx = totalBuf.length
|
|
197
|
+
for (let i = totalBuf.length - 1; i >= 0; i--) {
|
|
198
|
+
if (totalBuf[i] === 0x0A) {
|
|
199
|
+
if (i + 1 < endIdx) {
|
|
200
|
+
const lineBuf = totalBuf.subarray(i + 1, endIdx)
|
|
201
|
+
const lineStr = lineBuf.toString('utf8').trim()
|
|
202
|
+
if (lineStr) {
|
|
203
|
+
collectedLines.push(lineStr)
|
|
204
|
+
if (collectedLines.length >= countNeeded) break
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
endIdx = i
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (collectedLines.length >= countNeeded) {
|
|
212
|
+
remainderBuf = Buffer.alloc(0)
|
|
213
|
+
break
|
|
186
214
|
}
|
|
187
215
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
if (pos > 0) {
|
|
217
|
+
remainderBuf = totalBuf.subarray(0, endIdx)
|
|
218
|
+
} else {
|
|
219
|
+
if (endIdx > 0) {
|
|
220
|
+
const firstLineStr = totalBuf.subarray(0, endIdx).toString('utf8').trim()
|
|
221
|
+
if (firstLineStr && collectedLines.length < countNeeded) {
|
|
222
|
+
collectedLines.push(firstLineStr)
|
|
223
|
+
}
|
|
193
224
|
}
|
|
225
|
+
remainderBuf = Buffer.alloc(0)
|
|
194
226
|
}
|
|
195
227
|
}
|
|
196
|
-
|
|
197
|
-
|
|
228
|
+
|
|
229
|
+
if (remainderBuf.length > 0 && collectedLines.length < countNeeded) {
|
|
230
|
+
const lineStr = remainderBuf.toString('utf8').trim()
|
|
231
|
+
if (lineStr) collectedLines.push(lineStr)
|
|
198
232
|
}
|
|
233
|
+
|
|
199
234
|
return collectedLines
|
|
200
235
|
} finally {
|
|
201
|
-
fs.closeSync(fd)
|
|
236
|
+
try { fs.closeSync(fd) } catch { /* best effort */ }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Counts lines in a file by scanning for 0x0A newline bytes in chunks.
|
|
242
|
+
* Fast, memory-efficient, and caches result in _historyCache (#183).
|
|
243
|
+
*/
|
|
244
|
+
export function countFileLinesSync(filePath, stat) {
|
|
245
|
+
if (
|
|
246
|
+
_historyCache &&
|
|
247
|
+
_historyCache.filePath === filePath &&
|
|
248
|
+
_historyCache.mtimeMs === stat.mtimeMs &&
|
|
249
|
+
_historyCache.size === stat.size &&
|
|
250
|
+
typeof _historyCache.totalCount === 'number'
|
|
251
|
+
) {
|
|
252
|
+
return _historyCache.totalCount
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let count = 0
|
|
256
|
+
const fd = fs.openSync(filePath, 'r')
|
|
257
|
+
try {
|
|
258
|
+
const CHUNK_SIZE = 64 * 1024
|
|
259
|
+
const buf = Buffer.alloc(CHUNK_SIZE)
|
|
260
|
+
let pos = 0
|
|
261
|
+
let lastByte = 0
|
|
262
|
+
while (pos < stat.size) {
|
|
263
|
+
const bytesToRead = Math.min(CHUNK_SIZE, stat.size - pos)
|
|
264
|
+
fs.readSync(fd, buf, 0, bytesToRead, pos)
|
|
265
|
+
for (let i = 0; i < bytesToRead; i++) {
|
|
266
|
+
if (buf[i] === 0x0A) count++
|
|
267
|
+
}
|
|
268
|
+
lastByte = buf[bytesToRead - 1]
|
|
269
|
+
pos += bytesToRead
|
|
270
|
+
}
|
|
271
|
+
if (stat.size > 0 && lastByte !== 0x0A) {
|
|
272
|
+
count++
|
|
273
|
+
}
|
|
274
|
+
} finally {
|
|
275
|
+
try { fs.closeSync(fd) } catch { /* best effort */ }
|
|
202
276
|
}
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
!_historyCache ||
|
|
280
|
+
_historyCache.filePath !== filePath ||
|
|
281
|
+
_historyCache.mtimeMs !== stat.mtimeMs ||
|
|
282
|
+
_historyCache.size !== stat.size
|
|
283
|
+
) {
|
|
284
|
+
_historyCache = {
|
|
285
|
+
filePath,
|
|
286
|
+
mtimeMs: stat.mtimeMs,
|
|
287
|
+
size: stat.size,
|
|
288
|
+
leaderboards: new Map(),
|
|
289
|
+
totalCount: count,
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
_historyCache.totalCount = count
|
|
293
|
+
}
|
|
294
|
+
return count
|
|
203
295
|
}
|
|
204
296
|
|
|
205
297
|
/**
|
|
@@ -220,6 +312,23 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
220
312
|
const lines = raw.trim().split('\n').filter(Boolean)
|
|
221
313
|
const total = lines.length
|
|
222
314
|
|
|
315
|
+
if (
|
|
316
|
+
!_historyCache ||
|
|
317
|
+
_historyCache.filePath !== filePath ||
|
|
318
|
+
_historyCache.mtimeMs !== stat.mtimeMs ||
|
|
319
|
+
_historyCache.size !== stat.size
|
|
320
|
+
) {
|
|
321
|
+
_historyCache = {
|
|
322
|
+
filePath,
|
|
323
|
+
mtimeMs: stat.mtimeMs,
|
|
324
|
+
size: stat.size,
|
|
325
|
+
leaderboards: new Map(),
|
|
326
|
+
totalCount: total,
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
_historyCache.totalCount = total
|
|
330
|
+
}
|
|
331
|
+
|
|
223
332
|
const sliced = lines
|
|
224
333
|
.slice(Math.max(0, total - offset - limit), total - offset)
|
|
225
334
|
.reverse()
|
|
@@ -250,7 +359,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
250
359
|
|
|
251
360
|
let total = _historyCache?.totalCount
|
|
252
361
|
if (typeof total !== 'number') {
|
|
253
|
-
total = tailLines.length
|
|
362
|
+
total = tailLines.length < needed ? tailLines.length : countFileLinesSync(filePath, stat)
|
|
254
363
|
}
|
|
255
364
|
|
|
256
365
|
return { total, runs }
|
|
@@ -263,6 +372,7 @@ export function getMoaHistory(limit = 20, offset = 0, filePath = getDefaultHisto
|
|
|
263
372
|
/**
|
|
264
373
|
* Calculates win-rate leaderboard and model performance statistics across history.
|
|
265
374
|
* Employs in-memory caching keyed by file mtime and size for O(1) repeated queries.
|
|
375
|
+
* Robust matching for aliases, fallbacks, and winnerIndex (#185).
|
|
266
376
|
*/
|
|
267
377
|
export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options = {}) {
|
|
268
378
|
try {
|
|
@@ -307,10 +417,15 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
|
|
|
307
417
|
if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
|
|
308
418
|
filteredRuns++
|
|
309
419
|
|
|
310
|
-
const
|
|
420
|
+
const rawWinner = item.winnerModel ? String(item.winnerModel).trim() : null
|
|
421
|
+
const cleanWinner = rawWinner ? rawWinner.replace(/\s*\[fallback for .*\]$/i, '').trim() : null
|
|
422
|
+
const normWinner = cleanWinner && cleanWinner !== 'none' && cleanWinner !== 'unknown'
|
|
423
|
+
? cleanWinner.replace(/[\/:]/g, ':').toLowerCase()
|
|
424
|
+
: null
|
|
311
425
|
|
|
312
426
|
if (Array.isArray(item.candidates)) {
|
|
313
|
-
for (
|
|
427
|
+
for (let candIdx = 0; candIdx < item.candidates.length; candIdx++) {
|
|
428
|
+
const cand = item.candidates[candIdx]
|
|
314
429
|
const key = cand.provider && cand.model ? `${cand.provider}:${cand.model}` : (cand.model || 'unknown')
|
|
315
430
|
if (!modelStats.has(key)) {
|
|
316
431
|
modelStats.set(key, {
|
|
@@ -326,12 +441,25 @@ export function getMoaLeaderboard(filePath = getDefaultHistoryFile(), options =
|
|
|
326
441
|
const st = modelStats.get(key)
|
|
327
442
|
st.runs += 1
|
|
328
443
|
|
|
329
|
-
const inTok = cand.usage?.inputTokens
|
|
330
|
-
const outTok = cand.usage?.outputTokens
|
|
444
|
+
const inTok = cand.usage?.inputTokens ?? cand.usage?.totalInputTokens ?? cand.usage?.prompt_tokens ?? 0
|
|
445
|
+
const outTok = cand.usage?.outputTokens ?? cand.usage?.totalOutputTokens ?? cand.usage?.completion_tokens ?? 0
|
|
331
446
|
st.totalTokens += inTok + outTok
|
|
332
447
|
st.totalCostUsd += Number(cand.costUsd || 0)
|
|
333
448
|
|
|
334
|
-
|
|
449
|
+
let isWin = false
|
|
450
|
+
if (normWinner) {
|
|
451
|
+
const normKey = key.replace(/[\/:]/g, ':').toLowerCase()
|
|
452
|
+
const normModel = String(cand.model || '').replace(/[\/:]/g, ':').toLowerCase()
|
|
453
|
+
if (normWinner === normKey || normWinner === normModel || normWinner.endsWith(`:${normModel}`)) {
|
|
454
|
+
isWin = true
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (!isWin && typeof item.winnerIndex === 'number' && item.winnerIndex > 0) {
|
|
458
|
+
if (cand.index === item.winnerIndex || candIdx === item.winnerIndex - 1) {
|
|
459
|
+
isWin = true
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (isWin) {
|
|
335
463
|
st.wins += 1
|
|
336
464
|
}
|
|
337
465
|
}
|
|
@@ -433,22 +561,27 @@ export function exportMoaHistory(filePath = getDefaultHistoryFile(), options = {
|
|
|
433
561
|
}
|
|
434
562
|
}
|
|
435
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Formats candidate outputs for history storage (#169).
|
|
566
|
+
*/
|
|
436
567
|
export function candidatesForHistory(referenceOutputs) {
|
|
437
|
-
return (referenceOutputs || []).map((r) => ({
|
|
438
|
-
|
|
439
|
-
|
|
568
|
+
return (referenceOutputs || []).map((r, i) => ({
|
|
569
|
+
index: r.index ?? (i + 1),
|
|
570
|
+
provider: r.slot?.provider || r.provider || "",
|
|
571
|
+
model: r.slot?.model || r.model || "",
|
|
572
|
+
slot: r.slot ? { provider: r.slot.provider, model: r.slot.model } : undefined,
|
|
440
573
|
role: r.role_persona || r.slot?.role_persona || r.slot?.role || "general",
|
|
441
574
|
temperature: r.temperature,
|
|
442
575
|
wasFallback: Boolean(r.was_fallback),
|
|
443
576
|
originalModel: r.original_model || null,
|
|
444
|
-
files: (r.files || []).map((f) => f.relativePath),
|
|
577
|
+
files: (r.files || []).map((f) => f.relativePath || f),
|
|
445
578
|
usage: r.usage || { inputTokens: 0, outputTokens: 0 },
|
|
446
579
|
costUsd: r.costUsd || 0,
|
|
447
580
|
testResult: r.testResult ? {
|
|
448
|
-
passed: r.testResult.passed,
|
|
449
|
-
exitCode: r.testResult.exitCode,
|
|
450
|
-
summary: r.testResult.summary,
|
|
451
|
-
durationMs: r.testResult.durationMs,
|
|
581
|
+
passed: Boolean(r.testResult.passed),
|
|
582
|
+
exitCode: r.testResult.exitCode ?? 0,
|
|
583
|
+
summary: r.testResult.summary || '',
|
|
584
|
+
durationMs: r.testResult.durationMs ?? 0,
|
|
452
585
|
} : null,
|
|
453
586
|
}))
|
|
454
587
|
}
|
package/lib/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { registerMoaRoutes } from './routes.js'
|
|
|
17
17
|
import { resolvePresetForPrompt } from './moa-router.js'
|
|
18
18
|
import { createLiveCanvasClient } from './live-canvas.js'
|
|
19
19
|
import { registerPluginUpdater } from './updater.js'
|
|
20
|
+
import { bestEffort } from './best-effort.js'
|
|
20
21
|
|
|
21
22
|
export const name = '@goodandready/dsh-moa'
|
|
22
23
|
export { collectProjectContext, formatProjectContext } from './file-workspace.js'
|
|
@@ -38,7 +39,11 @@ export function apply(ctx, config) {
|
|
|
38
39
|
|
|
39
40
|
let localOverrides = null
|
|
40
41
|
const getSourceConfig = () => {
|
|
41
|
-
|
|
42
|
+
let liveCandidate = ctx.fiber?.config
|
|
43
|
+
if (!liveCandidate && ('config' in ctx)) {
|
|
44
|
+
bestEffort('ctx.config probe', () => { liveCandidate = ctx.config }, ctx.logger)
|
|
45
|
+
}
|
|
46
|
+
const liveSource = plainConfig(liveCandidate ?? config ?? {})
|
|
42
47
|
return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
|
|
43
48
|
}
|
|
44
49
|
const live = () => {
|
|
@@ -110,7 +115,7 @@ export function apply(ctx, config) {
|
|
|
110
115
|
Config,
|
|
111
116
|
plainConfig,
|
|
112
117
|
webServerPort: ctx.webServer?.port,
|
|
113
|
-
timeMachineEngine: ctx.get?.('timeMachineEngine')
|
|
118
|
+
timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
|
|
114
119
|
})
|
|
115
120
|
|
|
116
121
|
// Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
|
|
@@ -207,7 +212,7 @@ export function apply(ctx, config) {
|
|
|
207
212
|
prices: cfg.prices || {},
|
|
208
213
|
liveCanvas,
|
|
209
214
|
webServerPort: ctx.webServer?.port,
|
|
210
|
-
timeMachineEngine: ctx.get?.('timeMachineEngine')
|
|
215
|
+
timeMachineEngine: ctx.reflect?.get?.('timeMachineEngine', false),
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
if (event.signal) {
|
package/lib/moa-multi-judge.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { slotLabel } from './moa-prompts.js'
|
|
7
|
+
import { estimateTokenCost } from './pricing.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Extracts code fences and block-level assets from markdown text.
|
|
@@ -40,8 +41,10 @@ export function extractCodeBlocks(text = '') {
|
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Builds composite block directives highlighting complementary modules across candidates.
|
|
44
|
+
* Supports blind evaluation (#167).
|
|
43
45
|
*/
|
|
44
|
-
export function buildCompositeBlockDirectives(candidates = []) {
|
|
46
|
+
export function buildCompositeBlockDirectives(candidates = [], options = {}) {
|
|
47
|
+
const isBlind = Boolean(options === true || options?.blind)
|
|
45
48
|
const fileMap = new Map()
|
|
46
49
|
|
|
47
50
|
for (const c of candidates) {
|
|
@@ -52,7 +55,7 @@ export function buildCompositeBlockDirectives(candidates = []) {
|
|
|
52
55
|
if (!fileMap.has(b.filename)) {
|
|
53
56
|
fileMap.set(b.filename, [])
|
|
54
57
|
}
|
|
55
|
-
fileMap.get(b.filename).push(c.label || `Candidate ${c.index}`)
|
|
58
|
+
fileMap.get(b.filename).push(isBlind ? `Candidate ${c.index}` : (c.label || `Candidate ${c.index}`))
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
}
|
|
@@ -79,17 +82,38 @@ ${fileOverview}============================================
|
|
|
79
82
|
|
|
80
83
|
/**
|
|
81
84
|
* Parses judge output into structured scores and candidate choice.
|
|
85
|
+
* Accepts either candidateCount or candidate array/set to support active candidate indexing (#166).
|
|
82
86
|
*/
|
|
83
|
-
export function parseJudgeEvaluation(rawText = '',
|
|
87
|
+
export function parseJudgeEvaluation(rawText = '', candidatesOrCount = 2) {
|
|
88
|
+
let allowedIndices = null
|
|
89
|
+
let defaultCandidate = 1
|
|
90
|
+
|
|
91
|
+
if (Array.isArray(candidatesOrCount)) {
|
|
92
|
+
const active = candidatesOrCount.filter((c) => c && c.ok !== false)
|
|
93
|
+
allowedIndices = new Set(active.map((c) => c.index))
|
|
94
|
+
if (active.length > 0 && typeof active[0].index === 'number') {
|
|
95
|
+
defaultCandidate = active[0].index
|
|
96
|
+
}
|
|
97
|
+
} else if (candidatesOrCount instanceof Set) {
|
|
98
|
+
allowedIndices = candidatesOrCount
|
|
99
|
+
defaultCandidate = Array.from(allowedIndices)[0] || 1
|
|
100
|
+
} else if (typeof candidatesOrCount === 'number') {
|
|
101
|
+
allowedIndices = new Set(Array.from({ length: candidatesOrCount }, (_, i) => i + 1))
|
|
102
|
+
defaultCandidate = 1
|
|
103
|
+
} else {
|
|
104
|
+
allowedIndices = new Set([1, 2])
|
|
105
|
+
defaultCandidate = 1
|
|
106
|
+
}
|
|
107
|
+
|
|
84
108
|
const scores = {}
|
|
85
|
-
let bestCandidate =
|
|
109
|
+
let bestCandidate = defaultCandidate
|
|
86
110
|
let reasoning = ''
|
|
87
111
|
|
|
88
112
|
const scoreMatches = rawText.matchAll(/Candidate\s*(\d+)[:\s]+(?:Score\s*[:\s]*)?([0-9]+(?:\.[0-9]+)?)/gi)
|
|
89
113
|
for (const m of scoreMatches) {
|
|
90
114
|
const idx = parseInt(m[1], 10)
|
|
91
115
|
const val = parseFloat(m[2])
|
|
92
|
-
if (idx
|
|
116
|
+
if (allowedIndices.has(idx) && !Number.isNaN(val)) {
|
|
93
117
|
scores[idx] = Math.min(10, Math.max(0, val))
|
|
94
118
|
}
|
|
95
119
|
}
|
|
@@ -97,16 +121,17 @@ export function parseJudgeEvaluation(rawText = '', candidateCount = 2) {
|
|
|
97
121
|
const bestMatch = rawText.match(/BEST_CANDIDATE\s*[:\s]+(?:Candidate\s*)?(\d+)/i)
|
|
98
122
|
if (bestMatch) {
|
|
99
123
|
const parsedBest = parseInt(bestMatch[1], 10)
|
|
100
|
-
if (parsedBest
|
|
124
|
+
if (allowedIndices.has(parsedBest)) {
|
|
101
125
|
bestCandidate = parsedBest
|
|
102
126
|
}
|
|
103
127
|
} else {
|
|
104
|
-
// Find candidate with max parsed score
|
|
128
|
+
// Find candidate with max parsed score among allowed indices
|
|
105
129
|
let maxScore = -1
|
|
106
130
|
for (const [idxStr, s] of Object.entries(scores)) {
|
|
107
|
-
|
|
131
|
+
const idxNum = parseInt(idxStr, 10)
|
|
132
|
+
if (allowedIndices.has(idxNum) && s > maxScore) {
|
|
108
133
|
maxScore = s
|
|
109
|
-
bestCandidate =
|
|
134
|
+
bestCandidate = idxNum
|
|
110
135
|
}
|
|
111
136
|
}
|
|
112
137
|
}
|
|
@@ -114,25 +139,27 @@ export function parseJudgeEvaluation(rawText = '', candidateCount = 2) {
|
|
|
114
139
|
const reasonMatch = rawText.match(/CONSENSUS_REASONING\s*[:\s]+([^\n\r]+)/i)
|
|
115
140
|
if (reasonMatch) {
|
|
116
141
|
reasoning = reasonMatch[1].trim()
|
|
117
|
-
} else {
|
|
118
|
-
reasoning = rawText.slice(0, 180).replace(/\n/g, ' ').trim()
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
return { scores, bestCandidate, reasoning }
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
/**
|
|
125
|
-
*
|
|
148
|
+
* Runs a multi-judge consensus panel across candidate solutions.
|
|
149
|
+
* Supports majority, unanimous, and highest_score strategies.
|
|
150
|
+
* Returns consensus, votes, usage, and cost tracking (#165, #167, #168).
|
|
126
151
|
*/
|
|
127
152
|
export async function executeMultiJudgePanel({
|
|
128
153
|
judges = [],
|
|
129
154
|
candidates = [],
|
|
130
155
|
userPrompt = '',
|
|
131
156
|
callLlm,
|
|
132
|
-
strategy = 'majority',
|
|
157
|
+
strategy = 'majority',
|
|
133
158
|
timeoutMs = 45000,
|
|
134
159
|
signal,
|
|
135
160
|
onProgress,
|
|
161
|
+
prices = {},
|
|
162
|
+
blind = false,
|
|
136
163
|
}) {
|
|
137
164
|
const activeCandidates = (candidates || []).filter((c) => c && c.ok)
|
|
138
165
|
if (activeCandidates.length === 0) {
|
|
@@ -140,6 +167,12 @@ export async function executeMultiJudgePanel({
|
|
|
140
167
|
consensus: false,
|
|
141
168
|
winningCandidateIndex: 1,
|
|
142
169
|
strategy,
|
|
170
|
+
isUnanimous: false,
|
|
171
|
+
votes: {},
|
|
172
|
+
averageScores: {},
|
|
173
|
+
judgesResults: [],
|
|
174
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
175
|
+
totalCostUsd: 0,
|
|
143
176
|
reason: 'no_active_candidates',
|
|
144
177
|
}
|
|
145
178
|
}
|
|
@@ -150,46 +183,52 @@ export async function executeMultiJudgePanel({
|
|
|
150
183
|
winningCandidateIndex: activeCandidates[0].index,
|
|
151
184
|
winningCandidateLabel: activeCandidates[0].label,
|
|
152
185
|
strategy,
|
|
186
|
+
isUnanimous: true,
|
|
187
|
+
votes: { [activeCandidates[0].index]: 1 },
|
|
188
|
+
averageScores: { [activeCandidates[0].index]: 10.0 },
|
|
189
|
+
judgesResults: [],
|
|
190
|
+
totalUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
191
|
+
totalCostUsd: 0,
|
|
153
192
|
reason: 'single_active_candidate',
|
|
154
193
|
}
|
|
155
194
|
}
|
|
156
195
|
|
|
157
|
-
const
|
|
196
|
+
const judgePanel = (judges && judges.length > 0)
|
|
158
197
|
? judges
|
|
159
198
|
: [{ provider: 'default', model: 'aggregator-judge', label: 'Primary Judge' }]
|
|
160
199
|
|
|
200
|
+
// Blind evaluation: mask model labels in judge prompt (#167)
|
|
161
201
|
const candidateSummary = activeCandidates.map((c) => {
|
|
162
|
-
|
|
202
|
+
const label = blind ? `Candidate ${c.index}` : `Candidate ${c.index} (${c.label})`
|
|
203
|
+
return `### ${label}:\n${(c.text || '').slice(0, 4000)}`
|
|
163
204
|
}).join('\n\n---\n\n')
|
|
164
205
|
|
|
165
206
|
const judgePrompt = `You are an impartial Expert Code & Logic Judge evaluating multiple AI candidate solutions.
|
|
166
|
-
User
|
|
167
|
-
${userPrompt
|
|
207
|
+
User Task:
|
|
208
|
+
${userPrompt}
|
|
168
209
|
|
|
169
|
-
|
|
210
|
+
Candidates Under Review:
|
|
170
211
|
${candidateSummary}
|
|
171
212
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
213
|
+
INSTRUCTIONS:
|
|
214
|
+
1. Carefully compare all candidates on correctness, efficiency, architectural cleanliness, and edge-case resilience.
|
|
215
|
+
2. Assign each candidate a score from 0.0 to 10.0.
|
|
216
|
+
3. State your chosen best candidate and a brief reasoning sentence.
|
|
175
217
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
218
|
+
Format your response EXACTLY as follows:
|
|
219
|
+
CANDIDATE_EVALUATION:
|
|
220
|
+
Candidate [index]: Score [0-10]
|
|
221
|
+
...
|
|
222
|
+
BEST_CANDIDATE: Candidate [winning index]
|
|
180
223
|
CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
181
224
|
`
|
|
182
225
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const judgePromises = judgeSlots.map(async (judge, jIdx) => {
|
|
188
|
-
const label = slotLabel(judge)
|
|
226
|
+
const judgePromises = judgePanel.map(async (judge, jIdx) => {
|
|
227
|
+
const label = judge.label || slotLabel(judge)
|
|
189
228
|
try {
|
|
190
229
|
const abortCtrl = new AbortController()
|
|
191
230
|
if (signal) {
|
|
192
|
-
signal.addEventListener('abort', () => abortCtrl.abort(), { once: true })
|
|
231
|
+
signal.addEventListener('abort', () => abortCtrl.abort(signal.reason || new Error('Turn aborted')), { once: true })
|
|
193
232
|
}
|
|
194
233
|
const timer = setTimeout(() => abortCtrl.abort(new Error('Judge evaluation timeout')), timeoutMs)
|
|
195
234
|
timer.unref?.()
|
|
@@ -198,22 +237,31 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
198
237
|
provider: judge.provider,
|
|
199
238
|
model: judge.model,
|
|
200
239
|
messages: [
|
|
201
|
-
{ role: 'system', content: 'You are an objective
|
|
240
|
+
{ role: 'system', content: 'You are an objective AI code evaluation judge.' },
|
|
202
241
|
{ role: 'user', content: judgePrompt },
|
|
203
242
|
],
|
|
204
|
-
temperature: 0.1,
|
|
205
|
-
maxTokens: 512,
|
|
206
243
|
signal: abortCtrl.signal,
|
|
207
244
|
})
|
|
245
|
+
|
|
208
246
|
clearTimeout(timer)
|
|
209
247
|
|
|
210
248
|
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
211
|
-
|
|
249
|
+
// Preserve active candidate indices rather than assuming 1..N (#166)
|
|
250
|
+
const evalResult = parseJudgeEvaluation(text, activeCandidates)
|
|
251
|
+
|
|
252
|
+
// Track usage and cost (#168)
|
|
253
|
+
const usage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
254
|
+
inputTokens: Math.max(1, Math.round((judgePrompt.length + 50) / 4)),
|
|
255
|
+
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
256
|
+
}
|
|
257
|
+
const costInfo = estimateTokenCost(judge, usage, prices)
|
|
212
258
|
|
|
213
259
|
return {
|
|
214
260
|
judgeIndex: jIdx + 1,
|
|
215
261
|
judgeLabel: label,
|
|
216
262
|
ok: true,
|
|
263
|
+
usage,
|
|
264
|
+
costUsd: costInfo.costUsd || 0,
|
|
217
265
|
...evalResult,
|
|
218
266
|
}
|
|
219
267
|
} catch (err) {
|
|
@@ -221,8 +269,10 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
221
269
|
judgeIndex: jIdx + 1,
|
|
222
270
|
judgeLabel: label,
|
|
223
271
|
ok: false,
|
|
224
|
-
bestCandidate:
|
|
272
|
+
bestCandidate: activeCandidates[0].index,
|
|
225
273
|
scores: {},
|
|
274
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
275
|
+
costUsd: 0,
|
|
226
276
|
reasoning: err?.message || String(err),
|
|
227
277
|
}
|
|
228
278
|
}
|
|
@@ -231,64 +281,104 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
231
281
|
const results = await Promise.all(judgePromises)
|
|
232
282
|
const validResults = results.filter((r) => r.ok)
|
|
233
283
|
|
|
284
|
+
// Accumulate total usage and cost across all judges (#168)
|
|
285
|
+
let sumInTokens = 0
|
|
286
|
+
let sumOutTokens = 0
|
|
287
|
+
let sumCostUsd = 0
|
|
288
|
+
for (const r of results) {
|
|
289
|
+
if (r.usage) {
|
|
290
|
+
sumInTokens += r.usage.inputTokens || r.usage.promptTokens || 0
|
|
291
|
+
sumOutTokens += r.usage.outputTokens || r.usage.completionTokens || 0
|
|
292
|
+
}
|
|
293
|
+
if (typeof r.costUsd === 'number') {
|
|
294
|
+
sumCostUsd += r.costUsd
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const totalUsage = {
|
|
298
|
+
inputTokens: sumInTokens,
|
|
299
|
+
outputTokens: sumOutTokens,
|
|
300
|
+
totalTokens: sumInTokens + sumOutTokens,
|
|
301
|
+
costUsd: Number(sumCostUsd.toFixed(5)),
|
|
302
|
+
}
|
|
303
|
+
const totalCostUsd = totalUsage.costUsd
|
|
304
|
+
|
|
305
|
+
// #165: If all judges failed, consensus is false with 0 votes
|
|
306
|
+
if (validResults.length === 0) {
|
|
307
|
+
return {
|
|
308
|
+
consensus: false,
|
|
309
|
+
winningCandidateIndex: activeCandidates[0].index,
|
|
310
|
+
winningCandidateLabel: activeCandidates[0].label,
|
|
311
|
+
strategy,
|
|
312
|
+
isUnanimous: false,
|
|
313
|
+
votes: {},
|
|
314
|
+
averageScores: {},
|
|
315
|
+
judgesResults: results,
|
|
316
|
+
totalUsage,
|
|
317
|
+
totalCostUsd,
|
|
318
|
+
reason: 'all_judges_failed',
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
234
322
|
// Tally votes and compute average scores
|
|
235
323
|
const votes = {}
|
|
236
324
|
const totalScores = {}
|
|
237
325
|
const scoreCounts = {}
|
|
238
326
|
|
|
239
|
-
for (const
|
|
240
|
-
votes[
|
|
241
|
-
for (const [
|
|
242
|
-
totalScores[
|
|
243
|
-
scoreCounts[
|
|
327
|
+
for (const vr of validResults) {
|
|
328
|
+
votes[vr.bestCandidate] = (votes[vr.bestCandidate] || 0) + 1
|
|
329
|
+
for (const [candIdx, sc] of Object.entries(vr.scores)) {
|
|
330
|
+
totalScores[candIdx] = (totalScores[candIdx] || 0) + sc
|
|
331
|
+
scoreCounts[candIdx] = (scoreCounts[candIdx] || 0) + 1
|
|
244
332
|
}
|
|
245
333
|
}
|
|
246
334
|
|
|
247
335
|
const averageScores = {}
|
|
248
|
-
for (const
|
|
249
|
-
|
|
250
|
-
averageScores[c.index] = cnt > 0 ? Number((totalScores[c.index] / cnt).toFixed(2)) : 5.0
|
|
336
|
+
for (const [candIdx, sum] of Object.entries(totalScores)) {
|
|
337
|
+
averageScores[candIdx] = Number((sum / (scoreCounts[candIdx] || 1)).toFixed(2))
|
|
251
338
|
}
|
|
252
339
|
|
|
253
340
|
let winner = activeCandidates[0].index
|
|
254
341
|
let isUnanimous = false
|
|
342
|
+
let hasConsensus = true
|
|
255
343
|
|
|
256
344
|
if (strategy === 'highest_score') {
|
|
257
345
|
let highestAvg = -1
|
|
258
346
|
for (const c of activeCandidates) {
|
|
259
|
-
const avg = averageScores[c.index]
|
|
347
|
+
const avg = averageScores[c.index] ?? -1
|
|
260
348
|
if (avg > highestAvg) {
|
|
261
349
|
highestAvg = avg
|
|
262
350
|
winner = c.index
|
|
263
351
|
}
|
|
264
352
|
}
|
|
353
|
+
hasConsensus = validResults.length > 0
|
|
265
354
|
} else {
|
|
266
355
|
// majority or unanimous
|
|
267
356
|
let maxVotes = -1
|
|
268
|
-
for (const
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
maxVotes =
|
|
272
|
-
winner =
|
|
273
|
-
} else if (v === maxVotes && (averageScores[c.index] || 0) > (averageScores[winner] || 0)) {
|
|
274
|
-
winner = c.index
|
|
357
|
+
for (const [cIdxStr, count] of Object.entries(votes)) {
|
|
358
|
+
const cIdx = parseInt(cIdxStr, 10)
|
|
359
|
+
if (count > maxVotes) {
|
|
360
|
+
maxVotes = count
|
|
361
|
+
winner = cIdx
|
|
275
362
|
}
|
|
276
363
|
}
|
|
277
364
|
isUnanimous = (votes[winner] || 0) === validResults.length && validResults.length > 0
|
|
365
|
+
if (strategy === 'unanimous') {
|
|
366
|
+
hasConsensus = isUnanimous && validResults.length > 0
|
|
367
|
+
} else {
|
|
368
|
+
hasConsensus = maxVotes > (validResults.length / 2) || validResults.length === 1
|
|
369
|
+
}
|
|
278
370
|
}
|
|
279
371
|
|
|
280
372
|
const winningCand = activeCandidates.find((c) => c.index === winner) || activeCandidates[0]
|
|
281
|
-
|
|
282
|
-
const consensusReport = `Multi-
|
|
283
|
-
`Winner is Candidate ${winningCand.index} (${winningCand.label}) with ${votes[winner] || 0} votes, ` +
|
|
284
|
-
`avg score ${averageScores[winner] || 0}/10. Strategy: ${strategy}.`
|
|
373
|
+
const winnerVotes = votes[winner] || 0
|
|
374
|
+
const consensusReport = `Multi-judge consensus (${strategy}): Candidate ${winner} (${winningCand.label}) won with ${winnerVotes}/${validResults.length} votes.`
|
|
285
375
|
|
|
286
376
|
if (typeof onProgress === 'function') {
|
|
287
|
-
onProgress(
|
|
377
|
+
onProgress(`⚖️ *${consensusReport}*\n`)
|
|
288
378
|
}
|
|
289
379
|
|
|
290
380
|
return {
|
|
291
|
-
consensus:
|
|
381
|
+
consensus: hasConsensus,
|
|
292
382
|
winningCandidateIndex: winningCand.index,
|
|
293
383
|
winningCandidateLabel: winningCand.label,
|
|
294
384
|
strategy,
|
|
@@ -296,6 +386,8 @@ CONSENSUS_REASONING: [one concise sentence explaining your pick]
|
|
|
296
386
|
votes,
|
|
297
387
|
averageScores,
|
|
298
388
|
judgesResults: results,
|
|
389
|
+
totalUsage,
|
|
390
|
+
totalCostUsd,
|
|
299
391
|
consensusReport,
|
|
300
392
|
}
|
|
301
393
|
}
|
package/lib/moa-report.js
CHANGED
|
@@ -20,17 +20,19 @@ export function generateMoABenchmarkReport({
|
|
|
20
20
|
}) {
|
|
21
21
|
const durationSec = (durationMs / 1000).toFixed(2)
|
|
22
22
|
const totalCost = typeof costUsd === 'number' ? costUsd.toFixed(4) : '0.0000'
|
|
23
|
-
const inTokens = usage?.inputTokens
|
|
24
|
-
const outTokens = usage?.outputTokens
|
|
25
|
-
const totalTokens = usage?.totalTokens
|
|
23
|
+
const inTokens = usage?.inputTokens ?? usage?.totalInputTokens ?? usage?.prompt_tokens ?? 0
|
|
24
|
+
const outTokens = usage?.outputTokens ?? usage?.totalOutputTokens ?? usage?.completion_tokens ?? 0
|
|
25
|
+
const totalTokens = usage?.totalTokens ?? (inTokens + outTokens)
|
|
26
26
|
|
|
27
27
|
const candidateRows = (candidates || []).map((c) => {
|
|
28
28
|
const idx = c.index || '?'
|
|
29
29
|
const label = c.label || `${c.slot?.provider}:${c.slot?.model}`
|
|
30
|
-
const role = c.role_persona || 'general'
|
|
30
|
+
const role = c.role_persona || c.role || 'general'
|
|
31
31
|
const status = c.ok ? '✅ OK' : '❌ Failed'
|
|
32
|
-
const
|
|
33
|
-
const
|
|
32
|
+
const cIn = c.usage?.inputTokens ?? c.usage?.totalInputTokens ?? c.usage?.prompt_tokens ?? 0
|
|
33
|
+
const cOut = c.usage?.outputTokens ?? c.usage?.totalOutputTokens ?? c.usage?.completion_tokens ?? 0
|
|
34
|
+
const candTokens = c.usage?.totalTokens ?? (cIn + cOut)
|
|
35
|
+
const candCost = typeof c.costUsd === 'number' ? `$${c.costUsd.toFixed(4)}` : '$0.0000'
|
|
34
36
|
const score = consensus?.averageScores?.[c.index] !== undefined
|
|
35
37
|
? `${consensus.averageScores[c.index]}/10`
|
|
36
38
|
: 'N/A'
|
|
@@ -39,16 +41,31 @@ export function generateMoABenchmarkReport({
|
|
|
39
41
|
}).join('\n')
|
|
40
42
|
|
|
41
43
|
let testGateSection = '### 🧪 Automated Test Gate\n- **Status**: Not Configured / Skipped'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const gates = Array.isArray(testGate) ? testGate : (testGate ? [testGate] : [])
|
|
45
|
+
const activeGates = gates.filter((tg) => tg && (tg.enabled !== false || tg.passed !== undefined))
|
|
46
|
+
if (activeGates.length === 1) {
|
|
47
|
+
const tg = activeGates[0]
|
|
48
|
+
const tgStatus = tg.passed ? '✅ PASSED' : '❌ FAILED'
|
|
44
49
|
testGateSection = `### 🧪 Automated Test Gate
|
|
45
50
|
- **Status**: ${tgStatus}
|
|
46
|
-
- **Test Command**: \`${
|
|
47
|
-
- **Exit Code**: \`${
|
|
51
|
+
- **Test Command**: \`${tg.command || 'N/A'}\`
|
|
52
|
+
- **Exit Code**: \`${tg.exitCode ?? 0}\`
|
|
48
53
|
- **Output Snippet**:
|
|
49
54
|
\`\`\`
|
|
50
|
-
${(
|
|
55
|
+
${(tg.output || tg.summary || 'No output recorded').slice(0, 500)}
|
|
51
56
|
\`\`\``
|
|
57
|
+
} else if (activeGates.length > 1) {
|
|
58
|
+
const allPassed = activeGates.every((tg) => tg.passed)
|
|
59
|
+
const passedCount = activeGates.filter((tg) => tg.passed).length
|
|
60
|
+
const overallStatus = allPassed ? '✅ ALL PASSED' : `❌ ${activeGates.length - passedCount}/${activeGates.length} FAILED`
|
|
61
|
+
const details = activeGates.map((tg, idx) => {
|
|
62
|
+
const label = tg.label || tg.model || `Candidate ${tg.candidateIndex ?? idx + 1}`
|
|
63
|
+
const tgStatus = tg.passed ? '✅ PASSED' : '❌ FAILED'
|
|
64
|
+
return `- **${label}**: ${tgStatus} (Command: \`${tg.command || 'N/A'}\`, Exit Code: \`${tg.exitCode ?? 0}\`)\n\`\`\`\n${(tg.output || tg.summary || 'No output recorded').slice(0, 300)}\n\`\`\``
|
|
65
|
+
}).join('\n')
|
|
66
|
+
testGateSection = `### 🧪 Automated Test Gate
|
|
67
|
+
- **Status**: ${overallStatus}
|
|
68
|
+
${details}`
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
let consensusSection = '### ⚖️ Multi-Judge Consensus\n- **Status**: Standard single-aggregator synthesis'
|
|
@@ -104,15 +121,19 @@ ${testGateSection}
|
|
|
104
121
|
durationMs,
|
|
105
122
|
usage: { inputTokens: inTokens, outputTokens: outTokens, totalTokens },
|
|
106
123
|
costUsd,
|
|
107
|
-
candidates: (candidates || []).map((c) =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
candidates: (candidates || []).map((c) => {
|
|
125
|
+
const cIn = c.usage?.inputTokens ?? c.usage?.totalInputTokens ?? c.usage?.prompt_tokens ?? 0
|
|
126
|
+
const cOut = c.usage?.outputTokens ?? c.usage?.totalOutputTokens ?? c.usage?.completion_tokens ?? 0
|
|
127
|
+
return {
|
|
128
|
+
index: c.index,
|
|
129
|
+
label: c.label,
|
|
130
|
+
role: c.role_persona || c.role,
|
|
131
|
+
ok: c.ok,
|
|
132
|
+
costUsd: typeof c.costUsd === 'number' ? c.costUsd : 0,
|
|
133
|
+
usage: { inputTokens: cIn, outputTokens: cOut, totalTokens: c.usage?.totalTokens ?? (cIn + cOut) },
|
|
134
|
+
score: consensus?.averageScores?.[c.index] ?? null,
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
116
137
|
winningIndex,
|
|
117
138
|
winningLabel,
|
|
118
139
|
consensus,
|
package/lib/moa-runner.js
CHANGED
|
@@ -402,6 +402,8 @@ export async function runMoAPipeline({
|
|
|
402
402
|
userPrompt,
|
|
403
403
|
callLlm,
|
|
404
404
|
strategy: preset.judge_voting_strategy || 'majority',
|
|
405
|
+
prices: prices || {},
|
|
406
|
+
blind: Boolean(preset.blind_evaluation),
|
|
405
407
|
signal,
|
|
406
408
|
onProgress,
|
|
407
409
|
})
|
|
@@ -409,7 +411,7 @@ export async function runMoAPipeline({
|
|
|
409
411
|
|
|
410
412
|
// 7d. Feature 3: Composite Block Merge Directive
|
|
411
413
|
const compositeMergeDirective = (preset?.composite_merge_enabled && successfulRefs.length > 1)
|
|
412
|
-
? buildCompositeBlockDirectives(successfulRefs)
|
|
414
|
+
? buildCompositeBlockDirectives(successfulRefs, { blind: Boolean(preset.blind_evaluation) })
|
|
413
415
|
: null
|
|
414
416
|
|
|
415
417
|
// 8. Synthesis phase via primary judge or fallback chain
|
|
@@ -512,7 +514,7 @@ export async function runMoAPipeline({
|
|
|
512
514
|
|
|
513
515
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
514
516
|
const durationMs = Date.now() - startTime
|
|
515
|
-
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
|
|
517
|
+
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage, multiJudgeResult?.totalUsage)
|
|
516
518
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
517
519
|
const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
|
|
518
520
|
const finalAggLabel = slotLabel(chosenJudge)
|
|
@@ -532,7 +534,7 @@ export async function runMoAPipeline({
|
|
|
532
534
|
winningIndex,
|
|
533
535
|
winningLabel: winnerModel,
|
|
534
536
|
consensus: multiJudgeResult,
|
|
535
|
-
testGate: referenceOutputs.
|
|
537
|
+
testGate: referenceOutputs.filter((r) => r.testResult).map((r) => r.testResult),
|
|
536
538
|
isComposite: Boolean(preset?.composite_merge_enabled),
|
|
537
539
|
})
|
|
538
540
|
}
|
package/lib/pricing.js
CHANGED
|
@@ -310,12 +310,16 @@ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cach
|
|
|
310
310
|
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null) {
|
|
313
|
+
export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null, multiJudgeUsage = null) {
|
|
314
314
|
const normAgg = aggUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
const
|
|
315
|
+
const normMj = multiJudgeUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
316
|
+
const totalInputTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.inputTokens || r.usage?.promptTokens || r.usage?.prompt_tokens || 0), 0) + (normAgg.inputTokens || normAgg.promptTokens || normAgg.prompt_tokens || 0) + (normMj.inputTokens || normMj.promptTokens || normMj.prompt_tokens || 0)
|
|
317
|
+
const totalOutputTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.outputTokens || r.usage?.completionTokens || r.usage?.completion_tokens || 0), 0) + (normAgg.outputTokens || normAgg.completionTokens || normAgg.completion_tokens || 0) + (normMj.outputTokens || normMj.completionTokens || normMj.completion_tokens || 0)
|
|
318
|
+
const candTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens ?? ((r.usage?.inputTokens || 0) + (r.usage?.outputTokens || 0))), 0)
|
|
319
|
+
const aggTokens = normAgg.totalTokens ?? ((normAgg.inputTokens || 0) + (normAgg.outputTokens || 0))
|
|
320
|
+
const mjTokens = normMj.totalTokens ?? ((normMj.inputTokens || 0) + (normMj.outputTokens || 0))
|
|
321
|
+
const totalTokens = candTokens + aggTokens + mjTokens || (totalInputTokens + totalOutputTokens)
|
|
322
|
+
const totalCostUsd = Number(((referenceOutputs || []).reduce((acc, r) => acc + (r.costUsd || 0), 0) + (normAgg.costUsd || 0) + (normMj.costUsd || 0)).toFixed(5))
|
|
319
323
|
const candidates = (referenceOutputs || []).map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd }))
|
|
320
|
-
return { totalTokens, totalInputTokens, totalOutputTokens, totalCostUsd, candidates, aggregator: normAgg }
|
|
324
|
+
return { totalTokens, totalInputTokens, totalOutputTokens, totalCostUsd, candidates, aggregator: normAgg, multiJudge: normMj }
|
|
321
325
|
}
|
package/lib/routes.js
CHANGED
|
@@ -333,7 +333,7 @@ ctx.effect(() => {
|
|
|
333
333
|
runId,
|
|
334
334
|
keepMoa: true,
|
|
335
335
|
port: ctx.webServer?.port,
|
|
336
|
-
timeMachineEngine: ctx.get?.('timeMachineEngine')
|
|
336
|
+
timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
337
337
|
force: Boolean(body?.force),
|
|
338
338
|
})
|
|
339
339
|
writeJson(res, 200, { ok: true, candidateIndex, promotedFiles, checkpoint: promotedFiles?.checkpoint || null })
|
|
@@ -571,7 +571,7 @@ ctx.effect(() => {
|
|
|
571
571
|
userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
|
|
572
572
|
cwd: payload.cwd || process.cwd(), prices: cfg.prices || {}, liveCanvas,
|
|
573
573
|
webServerPort: webServerPort || ctx.webServer?.port,
|
|
574
|
-
timeMachineEngine: timeMachineEngine || ctx.get?.('timeMachineEngine')
|
|
574
|
+
timeMachineEngine: timeMachineEngine || ctx.reflect?.get?.('timeMachineEngine', false),
|
|
575
575
|
timeMachineUrl: payload.timeMachineUrl, force: payload.force,
|
|
576
576
|
checkpoint: payload.checkpoint, checkpointFn: payload.checkpointFn,
|
|
577
577
|
})
|