diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 504b2178..922807f1 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -324,6 +324,9 @@ compression: # vision: # provider: "auto" # model: "" # e.g. "google/gemini-2.5-flash", "openai/gpt-4o" +# timeout: 30 # LLM API call timeout (seconds) +# download_timeout: 30 # Image HTTP download timeout (seconds) +# # Increase for slow connections or self-hosted image servers # # # Web page scraping / summarization + browser page text extraction # web_extract: diff --git a/gateway/run.py b/gateway/run.py index 3c0ca181..3b519304 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -3891,7 +3891,7 @@ class GatewayRunner: # Send media files for media_path in (media_files or []): try: - await adapter.send_file( + await adapter.send_document( chat_id=source.chat_id, file_path=media_path, ) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 69530761..e2503ebe 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -223,7 +223,8 @@ DEFAULT_CONFIG = { "model": "", # e.g. "google/gemini-2.5-flash", "gpt-4o" "base_url": "", # direct OpenAI-compatible endpoint (takes precedence over provider) "api_key": "", # API key for base_url (falls back to OPENAI_API_KEY) - "timeout": 30, # seconds — increase for slow local vision models + "timeout": 30, # seconds — LLM API call timeout; increase for slow local vision models + "download_timeout": 30, # seconds — image HTTP download timeout; increase for slow connections }, "web_extract": { "provider": "auto", diff --git a/tools/vision_tools.py b/tools/vision_tools.py index 47b40684..404d06a5 100644 --- a/tools/vision_tools.py +++ b/tools/vision_tools.py @@ -45,6 +45,28 @@ logger = logging.getLogger(__name__) _debug = DebugSession("vision_tools", env_var="VISION_TOOLS_DEBUG") +# Configurable HTTP download timeout for _download_image(). +# Separate from auxiliary.vision.timeout which governs the LLM API call. +# Resolution: config.yaml auxiliary.vision.download_timeout → env var → 30s default. +def _resolve_download_timeout() -> float: + env_val = os.getenv("HERMES_VISION_DOWNLOAD_TIMEOUT", "").strip() + if env_val: + try: + return float(env_val) + except ValueError: + pass + try: + from hermes_cli.config import load_config + cfg = load_config() + val = cfg.get("auxiliary", {}).get("vision", {}).get("download_timeout") + if val is not None: + return float(val) + except Exception: + pass + return 30.0 + +_VISION_DOWNLOAD_TIMEOUT = _resolve_download_timeout() + def _validate_image_url(url: str) -> bool: """ @@ -146,7 +168,7 @@ async def _download_image(image_url: str, destination: Path, max_retries: int = # Enable follow_redirects to handle image CDNs that redirect (e.g., Imgur, Picsum) # SSRF: event_hooks validates each redirect target against private IP ranges async with httpx.AsyncClient( - timeout=30.0, + timeout=_VISION_DOWNLOAD_TIMEOUT, follow_redirects=True, event_hooks={"response": [_ssrf_redirect_guard]}, ) as client: @@ -183,6 +205,10 @@ async def _download_image(image_url: str, destination: Path, max_retries: int = exc_info=True, ) + if last_error is None: + raise RuntimeError( + f"_download_image exited retry loop without attempting (max_retries={max_retries})" + ) raise last_error diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 17f15dc5..48d76dd8 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1018,7 +1018,8 @@ auxiliary: model: "" # e.g. "openai/gpt-4o", "google/gemini-2.5-flash" base_url: "" # Custom OpenAI-compatible endpoint (overrides provider) api_key: "" # API key for base_url (falls back to OPENAI_API_KEY) - timeout: 30 # seconds — increase for slow local vision models + timeout: 30 # seconds — LLM API call; increase for slow local vision models + download_timeout: 30 # seconds — image HTTP download; increase for slow connections # Web page summarization + browser page text extraction web_extract: @@ -1042,7 +1043,7 @@ auxiliary: ``` :::tip -Each auxiliary task has a configurable `timeout` (in seconds). Defaults: vision 30s, web_extract 30s, approval 30s, compression 120s. Increase these if you use slow local models for auxiliary tasks. +Each auxiliary task has a configurable `timeout` (in seconds). Defaults: vision 30s, web_extract 30s, approval 30s, compression 120s. Increase these if you use slow local models for auxiliary tasks. Vision also has a separate `download_timeout` (default 30s) for the HTTP image download — increase this for slow connections or self-hosted image servers. ::: :::info