Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
41 changes: 30 additions & 11 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -1750,23 +1754,38 @@ 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.
# 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 {}
_aux_cfg = _config_source.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(
_config_source,
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
Expand Down
179 changes: 179 additions & 0 deletions tests/run_agent/test_compression_feasibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,185 @@ 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_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(
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,
)


@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)
Expand Down