From c8f0f6b136fe5ecc8aeba8f9840d93831efe1d4e Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:15:23 -0700 Subject: [PATCH] fix(moa): name the billed aggregator in the gateway /model block and qualify the advise-once copy Review follow-up for #112359: - gateway `_format_session_info` (the /model info block) printed only `Model: / Provider: moa`, so the messaging surfaces still hid who pays; append the same "Acting model (billed for the run)" line the CLI switch summary prints. - `_provider_mismatch_notice` compared providers case-sensitively and treated the routing pseudo-provider `auto` as a billing seat, printing a misleading "not to OpenAI" / "not to auto" line; lower-case both sides and skip `auto`. - "references only advise once per user turn" is only true for the default `fanout: user_turn`; say "by default" on the inventory row, the `hermes moa list` header, the Desktop pill/description (en/zh/zh-hant) and the docs line. --- apps/desktop/src/i18n/en.ts | 4 ++-- apps/desktop/src/i18n/zh-hant.ts | 4 ++-- apps/desktop/src/i18n/zh.ts | 4 ++-- gateway/run_turn.py | 7 +++++++ hermes_cli/inventory.py | 2 +- hermes_cli/moa_cmd.py | 8 ++++---- tests/gateway/test_session_info.py | 12 ++++++++++++ .../docs/user-guide/features/mixture-of-agents.md | 2 +- 8 files changed, 31 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 407c3e87be..8cd8d08919 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1386,10 +1386,10 @@ export const en: Translations = { moaTitle: 'Mixture of Agents', moaPreset: 'Preset', moaDescription: - 'Configure named presets that appear as models under the Mixture of Agents provider. The aggregator is the acting model — it runs every step of the tool loop, and almost all of the run’s cost is billed to its provider. References only advise once per user turn.', + 'Configure named presets that appear as models under the Mixture of Agents provider. The aggregator is the acting model — it runs every step of the tool loop, and almost all of the run’s cost is billed to its provider. References only advise once per user turn by default.', moaAggregator: 'Aggregator', moaAggregatorBilled: 'acting model · billed for the run', - moaReferenceHint: 'advises once per turn', + moaReferenceHint: 'advises once per turn by default', tasks: { vision: { label: 'Vision', hint: 'Image analysis' }, compression: { label: 'Compression', hint: 'Context compaction' }, diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index a23a185305..c7ab061d7f 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -1220,10 +1220,10 @@ export const zhHant = defineLocale({ moaTitle: '混合代理(Mixture of Agents)', moaPreset: '預設', moaDescription: - '設定以「混合代理」提供者下模型形式出現的命名預設。聚合模型是執行模型——它執行工具迴圈的每一步,整個運行幾乎全部費用都計入其提供者。參考模型每輪使用者訊息僅提供一次建議。', + '設定以「混合代理」提供者下模型形式出現的命名預設。聚合模型是執行模型——它執行工具迴圈的每一步,整個運行幾乎全部費用都計入其提供者。參考模型預設每輪使用者訊息僅提供一次建議。', moaAggregator: '聚合模型', moaAggregatorBilled: '執行模型 · 整個運行在此計費', - moaReferenceHint: '每輪僅建議一次', + moaReferenceHint: '默認每輪僅建議一次', tasks: { vision: { label: '視覺', hint: '圖片分析' }, compression: { label: '壓縮', hint: '上下文壓縮' }, diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 9af56b0a70..1af4a73183 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1581,10 +1581,10 @@ export const zh = defineLocale({ moaTitle: '混合智能体(Mixture of Agents)', moaPreset: '预设', moaDescription: - '配置以「混合智能体」提供商下模型形式出现的命名预设。聚合模型是执行模型——它运行工具循环的每一步,整个运行几乎全部费用都计入其提供商。参考模型每轮用户消息仅提供一次建议。', + '配置以「混合智能体」提供商下模型形式出现的命名预设。聚合模型是执行模型——它运行工具循环的每一步,整个运行几乎全部费用都计入其提供商。参考模型默认每轮用户消息仅提供一次建议。', moaAggregator: '聚合模型', moaAggregatorBilled: '执行模型 · 整个运行在此计费', - moaReferenceHint: '每轮仅建议一次', + moaReferenceHint: '默认每轮仅建议一次', tasks: { vision: { label: '视觉', hint: '图片分析' }, compression: { label: '压缩', hint: '上下文压缩' }, diff --git a/gateway/run_turn.py b/gateway/run_turn.py index 1490ecf756..73d3d07882 100644 --- a/gateway/run_turn.py +++ b/gateway/run_turn.py @@ -2233,6 +2233,13 @@ class GatewayTurnMixin: f"◆ Provider: {resolved.provider or 'openrouter'}", f"◆ Context: {ctx_display} tokens ({ctx_source})", ] + if (resolved.provider or "") == "moa": + # The preset name hides who pays: the aggregator runs every tool-loop step (#112359). + from hermes_cli.config import load_config + from hermes_cli.moa_config import normalize_moa_config + agg = normalize_moa_config(load_config().get("moa"))["presets"].get(resolved.model, {}).get("aggregator") or {} + if agg: + lines.append(f"◆ Acting model (billed for the run): {agg.get('provider')}:{agg.get('model')}") base_url = resolved.base_url if base_url and base_url_hostname(base_url) in ("localhost", "127.0.0.1", "0.0.0.0"): lines.append(f"◆ Endpoint: {base_url}") diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index bd649aca74..d3778cd4a5 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -744,6 +744,6 @@ def _moa_provider_row(current_provider: str = "") -> dict | None: return _row( "moa", "Mixture of Agents", (current_provider or "").lower() == "moa", models=models, total_models=len(models), source="virtual", authenticated=True, auth_type="virtual", - warning="Aggregator is the acting model billed for the run; references only advise once per user turn.") + warning="Aggregator is the acting model billed for the run; references only advise once per user turn by default.") except Exception: return None diff --git a/hermes_cli/moa_cmd.py b/hermes_cli/moa_cmd.py index 3a9d3dd9d3..61ac497ee0 100644 --- a/hermes_cli/moa_cmd.py +++ b/hermes_cli/moa_cmd.py @@ -76,12 +76,12 @@ def _provider_mismatch_notice( if isinstance(cfg, dict): model_section = cfg.get("model") if isinstance(model_section, dict): - main_provider = str(model_section.get("provider") or "").strip() - agg_provider = str((aggregator or {}).get("provider") or "").strip() + main_provider = str(model_section.get("provider") or "").strip().lower() + agg_provider = str((aggregator or {}).get("provider") or "").strip().lower() if ( not main_provider or not agg_provider - or main_provider == "moa" + or main_provider in ("moa", "auto") # "auto" is a routing pseudo-provider, not a billing seat or main_provider == agg_provider ): return None @@ -98,7 +98,7 @@ def _print_config(config: dict[str, Any]) -> None: print(f"Active in config: {cfg.get('active_preset') or '(off)'}") for name, preset in cfg["presets"].items(): print(f"\n{'*' if name == cfg['default_preset'] else ' '} {name}") - print(" Reference models (advise once per user turn):") + print(" Reference models (advise once per user turn by default):") for idx, slot in enumerate(preset["reference_models"], start=1): print(f" {idx}. {_format_slot(slot)}") agg_slot = preset["aggregator"] diff --git a/tests/gateway/test_session_info.py b/tests/gateway/test_session_info.py index 7d16d6b807..f160cf95cb 100644 --- a/tests/gateway/test_session_info.py +++ b/tests/gateway/test_session_info.py @@ -64,6 +64,18 @@ class TestFormatSessionInfo: assert "localhost:11434" in info assert "8K" in info + def test_moa_preset_names_the_billed_aggregator(self, runner, tmp_path): + """#112359: the preset name hides who pays; /model must name the acting aggregator.""" + p1, p2, p3 = _patch_info(tmp_path, "model:\n default: review\n provider: moa\n", + "review", {"provider": "moa", "base_url": "", "api_key": ""}) + moa_cfg = {"moa": {"presets": {"review": { + "reference_models": [{"provider": "openai", "model": "gpt-5.5"}], + "aggregator": {"provider": "nous", "model": "claude-opus-4.8"}, + }}}} + with p1, p2, p3, patch("hermes_cli.config.load_config", return_value=moa_cfg): + info = runner._format_session_info() + assert "Acting model (billed for the run): nous:claude-opus-4.8" in info + def test_named_custom_provider_keeps_context_pin_without_model_base_url( self, runner, tmp_path ): diff --git a/website/docs/user-guide/features/mixture-of-agents.md b/website/docs/user-guide/features/mixture-of-agents.md index 2dd9955c5f..f983918bba 100644 --- a/website/docs/user-guide/features/mixture-of-agents.md +++ b/website/docs/user-guide/features/mixture-of-agents.md @@ -218,7 +218,7 @@ hermes moa configure review # create or update a named preset hermes moa delete review ``` -`hermes moa list` marks the aggregator as the acting model that carries almost all of the cost and lists references as advising once per user turn. When the aggregator's provider differs from your main `model.provider`, both `list` and `configure` add: +`hermes moa list` marks the aggregator as the acting model that carries almost all of the cost and lists references as advising once per user turn (by default). When the aggregator's provider differs from your main `model.provider`, both `list` and `configure` add: ```text Aggregator is on nous; the whole tool loop will be billed there, not to openai-codex.