# NousResearch/hermes-agent · Model name normalization and provider prefix handling

## Open now

- 3 issues, 4 PRs open on GitHub.
- #7922: Gateway /model command sends provider-prefixed slug as raw model name for non-curated models (HTTP 400 Unknown Model)
- #55153: fix: model discovery fails for custom:<name> providers (e.g. custom:fireworks)
- #67106: doctor --fix leaves native-provider model IDs vendor-prefixed
- PR #8059: fix: strip provider prefix from model name in /model command
- PR #67112: fix(doctor): normalize model IDs during config repair
- PR #67169: fix(doctor): strip vendor prefixes from native-provider model IDs on config migration
- PR #67173: fix(doctor): normalize model IDs during config repair

These issues and fixes concern how model names and provider prefixes are parsed, stripped, and forwarded across CLI commands, config repair, and API requests. Recurring themes include vendor/provider prefixes not being stripped, model fields not propagating correctly to API calls, and inconsistencies between model.name, model.default, and model.model config fields.

## TL;DR

- 27 member(s): 3 open issue(s), 5 closed issue(s), 4 open PR(s), 5 merged PR(s), 10 PR(s) closed without merging.
- A fix was merged to main in PR #7187. Verify it addresses your case.
- **Recommended:** read PR #7187 (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: `partially_resolved` · deciding PR(s): `pr-7935,pr-54093,pr-115756`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.06`: 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.

## Open related issues

- #7922: Gateway /model command sends provider-prefixed slug as raw model name for non-curated models (HTTP 400 Unknown Model)
- #55153: fix: model discovery fails for custom:<name> providers (e.g. custom:fireworks)
- #67106: doctor --fix leaves native-provider model IDs vendor-prefixed

## Closed / duplicate issues

- #6211: Z.AI provider prefix not stripped from model name, causing HTTP 400
- #7828: \[Bug\]: custom_providers model field not preserved, causing "Model Not Exist" errors
- #34500: custom_providers: model name not forwarded to API request
- #64787: openai-codex:<model> colon prefix not stripped in normalize_model_for_provider -> HTTP 400
- #68347: custom_providers ignores model id field when selecting from model picker; sends prefixed provider/model string instead

## Prior & competing PRs

- **#7187: fix(model): salvage model/provider resolution PRs #6860 #6844 #6863 #6943**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/auxiliary_client.py`, `agent/error_classifier.py`, `cli.py`, `hermes_cli/main.py`, `hermes_cli/model_normalize.py`, `hermes_cli/model_switch.py`, `run_agent.py`, `tests/agent/test_auxiliary_named_custom_providers.py`, `tests/agent/test_error_classifier.py`, `tests/hermes_cli/test_codex_models.py`, `tests/hermes_cli/test_custom_provider_model_switch.py`, `tests/hermes_cli/test_model_normalize.py`, `tests/hermes_cli/test_opencode_go_in_model_list.py`, `tests/run_agent/test_fallback_model.py`, `tests/run_agent/test_run_agent.py`, `tests/run_agent/test_switch_model_context.py`
- **#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`; implication `superseded`
  - 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": api_key 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": "test-key",
    +            "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": "test-key",
    +            "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": "test-key",
    +            "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": "pool-key",
    +            "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": "test-key",
    +        },
    +    )
    +    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
    ```
- **#54093: fix(config): canonicalize model.name/model.model to model.default (#34500)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `gateway/run.py`, `hermes_cli/config.py`, `infographic/model-name-canon/infographic.png`, `tests/cli/test_cli_init.py`
- **#55162: fix(cli): strip custom: prefix in provider_model_ids for catalog lookups**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `c97d9a4c07b27c8ce94c10e83b916eea43a1e257`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#71144: fix(model_switch): don't send a picker prefix as the custom provider model id**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `contributors/emails/[redacted:email]`, `hermes_cli/model_switch.py`, `tests/hermes_cli/test_model_switch_custom_providers.py`
- **#115756: fix(cli): \`-m openai-codex:<model>\` and other provider:model switches no longer send the prefix to the API (#64787, salvage #64788)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `contributors/emails/[redacted:email]`, `hermes_cli/model_normalize.py`, `tests/hermes_cli/test_model_normalize.py`
- **#6224: fix: strip provider prefix from model name for direct API providers**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#6943: fix(model): normalize native provider-prefixed model ids**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#7187`
- **#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`
- **#8059: fix: strip provider prefix from model name in /model command**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#6211`
  - files: `hermes_cli/model_switch.py`, `hermes_cli/models.py`, `tests/hermes_cli/test_model_switch_custom_providers.py`
- **#34502: fix(cli): treat model.name as alias for model.default**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#54093`
- **#34527: fix: honor model.name for configured providers**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#54093`
- **#64788: fix(codex): strip \`openai-codex:\` colon prefix in model normalization**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `75400793448e`, `#115756`, `#64787`, `#64788`
- **#64916: fix(openai-codex): strip colon provider prefix in normalize_model_for_provider (#64787)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#64788`, `#64788`
- **#67112: fix(doctor): normalize model IDs during config repair**. open
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#67106`
  - files: `hermes_cli/doctor_config.py`, `tests/hermes_cli/test_doctor.py`
- **#67169: fix(doctor): strip vendor prefixes from native-provider model IDs on config migration**. open
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `5b06dbf7626f`, `#67106`, `#67112`, `#67173`, `#67169`, `#67112`, `#67169`, `#67173`, `#67112`, `#67173`, `#67112`, `#67112`, `#67173`, `#67173`, `#67112`, `#67112`, `#67173`, `#67112`, `#67112`, `#67169`, `#67106`
  - files: `hermes_cli/config.py`
- **#67173: fix(doctor): normalize model IDs during config repair**. open
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `34344e08af80`, `#67106`, `#67112`, `#67173`, `#67169`, `#67112`, `#67169`, `#67173`, `#67112`, `#67173`, `#67112`, `#67173`, `#67112`, `#67112`, `#67112`, `#67173`, `#67112`, `#67169`, `#67106`, `#374151`, `#67106`, `#67112`, `#67173`
  - files: `hermes_cli/doctor.py`, `tests/hermes_cli/test_doctor.py`
- **#68436: fix(models): resolve custom provider model ids**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#71144`
- **#71558: fix(cli): centralize matching provider: and provider/ prefix stripping**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `75400793448e`, `#115756`, `#64787`, `#64788`

## Fix status

- Merged to main in PR #7187 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-09-19`.
- Subsystem: `agent`
