NousResearch/hermes-agent · error · RuntimeError
Context compression LLM returned empty content (provider={pr
Error message
Context compression LLM returned empty content (provider={provider} model={model}) What it means
The auxiliary LLM used for context compression returned an HTTP 200 response whose content field is empty or whitespace-only. This is raised in agent/context_compressor.py because some OpenAI-compatible proxies (cmkey.cn, one-api channels) return a well-formed response with empty content instead of an error. Treating it as a failure routes it through the same main-model fallback and cooldown machinery as a transport error, rather than silently replacing the compacted conversation turns with an empty summary (which would make the model forget the in-progress task, per issues #11978/#11914).
Source
Thrown at agent/context_compressor.py:4267
if isinstance(message, dict):
content = message.get("content")
else:
content = getattr(message, "content", message)
# Handle cases where content is not a string (e.g., dict from llama.cpp)
if not isinstance(content, str):
content = str(content) if content else ""
# Some OpenAI-compatible proxies (e.g. cmkey.cn, one-api channels)
# return a well-formed HTTP 200 with an empty or whitespace-only
# ``content`` instead of an error or empty ``choices``. That payload
# passes ``_validate_llm_response`` (a ``message`` exists), so it
# reaches here and would otherwise be stored as a prefix-only
# summary with no body — silently wiping the compacted turns and
# making the model forget the in-progress task (#11978, #11914).
# Treat empty content as a failure so it routes through the same
# main-model fallback + cooldown machinery as a transport error,
# rather than replacing real context with an empty summary.
if not content.strip():
raise RuntimeError(
"Context compression LLM returned empty content "
f"(provider={self.provider or 'auto'} "
f"model={self.summary_model or self.model})"
)
# Strip reasoning blocks the summarizer model may have emitted
# (<think>...</think> etc. from thinking models like MiniMax,
# DeepSeek, QwQ). Without this the trace is stored in
# _previous_summary, injected into the conversation, AND fed back
# into every subsequent iterative-update prompt — compounding
# token bloat across compactions. Mirrors title_generator.py.
from agent.agent_runtime_helpers import strip_think_blocks
stripped = strip_think_blocks(None, content).strip()
if stripped:
content = stripped
# Redact the summary output as well — the summarizer LLM may
# ignore prompt instructions and echo back secrets verbatim.
summary = _redact_compaction_text(content.strip())
# P2 ghost-skill defense (#32106): deterministically restore anyView on GitHub (pinned to c896c09c42)
Solutions
- Retry the conversation turn — the error routes through the main-model fallback, so compression may succeed with the main model.
- Switch the compression model to a reliable provider (set auxiliary.compression.model in config.yaml) if your endpoint is a relay that frequently returns empty bodies.
- Inspect the provider's raw response (logs at ~/.hermes/logs/agent.log) to confirm the empty-content payload and report/fix the relay channel.
- If the summarizer is a thinking model (DeepSeek/QwQ/MiniMax), pick one that reliably emits visible summary text rather than only reasoning blocks.
Example fix
# config.yaml — point compression at a stable provider instead of a flaky relay
# before
auxiliary:
compression:
model: "some-relay-hosted-model"
# after
auxiliary:
compression:
model: "gemini-2.5-flash" Defensive patterns
Strategy: fallback
Try / catch
# Already handled internally: the compressor routes this through the # main-model fallback + cooldown machinery. Callers of chat()/run_conversation() # need no extra handling — just let the turn proceed and the fallback summarizes.
Prevention
- Avoid flaky OpenAI-compatible relays for auxiliary.compression.model
- Use a stable, first-party provider for the compression model
- Keep ~/.hermes/logs/agent.log accessible to confirm empty-body responses when suspected
When it happens
Trigger: Calling context compression with a provider whose endpoint returns a 200 with empty/whitespace-only content — typically a flaky OpenAI-compatible proxy or relay channel. Happens after _validate_llm_response passes (a message object exists) but before the content is stripped of <think> blocks and stored in _previous_summary.
Common situations: Using a third-party API relay/aggregator (one-api, cmkey.cn style channels) as the compression model provider; a summarizer model that emits only reasoning tags with no visible content; transient upstream provider truncation. Config: auxiliary.compression.model pointed at an unstable endpoint.
Related errors
- Reached the gateway over HTTP, but the live WebSocket (/api/
- Anthropic refresh response was missing access_token
- Malformed proxy environment variable {key}={value!r}. Fix or
- Auxiliary compression model {aux_model} has a context window
- gemini_invalid_json
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/a24ca4a4f94ffe16.
Report an issue: GitHub.