fix(providers): preserve Actual routing during startup and key reload (E-1047)
This commit is contained in:
@@ -845,6 +845,10 @@ def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Dict[str,
|
||||
_routed_client, _ = resolve_provider_client(
|
||||
agent.provider or "auto", model=agent.model, raw_codex=True)
|
||||
if _routed_client is not None:
|
||||
from hermes_cli.providers import is_actual_route, normalize_provider
|
||||
effective_provider = getattr(_routed_client, "_hermes_aux_effective_provider", "")
|
||||
if is_actual_route(effective_provider):
|
||||
agent.provider = normalize_provider(effective_provider)
|
||||
return _client_kwargs_from_routed(_routed_client, _provider_timeout)
|
||||
# No credentials: try the fallback chain BEFORE failing (an exhausted single-entry pool
|
||||
# must not die with a misleading "No LLM provider configured"); only explicitly named
|
||||
@@ -929,6 +933,11 @@ def _init_openai_client(agent, api_key, base_url, fallback_model, _provider_time
|
||||
client_kwargs = _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout)
|
||||
else:
|
||||
client_kwargs = _routed_client_kwargs(agent, fallback_model, _provider_timeout)
|
||||
from hermes_cli.providers import is_actual_route
|
||||
if is_actual_route(agent.provider, client_kwargs.get("base_url", "")):
|
||||
agent.api_mode = "chat_completions"
|
||||
if hasattr(agent, "_transport_cache"):
|
||||
agent._transport_cache.clear()
|
||||
try:
|
||||
from agent.bedrock_adapter import configure_bedrock_openai_client_kwargs
|
||||
configure_bedrock_openai_client_kwargs(client_kwargs, timeout=_provider_timeout)
|
||||
|
||||
@@ -677,7 +677,12 @@ class ClientLifecycleMixin:
|
||||
env_url = get_env_prefer_dotenv(url_var).strip().rstrip("/") if url_var else ""
|
||||
default_base = (pconfig.inference_base_url or "").strip().rstrip("/")
|
||||
base_url = env_url or default_base
|
||||
if self.provider in ("kimi-coding", "zai"):
|
||||
if self.provider == "actual":
|
||||
from hermes_cli.auth import normalize_actual_base_url
|
||||
from hermes_cli.runtime_provider import _config_base_url_for_provider, _get_model_config
|
||||
configured_base = _config_base_url_for_provider(_get_model_config(), "actual")
|
||||
base_url = normalize_actual_base_url(configured_base or base_url)
|
||||
elif self.provider in ("kimi-coding", "zai"):
|
||||
from hermes_cli import auth as _auth
|
||||
resolver = _auth._resolve_kimi_base_url if self.provider == "kimi-coding" else _auth._resolve_zai_base_url
|
||||
base_url = resolver(api_key, pconfig.inference_base_url, env_url).rstrip("/")
|
||||
|
||||
@@ -933,8 +933,9 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
||||
effective_base = current_base or pconfig.inference_base_url
|
||||
|
||||
if provider_id == "actual":
|
||||
from hermes_cli.providers import normalize_provider
|
||||
model_cfg = config.get("model") or {}
|
||||
if isinstance(model_cfg, dict) and model_cfg.get("provider") == provider_id:
|
||||
if isinstance(model_cfg, dict) and normalize_provider(str(model_cfg.get("provider") or "")) == provider_id:
|
||||
effective_base = str(model_cfg.get("base_url") or "").strip() or effective_base
|
||||
|
||||
if provider_id == "zai":
|
||||
|
||||
@@ -232,7 +232,17 @@ def test_actual_background_tasks_reach_chat_completions(
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"entrypoint", ["init", "auto", "switch", "fallback", "restore", "rotation"]
|
||||
"entrypoint",
|
||||
[
|
||||
"init",
|
||||
"auto",
|
||||
"switch",
|
||||
"fallback",
|
||||
"restore",
|
||||
"rotation",
|
||||
"init_fallback",
|
||||
"init_auto",
|
||||
],
|
||||
)
|
||||
def test_actual_runtime_transitions_reach_chat_completions(
|
||||
tmp_path, monkeypatch, actual_endpoint, provider, hosted, entrypoint
|
||||
@@ -266,12 +276,20 @@ def test_actual_runtime_transitions_reach_chat_completions(
|
||||
(tmp_path / "config.yaml").write_text(yaml.safe_dump(config), encoding="utf-8")
|
||||
runtime = resolve_runtime_provider(requested=provider)
|
||||
assert runtime["api_mode"] == "chat_completions"
|
||||
initial_base = base_url.removesuffix("/v1")
|
||||
initial_provider = provider
|
||||
initial_key = "actual-test-key"
|
||||
if entrypoint in {"init_fallback", "init_auto"}:
|
||||
initial_base = initial_key = None
|
||||
initial_provider = (
|
||||
"missing-provider" if entrypoint == "init_fallback" else "auto"
|
||||
)
|
||||
elif entrypoint == "rotation":
|
||||
initial_base = base_url.replace("api.actual.inc", "127.0.0.1")
|
||||
agent = AIAgent(
|
||||
provider=provider,
|
||||
base_url=base_url.replace("api.actual.inc", "127.0.0.1")
|
||||
if entrypoint == "rotation"
|
||||
else base_url.removesuffix("/v1"),
|
||||
api_key="actual-test-key",
|
||||
provider=initial_provider,
|
||||
base_url=initial_base,
|
||||
api_key=initial_key,
|
||||
api_mode=None if entrypoint == "auto" else "codex_responses",
|
||||
model="primary-model" if entrypoint == "fallback" else "gpt-5.4",
|
||||
enabled_toolsets=[],
|
||||
@@ -386,7 +404,10 @@ def test_actual_auxiliary_fallback_reaches_chat_completions(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("override", ["", "http://127.0.0.1:8081", "invalid-url"])
|
||||
def test_actual_setup_keeps_provider_settings_in_yaml(tmp_path, monkeypatch, override):
|
||||
@pytest.mark.parametrize("configured_provider", ["actual", "aci"])
|
||||
def test_actual_setup_keeps_provider_settings_in_yaml(
|
||||
tmp_path, monkeypatch, override, configured_provider
|
||||
):
|
||||
from hermes_cli import config as config_module
|
||||
from hermes_cli import model_setup_flows as setup
|
||||
from hermes_cli.auth import resolve_api_key_provider_credentials
|
||||
@@ -399,7 +420,7 @@ def test_actual_setup_keeps_provider_settings_in_yaml(tmp_path, monkeypatch, ove
|
||||
configured_url = "http://127.0.0.1:8080"
|
||||
raw = {
|
||||
"model": {
|
||||
"provider": "actual",
|
||||
"provider": configured_provider,
|
||||
"default": "old-model",
|
||||
"base_url": configured_url,
|
||||
}
|
||||
@@ -435,3 +456,47 @@ def test_actual_setup_keeps_provider_settings_in_yaml(tmp_path, monkeypatch, ove
|
||||
runtime = resolve_runtime_provider(requested="actual", **kwargs)
|
||||
assert runtime["base_url"] == expected_url + "/v1"
|
||||
assert runtime["api_mode"] == "chat_completions"
|
||||
|
||||
|
||||
def test_actual_key_reload_keeps_yaml_endpoint(tmp_path, monkeypatch, actual_endpoint):
|
||||
from run_agent import AIAgent
|
||||
|
||||
base_url, requests = actual_endpoint
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
monkeypatch.setenv("ACTUAL_API_KEY", "actual-test-key")
|
||||
monkeypatch.setenv("ACTUAL_BASE_URL", "http://127.0.0.1:1")
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
yaml.safe_dump({
|
||||
"model": {"provider": "aci", "default": "test-model", "base_url": base_url},
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_text("ACTUAL_API_KEY=actual-test-key\n", encoding="utf-8")
|
||||
agent = AIAgent(
|
||||
provider="actual",
|
||||
base_url=base_url,
|
||||
api_key="actual-test-key",
|
||||
model="test-model",
|
||||
enabled_toolsets=[],
|
||||
quiet_mode=True,
|
||||
skip_context_files=True,
|
||||
skip_memory=True,
|
||||
save_trajectories=False,
|
||||
)
|
||||
try:
|
||||
assert not agent._try_refresh_env_client_credentials()
|
||||
env_path.write_text("ACTUAL_API_KEY=actual-rotated-key\n", encoding="utf-8")
|
||||
assert agent._try_refresh_env_client_credentials()
|
||||
assert agent.api_key == "actual-rotated-key"
|
||||
assert agent.base_url == base_url + "/v1"
|
||||
assert agent.api_mode == "chat_completions"
|
||||
response = agent._interruptible_api_call(
|
||||
agent._build_api_kwargs([{"role": "user", "content": "Reply briefly."}], [])
|
||||
)
|
||||
assert response.choices[0].message.content == "The task is complete."
|
||||
assert [
|
||||
path for path, body in requests if body.get("model") == "test-model"
|
||||
] == ["/v1/chat/completions"]
|
||||
finally:
|
||||
agent.client.close()
|
||||
|
||||
Reference in New Issue
Block a user