Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui

This commit is contained in:
Brooklyn Nicholson
2026-05-10 07:05:16 -04:00
386 changed files with 16873 additions and 1099 deletions
+2 -5
View File
@@ -63,10 +63,7 @@ _XAI_ASPECT_RATIOS = {
}
# xAI resolutions
_XAI_RESOLUTIONS = {
"1k": "1024",
"2k": "2048",
}
_XAI_RESOLUTIONS = {"1k", "2k"}
DEFAULT_RESOLUTION = "1k"
@@ -177,7 +174,7 @@ class XAIImageGenProvider(ImageGenProvider):
aspect = resolve_aspect_ratio(aspect_ratio)
xai_ar = _XAI_ASPECT_RATIOS.get(aspect, "1:1")
resolution = _resolve_resolution()
xai_res = _XAI_RESOLUTIONS.get(resolution, "1024")
xai_res = resolution if resolution in _XAI_RESOLUTIONS else DEFAULT_RESOLUTION
payload: Dict[str, Any] = {
"model": API_MODEL,
+3 -3
View File
@@ -385,7 +385,7 @@
// --- load config once ---------------------------------------------------
useEffect(function () {
SDK.fetchJSON(`${API}/config`)
SDK.fetchJSON(withBoard(`${API}/config`, board))
.then(function (c) {
setConfig(c);
if (!configApplied) {
@@ -418,7 +418,7 @@
// --- load list of boards for the switcher ------------------------------
const loadBoardList = useCallback(function () {
return SDK.fetchJSON(`${API}/boards`)
return SDK.fetchJSON(withBoard(`${API}/boards`, board))
.then(function (data) {
const boards = (data && data.boards) || [];
setBoardList(boards);
@@ -640,7 +640,7 @@
if (slug && payload.switch) switchBoard(slug);
return res;
});
}, [loadBoardList, switchBoard]);
}, [loadBoardList, switchBoard, board]);
const deleteBoard = useCallback(function (slug) {
if (!slug || slug === "default") return Promise.resolve();
+31 -2
View File
@@ -46,6 +46,23 @@ class OpenRouterProfile(ProviderProfile):
prefs = context.get("provider_preferences")
if prefs:
body["provider"] = prefs
# Pareto Code router — model-gated. The plugins block is only
# meaningful for openrouter/pareto-code; sending it on any other
# model has no documented effect and would be confusing in logs.
# See: https://openrouter.ai/docs/guides/routing/routers/pareto-router
model = (context.get("model") or "")
if model == "openrouter/pareto-code":
score = context.get("openrouter_min_coding_score")
if score is not None and score != "":
try:
score_f = float(score)
except (TypeError, ValueError):
score_f = None
if score_f is not None and 0.0 <= score_f <= 1.0:
body["plugins"] = [
{"id": "pareto-router", "min_coding_score": score_f}
]
return body
def build_api_kwargs_extras(
@@ -53,16 +70,28 @@ class OpenRouterProfile(ProviderProfile):
*,
reasoning_config: dict | None = None,
supports_reasoning: bool = False,
model: str | None = None,
session_id: str | None = None,
**context: Any,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""OpenRouter passes the full reasoning_config dict as extra_body.reasoning."""
"""OpenRouter passes the full reasoning_config dict as extra_body.reasoning.
For xAI Grok models routed through OpenRouter, attach the
``x-grok-conv-id`` header so that xAI's prompt cache stays pinned to
the same backend server across turns.
"""
extra_body: dict[str, Any] = {}
if supports_reasoning:
if reasoning_config is not None:
extra_body["reasoning"] = dict(reasoning_config)
else:
extra_body["reasoning"] = {"enabled": True, "effort": "medium"}
return extra_body, {}
extra_headers: dict[str, Any] = {}
if session_id and model and model.startswith(("x-ai/grok-", "xai/grok-")):
extra_headers["x-grok-conv-id"] = session_id
return extra_body, {"extra_headers": extra_headers} if extra_headers else {}
openrouter = OpenRouterProfile(
+92 -22
View File
@@ -46,27 +46,75 @@ import re
from pathlib import Path as _Path
from typing import Any, Callable, Dict, List, Optional, Tuple
try:
import httplib2
from google.cloud import pubsub_v1
from google.api_core import exceptions as gax_exceptions
from google.oauth2 import service_account
from google_auth_httplib2 import AuthorizedHttp
from googleapiclient.discovery import build as build_service
from googleapiclient.errors import HttpError
from googleapiclient.http import MediaFileUpload
# Heavy google-cloud + googleapiclient imports are deferred to first
# adapter use. Importing them eagerly here added ~110ms wall and ~33MB
# RSS to *every* CLI invocation (the plugin loader imports this module at
# ``model_tools`` import time, so ``hermes status``, ``hermes chat``, etc.
# all paid the cost even though they never instantiate the adapter).
#
# All names below are module globals that ``_load_google_modules()``
# rebinds on first call. The ``HttpError = Exception`` placeholder is
# important: ``except HttpError as exc:`` clauses elsewhere in this
# module bind the *current* module-global at try/except evaluation time,
# so as long as ``_load_google_modules()`` runs before any such
# ``try`` block executes (which it does — ``__init__`` calls it), the
# rebound real ``googleapiclient.errors.HttpError`` is what actually
# matches at runtime.
GOOGLE_CHAT_AVAILABLE: bool = False
httplib2: Any = None # type: ignore
pubsub_v1: Any = None # type: ignore
gax_exceptions: Any = None # type: ignore
service_account: Any = None # type: ignore
AuthorizedHttp: Any = None # type: ignore
build_service: Any = None # type: ignore
HttpError: Any = Exception # type: ignore
MediaFileUpload: Any = None # type: ignore
_google_modules_loaded: bool = False
def _load_google_modules() -> bool:
"""Lazily import the heavy google-cloud + googleapiclient stack.
Idempotent. Returns True if the optional deps are installed and
were successfully imported, False otherwise. On success, mutates
the module globals so existing code using ``pubsub_v1``,
``service_account``, ``HttpError``, etc. transparently uses the
real classes.
Why deferred: the import chain pulls in google.cloud.pubsub_v1,
googleapiclient, grpc, and friends — about 33MB RSS and 110ms wall
on a fresh interpreter. Plugin discovery imports this module on
every CLI invocation, even ones that never touch a gateway.
"""
global GOOGLE_CHAT_AVAILABLE, _google_modules_loaded
global httplib2, pubsub_v1, gax_exceptions, service_account
global AuthorizedHttp, build_service, HttpError, MediaFileUpload
if _google_modules_loaded:
return GOOGLE_CHAT_AVAILABLE
_google_modules_loaded = True
try:
import httplib2 as _httplib2
from google.cloud import pubsub_v1 as _pubsub_v1
from google.api_core import exceptions as _gax_exceptions
from google.oauth2 import service_account as _service_account
from google_auth_httplib2 import AuthorizedHttp as _AuthorizedHttp
from googleapiclient.discovery import build as _build_service
from googleapiclient.errors import HttpError as _HttpError
from googleapiclient.http import MediaFileUpload as _MediaFileUpload
except ImportError:
GOOGLE_CHAT_AVAILABLE = False
return False
httplib2 = _httplib2
pubsub_v1 = _pubsub_v1
gax_exceptions = _gax_exceptions
service_account = _service_account
AuthorizedHttp = _AuthorizedHttp
build_service = _build_service
HttpError = _HttpError
MediaFileUpload = _MediaFileUpload
GOOGLE_CHAT_AVAILABLE = True
except ImportError:
GOOGLE_CHAT_AVAILABLE = False
httplib2 = None # type: ignore
pubsub_v1 = None # type: ignore
gax_exceptions = None # type: ignore
service_account = None # type: ignore
AuthorizedHttp = None # type: ignore
build_service = None # type: ignore
HttpError = Exception # type: ignore
MediaFileUpload = None # type: ignore
return True
from gateway.config import Platform, PlatformConfig
@@ -181,8 +229,14 @@ _TYPING_CONSUMED_SENTINEL = "<consumed>"
def check_google_chat_requirements() -> bool:
"""Check if Google Chat optional dependencies are installed."""
return GOOGLE_CHAT_AVAILABLE
"""Check if Google Chat optional dependencies are installed.
Triggers the lazy import of the google-cloud + googleapiclient stack
on first call. Subsequent calls hit the cached result. This is the
canonical "are the deps available" probe used by the plugin registry
and the adapter's own startup gate.
"""
return _load_google_modules()
# Hostnames we trust to host Google Chat attachment download URIs. Anything
@@ -400,6 +454,16 @@ class GoogleChatAdapter(BasePlatformAdapter):
# attribute to ``gateway.config.Platform`` — bundled platform plugins
# are looked up by value, not attribute (matches Teams, IRC).
super().__init__(config, Platform("google_chat"))
# Trigger the deferred google-cloud + googleapiclient import here so
# that any code path which constructs the adapter and then calls
# methods directly (notably the test suite, which builds an adapter
# and invokes ``_send_file`` / ``_create_message`` / etc. without
# going through ``connect()``) sees real classes for ``MediaFileUpload``,
# ``service_account``, ``HttpError``, and friends. The module-level
# globals were previously eager-imported; making this lazy saved
# ~110ms / ~33MB on every CLI invocation. Idempotent — pays the cost
# exactly once per process.
_load_google_modules()
self._subscriber: Optional[Any] = None
self._chat_api: Optional[Any] = None
# User-authed Chat API client built lazily from the OAuth refresh
@@ -685,7 +749,13 @@ class GoogleChatAdapter(BasePlatformAdapter):
# ------------------------------------------------------------------
async def connect(self) -> bool:
"""Validate config, authenticate, start Pub/Sub pull, resolve bot id."""
if not GOOGLE_CHAT_AVAILABLE:
# First call into the heavy google-cloud stack — trigger the lazy
# import. ``_load_google_modules()`` is idempotent and rebinds the
# module globals (``pubsub_v1``, ``service_account``, ``HttpError``,
# …) used throughout this file. Anything that runs *before* this
# call would see the placeholders, so connect() is the natural
# gate.
if not _load_google_modules():
self._set_fatal_error(
code="missing_deps",
message="google-cloud-pubsub / google-api-python-client not installed",
+12 -1
View File
@@ -30,7 +30,14 @@ import os
from typing import Any, Dict, Optional
from urllib.parse import quote
import httpx
# httpx is imported lazily — only the ``_write_summary_via_incoming_webhook``
# code path actually constructs an ``AsyncClient``. Top-level import here
# pulled in the entire httpx + httpcore stack (~37 ms, ~15 MB) on every
# process that triggered plugin discovery, even ones that never instantiate
# the Teams adapter. ``from __future__ import annotations`` above keeps the
# ``httpx.AsyncBaseTransport`` parameter annotation valid as a string at
# runtime; nothing in the codebase calls ``typing.get_type_hints()`` on
# this class so the annotation never has to resolve to a real symbol.
try:
from aiohttp import web
@@ -199,6 +206,10 @@ class TeamsSummaryWriter:
payload: Any,
config: dict[str, Any],
) -> dict[str, Any]:
# Lazy import — see module-level note. The teams plugin loads on
# every CLI invocation as a side effect of plugin discovery, but
# 99% of those processes never reach this method.
import httpx
webhook_url = str(config.get("incoming_webhook_url") or "").strip()
if not webhook_url:
raise ValueError("TEAMS_INCOMING_WEBHOOK_URL is required for incoming_webhook mode.")