{"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":456,"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":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/gitnexus/src/core/wiki/llm-client.ts#L438-L474","documentation":"Non-streaming success path: the provider returned HTTP 200 with valid JSON, but json.choices[0].message.content is missing or falsy. GitNexus refuses to return an empty completion because downstream wiki rendering would emit blank pages. Note this checks only .content; providers that put text in reasoning_content or in a different envelope will trip it.","triggerScenarios":"Provider returns 200 but choices[0].message.content is null/'' (some Azure deployments return null content when content_filter triggered without a 400; some OpenAI-compatible servers return empty for safety models; reasoning models that put everything in reasoning_content with empty primary content). Also: max_completion_tokens set so low the model emitted nothing.","commonSituations":"Reasoning model (o1/o3) parsed via non-reasoning path; Azure content filter returning 200 with null content; tiny max_completion_tokens budget; broken OpenAI-compatible server returning malformed choices array; tool-calling-format response with no text.","solutions":["Raise config.maxTokens / max_completion_tokens so the model has room to emit text.","If using a reasoning model, ensure it is detected (isReasoningModel) or pass provider correctly so the response envelope matches.","If Azure null-content due to content filter, sanitize the prompt or relax the filter (see error 224).","Switch to streaming (pass onChunk in options) which parses SSE chunks and surfaces partial content.","Verify the server is actually OpenAI-compatible (choices[].message.content); if it uses a different shape, use a different baseUrl/provider."],"exampleFix":"// before\nawait callLLM(prompt, { baseUrl, apiKey, model: 'o3-mini', maxTokens: 1 });\n// -> LLM returned empty response\n\n// after\nawait callLLM(prompt, { baseUrl, apiKey, model: 'o3-mini', maxTokens: 4096 });\n// or stream:\nawait callLLM(prompt, { baseUrl, apiKey, model }, undefined, { onChunk: n => progress(n) });","handlingStrategy":"validation","validationCode":"function looksReasoning(model) { return /^o[1-9]\\d*/i.test(model); }\n// give reasoning models enough tokens and parse via streaming for partial content\nconfig.maxTokens = Math.max(config.maxTokens ?? 0, 1024);\nif (looksReasoning(config.model)) config.isReasoningModel = true;","typeGuard":"function isEmptyResponseError(e) {\n  return e instanceof Error && /LLM returned empty response/.test(e.message);\n}","tryCatchPattern":"try { return await callLLM(prompt, config); }\ncatch (e) {\n  if (isEmptyResponseError(e)) {\n    // retry with streaming + larger budget, or non-reasoning fallback\n    return await callLLM(prompt, { ...config, maxTokens: 4096 }, undefined, { onChunk: () => {} });\n  }\n  throw e;\n}","preventionTips":["Set max_completion_tokens generously; tiny budgets cause empty completions.","For reasoning models pass isReasoningModel=true or rely on isReasoningModel detection.","Use streaming (onChunk) so partial content is captured even if the final envelope is unusual."],"tags":["llm","api-error","data","azure","reasoning-model","wiki"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}