Merge branch 'main' of github.com:NousResearch/hermes-agent into bb/gui
This commit is contained in:
@@ -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
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user