# NousResearch/hermes-agent · Custom provider config corrections across CLI and runtime

## Open now

- Nothing open on GitHub.

These items address bugs in how custom provider configurations are preserved and applied—covering migration from v11 to v12, CLI argument handling, API key dropping, and transport field support.

## 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 #17901. Verify it addresses your case.
- **Recommended:** read PR #17901 (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: `unresolved`. 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

- #14065: Named custom providers under providers: drop inline api_key during runtime resolution

## Prior & competing PRs

- **#17901: fix(runtime_provider): honour transport field on v12+ providers dict**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/runtime_provider.py`, `scripts/release.py`, `tests/hermes_cli/test_user_providers_model_switch.py`
  - fix hunks:
  - `hermes_cli/runtime_provider.py`

    ```diff
    @@ -391,7 +391,14 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
                             "api_key": [redacted:secret-assignment]
                             "model": entry.get("default_model", ""),
                         }
    -                    api_mode = _parse_api_mode(entry.get("api_mode"))
    +                    # The v11→v12 migration writes the API mode under the new
    +                    # ``transport`` field, but hand-edited configs may still
    +                    # use the legacy ``api_mode`` spelling.  Accept both —
    +                    # the runtime normaliser ``_normalize_custom_provider_entry``
    +                    # already does, so without this lift every migrated config
    +                    # silently downgrades codex_responses / anthropic_messages
    +                    # providers to chat_completions in the resolved runtime.
    +                    api_mode = _parse_api_mode(entry.get("api_mode") or entry.get("transport"))
                         if api_mode:
                             result["api_mode"] = api_mode
                         return result
    ```

    ```diff
    @@ -409,7 +416,7 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
                                 "api_key": [redacted:secret-assignment]
                                 "model": entry.get("default_model", ""),
                             }
    -                        api_mode = _parse_api_mode(entry.get("api_mode"))
    +                        api_mode = _parse_api_mode(entry.get("api_mode") or entry.get("transport"))
                             if api_mode:
                                 result["api_mode"] = api_mode
                             return result
    ```
  - `scripts/release.py`

    ```diff
    @@ -117,6 +117,7 @@
         "[redacted:email]": "Abnertheforeman",
         "[redacted:email]": "amanning3390",
         "[redacted:email]": "tochukwuada",
    +    "[redacted:email]": "zicochaos",
         "[redacted:email]": "hharry11",
         "[redacted:email]": "kshitijk4poor",
         "1294707+Tosko4@users.noreply.github.com": "Tosko4",
    ```
  - `tests/hermes_cli/test_user_providers_model_switch.py`

    ```diff
    @@ -748,6 +748,94 @@ def test_switch_model_resolves_user_provider_credentials(monkeypatch, tmp_path):
             is_global=False,
             user_providers=config["providers"],
         )
    -    
    +
         assert result.success is True
         assert result.error_message == ""
    +
    +
    +# =============================================================================
    +# Regression: providers: dict ``transport`` field must be honored
    +# =============================================================================
    +
    +
    +def test_get_named_custom_provider_reads_transport_field(monkeypatch):
    +    """v12+ ``providers:`` dict stores api mode under ``transport:`` (not the
    +    legacy ``api_mode:``).  ``_get_named_custom_provider`` must accept both
    +    field names.
    +
    +    Bug: this function read only ``entry.get("api_mode")`` for v12+ entries.
    +    After ``migrate_config()`` writes ``transport`` on every entry, the
    +    lookup returns None and ``_resolve_named_custom_runtime`` falls back
    +    through ``_detect_api_mode_for_url(base_url) or "chat_completions"``
    +    — silently downgrading every codex_responses / anthropic_messages
    +    provider to chat_completions.
    +    """
    +    config = {
    +        "_config_version": 12,
    +        "providers": {
    +            "my-codex-provider": {
    +                "name": "my-codex-provider",
    +                "api": "http://127.0.0.1:4000/v1",
    +                "api_key": "[redacted:secret-assignment]",
    +                "default_model": "gpt-5",
    +                "transport": "codex_responses",
    +            },
    +        },
    +    }
    +
    +    monkeypatch.setattr(rp, "load_config", lambda: config)
    +
    +    result = rp._get_named_custom_provider("my-codex-provider")
    +    assert result is not None
    +    assert result["api_mode"] == "codex_responses"
    +    assert result["base_url"] == "http://127.0.0.1:4000/v1"
    +    assert result["model"] == "gpt-5"
    +
    +
    +def test_get_named_custom_provider_legacy_api_mode_field_still_works(monkeypatch):
    +    """Hand-edited configs that used ``api_mode:`` (legacy spelling) inside
    +    the v12+ providers: dict shape must keep working — the migration writer
    +    produces ``transport:`` but human-edited configs may carry the older
    +    spelling forward."""
    +    config = {
    +        "_config_version": 12,
    +        "providers": {
    +            "anthropic-proxy": {
    +                "name": "anthropic-proxy",
    +                "api": "http://127.0.0.1:8082",
    +                "api_key": "[redacted:secret-assignment]",
    +                "default_model": "claude-opus-4-7",
    +                "api_mode": "anthropic_messages",  # legacy spelling
    +            },
    +        },
    +    }
    +
    +    monkeypatch.setattr(rp, "load_config", lambda: config)
    +
    +    result = rp._get_named_custom_provider("anthropic-proxy")
    +    assert result is not None
    +    assert result["api_mode"] == "anthropic_messages"
    +
    +
    +def test_get_named_custom_provider_transport_resolves_via_display_name(monkeypatch):
    +    """When the requested name matches the entry's ``name:`` field rather
    +    than its dict key, the same transport-vs-api_mode logic must apply
    +    (second branch in ``_get_named_custom_provider``)."""
    +    config = {
    +        "_config_version": 12,
    +        "providers": {
    +            "slug-different-from-name": {
    +                "name": "Codex Provider",  # display name
    +                "api": "http://127.0.0.1:4000/v1",
    +                "api_key": "[redacted:secret-assignment]",
    +                "default_model": "gpt-5",
    +                "transport": "codex_responses",
    +            },
    +        },
    +    }
    +
    +    monkeypatch.setattr(rp, "load_config", lambda: config)
    +
    +    result = rp._get_named_custom_provider("Codex Provider")
    +    assert result is not None
    +    assert result["api_mode"] == "codex_responses"
    ```
- **#14960: fix(cli): preserve api_mode/key_env/model for providers-dict named resolution**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17901`

## Fix status

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