# NousResearch/hermes-agent · Custom providers model field not propagated from config

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #7935. Verify it addresses your case.
- **Recommended:** read PR #7935 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-7935`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #7828: \[Bug\]: custom_providers model field not preserved, causing "Model Not Exist" errors

## Prior & competing PRs

- **#7935: fix(custom-providers): propagate model field from config so API receives the correct model name**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_cli/config.py`, `hermes_cli/runtime_provider.py`, `tests/hermes_cli/test_runtime_provider_resolution.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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 = [
    ```

    ```diff
    @@ -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)
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```
- **#7916: fix(custom-providers): propagate model field from config so API receives the correct model name**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#7935`

## Fix status

- Merged to main in PR #7935 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-11`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
