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
- Curl the configured chat/completions URL directly with a minimal request and confirm the body is JSON (resolveOpenRouterChatCompletionsUrl appends /chat/completions to your baseUrl).
- 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.
- If a proxy or captive portal intercepts the request, bypass it or whitelist the API host so JSON passes through untouched.
- 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
- Verify custom baseUrl values with a one-off curl before persisting them in settings.
- Prefer endpoints that declare application/json in responses; treat HTML content-type as a misconfigured base URL.
- Log the parse cause — it usually names the offending character position, which distinguishes truncation from wrong-endpoint HTML.
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
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)