{"record":{"id":"a24ca4a4f94ffe16","repo":"NousResearch/hermes-agent","slug":"context-compression-llm-returned-empty-content-pr","errorCode":null,"errorMessage":"Context compression LLM returned empty content (provider={provider} model={model})","messagePattern":"Context compression LLM returned empty content \\(provider=(.+?) model=(.+?)\\)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/context_compressor.py","lineNumber":4267,"sourceCode":"            if isinstance(message, dict):\n                content = message.get(\"content\")\n            else:\n                content = getattr(message, \"content\", message)\n            # Handle cases where content is not a string (e.g., dict from llama.cpp)\n            if not isinstance(content, str):\n                content = str(content) if content else \"\"\n            # Some OpenAI-compatible proxies (e.g. cmkey.cn, one-api channels)\n            # return a well-formed HTTP 200 with an empty or whitespace-only\n            # ``content`` instead of an error or empty ``choices``. That payload\n            # passes ``_validate_llm_response`` (a ``message`` exists), so it\n            # reaches here and would otherwise be stored as a prefix-only\n            # summary with no body — silently wiping the compacted turns and\n            # making the model forget the in-progress task (#11978, #11914).\n            # Treat empty content as a failure so it routes through the same\n            # main-model fallback + cooldown machinery as a transport error,\n            # rather than replacing real context with an empty summary.\n            if not content.strip():\n                raise RuntimeError(\n                    \"Context compression LLM returned empty content \"\n                    f\"(provider={self.provider or 'auto'} \"\n                    f\"model={self.summary_model or self.model})\"\n                )\n            # Strip reasoning blocks the summarizer model may have emitted\n            # (<think>...</think> etc. from thinking models like MiniMax,\n            # DeepSeek, QwQ). Without this the trace is stored in\n            # _previous_summary, injected into the conversation, AND fed back\n            # into every subsequent iterative-update prompt — compounding\n            # token bloat across compactions. Mirrors title_generator.py.\n            from agent.agent_runtime_helpers import strip_think_blocks\n            stripped = strip_think_blocks(None, content).strip()\n            if stripped:\n                content = stripped\n            # Redact the summary output as well — the summarizer LLM may\n            # ignore prompt instructions and echo back secrets verbatim.\n            summary = _redact_compaction_text(content.strip())\n            # P2 ghost-skill defense (#32106): deterministically restore any","sourceCodeStart":4249,"sourceCodeEnd":4285,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/context_compressor.py#L4249-L4285","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# config.yaml — point compression at a stable provider instead of a flaky relay\n# before\nauxiliary:\n  compression:\n    model: \"some-relay-hosted-model\"\n# after\nauxiliary:\n  compression:\n    model: \"gemini-2.5-flash\"","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"# Already handled internally: the compressor routes this through the\n# main-model fallback + cooldown machinery. Callers of chat()/run_conversation()\n# need no extra handling — just let the turn proceed and the fallback summarizes.","preventionTips":["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"],"tags":["llm","compression","proxy","empty-response"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}