release: v0.50.151 — credential_pool provider detection + Ollama Cloud support (PR #820 by @starship-s)

Surfaces providers added via credential_pool in the model dropdown. Ambient gh-cli tokens suppressed. _apply_provider_prefix helper extracted. Ollama Cloud display name + dynamic model list. looksLikeBareOllamaId heuristic tightened. Test isolation fixed.

PR #820 by @starship-s.
This commit is contained in:
nesquena-hermes
2026-04-22 13:18:02 -07:00
committed by GitHub
parent d8e1f37e2b
commit 5fa731ea4a
7 changed files with 879 additions and 32 deletions

View File

@@ -504,6 +504,7 @@ _PROVIDER_DISPLAY = {
"huggingface": "HuggingFace",
"alibaba": "Alibaba",
"ollama": "Ollama",
"ollama-cloud": "Ollama Cloud",
"opencode-zen": "OpenCode Zen",
"opencode-go": "OpenCode Go",
"lmstudio": "LM Studio",
@@ -723,6 +724,70 @@ _PROVIDER_MODELS = {
}
_AMBIENT_GH_CLI_MARKERS = frozenset({"gh_cli", "gh auth token"})
def _is_ambient_gh_cli_entry(source: str, label: str, key_source: str) -> bool:
"""True when a credential-pool entry is a seeded gh-cli token rather than
one the user added explicitly. Filter these so Copilot doesn't appear in
the dropdown just because `gh` is installed on the system.
"""
return (
source.strip().lower() in _AMBIENT_GH_CLI_MARKERS
or label.strip().lower() == "gh auth token"
or key_source.strip().lower() == "gh auth token"
)
def _format_ollama_label(mid: str) -> str:
"""Turn an Ollama model id (Ollama tag format) into a readable display label.
Examples: 'kimi-k2.5''Kimi K2.5', 'qwen3-vl:235b-instruct''Qwen3 VL (235B Instruct)'
"""
name_part, _, variant = mid.partition(":")
def _fmt(s: str) -> str:
tokens = s.replace("-", " ").replace("_", " ").split()
out = []
for t in tokens:
alpha_only = t.replace(".", "")
if alpha_only.isalpha() and len(t) <= 3:
out.append(t.upper()) # short acronym: glm → GLM, vl → VL, gpt → GPT
elif alpha_only.isalnum() and alpha_only and alpha_only[0].isdigit():
out.append(t.upper()) # size param: 235b → 235B, 1t → 1T
else:
out.append(t[0].upper() + t[1:] if t else t) # capitalize: kimi → Kimi
return " ".join(out)
label = _fmt(name_part)
if variant:
label += f" ({_fmt(variant)})"
return label
def _apply_provider_prefix(
raw_models: list[dict],
provider_id: str,
active_provider: str | None,
) -> list[dict]:
"""Return *raw_models* with @provider: prefixes applied when needed.
Prefixing is skipped when (a) the provider is already the active one, or
(b) a model id already starts with '@' or contains '/' (already routable).
"""
_active = (active_provider or "").lower()
if not _active or provider_id == _active:
return list(raw_models)
result = []
for m in raw_models:
mid = m["id"]
if mid.startswith("@") or "/" in mid:
result.append({"id": mid, "label": m["label"]})
else:
result.append({"id": f"@{provider_id}:{mid}", "label": m["label"]})
return result
def resolve_model_provider(model_id: str) -> tuple:
"""Resolve model name, provider, and base_url for AIAgent.
@@ -1048,24 +1113,28 @@ def get_available_models() -> dict:
if active_provider:
active_provider = _resolve_provider_alias(active_provider)
# 2. Try to read auth store for active provider (if hermes is installed)
if not active_provider:
# 2. Read auth store (active_provider fallback + credential_pool inspection)
auth_store = {}
try:
from api.profiles import get_active_hermes_home as _gah
auth_store_path = _gah() / "auth.json"
except ImportError:
auth_store_path = HOME / ".hermes" / "auth.json"
if auth_store_path.exists():
try:
from api.profiles import get_active_hermes_home as _gah
import json as _j
auth_store_path = _gah() / "auth.json"
except ImportError:
auth_store_path = HOME / ".hermes" / "auth.json"
if auth_store_path.exists():
try:
import json as _j
auth_store = _j.loads(auth_store_path.read_text(encoding="utf-8"))
if not active_provider:
# Re-run alias resolution: auth.json may store an aliased name
# (e.g. 'google', 'z.ai') that the prefixing logic compares
# against canonical pids.
active_provider = _resolve_provider_alias(auth_store.get("active_provider"))
except Exception:
logger.debug("Failed to load auth store from %s", auth_store_path)
auth_store = _j.loads(auth_store_path.read_text(encoding="utf-8"))
active_provider = auth_store.get("active_provider")
except Exception:
logger.debug("Failed to load auth store from %s", auth_store_path)
# 4. Detect available providers.
# 3. Detect available providers.
# Primary: ask hermes-agent's auth layer — the authoritative source. It checks
# auth.json, credential pools, and env vars the same way the agent does at runtime,
# so the dropdown reflects exactly what the user has configured.
@@ -1073,6 +1142,56 @@ def get_available_models() -> dict:
detected_providers = set()
if active_provider:
detected_providers.add(active_provider)
# Include providers that have usable credential-pool entries even when no
# process env var is present (e.g. service launched without shell env).
# Primary: delegate to upstream credential_pool.load_pool() so suppression
# and seeding/pruning rules live in one place.
# Fallback: manual field inspection when the upstream module is unavailable.
try:
_pool = auth_store.get("credential_pool", {}) if isinstance(auth_store, dict) else {}
if isinstance(_pool, dict) and _pool:
try:
from agent.credential_pool import load_pool as _load_pool
# load_pool() does NOT suppress ambient gh-cli tokens — filter
# them here so Copilot doesn't reappear just because the agent
# seeded 'gh auth token' into the pool.
for _pid in list(_pool.keys()):
try:
_canonical_pid = _resolve_provider_alias(str(_pid))
_all_entries = _load_pool(_pid).entries()
_explicit = [
e for e in _all_entries
if not _is_ambient_gh_cli_entry(
str(getattr(e, "source", "") or ""),
str(getattr(e, "label", "") or ""),
str(getattr(e, "key_source", "") or ""),
)
]
if _explicit:
detected_providers.add(_canonical_pid)
except Exception:
logger.debug("credential_pool.load_pool(%s) failed", _pid)
except ImportError:
# Fallback: inspect raw entry fields for suppression signals.
for _pid, _entries in _pool.items():
if not isinstance(_entries, list) or len(_entries) == 0:
continue
_has_explicit_cred = any(
isinstance(_entry, dict)
and not _is_ambient_gh_cli_entry(
str(_entry.get("source", "") or ""),
str(_entry.get("label", "") or ""),
str(_entry.get("key_source", "") or ""),
)
for _entry in _entries
)
if _has_explicit_cred:
detected_providers.add(_resolve_provider_alias(str(_pid)))
except Exception:
logger.debug("Failed to inspect credential_pool from auth store")
all_env: dict = {} # profile .env keys — populated below, used by custom endpoint auth
_hermes_auth_used = False
@@ -1156,7 +1275,7 @@ def get_available_models() -> dict:
if all_env.get("OPENCODE_GO_API_KEY"):
detected_providers.add("opencode-go")
# 3. Fetch models from custom endpoint if base_url is configured
# 4. Fetch models from custom endpoint if base_url is configured
auto_detected_models = []
if cfg_base_url:
try:
@@ -1287,7 +1406,8 @@ def get_available_models() -> dict:
)
model_name = model.get("name", "") or model.get("model", "") or model_id
if model_id and model_name:
auto_detected_models.append({"id": model_id, "label": model_name})
label = _format_ollama_label(model_id) if provider in ("ollama", "ollama-cloud") else model_name
auto_detected_models.append({"id": model_id, "label": label})
detected_providers.add(provider.lower())
except Exception:
logger.debug("Custom endpoint unreachable or misconfigured for provider: %s", provider)
@@ -1378,6 +1498,31 @@ def get_available_models() -> dict:
],
}
)
elif pid == "ollama-cloud":
# Ollama Cloud list is dynamic; fetch via hermes_cli provider catalog.
# When the catalog is unavailable, skip the group rather than emit a
# speculative static list — matches the named-custom and unknown-provider
# branches below.
raw_models = []
try:
from hermes_cli.models import provider_model_ids as _provider_model_ids
raw_models = [
{"id": mid, "label": _format_ollama_label(mid)}
for mid in (_provider_model_ids("ollama-cloud") or [])
]
except Exception:
logger.warning("Failed to load Ollama Cloud models from hermes_cli")
if raw_models:
models = _apply_provider_prefix(raw_models, pid, active_provider)
groups.append(
{
"provider": provider_name,
"provider_id": pid,
"models": models,
}
)
elif pid in _PROVIDER_MODELS or pid in cfg.get("providers", {}):
# For non-default providers, prefix model IDs with @provider:model
# so resolve_model_provider() routes through that specific provider
@@ -1394,18 +1539,7 @@ def get_available_models() -> dict:
raw_models = [{"id": k, "label": k} for k in cfg_models.keys()]
elif isinstance(cfg_models, list):
raw_models = [{"id": k, "label": k} for k in cfg_models]
_active = (active_provider or "").lower()
if _active and pid != _active:
models = []
for m in raw_models:
mid = m["id"]
# Don't double-prefix; use @provider: hint for bare names
if mid.startswith("@") or "/" in mid:
models.append({"id": mid, "label": m["label"]})
else:
models.append({"id": f"@{pid}:{mid}", "label": m["label"]})
else:
models = list(raw_models)
models = _apply_provider_prefix(raw_models, pid, active_provider)
groups.append(
{
"provider": provider_name,

View File

@@ -2152,9 +2152,15 @@ def _handle_live_models(handler, parsed):
if not ids:
return j(handler, {"provider": provider, "models": [], "count": 0})
# Normalise to {id, label} — provider_model_ids() returns plain string IDs
# Normalise to {id, label} — provider_model_ids() returns plain string IDs.
# For ollama-cloud use the shared Ollama formatter (handles `:variant` suffix).
# For all other providers use a simpler hyphen-split capitaliser.
from api.config import _format_ollama_label as _fmt_ollama
def _make_label(mid):
"""Best-effort human label from a model ID string."""
if provider in ("ollama", "ollama-cloud"):
return _fmt_ollama(mid)
# Preserve slashes for router IDs like "anthropic/claude-sonnet-4.6"
display = mid.split("/")[-1] if "/" in mid else mid
parts = display.split("-")