thedotmack/claude-mem · error · ServerClassifiedProviderError

parse_error

parse_error

Error message

OpenRouter returned invalid JSON

What it means

Thrown after a successful (2xx) HTTP response when response.json() fails to parse the body. It is wrapped as ServerClassifiedProviderError with kind 'parse_error', keeping the underlying SyntaxError as cause. It means the endpoint answered but the body was not valid JSON — usually an HTML page, plain-text message, or a gateway that is not actually OpenAI chat/completions compatible.

Solutions

  1. Curl the configured chat/completions URL directly with a minimal request and confirm the body is JSON (resolveOpenRouterChatCompletionsUrl appends /chat/completions to your baseUrl).
  2. Correct the base URL: add the missing /v1 segment for local/OpenAI-compatible servers, or remove a mismatched suffix so the final URL is exactly <base>/chat/completions.
  3. If a proxy or captive portal intercepts the request, bypass it or whitelist the API host so JSON passes through untouched.
  4. Retry once for transient truncation from local model servers (LM Studio/Ollama-style gateways occasionally emit malformed bodies).

Example fix

// before
new OpenRouterObservationProvider({
  apiKey,
  baseUrl: 'http://localhost:1234', // serves HTML at /chat/completions
});

// after — point at the OpenAI-compatible root; /chat/completions is appended
new OpenRouterObservationProvider({
  apiKey,
  baseUrl: 'http://localhost:1234/v1',
});
Defensive patterns

Strategy: try-catch

Validate before calling

// Preflight the OpenAI-compatible endpoint before wiring it in:
const res = await fetch(`${baseUrl}/chat/completions`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
  body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1 }),
});
const contentType = res.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) {
  throw new Error(`baseUrl ${baseUrl} does not return JSON (${contentType}); fix the base URL`);
}

Type guard

import { ServerClassifiedProviderError } from './shared/error-classification.js';

function isProviderParseError(e: unknown): boolean {
  return e instanceof ServerClassifiedProviderError && e.kind === 'parse_error';
}

Try / catch

try {
  const result = await provider.generate(context, signal);
} catch (e) {
  if (e instanceof ServerClassifiedProviderError && e.kind === 'parse_error') {
    // endpoint compatibility problem: log the cause (the JSON SyntaxError),
    // verify baseUrl, and fall back to another provider rather than retrying blindly
  }
  throw e;
}

Prevention

When it happens

Trigger: Setting options.baseUrl to an OpenAI-compatible endpoint (#2382/#2393) that returns 200 with HTML or plain text instead of a chat/completions JSON body; a base URL missing /v1 (e.g. http://localhost:1234 instead of http://localhost:1234/v1) so /chat/completions resolves to a 200 HTML page; an intermediary proxy or auth portal replying 200 with non-JSON content; a local model server truncating the response body.

Common situations: Using the baseUrl override for DeepSeek, LM Studio, or custom gateways with a slightly wrong URL; corporate proxies serving an HTML login page with status 200; version drift in local inference servers that changes response framing; SSE-enabled endpoints returning streamed chunks where a single JSON object was expected.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/7d0bdab3ce1868e2. Report an issue: GitHub.

Appendix: source

Thrown at src/server/generation/providers/OpenRouterObservationProvider.ts:109

    }

    if (!response.ok) {
      const bodyText = await safeReadBody(response);
      throw classifyHttpProviderError({
        status: response.status,
        bodyText,
        headers: response.headers,
        cause: new Error(`OpenRouter API error: ${response.status} - ${bodyText}`),
        providerLabel: 'OpenRouter',
      });
    }

    let data: OpenRouterResponse;
    try {
      data = (await response.json()) as OpenRouterResponse;
    } catch (parseError) {
      const err = parseError instanceof Error ? parseError : new Error(String(parseError));
      throw new ServerClassifiedProviderError('OpenRouter returned invalid JSON', {
        kind: 'parse_error',
        cause: err,
      });
    }

    if (data.error) {
      throw classifyHttpProviderError({
        status: response.status,
        bodyText: `${data.error.code ?? ''} ${data.error.message ?? ''}`,
        headers: response.headers,
        cause: new Error(`OpenRouter API error: ${data.error.code} - ${data.error.message}`),
        providerLabel: 'OpenRouter',
      });
    }

    const rawText = data.choices?.[0]?.message?.content?.trim() ?? '';
    if (!rawText) {
      logger.warn('SDK', 'OpenRouter returned empty content', {

View on GitHub (pinned to e2d1df569a)