NousResearch/hermes-agent · error · RuntimeError

Auxiliary {task or 'call'}: LLM returned invalid response (t

Error message

Auxiliary {task or 'call'}: LLM returned invalid response (type={response_type}): {response_preview!r}. Expected object with .choices[0].message — check provider adapter or custom endpoint compatibility.

What it means

The auxiliary response validator requires an object with .choices[0].message (OpenAI shape). It first tried _recover_aux_response_message() to salvage alternate shapes; recovery failed, so the response type name and a 120-char preview are embedded in the error. The message explicitly points at provider adapters or custom endpoints that do not produce OpenAI-compatible payloads.

Source

Thrown at agent/auxiliary_client.py:8513

            f"Auxiliary {task or 'call'}: LLM returned None response"
        )
    from agent.aux_accounting import record_aux_usage
    record_aux_usage(response, task, provider=provider, base_url=base_url)
    # Allow SimpleNamespace responses from adapters (CodexAuxiliaryClient,
    # AnthropicAuxiliaryClient) — they have .choices[0].message.
    try:
        choices = response.choices
        if not choices or not hasattr(choices[0], "message"):
            raise AttributeError("missing choices[0].message")
    except (AttributeError, TypeError, IndexError) as exc:
        recovered = _recover_aux_response_message(response)
        if recovered is not None:
            _record_relay_auxiliary_response_model(response)
            _complete_relay_auxiliary_call()
            return recovered
        response_type = type(response).__name__
        response_preview = str(response)[:120]
        raise RuntimeError(
            f"Auxiliary {task or 'call'}: LLM returned invalid response "
            f"(type={response_type}): {response_preview!r}. "
            f"Expected object with .choices[0].message — check provider "
            f"adapter or custom endpoint compatibility."
        ) from exc
    _record_relay_auxiliary_response_model(response)
    _complete_relay_auxiliary_call()
    return response


def _complete_relay_auxiliary_call(*, outcome: str = "success") -> None:
    """Close one auxiliary logical call after acceptance or terminal failure."""
    context = _RELAY_AUX_CALL_CONTEXT.get()
    if context is None:
        return
    from agent import relay_llm

    relay_llm.complete_logical_call(

View on GitHub (pinned to c896c09c42)

Solutions

  1. Inspect the response preview in the error message — it usually reveals an HTML error page or an Anthropic-shaped payload.
  2. Make the custom endpoint OpenAI chat.completions-compatible (`choices[0].message.content`).
  3. If the provider is Anthropic/Codex-style, use it via its proper adapter rather than the generic aux path.
  4. Pin the auxiliary task to a mainstream provider in config.yaml (`auxiliary.<task>.provider`) while the custom endpoint is fixed.
Defensive patterns

Strategy: validation

Validate before calling

import json, urllib.request
def endpoint_returns_openai_shape(base_url, model, api_key):
    req = urllib.request.Request(
        base_url.rstrip("/") + "/chat/completions",
        data=json.dumps({"model": model, "messages": [{"role": "user", "content": "hi"}]}).encode(),
        headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"})
    body = json.load(urllib.request.urlopen(req, timeout=30))
    return bool(body.get("choices") and body["choices"][0].get("message") is not None)

Type guard

def looks_like_chat_completion(obj) -> bool:
    choices = getattr(obj, "choices", None) or obj.get("choices") if isinstance(obj, dict) else getattr(obj, "choices", None)
    try:
        return bool(choices) and getattr(choices[0], "message", None) is not None
    except (TypeError, IndexError, AttributeError):
        return False

Try / catch

try:
    resp = await aux_call_async(...)
except RuntimeError as e:
    if "Expected object with .choices[0].message" in str(e):
        log.error("aux endpoint not OpenAI-compatible: %s", e)
        raise  # configuration problem — retrying won't help

Prevention

When it happens

Trigger: Calling an auxiliary task through a custom endpoint or adapter whose response object lacks .choices (e.g. a raw dict, an Anthropic-style content block response, or an error HTML payload stringified by the SDK).

Common situations: Pointing auxiliary tasks at a LiteLLM/proxy that returns a nonstandard schema on error; a provider plugin whose adapter returns a partially-built object; an endpoint that returns 200 with an error body.

Related errors


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