Files
hermes-agent/hermes_cli/observability/shared_metrics_catalog.py
teknium1 1763066334 fix(telemetry): Azure deployment names leave only when they are a public model id
Azure calls a model by a deployment name its owner chose, which can name
a team, customer or environment. On azure* providers the model id now
passes only when it is in Hermes' static catalogs or the local models.dev
cache (never a network call); anything else reads custom.
2026-09-28 12:43:03 -07:00

326 lines
12 KiB
Python

"""Public-name catalogs for shared metrics: the only non-enum names a package may carry.
Every set here is something Nous itself publishes (slash-command registry, bundled and optional
skills, optional-mcps/ and plugin-catalog/ entries, built-in auxiliary tasks, shipped locales).
A name outside its set is reported as ``custom`` so user-defined identities never leave the machine.
Loaders are cached: the catalogs only change with the installed Hermes version.
"""
from __future__ import annotations
import functools
import logging
import re
from pathlib import Path
logger = logging.getLogger(__name__)
_REPO_ROOT = Path(__file__).resolve().parents[2]
CUSTOM = "custom"
def _skill_dir_names(root: Path) -> frozenset[str]:
try:
return frozenset(p.parent.name.lower() for p in root.rglob("SKILL.md"))
except OSError:
return frozenset()
def _yaml_stems(root: Path) -> frozenset[str]:
try:
return frozenset(p.stem.lower() for p in root.glob("*.yaml"))
except OSError:
return frozenset()
@functools.cache
def slash_command_names() -> frozenset[str]:
from hermes_cli.commands import COMMAND_REGISTRY
return frozenset(command.name for command in COMMAND_REGISTRY)
@functools.cache
def bundled_skill_names() -> frozenset[str]:
from hermes_constants import get_bundled_skills_dir, get_optional_skills_dir
return _skill_dir_names(get_bundled_skills_dir(_REPO_ROOT / "skills")) | _skill_dir_names(
get_optional_skills_dir(_REPO_ROOT / "optional-skills")
)
@functools.cache
def mcp_catalog_names() -> frozenset[str]:
from hermes_constants import get_optional_mcps_dir
root = get_optional_mcps_dir(_REPO_ROOT / "optional-mcps")
try:
return frozenset(p.name.lower() for p in root.iterdir() if p.is_dir())
except OSError:
return frozenset()
@functools.cache
def plugin_catalog_names() -> frozenset[str]:
return _yaml_stems(_REPO_ROOT / "plugin-catalog")
@functools.cache
def aux_task_names() -> frozenset[str]:
from hermes_cli.main_provider_setup import _AUX_TASKS
return frozenset(key for key, _name, _desc in _AUX_TASKS)
@functools.cache
def display_languages() -> frozenset[str]:
return _yaml_stems(_REPO_ROOT / "locales")
@functools.cache
def provider_names() -> frozenset[str]:
"""Provider ids Hermes itself ships: built-in auth rows, overlays, alias tables and the
in-tree ``plugins/model-providers`` profiles. Never the live registries (``PROVIDER_REGISTRY``,
picker labels): ``$HERMES_HOME`` and pip provider plugins add their user-chosen names there."""
import providers
from hermes_cli.auth import _PROVIDER_ALIASES, BUILTIN_PROVIDER_IDS
from hermes_cli.models_catalog_static import _PROVIDER_ALIASES as _CATALOG_ALIASES
from hermes_cli.providers import ALIASES, HERMES_OVERLAYS
providers.list_providers() # runs discovery; bundled profiles land in the process-wide layer
bundled = {
alias for name, profile in providers._REGISTRY.items() if providers._SOURCES.get(name) == "bundled"
for alias in (name, *profile.aliases)
}
return BUILTIN_PROVIDER_IDS.union(
HERMES_OVERLAYS, ALIASES, _PROVIDER_ALIASES, _CATALOG_ALIASES, bundled, ("openrouter", CUSTOM)
)
@functools.cache
def user_named_model_providers() -> frozenset[str]:
"""Providers whose model ids the user names: custom endpoints and loopback servers."""
from urllib.parse import urlparse
from hermes_cli import auth, models, providers
from hermes_cli.auth import PROVIDER_REGISTRY
hosts = {"localhost", "127.0.0.1", "::1", "0.0.0.0"}
loopback = {
name for name, config in PROVIDER_REGISTRY.items()
if urlparse(str(getattr(config, "inference_base_url", "") or "")).hostname in hosts
}
# Alias spellings (``lm-studio``) pass provider_metric_name as shipped names, so they must collapse too.
tables = (providers.ALIASES, getattr(auth, "_PROVIDER_ALIASES", {}), getattr(models, "_PROVIDER_ALIASES", {}))
aliases = {alias for table in tables for alias, canon in table.items() if canon in loopback}
return frozenset(loopback | aliases | {CUSTOM})
@functools.cache
def custom_provider_aliases() -> frozenset[str]:
"""Provider ids Hermes routes through the generic ``custom`` provider (``ollama``, ``vllm``,
``llamacpp``...): shipped names, but the server and its model ids are the user's own."""
from hermes_cli import auth, models, providers
tables = (providers.ALIASES, getattr(auth, "_PROVIDER_ALIASES", {}), getattr(models, "_PROVIDER_ALIASES", {}))
return frozenset(alias for table in tables for alias, canon in table.items() if canon == CUSTOM)
# ---- v4 gateway ----
@functools.cache
def bundled_platform_names() -> frozenset[str]:
"""Messaging platforms Hermes ships as ``plugins/platforms/<name>`` (the dir is the registered name)."""
root = _REPO_ROOT / "plugins" / "platforms"
try:
return frozenset(p.name.lower() for p in root.iterdir() if (p / "plugin.yaml").is_file())
except OSError:
return frozenset()
@functools.cache
def catalog_platform_names() -> frozenset[str]:
"""``plugin-catalog/`` entries whose category is ``platform`` (in-tree only: never a network fetch)."""
from hermes_cli.plugin_catalog import CATALOG_TIERS, load_catalog
return frozenset(e.name for e in load_catalog() if e.category == "platform" and e.tier in CATALOG_TIERS)
def platform_metric_name(raw: object, core: frozenset[str]) -> str:
"""A messaging platform's public name: a core or bundled platform's own name, the catalog entry
name of the installed catalog plugin that registered it, else ``plugin``. ``core`` is the
contract's static platform vocabulary."""
name = _norm(getattr(raw, "value", raw))
if name in core or name in _safe(bundled_platform_names):
return name
if not name:
return "plugin"
from hermes_constants import get_hermes_home
return _catalog_platform_owner(str(get_hermes_home()), name) or "plugin"
@functools.lru_cache(maxsize=64)
def _catalog_platform_owner(home: str, platform: str) -> str | None:
"""The catalog entry that installed the plugin registering ``platform`` in this profile. The
plugin is located from where its adapter factory's code lives (not a name it could claim), and
the catalog install is proven only by the installer-owned ``.install-metadata.json`` record,
never by anything inside the plugin tree, so a URL install cannot claim a catalog name."""
try:
import inspect
from gateway.platform_registry import platform_registry
from hermes_cli.plugins_provenance import read_sidecar_rows
entry = platform_registry.get(platform)
if entry is None or getattr(entry, "source", "") != "plugin":
return None
plugins_dir = (Path(home) / "plugins").resolve()
code_path = Path(inspect.getfile(entry.adapter_factory)).resolve()
if not code_path.is_relative_to(plugins_dir) or code_path.parent == plugins_dir:
return None
row = read_sidecar_rows(plugins_dir).get(code_path.relative_to(plugins_dir).parts[0])
row = row if isinstance(row, dict) else {}
block = row.get("catalog")
name = _norm(block.get("name") if isinstance(block, dict) else row.get("catalog_name"))
return name if name in _safe(catalog_platform_names) else None
except Exception:
logger.debug("Shared-metrics platform provenance unavailable", exc_info=True)
return None
def _norm(value: object) -> str:
return value.strip().lower() if isinstance(value, str) else ""
def _safe(loader) -> frozenset[str]:
try:
return loader()
except Exception:
logger.debug("Shared-metrics catalog %s unavailable", loader.__name__, exc_info=True)
return frozenset()
def slash_command_metric_name(raw: object) -> str:
"""Canonical registry name for a command or alias; skill and plugin commands stay anonymous."""
name = _norm(raw).lstrip("/").split(" ", 1)[0]
if not name:
return "unknown"
try:
from hermes_cli.commands import resolve_command
command = resolve_command(name)
except Exception:
command = None
if command is not None and command.name in _safe(slash_command_names):
return command.name
try:
from agent.skill_commands import resolve_skill_command_key
if resolve_skill_command_key(name):
return "skill"
from hermes_cli.plugins import get_plugin_command_handler
if get_plugin_command_handler(name) is not None:
return "plugin"
except Exception:
pass
return "unknown"
def skill_metric_name(raw: object) -> str:
name = _norm(raw)
return name if name in _safe(bundled_skill_names) else CUSTOM
_EXTENSION_CATALOGS = {
"skill": bundled_skill_names,
"mcp_server": mcp_catalog_names,
"plugin": plugin_catalog_names,
}
def extension_metric_name(kind: str, raw: object) -> str:
"""A catalog entry's public name, else ``custom`` (URL/local installs of anything)."""
loader = _EXTENSION_CATALOGS.get(kind)
name = _norm(raw).rsplit("/", 1)[-1]
return name if loader is not None and name in _safe(loader) else CUSTOM
def aux_task_metric_name(raw: object) -> str:
name = _norm(raw)
if not name:
return "none"
return name if name in _safe(aux_task_names) else "other"
def provider_metric_name(raw: object) -> str:
"""A shipped provider id; user-named providers (``custom:<name>``, unknown ids) and the local
server aliases of ``custom`` read ``custom``."""
from .shared_metrics_contract import PROVIDER_IDENTIFIER_MAX_LENGTH, _metric_identifier
name = _metric_identifier(raw, max_length=PROVIDER_IDENTIFIER_MAX_LENGTH)
if name == "unknown":
return name
if name.startswith(CUSTOM) or name in _safe(custom_provider_aliases):
return CUSTOM
return name if name in _safe(provider_names) or _models_dev_provider(name) else CUSTOM
@functools.lru_cache(maxsize=256)
def _models_dev_provider(name: str) -> bool:
"""A public models.dev provider id, from the local cache only (never a network call)."""
try:
from agent.models_dev import get_provider_info
return get_provider_info(name, allow_network=False) is not None
except Exception:
return False
def model_metric_name(raw: object, provider: str, *, max_length: int) -> str:
"""The model id for a shipped remote provider; ``custom`` when the user names it (custom
endpoint, loopback server), the provider is unknown (nothing proves the id is public), or it
looks like a filesystem path or URL."""
from .shared_metrics_contract import _metric_identifier
if provider in _safe(user_named_model_providers):
return CUSTOM
model = _metric_identifier(raw, max_length=max_length)
if model == "unknown":
return model
if provider == "unknown" or _LOCATION_MODEL.match(model):
return CUSTOM
# Azure calls a deployment by the name its owner chose (``acme-legal-prod``): only a public model id passes.
if provider.startswith("azure") and model not in _safe(public_model_ids):
return CUSTOM
return model
@functools.cache
def public_model_ids() -> frozenset[str]:
"""Model ids Hermes ships in its static catalogs plus every id in the local models.dev cache
(never a network call), with and without a ``vendor/`` prefix."""
from agent.models_dev import fetch_models_dev
from hermes_cli.models_catalog_static import _PROVIDER_MODELS
ids = {model for models in _PROVIDER_MODELS.values() for model in models}
for entry in fetch_models_dev(allow_network=False).values():
if isinstance(entry, dict) and isinstance(entry.get("models"), dict):
ids.update(entry["models"])
return frozenset(form.lower() for model in ids if isinstance(model, str)
for form in (model, model.rsplit("/", 1)[-1]))
# The user's own server or account, never a public model id: a URL or path (``:/``), a weight file,
# a loopback or IPv4 host, ``host:port`` (a 2-5 digit port, so Bedrock's ``...-v1:0`` stays readable),
# or an AWS ARN (it carries the account id).
_LOCATION_MODEL = re.compile(
r"arn:|.*:/|.*\.(?:gguf|bin|safetensors)$|(?:localhost|\d{1,3}(?:\.\d{1,3}){3})(?:[:/]|$)|[^/:]+:\d{2,5}(?:/|$)"
)
def display_language_metric_name(raw: object) -> str:
name = _norm(raw) or "en"
return name if name in _safe(display_languages) else "other"