@goodandready/dsh-moa 0.2.14 → 0.2.16
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 +7 -0
- package/lib/best-effort.js +38 -0
- package/lib/client.js +7 -4
- package/lib/file-workspace.js +62 -36
- package/lib/history.js +14 -4
- package/lib/index.js +17 -382
- package/lib/live-canvas.js +22 -0
- package/lib/moa-candidates.js +225 -0
- package/lib/moa-prompts.js +7 -4
- package/lib/moa-runner.js +38 -252
- package/lib/pricing.js +10 -1
- package/lib/routes.js +408 -0
- package/lib/updater.js +280 -0
- package/package.json +1 -2
- package/docs/README.ru.md +0 -287
- package/docs/README.zh.md +0 -243
- package/docs/design/DESIGN.md +0 -73
- package/docs/plans/47-client-js-split-plan.md +0 -57
- package/docs/plans/55-resilience-plan.md +0 -33
- package/docs/plans/59-power-pack-plan.md +0 -73
package/lib/moa-prompts.js
CHANGED
|
@@ -191,8 +191,11 @@ export function buildPeerCritiquePrompt(userPrompt, myProposal, otherProposals =
|
|
|
191
191
|
const othersText = otherProposals
|
|
192
192
|
.map((p, idx) => {
|
|
193
193
|
const label = isBlind ? `Candidate ${idx + 1}` : (p.label || `Candidate ${idx + 1}`)
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
let text = p.text || ''
|
|
195
|
+
if (text.length > 3000) {
|
|
196
|
+
text = stripOrSummarizeCode(text)
|
|
197
|
+
}
|
|
198
|
+
return `### ${label} Alternative Proposal:\n${text}`
|
|
196
199
|
})
|
|
197
200
|
.join('\n\n---\n\n')
|
|
198
201
|
|
|
@@ -224,7 +227,7 @@ export function buildCuratorSynthesisPrompt(userPrompt, referenceOutputs = [], j
|
|
|
224
227
|
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
225
228
|
: ''
|
|
226
229
|
let textContent = r.text
|
|
227
|
-
if (
|
|
230
|
+
if (textContent.length > 3000) {
|
|
228
231
|
textContent = stripOrSummarizeCode(textContent)
|
|
229
232
|
}
|
|
230
233
|
const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
|
|
@@ -291,7 +294,7 @@ export function buildSynthesisPrompt(userPrompt, referenceOutputs = [], judgeCri
|
|
|
291
294
|
? ` [Files created: ${r.files.map((f) => f.relativePath).join(', ')}]`
|
|
292
295
|
: ''
|
|
293
296
|
let textContent = r.text
|
|
294
|
-
if (
|
|
297
|
+
if (textContent.length > 3000) {
|
|
295
298
|
textContent = stripOrSummarizeCode(textContent)
|
|
296
299
|
}
|
|
297
300
|
const syntaxNote = r.syntaxWarning ? ` [⚠️ Syntax Warning: ${r.syntaxWarning}]` : ''
|
package/lib/moa-runner.js
CHANGED
|
@@ -1,255 +1,30 @@
|
|
|
1
|
+
import { bestEffort } from './best-effort.js'
|
|
1
2
|
/**
|
|
2
3
|
* DeepSeek Harness Mixture of Agents (MoA) — Runner Engine
|
|
3
|
-
*
|
|
4
|
-
* Implements the full ensemble pipeline:
|
|
5
|
-
* - Parallel fan-out to reference models with transient retry & quorum straggler mitigation
|
|
6
|
-
* - Prompt caching aligned message structures
|
|
7
|
-
* - Curator synthesis & antipatterns evaluation
|
|
8
|
-
* - Aggregator fallback chain for resilience & malformed output recovery
|
|
9
|
-
* - Live token streaming for aggregator
|
|
10
|
-
* - File promotion & Live Canvas sandbox preview
|
|
4
|
+
* Parallel fan-out, Consilium Round 2 peer critique, aggregator synthesis & streaming.
|
|
11
5
|
*/
|
|
12
6
|
|
|
13
7
|
import path from 'node:path'
|
|
14
8
|
import crypto from 'node:crypto'
|
|
15
|
-
import { extractFileBlocks, collectProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
16
|
-
import { estimateTokenCost } from './pricing.js'
|
|
17
|
-
import { recordMoaRun, recordMoaRunAsync } from './history.js'
|
|
9
|
+
import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
|
|
10
|
+
import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
11
|
+
import { recordMoaRun, recordMoaRunAsync, candidatesForHistory } from './history.js'
|
|
12
|
+
import { createPromotedPreview } from './live-canvas.js'
|
|
18
13
|
import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
19
14
|
import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
20
15
|
|
|
21
16
|
// Re-exports for consumers & backward compatibility
|
|
22
17
|
export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
|
|
23
18
|
export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
|
|
24
|
-
export { estimateTokenCost } from './pricing.js'
|
|
19
|
+
export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
|
|
21
|
+
const DEFAULT_PROMPT = 'Please propose an optimal, well-structured, production-ready solution with full code and explanations.'
|
|
28
22
|
|
|
29
23
|
/**
|
|
30
24
|
* Invokes LLM call with transient retry for recoverable network/rate-limit errors.
|
|
31
25
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
while (true) {
|
|
35
|
-
try {
|
|
36
|
-
return await callLlmFn(callArgs)
|
|
37
|
-
} catch (err) {
|
|
38
|
-
attempt++
|
|
39
|
-
const msg = err?.message || String(err)
|
|
40
|
-
const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
|
|
41
|
-
if (attempt <= maxRetries && isTransient) {
|
|
42
|
-
await new Promise((r) => setTimeout(r, retryDelayMs))
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
throw err
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
|
|
52
|
-
*/
|
|
53
|
-
export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
|
|
54
|
-
if (!Array.isArray(references) || references.length === 0) {
|
|
55
|
-
return []
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const advisoryMessages = cleanAdvisoryMessages(messages)
|
|
59
|
-
const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
|
|
60
|
-
const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
|
|
61
|
-
const maxRetries = options.maxRetries ?? 0
|
|
62
|
-
const quorumEnabled = Boolean(options.quorumEnabled)
|
|
63
|
-
const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
|
|
64
|
-
|
|
65
|
-
const total = references.length
|
|
66
|
-
let finishedCount = 0
|
|
67
|
-
const results = new Array(total)
|
|
68
|
-
const abortControllers = references.map(() => new AbortController())
|
|
69
|
-
|
|
70
|
-
let onTaskFinished = null
|
|
71
|
-
const notifyFinished = () => {
|
|
72
|
-
if (typeof onTaskFinished === 'function') onTaskFinished()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (typeof onProgress === 'function') {
|
|
76
|
-
onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
references.forEach((slot, i) => {
|
|
80
|
-
const label = slotLabel(slot)
|
|
81
|
-
const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
|
|
82
|
-
? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
|
|
83
|
-
: ''
|
|
84
|
-
const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
|
|
85
|
-
|
|
86
|
-
const runOne = async () => {
|
|
87
|
-
try {
|
|
88
|
-
const callPromise = callWithTransientRetry(
|
|
89
|
-
callLlm,
|
|
90
|
-
{
|
|
91
|
-
provider: slot.provider,
|
|
92
|
-
model: slot.model,
|
|
93
|
-
messages: slotMessages,
|
|
94
|
-
temperature: options.temperature ?? 0.6,
|
|
95
|
-
maxTokens: options.maxTokens ?? 4096,
|
|
96
|
-
timeoutMs,
|
|
97
|
-
signal: abortControllers[i].signal,
|
|
98
|
-
},
|
|
99
|
-
maxRetries
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
103
|
-
const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
|
|
104
|
-
timer.unref?.()
|
|
105
|
-
abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const res = await Promise.race([callPromise, timeoutPromise])
|
|
109
|
-
const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
|
|
110
|
-
|
|
111
|
-
const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
|
|
112
|
-
inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
|
|
113
|
-
outputTokens: Math.max(1, Math.round(text.length / 4)),
|
|
114
|
-
}
|
|
115
|
-
const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
|
|
116
|
-
|
|
117
|
-
finishedCount++
|
|
118
|
-
if (typeof onProgress === 'function') {
|
|
119
|
-
const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
|
|
120
|
-
onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
results[i] = {
|
|
124
|
-
index: i + 1,
|
|
125
|
-
slot,
|
|
126
|
-
label,
|
|
127
|
-
text,
|
|
128
|
-
usage: costInfo,
|
|
129
|
-
costUsd: costInfo.costUsd,
|
|
130
|
-
ok: true,
|
|
131
|
-
}
|
|
132
|
-
} catch (err) {
|
|
133
|
-
finishedCount++
|
|
134
|
-
const errMsg = err?.message || String(err)
|
|
135
|
-
console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
|
|
136
|
-
if (typeof onProgress === 'function') {
|
|
137
|
-
onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
|
|
138
|
-
}
|
|
139
|
-
results[i] = {
|
|
140
|
-
index: i + 1,
|
|
141
|
-
slot,
|
|
142
|
-
label,
|
|
143
|
-
text: `[Model ${label} error: ${errMsg}]`,
|
|
144
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
145
|
-
costUsd: 0,
|
|
146
|
-
ok: false,
|
|
147
|
-
error: errMsg,
|
|
148
|
-
}
|
|
149
|
-
} finally {
|
|
150
|
-
notifyFinished()
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
runOne()
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
// Straggler mitigation via Quorum + Grace Period
|
|
158
|
-
if (quorumEnabled && total >= 3) {
|
|
159
|
-
const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
|
|
160
|
-
let graceTimer = null
|
|
161
|
-
|
|
162
|
-
await new Promise((resolve) => {
|
|
163
|
-
const checkQuorum = () => {
|
|
164
|
-
if (finishedCount >= total) {
|
|
165
|
-
if (graceTimer) clearTimeout(graceTimer)
|
|
166
|
-
resolve()
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (finishedCount >= quorumTarget && !graceTimer) {
|
|
171
|
-
if (typeof onProgress === 'function') {
|
|
172
|
-
onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
|
|
173
|
-
}
|
|
174
|
-
graceTimer = setTimeout(() => {
|
|
175
|
-
if (typeof onProgress === 'function' && finishedCount < total) {
|
|
176
|
-
onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
|
|
177
|
-
}
|
|
178
|
-
for (let k = 0; k < total; k++) {
|
|
179
|
-
if (!results[k]) {
|
|
180
|
-
try { abortControllers[k].abort(new Error('Quorum grace period timed out')) } catch {}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
resolve()
|
|
184
|
-
}, gracePeriodMs)
|
|
185
|
-
graceTimer.unref?.()
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
onTaskFinished = checkQuorum
|
|
190
|
-
checkQuorum()
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
for (let i = 0; i < total; i++) {
|
|
194
|
-
if (!results[i]) {
|
|
195
|
-
const slot = references[i]
|
|
196
|
-
const label = slotLabel(slot)
|
|
197
|
-
results[i] = {
|
|
198
|
-
index: i + 1,
|
|
199
|
-
slot,
|
|
200
|
-
label,
|
|
201
|
-
text: `[Model ${label} timed out (quorum grace period exceeded)]`,
|
|
202
|
-
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
|
|
203
|
-
costUsd: 0,
|
|
204
|
-
ok: false,
|
|
205
|
-
error: 'Quorum grace period timed out',
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return results
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Standard mode: wait for all tasks to complete
|
|
213
|
-
await new Promise((resolve) => {
|
|
214
|
-
const checkAll = () => {
|
|
215
|
-
if (finishedCount >= total) resolve()
|
|
216
|
-
}
|
|
217
|
-
onTaskFinished = checkAll
|
|
218
|
-
checkAll()
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
return results
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function candidatesForHistory(referenceOutputs) {
|
|
225
|
-
return (referenceOutputs || []).map((r) => ({
|
|
226
|
-
provider: r.slot?.provider || '',
|
|
227
|
-
model: r.slot?.model || '',
|
|
228
|
-
files: (r.files || []).map((f) => f.relativePath),
|
|
229
|
-
usage: r.usage || { inputTokens: 0, outputTokens: 0 },
|
|
230
|
-
costUsd: r.costUsd || 0,
|
|
231
|
-
}))
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs) {
|
|
235
|
-
if (!liveCanvas || typeof liveCanvas.createPreviewFromContent !== 'function') return null
|
|
236
|
-
const htmlRel = (promotedFiles || []).find((f) => f.endsWith('.html') || f.endsWith('.htm'))
|
|
237
|
-
if (!htmlRel) return null
|
|
238
|
-
let content = null
|
|
239
|
-
for (const r of referenceOutputs || []) {
|
|
240
|
-
const block = (r?.files || []).find((f) => f.relativePath === htmlRel)
|
|
241
|
-
if (block?.content) {
|
|
242
|
-
content = block.content
|
|
243
|
-
break
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
if (!content) return null
|
|
247
|
-
try {
|
|
248
|
-
return await liveCanvas.createPreviewFromContent({ content, title: htmlRel, filePath: path.join(cwd, htmlRel) })
|
|
249
|
-
} catch {
|
|
250
|
-
return null
|
|
251
|
-
}
|
|
252
|
-
}
|
|
26
|
+
import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
|
|
27
|
+
export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel }
|
|
253
28
|
|
|
254
29
|
/**
|
|
255
30
|
* Executes the full Mixture of Agents pipeline.
|
|
@@ -265,7 +40,9 @@ export async function runMoAPipeline({
|
|
|
265
40
|
historyFilePath = null,
|
|
266
41
|
liveCanvas = null,
|
|
267
42
|
onStreamDelta = null,
|
|
43
|
+
signal = null,
|
|
268
44
|
}) {
|
|
45
|
+
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
269
46
|
const startTime = Date.now()
|
|
270
47
|
|
|
271
48
|
// 1. Resolve configurations
|
|
@@ -298,13 +75,14 @@ export async function runMoAPipeline({
|
|
|
298
75
|
const runId = crypto.randomUUID()
|
|
299
76
|
|
|
300
77
|
// 2. Collect project context for refinement tasks
|
|
301
|
-
const
|
|
78
|
+
const collectedCtx = await collectProjectContext(cwd, 16000)
|
|
79
|
+
const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
|
|
302
80
|
let projectContext = ''
|
|
303
81
|
if (isRefinement) {
|
|
304
82
|
if (typeof onProgress === 'function') {
|
|
305
83
|
onProgress('🔍 *Reading project files for refinement context...*\n')
|
|
306
84
|
}
|
|
307
|
-
projectContext =
|
|
85
|
+
projectContext = formatProjectContext(collectedCtx.files)
|
|
308
86
|
}
|
|
309
87
|
|
|
310
88
|
// 3. Build prompts & evaluate broad questionnaire needs
|
|
@@ -330,11 +108,17 @@ export async function runMoAPipeline({
|
|
|
330
108
|
quorumEnabled: isQuorumEnabled,
|
|
331
109
|
gracePeriodSec,
|
|
332
110
|
maxRetries: candidateRetries,
|
|
111
|
+
signal,
|
|
333
112
|
},
|
|
334
113
|
callLlm,
|
|
335
114
|
onProgress
|
|
336
115
|
)
|
|
337
116
|
|
|
117
|
+
if (signal?.aborted) {
|
|
118
|
+
await cleanMoaWorkspaces(cwd)
|
|
119
|
+
return { error: new Error('Turn aborted') }
|
|
120
|
+
}
|
|
121
|
+
|
|
338
122
|
// Fail fast if all candidates failed
|
|
339
123
|
const successfulRefs = referenceOutputs.filter((r) => r.ok)
|
|
340
124
|
if (successfulRefs.length === 0) {
|
|
@@ -422,7 +206,9 @@ export async function runMoAPipeline({
|
|
|
422
206
|
totalCostUsd,
|
|
423
207
|
durationMs,
|
|
424
208
|
}, historyFilePath || undefined)
|
|
425
|
-
} catch {
|
|
209
|
+
} catch (histErr) {
|
|
210
|
+
console.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
|
|
211
|
+
}
|
|
426
212
|
|
|
427
213
|
return {
|
|
428
214
|
kind: 'questions',
|
|
@@ -475,7 +261,9 @@ export async function runMoAPipeline({
|
|
|
475
261
|
totalCostUsd,
|
|
476
262
|
durationMs,
|
|
477
263
|
}, historyFilePath || undefined)
|
|
478
|
-
} catch {
|
|
264
|
+
} catch (histErr) {
|
|
265
|
+
console.warn('[dsh-moa] Failed to record fast synthesis run in history:', histErr?.message || histErr)
|
|
266
|
+
}
|
|
479
267
|
|
|
480
268
|
return {
|
|
481
269
|
kind: 'synthesis',
|
|
@@ -518,6 +306,7 @@ export async function runMoAPipeline({
|
|
|
518
306
|
temperature: refTemp,
|
|
519
307
|
maxTokens,
|
|
520
308
|
timeoutMs: refTimeoutSec * 1000,
|
|
309
|
+
signal,
|
|
521
310
|
}, candidateRetries)
|
|
522
311
|
const refinedText = typeof res === 'string' ? res : (res?.content || res?.text || cand.text)
|
|
523
312
|
cand.text = refinedText
|
|
@@ -525,6 +314,7 @@ export async function runMoAPipeline({
|
|
|
525
314
|
if (r2Files.length > 0) {
|
|
526
315
|
cand.files = r2Files
|
|
527
316
|
cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
|
|
317
|
+
await writeCandidateWorkspace(cwd, cand.index, r2Files)
|
|
528
318
|
}
|
|
529
319
|
if (typeof res === 'object' && res?.usage) {
|
|
530
320
|
cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
|
|
@@ -533,7 +323,9 @@ export async function runMoAPipeline({
|
|
|
533
323
|
const extraCost = estimateTokenCost(slot, res.usage, prices)
|
|
534
324
|
cand.costUsd = Number(((cand.costUsd || 0) + extraCost.costUsd).toFixed(5))
|
|
535
325
|
}
|
|
536
|
-
} catch {
|
|
326
|
+
} catch (r2CostErr) {
|
|
327
|
+
console.warn('[dsh-moa] Failed to estimate Round 2 cost:', r2CostErr?.message || r2CostErr)
|
|
328
|
+
}
|
|
537
329
|
})
|
|
538
330
|
await Promise.allSettled(r2Promises)
|
|
539
331
|
}
|
|
@@ -567,6 +359,7 @@ export async function runMoAPipeline({
|
|
|
567
359
|
temperature: aggTemp,
|
|
568
360
|
maxTokens,
|
|
569
361
|
timeoutMs: aggTimeoutSec * 1000,
|
|
362
|
+
signal,
|
|
570
363
|
onStreamDelta: (delta) => {
|
|
571
364
|
if (isStreamAggregator && typeof onStreamDelta === 'function') {
|
|
572
365
|
onStreamDelta(delta)
|
|
@@ -626,15 +419,12 @@ export async function runMoAPipeline({
|
|
|
626
419
|
|
|
627
420
|
const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
|
|
628
421
|
|
|
629
|
-
const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + aggUsage.totalTokens
|
|
630
|
-
const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + aggUsage.costUsd).toFixed(5))
|
|
631
422
|
const durationMs = Date.now() - startTime
|
|
632
|
-
|
|
423
|
+
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
|
|
633
424
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
634
425
|
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
635
426
|
const finalAggLabel = slotLabel(chosenJudge)
|
|
636
427
|
|
|
637
|
-
// Record run to history
|
|
638
428
|
try {
|
|
639
429
|
recordMoaRun({
|
|
640
430
|
id: runId,
|
|
@@ -646,8 +436,8 @@ export async function runMoAPipeline({
|
|
|
646
436
|
winnerIndex: winningIndex,
|
|
647
437
|
winnerModel,
|
|
648
438
|
promotedFiles,
|
|
649
|
-
totalTokens,
|
|
650
|
-
totalCostUsd,
|
|
439
|
+
totalTokens: usageSummary.totalTokens,
|
|
440
|
+
totalCostUsd: usageSummary.totalCostUsd,
|
|
651
441
|
durationMs,
|
|
652
442
|
}, historyFilePath || undefined)
|
|
653
443
|
} catch (histErr) {
|
|
@@ -670,12 +460,7 @@ export async function runMoAPipeline({
|
|
|
670
460
|
runId,
|
|
671
461
|
allowCandidateOverride,
|
|
672
462
|
...(livePreview ? { liveCanvas: livePreview } : {}),
|
|
673
|
-
usage:
|
|
674
|
-
totalTokens,
|
|
675
|
-
totalCostUsd,
|
|
676
|
-
candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd })),
|
|
677
|
-
aggregator: aggUsage,
|
|
678
|
-
},
|
|
463
|
+
usage: usageSummary,
|
|
679
464
|
durationMs,
|
|
680
465
|
}
|
|
681
466
|
}
|
|
@@ -716,6 +501,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
|
|
|
716
501
|
prices,
|
|
717
502
|
historyFilePath,
|
|
718
503
|
liveCanvas,
|
|
504
|
+
signal,
|
|
719
505
|
})
|
|
720
506
|
.catch((err) => ({ error: err }))
|
|
721
507
|
.finally(() => {
|
package/lib/pricing.js
CHANGED
|
@@ -205,4 +205,13 @@ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cach
|
|
|
205
205
|
rates,
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
-
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null) {
|
|
212
|
+
const normAgg = aggUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
|
|
213
|
+
const totalTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + normAgg.totalTokens
|
|
214
|
+
const totalCostUsd = Number(((referenceOutputs || []).reduce((acc, r) => acc + (r.costUsd || 0), 0) + normAgg.costUsd).toFixed(5))
|
|
215
|
+
const candidates = (referenceOutputs || []).map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd }))
|
|
216
|
+
return { totalTokens, totalCostUsd, candidates, aggregator: normAgg }
|
|
217
|
+
}
|