Release v0.51.270 — Release IL (stage-u1 — un-hold batch: #3517 #3624 #3613) (#3674)
Some checks failed
Release & Docker / release (push) Has been cancelled
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(commands): add /use to force a skill for the next turn (#3517, #2977) Co-authored-by: Rod Boev <rod.boev@gmail.com> * fix(auth): cap pending passkey challenges by evicting oldest, not rejecting (#3624) Co-authored-by: Hinotobi <paperlantern.agent@gmail.com> * fix(providers): expose model-provider plugins in WebUI (#3613) Co-authored-by: Pamnard <pamnard@users.noreply.github.com> * docs(changelog): v0.51.270 — Release IL (stage-u1, 3-PR un-hold batch; #3448 + #3618 dropped) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: Hinotobi <paperlantern.agent@gmail.com> Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
This commit is contained in:
141
api/plugin_providers.py
Normal file
141
api/plugin_providers.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Helpers for model-provider plugins (``plugins/model-providers/<name>/``).
|
||||
|
||||
The Hermes agent discovers these via ``providers.list_providers()`` and exposes
|
||||
them in the CLI model picker. WebUI must mirror that registry instead of
|
||||
relying only on the static ``_PROVIDER_DISPLAY`` / ``_PROVIDER_MODELS`` tables.
|
||||
|
||||
Bundled agent profiles (gemini, nous, custom, …) also live in
|
||||
``list_providers()``. WebUI already handles those via static tables and
|
||||
dedicated code paths — only *plugin-only* slugs (e.g. user-installed yandex)
|
||||
should take the plugin discovery path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PROFILES_LOCK = threading.Lock()
|
||||
_PROFILES_BY_NAME: dict[str, Any] | None = None
|
||||
_WEBUI_STATIC_PROVIDER_IDS: frozenset[str] | None = None
|
||||
|
||||
|
||||
def _webui_static_provider_ids() -> frozenset[str]:
|
||||
"""Provider slugs already owned by WebUI static tables / special cases."""
|
||||
global _WEBUI_STATIC_PROVIDER_IDS
|
||||
if _WEBUI_STATIC_PROVIDER_IDS is not None:
|
||||
return _WEBUI_STATIC_PROVIDER_IDS
|
||||
try:
|
||||
from api.config import _PROVIDER_DISPLAY, _PROVIDER_MODELS
|
||||
|
||||
static = (
|
||||
frozenset(_PROVIDER_DISPLAY.keys())
|
||||
| frozenset(_PROVIDER_MODELS.keys())
|
||||
| frozenset({"custom"})
|
||||
)
|
||||
except Exception:
|
||||
static = frozenset({"custom"})
|
||||
_WEBUI_STATIC_PROVIDER_IDS = static
|
||||
return static
|
||||
|
||||
|
||||
def _load_profiles_by_name() -> dict[str, Any]:
|
||||
try:
|
||||
from providers import list_providers
|
||||
except Exception:
|
||||
logger.debug("providers package unavailable for plugin discovery", exc_info=True)
|
||||
return {}
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
try:
|
||||
for profile in list_providers():
|
||||
name = str(getattr(profile, "name", "") or "").strip().lower()
|
||||
if name:
|
||||
result[name] = profile
|
||||
except Exception:
|
||||
logger.debug("Failed to enumerate model-provider plugins", exc_info=True)
|
||||
return {}
|
||||
return result
|
||||
|
||||
|
||||
def plugin_model_provider_profiles() -> dict[str, Any]:
|
||||
"""Return registered model-provider profiles keyed by canonical slug."""
|
||||
global _PROFILES_BY_NAME
|
||||
cached = _PROFILES_BY_NAME
|
||||
if cached is not None:
|
||||
return cached
|
||||
with _PROFILES_LOCK:
|
||||
if _PROFILES_BY_NAME is None:
|
||||
_PROFILES_BY_NAME = _load_profiles_by_name()
|
||||
return _PROFILES_BY_NAME
|
||||
|
||||
|
||||
def invalidate_plugin_model_provider_cache() -> None:
|
||||
"""Clear cached plugin discovery (e.g. after config reload)."""
|
||||
global _PROFILES_BY_NAME
|
||||
with _PROFILES_LOCK:
|
||||
_PROFILES_BY_NAME = None
|
||||
|
||||
|
||||
def plugin_model_provider_ids() -> frozenset[str]:
|
||||
"""Slugs from ``list_providers()`` that are not already WebUI-static."""
|
||||
static = _webui_static_provider_ids()
|
||||
return frozenset(
|
||||
pid for pid in plugin_model_provider_profiles().keys() if pid not in static
|
||||
)
|
||||
|
||||
|
||||
def plugin_model_provider_display_name(provider_id: str) -> str | None:
|
||||
profile = plugin_model_provider_profiles().get((provider_id or "").strip().lower())
|
||||
if profile is None:
|
||||
return None
|
||||
return str(getattr(profile, "display_name", "") or getattr(profile, "name", "") or "").strip() or None
|
||||
|
||||
|
||||
def plugin_model_provider_api_key_env_var(provider_id: str) -> str | None:
|
||||
"""Return the primary API-key env var for a plugin provider, if any."""
|
||||
profile = plugin_model_provider_profiles().get((provider_id or "").strip().lower())
|
||||
if profile is None:
|
||||
return None
|
||||
env_vars = getattr(profile, "env_vars", ()) or ()
|
||||
for var in env_vars:
|
||||
upper = str(var).upper()
|
||||
if upper.endswith("_BASE_URL") or upper.endswith("_URL"):
|
||||
continue
|
||||
if upper.endswith("_FOLDER_ID"):
|
||||
continue
|
||||
return str(var)
|
||||
return None
|
||||
|
||||
|
||||
def effective_provider_env_var(provider_id: str, static_map: dict[str, str]) -> str | None:
|
||||
pid = (provider_id or "").strip().lower()
|
||||
if not pid:
|
||||
return None
|
||||
if pid in static_map:
|
||||
return static_map[pid]
|
||||
if not is_plugin_model_provider(pid):
|
||||
return None
|
||||
return plugin_model_provider_api_key_env_var(pid)
|
||||
|
||||
|
||||
def effective_provider_display_name(provider_id: str, static_map: dict[str, str]) -> str:
|
||||
pid = (provider_id or "").strip().lower()
|
||||
if pid in static_map:
|
||||
return static_map[pid]
|
||||
if is_plugin_model_provider(pid):
|
||||
plugin_name = plugin_model_provider_display_name(pid)
|
||||
if plugin_name:
|
||||
return plugin_name
|
||||
return pid.replace("-", " ").title()
|
||||
|
||||
|
||||
def is_plugin_model_provider(provider_id: str) -> bool:
|
||||
"""True for plugin-only providers (not already in WebUI static tables)."""
|
||||
pid = (provider_id or "").strip().lower()
|
||||
if not pid or pid in _webui_static_provider_ids():
|
||||
return False
|
||||
return pid in plugin_model_provider_profiles()
|
||||
Reference in New Issue
Block a user