feat: add video_analyze tool (Gemini) for local-file + URL video analysis#1
Closed
nekorytaylor666 wants to merge 192 commits into
Closed
feat: add video_analyze tool (Gemini) for local-file + URL video analysis#1nekorytaylor666 wants to merge 192 commits into
nekorytaylor666 wants to merge 192 commits into
Conversation
…ousResearch#14720) OpenAI launched GPT-5.5 on Codex today (Apr 23 2026). Adds it to the static catalog and pipes the user's OAuth access token into the openai-codex path of provider_model_ids() so /model mid-session and the gateway picker hit the live ChatGPT codex/models endpoint — new models appear for each user according to what ChatGPT actually lists for their account, without a Hermes release. Verified live: 'gpt-5.5' returns priority 0 (featured) from the endpoint, 400k context per OpenAI's launch article. 'hermes chat --provider openai-codex --model gpt-5.5' completes end-to-end. Changes: - hermes_cli/codex_models.py: add gpt-5.5 to DEFAULT_CODEX_MODELS + forward-compat - agent/model_metadata.py: 400k context length entry - hermes_cli/models.py: resolve codex OAuth token before calling get_codex_model_ids() in provider_model_ids('openai-codex')
…h#14725) Dashboard themes now control typography and layout, not just colors. Each built-in theme picks its own fonts, base size, radius, and density so switching produces visible changes beyond hue. Schema additions (per theme): - typography — fontSans, fontMono, fontDisplay, fontUrl, baseSize, lineHeight, letterSpacing. fontUrl is injected as <link> on switch so Google/Bunny/self-hosted stylesheets all work. - layout — radius (any CSS length) and density (compact | comfortable | spacious, multiplies Tailwind spacing). - colorOverrides (optional) — pin individual shadcn tokens that would otherwise derive from the palette. Built-in themes are now distinct beyond palette: - default — system stack, 15px, 0.5rem radius, comfortable - midnight — Inter + JetBrains Mono, 14px, 0.75rem, comfortable - ember — Spectral (serif) + IBM Plex Mono, 15px, 0.25rem - mono — IBM Plex Sans + Mono, 13px, 0 radius, compact - cyberpunk— Share Tech Mono everywhere, 14px, 0 radius, compact - rose — Fraunces (serif) + DM Mono, 16px, 1rem, spacious Also fixes two bugs: 1. Custom user themes silently fell back to default. ThemeProvider only applied BUILTIN_THEMES[name], so YAML files in ~/.hermes/dashboard-themes/ showed in the picker but did nothing. Server now ships the full normalised definition; client applies it. 2. Docs documented a 21-token flat colors schema that never matched the code (applyPalette reads a 3-layer palette). Rewrote the Themes section against the actual shape. Implementation: - web/src/themes/types.ts: extend DashboardTheme with typography, layout, colorOverrides; ThemeListEntry carries optional definition. - web/src/themes/presets.ts: 6 built-ins with distinct typography+layout. - web/src/themes/context.tsx: applyTheme() writes palette+typography+ layout+overrides as CSS vars, injects fontUrl stylesheet, fixes the fallback-to-default bug via resolveTheme(name). - web/src/index.css: html/body/code read the new theme-font vars; --radius-sm/md/lg/xl derive from --theme-radius; --spacing scales with --theme-spacing-mul so Tailwind utilities shift with density. - hermes_cli/web_server.py: _normalise_theme_definition() parses loose YAML (bare hex strings, partial blocks) into the canonical wire shape; /api/dashboard/themes ships full definitions for user themes. - tests/hermes_cli/test_web_server.py: 16 new tests covering the normaliser and discovery (rejection cases, clamping, defaults). - website/docs/user-guide/features/web-dashboard.md: rewrite Themes section with real schema, per-model tables, full YAML example.
…ox-escaping test (NousResearch#14729) A test in tests/agent/test_credential_pool.py (test_try_refresh_current_updates_only_current_entry) monkeypatched refresh_codex_oauth_pure() to return the literal fixture strings 'access-new'/'refresh-new', then executed the real production code path in agent/credential_pool.py::try_refresh_current which calls _sync_device_code_entry_to_auth_store → _save_provider_state → writes to `providers.openai-codex.tokens`. That writer resolves the target via get_hermes_home()/auth.json. If the test ran with HERMES_HOME unset (direct pytest invocation, IDE runner bypassing conftest discovery, or any other sandbox escape), it would overwrite the real user's auth store with the fixture strings. Observed in the wild: Teknium's ~/.hermes/auth.json providers.openai-codex.tokens held 'access-new'/'refresh-new' for five days. His CLI kept working because the credential_pool entries still held real JWTs, but `hermes model`'s live discovery path (which reads via resolve_codex_runtime_credentials → _read_codex_tokens → providers.tokens) was silently 401-ing. Fixes: - Delete test_try_refresh_current_updates_only_current_entry. It was the only test that exercised a writer hitting providers.openai-codex.tokens with literal stub tokens. The entry-level rotation behavior it asserted is still covered by test_mark_exhausted_and_rotate_persists_status above. - Add a seat belt in hermes_cli.auth._auth_file_path(): if PYTEST_CURRENT_TEST is set AND the resolved path equals the real ~/.hermes/auth.json, raise with a clear message. In production (no PYTEST_CURRENT_TEST), a single dict lookup. Any future test that forgets to monkeypatch HERMES_HOME fails loudly instead of corrupting the user's credentials. Validation: - production (no PYTEST_CURRENT_TEST): returns real path, unchanged behavior - pytest + HERMES_HOME unset (points at real home): raises with message - pytest + HERMES_HOME=/tmp/...: returns tmp path, tests pass normally
… authoritative (NousResearch#14732) Previously delegate_task exposed 'max_iterations' in its JSON schema and used `max_iterations or default_max_iter` — so a model guessing conservatively (or copy-pasting a docstring hint like 'Only set lower for simple tasks') could silently shrink a subagent's budget below the user's configured delegation.max_iterations. One such call this session capped a deep forensic audit at 40 iterations while the user's config was set to 250. Changes: - Drop 'max_iterations' from DELEGATE_TASK_SCHEMA['parameters']['properties']. Models can no longer emit it. - In delegate_task(): ignore any caller-supplied max_iterations, always use delegation.max_iterations from config. Log at debug if a stale schema or internal caller still passes one through. - Keep the Python kwarg on the function signature for internal callers (_build_child_agent tests pass it through the plumbing layer). - Update test_schema_valid to assert the param is now absent (intentional contract change, not a change-detector).
…in timeout (NousResearch#14728) Closes NousResearch#8202. Root cause: stop() reclaimed tool-call bash/sleep children only at the very end of the shutdown sequence — after a 60s drain, 5s interrupt grace, and per-adapter disconnect. Under systemd (TimeoutStopSec bounded by drain_timeout), that meant the cgroup SIGKILL escalation fired first, and systemd reaped the bash/sleep children instead of us. Fix: - Extract tool-subprocess cleanup into a local helper _kill_tool_subprocesses() in _stop_impl(). - Invoke it eagerly right after _interrupt_running_agents() on the drain-timeout path, before adapter disconnect. - Keep the existing catch-all call at the end for the graceful path and defense in depth against mid-teardown respawns. - Bump generated systemd unit TimeoutStopSec to drain_timeout + 30s so cleanup + disconnect + DB close has headroom above the drain budget, matching the 'subprocess timeout > TimeoutStopSec + margin' rule from the skill. Tests: - New: test_gateway_stop_kills_tool_subprocesses_before_adapter_disconnect_on_timeout asserts kill_all() runs before disconnect() when drain times out. - New: test_gateway_stop_kills_tool_subprocesses_on_graceful_path guards that the final catch-all still fires when drain succeeds (regression guard against accidental removal during refactor). - Updated: existing systemd unit generator tests expect TimeoutStopSec=90 (= 60s drain + 30s headroom) with explanatory comment.
…ies (NousResearch#14730) Closes NousResearch#11616. The agent's API retry loop hardcoded max_retries = 3, so users with fallback providers on flaky primaries burned through ~3 × provider timeout (e.g. 3 × 180s = 9 minutes) before their fallback chain got a chance to kick in. Expose a new config key: agent: api_max_retries: 3 # default unchanged Set it to 1 for fast failover when you have fallback providers, or raise it if you prefer longer tolerance on a single provider. Values < 1 are clamped to 1 (single attempt, no retry); non-integer values fall back to the default. This wraps the Hermes-level retry loop only — the OpenAI SDK's own low-level retries (max_retries=2 default) still run beneath this for transient network errors. Changes: - hermes_cli/config.py: add agent.api_max_retries default 3 with comment. - run_agent.py: read self._api_max_retries in AIAgent.__init__; replace hardcoded max_retries = 3 in the retry loop with self._api_max_retries. - cli-config.yaml.example: documented example entry. - hermes_cli/tips.py: discoverable tip line. - tests/run_agent/test_api_max_retries_config.py: 4 tests covering default, override, clamp-to-one, and invalid-value fallback.
…ousResearch#14736) cmd_update no longer SIGKILLs in-flight agent runs, and users get 'still working' status every 3 min instead of 10. Two long-standing sources of '@user — agent gives up mid-task' reports on Telegram and other gateways. Drain-aware update: - New helper hermes_cli.gateway._graceful_restart_via_sigusr1(pid, drain_timeout) sends SIGUSR1 to the gateway and polls os.kill(pid, 0) until the process exits or the budget expires. - cmd_update's systemd loop now reads MainPID via 'systemctl show --property=MainPID --value' and tries the graceful path first. The gateway's existing SIGUSR1 handler -> request_restart(via_service= True) -> drain -> exit(75) is wired in gateway/run.py and is respawned by systemd's Restart=on-failure (and the explicit RestartForceExitStatus=75 on newer units). - Falls back to 'systemctl restart' when MainPID is unknown, the drain budget elapses, or the unit doesn't respawn after exit (older units missing Restart=on-failure). Old install behavior preserved. - Drain budget = max(restart_drain_timeout, 30s) + 15s margin so the drain loop in run_agent + final exit have room before fallback fires. Composes with NousResearch#14728's tool-subprocess reaping. Notification interval: - agent.gateway_notify_interval default 600 -> 180. - HERMES_AGENT_NOTIFY_INTERVAL env-var fallback in gateway/run.py matched. - 9-minute weak-model spinning runs now ping at 3 min and 6 min instead of 27 seconds before completion, removing the 'is the bot dead?' reflex that drives gateway-restart cycles. Tests: - Two new tests in tests/hermes_cli/test_update_gateway_restart.py: one asserts SIGUSR1 is sent and 'systemctl restart' is NOT called when MainPID is known and the helper succeeds; one asserts the fallback fires when the helper returns False. - E2E: spawned detached bash processes confirm the helper returns True on SIGUSR1-handling exit (~0.5s) and False on SIGUSR1-ignoring processes (timeout). Verified non-existent PID and pid=0 edge cases. - 41/41 in test_update_gateway_restart.py (was 39, +2 new). - 154/154 in shutdown-related suites including NousResearch#14728's new tests. Reported by @geoffwellman and @ANT_1515 on X.
The original regex only matched relative paths (./foo/.env or bare .env), so the exact command from the bug report — `cp /opt/data/.env.local /opt/data/.env` — did not trigger approval. Broaden the leading-path prefix to accept an absolute leading slash alongside ./ and ../, and add regressions for the bug-report command and its redirection variant.
For release-notes attribution of PR NousResearch#9170 (MiniMax context preservation).
float(os.getenv(...)) at module level raises ValueError on any non-numeric value, crashing the web server at import before it starts. Wrap in try/except with a warning log and fallback to 3.0s.
…estart - _stdio_pids: set → Dict[int,str] tracks pid→server_name - SIGTERM-first with 2s grace before SIGKILL escalation - hasattr guard for SIGKILL on platforms without it - Updated tests for dict-based tracking and 3-phase kill sequence
Replaces blind tree.sync() on every Discord reconnect with a diff-based reconcile. In safe mode (default), fetch existing global commands, compare desired vs existing payloads, skip unchanged, PATCH changed, recreate when non-patchable metadata differs, POST missing, and delete stale commands one-by-one. Keeps 'bulk' for legacy behavior and 'off' to skip startup sync entirely. Fixes restart-heavy workflows that burn Discord's command write budget and can surface 429s when iterating on native slash commands. Env var: DISCORD_COMMAND_SYNC_POLICY (safe|bulk|off), default 'safe'. Co-authored-by: Codex <codex@openai.invalid>
…lize contexts Follow-up to Magaav's safe sync policy. Two gaps in the canonicalizer caused false diffs or silent drift: 1. discord.py's AppCommand.to_dict() omits nsfw, dm_permission, and default_member_permissions — those live only on attributes. The canonicalizer was reading them via payload.get() and getting defaults (False/True/None), while the desired side from Command.to_dict(tree) had the real values. Any command using non-default permissions false-diffed on every startup. Pull them from the AppCommand attributes via _existing_command_to_payload(). 2. contexts and integration_types weren't canonicalized at all, so drift in either was silently ignored. Added both to _canonicalize_app_command_payload (sorted for stable compare). Also normalized default_member_permissions to str-or-None since the server emits strings but discord.py stores ints locally. Added regression tests for both gaps.
…test runner (NousResearch#14758) - Drop broken tinker-atropos submodule instructions: no .gitmodules exists, tinker-atropos/ is empty, and atroposlib + tinker are regular pip deps in pyproject.toml pulled in by .[all,dev]. Replace with a one-line note. - CLI vs Messaging table: /skills is cli_only=True in COMMAND_REGISTRY, so remove it from the messaging column. /<skill-name> still works there. - Point contributors at scripts/run_tests.sh (the canonical runner enforcing CI-parity env) instead of bare pytest.
…lls sections (NousResearch#14763) Fixes several outright-wrong facts and gaps vs current main: - venv activation: .venv is preferred, venv is fallback (per run_tests.sh) - AIAgent default model is "" (empty, resolved from config), not hardcoded opus - Test suite is ~15k tests / ~700 files, not ~3000 - tools/mcp_tool.py is 2.6k LOC, not 1050 - Remove stale "currently 5" config_version note; the real bump-trigger rule is migration-only, not every new key - Remove MESSAGING_CWD as the messaging cwd — it's been removed in favor of terminal.cwd in config.yaml (gateway bridges to TERMINAL_CWD env var) - .env is secrets-only; non-secret settings belong in config.yaml - simple_term_menu pitfall: existing sites are legacy fallback, rule is no new usage Incomplete/missing sections filled in: - Gateway platforms list updated to reflect actual adapters (matrix, mattermost, email, sms, dingtalk, wecom, weixin, feishu, bluebubbles, webhook, api_server, etc.) - New 'Plugins' section covering general plugins, memory-provider plugins, and dashboard/context-engine/image-gen plugin directories — including the May 2026 rule that plugins must not touch core files - New 'Skills' section covering skills/ vs optional-skills/ split and SKILL.md frontmatter fields - Logs section pointing at ~/.hermes/logs/ and 'hermes logs' CLI - Prompt-cache policy now explicitly mentions --now / deferred slash-command invalidation pattern - Two new pitfalls: gateway two-guard dispatch rule, squash-merge-from-stale branch silent revert, don't-wire-dead-code rule Tree layout trimmed to load-bearing entry points — per-file subtrees were ~70% stale so replaced with directory-level notes pointing readers at the filesystem as the source of truth.
Add xAI as a plugin-based image generation backend using grok-imagine-image. Follows the existing ImageGenProvider ABC pattern used by OpenAI and FAL. Changes: - plugins/image_gen/xai/__init__.py: xAI provider implementation - Uses xAI /images/generations endpoint - Supports text-to-image and image editing with reference images - Multiple aspect ratios (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3) - Multiple resolutions (1K, 2K) - Base64 output saved to cache - Config via config.yaml image_gen.xai section - plugins/image_gen/xai/plugin.yaml: plugin metadata - tests/plugins/image_gen/test_xai_provider.py: 19 unit tests - Provider class (name, display_name, is_available, list_models, setup_schema) - Config (default model, resolution, custom model) - Generate (missing key, success b64/url, API error, timeout, empty response, reference images, auth header) - Registration Requires XAI_API_KEY in ~/.hermes/.env. To use: set image_gen.provider: xai in config.yaml.
The agent-facing image_generate tool only passes prompt + aspect_ratio to provider.generate() (see tools/image_generation_tool.py:953). The editing block (reference_images / edit_image kwargs) could never fire from the tool surface, and the xAI edits endpoint is /images/edits with a different payload shape anyway — not /images/generations as submitted. - Remove reference_images / edit_image kwargs handling from generate() - Remove matching test_with_reference_images case - Update docstring + plugin.yaml description to text-to-image only - Surface resolution in the success extras Follow-up to PR NousResearch#14547. Tests: 18/18 pass.
…max_tokens to OpenRouter (NousResearch#6621)
…ariant - AUTHOR_MAP entry for 130918800+devorun for NousResearch#6636 attribution - test_moa_defaults: was a change-detector tied to the exact frontier model list — flips red every OpenRouter churn. Rewritten as an invariant (non-empty, valid vendor/model slugs).
Add event hook to httpx.AsyncClient in MCP HTTP transport that strips Authorization headers when a redirect targets a different origin, preventing credential leakage to third-party servers.
…res (NousResearch#13383) Streamable HTTP MCP servers may garbage-collect their server-side session state while the OAuth token remains valid — idle TTL, server restart, pod rotation, etc. Before this fix, the tool-call handler treated the resulting "Invalid or expired session" error as a plain tool failure with no recovery path, so **every subsequent call on the affected server failed until the gateway was manually restarted**. Reporter: NousResearch#13383. The OAuth-based recovery path (``_handle_auth_error_and_retry``) already exists for 401s, but it only fires on auth errors. Session expiry slipped through because the access token is still valid — nothing 401'd, so the existing recovery branch was skipped. Fix --- Add a sibling function ``_handle_session_expired_and_retry`` that detects MCP session-expiry via ``_is_session_expired_error`` (a narrow allow-list of known-stable substrings: ``"invalid or expired session"``, ``"session expired"``, ``"session not found"``, ``"unknown session"``, etc.) and then uses the existing transport reconnect mechanism: * Sets ``MCPServerTask._reconnect_event`` — the server task's lifecycle loop already interprets this as "tear down the current ``streamablehttp_client`` + ``ClientSession`` and rebuild them, reusing the existing OAuth provider instance". * Waits up to 15 s for the new session to come back ready. * Retries the original call once. If the retry succeeds, returns its result and resets the circuit-breaker error count. If the retry raises, or if the reconnect doesn't ready in time, falls through to the caller's generic error path. Unlike the 401 path, this does **not** call ``handle_401`` — the access token is already valid and running an OAuth refresh would be a pointless round-trip. All 5 MCP handlers (``call_tool``, ``list_resources``, ``read_resource``, ``list_prompts``, ``get_prompt``) now consult both recovery paths before falling through: recovered = _handle_auth_error_and_retry(...) # 401 path if recovered is not None: return recovered recovered = _handle_session_expired_and_retry(...) # new if recovered is not None: return recovered # generic error response Narrow scope — explicitly not changed ------------------------------------- * **Detection is string-based on a 5-entry allow-list.** The MCP SDK wraps JSON-RPC errors in ``McpError`` whose exception type + attributes vary across SDK versions, so matching on message substrings is the durable path. Kept narrow to avoid false positives — a regular ``RuntimeError("Tool failed")`` will NOT trigger spurious reconnects (pinned by ``test_is_session_expired_rejects_unrelated_errors``). * **No change to the existing 401 recovery flow.** The new path is consulted only after the auth path declines (returns ``None``). * **Retry count stays at 1.** If the reconnect-then-retry also fails, we don't loop — the error surfaces normally so the model sees a failed tool call rather than a hang. * **``InterruptedError`` is explicitly excluded** from session-expired detection so user-cancel signals always short-circuit the same way they did before (pinned by ``test_is_session_expired_rejects_interrupted_error``). Regression coverage ------------------- ``tests/tools/test_mcp_tool_session_expired.py`` (new, 16 cases): Unit tests for ``_is_session_expired_error``: * ``test_is_session_expired_detects_invalid_or_expired_session`` — reporter's exact wpcom-mcp text. * ``test_is_session_expired_detects_expired_session_variant`` — "Session expired" / "expired session" variants. * ``test_is_session_expired_detects_session_not_found`` — server GC variant ("session not found", "unknown session"). * ``test_is_session_expired_is_case_insensitive``. * ``test_is_session_expired_rejects_unrelated_errors`` — narrow-scope canary: random RuntimeError / ValueError / 401 don't trigger. * ``test_is_session_expired_rejects_interrupted_error`` — user cancel must never route through reconnect. * ``test_is_session_expired_rejects_empty_message``. Handler integration tests: * ``test_call_tool_handler_reconnects_on_session_expired`` — reporter's full repro: first call raises "Invalid or expired session", handler signals ``_reconnect_event``, retries once, returns the retry's success result with no ``error`` key. * ``test_call_tool_handler_non_session_expired_error_falls_through`` — preserved-behaviour canary: random tool failures do NOT trigger reconnect. * ``test_session_expired_handler_returns_none_without_loop`` — defensive: cold-start / shutdown race. * ``test_session_expired_handler_returns_none_without_server_record`` — torn-down server falls through cleanly. * ``test_session_expired_handler_returns_none_when_retry_also_fails`` — no retry loop on repeated failure. Parametrised across all 4 non-``tools/call`` handlers: * ``test_non_tool_handlers_also_reconnect_on_session_expired`` [list_resources / read_resource / list_prompts / get_prompt]. **15 of 16 fail on clean ``origin/main`` (``6fb69229``)** with ``ImportError: cannot import name '_is_session_expired_error'`` — the fix's surface symbols don't exist there yet. The 1 passing test is an ordering artefact of pytest-xdist worker collection. Validation ---------- ``source venv/bin/activate && python -m pytest tests/tools/test_mcp_tool_session_expired.py -q`` → **16 passed**. Broader MCP suite (5 files: ``test_mcp_tool.py``, ``test_mcp_tool_401_handling.py``, ``test_mcp_tool_session_expired.py``, ``test_mcp_reconnect_signal.py``, ``test_mcp_oauth.py``) → **230 passed, 0 regressions**. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously 'hermes auth spotify' crashed with 'HERMES_SPOTIFY_CLIENT_ID is required' if the user hadn't manually created a Spotify developer app and set env vars. Now the command detects a missing client_id and walks the user through the one-time app registration inline: - Opens https://developer.spotify.com/dashboard in the browser - Tells the user exactly what to paste into the Spotify form (including the correct default redirect URI, 127.0.0.1:43827) - Prompts for the Client ID - Persists HERMES_SPOTIFY_CLIENT_ID to ~/.hermes/.env so subsequent runs skip the wizard - Continues straight into the PKCE OAuth flow Also prints the docs URL at both the start of the wizard and the end of a successful login so users can find the full guide. Adds website/docs/user-guide/features/spotify.md with the complete setup walkthrough, tool reference, and troubleshooting, and wires it into the sidebar under User Guide > Features > Advanced. Fixes a stale redirect URI default in the hermes_cli/tools_config.py TOOL_CATEGORIES entry (was 8888/callback from the PR description instead of the actual DEFAULT_SPOTIFY_REDIRECT_URI value 43827/spotify/callback defined in auth.py).
…Research#15124) Claude-style and some Anthropic-tuned models occasionally emit tool names as class-like identifiers: TodoTool_tool, Patch_tool, BrowserClick_tool, PatchTool. These failed strict-dict lookup in valid_tool_names and triggered the 'Unknown tool' self-correction loop, wasting a full turn of iteration and tokens. _repair_tool_call already handled lowercase / separator / fuzzy matches but couldn't bridge the CamelCase-to-snake_case gap or the trailing '_tool' suffix that Claude sometimes tacks on. Extend it with two bounded normalization passes: 1. CamelCase -> snake_case (via regex lookbehind). 2. Strip trailing _tool / -tool / tool suffix (case-insensitive, applied twice so TodoTool_tool reduces all the way: strip _tool -> TodoTool, snake -> todo_tool, strip 'tool' -> todo). Cheap fast-paths (lowercase / separator-normalized) still run first so the common case stays zero-cost. Fuzzy match remains the last resort unchanged. Tests: tests/run_agent/test_repair_tool_call_name.py covers the three original reports (TodoTool_tool, Patch_tool, BrowserClick_tool), plus PatchTool, WriteFileTool, ReadFile_tool, write-file_Tool, patch-tool, and edge cases (empty, None, '_tool' alone, genuinely unknown names). 18 new tests + 17 existing arg-repair tests = 35/35 pass. Closes NousResearch#14784
…NousResearch#15133) Two small fixes triggered by a support report where the user saw a cryptic 'HTTP 400 - Error 400 (Bad Request)!!1' (Google's GFE HTML error page, not a real API error) on every gemini-2.5-pro request. The underlying cause was an empty GOOGLE_API_KEY / GEMINI_API_KEY, but nothing in our output made that diagnosable: 1. hermes_cli/dump.py: the api_keys section enumerated 23 providers but omitted Google entirely, so users had no way to verify from 'hermes dump' whether the key was set. Added GOOGLE_API_KEY and GEMINI_API_KEY rows. 2. agent/gemini_native_adapter.py: GeminiNativeClient.__init__ accepted an empty/whitespace api_key and stamped it into the x-goog-api-key header, which made Google's frontend return a generic HTML 400 long before the request reached the Generative Language backend. Now we raise RuntimeError at construction with an actionable message pointing at GOOGLE_API_KEY/GEMINI_API_KEY and aistudio.google.com. Added a regression test that covers '', ' ', and None.
…event non-retryable abort ssl.SSLError (and its subclass ssl.SSLCertVerificationError) inherits from OSError *and* ValueError via Python's MRO. The is_local_validation_error check used isinstance(api_error, (ValueError, TypeError)) to detect programming bugs that should abort immediately — but this inadvertently caught ssl.SSLError, treating a TLS transport failure as a non-retryable client error. The error classifier already maps SSLCertVerificationError to FailoverReason.timeout with retryable=True (its type name is in _TRANSPORT_ERROR_TYPES), but the inline isinstance guard was overriding that classification and triggering an unnecessary abort. Fix: add ssl.SSLError to the exclusion list alongside the existing UnicodeEncodeError carve-out so TLS errors fall through to the classifier's retryable path. Closes NousResearch#14367
Try to activate fallback model after errors was calling get_model_context_length() without the config_context_length parameter, causing it to fall through to DEFAULT_FALLBACK_CONTEXT (128K) even when config.yaml has an explicit model.context_length value (e.g. 204800 for MiniMax-M2.7). This mirrors the fix already present in switch_model() at line 1988, which correctly passes config_context_length. The fallback path was missed. Fixes: context_length forced to 128K on fallback activation
… fails When the primary provider raises AuthError (expired OAuth token, revoked API key), the error was re-raised before AIAgent was created, so fallback_model was never consulted. Now both gateway/run.py and cron/scheduler.py catch AuthError specifically and attempt to resolve credentials from the fallback_providers/fallback_model config chain before propagating the error. Closes NousResearch#7230
…matrix, troubleshooting (NousResearch#15135) The initial Spotify docs page shipped in NousResearch#15130 was a setup guide. This expands it into a full feature reference: - Per-tool parameter table for all 9 tools, extracted from the real schemas in tools/spotify_tool.py (actions, required/optional args, premium gating). - Free vs Premium feature matrix — which actions work on which tier, so Free users don't assume Spotify tools are useless to them. - Active-device prerequisite called out at the top; this is the #1 cause of '403 no active device' reports for every Spotify integration. - SSH / headless section explaining that browser auto-open is skipped when SSH_CLIENT/SSH_TTY is set, and how to tunnel the callback port. - Token lifecycle: refresh on 401, persistence across restarts, how to revoke server-side via spotify.com/account/apps. - Example prompt list so users know what to ask the agent. - Troubleshooting expanded: no-active-device, Premium-required, 204 now_playing, INVALID_CLIENT, 429, 401 refresh-revoked, wizard not opening browser. - 'Where things live' table mapping auth.json / .env / Spotify app. Verified with 'node scripts/prebuild.mjs && npx docusaurus build' — page compiles, no new warnings.
Regression test for NousResearch#14981. Verifies that _session_expiry_watcher fires on_session_finalize for each session swept out of the store, matching the contract documented for /new, /reset, CLI shutdown, and gateway stop. Verified the test fails cleanly on pre-fix code (hook call list missing sess-expired) and passes with the fix applied.
Sync/upstream 2026 04 24
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace every workflow's `on:` block with `workflow_dispatch` only, so nothing runs automatically on push/PR/cron. Jobs stay intact and can still be invoked manually from the Actions tab. Avoids running NousResearch-specific pipelines (deploy-site, docker-publish to their registry, contributor-check against their roster) that would otherwise fail on every push. Re-enable a workflow by restoring its original `on:` block from upstream NousResearch/hermes-agent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # .gitignore # web/src/App.tsx
…gsfield - Add perplexitycli binary and integrate Perplexity as a web search backend - Update prompt builder subscription text from FAL to Higgsfield
…sfield - Update all GitHub links from NousResearch to higgsfield-ai org - Replace FAL with Higgsfield as default image generation provider - Add bundled CLI binary sync in setup-hermes.sh and hermes update - Upgrade default SOUL.md with Higgsfield and social media CLI references - Remove image_generate tool in favor of higgsfieldcli via terminal - Add RL Training Guide and Skills How-To docs - Update tests to match new provider and toolset changes
Adds tools/video_tools.py exposing `video_analyze` — accepts a local file path or URL and returns Gemini's analysis text. YouTube URLs are sent natively; TikTok/Instagram/Vimeo/X/Facebook/Reddit are downloaded via yt-dlp; other HTTP URLs stream directly with a yt-dlp fallback on 401/403/404. Uploaded files use the resumable File API so large videos bypass proxy body limits, and are deleted post-analysis. Model, API key, proxy base URL, and size cap are all configurable via env vars (HERMES_VIDEO_MODEL, GOOGLE_GENERATIVE_AI_API_KEY, GOOGLE_GENERATIVE_AI_BASE_URL, HERMES_VIDEO_MAX_SIZE_MB). The tool is registered under the `vision` toolset and wired into the hermes-cli, hermes-acp, and hermes-api-server toolsets. Includes unit tests for URL classification, mime mapping, env resolution, availability gating (key-only / proxy-only / neither), and input validation. Also refreshes the built-in-discovery allow-list in test_registry.py (was already stale for tools.spotify_tool).
Owner
Author
|
Superseded by https://github.com/higgsfield-ai/hermes-agent/pull/5 |
nekorytaylor666
pushed a commit
that referenced
this pull request
Apr 24, 2026
…matrix, troubleshooting (NousResearch#15135) The initial Spotify docs page shipped in NousResearch#15130 was a setup guide. This expands it into a full feature reference: - Per-tool parameter table for all 9 tools, extracted from the real schemas in tools/spotify_tool.py (actions, required/optional args, premium gating). - Free vs Premium feature matrix — which actions work on which tier, so Free users don't assume Spotify tools are useless to them. - Active-device prerequisite called out at the top; this is the #1 cause of '403 no active device' reports for every Spotify integration. - SSH / headless section explaining that browser auto-open is skipped when SSH_CLIENT/SSH_TTY is set, and how to tunnel the callback port. - Token lifecycle: refresh on 401, persistence across restarts, how to revoke server-side via spotify.com/account/apps. - Example prompt list so users know what to ask the agent. - Troubleshooting expanded: no-active-device, Premium-required, 204 now_playing, INVALID_CLIENT, 429, 401 refresh-revoked, wizard not opening browser. - 'Where things live' table mapping auth.json / .env / Spotify app. Verified with 'node scripts/prebuild.mjs && npx docusaurus build' — page compiles, no new warnings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
video_analyzetool (tools/video_tools.py) — accepts local file path or URL, routes YouTube natively, downloads TikTok/Instagram/etc. via yt-dlp, streams direct MP4 URLs with yt-dlp fallback on 401/403/404.HERMES_VIDEO_MODEL(defaultgemini-2.5-pro),GOOGLE_GENERATIVE_AI_API_KEY,GOOGLE_GENERATIVE_AI_BASE_URL(proxy to conceal keys),HERMES_VIDEO_MAX_SIZE_MB(default 500).visiontoolset; wired into_HERMES_CORE_TOOLS,hermes-acp, andhermes-api-server.test_registry.py(was already missingtools.spotify_toolonmain).Motivation
Hermes already had
vision_analyzefor images; video needed the same affordance. The previousskills/research/trend-picker/scripts/analyze_video.pywas marked DEPRECATED. This tool lifts that logic into a first-class, proxy-aware, async registry tool so any agent session can analyze a video from chat.Test plan
uv run python -m pytest tests/tools/test_video_tools.py -q— 34 passuv run python -m pytest tests/tools/test_registry.py tests/test_toolsets.py tests/tools/test_vision_tools.py tests/gateway/test_api_server_toolset.py -q— 165 pass, 6 skipped.mp4file (upload path).