feat: expose Ollama models in Hermes picker

This commit is contained in:
Hermes Agent
2026-08-25 19:17:59 +10:00
parent 73a38617e3
commit 20555c802a
6 changed files with 127 additions and 2 deletions
+6
View File
@@ -47,6 +47,12 @@ The Available downloads controls support:
Popularity and date ordering use upstream metadata only; the plugin does not invent popularity, dates, RAM requirements, or token metrics. Popularity and date ordering use upstream metadata only; the plugin does not invent popularity, dates, RAM requirements, or token metrics.
## Hermes native model selector
During plugin load after install/update, the plugin registers the active Ollama endpoint(s) in Hermes' native `providers:` configuration as **Ollama Models (Local)** and, when configured, **Ollama Models (Remote)**. Hermes discovers the installed IDs through each endpoint's OpenAI-compatible `/v1/models` route, so models such as `gemma4:latest` become selectable with their exact Ollama tags.
Changing the Hermes model selector only changes the selected provider/model. It does not call Ollama load or keep-alive APIs. End users must use the Ollama Models page's **Load selected permanently** action when they want a model loaded and kept available. Installing/updating the plugin and synchronizing providers does not change Hermes' existing default model or provider.
## Installation ## Installation
This plugin is installable from the Hermes dashboard Plugin Section using the repository URL: This plugin is installable from the Hermes dashboard Plugin Section using the repository URL:
+8
View File
@@ -0,0 +1,8 @@
"""Hermes Ollama Models plugin integration."""
from .ollama_provider import sync_hermes_ollama_providers
def register(ctx) -> None:
"""Register the Ollama endpoints in Hermes' native model picker."""
sync_hermes_ollama_providers()
+1 -1
View File
@@ -3,7 +3,7 @@
"label": "Ollama Models", "label": "Ollama Models",
"description": "Inspect, manage, and chat with local Ollama models, including shared persistent conversations, performance metrics, images, PDFs, URLs, and live memory telemetry.", "description": "Inspect, manage, and chat with local Ollama models, including shared persistent conversations, performance metrics, images, PDFs, URLs, and live memory telemetry.",
"icon": "Cpu", "icon": "Cpu",
"version": "1.5.10", "version": "1.5.11",
"tab": {"path": "/ollama-manager", "position": "after:models"}, "tab": {"path": "/ollama-manager", "position": "after:models"},
"entry": "dist/index.js", "entry": "dist/index.js",
"css": "dist/style.css", "css": "dist/style.css",
+20
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import base64 import base64
import binascii import binascii
import io import io
import importlib.util
import ipaddress import ipaddress
import json import json
import mimetypes import mimetypes
@@ -311,6 +312,22 @@ def _write_connections(value: dict[str, str]) -> None:
pass pass
def _sync_native_ollama_providers(connections: dict[str, str] | None = None) -> None:
"""Keep Hermes' native model picker aligned with this plugin's endpoints."""
try:
helper_path = Path(__file__).resolve().parent.parent / "ollama_provider.py"
spec = importlib.util.spec_from_file_location("_ollama_manager_provider_sync", helper_path)
if spec is None or spec.loader is None:
return
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.sync_hermes_ollama_providers(connections)
except Exception:
# Provider synchronization must never make the Ollama dashboard route fail.
return
def _apply_saved_connection() -> None: def _apply_saved_connection() -> None:
global LOCAL_OLLAMA global LOCAL_OLLAMA
saved = _read_connections().get("active_url") saved = _read_connections().get("active_url")
@@ -1511,6 +1528,7 @@ def connections_configure(body: ConnectionRequest) -> dict[str, Any]:
if role == "local": if role == "local":
saved["active_url"] = url saved["active_url"] = url
_write_connections(saved) _write_connections(saved)
_sync_native_ollama_providers(saved)
if role == "local": if role == "local":
global LOCAL_OLLAMA global LOCAL_OLLAMA
LOCAL_OLLAMA = url LOCAL_OLLAMA = url
@@ -1533,6 +1551,7 @@ def connections_delete(role: str) -> dict[str, Any]:
if role == "local": if role == "local":
saved["active_url"] = "" saved["active_url"] = ""
_write_connections(saved) _write_connections(saved)
_sync_native_ollama_providers(saved)
if role == "local": if role == "local":
_apply_saved_connection() _apply_saved_connection()
return {"ok": True, "role": role, "removed_url": removed, "message": f"Removed saved {role} Ollama endpoint"} return {"ok": True, "role": role, "removed_url": removed, "message": f"Removed saved {role} Ollama endpoint"}
@@ -1687,4 +1706,5 @@ def delete_model(body: ModelRequest) -> dict[str, Any]:
def create_ollama_routes(app) -> None: def create_ollama_routes(app) -> None:
_sync_native_ollama_providers()
app.include_router(router, prefix="/api/plugins/ollama-manager") app.include_router(router, prefix="/api/plugins/ollama-manager")
+91
View File
@@ -0,0 +1,91 @@
"""Synchronize Ollama Models endpoints into Hermes' native model picker."""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
from typing import Any
from hermes_constants import get_hermes_home
logger = logging.getLogger(__name__)
LOCAL_PROVIDER_KEY = "ollama-manager-local"
REMOTE_PROVIDER_KEY = "ollama-manager-remote"
_OWNED_PROVIDER_KEYS = {LOCAL_PROVIDER_KEY, REMOTE_PROVIDER_KEY}
def _running_in_container() -> bool:
return Path("/.dockerenv").exists() or Path("/run/.containerenv").exists()
def _default_endpoint() -> str:
configured = os.environ.get("OLLAMA_HOST", "").strip().rstrip("/")
if configured:
return configured
return "http://ollama:11434" if _running_in_container() else "http://localhost:11434"
def _connections() -> dict[str, str]:
path = get_hermes_home() / "ollama-manager" / "connections.json"
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError):
value = {}
if not isinstance(value, dict):
value = {}
return {
key: str(value.get(key) or "").strip().rstrip("/")
for key in ("active_url", "local_url", "remote_url")
}
def _endpoint_to_api(endpoint: str) -> str:
endpoint = str(endpoint or "").strip().rstrip("/")
if endpoint.endswith("/v1"):
return endpoint
return endpoint + "/v1"
def _provider_entry(name: str, endpoint: str) -> dict[str, Any]:
return {
"name": name,
"api": _endpoint_to_api(endpoint),
"transport": "chat_completions",
"discover_models": True,
}
def sync_hermes_ollama_providers(connections: dict[str, str] | None = None) -> bool:
"""Upsert the plugin's local/remote providers without changing defaults."""
try:
from hermes_cli.config import load_config, save_config
saved = connections or _connections()
local = saved.get("local_url") or saved.get("active_url") or _default_endpoint()
remote = saved.get("remote_url")
if remote and remote == local:
remote = ""
config = load_config()
providers = config.get("providers")
if not isinstance(providers, dict):
providers = {}
config["providers"] = providers
for key in _OWNED_PROVIDER_KEYS:
providers.pop(key, None)
providers[LOCAL_PROVIDER_KEY] = _provider_entry("Ollama Models (Local)", local)
if remote:
providers[REMOTE_PROVIDER_KEY] = _provider_entry("Ollama Models (Remote)", remote)
# The provider is selectable, not the active default. Do not touch
# model.default or model.provider while installing/updating the plugin.
save_config(config, strip_defaults=False)
return True
except Exception as exc: # provider registration must never break the dashboard
logger.warning("Could not synchronize Ollama providers into Hermes config: %s", exc)
return False
+1 -1
View File
@@ -1,5 +1,5 @@
name: ollama-manager name: ollama-manager
version: 1.5.10 version: 1.5.11
description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, shared persistent conversations, performance metrics, and live runtime telemetry. description: Native dashboard manager and chat interface for local Ollama models, attachments, URLs, shared persistent conversations, performance metrics, and live runtime telemetry.
auto_install_dependencies: true auto_install_dependencies: true
python_dependencies: python_dependencies: