diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 03f70b3fe41a..6fc79e48b0a3 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -939,6 +939,54 @@ 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. Config-driven overrides from custom_providers[].models[].context_length. + # When provider/base_url is known, scope the lookup to the matching custom + # provider entry to avoid collisions between identically-named models on + # different endpoints. When neither provider nor base_url is known and there + # is only one custom provider defined, allow matching that single entry. + try: + from hermes_cli.config import read_raw_config + from hermes_cli.runtime_provider import _normalize_custom_provider_name + + _cfg = read_raw_config() + _custom_providers = _cfg.get("custom_providers", []) if isinstance(_cfg, dict) else [] + if isinstance(_custom_providers, list): + _normalized_base_url = ( + base_url.strip().rstrip("/") + if isinstance(base_url, str) and base_url.strip() + else None + ) + _active_provider = None + if isinstance(provider, str) and provider.strip(): + _ap = provider.strip() + if _ap.lower().startswith("custom:"): + _ap = _ap.split(":", 1)[1] + _active_provider = _normalize_custom_provider_name(_ap) + _valid_entries = [cp for cp in _custom_providers if isinstance(cp, dict)] + for cp in _valid_entries: + _cp_base = str(cp.get("base_url", "")).strip().rstrip("/") + _cp_name = _normalize_custom_provider_name(str(cp.get("name", ""))) + + if _normalized_base_url is not None and _cp_base: + if _cp_base != _normalized_base_url: + continue + elif _active_provider is not None and _cp_name: + if _cp_name != _active_provider: + continue + elif len(_valid_entries) > 1: + # Ambiguous: skip unscoped entries when multiple providers exist + continue + + _models = cp.get("models", {}) + if isinstance(_models, dict): + _model_entry = _models.get(model, {}) + if isinstance(_model_entry, dict): + _ctx = _model_entry.get("context_length") + if isinstance(_ctx, int) and _ctx > 0: + return _ctx + except Exception: + pass + # Normalise provider-prefixed model names (e.g. "local:model-name" → # "model-name") so cache lookups and server queries use the bare ID that # local servers actually know about. Ollama "model:tag" colons are preserved. diff --git a/tests/agent/test_model_metadata.py b/tests/agent/test_model_metadata.py index df680fb241c5..3505235c3182 100644 --- a/tests/agent/test_model_metadata.py +++ b/tests/agent/test_model_metadata.py @@ -709,3 +709,121 @@ def test_special_chars_in_model_name(self, tmp_path): with patch("agent.model_metadata._get_context_cache_path", return_value=cache_file): save_context_length(model, url, 200000) assert get_cached_context_length(model, url) == 200000 + + +class TestCustomProvidersContextLength: + """Tests for config-driven context_length overrides in custom_providers.""" + + @patch("hermes_cli.config.read_raw_config") + def test_match_by_provider_name(self, mock_raw_config): + mock_raw_config.return_value = { + "custom_providers": [ + { + "name": "zai-customize", + "base_url": "https://open.bigmodel.cn/api/coding/paas/v4", + "models": { + "glm-5.1": {"context_length": 200000}, + }, + } + ] + } + result = get_model_context_length("glm-5.1", provider="zai-customize") + assert result == 200000 + + @patch("hermes_cli.config.read_raw_config") + def test_match_by_base_url(self, mock_raw_config): + mock_raw_config.return_value = { + "custom_providers": [ + { + "name": "zai-customize", + "base_url": "https://open.bigmodel.cn/api/coding/paas/v4", + "models": { + "glm-5.1": {"context_length": 200000}, + }, + } + ] + } + result = get_model_context_length( + "glm-5.1", base_url="https://open.bigmodel.cn/api/coding/paas/v4" + ) + assert result == 200000 + + @patch("hermes_cli.config.read_raw_config") + def test_unmatched_provider_is_ignored(self, mock_raw_config): + """A model name in a different custom provider must not leak across.""" + mock_raw_config.return_value = { + "custom_providers": [ + { + "name": "zai-customize", + "base_url": "https://open.bigmodel.cn/api/coding/paas/v4", + "models": { + "glm-5.1": {"context_length": 200000}, + }, + }, + { + "name": "other-provider", + "base_url": "https://other.example.com/v1", + "models": { + "glm-5.1": {"context_length": 128000}, + }, + }, + ] + } + result = get_model_context_length("glm-5.1", provider="zai-customize") + assert result == 200000 + + @patch("hermes_cli.config.read_raw_config") + def test_single_provider_fallback_when_no_scope(self, mock_raw_config): + """With only one custom provider, allow matching even without provider/base_url.""" + mock_raw_config.return_value = { + "custom_providers": [ + { + "name": "zai-customize", + "models": { + "glm-5.1": {"context_length": 200000}, + }, + } + ] + } + result = get_model_context_length("glm-5.1") + assert result == 200000 + + @patch("agent.model_metadata.fetch_model_metadata") + @patch("hermes_cli.config.read_raw_config") + def test_ambiguous_multiple_providers_skipped_without_scope(self, mock_raw_config, mock_fetch_meta): + """When multiple providers exist and no scope is given, skip to avoid collisions.""" + mock_raw_config.return_value = { + "custom_providers": [ + { + "name": "zai-customize", + "models": {"glm-5.1": {"context_length": 200000}}, + }, + { + "name": "other-provider", + "models": {"glm-5.1": {"context_length": 128000}}, + }, + ] + } + mock_fetch_meta.return_value = {} + # Must NOT pick either custom provider value because scope is ambiguous + result = get_model_context_length("glm-5.1") + assert result not in (200000, 128000) + + @patch("hermes_cli.config.read_raw_config") + def test_match_by_runtime_custom_provider_key(self, mock_raw_config): + """Provider keys like custom: must resolve correctly.""" + mock_raw_config.return_value = { + "custom_providers": [ + { + "name": "local-(127.0.0.1:4141)", + "base_url": "http://127.0.0.1:4141/v1", + "models": { + "llama3.2": {"context_length": 8192}, + }, + } + ] + } + result = get_model_context_length( + "llama3.2", provider="custom:local-(127.0.0.1:4141)" + ) + assert result == 8192