fix(models): memoize the Copilot ACP session probe; source it from the provider profile

`/model <x>` onto copilot-acp validates through `models_validate._static_catalog`, which
reads `provider_model_ids` with no disk cache. After the session probe landed, every such
switch spawned `copilot --acp`, ran the handshake, and killed it (1-3 s; up to the 15 s
probe timeout when the CLI is installed but the session stalls). The GitHub-API tier that
path used before sat behind a 5-minute in-memory memo; the ACP tier now has the same memo,
and it remembers failures too so a broken CLI is not re-spawned per switch.

The probe itself moves to `CopilotACPProfile.fetch_models` — the slot that already said
"model listing is handled by the ACP subprocess" and returned None — so hermes_cli/models.py
no longer hand-builds `CopilotACPClient` kwargs that `profile.create_client` owns.
Discovery failures are logged at debug instead of swallowed.

Tests: the two picker wiring tests collapse into one parametrized contract; a new test
proves three consecutive switch validations pay one probe and a failed probe is not retried
(fails when the memo read is removed).
This commit is contained in:
kshitijk4poor
2026-09-12 11:56:05 +05:30
committed by kshitij
parent 0cd897286a
commit d5ceff958d
4 changed files with 100 additions and 68 deletions
@@ -21,6 +21,26 @@ class CopilotACPProfile(ProviderProfile):
return CopilotACPClient(**client_kwargs)
def fetch_models(
self, *, api_key: str | None = None, base_url: str | None = None, timeout: float = 15.0
) -> list[str] | None:
"""Enabled models advertised by a short-lived signed-in ACP session (``session/new``).
The CLI may keep its login in an OS credential store with no token Hermes can reuse, so
the session is the only source that reflects the account's enablement. ``api_key`` /
``base_url`` are ignored: the subprocess owns auth. None when the CLI is missing, refuses
``--acp``, or the probe fails/times out — callers fall back to their next source.
"""
from hermes_cli.auth import resolve_external_process_provider_credentials
creds = resolve_external_process_provider_credentials(self.name)
if not str(creds.get("base_url") or "").startswith("acp://"):
return None
client = self.create_client(
api_key=creds.get("api_key"), base_url=creds.get("base_url"),
command=creds.get("command"), args=creds.get("args"))
return client.list_models(timeout_seconds=timeout) or None
copilot_acp = CopilotACPProfile(
name="copilot-acp", aliases=("github-copilot-acp", "copilot-acp-agent"),
@@ -28,7 +48,6 @@ copilot_acp = CopilotACPProfile(
env_vars=(), # Managed by ACP subprocess
base_url="acp://copilot", # ACP internal scheme
auth_type="external_process",
supports_model_listing=False, # model listing is handled by the ACP subprocess
# How to launch the CLI; env var names predate this profile (formerly hardcoded in
# hermes_cli/auth.py), so existing setups keep working.
process_command="copilot",