Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def __repr__(self):
from agent.credential_pool import load_pool
from hermes_cli.config import get_hermes_home
from hermes_constants import OPENROUTER_BASE_URL
from utils import base_url_host_matches, base_url_hostname, model_forces_max_completion_tokens, normalize_proxy_env_vars
from utils import base_url_host_matches, base_url_hostname, env_float, model_forces_max_completion_tokens, normalize_proxy_env_vars

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1312,7 +1312,7 @@ def _resolve_nous_runtime_api(*, force_refresh: bool = False) -> Optional[tuple[
from hermes_cli.auth import resolve_nous_runtime_credentials

creds = resolve_nous_runtime_credentials(
timeout_seconds=float(os.getenv("HERMES_NOUS_TIMEOUT_SECONDS", "15")),
timeout_seconds=env_float("HERMES_NOUS_TIMEOUT_SECONDS", 15),
force_refresh=force_refresh,
)
except Exception as exc:
Expand Down Expand Up @@ -2905,7 +2905,7 @@ def _refresh_provider_credentials(provider: str) -> bool:
from hermes_cli.auth import resolve_nous_runtime_credentials

creds = resolve_nous_runtime_credentials(
timeout_seconds=float(os.getenv("HERMES_NOUS_TIMEOUT_SECONDS", "15")),
timeout_seconds=env_float("HERMES_NOUS_TIMEOUT_SECONDS", 15),
force_refresh=True,
)
if not str(creds.get("api_key", "") or "").strip():
Expand Down
8 changes: 4 additions & 4 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
_repair_tool_call_arguments,
)
from tools.terminal_tool import is_persistent_env
from utils import base_url_host_matches, base_url_hostname, env_int
from utils import base_url_host_matches, base_url_hostname, env_float, env_int

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1761,14 +1761,14 @@ def _call_chat_completions():
_base_timeout = (
_provider_timeout_cfg
if _provider_timeout_cfg is not None
else float(os.getenv("HERMES_API_TIMEOUT", 1800.0))
else env_float("HERMES_API_TIMEOUT", 1800.0)
)
# Read timeout: config wins here too. Otherwise use
# HERMES_STREAM_READ_TIMEOUT (default 120s) for cloud providers.
if _provider_timeout_cfg is not None:
_stream_read_timeout = _provider_timeout_cfg
else:
_stream_read_timeout = float(os.getenv("HERMES_STREAM_READ_TIMEOUT", 120.0))
_stream_read_timeout = env_float("HERMES_STREAM_READ_TIMEOUT", 120.0)
# Local providers (Ollama, llama.cpp, vLLM) can take minutes for
# prefill on large contexts before producing the first token.
# Auto-increase the httpx read timeout unless the user explicitly
Expand Down Expand Up @@ -2508,7 +2508,7 @@ def _call():
if _cfg_stale is not None:
_stream_stale_timeout_base = _cfg_stale
else:
_stream_stale_timeout_base = float(os.getenv("HERMES_STREAM_STALE_TIMEOUT", 180.0))
_stream_stale_timeout_base = env_float("HERMES_STREAM_STALE_TIMEOUT", 180.0)
# Local providers (Ollama, oMLX, llama-cpp) can take 300+ seconds
# for prefill on large contexts. Disable the stale detector unless
# the user explicitly set HERMES_STREAM_STALE_TIMEOUT.
Expand Down
6 changes: 3 additions & 3 deletions gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from enum import Enum

from hermes_cli.config import get_hermes_home
from utils import is_truthy_value
from utils import env_int, is_truthy_value

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1860,7 +1860,7 @@ def _enable_from_env(platform: Platform) -> PlatformConfig:
"token": os.getenv("WECOM_CALLBACK_TOKEN", ""),
"encoding_aes_key": os.getenv("WECOM_CALLBACK_ENCODING_AES_KEY", ""),
"host": os.getenv("WECOM_CALLBACK_HOST", "0.0.0.0"),
"port": int(os.getenv("WECOM_CALLBACK_PORT", "8645")),
"port": env_int("WECOM_CALLBACK_PORT", 8645),
})

