{"record":{"id":"7d0bdab3ce1868e2","repo":"thedotmack/claude-mem","slug":"parse-error-7d0bda","errorCode":"parse_error","errorMessage":"OpenRouter returned invalid JSON","messagePattern":"OpenRouter returned invalid JSON","errorType":"exception","errorClass":"ServerClassifiedProviderError","httpStatus":null,"severity":"error","filePath":"src/server/generation/providers/OpenRouterObservationProvider.ts","lineNumber":109,"sourceCode":"    }\n\n    if (!response.ok) {\n      const bodyText = await safeReadBody(response);\n      throw classifyHttpProviderError({\n        status: response.status,\n        bodyText,\n        headers: response.headers,\n        cause: new Error(`OpenRouter API error: ${response.status} - ${bodyText}`),\n        providerLabel: 'OpenRouter',\n      });\n    }\n\n    let data: OpenRouterResponse;\n    try {\n      data = (await response.json()) as OpenRouterResponse;\n    } catch (parseError) {\n      const err = parseError instanceof Error ? parseError : new Error(String(parseError));\n      throw new ServerClassifiedProviderError('OpenRouter returned invalid JSON', {\n        kind: 'parse_error',\n        cause: err,\n      });\n    }\n\n    if (data.error) {\n      throw classifyHttpProviderError({\n        status: response.status,\n        bodyText: `${data.error.code ?? ''} ${data.error.message ?? ''}`,\n        headers: response.headers,\n        cause: new Error(`OpenRouter API error: ${data.error.code} - ${data.error.message}`),\n        providerLabel: 'OpenRouter',\n      });\n    }\n\n    const rawText = data.choices?.[0]?.message?.content?.trim() ?? '';\n    if (!rawText) {\n      logger.warn('SDK', 'OpenRouter returned empty content', {","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/server/generation/providers/OpenRouterObservationProvider.ts#L91-L127","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nnew OpenRouterObservationProvider({\n  apiKey,\n  baseUrl: 'http://localhost:1234', // serves HTML at /chat/completions\n});\n\n// after — point at the OpenAI-compatible root; /chat/completions is appended\nnew OpenRouterObservationProvider({\n  apiKey,\n  baseUrl: 'http://localhost:1234/v1',\n});","handlingStrategy":"try-catch","validationCode":"// Preflight the OpenAI-compatible endpoint before wiring it in:\nconst res = await fetch(`${baseUrl}/chat/completions`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },\n  body: JSON.stringify({ model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1 }),\n});\nconst contentType = res.headers.get('content-type') ?? '';\nif (!contentType.includes('application/json')) {\n  throw new Error(`baseUrl ${baseUrl} does not return JSON (${contentType}); fix the base URL`);\n}","typeGuard":"import { ServerClassifiedProviderError } from './shared/error-classification.js';\n\nfunction isProviderParseError(e: unknown): boolean {\n  return e instanceof ServerClassifiedProviderError && e.kind === 'parse_error';\n}","tryCatchPattern":"try {\n  const result = await provider.generate(context, signal);\n} catch (e) {\n  if (e instanceof ServerClassifiedProviderError && e.kind === 'parse_error') {\n    // endpoint compatibility problem: log the cause (the JSON SyntaxError),\n    // verify baseUrl, and fall back to another provider rather than retrying blindly\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["openrouter","json","parse-error","base-url","llm-provider","openai-compatible"],"backgroundTag":"invalid-json-response","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}