Skip to content

fix: guard aux LLM calls against None content + reasoning fallback + retry (salvage #3389)#3449

Merged
teknium1 merged 2 commits intomainfrom
hermes/hermes-a2b72b01
Mar 27, 2026
Merged

fix: guard aux LLM calls against None content + reasoning fallback + retry (salvage #3389)#3449
teknium1 merged 2 commits intomainfrom
hermes/hermes-a2b72b01

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

Summary

Salvage of #3389 by @binhnt92 with reasoning fallback and retry logic added on top.

Problem: 7 auxiliary LLM call sites crash with AttributeError: NoneType has no attribute 'strip' when reasoning models (DeepSeek-R1, Qwen-QwQ) return content=None with reasoning in structured fields.

Original fix (#3389): Added (content or "").strip() guard at all 7 sites. Prevents crash but silently returns empty string — the reasoning content is lost and no retry is attempted.

Added on top: Mirrors the main agent loop's behavior for reasoning-only responses:

  1. extract_content_or_reasoning(response) in auxiliary_client.py — shared helper that:

    • Extracts content, strips inline think/reasoning blocks
    • Falls back to structured reasoning fields (.reasoning, .reasoning_content, .reasoning_details)
    • Handles all provider formats: DeepSeek, Moonshot/Novita, OpenRouter unified
  2. Retry on empty content at each call site — sites with existing retry loops (web_tools, session_search, mixture_of_agents) reuse them; sites without (vision_tools, skills_guard, web_tools synthesis) get a simple one-retry.

Files changed

  • agent/auxiliary_client.py — new extract_content_or_reasoning() helper
  • tools/web_tools.py (2 sites), tools/vision_tools.py, tools/session_search_tool.py, tools/skills_guard.py, tools/mixture_of_agents_tool.py (2 sites) — all 7 sites updated
  • tests/tools/test_llm_content_none_guard.py — 31 tests (20 original + 11 new for extract helper)

Test plan

python -m pytest tests/tools/test_llm_content_none_guard.py -v  # 31 pass
python -m pytest tests/tools/ -q                                 # 1969 pass

Closes #3389. Original commit by @binhnt92 preserved via cherry-pick.

binhnt92 and others added 2 commits March 27, 2026 13:25
OpenAI-compatible APIs return message.content=None when a model responds
with tool calls only or reasoning-only output (e.g. DeepSeek-R1 via
OpenRouter). Calling .strip() on None raises AttributeError. Apply
(content or "").strip() guard at all 7 call sites across 5 tool files.
The original PR (#3389) guarded against None content with (x or '').strip(),
preventing crashes but silently returning empty strings when reasoning models
return content=None with reasoning in structured fields.

This adds extract_content_or_reasoning() to auxiliary_client.py which mirrors
the main agent loop's behavior:
1. Extract content, strip inline think/reasoning blocks
2. Fall back to structured reasoning fields (.reasoning, .reasoning_content,
   .reasoning_details) when content is empty
3. Each call site retries on empty content using its existing retry loop
   (or a simple one-retry for sites without retry loops)

Covers all 7 auxiliary LLM call sites: web_tools (2), vision_tools,
session_search_tool, skills_guard, mixture_of_agents_tool (2).

Added 11 tests for extract_content_or_reasoning covering all reasoning
field formats and edge cases.
@teknium1 teknium1 merged commit 6586927 into main Mar 27, 2026
2 checks passed
binhnt92 added a commit to binhnt92/hermes-agent that referenced this pull request Mar 28, 2026
… vision

_extract_relevant_content and browser_vision both access
response.choices[0].message.content without a None check. Reasoning-only
models (DeepSeek-R1, QwQ via OpenRouter) return content=None, producing
null snapshots and null vision analysis.

Apply the same (content or "").strip() guard used across the rest of the
codebase since NousResearch#3449. For _extract_relevant_content, fall back to the
truncated snapshot. For browser_vision, fall back to a descriptive message.
teknium1 pushed a commit that referenced this pull request Mar 29, 2026
… vision

Reasoning-only models (DeepSeek-R1, QwQ) return content=None, causing
null snapshots from _extract_relevant_content and null analysis from
browser_vision. Follow-up to #3449 which applied the same guard
elsewhere.

- _extract_relevant_content: falls back to truncated raw snapshot
- browser_vision: falls back to descriptive message
teknium1 added a commit that referenced this pull request Mar 29, 2026
… vision (#3642)

Salvage of PR #3532 (binhnt92). Guards browser_tool.py against None content from reasoning-only models (DeepSeek-R1, QwQ). Follow-up to #3449.

Co-Authored-By: binhnt92 <binhnt92@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants