Custom providers model field not propagated from config
Cluster 7828. Data as of
Nothing open on GitHub. Fix merged in #7935 on 11 Apr 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #7935. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #7935. That is separate from the fix event. - Still open
- Nothing open on GitHub.
One bug report describes the custom_providers model field not being preserved, leading to "Model Not Exist" errors. Two pull requests with identical titles propose propagating the model field from config so the API receives the correct model name.
Open issues0
Open PRs0
Fixes on record
-
#7935 fix(custom-providers): propagate model field from config so API receives the correct model name
files:
cli.py,hermes_cli/config.py,hermes_cli/runtime_provider.py,tests/hermes_cli/test_runtime_provider_resolution.pyShow the diff
-
cli.py@@ -2710,6 +2710,15 @@ def _ensure_runtime_credentials(self) -> bool: self.api_key = [redacted:secret-assignment] self.base_url = base_url + # When a custom_provider entry carries an explicit `model` field, + # use it as the effective model name. Without this, running + # `hermes chat --model <provider-name>` sends the provider name + # (e.g. "my-provider") as the model string to the API instead of + # the configured model (e.g. "qwen3.6-plus"), causing 400 errors. + runtime_model = runtime.get("model") + if runtime_model and isinstance(runtime_model, str): + self.model = runtime_model + # Normalize model for the resolved provider (e.g. swap non-Codex # models when provider is openai-codex). Fixes #651. model_changed = self._normalize_model_for_provider(resolved_provider) -
hermes_cli/config.py@@ -1497,7 +1497,7 @@ def check_config_version() -> Tuple[int, int]: # Valid fields inside a custom_providers list entry _VALID_CUSTOM_PROVIDER_FIELDS = { - "name", "base_url", "api_key", "api_mode", "models", + "name", "base_url", "api_key", "api_mode", "model", "models", "context_length", "rate_limit_delay", } -
hermes_cli/runtime_provider.py@@ -304,6 +304,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An api_mode = _parse_api_mode(entry.get("api_mode")) if api_mode: result["api_mode"] = api_mode + model_name = str(entry.get("model", "") or "").strip() + if model_name: + result["model"] = model_name return result return None @@ -329,6 +332,11 @@ def _resolve_named_custom_runtime( # Check if a credential pool exists for this custom endpoint pool_result = _try_resolve_from_custom_pool(base_url, "custom", custom_provider.get("api_mode")) if pool_result: + # Propagate the model name even when using pooled credentials — + # the pool doesn't know about the custom_providers model field. + model_name = custom_provider.get("model") + if model_name: + pool_result["model"] = model_name return pool_result api_key_candidates = [ @@ -339,7 +347,7 @@ def _resolve_named_custom_runtime( ] api_key = [redacted:secret-assignment] for candidate in api_key_candidates if has_usable_secret(candidate)), "") - return { + result = { "provider": "custom", "api_mode": custom_provider.get("api_mode") or _detect_api_mode_for_url(base_url) @@ -348,6 +356,11 @@ def _resolve_named_custom_runtime( "api_key": [redacted:secret-assignment] or "no-key-required", "source": f"custom_provider:{custom_provider.get('name', requested_provider)}", } + # Propagate the model name so callers can override self.model when the + # provider name differs from the actual model string the API expects. + if custom_provider.get("model"): + result["model"] = custom_provider["model"] + return result def _resolve_openrouter_runtime( -
tests/hermes_cli/test_runtime_provider_resolution.py@@ -1214,3 +1214,115 @@ def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch): resolved = rp.resolve_runtime_provider(requested="openrouter") assert resolved["provider"] == "openrouter" + + +# ------------------------------------------------------------------ +# fix #7828 — custom_providers model field must propagate to runtime +# ------------------------------------------------------------------ + + +def test_get_named_custom_provider_includes_model(monkeypatch): + """_get_named_custom_provider should include the model field from config.""" + monkeypatch.setattr(rp, "load_config", lambda: { + "custom_providers": [{ + "name": "my-dashscope", + "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "api_key": "[redacted:secret-assignment]", + "api_mode": "chat_completions", + "model": "qwen3.6-plus", + }], + }) + + result = rp._get_named_custom_provider("my-dashscope") + assert result is not None + assert result["model"] == "qwen3.6-plus" + + +def test_get_named_custom_provider_excludes_empty_model(monkeypatch): + """Empty or whitespace-only model field should not appear in result.""" + for model_val in ["", " ", None]: + entry = { + "name": "test-ep", + "base_url": "https://example.com/v1", + "api_key": "key", + } + if model_val is not None: + entry["model"] = model_val + + monkeypatch.setattr(rp, "load_config", lambda e=entry: { + "custom_providers": [e], + }) + + result = rp._get_named_custom_provider("test-ep") + assert result is not None + assert "model" not in result, ( + f"model field {model_val!r} should not be included in result" + ) + + +def test_named_custom_runtime_propagates_model_direct_path(monkeypatch): + """Model should propagate through the direct (non-pool) resolution path.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server") + monkeypatch.setattr( + rp, "_get_named_custom_provider", + lambda p: { + "name": "my-server", + "base_url": "http://localhost:8000/v1", + "api_key": "[redacted:secret-assignment]", + "model": "qwen3.6-plus", + }, + ) + # Ensure pool doesn't intercept + monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None) + + resolved = rp.resolve_runtime_provider(requested="my-server") + assert resolved["model"] == "qwen3.6-plus" + assert resolved["provider"] == "custom" + + +def test_named_custom_runtime_propagates_model_pool_path(monkeypatch): + """Model should propagate even when credential pool handles credentials.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server") + monkeypatch.setattr( + rp, "_get_named_custom_provider", + lambda p: { + "name": "my-server", + "base_url": "http://localhost:8000/v1", + "api_key": "[redacted:secret-assignment]", + "model": "qwen3.6-plus", + }, + ) + # Pool returns a result (intercepting the normal path) + monkeypatch.setattr( + rp, "_try_resolve_from_custom_pool", + lambda *a, **k: { + "provider": "custom", + "api_mode": "chat_completions", + "base_url": "http://localhost:8000/v1", + "api_key": "[redacted:secret-assignment]", + "source": "pool:custom:my-server", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="my-server") + assert resolved["model"] == "qwen3.6-plus", ( + "model must be injected into pool result" + ) + assert resolved["api_key"] == "pool-key", "pool credentials should be used" + + +def test_named_custom_runtime_no_model_when_absent(monkeypatch): + """When custom_providers entry has no model field, runtime should not either.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "my-server") + monkeypatch.setattr( + rp, "_get_named_custom_provider", + lambda p: { + "name": "my-server", + "base_url": "http://localhost:8000/v1", + "api_key": "[redacted:secret-assignment]", + }, + ) + monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None) + + resolved = rp.resolve_runtime_provider(requested="my-server") + assert "model" not in resolved
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#7916 fix(custom-providers): propagate model field from config so API receives the correct model name · merged via another change →
#7935
Closed issues · 1
- Closed#7828[Bug]: custom_providers model field not preserved, causing "Model Not Exist" errors
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #7935
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 11 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.