{"record":{"id":"09cb26fd5e9a3ea8","repo":"NousResearch/hermes-agent","slug":"auxiliary-task-or-call-llm-returned-none-resp","errorCode":null,"errorMessage":"Auxiliary {task or 'call'}: LLM returned None response","messagePattern":"Auxiliary (.+?): LLM returned None response","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/auxiliary_client.py","lineNumber":8494,"sourceCode":") -> Any:\n    \"\"\"Validate that an LLM response has the expected .choices[0].message shape.\n\n    Fails fast with a clear error instead of letting malformed payloads\n    propagate to downstream consumers where they crash with misleading\n    AttributeError (e.g. \"'str' object has no attribute 'choices'\").\n\n    See #7264.\n\n    Also the single accounting chokepoint for auxiliary usage: every\n    successful non-streaming aux response passes through here exactly once,\n    so token usage is recorded against the ambient session context published\n    by the agent loop (``agent.aux_accounting``, issue #23270). Recording is\n    best-effort and never affects validation. *provider*/*base_url* are\n    optional accounting hints — fallback-path calls omit them and the row\n    keeps the model (read from the response itself) with an empty route.\n    \"\"\"\n    if response is None:\n        raise RuntimeError(\n            f\"Auxiliary {task or 'call'}: LLM returned None response\"\n        )\n    from agent.aux_accounting import record_aux_usage\n    record_aux_usage(response, task, provider=provider, base_url=base_url)\n    # Allow SimpleNamespace responses from adapters (CodexAuxiliaryClient,\n    # AnthropicAuxiliaryClient) — they have .choices[0].message.\n    try:\n        choices = response.choices\n        if not choices or not hasattr(choices[0], \"message\"):\n            raise AttributeError(\"missing choices[0].message\")\n    except (AttributeError, TypeError, IndexError) as exc:\n        recovered = _recover_aux_response_message(response)\n        if recovered is not None:\n            _record_relay_auxiliary_response_model(response)\n            _complete_relay_auxiliary_call()\n            return recovered\n        response_type = type(response).__name__\n        response_preview = str(response)[:120]","sourceCodeStart":8476,"sourceCodeEnd":8512,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/auxiliary_client.py#L8476-L8512","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the auxiliary call — a one-off None is usually a transient network/endpoint hiccup.","Test the same provider/model with a direct curl or small script to confirm it returns a real completion body.","If a custom base_url is in use, verify the endpoint emits a valid OpenAI chat.completion JSON body.","If it persists, switch the auxiliary task's provider (config.yaml `auxiliary.<task>.provider`) to a known-good backend."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"def is_valid_aux_response(r) -> bool:\n    return r is not None and hasattr(r, \"choices\") and len(r.choices) > 0","tryCatchPattern":"for attempt in range(2):\n    try:\n        return await aux_call_async(...)\n    except RuntimeError as e:\n        if \"LLM returned None response\" in str(e) and attempt == 0:\n            continue  # transient — retry once\n        raise","preventionTips":["Smoke-test custom endpoints with a one-shot completion before wiring them into auxiliary tasks.","Prefer mainstream providers for auxiliary work; keep custom gateways out of the hot path until proven stable."],"tags":["auxiliary","validation","provider","response-shape"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}