{"record":{"id":"b53c01e255c99bd5","repo":"NousResearch/hermes-agent","slug":"auxiliary-task-or-call-llm-returned-invalid-r","errorCode":null,"errorMessage":"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.","messagePattern":"Auxiliary (.+?): LLM returned invalid response \\(type=(.+?)\\): (.+?)\\. Expected object with \\.choices\\[0\\]\\.message — check provider adapter or custom endpoint compatibility\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"agent/auxiliary_client.py","lineNumber":8513,"sourceCode":"            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]\n        raise RuntimeError(\n            f\"Auxiliary {task or 'call'}: LLM returned invalid response \"\n            f\"(type={response_type}): {response_preview!r}. \"\n            f\"Expected object with .choices[0].message — check provider \"\n            f\"adapter or custom endpoint compatibility.\"\n        ) from exc\n    _record_relay_auxiliary_response_model(response)\n    _complete_relay_auxiliary_call()\n    return response\n\n\ndef _complete_relay_auxiliary_call(*, outcome: str = \"success\") -> None:\n    \"\"\"Close one auxiliary logical call after acceptance or terminal failure.\"\"\"\n    context = _RELAY_AUX_CALL_CONTEXT.get()\n    if context is None:\n        return\n    from agent import relay_llm\n\n    relay_llm.complete_logical_call(","sourceCodeStart":8495,"sourceCodeEnd":8531,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/agent/auxiliary_client.py#L8495-L8531","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Inspect the response preview in the error message — it usually reveals an HTML error page or an Anthropic-shaped payload.","Make the custom endpoint OpenAI chat.completions-compatible (`choices[0].message.content`).","If the provider is Anthropic/Codex-style, use it via its proper adapter rather than the generic aux path.","Pin the auxiliary task to a mainstream provider in config.yaml (`auxiliary.<task>.provider`) while the custom endpoint is fixed."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import json, urllib.request\ndef endpoint_returns_openai_shape(base_url, model, api_key):\n    req = urllib.request.Request(\n        base_url.rstrip(\"/\") + \"/chat/completions\",\n        data=json.dumps({\"model\": model, \"messages\": [{\"role\": \"user\", \"content\": \"hi\"}]}).encode(),\n        headers={\"Authorization\": f\"Bearer {api_key}\", \"Content-Type\": \"application/json\"})\n    body = json.load(urllib.request.urlopen(req, timeout=30))\n    return bool(body.get(\"choices\") and body[\"choices\"][0].get(\"message\") is not None)","typeGuard":"def looks_like_chat_completion(obj) -> bool:\n    choices = getattr(obj, \"choices\", None) or obj.get(\"choices\") if isinstance(obj, dict) else getattr(obj, \"choices\", None)\n    try:\n        return bool(choices) and getattr(choices[0], \"message\", None) is not None\n    except (TypeError, IndexError, AttributeError):\n        return False","tryCatchPattern":"try:\n    resp = await aux_call_async(...)\nexcept RuntimeError as e:\n    if \"Expected object with .choices[0].message\" in str(e):\n        log.error(\"aux endpoint not OpenAI-compatible: %s\", e)\n        raise  # configuration problem — retrying won't help","preventionTips":["Validate custom endpoints return OpenAI chat.completion JSON before assigning them auxiliary tasks.","Route Anthropic/Codex-style providers through their dedicated adapters, not the generic aux path."],"tags":["auxiliary","response-shape","custom-endpoint","compatibility"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}