NousResearch/hermes-agent · error · RuntimeError

Auxiliary {task or 'call'}: LLM returned None response

Error message

Auxiliary {task or 'call'}: LLM returned None response

What it means

Validation chokepoint for auxiliary responses: every successful non-streaming auxiliary LLM response passes through this validator exactly once (it is also the aux token-accounting point, issue #23270). If the response object is None — the SDK or adapter returned nothing instead of raising — the call is treated as invalid and aborted. This guards downstream code that unconditionally reads response.choices.

Source

Thrown at agent/auxiliary_client.py:8494

) -> Any:
    """Validate that an LLM response has the expected .choices[0].message shape.

    Fails fast with a clear error instead of letting malformed payloads
    propagate to downstream consumers where they crash with misleading
    AttributeError (e.g. "'str' object has no attribute 'choices'").

    See #7264.

    Also the single accounting chokepoint for auxiliary usage: every
    successful non-streaming aux response passes through here exactly once,
    so token usage is recorded against the ambient session context published
    by the agent loop (``agent.aux_accounting``, issue #23270). Recording is
    best-effort and never affects validation. *provider*/*base_url* are
    optional accounting hints — fallback-path calls omit them and the row
    keeps the model (read from the response itself) with an empty route.
    """
    if response is None:
        raise RuntimeError(
            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]

View on GitHub (pinned to c896c09c42)

Solutions

  1. Retry the auxiliary call — a one-off None is usually a transient network/endpoint hiccup.
  2. Test the same provider/model with a direct curl or small script to confirm it returns a real completion body.
  3. If a custom base_url is in use, verify the endpoint emits a valid OpenAI chat.completion JSON body.
  4. If it persists, switch the auxiliary task's provider (config.yaml `auxiliary.<task>.provider`) to a known-good backend.
Defensive patterns

Strategy: retry

Type guard

def is_valid_aux_response(r) -> bool:
    return r is not None and hasattr(r, "choices") and len(r.choices) > 0

Try / catch

for attempt in range(2):
    try:
        return await aux_call_async(...)
    except RuntimeError as e:
        if "LLM returned None response" in str(e) and attempt == 0:
            continue  # transient — retry once
        raise

Prevention

When it happens

Trigger: A non-streaming auxiliary call whose provider adapter/SDK returns None on transport or serialization failure instead of raising; a custom endpoint that closes the connection and the SDK swallows it into a None result.

Common situations: Flaky custom OpenAI-compatible gateway returning an empty body; an adapter (CodexAuxiliaryClient-style SimpleNamespace shim) with a bug returning None on an error branch; proxy timeouts producing empty responses.

Related errors


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