@goodandready/dsh-moa 0.2.4 → 0.2.6
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 +8 -1
- package/docs/README.ru.md +8 -1
- package/lib/client.js +258 -135
- package/lib/index.js +52 -40
- package/lib/moa-runner.js +11 -33
- package/lib/pricing.js +194 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Single-model AI generation often suffers from blind spots, single-perspective bi
|
|
|
37
37
|
4. **Instant Live Canvas Previewing**: When web applications or UI components are generated, `dsh-moa` integrates seamlessly with `@goodandready/dsh-live-canvas`, automatically spawning sandboxes for 1-click browser previewing.
|
|
38
38
|
5. **Token-Saving Chat Summarization**: Replaces massive code dumps in chat bubbles with compact file listings and clean architectural summaries.
|
|
39
39
|
6. **One-Shot Session Model Restoration**: Executes cleanly as a one-shot turn modifier, automatically reverting back to the user's primary session model immediately after completion.
|
|
40
|
-
7. **
|
|
40
|
+
7. **Dynamic Model Pricing Catalog & Token Estimation**: Real-time rate resolution for 300+ models fetched automatically in the background from OpenRouter's public catalog (cached locally in `~/.dsh/storages/dsh-moa-catalog.json` for 24h), plus support for direct vendor rates and custom `prices` overrides in `settings.yaml`.
|
|
41
41
|
8. **Refinement Mode (Incremental Edits)**: Automatically detects existing codebase context to generate precise delta modifications instead of destructive full-file rewrites.
|
|
42
42
|
9. **Fast Mode & Custom Judge Criteria**: Ultra-fast single-model preset for quick tasks and customizable evaluation guidelines for the judge.
|
|
43
43
|
10. **Run History & Win-Rate Leaderboard**: Persistent logging with built-in REST endpoints (`/dsh-moa/history` and `/dsh-moa/leaderboard`).
|
|
@@ -156,6 +156,13 @@ dsh-moa:
|
|
|
156
156
|
aggregator:
|
|
157
157
|
provider: "your-reasoning-provider"
|
|
158
158
|
model: "your-judge-model"
|
|
159
|
+
prices:
|
|
160
|
+
"my-provider/my-model":
|
|
161
|
+
input: 0.20
|
|
162
|
+
output: 0.80
|
|
163
|
+
"ollama/*":
|
|
164
|
+
input: 0
|
|
165
|
+
output: 0
|
|
159
166
|
code-review:
|
|
160
167
|
references:
|
|
161
168
|
- provider: "your-fast-provider"
|
package/docs/README.ru.md
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
4. **Мгновенный предпросмотр через Live Canvas**: При создании веб-приложений и UI-компонентов `dsh-moa` бесшовно интегрируется с `@goodandready/dsh-live-canvas`, автоматически инициализируя контейнер для предпросмотра в браузере в 1 клик.
|
|
38
38
|
5. **Экономия токенов в чате**: Вместо вывода огромных листингов кода в чат формируется компактный отчет с перечнем созданных файлов и архитектурным резюме.
|
|
39
39
|
6. **Одноразовый модификатор сессии**: Команда выполняется в рамках одного такта и автоматически возвращает исходную модель сессии пользователя сразу после завершения.
|
|
40
|
-
7.
|
|
40
|
+
7. **Динамический каталог тарифов и подсчет токенов**: Актуальные цены на 300+ моделей автоматически подтягиваются из публичного каталога OpenRouter (без ключей и авторизации), кешируются в `~/.dsh/storages/dsh-moa-catalog.json` на 24 часа, а также поддерживают прямые вендорские тарифы и пользовательские оверрайды `prices` в `settings.yaml`.
|
|
41
41
|
8. **Режим доработки (Refinement Mode)**: Автоматически считывает контекст существующего проекта и генерирует точечные дельта-правки без перетирания всей кодовой базы.
|
|
42
42
|
9. **Быстрый режим (Fast Mode) и критерии судьи**: Режим для одиночных быстрых задач без судьи и гибкая настройка фокуса оценки (безопасность, производительность, минимализм).
|
|
43
43
|
10. **История запусков и лидерборд моделей**: Персистентное логирование запусков и REST-эндпоинты (`/dsh-moa/history` и `/dsh-moa/leaderboard`).
|
|
@@ -156,6 +156,13 @@ dsh-moa:
|
|
|
156
156
|
aggregator:
|
|
157
157
|
provider: "your-reasoning-provider"
|
|
158
158
|
model: "your-judge-model"
|
|
159
|
+
prices:
|
|
160
|
+
"my-provider/my-model":
|
|
161
|
+
input: 0.20
|
|
162
|
+
output: 0.80
|
|
163
|
+
"ollama/*":
|
|
164
|
+
input: 0
|
|
165
|
+
output: 0
|
|
159
166
|
code-review:
|
|
160
167
|
references:
|
|
161
168
|
- provider: "your-fast-provider"
|
package/lib/client.js
CHANGED
|
@@ -379,6 +379,10 @@ window.__ModuleLoader__.load({
|
|
|
379
379
|
updateCurrentPreset((p) => ({ ...p, reference_temperature: val }))
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
const setJudgeCriteria = (criteria) => {
|
|
383
|
+
updateCurrentPreset((p) => ({ ...p, judge_criteria: criteria }))
|
|
384
|
+
}
|
|
385
|
+
|
|
382
386
|
const setReferenceModel = (idx, prov, mod) => {
|
|
383
387
|
updateCurrentPreset((p) => {
|
|
384
388
|
const refs = [...(p.reference_models || [])]
|
|
@@ -469,6 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
469
473
|
const currentRefs = currentPreset.reference_models || []
|
|
470
474
|
const aggTemp = currentPreset.aggregator_temperature ?? 0.4
|
|
471
475
|
const refTemp = currentPreset.reference_temperature ?? 0.6
|
|
476
|
+
const judgeCriteria = currentPreset.judge_criteria || ''
|
|
472
477
|
|
|
473
478
|
return React.createElement(
|
|
474
479
|
React.Fragment,
|
|
@@ -769,16 +774,159 @@ window.__ModuleLoader__.load({
|
|
|
769
774
|
)
|
|
770
775
|
}
|
|
771
776
|
|
|
777
|
+
let rootCtx = null
|
|
778
|
+
|
|
779
|
+
const en = {
|
|
780
|
+
title: 'Mixture of Agents (MoA)',
|
|
781
|
+
description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
|
|
782
|
+
slashMoa: 'Run task through Mixture of Agents (/moa <prompt>)',
|
|
783
|
+
defaultPreset: 'Default Preset (used on /moa without arguments)',
|
|
784
|
+
isDefault: 'default',
|
|
785
|
+
presetSelectLabel: 'Active Preset to Configure:',
|
|
786
|
+
createNewPreset: 'Create new preset...',
|
|
787
|
+
makeDefault: 'Make Default',
|
|
788
|
+
makeDefaultHint: 'Use this preset for /moa when no preset argument is given',
|
|
789
|
+
deletePreset: 'Delete preset',
|
|
790
|
+
delete: 'Delete',
|
|
791
|
+
confirmDeletePreset: 'Are you sure you want to delete preset',
|
|
792
|
+
newPresetPlaceholder: 'New preset name (e.g. fast-audit)',
|
|
793
|
+
create: 'Create',
|
|
794
|
+
cancel: 'Cancel',
|
|
795
|
+
aggregatorTitle: 'Judge / Aggregator Model (who reviews and synthesizes)',
|
|
796
|
+
aggregatorDesc: 'Evaluates candidate proposals, corrects hallucinations, and outputs the final unified solution.',
|
|
797
|
+
temperature: 'Temperature',
|
|
798
|
+
aggTempHint: 'Recommended 0.2 - 0.4 for rigorous verification and factual synthesis.',
|
|
799
|
+
proposersTitle: 'Candidate Models (Advisors)',
|
|
800
|
+
proposersDesc: 'Generates independent candidate answers in parallel to provide diverse perspectives.',
|
|
801
|
+
refTempLabel: 'Candidates Temperature (solution diversity):',
|
|
802
|
+
refTempHint: 'Recommended 0.6 - 0.8 for diverse creative hypotheses.',
|
|
803
|
+
parallelCount: 'Parallel workers',
|
|
804
|
+
candidate: 'Candidate',
|
|
805
|
+
addCandidate: 'Add parallel model slot',
|
|
806
|
+
remove: 'Remove',
|
|
807
|
+
save: 'Save Changes',
|
|
808
|
+
saved: 'Saved successfully',
|
|
809
|
+
loading: 'Saving…',
|
|
810
|
+
retry: 'Retry connection',
|
|
811
|
+
unavailable: 'MoA service temporarily unavailable (reconnecting...)',
|
|
812
|
+
searchPlaceholder: 'Search by model name or provider...',
|
|
813
|
+
modelsFound: 'Found',
|
|
814
|
+
of: 'of',
|
|
815
|
+
clear: 'Clear',
|
|
816
|
+
noModelsMatch: 'No models found matching query.',
|
|
817
|
+
useCustom: 'Use custom',
|
|
818
|
+
selectModel: 'Select model...',
|
|
819
|
+
judgeCriteriaLabel: 'Custom Judge Evaluation Criteria (optional):',
|
|
820
|
+
judgeCriteriaPlaceholder: 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const ru = {
|
|
824
|
+
title: 'Mixture of Agents (MoA)',
|
|
825
|
+
description: 'Запуск задач через ансамбль параллельных моделей-советников и модель-судью',
|
|
826
|
+
slashMoa: 'Запустить задачу через архитектуру Mixture of Agents (/moa <prompt>)',
|
|
827
|
+
defaultPreset: 'Основной пресет (при вызове /moa без параметров)',
|
|
828
|
+
isDefault: 'основной',
|
|
829
|
+
presetSelectLabel: 'Текущий пресет для настройки:',
|
|
830
|
+
createNewPreset: 'Создать новый пресет...',
|
|
831
|
+
makeDefault: 'Сделать основным',
|
|
832
|
+
makeDefaultHint: 'Использовать этот пресет для /moa при вызове без параметров',
|
|
833
|
+
deletePreset: 'Удалить пресет',
|
|
834
|
+
delete: 'Удалить',
|
|
835
|
+
confirmDeletePreset: 'Вы уверены, что хотите удалить пресет',
|
|
836
|
+
newPresetPlaceholder: 'Название нового пресета (например: fast-audit)',
|
|
837
|
+
create: 'Создать',
|
|
838
|
+
cancel: 'Отмена',
|
|
839
|
+
aggregatorTitle: 'Ведущая модель / Судья (кто проверяет и объединяет)',
|
|
840
|
+
aggregatorDesc: 'Сопоставляет варианты кандидатов, устраняет ошибки и синтезирует итоговое решение.',
|
|
841
|
+
temperature: 'Температура',
|
|
842
|
+
aggTempHint: 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
|
|
843
|
+
proposersTitle: 'Модели-кандидаты (советники)',
|
|
844
|
+
proposersDesc: 'Параллельно формируют независимые варианты решения одной задачи.',
|
|
845
|
+
refTempLabel: 'Температура кандидатов (разнообразие решений):',
|
|
846
|
+
refTempHint: 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.',
|
|
847
|
+
parallelCount: 'Параллельных моделей',
|
|
848
|
+
candidate: 'Кандидат',
|
|
849
|
+
addCandidate: 'Добавить модель в параллель',
|
|
850
|
+
remove: 'Удалить',
|
|
851
|
+
save: 'Сохранить',
|
|
852
|
+
saved: 'Успешно сохранено',
|
|
853
|
+
loading: 'Сохранение…',
|
|
854
|
+
retry: 'Повторить подключение',
|
|
855
|
+
unavailable: 'Служба MoA временно недоступна (переподключение...)',
|
|
856
|
+
searchPlaceholder: 'Поиск модели по названию или провайдеру...',
|
|
857
|
+
modelsFound: 'Найдено',
|
|
858
|
+
of: 'из',
|
|
859
|
+
clear: 'Сбросить',
|
|
860
|
+
noModelsMatch: 'Модели не найдены.',
|
|
861
|
+
useCustom: 'Использовать кастомное',
|
|
862
|
+
selectModel: 'Выберите модель...',
|
|
863
|
+
judgeCriteriaLabel: 'Критерии оценки судьи (опционально):',
|
|
864
|
+
judgeCriteriaPlaceholder: 'например: Приоритет — производительность, минимум зависимостей, обработка граничных случаев...',
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function makeT(ctx, props) {
|
|
868
|
+
return (key) => {
|
|
869
|
+
if (props && typeof props.t === 'function') {
|
|
870
|
+
try {
|
|
871
|
+
const res = props.t(key)
|
|
872
|
+
if (res && res !== key) return res
|
|
873
|
+
} catch (_) {}
|
|
874
|
+
}
|
|
875
|
+
const effectiveCtx = (props && props.ctx) || ctx || rootCtx
|
|
876
|
+
if (effectiveCtx && effectiveCtx.locale && typeof effectiveCtx.locale.bind === 'function') {
|
|
877
|
+
try {
|
|
878
|
+
const bound = effectiveCtx.locale.bind(NS)
|
|
879
|
+
const res = bound(key)
|
|
880
|
+
if (res && res !== key) return res
|
|
881
|
+
} catch (_) {}
|
|
882
|
+
}
|
|
883
|
+
const curLocale = (effectiveCtx && effectiveCtx.locale && effectiveCtx.locale.getSnapshot && effectiveCtx.locale.getSnapshot().active) || 'ru'
|
|
884
|
+
const dict = curLocale === 'en' ? en : ru
|
|
885
|
+
return dict[key] || ru[key] || en[key] || key
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
772
889
|
function useMoASettings(props) {
|
|
773
|
-
const t =
|
|
890
|
+
const t = makeT(rootCtx, props)
|
|
891
|
+
const effectiveCtx = (props && props.ctx) || rootCtx
|
|
892
|
+
|
|
893
|
+
// Reactive binding to settingsScope if available
|
|
894
|
+
const scope = React.useMemo(() => {
|
|
895
|
+
if (!effectiveCtx) return undefined
|
|
896
|
+
const s = effectiveCtx.get ? effectiveCtx.get('settingsScope') : effectiveCtx.settingsScope
|
|
897
|
+
return s && typeof s.bind === 'function' ? s.bind({ namespace: NS }) : undefined
|
|
898
|
+
}, [effectiveCtx])
|
|
899
|
+
|
|
900
|
+
const snapshot = React.useSyncExternalStore
|
|
901
|
+
? React.useSyncExternalStore(
|
|
902
|
+
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
903
|
+
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'ready' }), [scope]),
|
|
904
|
+
React.useCallback(() => ({ status: 'loading' }), [])
|
|
905
|
+
)
|
|
906
|
+
: null
|
|
907
|
+
|
|
774
908
|
const [status, setStatus] = React.useState('loading')
|
|
775
909
|
const [presets, setPresets] = React.useState([])
|
|
776
910
|
const [defaultPreset, setDefaultPreset] = React.useState('default')
|
|
777
911
|
const [availableModels, setAvailableModels] = React.useState([])
|
|
778
912
|
const [saveStatus, setSaveStatus] = React.useState('')
|
|
779
913
|
|
|
914
|
+
// Sync from settingsScope snapshot when available
|
|
915
|
+
React.useEffect(() => {
|
|
916
|
+
if (!scope || !snapshot) return
|
|
917
|
+
const snapStatus = snapshot.status || 'ready'
|
|
918
|
+
if (snapStatus === 'ready') {
|
|
919
|
+
const val = snapshot.value || {}
|
|
920
|
+
if (Array.isArray(val.presets)) setPresets(val.presets)
|
|
921
|
+
if (val.default_preset) setDefaultPreset(val.default_preset)
|
|
922
|
+
setStatus('ready')
|
|
923
|
+
} else if (snapStatus === 'unavailable') {
|
|
924
|
+
setStatus('unavailable')
|
|
925
|
+
}
|
|
926
|
+
}, [scope, snapshot])
|
|
927
|
+
|
|
780
928
|
const reload = React.useCallback(() => {
|
|
781
|
-
setStatus('loading')
|
|
929
|
+
if (!scope) setStatus('loading')
|
|
782
930
|
|
|
783
931
|
fetch('/dsh-moa/presets', { cache: 'no-store' })
|
|
784
932
|
.then((r) => {
|
|
@@ -790,13 +938,13 @@ window.__ModuleLoader__.load({
|
|
|
790
938
|
setPresets(data.presets || [])
|
|
791
939
|
setDefaultPreset(data.defaultPreset || 'default')
|
|
792
940
|
setStatus('ready')
|
|
793
|
-
} else {
|
|
941
|
+
} else if (!scope) {
|
|
794
942
|
setStatus('unavailable')
|
|
795
943
|
}
|
|
796
944
|
})
|
|
797
945
|
.catch((err) => {
|
|
798
946
|
console.warn('[dsh-moa] Presets fetch failed:', err)
|
|
799
|
-
setStatus('unavailable')
|
|
947
|
+
if (!scope) setStatus('unavailable')
|
|
800
948
|
})
|
|
801
949
|
|
|
802
950
|
fetch('/dsh-moa/models', { cache: 'no-store' })
|
|
@@ -809,7 +957,7 @@ window.__ModuleLoader__.load({
|
|
|
809
957
|
.catch((err) => {
|
|
810
958
|
console.warn('[dsh-moa] Models fetch failed:', err)
|
|
811
959
|
})
|
|
812
|
-
}, [])
|
|
960
|
+
}, [scope])
|
|
813
961
|
|
|
814
962
|
React.useEffect(() => {
|
|
815
963
|
reload()
|
|
@@ -817,6 +965,27 @@ window.__ModuleLoader__.load({
|
|
|
817
965
|
|
|
818
966
|
const handleSave = () => {
|
|
819
967
|
setSaveStatus(t('loading'))
|
|
968
|
+
|
|
969
|
+
if (scope && typeof scope.set === 'function') {
|
|
970
|
+
Promise.all([
|
|
971
|
+
scope.set('presets', presets),
|
|
972
|
+
scope.set('default_preset', defaultPreset),
|
|
973
|
+
])
|
|
974
|
+
.then(() => {
|
|
975
|
+
setSaveStatus(t('saved'))
|
|
976
|
+
setTimeout(() => setSaveStatus(''), 2500)
|
|
977
|
+
})
|
|
978
|
+
.catch((err) => {
|
|
979
|
+
console.warn('[dsh-moa] scope.set failed, trying REST fallback:', err)
|
|
980
|
+
doRestSave()
|
|
981
|
+
})
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
doRestSave()
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const doRestSave = () => {
|
|
820
989
|
fetch('/dsh-moa/presets', {
|
|
821
990
|
method: 'POST',
|
|
822
991
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -883,6 +1052,64 @@ window.__ModuleLoader__.load({
|
|
|
883
1052
|
)
|
|
884
1053
|
}
|
|
885
1054
|
|
|
1055
|
+
|
|
1056
|
+
// ── ERROR BOUNDARY ──────────────────────────────────────────────
|
|
1057
|
+
class MoAErrorBoundary extends React.Component {
|
|
1058
|
+
constructor(props) {
|
|
1059
|
+
super(props)
|
|
1060
|
+
this.state = { hasError: false, error: null, errorInfo: null }
|
|
1061
|
+
}
|
|
1062
|
+
static getDerivedStateFromError(error) {
|
|
1063
|
+
return { hasError: true, error }
|
|
1064
|
+
}
|
|
1065
|
+
componentDidCatch(error, errorInfo) {
|
|
1066
|
+
console.error('[dsh-moa] MoAErrorBoundary caught error:', error, errorInfo)
|
|
1067
|
+
this.setState({ errorInfo })
|
|
1068
|
+
}
|
|
1069
|
+
render() {
|
|
1070
|
+
if (this.state.hasError) {
|
|
1071
|
+
return React.createElement(
|
|
1072
|
+
'div',
|
|
1073
|
+
{
|
|
1074
|
+
style: {
|
|
1075
|
+
padding: '16px',
|
|
1076
|
+
borderRadius: '8px',
|
|
1077
|
+
border: '1px solid #ef4444',
|
|
1078
|
+
background: 'rgba(239,68,68,0.08)',
|
|
1079
|
+
color: '#ef4444',
|
|
1080
|
+
fontSize: '13px',
|
|
1081
|
+
fontFamily: 'monospace',
|
|
1082
|
+
whiteSpace: 'pre-wrap',
|
|
1083
|
+
wordBreak: 'break-word',
|
|
1084
|
+
},
|
|
1085
|
+
},
|
|
1086
|
+
React.createElement('div', { style: { fontWeight: 700, marginBottom: 8 } }, '⚠ MoA Settings Error'),
|
|
1087
|
+
React.createElement('div', null, String(this.state.error)),
|
|
1088
|
+
this.state.errorInfo && React.createElement(
|
|
1089
|
+
'details',
|
|
1090
|
+
{ style: { marginTop: 8 } },
|
|
1091
|
+
React.createElement('summary', null, 'Component stack'),
|
|
1092
|
+
React.createElement('pre', { style: { fontSize: 11 } }, this.state.errorInfo.componentStack || '')
|
|
1093
|
+
),
|
|
1094
|
+
React.createElement(
|
|
1095
|
+
'button',
|
|
1096
|
+
{
|
|
1097
|
+
type: 'button',
|
|
1098
|
+
style: {
|
|
1099
|
+
marginTop: 12, padding: '6px 16px', border: '1px solid #ef4444',
|
|
1100
|
+
background: 'transparent', color: '#ef4444', borderRadius: 6,
|
|
1101
|
+
cursor: 'pointer', fontSize: 12,
|
|
1102
|
+
},
|
|
1103
|
+
onClick: () => this.setState({ hasError: false, error: null, errorInfo: null }),
|
|
1104
|
+
},
|
|
1105
|
+
'Retry'
|
|
1106
|
+
)
|
|
1107
|
+
)
|
|
1108
|
+
}
|
|
1109
|
+
return this.props.children
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
886
1113
|
function MoACard(props) {
|
|
887
1114
|
ensureStyles()
|
|
888
1115
|
const state = useMoASettings(props)
|
|
@@ -936,142 +1163,41 @@ window.__ModuleLoader__.load({
|
|
|
936
1163
|
)
|
|
937
1164
|
}
|
|
938
1165
|
|
|
939
|
-
exports.inject = ['slots', 'locale', 'inputTriggers']
|
|
1166
|
+
exports.inject = ['slots', 'locale', 'inputTriggers', 'settingsScope']
|
|
940
1167
|
exports.apply = function apply(ctx) {
|
|
941
|
-
|
|
942
|
-
|
|
1168
|
+
rootCtx = ctx
|
|
1169
|
+
const t = makeT(ctx)
|
|
1170
|
+
const addLocale = (locale, dictionary) => {
|
|
943
1171
|
try {
|
|
944
1172
|
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
945
|
-
|
|
946
|
-
return ctx.locale.register(NS, {
|
|
947
|
-
en: {
|
|
948
|
-
title: 'Mixture of Agents (MoA)',
|
|
949
|
-
description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
|
|
950
|
-
slashMoa: 'Run task through Mixture of Agents (/moa <prompt>)',
|
|
951
|
-
defaultPreset: 'Default Preset (used on /moa without arguments)',
|
|
952
|
-
isDefault: 'default',
|
|
953
|
-
presetSelectLabel: 'Active Preset to Configure:',
|
|
954
|
-
createNewPreset: 'Create new preset...',
|
|
955
|
-
makeDefault: 'Make Default',
|
|
956
|
-
makeDefaultHint: 'Use this preset for /moa when no preset argument is given',
|
|
957
|
-
deletePreset: 'Delete preset',
|
|
958
|
-
delete: 'Delete',
|
|
959
|
-
confirmDeletePreset: 'Are you sure you want to delete preset',
|
|
960
|
-
newPresetPlaceholder: 'New preset name (e.g. fast-audit)',
|
|
961
|
-
create: 'Create',
|
|
962
|
-
cancel: 'Cancel',
|
|
963
|
-
aggregatorTitle: 'Judge / Aggregator Model (who reviews and synthesizes)',
|
|
964
|
-
aggregatorDesc: 'Evaluates candidate proposals, corrects hallucinations, and outputs the final unified solution.',
|
|
965
|
-
temperature: 'Temperature',
|
|
966
|
-
aggTempHint: 'Recommended 0.2 - 0.4 for rigorous verification and factual synthesis.',
|
|
967
|
-
proposersTitle: 'Candidate Models (Advisors)',
|
|
968
|
-
proposersDesc: 'Generates independent candidate answers in parallel to provide diverse perspectives.',
|
|
969
|
-
refTempLabel: 'Candidates Temperature (solution diversity):',
|
|
970
|
-
refTempHint: 'Recommended 0.6 - 0.8 for diverse creative hypotheses.',
|
|
971
|
-
parallelCount: 'Parallel workers',
|
|
972
|
-
candidate: 'Candidate',
|
|
973
|
-
addCandidate: 'Add parallel model slot',
|
|
974
|
-
remove: 'Remove',
|
|
975
|
-
save: 'Save Changes',
|
|
976
|
-
saved: 'Saved successfully',
|
|
977
|
-
loading: 'Saving…',
|
|
978
|
-
retry: 'Retry connection',
|
|
979
|
-
unavailable: 'MoA service temporarily unavailable (reconnecting...)',
|
|
980
|
-
searchPlaceholder: 'Search by model name or provider...',
|
|
981
|
-
modelsFound: 'Found',
|
|
982
|
-
of: 'of',
|
|
983
|
-
clear: 'Clear',
|
|
984
|
-
noModelsMatch: 'No models found matching query.',
|
|
985
|
-
useCustom: 'Use custom',
|
|
986
|
-
selectModel: 'Select model...',
|
|
987
|
-
judgeCriteriaLabel: 'Custom Judge Evaluation Criteria (optional):',
|
|
988
|
-
judgeCriteriaPlaceholder: 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
|
|
989
|
-
},
|
|
990
|
-
ru: {
|
|
991
|
-
title: 'Mixture of Agents (MoA)',
|
|
992
|
-
description: 'Запуск задач через ансамбль параллельных моделей-советников и модель-судью',
|
|
993
|
-
slashMoa: 'Запустить задачу через архитектуру Mixture of Agents (/moa <prompt>)',
|
|
994
|
-
defaultPreset: 'Основной пресет (при вызове /moa без параметров)',
|
|
995
|
-
isDefault: 'основной',
|
|
996
|
-
presetSelectLabel: 'Текущий пресет для настройки:',
|
|
997
|
-
createNewPreset: 'Создать новый пресет...',
|
|
998
|
-
makeDefault: 'Сделать основным',
|
|
999
|
-
makeDefaultHint: 'Использовать этот пресет для /moa при вызове без параметров',
|
|
1000
|
-
deletePreset: 'Удалить пресет',
|
|
1001
|
-
delete: 'Удалить',
|
|
1002
|
-
confirmDeletePreset: 'Вы уверены, что хотите удалить пресет',
|
|
1003
|
-
newPresetPlaceholder: 'Название нового пресета (например: fast-audit)',
|
|
1004
|
-
create: 'Создать',
|
|
1005
|
-
cancel: 'Отмена',
|
|
1006
|
-
aggregatorTitle: 'Ведущая модель / Судья (кто проверяет и объединяет)',
|
|
1007
|
-
aggregatorDesc: 'Сопоставляет варианты кандидатов, устраняет ошибки и синтезирует итоговое решение.',
|
|
1008
|
-
temperature: 'Температура',
|
|
1009
|
-
aggTempHint: 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
|
|
1010
|
-
proposersTitle: 'Модели-кандидаты (советники)',
|
|
1011
|
-
proposersDesc: 'Параллельно формируют независимые варианты решения одной задачи.',
|
|
1012
|
-
refTempLabel: 'Температура кандидатов (разнообразие решений):',
|
|
1013
|
-
refTempHint: 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.',
|
|
1014
|
-
parallelCount: 'Параллельных моделей',
|
|
1015
|
-
candidate: 'Кандидат',
|
|
1016
|
-
addCandidate: 'Добавить модель в параллель',
|
|
1017
|
-
remove: 'Удалить',
|
|
1018
|
-
save: 'Сохранить',
|
|
1019
|
-
saved: 'Успешно сохранено',
|
|
1020
|
-
loading: 'Сохранение…',
|
|
1021
|
-
retry: 'Повторить подключение',
|
|
1022
|
-
unavailable: 'Служба MoA временно недоступна (переподключение...)',
|
|
1023
|
-
searchPlaceholder: 'Поиск модели по названию или провайдеру...',
|
|
1024
|
-
modelsFound: 'Найдено',
|
|
1025
|
-
of: 'из',
|
|
1026
|
-
clear: 'Сбросить',
|
|
1027
|
-
noModelsMatch: 'Модели не найдены.',
|
|
1028
|
-
useCustom: 'Использовать кастомное',
|
|
1029
|
-
selectModel: 'Выберите модель...',
|
|
1030
|
-
judgeCriteriaLabel: 'Критерии оценки судьи (опционально):',
|
|
1031
|
-
judgeCriteriaPlaceholder: 'например: Приоритет — производительность, минимум зависимостей, обработка граничных случаев...',
|
|
1032
|
-
},
|
|
1033
|
-
})
|
|
1173
|
+
return ctx.locale.register(NS, locale, dictionary)
|
|
1034
1174
|
}
|
|
1035
|
-
} catch
|
|
1036
|
-
|
|
1175
|
+
} catch {
|
|
1176
|
+
// Если язык уже занят (например, dsh-russian-lang) или зарегистрирован ранее — уступаем без ошибки
|
|
1177
|
+
return () => {}
|
|
1037
1178
|
}
|
|
1179
|
+
return () => {}
|
|
1038
1180
|
}
|
|
1039
1181
|
|
|
1040
1182
|
if (typeof ctx.effect === 'function') {
|
|
1041
|
-
ctx.effect(() =>
|
|
1183
|
+
ctx.effect(() => {
|
|
1184
|
+
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1185
|
+
return () => {
|
|
1186
|
+
for (const off of undo) {
|
|
1187
|
+
if (typeof off === 'function') {
|
|
1188
|
+
try { off() } catch {}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}, 'dsh-moa: dictionaries')
|
|
1042
1193
|
} else {
|
|
1043
|
-
|
|
1194
|
+
addLocale('en', en)
|
|
1195
|
+
addLocale('ru', ru)
|
|
1044
1196
|
}
|
|
1045
1197
|
|
|
1046
|
-
const t = (key) => {
|
|
1047
|
-
try {
|
|
1048
|
-
if (ctx.locale && typeof ctx.locale.bind === 'function') {
|
|
1049
|
-
return ctx.locale.bind(NS)(key) || key
|
|
1050
|
-
}
|
|
1051
|
-
} catch {}
|
|
1052
|
-
return key
|
|
1053
|
-
}
|
|
1054
1198
|
|
|
1055
1199
|
if (ctx.slots) {
|
|
1056
|
-
const
|
|
1057
|
-
try {
|
|
1058
|
-
ctx.slots.register(
|
|
1059
|
-
{
|
|
1060
|
-
name: 'settings.section',
|
|
1061
|
-
id: NS,
|
|
1062
|
-
order: 35,
|
|
1063
|
-
locale: NS,
|
|
1064
|
-
label: () => t('title'),
|
|
1065
|
-
inject: () => ({ ctx, t }),
|
|
1066
|
-
},
|
|
1067
|
-
MoASection
|
|
1068
|
-
)
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
console.warn('[dsh-moa] Failed to register settings.section:', err)
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
const registerPluginCard = () => {
|
|
1200
|
+
const registerCard = () => {
|
|
1075
1201
|
try {
|
|
1076
1202
|
ctx.slots.register(
|
|
1077
1203
|
{
|
|
@@ -1079,10 +1205,9 @@ window.__ModuleLoader__.load({
|
|
|
1079
1205
|
key: NS,
|
|
1080
1206
|
order: 35,
|
|
1081
1207
|
locale: NS,
|
|
1082
|
-
|
|
1083
|
-
inject: () => ({ ctx, t }),
|
|
1208
|
+
inject: () => ({ ctx }),
|
|
1084
1209
|
},
|
|
1085
|
-
MoACard
|
|
1210
|
+
(props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
|
|
1086
1211
|
)
|
|
1087
1212
|
} catch (err) {
|
|
1088
1213
|
console.warn('[dsh-moa] Failed to register settings.plugin.item:', err)
|
|
@@ -1090,11 +1215,9 @@ window.__ModuleLoader__.load({
|
|
|
1090
1215
|
}
|
|
1091
1216
|
|
|
1092
1217
|
if (typeof ctx.slots.inject === 'function') {
|
|
1093
|
-
ctx.slots.inject('settings.
|
|
1094
|
-
ctx.slots.inject('settings.plugin.item', registerPluginCard)
|
|
1218
|
+
ctx.slots.inject('settings.plugin.item', registerCard)
|
|
1095
1219
|
} else {
|
|
1096
|
-
|
|
1097
|
-
registerPluginCard()
|
|
1220
|
+
registerCard()
|
|
1098
1221
|
}
|
|
1099
1222
|
}
|
|
1100
1223
|
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refreshCatalogInBackground } from './pricing.js'
|
|
1
2
|
/**
|
|
2
3
|
* DeepSeek Harness Mixture of Agents (MoA) Plugin
|
|
3
4
|
*
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
* 5. Native cost tracking, history logging and leaderboard analytics
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import
|
|
13
|
+
import z from '@deepseek-ai/schemastery'
|
|
13
14
|
import path from 'node:path'
|
|
14
15
|
import os from 'node:os'
|
|
15
16
|
import {
|
|
@@ -29,6 +30,12 @@ export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
|
|
|
29
30
|
|
|
30
31
|
export const NS = 'dsh-moa'
|
|
31
32
|
|
|
33
|
+
export const PriceRow = z.object({
|
|
34
|
+
input: z.number().default(0),
|
|
35
|
+
output: z.number().default(0),
|
|
36
|
+
cacheHit: z.number().default(0),
|
|
37
|
+
})
|
|
38
|
+
|
|
32
39
|
export const ModelSlotSchema = z.object({
|
|
33
40
|
provider: z.string().default('opencode-go'),
|
|
34
41
|
model: z.string().default('deepseek-v4-flash'),
|
|
@@ -39,16 +46,17 @@ export const PresetSchema = z.object({
|
|
|
39
46
|
enabled: z.boolean().default(true),
|
|
40
47
|
reference_models: z.array(ModelSlotSchema).default([]),
|
|
41
48
|
aggregator: ModelSlotSchema.default({ provider: 'codex', model: 'gpt-5.6-sol' }),
|
|
42
|
-
reference_temperature: z.number().
|
|
43
|
-
aggregator_temperature: z.number().
|
|
44
|
-
max_tokens: z.number().
|
|
45
|
-
judge_criteria: z.string().
|
|
46
|
-
judge_mode: z.
|
|
49
|
+
reference_temperature: z.number().default(0.6),
|
|
50
|
+
aggregator_temperature: z.number().default(0.4),
|
|
51
|
+
max_tokens: z.number().default(4096),
|
|
52
|
+
judge_criteria: z.string().default(''),
|
|
53
|
+
judge_mode: z.string().default('auto'),
|
|
47
54
|
})
|
|
48
55
|
|
|
49
56
|
export const Config = z.object({
|
|
50
57
|
enabled: z.boolean().default(true),
|
|
51
58
|
default_preset: z.string().default('default'),
|
|
59
|
+
prices: z.dict(PriceRow).default({}),
|
|
52
60
|
presets: z.array(PresetSchema).default([
|
|
53
61
|
{
|
|
54
62
|
name: 'default',
|
|
@@ -118,24 +126,26 @@ function readBody(req) {
|
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
export function apply(ctx, config) {
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
refreshCatalogInBackground()
|
|
130
|
+
|
|
131
|
+
let settingsScope = null
|
|
132
|
+
let getConfig = () => config
|
|
133
|
+
const live = () => Config(structuredClone(getConfig() ?? {})) ?? config
|
|
123
134
|
|
|
124
135
|
ctx.inject(['settings'], (sctx) => {
|
|
125
136
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
const scope = sctx.settings.register(NS, Config, { base: config })
|
|
138
|
+
settingsScope = scope
|
|
139
|
+
getConfig = () => scope.get() ?? config
|
|
140
|
+
sctx.effect(() => () => {
|
|
141
|
+
settingsScope = null
|
|
142
|
+
getConfig = () => config
|
|
143
|
+
}, 'dsh-moa: settings')
|
|
132
144
|
} catch (e) {
|
|
133
145
|
console.warn('[dsh-moa] Settings registration warning:', e)
|
|
134
146
|
}
|
|
135
147
|
})
|
|
136
148
|
|
|
137
|
-
const getConfig = () => currentConfig
|
|
138
|
-
|
|
139
149
|
/**
|
|
140
150
|
* Unified LLM dispatch function using ctx.llm.prepareCall / stream
|
|
141
151
|
*/
|
|
@@ -144,7 +154,21 @@ export function apply(ctx, config) {
|
|
|
144
154
|
throw new Error('ctx.llm is not available in cordis context')
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
let prep
|
|
158
|
+
try {
|
|
159
|
+
// DSH 0.1.2-rc.1 API expects { provider, model } object
|
|
160
|
+
prep = await ctx.llm.prepareCall({ provider, model })
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (typeof ctx.llm.prepareCall === 'function') {
|
|
163
|
+
try {
|
|
164
|
+
prep = await ctx.llm.prepareCall(provider, model)
|
|
165
|
+
} catch {
|
|
166
|
+
throw err
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
throw err
|
|
170
|
+
}
|
|
171
|
+
}
|
|
148
172
|
if (!prep || typeof prep.stream !== 'function') {
|
|
149
173
|
throw new Error(`LLM provider/model ${provider}:${model} could not be prepared`)
|
|
150
174
|
}
|
|
@@ -195,7 +219,7 @@ export function apply(ctx, config) {
|
|
|
195
219
|
kind: 'exact',
|
|
196
220
|
path: '/dsh-moa/status',
|
|
197
221
|
handler: (_req, res) => {
|
|
198
|
-
const cfg =
|
|
222
|
+
const cfg = live()
|
|
199
223
|
writeJson(res, 200, {
|
|
200
224
|
ok: true,
|
|
201
225
|
enabled: cfg.enabled !== false,
|
|
@@ -299,7 +323,7 @@ export function apply(ctx, config) {
|
|
|
299
323
|
kind: 'exact',
|
|
300
324
|
path: '/dsh-moa/presets',
|
|
301
325
|
handler: async (req, res) => {
|
|
302
|
-
const cfg =
|
|
326
|
+
const cfg = live()
|
|
303
327
|
if (req.method === 'GET') {
|
|
304
328
|
writeJson(res, 200, {
|
|
305
329
|
ok: true,
|
|
@@ -319,24 +343,12 @@ export function apply(ctx, config) {
|
|
|
319
343
|
presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
|
|
320
344
|
}
|
|
321
345
|
|
|
322
|
-
//
|
|
323
|
-
if (
|
|
324
|
-
await
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// 2. Direct persistence to ~/.dsh/settings.yaml
|
|
329
|
-
try {
|
|
330
|
-
const fs = await import('node:fs')
|
|
331
|
-
const yaml = await import('yaml')
|
|
332
|
-
const p = path.join(os.homedir(), '.dsh', 'settings.yaml')
|
|
333
|
-
if (fs.existsSync(p)) {
|
|
334
|
-
const curYaml = yaml.parse(fs.readFileSync(p, 'utf8')) || {}
|
|
335
|
-
curYaml[NS] = newConfig
|
|
336
|
-
fs.writeFileSync(p, yaml.stringify(curYaml), 'utf8')
|
|
337
|
-
}
|
|
338
|
-
} catch (fsErr) {
|
|
339
|
-
console.warn('[dsh-moa] direct file write error:', fsErr)
|
|
346
|
+
// Persist via Cordis Settings API
|
|
347
|
+
if (settingsScope && typeof settingsScope.replace === 'function') {
|
|
348
|
+
await settingsScope.replace(newConfig)
|
|
349
|
+
} else {
|
|
350
|
+
// In-memory fallback if settings service is unavailable
|
|
351
|
+
getConfig = () => newConfig
|
|
340
352
|
}
|
|
341
353
|
|
|
342
354
|
writeJson(res, 200, {
|
|
@@ -418,7 +430,7 @@ export function apply(ctx, config) {
|
|
|
418
430
|
return
|
|
419
431
|
}
|
|
420
432
|
|
|
421
|
-
const cfg =
|
|
433
|
+
const cfg = live()
|
|
422
434
|
const presetName = payload.preset || cfg.default_preset || 'default'
|
|
423
435
|
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
424
436
|
|
|
@@ -450,7 +462,7 @@ export function apply(ctx, config) {
|
|
|
450
462
|
? lastUser.content.filter((p) => p.type === 'text').map((p) => p.text).join('\n')
|
|
451
463
|
: '')
|
|
452
464
|
|
|
453
|
-
const cfg =
|
|
465
|
+
const cfg = live()
|
|
454
466
|
const parsed = parseMoACommand(userText, cfg.presets || []) || { prompt: userText, presetName: 'default' }
|
|
455
467
|
const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName) || cfg.presets?.[0]
|
|
456
468
|
|
|
@@ -487,7 +499,7 @@ export function apply(ctx, config) {
|
|
|
487
499
|
: '')
|
|
488
500
|
|
|
489
501
|
if (userText.startsWith('/moa')) {
|
|
490
|
-
const cfg =
|
|
502
|
+
const cfg = live()
|
|
491
503
|
if (cfg.enabled !== false) {
|
|
492
504
|
if (event.signal) {
|
|
493
505
|
moaPendingSignals.add(event.signal)
|
package/lib/moa-runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { estimateTokenCost as calculateTokenCost, resolveModelRates, refreshCatalogInBackground, DIRECT_VENDOR_RATES, FALLBACK_RATES } from './pricing.js'
|
|
1
2
|
/**
|
|
2
3
|
* Mixture of Agents (MoA) execution engine with Interactive Questioning,
|
|
3
4
|
* Isolated Multi-Candidate File Execution, Native Cost Tracking,
|
|
@@ -59,36 +60,10 @@ The user has provided a prompt that may have multiple design choices, architectu
|
|
|
59
60
|
Review the user prompt and identify 1 to 3 critical, high-impact clarifying questions or architectural options that would define the implementation (e.g. framework/vanilla, features, design style, target environment).
|
|
60
61
|
Keep questions very clear, structured, and actionable. Avoid trivial questions. Respond directly in Russian.`
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{ pattern: /deepseek.*flash|v4.*flash/i, inputPerM: 0.14, outputPerM: 0.28 },
|
|
67
|
-
{ pattern: /deepseek.*reasoner|r1/i, inputPerM: 0.55, outputPerM: 2.19 },
|
|
68
|
-
{ pattern: /deepseek/i, inputPerM: 0.27, outputPerM: 1.10 },
|
|
69
|
-
{ pattern: /gpt-4o-mini|gpt-5.*mini/i, inputPerM: 0.15, outputPerM: 0.60 },
|
|
70
|
-
{ pattern: /gpt-5|gpt-4o|sol/i, inputPerM: 2.50, outputPerM: 10.00 },
|
|
71
|
-
{ pattern: /grok/i, inputPerM: 2.00, outputPerM: 10.00 },
|
|
72
|
-
{ pattern: /claude-3-5-sonnet|claude-3-7-sonnet/i, inputPerM: 3.00, outputPerM: 15.00 },
|
|
73
|
-
{ pattern: /claude-3-5-haiku/i, inputPerM: 0.80, outputPerM: 4.00 },
|
|
74
|
-
{ pattern: /claude/i, inputPerM: 3.00, outputPerM: 15.00 },
|
|
75
|
-
{ pattern: /qwen/i, inputPerM: 0.40, outputPerM: 1.20 },
|
|
76
|
-
{ pattern: /commandcode/i, inputPerM: 0.50, outputPerM: 1.50 },
|
|
77
|
-
{ pattern: /.*/, inputPerM: 0.50, outputPerM: 1.50 },
|
|
78
|
-
]
|
|
79
|
-
|
|
80
|
-
export function estimateTokenCost(slot, usage = {}) {
|
|
81
|
-
const modelStr = `${slot?.provider || ''} ${slot?.model || ''}`.trim()
|
|
82
|
-
const match = MODEL_PRICING_REGISTRY.find((p) => p.pattern.test(modelStr)) || MODEL_PRICING_REGISTRY[MODEL_PRICING_REGISTRY.length - 1]
|
|
83
|
-
const inTokens = usage.inputTokens || usage.promptTokens || usage.input_tokens || 0
|
|
84
|
-
const outTokens = usage.outputTokens || usage.completionTokens || usage.output_tokens || 0
|
|
85
|
-
const cost = (inTokens * match.inputPerM / 1_000_000) + (outTokens * match.outputPerM / 1_000_000)
|
|
86
|
-
return {
|
|
87
|
-
inputTokens: inTokens,
|
|
88
|
-
outputTokens: outTokens,
|
|
89
|
-
totalTokens: inTokens + outTokens,
|
|
90
|
-
costUsd: Number(cost.toFixed(5)),
|
|
91
|
-
}
|
|
63
|
+
export { resolveModelRates, refreshCatalogInBackground, DIRECT_VENDOR_RATES, FALLBACK_RATES }
|
|
64
|
+
|
|
65
|
+
export function estimateTokenCost(slot, usage = {}, customPrices = {}) {
|
|
66
|
+
return calculateTokenCost(slot, usage, customPrices)
|
|
92
67
|
}
|
|
93
68
|
|
|
94
69
|
export function slotLabel(slot) {
|
|
@@ -323,7 +298,7 @@ export async function runReferencesParallel(references, messages, options = {},
|
|
|
323
298
|
inputTokens: Math.round(JSON.stringify(fullMessages).length / 4),
|
|
324
299
|
outputTokens: Math.round(text.length / 4),
|
|
325
300
|
}
|
|
326
|
-
const costInfo = estimateTokenCost(slot, rawUsage)
|
|
301
|
+
const costInfo = estimateTokenCost(slot, rawUsage, options.prices)
|
|
327
302
|
|
|
328
303
|
if (typeof onProgress === 'function') {
|
|
329
304
|
const fileMsg = files.length > 0 ? ` (создано файлов: ${files.length})` : ''
|
|
@@ -378,6 +353,7 @@ export async function runMoAPipeline({
|
|
|
378
353
|
cwd,
|
|
379
354
|
onProgress,
|
|
380
355
|
skipQuestions = false,
|
|
356
|
+
prices = {},
|
|
381
357
|
}) {
|
|
382
358
|
if (typeof callLlm !== 'function') {
|
|
383
359
|
throw new Error('callLlm function is required for runMoAPipeline')
|
|
@@ -520,7 +496,7 @@ export async function runMoAPipeline({
|
|
|
520
496
|
inputTokens: Math.round(synthPrompt.length / 4),
|
|
521
497
|
outputTokens: Math.round(synthesizedText.length / 4),
|
|
522
498
|
}
|
|
523
|
-
aggUsage = estimateTokenCost(aggregator, rawAggUsage)
|
|
499
|
+
aggUsage = estimateTokenCost(aggregator, rawAggUsage, prices)
|
|
524
500
|
} catch (err) {
|
|
525
501
|
synthesizedText = `[Aggregator error: ${err?.message || String(err)}]\n\nFallback candidate outputs:\n\n` +
|
|
526
502
|
referenceOutputs.map((r, i) => `### ${r.label}\n${r.text}`).join('\n\n')
|
|
@@ -734,7 +710,9 @@ export class MoaRunnerAdapter {
|
|
|
734
710
|
return { provider, id: model, name: 'MoA Ensemble' }
|
|
735
711
|
}
|
|
736
712
|
|
|
737
|
-
async prepareCall(
|
|
713
|
+
async prepareCall(providerOrConfig, modelOrSignal, _signal) {
|
|
714
|
+
const provider = typeof providerOrConfig === 'object' && providerOrConfig !== null ? providerOrConfig.provider : providerOrConfig
|
|
715
|
+
const model = typeof providerOrConfig === 'object' && providerOrConfig !== null ? providerOrConfig.model : modelOrSignal
|
|
738
716
|
return {
|
|
739
717
|
model: { provider, id: model, name: 'MoA Ensemble' },
|
|
740
718
|
stream: (options) => this.stream(options),
|
package/lib/pricing.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// lib/pricing.js
|
|
2
|
+
// Dynamic pricing registry for @goodandready/dsh-moa.
|
|
3
|
+
//
|
|
4
|
+
// Fetches real-time model rates from the public OpenRouter catalog
|
|
5
|
+
// (https://openrouter.ai/api/v1/models, no auth required), caches rates in
|
|
6
|
+
// ~/.dsh/storages/dsh-moa-catalog.json, and supports direct vendor tariffs
|
|
7
|
+
// and user config overrides in settings.yaml.
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
export const CATALOG_URL = 'https://openrouter.ai/api/v1/models'
|
|
14
|
+
export const CACHE_FILE = join(homedir(), '.dsh', 'storages', 'dsh-moa-catalog.json')
|
|
15
|
+
export const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Direct vendor tariffs (USD per 1M tokens) for official direct routes.
|
|
19
|
+
*/
|
|
20
|
+
export const DIRECT_VENDOR_RATES = {
|
|
21
|
+
'deepseek-chat': { input: 0.14, output: 0.28, cacheHit: 0.014 },
|
|
22
|
+
'deepseek-reasoner': { input: 0.55, output: 2.19, cacheHit: 0.14 },
|
|
23
|
+
'deepseek-v4-flash': { input: 0.14, output: 0.28, cacheHit: 0.014 },
|
|
24
|
+
'deepseek-v4-pro': { input: 0.55, output: 2.19, cacheHit: 0.044 },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Default fallback rate for uncataloged models ($0.50 prompt / $1.50 completion per 1M tokens) */
|
|
28
|
+
export const FALLBACK_RATES = { input: 0.50, output: 1.50, cacheHit: 0.15 }
|
|
29
|
+
|
|
30
|
+
let memoryCatalog = null
|
|
31
|
+
let lastFetchedAt = 0
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load cached catalog from disk into memory.
|
|
35
|
+
*/
|
|
36
|
+
export function loadCachedCatalog(cachePath = CACHE_FILE) {
|
|
37
|
+
if (memoryCatalog) return memoryCatalog
|
|
38
|
+
try {
|
|
39
|
+
if (existsSync(cachePath)) {
|
|
40
|
+
const raw = readFileSync(cachePath, 'utf8')
|
|
41
|
+
const parsed = JSON.parse(raw)
|
|
42
|
+
if (parsed && typeof parsed.models === 'object') {
|
|
43
|
+
memoryCatalog = parsed.models
|
|
44
|
+
lastFetchedAt = parsed.updatedAt || 0
|
|
45
|
+
return memoryCatalog
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Ignore read/parse errors
|
|
50
|
+
}
|
|
51
|
+
memoryCatalog = {}
|
|
52
|
+
return memoryCatalog
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fetch and update catalog from OpenRouter public API.
|
|
57
|
+
*/
|
|
58
|
+
export async function fetchCatalog({
|
|
59
|
+
url = CATALOG_URL,
|
|
60
|
+
cachePath = CACHE_FILE,
|
|
61
|
+
signal = AbortSignal.timeout(5000),
|
|
62
|
+
} = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(url, { signal })
|
|
65
|
+
if (!res.ok) return loadCachedCatalog(cachePath)
|
|
66
|
+
const json = await res.json()
|
|
67
|
+
if (!json || !Array.isArray(json.data)) return loadCachedCatalog(cachePath)
|
|
68
|
+
|
|
69
|
+
const models = {}
|
|
70
|
+
for (const item of json.data) {
|
|
71
|
+
if (!item.id || !item.pricing) continue
|
|
72
|
+
const promptPer1M = Number(item.pricing.prompt || 0) * 1e6
|
|
73
|
+
const completionPer1M = Number(item.pricing.completion || 0) * 1e6
|
|
74
|
+
const cacheHitPer1M = Number(item.pricing.input_cache_hit || item.pricing.prompt || 0) * 1e6
|
|
75
|
+
|
|
76
|
+
models[item.id] = {
|
|
77
|
+
input: promptPer1M,
|
|
78
|
+
output: completionPer1M,
|
|
79
|
+
cacheHit: cacheHitPer1M,
|
|
80
|
+
contextLength: item.context_length || 0,
|
|
81
|
+
name: item.name || item.id,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
memoryCatalog = models
|
|
86
|
+
lastFetchedAt = Date.now()
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
mkdirSync(dirname(cachePath), { recursive: true })
|
|
90
|
+
writeFileSync(
|
|
91
|
+
cachePath,
|
|
92
|
+
JSON.stringify({ updatedAt: lastFetchedAt, count: Object.keys(models).length, models }, null, 2),
|
|
93
|
+
'utf8',
|
|
94
|
+
)
|
|
95
|
+
} catch {
|
|
96
|
+
// Non-fatal if filesystem is read-only
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return models
|
|
100
|
+
} catch {
|
|
101
|
+
return loadCachedCatalog(cachePath)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Background refresh catalog if stale (> 24 hours).
|
|
107
|
+
*/
|
|
108
|
+
export function refreshCatalogInBackground(cachePath = CACHE_FILE) {
|
|
109
|
+
const catalog = loadCachedCatalog(cachePath)
|
|
110
|
+
const isStale = !lastFetchedAt || Date.now() - lastFetchedAt > REFRESH_INTERVAL_MS
|
|
111
|
+
if (isStale) {
|
|
112
|
+
fetchCatalog({ cachePath }).catch(() => {})
|
|
113
|
+
}
|
|
114
|
+
return catalog
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve price rates (USD per 1M tokens) for a given provider/model slot.
|
|
119
|
+
*/
|
|
120
|
+
export function resolveModelRates(slot = {}, customPrices = {}, cachePath = CACHE_FILE) {
|
|
121
|
+
const provider = (slot?.provider || '').trim().toLowerCase()
|
|
122
|
+
const model = (slot?.model || '').trim().toLowerCase()
|
|
123
|
+
const fullKey = provider ? `${provider}/${model}` : model
|
|
124
|
+
|
|
125
|
+
// 1. Check custom user overrides in config
|
|
126
|
+
if (customPrices && typeof customPrices === 'object') {
|
|
127
|
+
if (customPrices[fullKey]) return normalizeRates(customPrices[fullKey])
|
|
128
|
+
if (customPrices[model]) return normalizeRates(customPrices[model])
|
|
129
|
+
if (provider && customPrices[`${provider}/*`]) return normalizeRates(customPrices[`${provider}/*`])
|
|
130
|
+
if (customPrices['*']) return normalizeRates(customPrices['*'])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Check direct vendor rates
|
|
134
|
+
if (DIRECT_VENDOR_RATES[model]) {
|
|
135
|
+
return { ...DIRECT_VENDOR_RATES[model] }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 3. Check OpenRouter catalog
|
|
139
|
+
const catalog = loadCachedCatalog(cachePath) || {}
|
|
140
|
+
|
|
141
|
+
if (catalog[fullKey]) return { ...catalog[fullKey] }
|
|
142
|
+
if (catalog[model]) return { ...catalog[model] }
|
|
143
|
+
|
|
144
|
+
for (const [catId, catRate] of Object.entries(catalog)) {
|
|
145
|
+
const catLower = catId.toLowerCase()
|
|
146
|
+
if (catLower.endsWith(`/${model}`) || catLower === model) {
|
|
147
|
+
return { ...catRate }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const [catId, catRate] of Object.entries(catalog)) {
|
|
152
|
+
const catLower = catId.toLowerCase()
|
|
153
|
+
if (model && (catLower.includes(model) || model.includes(catLower))) {
|
|
154
|
+
return { ...catRate }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 4. Fallback rate
|
|
159
|
+
return { ...FALLBACK_RATES }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeRates(rate) {
|
|
163
|
+
if (!rate || typeof rate !== 'object') return { ...FALLBACK_RATES }
|
|
164
|
+
return {
|
|
165
|
+
input: Number(rate.input ?? rate.prompt ?? FALLBACK_RATES.input),
|
|
166
|
+
output: Number(rate.output ?? rate.completion ?? FALLBACK_RATES.output),
|
|
167
|
+
cacheHit: Number(rate.cacheHit ?? rate.cache_hit ?? FALLBACK_RATES.cacheHit),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Estimate USD cost and token totals for a single model call.
|
|
173
|
+
*/
|
|
174
|
+
export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cachePath = CACHE_FILE) {
|
|
175
|
+
const promptTokens = usage.prompt_tokens ?? usage.inputTokens ?? usage.input ?? usage.promptTokens ?? 0
|
|
176
|
+
const completionTokens = usage.completion_tokens ?? usage.outputTokens ?? usage.output ?? usage.completionTokens ?? 0
|
|
177
|
+
const cacheHitTokens = usage.prompt_tokens_details?.cached_tokens ?? usage.cacheHitTokens ?? 0
|
|
178
|
+
const totalTokens = promptTokens + completionTokens
|
|
179
|
+
|
|
180
|
+
const rates = resolveModelRates(slot, customPrices, cachePath)
|
|
181
|
+
|
|
182
|
+
const nonCachedPrompt = Math.max(0, promptTokens - cacheHitTokens)
|
|
183
|
+
const inputCost = (nonCachedPrompt / 1e6) * rates.input + (cacheHitTokens / 1e6) * rates.cacheHit
|
|
184
|
+
const outputCost = (completionTokens / 1e6) * rates.output
|
|
185
|
+
const totalCost = inputCost + outputCost
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
inputTokens: promptTokens,
|
|
189
|
+
outputTokens: completionTokens,
|
|
190
|
+
totalTokens,
|
|
191
|
+
costUsd: Number(totalCost.toFixed(5)),
|
|
192
|
+
rates,
|
|
193
|
+
}
|
|
194
|
+
}
|