{"record":{"id":"5ad39777a0959d6a","repo":"Mintplex-Labs/anything-llm","slug":"invalid-response-body-returned-from-openrouter","errorCode":null,"errorMessage":"Invalid response body returned from OpenRouter: ${result.output?.error?.message || \"Unknown error\"} ${result.output?.error?.code || \"Unknown code\"}","messagePattern":"Invalid response body returned from OpenRouter: (.+?) (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/utils/AiProviders/openRouter/index.js","lineNumber":267,"sourceCode":"        .create({\n          model: this.model,\n          messages,\n          temperature,\n          // This is an OpenRouter specific option that allows us to get the reasoning text\n          // before the token text.\n          include_reasoning: true,\n          user: user?.id ? `user_${user.id}` : \"\",\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result?.output?.hasOwnProperty(\"choices\") ||\n      result?.output?.choices?.length === 0\n    )\n      throw new Error(\n        `Invalid response body returned from OpenRouter: ${result.output?.error?.message || \"Unknown error\"} ${result.output?.error?.code || \"Unknown code\"}`\n      );\n\n    return {\n      textResponse: this.#parseReasoningFromResponse(result.output.choices[0]),\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/Mintplex-Labs/anything-llm/blob/526360e320da9d1b36074be5ed64fe76e5bbfbbd/server/utils/AiProviders/openRouter/index.js#L249-L285","documentation":"Post-success structural validation: the chat.completions.create call resolved without throwing, but the body lacks a 'choices' array (or it is empty). OpenRouter sometimes returns 200 with an inline error object; the message includes result.output.error.message and result.output.error.code when present. This is the catch for shape problems the SDK did not reject on.","triggerScenarios":"OpenRouter forwards an upstream error as HTTP 200 with an error body (no choices); empty choices array from a content filter; response schema drift; provider returned only usage/metadata with no completion; rate-limit-style body that the SDK treated as success.","commonSituations":"Upstream provider errored and OpenRouter wrapped it as 200-with-error; content policy produced zero choices; transient provider hiccup; model returns tool-call-only with no text choices; partial outage where OpenRouter returns metadata but no content.","solutions":["Inspect the embedded error.message/error.code in the thrown text for the upstream cause.","Retry once — many of these are transient upstream blips.","Switch to a different model/provider slug for the same capability.","Check OpenRouter status (status.openrouter.ai) for provider outages."],"exampleFix":"// before\nif (!result?.output?.hasOwnProperty('choices') || result?.output?.choices?.length === 0)\n  throw new Error(`Invalid response body returned from OpenRouter: ${result.output?.error?.message || 'Unknown error'} ${result.output?.error?.code || 'Unknown code'}`);\n\n// after - treat upstream errors as retryable signals with a code\nconst body = result?.output;\nif (!body?.hasOwnProperty('choices') || body?.choices?.length === 0) {\n  const err = new Error(`OpenRouter returned no choices: ${body?.error?.message || 'Unknown error'} (${body?.error?.code || 'Unknown code'})`);\n  err.code = body?.error?.code || 'NO_CHOICES';\n  err.retryable = true;\n  throw err;\n}","handlingStrategy":"fallback","validationCode":"const hasOpenRouterChoices = (o) =>\n  !!o && typeof o === 'object' && Array.isArray(o.choices) && o.choices.length > 0;","typeGuard":"const hasOpenRouterChoices = (o) =>\n  !!o && typeof o === 'object' && Array.isArray(o.choices) && o.choices.length > 0;","tryCatchPattern":"try {\n  const r = await llm.getChatCompletion(messages, { temperature });\n  return r;\n} catch (e) {\n  if (e.message.startsWith('Invalid response body returned from OpenRouter'))\n    return fallbackResponse(); // upstream 200-with-error; retry or degrade\n  throw e;\n}","preventionTips":["Treat a 200-with-no-choices as a transient upstream signal and retry once before degrading.","Surface result.output.error.code so callers can distinguish content-filter from provider outage.","Keep a fallback model/provider for cases where the primary returns empty bodies."],"tags":["openrouter","llm-provider","response-validation","runtime"],"backgroundTag":null,"analyzedSha":"526360e320da9d1b36074be5ed64fe76e5bbfbbd","analyzedAt":"2026-08-13T01:45:47.170Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}