# Weixin (personal WeChat via iLink Bot API)
Expand Down Expand Up @@ -1916,7 +1916,7 @@ def _enable_from_env(platform: Platform) -> PlatformConfig:
"server_url": bluebubbles_server_url.rstrip("/"),
"password": bluebubbles_password,
"webhook_host": os.getenv("BLUEBUBBLES_WEBHOOK_HOST", "127.0.0.1"),
"webhook_port": int(os.getenv("BLUEBUBBLES_WEBHOOK_PORT", "8645")),
"webhook_port": env_int("BLUEBUBBLES_WEBHOOK_PORT", 8645),
"webhook_path": os.getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles-webhook"),
"send_read_receipts": os.getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true").lower() in {"true", "1", "yes"},
})
Expand Down
7 changes: 4 additions & 3 deletions gateway/platforms/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
cache_image_from_bytes,
)
from gateway.config import Platform, PlatformConfig
from utils import env_int

logger = logging.getLogger(__name__)
# Automated sender patterns — emails from these are silently ignored
Expand Down Expand Up @@ -309,10 +310,10 @@ def __init__(self, config: PlatformConfig):
self._address = os.getenv("EMAIL_ADDRESS", "")
self._password = os.getenv("EMAIL_PASSWORD", "")
self._imap_host = os.getenv("EMAIL_IMAP_HOST", "")
self._imap_port = int(os.getenv("EMAIL_IMAP_PORT", "993"))
self._imap_port = env_int("EMAIL_IMAP_PORT", 993)
self._smtp_host = os.getenv("EMAIL_SMTP_HOST", "")
self._smtp_port = int(os.getenv("EMAIL_SMTP_PORT", "587"))
self._poll_interval = int(os.getenv("EMAIL_POLL_INTERVAL", "15"))
self._smtp_port = env_int("EMAIL_SMTP_PORT", 587)
self._poll_interval = env_int("EMAIL_POLL_INTERVAL", 15)

