{"repo":"NousResearch/hermes-agent","public_id":"45480","display_label":"Auxiliary client resolution for named custom providers","summary":"All three members concern how the auxiliary client resolves or selects a provider when the main provider is a named custom one (e.g. defined in custom_providers/providers). One describes auto-detect landing on the wrong provider, and the other two appear to propose fixes involving resolution failures and reusing main runtime credentials; their outcomes are not stated in the titles.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":59301,"merged_at":"2026-07-06T02:23:27+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-06T02:23:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":45472,"pr_number":59301,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-06T02:23:27+00:00"},{"issue_number":45480,"pr_number":59301,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-07-06T02:23:27+00:00"}],"members":[{"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":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"},"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\": \"[redacted:secret-assignment]\",\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"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}