NousResearch/hermes-agent

Keeping Copilot provider identity when endpoints are looked up

Cluster 45276 · agent. Data as of

Nothing open on GitHub. Fix merged in #55605 on 30 Jun 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 #55605. 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 appear to concern the Copilot provider being treated as a custom provider at runtime, with one report mentioning a malformed Authorization header. Two pull requests seem to address keeping the provider identity when a base URL or endpoint is explicitly set or looked up, but the titles alone don't confirm outcomes.

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.

  • #55605 fix(aux): preserve provider identity for resolved endpoints

    merged to main

    files: agent/auxiliary_client.py, tests/agent/test_auxiliary_client.py, tests/run_agent/test_moa_loop_mode.py

    Show the diff
    • agent/auxiliary_client.py
      @@ -5238,9 +5238,10 @@ def _resolve_task_provider_model(
             3. "auto" (full auto-detection chain)
       
           Returns (provider, model, base_url, api_key, api_mode) where model may
      -    be None (use provider default). When base_url is set, provider is forced
      -    to "custom" and the task uses that direct endpoint. api_mode is one of
      -    "chat_completions", "codex_responses", or None (auto-detect).
      +    be None (use provider default). A bare base_url is treated as custom, but
      +    a first-class provider plus base_url keeps the provider identity so its
      +    auth, transport, and request-shaping behavior still apply. api_mode is one
      +    of "chat_completions", "codex_responses", or None (auto-detect).
           """
           cfg_provider = None
           cfg_model = None
      @@ -5273,11 +5274,35 @@ def _expand_direct_api_alias(prov: Optional[str], existing_base: Optional[str])
                   return prov, existing_base
               return "custom", existing_base or target_base
       
      +    def _preserve_provider_with_base_url(prov: Optional[str]) -> bool:
      +        normalized = str(prov or "").strip().lower()
      +        if normalized in {"", "auto", "custom"} or normalized.startswith("custom:"):
      +            return False
      +        try:
      +            from hermes_cli.providers import get_provider
      +
      +            return get_provider(normalized) is not None
      +        except Exception:
      +            # Keep the high-risk provider-backed routes safe even if provider
      +            # catalog loading is unavailable during early import/test paths.
      +            return normalized in {
      +                "anthropic",
      +                "copilot",
      +                "copilot-acp",
      +                "minimax-oauth",
      +                "nous",
      +                "openai-codex",
      +                "qwen-oauth",
      +                "xai-oauth",
      +            }
      +
           if provider:
               provider, base_url = _expand_direct_api_alias(provider, base_url)
           if cfg_provider:
               cfg_provider, cfg_base_url = _expand_direct_api_alias(cfg_provider, cfg_base_url)
       
      +    if base_url and _preserve_provider_with_base_url(provider):
      +        return provider, resolved_model, base_url, api_key, resolved_api_mode
           if base_url:
               return "custom", resolved_model, base_url, api_key, resolved_api_mode
           if provider:
    • tests/agent/test_auxiliary_client.py
      @@ -31,6 +31,7 @@
           _OPENROUTER_MODEL,
           OPENROUTER_BASE_URL,
           _resolve_auto,
      +    _resolve_task_provider_model,
           _resolve_xai_oauth_for_aux,
           _CodexCompletionsAdapter,
       )
      @@ -108,6 +109,65 @@ def test_uses_max_completion_tokens_for_github_copilot_custom_base_path(self):
                   assert auxiliary_max_tokens_param(2048) == {"max_completion_tokens": 2048}
       
       
      +class TestResolveTaskProviderModel:
      +    @pytest.mark.parametrize(
      +        "provider",
      +        [
      +            "anthropic",
      +            "minimax-oauth",
      +            "nous",
      +            "openai-codex",
      +            "qwen-oauth",
      +            "xai-oauth",
      +        ],
      +    )
      +    def test_explicit_base_url_preserves_first_class_provider_identity(self, provider):
      +        resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
      +            task="moa_reference",
      +            provider=provider,
      +            model="test-model",
      +            base_url="https://provider.example/v1",
      +            api_key="[redacted:secret-assignment]",
      +        )
      +
      +        assert resolved_provider == provider
      +        assert model == "test-model"
      +        assert base_url == "https://provider.example/v1"
      +        assert api_key == "resolved-token"
      +        assert api_mode is None
      +
      +    @pytest.mark.parametrize("provider", ["", "auto", "custom", "custom:local", "unknown-provider"])
      +    def test_explicit_base_url_without_first_class_provider_routes_as_custom(self, provider):
      +        resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
      +            task="moa_reference",
      +            provider=provider,
      +            model="test-model",
      +            base_url="https://provider.example/v1",
      +            api_key="[redacted:secret-assignment]",
      +        )
      +
      +        assert resolved_provider == "custom"
      +        assert model == "test-model"
      +        assert base_url == "https://provider.example/v1"
      +        assert api_key == "resolved-token"
      +        assert api_mode is None
      +
      +    def test_direct_openai_alias_with_base_url_still_routes_as_custom(self):
      +        resolved_provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(
      +            task="vision",
      +            provider="openai",
      +            model="gpt-4o-mini",
      +            base_url="https://proxy.example/v1",
      +            api_key="[redacted:secret-assignment]",
      +        )
      +
      +        assert resolved_provider == "custom"
      +        assert model == "gpt-4o-mini"
      +        assert base_url == "https://proxy.example/v1"
      +        assert api_key == "sk-test"
      +        assert api_mode is None
      +
      +
       class TestBuildCallKwargsMaxTokens:
           """_build_call_kwargs should not cap output by default (#34530).
       
    • tests/run_agent/test_moa_loop_mode.py
      @@ -1,6 +1,8 @@
       from types import SimpleNamespace
       from unittest.mock import MagicMock
       
      +import pytest
      +
       from run_agent import AIAgent
       
       
      @@ -198,6 +200,47 @@ def fake_resolve(*, requested, target_model=None):
           assert rt == {"provider": "openai-codex", "model": "gpt-5.5"}
       
       
      [redacted:email]("provider", ["anthropic", "minimax-oauth", "qwen-oauth"])
      +def test_moa_provider_backed_slot_survives_aux_resolution(monkeypatch, provider):
      +    """MoA can pass resolved endpoints for provider-backed slots without
      +    call_llm flattening them to generic custom endpoints.
      +
      +    ``_slot_runtime`` resolves a provider-backed slot to ``provider`` plus a
      +    concrete ``base_url``/``api_key``/``api_mode``; ``_run_reference`` then
      +    forwards that dict to ``call_llm``. ``call_llm`` resolves the routing tuple
      +    via ``_resolve_task_provider_model`` (which takes everything except
      +    ``api_mode``, handled separately). The provider identity must survive that
      +    resolution rather than being flattened to ``custom``.
      +    """
      +    from agent import moa_loop
      +    from agent.auxiliary_client import _resolve_task_provider_model
      +
      +    def fake_resolve(*, requested, target_model=None):
      +        return {
      +            "provider": requested,
      +            "api_mode": "anthropic_messages",
      +            "base_url": f"https://{requested}.example/v1",
      +            "api_key": f"token-for-{requested}",
      +        }
      +
      +    monkeypatch.setattr(
      +        "hermes_cli.runtime_provider.resolve_runtime_provider", fake_resolve
      +    )
      +
      +    rt = moa_loop._slot_runtime({"provider": provider, "model": "test-model"})
      +    # api_mode is forwarded to call_llm directly, not to _resolve_task_provider_model.
      +    resolver_kwargs = {k: v for k, v in rt.items() if k != "api_mode"}
      +    resolved_provider, model, base_url, api_key, _mode = _resolve_task_provider_model(
      +        task="moa_reference",
      +        **resolver_kwargs,
      +    )
      +
      +    assert resolved_provider == provider
      +    assert model == "test-model"
      +    assert base_url == f"https://{provider}.example/v1"
      +    assert api_key == f"token-for-{provider}"
      +
      +
       def test_moa_slot_runtime_falls_back_on_resolution_error(monkeypatch):
           """A slot whose provider can't be resolved still attempts the call with the
           bare provider/model rather than aborting the whole MoA turn."""

1 PR closed without merging

From the close comments: done elsewhere 1

  • #45330 fix(agent): preserve Copilot provider for explicit base URL · done elsewhere → #55605, #45330
Closed issues · 1
  • Closed#45276[Setup]: Copilot provider resolves as custom at runtime and sends malformed Authorization header
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
agent
Merged PRs
#55605
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
30 Jun 2026

For your agent

The same facts, typed.