NousResearch/hermes-agent

Custom provider config corrections across CLI and runtime

Cluster 14065 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #17901 on 30 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #17901. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #17901 fix(runtime_provider): honour transport field on v12+ providers dict

    merged to main

    files: hermes_cli/runtime_provider.py, scripts/release.py, tests/hermes_cli/test_user_providers_model_switch.py

    Show the diff
    • hermes_cli/runtime_provider.py
      @@ -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
      @@ -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
      @@ -117,6 +117,7 @@
           "[redacted:email]": "Abnertheforeman",
           "[redacted:email]": "amanning3390",
           "[redacted:email]": "tochukwuada",
      +    "[redacted:email]": "zicochaos",
           "[redacted:email]": "hharry11",
           "[redacted:email]": "kshitijk4poor",
           "[email protected]": "Tosko4",
    • tests/hermes_cli/test_user_providers_model_switch.py
      @@ -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"

1 PR closed without merging

From the close comments: superseded by another PR 1

  • #14960 fix(cli): preserve api_mode/key_env/model for providers-dict named resolution · superseded by another PR → #17901
Closed issues · 1
  • Closed#14065Named custom providers under providers: drop inline api_key during runtime resolution
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.00 means 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 Lab

At a glance

Subsystem
hermes cli
Merged PRs
#17901
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
30 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.