fix(auxiliary): reuse main_runtime credentials for named custom providers

When the main agent uses a named custom provider (custom:<name>),
resolve_runtime_provider correctly resolves the base_url and api_key.
But the auxiliary client re-resolves from the bare 'custom' provider
name, losing the provider identity.  The bare 'custom' falls back to
OpenRouter, which _resolve_custom_runtime() then rejects — leaving all
auxiliary tasks (title gen, compression, vision, session search, etc.)
with no credentials.

Fix: when resolve_provider_client receives a main_runtime dict
containing concrete base_url + api_key, use it directly instead of
re-resolving.  The main agent already solved provider resolution;
the auxiliary client just needs to reuse its answer.

Closes #45472
This commit is contained in:
Ray 2026-07-04 14:21:33 +08:00 committed by Teknium
parent 3c2f628f5b
commit 92da7a9970
2 changed files with 113 additions and 0 deletions

View file

@ -4357,6 +4357,49 @@ def resolve_provider_client(
else (client, final_model))
# Try custom first, then API-key providers (Codex excluded here:
# falling through to Codex with no model is a stale-constant trap).
#
# 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)
if main_runtime:
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:
final_model = _normalize_resolved_model(
model or main_runtime.get("model") or "gpt-4o-mini",
provider,
)
extra = {}
_clean_base, _dq = _extract_url_query_params(main_base)
if _dq:
extra["default_query"] = _dq
if base_url_host_matches(main_base, "api.kimi.com"):
extra["default_headers"] = {"User-Agent": "claude-code/0.1.0"}
elif base_url_host_matches(main_base, "api.githubcopilot.com"):
from hermes_cli.copilot_auth import copilot_request_headers
extra["default_headers"] = copilot_request_headers(
is_agent_turn=True, is_vision=is_vision
)
elif base_url_host_matches(main_base, "integrate.api.nvidia.com"):
extra["default_headers"] = build_nvidia_nim_headers(main_base)
else:
try:
from providers import get_provider_profile as _gpf_main
_ph_main = _gpf_main(provider)
if _ph_main and _ph_main.default_headers:
extra["default_headers"] = dict(_ph_main.default_headers)
except Exception:
pass
_merged_main = _apply_user_default_headers(extra.get("default_headers"))
if _merged_main:
extra["default_headers"] = _merged_main
client = OpenAI(api_key=main_key, base_url=_clean_base, **extra)
client = _wrap_if_needed(client, final_model, main_base, main_key)
return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode
else (client, final_model))
for try_fn in (_try_custom_endpoint, _resolve_api_key_provider):
client, default = try_fn()
if client is not None:

View file

@ -491,3 +491,73 @@ class TestCustomProviderAliasCollision:
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": "sk-main",
"model": "ignored-model",
}
client, model = resolve_provider_client(
"custom",
model="explicit-model",
explicit_base_url="https://explicit.example.com/v1",
explicit_api_key="sk-explicit",
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"