paperclipai/paperclip · warning

OpenRouter returned an invalid model catalog.

Error message

OpenRouter returned an invalid model catalog.

What it means

listOpenRouterModels validates the JSON body of the OpenRouter catalog response. If body.data is not an array, it throws 'OpenRouter returned an invalid model catalog.' This means the HTTP call succeeded but the payload shape did not match the expected { data: [...] } envelope.

Solutions

  1. Log the raw response body when this occurs to see what was actually returned.
  2. Check https://openrouter.ai/api/v1/models directly for a changed response shape and update parsing if the schema changed.
  3. Bypass any intercepting proxy or fix captive-portal/TLS issues so the genuine OpenRouter response arrives.
  4. Enter a model ID manually while the catalog is unavailable.

Example fix

// before
if (!Array.isArray(body.data)) throw new Error("OpenRouter returned an invalid model catalog.");
// after
if (!Array.isArray(body.data)) {
  console.error("openrouter catalog body:", JSON.stringify(body).slice(0, 500));
  throw new Error("OpenRouter returned an invalid model catalog.");
}
Defensive patterns

Strategy: fallback

Validate before calling

const body = await res.json();
if (body && Array.isArray(body.data)) { /* safe to parse */ }

Type guard

const isCatalog = (b: unknown): b is { data: Array<{ id?: unknown; name?: unknown }> } =>
  typeof b === "object" && b !== null && Array.isArray((b as { data?: unknown }).data);

Try / catch

try {
  models = await listOpenRouterModels();
} catch (e) {
  if (e.message.includes("invalid model catalog")) { models = lastKnownGoodModels; }
  else throw e;
}

Prevention

When it happens

Trigger: OpenRouter returns 200 with an unexpected body: an HTML error/captcha page, a proxy or captive-portal response, an API schema change removing/renaming the 'data' field, or a truncated JSON payload.

Common situations: Responses from an intercepting proxy (auth portals, TLS inspection blocks); OpenRouter API version change altering the response shape; middleware returning JSON without a 'data' array.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/de90d05331bba5b7. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/openrouter-models.ts:14

import type { AdapterModel } from "@paperclipai/adapter-utils";

let cached: { until: number; models: AdapterModel[] } | undefined;
let pending: Promise<AdapterModel[]> | undefined;

/** OpenRouter's public catalog does not require access to anyone's credentials. */
export async function listOpenRouterModels(refresh = false): Promise<AdapterModel[]> {
  if (!refresh && cached && cached.until > Date.now()) return cached.models;
  if (pending) return pending;
  pending = (async () => {
    const response = await fetch("https://openrouter.ai/api/v1/models", { signal: AbortSignal.timeout(10_000) });
    if (!response.ok) throw new Error("Could not load OpenRouter models. Retry or enter a model ID manually.");
    const body = await response.json() as { data?: Array<{ id?: unknown; name?: unknown }> };
    if (!Array.isArray(body.data)) throw new Error("OpenRouter returned an invalid model catalog.");
    const models = body.data.flatMap(model => typeof model.id === "string" && model.id.includes("/")
      ? [{ id: `openrouter/${model.id}`, label: typeof model.name === "string" ? model.name : model.id }]
      : []).sort((a, b) => a.label.localeCompare(b.label));
    cached = { until: Date.now() + 60_000, models };
    return models;
  })();
  try { return await pending; } finally { pending = undefined; }
}

View on GitHub (pinned to 3f1d897a7c)