diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 4493eae5f1f8..444ad6525eae 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -1646,6 +1646,34 @@ def get_model_context_length( if config_context_length is not None and isinstance(config_context_length, int) and config_context_length > 0: return config_context_length + # 0a. MoA virtual provider — ``model`` is a preset name, not a real model, + # and ``base_url`` is the local virtual endpoint, so every probe below would + # miss and fall through to the 256K default. The aggregator is the acting + # model, so resolve the context window from the aggregator slot's real + # provider+model instead. References are advisory-only and never bound the + # acting context, so they're ignored here. + if (provider or "").strip().lower() == "moa": + try: + from hermes_cli.config import load_config + from hermes_cli.moa_config import resolve_moa_preset + from hermes_cli.runtime_provider import resolve_runtime_provider + + preset = resolve_moa_preset(load_config().get("moa") or {}, model) + agg = preset.get("aggregator") or {} + agg_provider = str(agg.get("provider") or "").strip() + agg_model = str(agg.get("model") or "").strip() + if agg_model and agg_provider and agg_provider.lower() != "moa": + rt = resolve_runtime_provider(requested=agg_provider, target_model=agg_model) + return get_model_context_length( + agg_model, + base_url=rt.get("base_url", "") or "", + api_key=rt.get("api_key", "") or "", + provider=agg_provider, + ) + except Exception: + logger.debug("MoA aggregator context-length resolution failed", exc_info=True) + # Fall through to the generic default if aggregator resolution failed. + # 0b. custom_providers per-model override — check before any probe. # This closes the gap where /model switch and display paths used to fall # back to 128K despite the user having a per-model context_length set. diff --git a/tests/agent/test_model_metadata.py b/tests/agent/test_model_metadata.py index ecde355d0590..a18d9fb46182 100644 --- a/tests/agent/test_model_metadata.py +++ b/tests/agent/test_model_metadata.py @@ -1524,3 +1524,51 @@ def test_grok_4_not_clobbered(self, tmp_path, monkeypatch): slug, base_url=base, api_key="", provider="xai" ) assert ctx == 256_000, f"{slug} should stay 256000, got {ctx}" + + +class TestMoAContextLength: + """MoA virtual provider resolves context from the aggregator slot, not 256K default.""" + + def _write_moa_config(self, home, aggregator): + import os + os.makedirs(home, exist_ok=True) + with open(os.path.join(home, "config.yaml"), "w") as f: + yaml.safe_dump( + { + "moa": { + "default_preset": "p", + "presets": { + "p": { + "enabled": True, + "reference_models": [ + {"provider": "openrouter", "model": "openai/gpt-5.5"} + ], + "aggregator": aggregator, + } + }, + } + }, + f, + ) + + def test_moa_resolves_from_aggregator(self, tmp_path, monkeypatch): + home = str(tmp_path / ".hermes") + monkeypatch.setenv("HERMES_HOME", home) + self._write_moa_config(home, {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"}) + + # The MoA preset name + virtual base_url would otherwise fall through to + # the 256K default; instead it mirrors the aggregator's real window. + agg_ctx = get_model_context_length( + "anthropic/claude-opus-4.8", base_url="https://openrouter.ai/api/v1", provider="openrouter" + ) + moa_ctx = get_model_context_length("p", base_url="http://127.0.0.1/v1", provider="moa") + assert moa_ctx == agg_ctx + + def test_moa_config_override_still_wins(self, tmp_path, monkeypatch): + home = str(tmp_path / ".hermes") + monkeypatch.setenv("HERMES_HOME", home) + self._write_moa_config(home, {"provider": "openrouter", "model": "anthropic/claude-opus-4.8"}) + ctx = get_model_context_length( + "p", base_url="http://127.0.0.1/v1", provider="moa", config_context_length=500_000 + ) + assert ctx == 500_000