fix(models): seat configured external_process providers in the picker payload

list_authenticated_providers' canonical pass keyed credentials on env
vars/auth.json/pool only, and model_ids on models.dev/curated lists —
neither exists for agent-CLI providers, so even picker-admitted
external_process plugin providers produced no row. Credential evidence
now falls back to auth.get_external_process_provider_status
(reachability), and an empty catalog falls back to the self-named model
(provider name → session-default model). Matches the in-tree
copilot-acp precedent, which is hardcoded into the static list.

Tests: payload seats a configured provider with models=[slug] +
authenticated=True; an unresolvable binary stays hidden. 10/10.
This commit is contained in:
tobenwarrior
2026-09-17 14:49:36 +08:00
committed by Teknium
parent 3c5d4b0578
commit df2e6f2d0c
2 changed files with 73 additions and 0 deletions

View File

@@ -937,6 +937,14 @@ def _lap_canonical_rows(b: _PickerBuild) -> None:
continue
has_creds = has_creds or _auth_store_has_provider(cp.slug) or _pool_usable(cp.slug) or (
_is_aws_sdk(cp_config) and _has_aws_sdk_creds_for_listing(cp.slug, b.current_provider))
if not has_creds and cp_config is not None and getattr(cp_config, "auth_type", "") == "external_process":
# Subprocess-backed providers own their auth; structural reachability
# (the binary resolves) is the credential evidence for listing.
try:
from hermes_cli.auth import get_external_process_provider_status
has_creds = bool(get_external_process_provider_status(cp.slug).get("configured"))
except Exception:
has_creds = False
if not has_creds:
continue
if _is_aws_sdk(cp_config):
@@ -944,6 +952,10 @@ def _lap_canonical_rows(b: _PickerBuild) -> None:
else:
model_ids = _live_or_curated_ids(cp.slug, b.curated, merge_models_dev=False,
non_blocking=b.non_blocking_catalogs)
if not model_ids and cp_config is not None and getattr(cp_config, "auth_type", "") == "external_process":
# No models.dev / curated entry exists for agent CLIs; the provider
# name is itself the selectable model (maps to the session default).
model_ids = [cp.slug]
b.add_builtin_row(
cp.slug, cp.label, cp.slug == b.current_provider, model_ids, "canonical", uncapped_ok=False)

View File

@@ -40,6 +40,67 @@ def test_api_key_plugin_providers_unchanged():
assert _plugin_provider_enters_picker(_profile("acme-api", "api_key")) is True
def test_external_process_providers_in_model_options_payload(monkeypatch):
"""Layer 1 admits it into CANONICAL_PROVIDERS; layer 2 (inventory) seats a
row with a self-named model, reachability as the credential."""
import hermes_cli.auth as auth
import hermes_cli.inventory as inv
import hermes_cli.models as models
fake = _make_registry_profile()
monkeypatch.setattr("providers.list_providers", lambda: [fake])
monkeypatch.setattr(auth, "get_external_process_provider_status", lambda slug: {"configured": True})
entry = models.ProviderEntry("ext-proc", "Ext Proc", "external process test provider")
monkeypatch.setattr(models, "CANONICAL_PROVIDERS", list(models.CANONICAL_PROVIDERS) + [entry])
ctx = inv.ConfigContext(
current_provider="zai", current_model="glm-5.3",
current_base_url="https://api.z.ai/api/coding/paas/v4",
user_providers={}, custom_providers=[])
payload = inv.build_model_options_payload(ctx, explicit_only=False)
rows = [p for p in payload["providers"] if p.get("slug") == "ext-proc"]
assert rows, "external_process plugin provider missing from model.options payload"
assert rows[0]["models"] == ["ext-proc"]
assert rows[0]["authenticated"] is True
def test_unresolvable_external_process_binary_not_listed(monkeypatch):
"""Reachability is the credential: a binary that doesn't resolve stays hidden."""
import hermes_cli.auth as auth
import hermes_cli.inventory as inv
import hermes_cli.models as models
fake = _make_registry_profile()
monkeypatch.setattr("providers.list_providers", lambda: [fake])
monkeypatch.setattr(
auth, "get_external_process_provider_status",
lambda slug: {"configured": False})
entry = models.ProviderEntry("ext-proc", "Ext Proc", "external process test provider")
monkeypatch.setattr(models, "CANONICAL_PROVIDERS", list(models.CANONICAL_PROVIDERS) + [entry])
ctx = inv.ConfigContext(
current_provider="zai", current_model="glm-5.3", current_base_url="",
user_providers={}, custom_providers=[])
payload = inv.build_model_options_payload(ctx, explicit_only=False)
assert not [p for p in payload["providers"] if p.get("slug") == "ext-proc"]
def _make_registry_profile():
"""A profile + matching ProviderConfig, registered in both registries."""
from types import SimpleNamespace
profile = SimpleNamespace(
name="ext-proc", auth_type="external_process", display_name="Ext Proc",
description="external process test provider", aliases=(),
process_command="/fake/does-not-exist", process_args=("acp",),
)
import hermes_cli.auth as auth
registry_entry = SimpleNamespace(
name="ext-proc", display_name="Ext Proc", auth_type="external_process",
api_key_env_vars=(), base_url="acp://ext", inference_base_url="acp://ext",
command="ext-proc", args=("acp",), base_url_env_var=None,
)
auth.PROVIDER_REGISTRY.setdefault("ext-proc", registry_entry)
return profile
def test_auto_extend_seats_external_process_provider(monkeypatch):
"""Integration through the real auto-extend loop (reloaded with a stubbed
plugin registry, mirroring discovery output)."""