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 any

View on GitHub (pinned to c896c09c42)

Solutions

  1. Retry the conversation turn — the error routes through the main-model fallback, so compression may succeed with the main model.
  2. 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.
  3. Inspect the provider's raw response (logs at ~/.hermes/logs/agent.log) to confirm the empty-content payload and report/fix the relay channel.
  4. 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

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


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/a24ca4a4f94ffe16. Report an issue: GitHub.