{"record":{"id":"1dc49cd226b62c16","repo":"abhigyanpatwari/GitNexus","slug":"llm-returned-empty-response","errorCode":null,"errorMessage":"LLM returned empty response","messagePattern":"LLM returned empty response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/wiki/llm-client.ts","lineNumber":460,"sourceCode":"        `Azure content filter blocked this request. The prompt triggered content policy. Details: ${errorText.slice(0, 300)}`,\n      );\n    }\n\n    // Any other non-OK response here is a terminal 4xx — resilientFetch\n    // already retried 5xx/429 to exhaustion and would have thrown above.\n    throw new Error(`LLM API error (${response.status}): ${errorText.slice(0, 500)}`);\n  }\n\n  // Streaming path\n  if (useStream && response.body) {\n    return await readSSEStream(response.body, options!.onChunk!);\n  }\n\n  // Non-streaming path\n  const json = (await response.json()) as any;\n  const choice = json.choices?.[0];\n  if (!choice?.message?.content) {\n    throw new Error('LLM returned empty response');\n  }\n\n  return {\n    content: choice.message.content,\n    promptTokens: json.usage?.prompt_tokens,\n    completionTokens: json.usage?.completion_tokens,\n  };\n}\n\n/**\n * Read an SSE stream from an OpenAI-compatible streaming response.\n */\nasync function readSSEStream(\n  body: ReadableStream<Uint8Array>,\n  onChunk: (charsReceived: number) => void,\n): Promise<LLMResponse> {\n  const decoder = new TextDecoder();\n  const reader = body.getReader();","sourceCodeStart":442,"sourceCodeEnd":478,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/core/wiki/llm-client.ts#L442-L478","documentation":"The non-streaming call returned HTTP 200, but the JSON body has no choices[0].message.content. The client expects an OpenAI-compatible chat-completions shape, so a 200 response without that path means the endpoint is not returning chat completions — wrong route, a gateway 'success' wrapping an error, or a genuinely empty completion.","triggerScenarios":"`--base-url` points at a non-chat endpoint that still returns 200 JSON (root path, /models, a management API); a proxy that returns 200 with an error object instead of a status code; a model returning empty content (empty string is also falsy); API version mismatch producing a different schema.","commonSituations":"Custom/self-hosted servers that are OpenAI-ish but not compliant (LiteLLM misconfig, old vLLM); base URL accidentally including /models; gateway 200-wrapping errors; using --api-version with a provider that ignores it and returns a different envelope.","solutions":["Point `--base-url` at the chat-completions root (usually `https://host/v1`) — the client appends the completion path","Reproduce with curl against $BASE_URL/chat/completions and inspect the JSON shape","If the body wraps an error in a 200, fix the gateway to return proper status codes","Retry once — some providers transiently return empty completions under load"],"exampleFix":"# before\ngitnexus wiki --provider custom --base-url https://my-proxy.example.com\n\n# after\ngitnexus wiki --provider custom --base-url https://my-proxy.example.com/v1","handlingStrategy":"validation","validationCode":"// Verify the endpoint returns an OpenAI-shaped body before the run:\nconst r = await fetch(`${baseUrl}/chat/completions`, { method: 'POST', headers, body: minimalPayload });\nconst j = await r.json();\nif (!(j as any)?.choices?.[0]?.message?.content) {\n  throw new Error('Endpoint is not returning OpenAI-compatible chat completions — fix --base-url');\n}","typeGuard":"function isOpenAIChatCompletion(json: unknown): json is { choices: { message: { content: string } }[]; usage?: Record<string, number> } {\n  return typeof json === 'object' && json !== null &&\n    Array.isArray((json as any).choices) &&\n    typeof (json as any).choices[0]?.message?.content === 'string';\n}","tryCatchPattern":"try {\n  await callLLM(prompt, config);\n} catch (err) {\n  if (err instanceof Error && err.message === 'LLM returned empty response') {\n    // endpoint shape mismatch or transient empty completion: verify with curl, then retry once\n  }\n  throw err;\n}","preventionTips":["Validate custom gateways with an OpenAI-compatible conformance check before pointing gitnexus at them","Always end custom --base-url values with the versioned prefix (usually /v1)","Treat a 200-without-choices as a config smell, not a provider flake — investigate the response body"],"tags":["llm","api-contract","openai-compatible","empty-response","gitnexus"],"backgroundTag":"unexpected-api-response","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}