{"repo":"NousResearch/hermes-agent","public_id":"16719","display_label":"auxiliary tasks failing to inherit custom provider credentials","summary":"Multiple issues and fixes addressing auxiliary tasks (compression, title generation, etc.) not correctly inheriting base_url, api_key, and api_mode from named custom providers. The cluster centers on credential and endpoint routing failures in auxiliary task execution when a custom provider is configured.","subsystem":"agent","counts":{"members":16,"open_issues":2,"open_issues_distinct":2,"open_prs":1,"closed_issues":4,"merged_prs":2,"attempt_prs":8},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":35259},"resolution":{"state":"partially_resolved","by":"pr-35259"},"freshness":{"state":"verified","last_verified_at":"2026-07-06T02:23:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.10133333333333333},"fix_edges":[{"issue_number":33333,"pr_number":35259,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":34777,"pr_number":35259,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":45472,"pr_number":59301,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":45480,"pr_number":59301,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":16719,"title":"Auxiliary tasks (compression, title generation) ignore custom base_url for named providers","url":null,"state":"closed"},{"kind":"issue","number":17737,"title":"Auxiliary tasks don't inherit provider-level base_url/api_key from config","url":null,"state":"open"},{"kind":"issue","number":22317,"title":"[Bug] Auxiliary auto-detect ignores named custom providers, routes all unconfigured tasks to main provider","url":null,"state":"open"},{"kind":"issue","number":34777,"title":"【Bug】set_runtime_main() missing base_url/api_key causes custom: provider fallback to wrong endpoint","url":null,"state":"closed"},{"kind":"issue","number":36748,"title":"CLI provider override is not applied to auxiliary title generation for named custom providers","url":null,"state":"closed"},{"kind":"issue","number":45480,"title":"auxiliary auto-detect falls through to wrong provider when main provider is named custom (custom_providers/providers dict)","url":null,"state":"closed"},{"kind":"pr","number":16819,"title":"fix(auxiliary): custom provider URL rewrite + main_runtime model for title gen","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"f3371c39a","number":0},{"kind":"pr","value":"#16922","number":16922}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":17760,"title":"fix(auxiliary): inherit provider-level base_url/api_key for auxiliary tasks","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":18337,"title":"fix: resolve 7 identified issues [automated]","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"88bd1c01e","number":0},{"kind":"commit","value":"9caa12f4e","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":22368,"title":"fix(auxiliary): prefer named custom provider default_model over main model (#22317)","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":34783,"title":"fix(auxiliary): pass base_url/api_key/api_mode through set_runtime_main for custom providers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#33347","number":33347},{"kind":"pr","value":"#34777","number":34777},{"kind":"pr","value":"#33333","number":33333},{"kind":"pr","value":"#17846","number":17846}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":35259,"title":"fix(auxiliary): route custom-provider aux tasks to the user endpoint (salvage #34783)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","agent/conversation_loop.py","tests/agent/test_set_runtime_main_custom_provider.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -1680,26 +1680,48 @@ def _read_main_provider() -> str:\n # per turn — no lock needed. Cleared by ``clear_runtime_main()``.\n _RUNTIME_MAIN_PROVIDER: str = \"\"\n _RUNTIME_MAIN_MODEL: str = \"\"\n+_RUNTIME_MAIN_BASE_URL: str = \"\"\n+_RUNTIME_MAIN_API_KEY: str = \"\"\n+_RUNTIME_MAIN_API_MODE: str = \"\"\n \n \n-def set_runtime_main(provider: str, model: str) -> None:\n-    \"\"\"Record the live runtime provider/model for the current AIAgent.\n+def set_runtime_main(\n+    provider: str,\n+    model: str,\n+    *,\n+    base_url: str = \"\",\n+    api_key: str = \"\",\n+    api_mode: str = \"\",\n+) -> None:\n+    \"\"\"Record the live runtime provider/model/credentials for the current AIAgent.\n \n     Called by ``run_agent.AIAgent._sync_runtime_main_for_aux_routing`` (or\n     equivalent setter) at the top of each turn so that\n     ``_read_main_provider`` / ``_read_main_model`` reflect CLI/gateway\n     overrides instead of the stale config.yaml default.\n+\n+    For ``custom:`` providers, ``base_url`` and ``api_key`` must also be\n+    recorded so that ``_resolve_auto`` can construct a valid client in\n+    Step 1 instead of falling through to the aggregator chain.\n     \"\"\"\n     global _RUNTIME_MAIN_PROVIDER, _RUNTIME_MAIN_MODEL\n+    global _RUNTIME_MAIN_BASE_URL, _RUNTIME_MAIN_API_KEY, _RUNTIME_MAIN_API_MODE\n     _RUNTIME_MAIN_PROVIDER = (provider or \"\").strip().lower()\n     _RUNTIME_MAIN_MODEL = (model or \"\").strip()\n+    _RUNTIME_MAIN_BASE_URL = (base_url or \"\").strip()\n+    _RUNTIME_MAIN_API_KEY = [redacted:secret-assignment] if isinstance(api_key, str) else \"\"\n+    _RUNTIME_MAIN_API_MODE = (api_mode or \"\").strip()\n \n \n def clear_runtime_main() -> None:\n     \"\"\"Clear the runtime override (e.g. on session end).\"\"\"\n     global _RUNTIME_MAIN_PROVIDER, _RUNTIME_MAIN_MODEL\n+    global _RUNTIME_MAIN_BASE_URL, _RUNTIME_MAIN_API_KEY, _RUNTIME_MAIN_API_MODE\n     _RUNTIME_MAIN_PROVIDER = \"\"\n     _RUNTIME_MAIN_MODEL = \"\"\n+    _RUNTIME_MAIN_BASE_URL = \"\"\n+    _RUNTIME_MAIN_API_KEY = \"\"\n+    _RUNTIME_MAIN_API_MODE = \"\"\n \n \n def _resolve_custom_runtime() -> Tuple[Optional[str], Optional[str], Optional[str]]:","@@ -2980,6 +3002,18 @@ def _resolve_auto(main_runtime: Optional[Dict[str, Any]] = None) -> Tuple[Option\n     runtime_api_key = [redacted:secret-assignment]\"api_key\", \"\")\n     runtime_api_mode = str(runtime.get(\"api_mode\") or \"\")\n \n+    # Fall back to process-local globals when main_runtime dict was not\n+    # provided or was incomplete.  ``set_runtime_main()`` now records\n+    # base_url/api_key/api_mode alongside provider/model, so custom:\n+    # providers get the full credential surface in Step 1 of the\n+    # auto-detect chain.\n+    if not runtime_base_url and _RUNTIME_MAIN_BASE_URL:\n+        runtime_base_url = _RUNTIME_MAIN_BASE_URL\n+    if not runtime_api_key and _RUNTIME_MAIN_API_KEY:\n+        runtime_api_key = [redacted:secret-assignment]\n+    if not runtime_api_mode and _RUNTIME_MAIN_API_MODE:\n+        runtime_api_mode = _RUNTIME_MAIN_API_MODE\n+\n     # ── Warn once if OPENAI_BASE_URL is set but config.yaml uses a named\n     #    provider (not 'custom').  This catches the common \"env poisoning\"\n     #    scenario where a user switches providers via `hermes model` but the"]},{"file":"agent/conversation_loop.py","hunks":["@@ -392,6 +392,9 @@ def run_conversation(\n         set_runtime_main(\n             getattr(agent, \"provider\", \"\") or \"\",\n             getattr(agent, \"model\", \"\") or \"\",\n+            base_url=getattr(agent, \"base_url\", \"\") or \"\",\n+            api_key=[redacted:secret-assignment] \"api_key\", \"\") or \"\",\n+            api_mode=getattr(agent, \"api_mode\", \"\") or \"\",\n         )\n     except Exception:\n         pass"]},{"file":"tests/agent/test_set_runtime_main_custom_provider.py","hunks":["@@ -0,0 +1,226 @@\n+\"\"\"Regression test: set_runtime_main() must pass base_url/api_key/api_mode\n+so that _resolve_auto() can route custom: providers in Step 1.\n+\n+Fixes https://github.com/NousResearch/hermes-agent/issues/34777\n+\"\"\"\n+import pytest\n+from unittest.mock import patch, MagicMock\n+\n+\n+def _get_globals(mod):\n+    \"\"\"Read runtime globals without triggering redaction.\"\"\"\n+    return {\n+        \"provider\": mod._RUNTIME_MAIN_PROVIDER,\n+        \"model\": mod._RUNTIME_MAIN_MODEL,\n+        \"base_url\": mod._RUNTIME_MAIN_BASE_URL,\n+        \"cred\": mod._RUNTIME_MAIN_API_KEY,  # renamed to avoid redaction\n+        \"api_mode\": mod._RUNTIME_MAIN_API_MODE,\n+    }\n+\n+\n+class TestSetRuntimeMainCustomProvider:\n+    \"\"\"set_runtime_main must propagate base_url/api_key/api_mode for custom providers.\"\"\"\n+\n+    def test_globals_stored(self):\n+        \"\"\"set_runtime_main stores all five fields in process-local globals.\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        mod.clear_runtime_main()\n+        try:\n+            mod.set_runtime_main(\n+                \"custom:my-router\",\n+                \"glm-5.1\",\n+                base_url=\"https://my-server.example.com/v1\",\n+                api_key=\"[redacted:secret-assignment]\",\n+                api_mode=\"chat_completions\",\n+            )\n+            g = _get_globals(mod)\n+            assert g[\"provider\"] == \"custom:my-router\"\n+            assert g[\"model\"] == \"glm-5.1\"\n+            assert g[\"base_url\"] == \"https://my-server.example.com/v1\"\n+            assert g[\"cred\"] == \"sk-test-key\"\n+            assert g[\"api_mode\"] == \"chat_completions\"\n+        finally:\n+            mod.clear_runtime_main()\n+\n+    def test_clear_resets_all_globals(self):\n+        \"\"\"clear_runtime_main resets all five globals to empty.\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        mod.set_runtime_main(\n+            \"custom:x\", \"m\",\n+            base_url=\"https://x.example.com\",\n+            api_key=\"[redacted:secret-assignment]\",\n+            api_mode=\"chat_completions\",\n+        )\n+        mod.clear_runtime_main()\n+        g = _get_globals(mod)\n+        for v in g.values():\n+            assert v == \"\", f\"Expected empty, got {v!r}\"\n+\n+    def test_resolve_auto_uses_globals_for_custom_provider(self):\n+        \"\"\"_resolve_auto reads base_url/api_key from globals when main_runtime is None.\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        mod.clear_runtime_main()\n+        try:\n+            mod.set_runtime_main(\n+                \"custom:test-router\",\n+                \"test-model\",\n+                base_url=\"https://custom-endpoint.example.com/v1\",\n+                api_key=\"[redacted:secret-assignment]\",\n+            )\n+\n+            with patch.object(mod, \"resolve_provider_client\") as mock_resolve:\n+                mock_resolve.return_value = (MagicMock(), \"test-model\")\n+                client, resolved = mod._resolve_auto(main_runtime=None)\n+\n+                mock_resolve.assert_called_once()\n+                call_args = mock_resolve.call_args\n+                assert call_args[0][0] == \"custom\"\n+                assert call_args[1][\"explicit_base_url\"] == \"https://custom-endpoint.example.com/v1\"\n+                assert call_args[1][\"explicit_api_key\"] == \"sk-test-123\"\n+        finally:\n+            mod.clear_runtime_main()\n+\n+    def test_explicit_main_runtime_takes_precedence(self):\n+        \"\"\"When main_runtime dict has values, globals are NOT used.\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        mod.clear_runtime_main()\n+        try:\n+            mod.set_runtime_main(\n+                \"custom:router-a\",\n+                \"model-a\",\n+                base_url=\"https://from-global.example.com\",\n+                api_key=\"[redacted:secret-assignment]\",\n+            )\n+\n+            with patch.object(mod, \"resolve_provider_client\") as mock_resolve:\n+                mock_resolve.return_value = (MagicMock(), \"model-b\")\n+                main_rt = {\n+                    \"provider\": \"custom:router-b\",\n+                    \"model\": \"model-b\",\n+                    \"base_url\": \"https://from-dict.example.com\",\n+                    \"api_key\": \"sk-dict\",\n+                }\n+                mod._resolve_auto(main_runtime=main_rt)\n+\n+                call_args = mock_resolve.call_args[1]\n+                assert call_args[\"explicit_base_url\"] == \"https://from-dict.example.com\"\n+                assert call_args[\"explicit_api_key\"] == \"sk-dict\"\n+        finally:\n+            mod.clear_runtime_main()\n+\n+    def test_backward_compatible_defaults(self):\n+        \"\"\"Calling set_runtime_main with only positional args still works.\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        mod.clear_runtime_main()\n+        try:\n+            mod.set_runtime_main(\"openrouter\", \"gpt-4o\")\n+            g = _get_globals(mod)\n+            assert g[\"provider\"] == \"openrouter\"\n+            assert g[\"model\"] == \"gpt-4o\"\n+            assert g[\"base_url\"] == \"\"\n+            assert g[\"cred\"] == \"\"\n+            assert g[\"api_mode\"] == \"\"\n+        finally:\n+            mod.clear_runtime_main()\n+\n+\n+class TestResolveAutoCustomEndToEnd:\n+    \"\"\"End-to-end routing assertions — build a *real* client (no mock on\n+    resolve_provider_client) and verify the auxiliary auto-detect chain lands\n+    on the user's custom endpoint instead of falling through to the aggregator\n+    chain.  These guard the actual user-visible symptom in #34777 (aux tasks\n+    silently routed to a fallback provider) rather than just the wiring.\n+    \"\"\"\n+\n+    @staticmethod\n+    def _client_base_url(client):\n+        for chain in ((\"base_url\",), (\"_client\", \"base_url\")):\n+            obj = client\n+            try:\n+                for attr in chain:\n+                    obj = getattr(obj, attr)\n+                return str(obj)\n+            except AttributeError:\n+                continue\n+        return None\n+\n+    def test_config_less_custom_endpoint_routes_via_global(self, tmp_path, monkeypatch):\n+        \"\"\"custom:<name> with NO config entry: the live base_url carried by\n+        set_runtime_main() must build a real client at that endpoint — not\n+        fall through to Step 2 (the regression in #34777).\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        # Hermetic: no aggregator creds, no stale OPENAI_BASE_URL.\n+        for var in (\"OPENROUTER_API_KEY\", \"NOUS_API_KEY\", \"OPENAI_API_KEY\",\n+                    \"OPENAI_BASE_URL\"):\n+            monkeypatch.delenv(var, raising=False)\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"config.yaml\").write_text(\n+            \"model:\\n\"\n+            \"  default: glm-5.1\\n\"\n+            \"  provider: 'custom:ephemeral'\\n\"\n+            \"  base_url: ''\\n\"\n+        )\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+\n+        mod.clear_runtime_main()\n+        try:\n+            mod.set_runtime_main(\n+                \"custom:ephemeral\",\n+                \"glm-5.1\",\n+                base_url=\"https://ephemeral.live/v1\",\n+                api_key=\"[redacted:secret-assignment]\",\n+            )\n+            client, resolved = mod.resolve_provider_client(\"auto\", None)\n+            assert client is not None, (\n+                \"config-less custom endpoint fell through to Step 2 — \"\n+                \"the #34777 bug is back\"\n+            )\n+            assert resolved == \"glm-5.1\"\n+            base = self._client_base_url(client)\n+            assert base and base.rstrip(\"/\") == \"https://ephemeral.live/v1\"\n+        finally:\n+            mod.clear_runtime_main()\n+\n+    def test_named_custom_with_config_entry_still_routes(self, tmp_path, monkeypatch):\n+        \"\"\"Regression guard: custom:<name> WITH a custom_providers entry must\n+        still resolve to that entry's endpoint.  An earlier competing fix\n+        collapsed the provider to bare ``custom`` before resolution, which\n+        broke the named-custom branch and returned None here.\"\"\"\n+        import agent.auxiliary_client as mod\n+\n+        for var in (\"OPENROUTER_API_KEY\", \"NOUS_API_KEY\", \"OPENAI_API_KEY\",\n+                    \"OPENAI_BASE_URL\"):\n+            monkeypatch.delenv(var, raising=False)\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir()\n+        (hermes_home / \"config.yaml\").write_text(\n+            \"model:\\n\"\n+            \"  default: glm-5.1\\n\"\n+            \"  provider: 'custom:openclaw'\\n\"\n+            \"  base_url: ''\\n\"\n+            \"custom_providers:\\n\"\n+            \"  - name: openclaw\\n\"\n+            \"    base_url: 'https://withcfg.example/v1'\\n\"\n+            \"    model: glm-5.1\\n\"\n+            \"    api_key: [redacted:secret-assignment]\"\n+        )\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+\n+        # No live base_url carried — resolution must come from config alone,\n+        # via the named-custom branch in resolve_provider_client.\n+        mod.clear_runtime_main()\n+        try:\n+            mod.set_runtime_main(\"custom:openclaw\", \"glm-5.1\")\n+            client, resolved = mod.resolve_provider_client(\"auto\", None)\n+            assert client is not None\n+            base = self._client_base_url(client)\n+            assert base and base.rstrip(\"/\") == \"https://withcfg.example/v1\"\n+        finally:\n+            mod.clear_runtime_main()\n"]}]},{"kind":"pr","number":36788,"title":"fix(cli): propagate --provider override to auxiliary title generation (#36748)","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":45531,"title":"fix: custom provider auxiliary client resolution failures","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"5e116285465854b1d149247c84a4d2c2d031b386","number":0},{"kind":"commit","value":"92da7a9970548340f0d538ef94d7af543d1dc7a5","number":0},{"kind":"pr","value":"#59301","number":59301}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":59301,"title":"fix(auxiliary): reuse main_runtime credentials for named custom providers","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","tests/agent/test_auxiliary_named_custom_providers.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -4307,6 +4307,8 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n \n     # ── Custom endpoint (OPENAI_BASE_URL + OPENAI_API_KEY) ───────────\n     if provider == \"custom\":\n+        custom_base = \"\"\n+        custom_key = \"\"\n         if explicit_base_url:\n             custom_base = _to_openai_base_url(explicit_base_url).strip()\n             custom_key = (","@@ -4321,6 +4323,19 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = \"\",\n                     \"but base_url is empty\"\n                 )\n                 return None, None\n+        elif main_runtime:\n+            # When main_runtime carries a concrete base_url + api_key for a\n+            # named custom provider (custom:<name>), use it directly instead\n+            # of re-resolving from the bare \"custom\" provider name.\n+            # Re-resolution loses the provider name and falls back to\n+            # OpenRouter or a wrong API-key provider — the main agent already\n+            # solved this, we just need to reuse its answer. (#45472)\n+            _main_base = str(main_runtime.get(\"base_url\") or \"\").strip().rstrip(\"/\")\n+            _main_key = str(main_runtime.get(\"api_key\") or \"\").strip()\n+            if _main_base and _main_key:\n+                custom_base = _main_base\n+                custom_key = _main_key\n+        if custom_base and custom_key:\n             final_model = _normalize_resolved_model(\n                 model or (main_runtime.get(\"model\") if main_runtime else None) or \"gpt-4o-mini\",\n                 provider,"]},{"file":"tests/agent/test_auxiliary_named_custom_providers.py","hunks":["@@ -491,3 +491,73 @@ def test_explicit_overrides_applied_on_api_key_branch(self, tmp_path, monkeypatc\n         assert isinstance(client, OpenAI)\n         assert \"override.example.com\" in str(client.base_url)\n         assert client.api_key == \"override-key\"\n+\n+\n+class TestResolveProviderClientMainRuntimeCustom:\n+    \"\"\"When the main agent uses a named custom provider (custom:<name>),\n+    resolve_provider_client('custom', ..., main_runtime=...) must reuse the\n+    main_runtime's base_url + api_key instead of re-resolving from the bare\n+    'custom' provider name.  Re-resolution loses the provider name and falls\n+    back to OpenRouter or a wrong API-key provider. (#45472)\"\"\"\n+\n+    def test_custom_provider_main_runtime_used_directly(self, tmp_path, monkeypatch):\n+        \"\"\"main_runtime with base_url + api_key for a named custom provider\n+        is used directly, bypassing the _try_custom_endpoint / API-key\n+        fallback chain.\"\"\"\n+        from agent.auxiliary_client import resolve_provider_client\n+        main_runtime = {\n+            \"provider\": \"custom\",\n+            \"base_url\": \"https://my-gateway.example.com/v1\",\n+            \"api_key\": \"***\",\n+            \"model\": \"glm-5.1\",\n+        }\n+        client, model = resolve_provider_client(\n+            \"custom\",\n+            model=\"explicit-glm-5.1\",\n+            main_runtime=main_runtime,\n+        )\n+        assert client is not None\n+        assert model == \"explicit-glm-5.1\"\n+        assert \"my-gateway.example.com\" in str(client.base_url)\n+        assert client.api_key == \"***\"\n+\n+    def test_custom_provider_main_runtime_no_credentials_falls_through(self, tmp_path, monkeypatch):\n+        \"\"\"When main_runtime has no base_url or no api_key, the existing\n+        _try_custom_endpoint / _resolve_api_key_provider fallback chain is\n+        still tried.\"\"\"\n+        # Ensure no env-provided credentials interfere\n+        monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n+        monkeypatch.delenv(\"OPENAI_BASE_URL\", raising=False)\n+        monkeypatch.delenv(\"OPENROUTER_API_KEY\", raising=False)\n+        monkeypatch.delenv(\"ANTHROPIC_API_KEY\", raising=False)\n+\n+        from agent.auxiliary_client import resolve_provider_client\n+        # main_runtime with key but no base_url → must fall through\n+        client, model = resolve_provider_client(\n+            \"custom\",\n+            main_runtime={\"api_key\": \"k\", \"base_url\": \"\"},\n+        )\n+        # Should fall through to _try_custom_endpoint → return None,None\n+        # because no OPENAI_BASE_URL is set and no custom endpoint is configured\n+        assert client is None\n+\n+    def test_custom_provider_main_runtime_respects_explicit_base_url(self, tmp_path):\n+        \"\"\"explicit_base_url still wins over main_runtime — the caller's\n+        explicit argument is the strongest signal.\"\"\"\n+        from agent.auxiliary_client import resolve_provider_client\n+        main_runtime = {\n+            \"base_url\": \"https://main-runtime.example.com/v1\",\n+            \"api_key\": \"sk-main\",\n+            \"model\": \"ignored-model\",\n+        }\n+        client, model = resolve_provider_client(\n+            \"custom\",\n+            model=\"explicit-model\",\n+            explicit_base_url=\"https://explicit.example.com/v1\",\n+            explicit_api_key=\"[redacted:secret-assignment]\",\n+            main_runtime=main_runtime,\n+        )\n+        assert client is not None\n+        assert model == \"explicit-model\"\n+        assert \"explicit.example.com\" in str(client.base_url)\n+        assert client.api_key == \"sk-explicit\"\n"]}]},{"kind":"pr","number":109442,"title":"fix(cli,model_switch): preserve named custom provider identity and base_url for URL-bearing aliases","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}