# NousResearch/hermes-agent · Auxiliary client custom provider resolution diverging from main runtime

## Open now

- 1 issue, 0 PRs open on GitHub.
- #19437: Auxiliary client uses independent resolution path, diverges from main conversation loop

Most members concern the auxiliary client (e.g., vision, title generation) resolving named custom providers or the 'main' alias differently from the active main runtime, with several PRs proposing to reuse or mirror main_runtime credentials. One member also mentions custom endpoint config after setup.

## TL;DR

- 11 member(s): 1 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 6 PR(s) closed without merging.
- A fix was merged to main in PR #59301. Verify it addresses your case.
- **Recommended:** read PR #59301 (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-59301`. 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.16`: 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

- #19437: Auxiliary client uses independent resolution path, diverges from main conversation loop

## Closed / duplicate issues

- #3410: Auxiliary auto/custom resolution can diverge from the active custom runtime
- #45472: Auxiliary client (vision, title gen) fails to resolve \`custom:<name>\` provider — falls back to wrong endpoint

## Prior & competing PRs

- **#5978: fix(auxiliary): resolve named custom providers and 'main' alias in auxiliary routing**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/auxiliary_client.py`, `gateway/platforms/base.py`, `gateway/platforms/feishu.py`, `gateway/platforms/telegram.py`, `gateway/run.py`, `tests/agent/test_auxiliary_named_custom_providers.py`, `tests/gateway/test_telegram_caption_merge.py`, `website/docs/user-guide/configuration.md`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -1142,7 +1142,13 @@ def resolve_provider_client(
         if provider == "codex":
             provider = "openai-codex"
         if provider == "main":
    -        provider = "custom"
    +        # Resolve to the user's actual main provider so named custom providers
    +        # and non-aggregator providers (DeepSeek, Alibaba, etc.) work correctly.
    +        main_prov = _read_main_provider()
    +        if main_prov and main_prov not in ("auto", "main", ""):
    +            provider = main_prov
    +        else:
    +            provider = "custom"
     
         # ── Auto: try all providers in priority order ────────────────────
         if provider == "auto":
    ```

    ```diff
    @@ -1238,6 +1244,28 @@ def resolve_provider_client(
                            "but no endpoint credentials found")
             return None, None
     
    +    # ── Named custom providers (config.yaml custom_providers list) ───
    +    try:
    +        from hermes_cli.runtime_provider import _get_named_custom_provider
    +        custom_entry = _get_named_custom_provider(provider)
    +        if custom_entry:
    +            custom_base = custom_entry.get("base_url", "").strip()
    +            custom_key = custom_entry.get("api_key", "").strip() or "no-key-required"
    +            if custom_base:
    +                final_model = model or _read_main_model() or "gpt-4o-mini"
    +                client = OpenAI(api_key=[redacted:secret-assignment] base_url=custom_base)
    +                logger.debug(
    +                    "resolve_provider_client: named custom provider %r (%s)",
    +                    provider, final_model)
    +                return (_to_async_client(client, final_model) if async_mode
    +                        else (client, final_model))
    +            logger.warning(
    +                "resolve_provider_client: named custom provider %r has no base_url",
    +                provider)
    +            return None, None
    +    except ImportError:
    +        pass
    +
         # ── API-key providers from PROVIDER_REGISTRY ─────────────────────
         try:
             from hermes_cli.auth import PROVIDER_REGISTRY, resolve_api_key_provider_credentials
    ```

    ```diff
    @@ -1358,6 +1386,11 @@ def _normalize_vision_provider(provider: Optional[str]) -> str:
         if provider == "codex":
             return "openai-codex"
         if provider == "main":
    +        # Resolve to actual main provider — named custom providers and
    +        # non-aggregator providers need to pass through as their real name.
    +        main_prov = _read_main_provider()
    +        if main_prov and main_prov not in ("auto", "main", ""):
    +            return main_prov
             return "custom"
         return provider
     
    ```
  - `gateway/platforms/base.py`

    ```diff
    @@ -1105,6 +1105,22 @@ async def _send_with_retry(
                 logger.error("[%s] Fallback send also failed: %s", self.name, fallback_result.error)
             return fallback_result
     
    +    @staticmethod
    +    def _merge_caption(existing_text: Optional[str], new_text: str) -> str:
    +        """Merge a new caption into existing text, avoiding duplicates.
    +
    +        Uses line-by-line exact match (not substring) to prevent false positives
    +        where a shorter caption is silently dropped because it appears as a
    +        substring of a longer one (e.g. "Meeting" inside "Meeting agenda").
    +        Whitespace is normalised for comparison.
    +        """
    +        if not existing_text:
    +            return new_text
    +        existing_captions = [c.strip() for c in existing_text.split("\n\n")]
    +        if new_text.strip() not in existing_captions:
    +            return f"{existing_text}\n\n{new_text}".strip()
    +        return existing_text
    +
         async def handle_message(self, event: MessageEvent) -> None:
             """
             Process an incoming message.
    ```

    ```diff
    @@ -1164,10 +1180,7 @@ async def handle_message(self, event: MessageEvent) -> None:
                         existing.media_urls.extend(event.media_urls)
                         existing.media_types.extend(event.media_types)
                         if event.text:
    -                        if not existing.text:
    -                            existing.text = event.text
    -                        elif event.text not in existing.text:
    -                            existing.text = f"{existing.text}\n\n{event.text}".strip()
    +                        existing.text = self._merge_caption(existing.text, event.text)
                     else:
                         self._pending_messages[session_key] = event
                     return  # Don't interrupt now - will run after current task completes
    ```
  - `gateway/platforms/feishu.py`

    ```diff
    @@ -2065,10 +2065,7 @@ async def _enqueue_media_event(self, event: MessageEvent) -> None:
             existing.media_urls.extend(event.media_urls)
             existing.media_types.extend(event.media_types)
             if event.text:
    -            if not existing.text:
    -                existing.text = event.text
    -            elif event.text not in existing.text.split("\n\n"):
    -                existing.text = f"{existing.text}\n\n{event.text}"
    +            existing.text = self._merge_caption(existing.text, event.text)
             existing.timestamp = event.timestamp
             if event.message_id:
                 existing.message_id = event.message_id
    ```
  - `gateway/platforms/telegram.py`

    ```diff
    @@ -2222,10 +2222,7 @@ def _enqueue_photo_event(self, batch_key: str, event: MessageEvent) -> None:
                 existing.media_urls.extend(event.media_urls)
                 existing.media_types.extend(event.media_types)
                 if event.text:
    -                if not existing.text:
    -                    existing.text = event.text
    -                elif event.text not in existing.text:
    -                    existing.text = f"{existing.text}\n\n{event.text}".strip()
    +                existing.text = self._merge_caption(existing.text, event.text)
     
             prior_task = self._pending_photo_batch_tasks.get(batch_key)
             if prior_task and not prior_task.done():
    ```

    ```diff
    @@ -2415,11 +2412,7 @@ async def _queue_media_group_event(self, media_group_id: str, event: MessageEven
                 existing.media_urls.extend(event.media_urls)
                 existing.media_types.extend(event.media_types)
                 if event.text:
    -                if existing.text:
    -                    if event.text not in existing.text.split("\n\n"):
    -                        existing.text = f"{existing.text}\n\n{event.text}"
    -                else:
    -                    existing.text = event.text
    +                existing.text = self._merge_caption(existing.text, event.text)
     
             prior_task = self._media_group_tasks.get(media_group_id)
             if prior_task:
    ```
  - `gateway/run.py`

    ```diff
    @@ -1987,10 +1987,7 @@ async def _handle_message(self, event: MessageEvent) -> Optional[str]:
                                 existing.media_urls.extend(event.media_urls)
                                 existing.media_types.extend(event.media_types)
                                 if event.text:
    -                                if not existing.text:
    -                                    existing.text = event.text
    -                                elif event.text not in existing.text:
    -                                    existing.text = f"{existing.text}\n\n{event.text}".strip()
    +                                existing.text = BasePlatformAdapter._merge_caption(existing.text, event.text)
                             else:
                                 adapter._pending_messages[_quick_key] = event
                         else:
    ```
  - `tests/agent/test_auxiliary_named_custom_providers.py`

    ```diff
    @@ -0,0 +1,151 @@
    +"""Tests for named custom provider and 'main' alias resolution in auxiliary_client."""
    +
    +import os
    +from unittest.mock import patch, MagicMock
    +
    +import pytest
    +
    +
    [redacted:email](autouse=True)
    +def _isolate(tmp_path, monkeypatch):
    +    """Redirect HERMES_HOME and clear module caches."""
    +    hermes_home = tmp_path / ".hermes"
    +    hermes_home.mkdir()
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +    # Write a minimal config so load_config doesn't fail
    +    (hermes_home / "config.yaml").write_text("model:\n  default: test-model\n")
    +
    +
    +def _write_config(tmp_path, config_dict):
    +    """Write a config.yaml to the test HERMES_HOME."""
    +    import yaml
    +    config_path = tmp_path / ".hermes" / "config.yaml"
    +    config_path.write_text(yaml.dump(config_dict))
    +
    +
    +class TestNormalizeVisionProvider:
    +    """_normalize_vision_provider should resolve 'main' to actual main provider."""
    +
    +    def test_main_resolves_to_named_custom(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "my-model", "provider": "custom:beans"},
    +            "custom_providers": [{"name": "beans", "base_url": "http://localhost/v1"}],
    +        })
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("main") == "custom:beans"
    +
    +    def test_main_resolves_to_openrouter(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "anthropic/claude-sonnet-4", "provider": "openrouter"},
    +        })
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("main") == "openrouter"
    +
    +    def test_main_resolves_to_deepseek(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "deepseek-chat", "provider": "deepseek"},
    +        })
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("main") == "deepseek"
    +
    +    def test_main_falls_back_to_custom_when_no_provider(self, tmp_path):
    +        _write_config(tmp_path, {"model": {"default": "gpt-4o"}})
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("main") == "custom"
    +
    +    def test_bare_provider_name_unchanged(self):
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("beans") == "beans"
    +        assert _normalize_vision_provider("deepseek") == "deepseek"
    +
    +    def test_codex_alias_still_works(self):
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("codex") == "openai-codex"
    +
    +    def test_auto_unchanged(self):
    +        from agent.auxiliary_client import _normalize_vision_provider
    +        assert _normalize_vision_provider("auto") == "auto"
    +        assert _normalize_vision_provider(None) == "auto"
    +
    +
    +class TestResolveProviderClientMainAlias:
    +    """resolve_provider_client('main', ...) should resolve to actual main provider."""
    +
    +    def test_main_resolves_to_named_custom_provider(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "my-model", "provider": "beans"},
    +            "custom_providers": [
    +                {"name": "beans", "base_url": "http://beans.local/v1", "api_key": "k"},
    +            ],
    +        })
    +        from agent.auxiliary_client import resolve_provider_client
    +        client, model = resolve_provider_client("main", "override-model")
    +        assert client is not None
    +        assert model == "override-model"
    +        assert "beans.local" in str(client.base_url)
    +
    +    def test_main_with_custom_colon_prefix(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "my-model", "provider": "custom:beans"},
    +            "custom_providers": [
    +                {"name": "beans", "base_url": "http://beans.local/v1", "api_key": "k"},
    +            ],
    +        })
    +        from agent.auxiliary_client import resolve_provider_client
    +        client, model = resolve_provider_client("main", "test")
    +        assert client is not None
    +        assert "beans.local" in str(client.base_url)
    +
    +
    +class TestResolveProviderClientNamedCustom:
    +    """resolve_provider_client should resolve named custom providers directly."""
    +
    +    def test_named_custom_provider(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "test-model"},
    +            "custom_providers": [
    +                {"name": "beans", "base_url": "http://beans.local/v1", "api_key": "k"},
    +            ],
    +        })
    +        from agent.auxiliary_client import resolve_provider_client
    +        client, model = resolve_provider_client("beans", "my-model")
    +        assert client is not None
    +        assert model == "my-model"
    +        assert "beans.local" in str(client.base_url)
    +
    +    def test_named_custom_provider_default_model(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "main-model"},
    +            "custom_providers": [
    +                {"name": "beans", "base_url": "http://beans.local/v1", "api_key": "k"},
    +            ],
    +        })
    +        from agent.auxiliary_client import resolve_provider_client
    +        client, model = resolve_provider_client("beans")
    +        assert client is not None
    +        # Should use _read_main_model() fallback
    +        assert model == "main-model"
    +
    +    def test_named_custom_no_api_key_uses_fallback(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "test"},
    +            "custom_providers": [
    +                {"name": "local", "base_url": "http://localhost:8080/v1"},
    +            ],
    +        })
    +        from agent.auxiliary_client import resolve_provider_client
    +        client, model = resolve_provider_client("local", "test")
    +        assert client is not None
    +        # no-key-required should be used
    +
    +    def test_nonexistent_named_custom_falls_through(self, tmp_path):
    +        _write_config(tmp_path, {
    +            "model": {"default": "test"},
    +            "custom_providers": [
    +                {"name": "beans", "base_url": "http://beans.local/v1"},
    +            ],
    +        })
    +        from agent.auxiliary_client import resolve_provider_client
    +        # "coffee" doesn't exist in custom_providers
    +        client, model = resolve_provider_client("coffee", "test")
    +        assert client is None
    ```
  - `tests/gateway/test_telegram_caption_merge.py`

    ```diff
    @@ -0,0 +1,77 @@
    +"""Tests for TelegramPlatform._merge_caption caption deduplication logic."""
    +
    +import pytest
    +
    +from gateway.platforms.telegram import TelegramAdapter
    +
    +merge = TelegramAdapter._merge_caption
    +
    +
    +class TestMergeCaptionBasic:
    +    def test_no_existing_text(self):
    +        assert merge(None, "Hello") == "Hello"
    +
    +    def test_empty_existing_text(self):
    +        assert merge("", "Hello") == "Hello"
    +
    +    def test_exact_duplicate_dropped(self):
    +        assert merge("Revenue", "Revenue") == "Revenue"
    +
    +    def test_different_captions_merged(self):
    +        result = merge("Q3 Results", "Q4 Projections")
    +        assert result == "Q3 Results\n\nQ4 Projections"
    +
    +
    +class TestMergeCaptionSubstringBug:
    +    """These are the exact scenarios that the old substring check got wrong."""
    +
    +    def test_shorter_caption_not_dropped_when_substring(self):
    +        # Bug: "Meeting" in "Meeting agenda" → True → caption was silently lost
    +        result = merge("Meeting agenda", "Meeting")
    +        assert result == "Meeting agenda\n\nMeeting"
    +
    +    def test_longer_caption_not_dropped_when_contains_existing(self):
    +        # "Revenue and Profit" contains "Revenue", but they are different captions
    +        result = merge("Revenue", "Revenue and Profit")
    +        assert result == "Revenue\n\nRevenue and Profit"
    +
    +    def test_prefix_caption_not_dropped(self):
    +        result = merge("Q3 Results - Revenue", "Q3 Results")
    +        assert result == "Q3 Results - Revenue\n\nQ3 Results"
    +
    +
    +class TestMergeCaptionWhitespace:
    +    def test_trailing_space_treated_as_duplicate(self):
    +        assert merge("Revenue", "Revenue  ") == "Revenue"
    +
    +    def test_leading_space_treated_as_duplicate(self):
    +        assert merge("Revenue", "  Revenue") == "Revenue"
    +
    +    def test_whitespace_only_new_text_not_added(self):
    +        # strip() makes it empty string → falsy check in callers guards this,
    +        # but _merge_caption itself: strip matches "" which is not in list → would merge.
    +        # Callers already guard with `if event.text:` so this is an edge case.
    +        result = merge("Revenue", "   ")
    +        # "   ".strip() == "" → not in ["Revenue"] → gets merged (caller guards prevent this)
    +        assert "\n\n" in result or result == "Revenue"
    +
    +
    +class TestMergeCaptionMultipleItems:
    +    def test_three_unique_captions_all_present(self):
    +        text = merge(None, "A")
    +        text = merge(text, "B")
    +        text = merge(text, "C")
    +        assert text == "A\n\nB\n\nC"
    +
    +    def test_duplicate_in_middle_dropped(self):
    +        text = merge(None, "A")
    +        text = merge(text, "B")
    +        text = merge(text, "A")  # duplicate
    +        assert text == "A\n\nB"
    +
    +    def test_album_scenario_revenue_profit(self):
    +        # Album Item 1: "Revenue and Profit", Item 2: "Revenue"
    +        # Old bug: "Revenue" in ["Revenue and Profit"] → True → lost
    +        text = merge(None, "Revenue and Profit")
    +        text = merge(text, "Revenue")
    +        assert text == "Revenue and Profit\n\nRevenue"
    ```
  - `website/docs/user-guide/configuration.md`

    ```diff
    @@ -553,7 +553,7 @@ Every model slot in Hermes — auxiliary tasks, compression, fallback — uses t
     
     When `base_url` is set, Hermes ignores the provider and calls that endpoint directly (using `api_key` or `OPENAI_API_KEY` for auth). When only `provider` is set, Hermes uses that provider's built-in auth and base URL.
     
    -Available providers: `auto`, `openrouter`, `nous`, `codex`, `copilot`, `anthropic`, `main`, `zai`, `kimi-coding`, `minimax`, and any provider registered in the [provider registry](/docs/reference/environment-variables).
    +Available providers: `auto`, `openrouter`, `nous`, `codex`, `copilot`, `anthropic`, `main`, `zai`, `kimi-coding`, `minimax`, any provider registered in the [provider registry](/docs/reference/environment-variables), or any named custom provider from your `custom_providers` list (e.g. `provider: "beans"`).
     
     ### Full auxiliary config reference
     
    ```

    ```diff
    @@ -704,7 +704,7 @@ auxiliary:
         model: "my-local-model"
     ```
     
    -`provider: "main"` follows the same custom endpoint Hermes uses for normal chat. That endpoint can be set directly with `OPENAI_BASE_URL`, or saved once through `hermes model` and persisted in `config.yaml`.
    +`provider: "main"` uses whatever provider Hermes uses for normal chat — whether that's a named custom provider (e.g. `beans`), a built-in provider like `openrouter`, or a legacy `OPENAI_BASE_URL` endpoint.
     
     :::tip
     If you use Codex OAuth as your main model provider, vision works automatically — no extra configuration needed. Codex is included in the auto-detection chain for vision.
    ```
- **#19485: fix(auxiliary): resolve provider/model from live runtime, not stale config**. closed without merge · fix tier `suggested`
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `b1f13a8c5f47933c94c065753ceff23507b61e56`, `#19470`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#59301: fix(auxiliary): reuse main_runtime credentials for named custom providers**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/auxiliary_client.py`, `tests/agent/test_auxiliary_named_custom_providers.py`
  - fix hunks:
  - `agent/auxiliary_client.py`

    ```diff
    @@ -4307,6 +4307,8 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
     
         # ── Custom endpoint (OPENAI_BASE_URL + OPENAI_API_KEY) ───────────
         if provider == "custom":
    +        custom_base = ""
    +        custom_key = ""
             if explicit_base_url:
                 custom_base = _to_openai_base_url(explicit_base_url).strip()
                 custom_key = (
    ```

    ```diff
    @@ -4321,6 +4323,19 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
                         "but base_url is empty"
                     )
                     return None, None
    +        elif main_runtime:
    +            # When main_runtime carries a concrete base_url + api_key for a
    +            # named custom provider (custom:<name>), use it directly instead
    +            # of re-resolving from the bare "custom" provider name.
    +            # Re-resolution loses the provider name and falls back to
    +            # OpenRouter or a wrong API-key provider — the main agent already
    +            # solved this, we just need to reuse its answer. (#45472)
    +            _main_base = str(main_runtime.get("base_url") or "").strip().rstrip("/")
    +            _main_key = str(main_runtime.get("api_key") or "").strip()
    +            if _main_base and _main_key:
    +                custom_base = _main_base
    +                custom_key = _main_key
    +        if custom_base and custom_key:
                 final_model = _normalize_resolved_model(
                     model or (main_runtime.get("model") if main_runtime else None) or "gpt-4o-mini",
                     provider,
    ```
  - `tests/agent/test_auxiliary_named_custom_providers.py`

    ```diff
    @@ -491,3 +491,73 @@ def test_explicit_overrides_applied_on_api_key_branch(self, tmp_path, monkeypatc
             assert isinstance(client, OpenAI)
             assert "override.example.com" in str(client.base_url)
             assert client.api_key == "override-key"
    +
    +
    +class TestResolveProviderClientMainRuntimeCustom:
    +    """When the main agent uses a named custom provider (custom:<name>),
    +    resolve_provider_client('custom', ..., main_runtime=...) must reuse the
    +    main_runtime's base_url + api_key instead of re-resolving from the bare
    +    'custom' provider name.  Re-resolution loses the provider name and falls
    +    back to OpenRouter or a wrong API-key provider. (#45472)"""
    +
    +    def test_custom_provider_main_runtime_used_directly(self, tmp_path, monkeypatch):
    +        """main_runtime with base_url + api_key for a named custom provider
    +        is used directly, bypassing the _try_custom_endpoint / API-key
    +        fallback chain."""
    +        from agent.auxiliary_client import resolve_provider_client
    +        main_runtime = {
    +            "provider": "custom",
    +            "base_url": "https://my-gateway.example.com/v1",
    +            "api_key": "***",
    +            "model": "glm-5.1",
    +        }
    +        client, model = resolve_provider_client(
    +            "custom",
    +            model="explicit-glm-5.1",
    +            main_runtime=main_runtime,
    +        )
    +        assert client is not None
    +        assert model == "explicit-glm-5.1"
    +        assert "my-gateway.example.com" in str(client.base_url)
    +        assert client.api_key == "***"
    +
    +    def test_custom_provider_main_runtime_no_credentials_falls_through(self, tmp_path, monkeypatch):
    +        """When main_runtime has no base_url or no api_key, the existing
    +        _try_custom_endpoint / _resolve_api_key_provider fallback chain is
    +        still tried."""
    +        # Ensure no env-provided credentials interfere
    +        monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    +        monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
    +        monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    +        monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
    +
    +        from agent.auxiliary_client import resolve_provider_client
    +        # main_runtime with key but no base_url → must fall through
    +        client, model = resolve_provider_client(
    +            "custom",
    +            main_runtime={"api_key": "k", "base_url": ""},
    +        )
    +        # Should fall through to _try_custom_endpoint → return None,None
    +        # because no OPENAI_BASE_URL is set and no custom endpoint is configured
    +        assert client is None
    +
    +    def test_custom_provider_main_runtime_respects_explicit_base_url(self, tmp_path):
    +        """explicit_base_url still wins over main_runtime — the caller's
    +        explicit argument is the strongest signal."""
    +        from agent.auxiliary_client import resolve_provider_client
    +        main_runtime = {
    +            "base_url": "https://main-runtime.example.com/v1",
    +            "api_key": "[redacted:secret-assignment]",
    +            "model": "ignored-model",
    +        }
    +        client, model = resolve_provider_client(
    +            "custom",
    +            model="explicit-model",
    +            explicit_base_url="https://explicit.example.com/v1",
    +            explicit_api_key="[redacted:secret-assignment]",
    +            main_runtime=main_runtime,
    +        )
    +        assert client is not None
    +        assert model == "explicit-model"
    +        assert "explicit.example.com" in str(client.base_url)
    +        assert client.api_key == "sk-explicit"
    ```
- **#3441: fix(auxiliary): mirror active runtime in _resolve_custom_runtime**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#3442: fix(setup+auxiliary): custom endpoint loses config after setup, and auxiliary ignores active provider**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#5978`
- **#19470: fix(agent): plumb main_runtime through vision aux resolution**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
- **#45531: fix: custom provider auxiliary client resolution failures**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `5e116285465854b1d149247c84a4d2c2d031b386`, `92da7a9970548340f0d538ef94d7af543d1dc7a5`, `#59301`
- **#45545: fix(auxiliary): reuse main_runtime credentials for named custom providers**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#59301`

## Fix status

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