feat: expose Ollama models in Hermes picker
This commit is contained in:
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
This plugin is installable from the Hermes dashboard Plugin Section using the repository URL:
|
||||
|
||||
@@ -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()
|
||||
@@ -3,7 +3,7 @@
|
||||
"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.",
|
||||
"icon": "Cpu",
|
||||
"version": "1.5.10",
|
||||
"version": "1.5.11",
|
||||
"tab": {"path": "/ollama-manager", "position": "after:models"},
|
||||
"entry": "dist/index.js",
|
||||
"css": "dist/style.css",
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import base64
|
||||
import binascii
|
||||
import io
|
||||
import importlib.util
|
||||
import ipaddress
|
||||
import json
|
||||
import mimetypes
|
||||
@@ -311,6 +312,22 @@ def _write_connections(value: dict[str, str]) -> None:
|
||||
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:
|
||||
global LOCAL_OLLAMA
|
||||
saved = _read_connections().get("active_url")
|
||||
@@ -1511,6 +1528,7 @@ def connections_configure(body: ConnectionRequest) -> dict[str, Any]:
|
||||
if role == "local":
|
||||
saved["active_url"] = url
|
||||
_write_connections(saved)
|
||||
_sync_native_ollama_providers(saved)
|
||||
if role == "local":
|
||||
global LOCAL_OLLAMA
|
||||
LOCAL_OLLAMA = url
|
||||
@@ -1533,6 +1551,7 @@ def connections_delete(role: str) -> dict[str, Any]:
|
||||
if role == "local":
|
||||
saved["active_url"] = ""
|
||||
_write_connections(saved)
|
||||
_sync_native_ollama_providers(saved)
|
||||
if role == "local":
|
||||
_apply_saved_connection()
|
||||
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:
|
||||
_sync_native_ollama_providers()
|
||||
app.include_router(router, prefix="/api/plugins/ollama-manager")
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
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.
|
||||
auto_install_dependencies: true
|
||||
python_dependencies:
|
||||
|
||||
Reference in New Issue
Block a user