NousResearch/hermes-agent

Lazy imports and cron interpreter import-error handling

Cluster 11610 · cron. Data as of

Nothing open on GitHub. Fix merged in #17046 on 28 Apr 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #17046. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

The members touch on Python import behavior: one proposes lazy-importing several libraries (OpenAI, Anthropic, Firecrawl, account_usage) for startup performance, one reports an intermittent cron ModuleNotFoundError for 'openai' tied to an interpreter mismatch, and one proposes a preflight import check with a clearer error for that mismatch. The startup-performance item is only loosely related to the other two.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #17046 perf(startup): lazy-import OpenAI, Anthropic, Firecrawl, account_usage

    merged to main

    files: agent/anthropic_adapter.py, agent/auxiliary_client.py, cli.py, gateway/run.py, run_agent.py, tools/web_tools.py

    Show the diff
    • agent/anthropic_adapter.py
      @@ -22,10 +22,25 @@
       from typing import Any, Dict, List, Optional, Tuple
       from utils import normalize_proxy_env_vars
       
      -try:
      -    import anthropic as _anthropic_sdk
      -except ImportError:
      -    _anthropic_sdk = None  # type: ignore[assignment]
      +# NOTE: `import anthropic` is deliberately NOT at module top — the SDK pulls
      +# ~220 ms of imports (anthropic.types, anthropic.lib.tools._beta_runner, etc.)
      +# and the 3 usage sites (build_anthropic_client, build_anthropic_bedrock_client,
      +# read_claude_code_credentials_from_keychain) are all on cold user-triggered
      +# paths. Access via the `_get_anthropic_sdk()` accessor below, which caches
      +# the module after the first call and returns None on ImportError.
      +_anthropic_sdk: Any = ...  # sentinel — None means "tried and missing"
      +
      +
      +def _get_anthropic_sdk():
      +    """Return the ``anthropic`` SDK module, importing lazily. None if not installed."""
      +    global _anthropic_sdk
      +    if _anthropic_sdk is ...:
      +        try:
      +            import anthropic as _sdk
      +            _anthropic_sdk = _sdk
      +        except ImportError:
      +            _anthropic_sdk = None
      +    return _anthropic_sdk
       
       logger = logging.getLogger(__name__)
       
      @@ -395,6 +410,7 @@ def build_anthropic_client(api_key: str, base_url: str = None, timeout: float =
       
           Returns an anthropic.Anthropic instance.
           """
      +    _anthropic_sdk = _get_anthropic_sdk()
           if _anthropic_sdk is None:
               raise ImportError(
                   "The 'anthropic' package is required for the Anthropic provider. "
      @@ -492,6 +508,7 @@ def build_anthropic_bedrock_client(region: str):
       
           Auth uses the boto3 default credential chain (IAM roles, SSO, env vars).
           """
      +    _anthropic_sdk = _get_anthropic_sdk()
           if _anthropic_sdk is None:
               raise ImportError(
                   "The 'anthropic' package is required for the Bedrock provider. "
    • agent/auxiliary_client.py
      @@ -41,10 +41,57 @@
       import time
       from pathlib import Path  # noqa: F401 — used by test mocks
       from types import SimpleNamespace
      -from typing import Any, Dict, List, Optional, Tuple
      +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
       from urllib.parse import urlparse, parse_qs, urlunparse
       
      -from openai import OpenAI
      +# NOTE: `from openai import OpenAI` is deliberately NOT at module top — the
      +# openai SDK pulls a large type tree (~240 ms cold, including responses/*,
      +# graders/*). We expose `OpenAI` here as a thin proxy that imports the SDK on
      +# first call and forwards, so:
      +#   (a) the 15+ in-module `OpenAI(...)` construction sites work unchanged
      +#       (Python's function-scope name lookup resolves `OpenAI` to the proxy
      +#       object bound in module globals here, without triggering any import);
      +#   (b) external code can still do `auxiliary_client.OpenAI` or
      +#       `patch("agent.auxiliary_client.OpenAI", ...)` — tests see the proxy,
      +#       and patch replaces the module attribute as usual;
      +#   (c) `OpenAI` as a type annotation resolves at runtime to the proxy class
      +#       (which is harmless — annotations aren't type-checked at runtime).
      +# See tests/agent/test_auxiliary_client.py for patch patterns this supports.
      +if TYPE_CHECKING:
      +    from openai import OpenAI  # noqa: F401 — type hints only
      +
      +_OPENAI_CLS_CACHE: Optional[type] = None
      +
      +
      +def _load_openai_cls() -> type:
      +    """Import and cache ``openai.OpenAI``."""
      +    global _OPENAI_CLS_CACHE
      +    if _OPENAI_CLS_CACHE is None:
      +        from openai import OpenAI as _cls
      +        _OPENAI_CLS_CACHE = _cls
      +    return _OPENAI_CLS_CACHE
      +
      +
      +class _OpenAIProxy:
      +    """Module-level proxy that looks like the ``openai.OpenAI`` class.
      +
      +    Forwards ``OpenAI(...)`` calls and ``isinstance(x, OpenAI)`` checks to the
      +    real SDK class, importing the SDK lazily on first use.
      +    """
      +
      +    __slots__ = ()
      +
      +    def __call__(self, *args, **kwargs):
      +        return _load_openai_cls()(*args, **kwargs)
      +
      +    def __instancecheck__(self, obj):
      +        return isinstance(obj, _load_openai_cls())
      +
      +    def __repr__(self):
      +        return "<lazy openai.OpenAI proxy>"
      +
      +
      +OpenAI = _OpenAIProxy()  # module-level name, resolves lazily on call/isinstance
       
       from agent.credential_pool import load_pool
       from hermes_cli.config import get_hermes_home
    • cli.py
      @@ -69,7 +69,9 @@
           format_duration_compact,
           format_token_count_compact,
       )
      -from agent.account_usage import fetch_account_usage, render_account_usage_lines
      +# NOTE: `from agent.account_usage import ...` is deliberately NOT at module
      +# top — it transitively pulls the OpenAI SDK chain (~230 ms cold) and is only
      +# needed when the user runs `/limits`. Lazy-imported inside the handler below.
       from hermes_cli.banner import _format_context_length, format_banner_version_label
       
       _COMMAND_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
      @@ -7285,6 +7287,8 @@ def _show_usage(self):
               provider = getattr(agent, "provider", None) or getattr(self, "provider", None)
               base_url = getattr(agent, "base_url", None) or getattr(self, "base_url", None)
               api_key = [redacted:secret-assignment] "api_key", None) or getattr(self, "api_key", None)
      +        # Lazy import — pulls the OpenAI SDK chain, only needed here.
      +        from agent.account_usage import fetch_account_usage, render_account_usage_lines
               account_snapshot = None
               if provider:
                   with concurrent.futures.ThreadPoolExecutor(max_workers=1) as _pool:
    • gateway/run.py
      @@ -31,6 +31,12 @@
       from datetime import datetime
       from typing import Dict, Optional, Any, List
       
      +# account_usage imports the OpenAI SDK chain (~230 ms). Only needed by
      +# /usage; we still import it at module top in the gateway because test
      +# patches (tests/gateway/test_usage_command.py) target
      +# `gateway.run.fetch_account_usage` as a module-level attribute. The
      +# gateway is a long-running daemon, so its boot cost matters less than
      +# preserving the established test-patch surface.
       from agent.account_usage import fetch_account_usage, render_account_usage_lines
       
       # --- Agent cache tuning ---------------------------------------------------
    • run_agent.py
      @@ -41,13 +41,48 @@
       import uuid
       from typing import List, Dict, Any, Optional
       from urllib.parse import urlparse, parse_qs, urlunparse
      -from openai import OpenAI
      +# NOTE: `from openai import OpenAI` is deliberately NOT at module top — the
      +# SDK pulls ~240 ms of imports. We expose `OpenAI` as a thin proxy object
      +# that imports the SDK on first call/isinstance check. This preserves:
      +#   (a) the single in-module `OpenAI(**client_kwargs)` call site at
      +#       _create_openai_client, and
      +#   (b) `patch("run_agent.OpenAI", ...)` test patterns used by ~28 test files.
       import fire
       from datetime import datetime
       from pathlib import Path
       
       from hermes_constants import get_hermes_home
       
      +
      +_OPENAI_CLS_CACHE: Optional[type] = None
      +
      +
      +def _load_openai_cls() -> type:
      +    """Import and cache ``openai.OpenAI``."""
      +    global _OPENAI_CLS_CACHE
      +    if _OPENAI_CLS_CACHE is None:
      +        from openai import OpenAI as _cls
      +        _OPENAI_CLS_CACHE = _cls
      +    return _OPENAI_CLS_CACHE
      +
      +
      +class _OpenAIProxy:
      +    """Module-level proxy that looks like ``openai.OpenAI`` but imports lazily."""
      +
      +    __slots__ = ()
      +
      +    def __call__(self, *args, **kwargs):
      +        return _load_openai_cls()(*args, **kwargs)
      +
      +    def __instancecheck__(self, obj):
      +        return isinstance(obj, _load_openai_cls())
      +
      +    def __repr__(self):
      +        return "<lazy openai.OpenAI proxy>"
      +
      +
      +OpenAI = _OpenAIProxy()
      +
       # Load .env from ~/.hermes/.env first, then project root as dev fallback.
       # User-managed env files should override stale shell exports on restart.
       from hermes_cli.env_loader import load_hermes_dotenv
      @@ -5243,6 +5278,8 @@ def _create_openai_client(self, client_kwargs: dict, *, reason: str, shared: boo
                   keepalive_http = self._build_keepalive_http_client(client_kwargs.get("base_url", ""))
                   if keepalive_http is not None:
                       client_kwargs["http_client"] = keepalive_http
      +        # Uses the module-level `OpenAI` name, resolved lazily on first
      +        # access via __getattr__ below. Tests patch via `run_agent.OpenAI`.
               client = OpenAI(**client_kwargs)
               logger.info(
                   "OpenAI client created (%s, shared=%s) %s",
    • tools/web_tools.py
      @@ -47,7 +47,10 @@
       import asyncio
       from typing import List, Dict, Any, Optional
       import httpx
      -from firecrawl import Firecrawl
      +# NOTE: `from firecrawl import Firecrawl` is deliberately NOT at module top —
      +# the SDK pulls ~200 ms of imports (httpcore, firecrawl.v1/v2 type trees) and
      +# we only need it when the backend is actually "firecrawl". See
      +# _get_firecrawl_client() below for the lazy import.
       from agent.auxiliary_client import (
           async_call_llm,
           extract_content_or_reasoning,
      @@ -236,6 +239,8 @@ def _get_firecrawl_client():
           if _firecrawl_client is not None and _firecrawl_client_config == client_config:
               return _firecrawl_client
       
      +    # Lazy import — ~200 ms of SDK init, only paid when firecrawl is actually used.
      +    from firecrawl import Firecrawl  # noqa: E402
           _firecrawl_client = Firecrawl(**kwargs)
           _firecrawl_client_config = client_config
           return _firecrawl_client

1 PR closed without merging

From the close comments: done elsewhere 1

  • #11869 fix(cron): preflight import check with clear error on interpreter mismatch (#11610) · done elsewhere → b5128a7, #17046
Closed issues · 1
  • Closed#11610[Bug] Intermittent cron failure: ModuleNotFoundError: No module named 'openai' (Python interpreter mismatch)
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
cron
Merged PRs
#17046
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
28 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.