mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
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:
parent
3c2f628f5b
commit
92da7a9970
2 changed files with 113 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue