fix(auth): a configured custom endpoint counts as a provider in auto resolution (#108383)
`resolve_provider("auto")` only honoured `model.provider` when it named a
registry provider, so `provider: custom` (llama.cpp / vLLM / ollama, or a
loopback `base_url` alone) fell straight through to "No inference provider
configured". The boot inventory in free_tier_bootstrap asks exactly that
question, records provider_configured=False, and every `setup.status` in
`hermes serve` answers from that record — the dashboard's Ink chat then
parked each new session on "Setup Required" while `hermes chat` (which
builds the runtime through resolve_runtime_provider) worked on the same
config. Merged in v0.21.2 (#107697 4bdd64b334); reporters on custom and
on named registry providers were hit by the same record path.
Recognise `custom` / `custom:<name>` / local-server aliases, and a
base_url the bare-custom runtime rung already trusts
(_config_base_url_trustworthy_for_bare_custom), as explicit intent in
_config_model_provider.
Probe (temp HERMES_HOME, provider: custom, base_url 127.0.0.1:8000):
before record.provider_configured=False setup.status.provider_configured=False
after record.provider_configured=True setup.status.provider_configured=True
This commit is contained in:
+20
-3
@@ -1356,16 +1356,33 @@ def _logged_in_oauth_active_provider(*, skip_free_tier: bool = False) -> Optiona
|
||||
|
||||
|
||||
def _config_model_provider() -> Tuple[Any, Optional[str]]:
|
||||
"""``(model_cfg, provider)`` from config.yaml when ``model.provider`` names a registry provider.
|
||||
"""``(model_cfg, provider)`` from config.yaml when ``model.provider`` names a registry provider
|
||||
or a custom OpenAI-compatible endpoint (``custom``, ``custom:<name>``, ``vllm``/``ollama``/...).
|
||||
|
||||
The normal chat/gateway path resolves config.provider upstream in resolve_requested_provider();
|
||||
this is the safety net for the lone direct caller (main.py resolve_provider("auto"))."""
|
||||
this is the safety net for the direct ``resolve_provider("auto")`` callers. A configured custom
|
||||
endpoint is explicit intent like any registry pin: without this rung the boot inventory
|
||||
(``free_tier_bootstrap``) read a llama.cpp/vLLM install as "nothing configured" and the
|
||||
dashboard's Ink chat parked every session on Setup Required while ``hermes chat`` worked
|
||||
(#108383)."""
|
||||
try:
|
||||
from hermes_cli.config import load_config
|
||||
model_cfg = (load_config() or {}).get("model")
|
||||
provider = model_cfg.get("provider") if isinstance(model_cfg, dict) else None
|
||||
provider = provider.strip().lower() if isinstance(provider, str) else ""
|
||||
return model_cfg, (provider if provider in PROVIDER_REGISTRY else None)
|
||||
provider = _plugin_aliases().get(provider, provider)
|
||||
if provider == "custom" or provider.startswith("custom:"):
|
||||
return model_cfg, "custom"
|
||||
if provider in PROVIDER_REGISTRY:
|
||||
return model_cfg, provider
|
||||
# No provider pin but a base_url the bare-custom runtime rung would honour (a loopback
|
||||
# llama.cpp/vLLM/ollama server) — same explicit intent, spelled by URL.
|
||||
base_url = str(model_cfg.get("base_url") or "").strip() if isinstance(model_cfg, dict) else ""
|
||||
if base_url:
|
||||
from hermes_cli.runtime_provider import _config_base_url_trustworthy_for_bare_custom
|
||||
if _config_base_url_trustworthy_for_bare_custom(base_url, provider):
|
||||
return model_cfg, "custom"
|
||||
return model_cfg, None
|
||||
except Exception as e:
|
||||
logger.debug("Could not read config.yaml model.provider for auto-resolution: %s", e)
|
||||
return None, None
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""A configured custom (OpenAI-compatible) endpoint is explicit provider intent.
|
||||
|
||||
Regression for #108383: ``resolve_provider("auto")`` recognised only registry providers from
|
||||
``model.provider``, so the boot inventory (``free_tier_bootstrap``) read a llama.cpp / vLLM /
|
||||
ollama install as "nothing configured" and ``setup.status`` reported ``provider_configured:
|
||||
False`` — the dashboard's Ink chat parked every new session on "Setup Required" while
|
||||
``hermes chat`` (which resolves the runtime directly) worked against the same config.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_home(tmp_path, monkeypatch):
|
||||
home = tmp_path / "hermes"
|
||||
home.mkdir()
|
||||
(home / ".env").write_text("", encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
monkeypatch.delenv("HERMES_GUEST_ONBOARDING", raising=False)
|
||||
for var in ("OPENAI_API_KEY", "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_BASE_URL",
|
||||
"OPENROUTER_BASE_URL", "HERMES_INFERENCE_PROVIDER", "NOUS_API_KEY"):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setattr("agent.bedrock_adapter.has_aws_credentials", lambda: False)
|
||||
from hermes_cli import free_tier_bootstrap as fb
|
||||
fb.reset_for_tests()
|
||||
return home
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_block",
|
||||
[
|
||||
pytest.param(
|
||||
"model:\n default: nvidia/Nemotron\n provider: custom\n"
|
||||
" base_url: http://127.0.0.1:8000/v1\n api_key: dummy\n",
|
||||
id="provider-custom",
|
||||
),
|
||||
pytest.param(
|
||||
"model:\n default: qwen3\n provider: vllm\n base_url: http://127.0.0.1:8000/v1\n",
|
||||
id="local-server-alias",
|
||||
),
|
||||
pytest.param(
|
||||
"model:\n default: qwen3\n base_url: http://localhost:8080/v1\n",
|
||||
id="loopback-base-url-only",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_configured_custom_endpoint_resolves_as_a_provider(isolated_home, model_block):
|
||||
(isolated_home / "config.yaml").write_text(model_block, encoding="utf-8")
|
||||
from hermes_cli.auth import resolve_provider
|
||||
from hermes_cli.free_tier_bootstrap import run_bootstrap
|
||||
|
||||
assert resolve_provider("auto") == "custom"
|
||||
record = run_bootstrap(announce=False)
|
||||
assert record.provider_configured is True
|
||||
assert record.other_providers is True
|
||||
assert record.inference_provider == "custom"
|
||||
|
||||
|
||||
def test_stale_remote_base_url_without_a_custom_pin_is_not_a_provider(isolated_home):
|
||||
"""The URL rung follows the runtime's own trust rule: a non-loopback ``base_url`` left behind
|
||||
under another provider's pin is not custom intent (#14676), so a blank machine still reads
|
||||
as unconfigured."""
|
||||
(isolated_home / "config.yaml").write_text(
|
||||
"model:\n default: some/model\n provider: openrouter\n base_url: https://api.z.ai/v1\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
from hermes_cli.auth import AuthError, resolve_provider
|
||||
from hermes_cli.free_tier_bootstrap import run_bootstrap
|
||||
|
||||
with pytest.raises(AuthError):
|
||||
resolve_provider("auto")
|
||||
assert run_bootstrap(announce=False).provider_configured is False
|
||||
Reference in New Issue
Block a user