@goodandready/dsh-moa 0.2.4 → 0.2.5

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 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. **Inherent Cost Tracking & Token Estimation**: Embedded pricing calculator for frontier models estimating token usage and run costs directly in the summary badge without external dependencies.
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,8 +774,120 @@ 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 = props.t || ((k) => k)
890
+ const t = makeT(rootCtx, props)
774
891
  const [status, setStatus] = React.useState('loading')
775
892
  const [presets, setPresets] = React.useState([])
776
893
  const [defaultPreset, setDefaultPreset] = React.useState('default')
@@ -883,6 +1000,64 @@ window.__ModuleLoader__.load({
883
1000
  )
884
1001
  }
885
1002
 
1003
+
1004
+ // ── ERROR BOUNDARY ──────────────────────────────────────────────
1005
+ class MoAErrorBoundary extends React.Component {
1006
+ constructor(props) {
1007
+ super(props)
1008
+ this.state = { hasError: false, error: null, errorInfo: null }
1009
+ }
1010
+ static getDerivedStateFromError(error) {
1011
+ return { hasError: true, error }
1012
+ }
1013
+ componentDidCatch(error, errorInfo) {
1014
+ console.error('[dsh-moa] MoAErrorBoundary caught error:', error, errorInfo)
1015
+ this.setState({ errorInfo })
1016
+ }
1017
+ render() {
1018
+ if (this.state.hasError) {
1019
+ return React.createElement(
1020
+ 'div',
1021
+ {
1022
+ style: {
1023
+ padding: '16px',
1024
+ borderRadius: '8px',
1025
+ border: '1px solid #ef4444',
1026
+ background: 'rgba(239,68,68,0.08)',
1027
+ color: '#ef4444',
1028
+ fontSize: '13px',
1029
+ fontFamily: 'monospace',
1030
+ whiteSpace: 'pre-wrap',
1031
+ wordBreak: 'break-word',
1032
+ },
1033
+ },
1034
+ React.createElement('div', { style: { fontWeight: 700, marginBottom: 8 } }, '⚠ MoA Settings Error'),
1035
+ React.createElement('div', null, String(this.state.error)),
1036
+ this.state.errorInfo && React.createElement(
1037
+ 'details',
1038
+ { style: { marginTop: 8 } },
1039
+ React.createElement('summary', null, 'Component stack'),
1040
+ React.createElement('pre', { style: { fontSize: 11 } }, this.state.errorInfo.componentStack || '')
1041
+ ),
1042
+ React.createElement(
1043
+ 'button',
1044
+ {
1045
+ type: 'button',
1046
+ style: {
1047
+ marginTop: 12, padding: '6px 16px', border: '1px solid #ef4444',
1048
+ background: 'transparent', color: '#ef4444', borderRadius: 6,
1049
+ cursor: 'pointer', fontSize: 12,
1050
+ },
1051
+ onClick: () => this.setState({ hasError: false, error: null, errorInfo: null }),
1052
+ },
1053
+ 'Retry'
1054
+ )
1055
+ )
1056
+ }
1057
+ return this.props.children
1058
+ }
1059
+ }
1060
+
886
1061
  function MoACard(props) {
887
1062
  ensureStyles()
888
1063
  const state = useMoASettings(props)
@@ -938,140 +1113,39 @@ window.__ModuleLoader__.load({
938
1113
 
939
1114
  exports.inject = ['slots', 'locale', 'inputTriggers']
940
1115
  exports.apply = function apply(ctx) {
941
- const registerLocale = () => {
942
- if (localeRegistered) return
1116
+ rootCtx = ctx
1117
+ const t = makeT(ctx)
1118
+ const addLocale = (locale, dictionary) => {
943
1119
  try {
944
1120
  if (ctx.locale && typeof ctx.locale.register === 'function') {
945
- localeRegistered = true
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
- })
1121
+ return ctx.locale.register(NS, locale, dictionary)
1034
1122
  }
1035
- } catch (err) {
1036
- console.warn('[dsh-moa] locale registration error:', err)
1123
+ } catch {
1124
+ // Если язык уже занят (например, dsh-russian-lang) или зарегистрирован ранее — уступаем без ошибки
1125
+ return () => {}
1037
1126
  }
1127
+ return () => {}
1038
1128
  }
1039
1129
 
1040
1130
  if (typeof ctx.effect === 'function') {
1041
- ctx.effect(() => registerLocale(), 'dsh-moa: dictionaries')
1131
+ ctx.effect(() => {
1132
+ const undo = [addLocale('en', en), addLocale('ru', ru)]
1133
+ return () => {
1134
+ for (const off of undo) {
1135
+ if (typeof off === 'function') {
1136
+ try { off() } catch {}
1137
+ }
1138
+ }
1139
+ }
1140
+ }, 'dsh-moa: dictionaries')
1042
1141
  } else {
1043
- registerLocale()
1142
+ addLocale('en', en)
1143
+ addLocale('ru', ru)
1044
1144
  }
1045
1145
 
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
1146
 
1055
1147
  if (ctx.slots) {
1056
- const registerDirectSection = () => {
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 = () => {
1148
+ const registerCard = () => {
1075
1149
  try {
1076
1150
  ctx.slots.register(
1077
1151
  {
@@ -1079,10 +1153,9 @@ window.__ModuleLoader__.load({
1079
1153
  key: NS,
1080
1154
  order: 35,
1081
1155
  locale: NS,
1082
- label: () => t('title'),
1083
- inject: () => ({ ctx, t }),
1156
+ inject: () => ({ ctx }),
1084
1157
  },
1085
- MoACard
1158
+ (props) => React.createElement(MoAErrorBoundary, null, React.createElement(MoACard, props))
1086
1159
  )
1087
1160
  } catch (err) {
1088
1161
  console.warn('[dsh-moa] Failed to register settings.plugin.item:', err)
@@ -1090,11 +1163,9 @@ window.__ModuleLoader__.load({
1090
1163
  }
1091
1164
 
1092
1165
  if (typeof ctx.slots.inject === 'function') {
1093
- ctx.slots.inject('settings.section', registerDirectSection)
1094
- ctx.slots.inject('settings.plugin.item', registerPluginCard)
1166
+ ctx.slots.inject('settings.plugin.item', registerCard)
1095
1167
  } else {
1096
- registerDirectSection()
1097
- registerPluginCard()
1168
+ registerCard()
1098
1169
  }
1099
1170
  }
1100
1171
 
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 { z } from 'zod'
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().min(0).max(2).default(0.6),
43
- aggregator_temperature: z.number().min(0).max(2).default(0.4),
44
- max_tokens: z.number().int().positive().default(4096),
45
- judge_criteria: z.string().optional().default(''),
46
- judge_mode: z.enum(['auto', 'review']).optional().default('auto'),
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
- let currentConfig = Config.parse(config || {})
122
- let settingsApi = null
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
- settingsApi = sctx.settings.register(NS, Config, { base: currentConfig })
127
- const val = settingsApi.get()
128
- if (val) currentConfig = Config.parse(val)
129
- settingsApi.onChange((next) => {
130
- if (next) currentConfig = Config.parse(next)
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
- const prep = await ctx.llm.prepareCall(provider, model)
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 = getConfig()
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 = getConfig()
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
- // 1. Persist via Cordis Settings API
323
- if (settingsApi && typeof settingsApi.replace === 'function') {
324
- await settingsApi.replace(newConfig)
325
- }
326
- currentConfig = newConfig
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 = getConfig()
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 = getConfig()
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 = getConfig()
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
- * Built-in pricing table for major LLM model families ($ per 1M tokens) (#10).
64
- */
65
- export const MODEL_PRICING_REGISTRY = [
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(provider, model) {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -58,4 +58,4 @@
58
58
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
59
59
  "@deepseek-ai/schemastery": "^3.18.1"
60
60
  }
61
- }
61
+ }