From cffb9b06e0e00067ccc215c22f383d05734a1a0f Mon Sep 17 00:00:00 2001 From: Hanson Mei Date: Mon, 13 Apr 2026 09:58:13 +0800 Subject: [PATCH 1/3] fix(agent): honor custom provider context in compression feasibility check - resolve config context length for auxiliary compression models - scope top-level model.context_length to the active primary endpoint - add regression coverage for custom-provider overrides and non-primary summary models Fixes #5089 --- agent/model_metadata.py | 87 +++++++++++++++++++ run_agent.py | 37 +++++--- .../run_agent/test_compression_feasibility.py | 74 ++++++++++++++++ 3 files changed, 187 insertions(+), 11 deletions(-) diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 03f70b3fe41a..c6106f1bd063 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -196,6 +196,93 @@ def _normalize_base_url(base_url: str) -> str: return (base_url or "").strip().rstrip("/") +def resolve_config_context_length( + agent_config: Dict[str, Any] | None, + model: str, + base_url: str = "", + *, + primary_model: str | None = None, + primary_base_url: str = "", +) -> int | None: + """Resolve context_length from config.yaml overrides. + + Resolution order: + 1. Top-level ``model.context_length`` when ``model`` matches the configured + primary model + 2. Matching ``custom_providers[].models[model].context_length`` by base URL + """ + if not isinstance(agent_config, dict): + return None + + model_keys = [model] + stripped_model = _strip_provider_prefix(model) + if stripped_model not in model_keys: + model_keys.append(stripped_model) + + primary_model_keys: list[str] = [] + if primary_model: + primary_model_keys.append(primary_model) + stripped_primary_model = _strip_provider_prefix(primary_model) + if stripped_primary_model not in primary_model_keys: + primary_model_keys.append(stripped_primary_model) + + normalized_base_url = _normalize_base_url(base_url) + normalized_primary_base_url = _normalize_base_url(primary_base_url) + + matches_primary_model = bool( + primary_model_keys and any(key in primary_model_keys for key in model_keys) + ) + same_primary_endpoint = matches_primary_model and ( + (normalized_base_url and normalized_primary_base_url and normalized_base_url == normalized_primary_base_url) + or (not normalized_base_url and not normalized_primary_base_url) + ) + + if same_primary_endpoint: + model_cfg = agent_config.get("model", {}) + if isinstance(model_cfg, dict): + raw_ctx = model_cfg.get("context_length") + if raw_ctx is not None: + try: + ctx = int(raw_ctx) + if ctx > 0: + return ctx + except (TypeError, ValueError): + pass + + if not normalized_base_url: + return None + + custom_providers = agent_config.get("custom_providers") + if not isinstance(custom_providers, list): + return None + + for provider_cfg in custom_providers: + if not isinstance(provider_cfg, dict): + continue + provider_base_url = _normalize_base_url(provider_cfg.get("base_url") or "") + if provider_base_url != normalized_base_url: + continue + models_cfg = provider_cfg.get("models", {}) + if not isinstance(models_cfg, dict): + return None + for model_key in model_keys: + model_override = models_cfg.get(model_key, {}) + if not isinstance(model_override, dict): + continue + raw_ctx = model_override.get("context_length") + if raw_ctx is None: + continue + try: + ctx = int(raw_ctx) + if ctx > 0: + return ctx + except (TypeError, ValueError): + pass + return None + + return None + + def _is_openrouter_base_url(base_url: str) -> bool: return "openrouter.ai" in _normalize_base_url(base_url).lower() diff --git a/run_agent.py b/run_agent.py index 37572db5e1f1..b0d54a2f00c5 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1085,6 +1085,7 @@ def __init__( _agent_cfg = _load_agent_config() except Exception: _agent_cfg = {} + self._agent_config = _agent_cfg if isinstance(_agent_cfg, dict) else {} # Persistent memory (MEMORY.md + USER.md) -- loaded from disk self._memory_store = None @@ -1726,7 +1727,10 @@ def _check_compression_model_feasibility(self) -> None: return try: from agent.auxiliary_client import get_text_auxiliary_client - from agent.model_metadata import get_model_context_length + from agent.model_metadata import ( + get_model_context_length, + resolve_config_context_length, + ) client, aux_model = get_text_auxiliary_client( "compression", @@ -1750,23 +1754,34 @@ def _check_compression_model_feasibility(self) -> None: aux_api_key = str(getattr(client, "api_key", "")) # Read user-configured context_length for the compression model. - # Custom endpoints often don't support /models API queries so - # get_model_context_length() falls through to the 128K default, - # ignoring the explicit config value. Pass it as the highest- - # priority hint so the configured value is always respected. - _aux_cfg = (self.config or {}).get("auxiliary", {}).get("compression", {}) - _aux_context_config = _aux_cfg.get("context_length") if isinstance(_aux_cfg, dict) else None - if _aux_context_config is not None: + # This is the most specific override, so it should win over the + # primary-model and custom_provider fallbacks below. + _aux_cfg = ( + getattr(self, "_agent_config", {}) or {} + ).get("auxiliary", {}).get("compression", {}) + aux_config_context_length = ( + _aux_cfg.get("context_length") if isinstance(_aux_cfg, dict) else None + ) + if aux_config_context_length is not None: try: - _aux_context_config = int(_aux_context_config) + aux_config_context_length = int(aux_config_context_length) except (TypeError, ValueError): - _aux_context_config = None + aux_config_context_length = None + + if aux_config_context_length is None: + aux_config_context_length = resolve_config_context_length( + getattr(self, "_agent_config", None), + aux_model, + aux_base_url, + primary_model=getattr(self, "model", None), + primary_base_url=getattr(self, "base_url", ""), + ) aux_context = get_model_context_length( aux_model, base_url=aux_base_url, api_key=aux_api_key, - config_context_length=_aux_context_config, + config_context_length=aux_config_context_length, ) threshold = self.context_compressor.threshold_tokens diff --git a/tests/run_agent/test_compression_feasibility.py b/tests/run_agent/test_compression_feasibility.py index 0756fcda6a90..19fac4ebde40 100644 --- a/tests/run_agent/test_compression_feasibility.py +++ b/tests/run_agent/test_compression_feasibility.py @@ -101,6 +101,80 @@ def test_no_warning_when_aux_context_sufficient(mock_get_client, mock_ctx_len): assert agent._compression_warning is None +@patch("agent.auxiliary_client.get_text_auxiliary_client") +@patch("agent.model_metadata.get_model_context_length") +def test_no_warning_when_aux_context_from_custom_provider_config(mock_ctx_len, mock_get_client): + """Custom-provider context_length should suppress false compression warnings.""" + agent = _make_agent(main_context=258_000, threshold_percent=0.50) + agent._agent_config = { + "model": {"context_length": 1_000_000}, + "custom_providers": [ + { + "name": "Example Gateway", + "base_url": "https://example-gateway.invalid/v1", + "models": { + "gpt-5.4": {"context_length": 258_000}, + }, + } + ] + } + + mock_client = MagicMock() + mock_client.base_url = "https://example-gateway.invalid/v1" + mock_client.api_key = "sk-aux" + mock_get_client.return_value = (mock_client, "gpt-5.4") + + def _context_lookup(model, **kwargs): + if kwargs.get("config_context_length") == 258_000: + return 258_000 + return 128_000 + + mock_ctx_len.side_effect = _context_lookup + + messages = [] + agent._emit_status = lambda msg: messages.append(msg) + + agent._check_compression_model_feasibility() + + assert len(messages) == 0 + assert agent._compression_warning is None + + +@patch("agent.auxiliary_client.get_text_auxiliary_client") +@patch("agent.model_metadata.get_model_context_length") +def test_aux_feasibility_does_not_reuse_main_model_context_override_for_other_summary_models( + mock_ctx_len, + mock_get_client, +): + """Top-level model.context_length must not mask a smaller aux model window.""" + agent = _make_agent(main_context=1_000_000, threshold_percent=0.50) + agent.model = "gpt-5.4" + agent._agent_config = { + "model": {"context_length": 1_000_000}, + } + + mock_client = MagicMock() + mock_client.base_url = "https://example-gateway.invalid/v1" + mock_client.api_key = "sk-aux" + mock_get_client.return_value = (mock_client, "gpt-4.1-mini") + mock_ctx_len.return_value = 128_000 + + messages = [] + agent._emit_status = lambda msg: messages.append(msg) + + agent._check_compression_model_feasibility() + + assert len(messages) == 1 + assert "128,000" in messages[0] + assert "500,000" in messages[0] + mock_ctx_len.assert_called_once_with( + "gpt-4.1-mini", + base_url="https://example-gateway.invalid/v1", + api_key="sk-aux", + config_context_length=None, + ) + + def test_feasibility_check_passes_live_main_runtime(): """Compression feasibility should probe using the live session runtime.""" agent = _make_agent(main_context=200_000, threshold_percent=0.50) From a029cf7fbfe85b496362db17c2a42c16999ebeba Mon Sep 17 00:00:00 2001 From: Hanson Mei Date: Mon, 13 Apr 2026 16:33:53 +0800 Subject: [PATCH 2/3] fix(agent): use runtime config source in compression feasibility check --- run_agent.py | 9 +++-- .../run_agent/test_compression_feasibility.py | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/run_agent.py b/run_agent.py index b0d54a2f00c5..266f14dda0e2 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1756,9 +1756,10 @@ def _check_compression_model_feasibility(self) -> None: # Read user-configured context_length for the compression model. # This is the most specific override, so it should win over the # primary-model and custom_provider fallbacks below. - _aux_cfg = ( - getattr(self, "_agent_config", {}) or {} - ).get("auxiliary", {}).get("compression", {}) + _config_source = getattr(self, "config", None) + if not isinstance(_config_source, dict): + _config_source = getattr(self, "_agent_config", {}) or {} + _aux_cfg = _config_source.get("auxiliary", {}).get("compression", {}) aux_config_context_length = ( _aux_cfg.get("context_length") if isinstance(_aux_cfg, dict) else None ) @@ -1770,7 +1771,7 @@ def _check_compression_model_feasibility(self) -> None: if aux_config_context_length is None: aux_config_context_length = resolve_config_context_length( - getattr(self, "_agent_config", None), + _config_source, aux_model, aux_base_url, primary_model=getattr(self, "model", None), diff --git a/tests/run_agent/test_compression_feasibility.py b/tests/run_agent/test_compression_feasibility.py index 19fac4ebde40..ae4f26a853c9 100644 --- a/tests/run_agent/test_compression_feasibility.py +++ b/tests/run_agent/test_compression_feasibility.py @@ -140,6 +140,45 @@ def _context_lookup(model, **kwargs): assert agent._compression_warning is None +@patch("agent.auxiliary_client.get_text_auxiliary_client") +@patch("agent.model_metadata.get_model_context_length") +def test_custom_provider_context_resolution_uses_runtime_config_source(mock_ctx_len, mock_get_client): + """resolve_config_context_length should use the active runtime config source.""" + agent = _make_agent(main_context=258_000, threshold_percent=0.50) + agent.config = { + "custom_providers": [ + { + "name": "Example Gateway", + "base_url": "https://example-gateway.invalid/v1", + "models": { + "gpt-5.4": {"context_length": 258_000}, + }, + } + ] + } + agent._agent_config = {} + + mock_client = MagicMock() + mock_client.base_url = "https://example-gateway.invalid/v1" + mock_client.api_key = "sk-aux" + mock_get_client.return_value = (mock_client, "gpt-5.4") + + def _context_lookup(model, **kwargs): + if kwargs.get("config_context_length") == 258_000: + return 258_000 + return 128_000 + + mock_ctx_len.side_effect = _context_lookup + + messages = [] + agent._emit_status = lambda msg: messages.append(msg) + + agent._check_compression_model_feasibility() + + assert len(messages) == 0 + assert agent._compression_warning is None + + @patch("agent.auxiliary_client.get_text_auxiliary_client") @patch("agent.model_metadata.get_model_context_length") def test_aux_feasibility_does_not_reuse_main_model_context_override_for_other_summary_models( From fd023f13f714969120e707ed0bc5fde4975dee2d Mon Sep 17 00:00:00 2001 From: Hanson Mei Date: Tue, 14 Apr 2026 10:58:19 +0800 Subject: [PATCH 3/3] fix(agent): merge mainline aux context override into feasibility check --- run_agent.py | 3 + .../run_agent/test_compression_feasibility.py | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/run_agent.py b/run_agent.py index 266f14dda0e2..4ec51f904daa 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1756,6 +1756,9 @@ def _check_compression_model_feasibility(self) -> None: # Read user-configured context_length for the compression model. # This is the most specific override, so it should win over the # primary-model and custom_provider fallbacks below. + # Prefer an explicit auxiliary.compression.context_length override. + # If absent, fall back to the same config source used for the active + # runtime, then resolve per-model overrides from that same source. _config_source = getattr(self, "config", None) if not isinstance(_config_source, dict): _config_source = getattr(self, "_agent_config", {}) or {} diff --git a/tests/run_agent/test_compression_feasibility.py b/tests/run_agent/test_compression_feasibility.py index ae4f26a853c9..793fab583638 100644 --- a/tests/run_agent/test_compression_feasibility.py +++ b/tests/run_agent/test_compression_feasibility.py @@ -214,6 +214,72 @@ def test_aux_feasibility_does_not_reuse_main_model_context_override_for_other_su ) +@patch("agent.auxiliary_client.get_text_auxiliary_client") +@patch("agent.model_metadata.get_model_context_length", return_value=1_000_000) +def test_feasibility_check_prefers_auxiliary_context_override(mock_ctx_len, mock_get_client): + """Explicit auxiliary.compression.context_length should override other guesses.""" + agent = _make_agent(main_context=200_000, threshold_percent=0.85) + agent._agent_config = { + "model": {"context_length": 200_000}, + "auxiliary": { + "compression": { + "context_length": 1_000_000, + }, + }, + "custom_providers": [ + { + "name": "Example Gateway", + "base_url": "http://custom-endpoint:8080/v1", + "models": { + "custom/big-model": {"context_length": 128_000}, + }, + } + ], + } + mock_client = MagicMock() + mock_client.base_url = "http://custom-endpoint:8080/v1" + mock_client.api_key = "sk-custom" + mock_get_client.return_value = (mock_client, "custom/big-model") + + agent._emit_status = lambda msg: None + agent._check_compression_model_feasibility() + + mock_ctx_len.assert_called_once_with( + "custom/big-model", + base_url="http://custom-endpoint:8080/v1", + api_key="sk-custom", + config_context_length=1_000_000, + ) + + +@patch("agent.auxiliary_client.get_text_auxiliary_client") +@patch("agent.model_metadata.get_model_context_length", return_value=128_000) +def test_feasibility_check_ignores_invalid_auxiliary_context_override(mock_ctx_len, mock_get_client): + """Invalid auxiliary.compression.context_length should fall back cleanly.""" + agent = _make_agent(main_context=200_000, threshold_percent=0.50) + agent._agent_config = { + "auxiliary": { + "compression": { + "context_length": "not-a-number", + }, + }, + } + mock_client = MagicMock() + mock_client.base_url = "http://custom:8080/v1" + mock_client.api_key = "sk-test" + mock_get_client.return_value = (mock_client, "custom/model") + + agent._emit_status = lambda msg: None + agent._check_compression_model_feasibility() + + mock_ctx_len.assert_called_once_with( + "custom/model", + base_url="http://custom:8080/v1", + api_key="sk-test", + config_context_length=None, + ) + + def test_feasibility_check_passes_live_main_runtime(): """Compression feasibility should probe using the live session runtime.""" agent = _make_agent(main_context=200_000, threshold_percent=0.50)