# Skip attachments — configured via config.yaml:
# platforms:
Expand Down
20 changes: 10 additions & 10 deletions gateway/platforms/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@
)
from gateway.status import acquire_scoped_lock, release_scoped_lock
from hermes_constants import get_hermes_home
from utils import atomic_json_write
from utils import atomic_json_write, env_float, env_int

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1535,24 +1535,24 @@ def _load_settings(extra: Dict[str, Any]) -> FeishuAdapterSettings:
bot_name=os.getenv("FEISHU_BOT_NAME", "").strip(),
dedup_cache_size=max(
32,
int(os.getenv("HERMES_FEISHU_DEDUP_CACHE_SIZE", str(_DEFAULT_DEDUP_CACHE_SIZE))),
env_int("HERMES_FEISHU_DEDUP_CACHE_SIZE", _DEFAULT_DEDUP_CACHE_SIZE),
),
text_batch_delay_seconds=float(
os.getenv("HERMES_FEISHU_TEXT_BATCH_DELAY_SECONDS", str(_DEFAULT_TEXT_BATCH_DELAY_SECONDS))
text_batch_delay_seconds=env_float(
"HERMES_FEISHU_TEXT_BATCH_DELAY_SECONDS", _DEFAULT_TEXT_BATCH_DELAY_SECONDS
),
text_batch_split_delay_seconds=float(
os.getenv("HERMES_FEISHU_TEXT_BATCH_SPLIT_DELAY_SECONDS", "2.0")
text_batch_split_delay_seconds=env_float(
"HERMES_FEISHU_TEXT_BATCH_SPLIT_DELAY_SECONDS", 2.0
),
text_batch_max_messages=max(
1,
int(os.getenv("HERMES_FEISHU_TEXT_BATCH_MAX_MESSAGES", str(_DEFAULT_TEXT_BATCH_MAX_MESSAGES))),
env_int("HERMES_FEISHU_TEXT_BATCH_MAX_MESSAGES", _DEFAULT_TEXT_BATCH_MAX_MESSAGES),
),
text_batch_max_chars=max(
1,
int(os.getenv("HERMES_FEISHU_TEXT_BATCH_MAX_CHARS", str(_DEFAULT_TEXT_BATCH_MAX_CHARS))),
env_int("HERMES_FEISHU_TEXT_BATCH_MAX_CHARS", _DEFAULT_TEXT_BATCH_MAX_CHARS),
),
media_batch_delay_seconds=float(
os.getenv("HERMES_FEISHU_MEDIA_BATCH_DELAY_SECONDS", str(_DEFAULT_MEDIA_BATCH_DELAY_SECONDS))
media_batch_delay_seconds=env_float(
"HERMES_FEISHU_MEDIA_BATCH_DELAY_SECONDS", _DEFAULT_MEDIA_BATCH_DELAY_SECONDS
),
webhook_host=str(
extra.get("webhook_host") or os.getenv("FEISHU_WEBHOOK_HOST", _DEFAULT_WEBHOOK_HOST)
Expand Down
6 changes: 3 additions & 3 deletions gateway/platforms/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class _MockContextTypes:
discover_fallback_ips,
parse_fallback_ip_env,
)
from utils import atomic_replace
from utils import atomic_replace, env_float, env_int

_TELEGRAM_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
_TELEGRAM_IMAGE_MIME_TO_EXT = {
Expand Down Expand Up @@ -433,7 +433,7 @@ def __init__(self, config: PlatformConfig):
self._rich_draft_disabled: bool = False
# Buffer rapid/album photo updates so Telegram image bursts are handled
# as a single MessageEvent instead of self-interrupting multiple turns.
self._media_batch_delay_seconds = float(os.getenv("HERMES_TELEGRAM_MEDIA_BATCH_DELAY_SECONDS", "0.8"))
self._media_batch_delay_seconds = env_float("HERMES_TELEGRAM_MEDIA_BATCH_DELAY_SECONDS", 0.8)
self._pending_photo_batches: Dict[str, MessageEvent] = {}
self._pending_photo_batch_tasks: Dict[str, asyncio.Task] = {}
self._media_group_events: Dict[str, MessageEvent] = {}
Expand Down Expand Up @@ -2153,7 +2153,7 @@ def _env_float(name: str, default: float) -> float:
# inject forged updates as if from Telegram. Refuse to
# start rather than silently run in fail-open mode.
# See GHSA-3vpc-7q5r-276h.
webhook_port = int(os.getenv("TELEGRAM_WEBHOOK_PORT", "8443"))
webhook_port = env_int("TELEGRAM_WEBHOOK_PORT", 8443)
webhook_secret = os.getenv("TELEGRAM_WEBHOOK_SECRET", "").strip()
if not webhook_secret:
raise RuntimeError(
Expand Down
5 changes: 3 additions & 2 deletions gateway/platforms/wecom.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
cache_document_from_bytes,
cache_image_from_bytes,
)
from utils import env_float

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -186,8 +187,8 @@ def __init__(self, config: PlatformConfig):

# Text batching: merge rapid successive messages (Telegram-style).
# WeCom clients split long messages around 4000 chars.
self._text_batch_delay_seconds = float(os.getenv("HERMES_WECOM_TEXT_BATCH_DELAY_SECONDS", "0.6"))
self._text_batch_split_delay_seconds = float(os.getenv("HERMES_WECOM_TEXT_BATCH_SPLIT_DELAY_SECONDS", "2.0"))
self._text_batch_delay_seconds = env_float("HERMES_WECOM_TEXT_BATCH_DELAY_SECONDS", 0.6)
self._text_batch_split_delay_seconds = env_float("HERMES_WECOM_TEXT_BATCH_SPLIT_DELAY_SECONDS", 2.0)
self._pending_text_batches: Dict[str, MessageEvent] = {}
self._pending_text_batch_tasks: Dict[str, asyncio.Task] = {}
self._device_id = uuid.uuid4().hex
Expand Down
3 changes: 2 additions & 1 deletion gateway/platforms/whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def _terminate_bridge_process(proc, *, force: bool = False) -> None:
cache_image_from_url,
cache_audio_from_url,
)
from utils import env_int


def _file_content_hash(path: Path) -> str:
Expand Down Expand Up @@ -412,7 +413,7 @@ async def connect(self) -> bool:
try:
# Read timeout from environment variable, default to 300 seconds (5 minutes)
# to accommodate slower systems like Unraid NAS
npm_install_timeout = int(os.environ.get("WHATSAPP_NPM_INSTALL_TIMEOUT", "300"))
npm_install_timeout = env_int("WHATSAPP_NPM_INSTALL_TIMEOUT", 300)
install_result = subprocess.run(
[_npm_bin, "install", "--silent"],
cwd=str(bridge_dir),
Expand Down
6 changes: 3 additions & 3 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from hermes_cli.config import get_hermes_home, get_config_path, read_raw_config
from hermes_constants import OPENROUTER_BASE_URL, secure_parent_dir
from agent.credential_persistence import sanitize_borrowed_credential_payload
from utils import atomic_replace, atomic_yaml_write, is_truthy_value
from utils import atomic_replace, atomic_yaml_write, env_float, is_truthy_value

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -3838,7 +3838,7 @@ def resolve_codex_runtime_credentials(

tokens = dict(data["tokens"])
access_token = str(tokens.get("access_token", "") or "").strip()
refresh_timeout_seconds = float(os.getenv("HERMES_CODEX_REFRESH_TIMEOUT_SECONDS", "20"))
refresh_timeout_seconds = env_float("HERMES_CODEX_REFRESH_TIMEOUT_SECONDS", 20)

should_refresh = bool(force_refresh)
if (not should_refresh) and refresh_if_expiring:
Expand Down Expand Up @@ -4475,7 +4475,7 @@ def resolve_xai_oauth_runtime_credentials(
data = _read_xai_oauth_tokens()
tokens = dict(data["tokens"])
access_token = str(tokens.get("access_token", "") or "").strip()
refresh_timeout_seconds = float(os.getenv("HERMES_XAI_REFRESH_TIMEOUT_SECONDS", "20"))
refresh_timeout_seconds = env_float("HERMES_XAI_REFRESH_TIMEOUT_SECONDS", 20)
discovery = dict(data.get("discovery") or {})
token_endpoint = str(discovery.get("token_endpoint", "") or "").strip()
redirect_uri = str(data.get("redirect_uri", "") or "").strip()
Expand Down
6 changes: 3 additions & 3 deletions plugins/platforms/discord/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def __init__(self, id: int) -> None: # noqa: A002 - matches discord API
from gateway.config import Platform, PlatformConfig

from gateway.platforms.helpers import MessageDeduplicator, ThreadParticipationTracker
from utils import atomic_json_write
from utils import atomic_json_write, env_float
from gateway.platforms.base import (
BasePlatformAdapter,
MessageEvent,
Expand Down Expand Up @@ -746,8 +746,8 @@ def __init__(self, config: PlatformConfig):
self._voice_clients: Dict[int, Any] = {} # guild_id -> VoiceClient
self._voice_locks: Dict[int, asyncio.Lock] = {} # guild_id -> serialize join/leave
# Text batching: merge rapid successive messages (Telegram-style)
self._text_batch_delay_seconds = float(os.getenv("HERMES_DISCORD_TEXT_BATCH_DELAY_SECONDS", "0.6"))
self._text_batch_split_delay_seconds = float(os.getenv("HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS", "2.0"))
self._text_batch_delay_seconds = env_float("HERMES_DISCORD_TEXT_BATCH_DELAY_SECONDS", 0.6)
self._text_batch_split_delay_seconds = env_float("HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS", 2.0)
self._pending_text_batches: Dict[str, MessageEvent] = {}
self._pending_text_batch_tasks: Dict[str, asyncio.Task] = {}
self._voice_text_channels: Dict[int, int] = {} # guild_id -> text_channel_id
Expand Down
6 changes: 3 additions & 3 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def _session_source_for_agent(platform: Optional[str]) -> str:
_extract_error_preview,
_trajectory_normalize_msg, # noqa: F401 # re-exported for tests that `from run_agent import _trajectory_normalize_msg`
)
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, is_truthy_value, model_forces_max_completion_tokens
from utils import atomic_json_write, base_url_host_matches, base_url_hostname, env_float, is_truthy_value, model_forces_max_completion_tokens



Expand Down Expand Up @@ -1109,7 +1109,7 @@ def _resolved_api_call_timeout(self) -> float:
cfg = get_provider_request_timeout(self.provider, self.model)
if cfg is not None:
return cfg
return float(os.getenv("HERMES_API_TIMEOUT", 1800.0))
return env_float("HERMES_API_TIMEOUT", 1800.0)

def _resolved_api_call_stale_timeout_base(self) -> tuple[float, bool]:
"""Resolve the base non-stream stale timeout and whether it is implicit.
Expand Down Expand Up @@ -3839,7 +3839,7 @@ def _try_refresh_nous_client_credentials(
from hermes_cli.auth import resolve_nous_runtime_credentials

creds = resolve_nous_runtime_credentials(
timeout_seconds=float(os.getenv("HERMES_NOUS_TIMEOUT_SECONDS", "15")),
timeout_seconds=env_float("HERMES_NOUS_TIMEOUT_SECONDS", 15),
force_refresh=force,
)
except Exception as exc:
Expand Down
11 changes: 11 additions & 0 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,17 @@ def env_int(key: str, default: int = 0) -> int:
return default


def env_float(key: str, default: float = 0.0) -> float:
"""Read an environment variable as a float, with fallback."""
raw = os.getenv(key, "").strip()
if not raw:
return default
try:
return float(raw)
except (ValueError, TypeError):
return default


def env_bool(key: str, default: bool = False) -> bool:
"""Read an environment variable as a boolean."""
return is_truthy_value(os.getenv(key, ""), default=default)
Expand Down
Loading