fix: guard aux LLM calls against None content + reasoning fallback + retry (salvage #3389)#3449
Merged
fix: guard aux LLM calls against None content + reasoning fallback + retry (salvage #3389)#3449
Conversation
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.
3 tasks
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.
3 tasks
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
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
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) returncontent=Nonewith 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:
extract_content_or_reasoning(response)inauxiliary_client.py— shared helper that:.reasoning,.reasoning_content,.reasoning_details)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— newextract_content_or_reasoning()helpertools/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 updatedtests/tools/test_llm_content_none_guard.py— 31 tests (20 original + 11 new for extract helper)Test plan
Closes #3389. Original commit by @binhnt92 preserved via cherry-pick.