{"record":{"id":"8eeed5df3d87deee","repo":"justjavac/wechat-miniapp-radar","slug":"ai-provider-returned-an-empty-response-for-model","errorCode":null,"errorMessage":"AI provider returned an empty response for ${model}.","messagePattern":"AI provider returned an empty response for (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"lib/ai-client.ts","lineNumber":161,"sourceCode":"        messages,\n        temperature: 0.2,\n        max_tokens: DEFAULT_AI_MAX_TOKENS,\n        stream: false,\n        ...responseFormatForModel(config, model)\n      })\n    },\n    timeoutMs\n  );\n\n  const payload = parseCompletionPayload(text);\n  if (!response.ok) {\n    const providerError = stringifyProviderError(payload?.error);\n    throw new Error(providerError ? `AI provider rejected ${model}: ${providerError}` : `AI provider rejected ${model} with HTTP ${response.status}.`);\n  }\n\n  const content = payload?.choices?.[0]?.message?.content;\n  if (typeof content !== \"string\" || content.trim().length === 0) {\n    throw new Error(`AI provider returned an empty response for ${model}.`);\n  }\n\n  return parseJsonObject<T>(content);\n}\n\nexport async function createAiJsonCompletion<T>({\n  messages,\n  timeoutMs = DEFAULT_AI_TIMEOUT_MS,\n  totalTimeoutMs = DEFAULT_AI_TOTAL_TIMEOUT_MS\n}: {\n  messages: AiPromptMessage[];\n  timeoutMs?: number;\n  totalTimeoutMs?: number;\n}): Promise<AiJsonCompletionResult<T>> {\n  const config = getAiConfig();\n  if (!config.configured) {\n    return {\n      ok: false,","sourceCodeStart":143,"sourceCodeEnd":179,"githubUrl":"https://github.com/justjavac/wechat-miniapp-radar/blob/02a010ecea0320b7aa975bb62a5dde271ae630c9/lib/ai-client.ts#L143-L179","documentation":"Thrown by requestChatCompletion() in lib/ai-client.ts after a 2xx response when payload.choices[0].message.content is absent, not a string, or whitespace-only. It guards the contract that the provider returned usable text before parseJsonObject() runs. Typical of content-filter refusals, reasoning models that populate a reasoning field but leave content empty, or responses truncated by the small max_tokens (DEFAULT_AI_MAX_TOKENS = 1400). createAiJsonCompletion() catches it and retries the fallback model.","triggerScenarios":"Provider returns 200 with choices[0].message.content equal to null or '' (content filter, safety refusal, or empty completion); a reasoning model places output in a non-content field; the response shape lacks choices/message; output larger than max_tokens and returned empty; OpenRouter routing glitch returning a 200 with no choices.","commonSituations":"Free models with aggressive safety filters; prompts that trip refusals; switching to a 'reasoning' variant that does not populate content; requested JSON exceeding the 1400-token cap; provider returning an unrelated 200 JSON without choices.","solutions":["Inspect result.error/result.model from createAiJsonCompletion; if the primary model is the culprit, rely on the automatic fallback and ensure OPENAI_FALLBACK_MODEL is set and available.","If refusals are likely, soften or rewrite the prompt and re-send.","If truncation is likely, raise the token budget for the call.","When all models return empty, use the rule-based fallback so the user-facing flow still answers."],"exampleFix":"// before\nconst { value } = await requestChatCompletion<T>({ config, model, messages, timeoutMs });\n\n// after (use the public API and handle empty-response failure)\nconst result = await createAiJsonCompletion<T>({ messages });\nif (!result.ok || !result.value) {\n  return ruleBasedFallback();\n}","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"const result = await createAiJsonCompletion<T>({ messages });\nif (!result.ok || !result.value) {\n  // 'AI provider returned an empty response for <model>.' is in result.error\n  return ruleBasedFallback();\n}\nreturn result.value;","preventionTips":["Always consume createAiJsonCompletion and branch on ok/value rather than calling requestChatCompletion directly.","Keep a distinct, available OPENAI_FALLBACK_MODEL.","Avoid prompts likely to trigger content-filter refusals and keep requested JSON within the 1400-token budget."],"tags":["ai","provider-error","empty-response"],"backgroundTag":null,"analyzedSha":"02a010ecea0320b7aa975bb62a5dde271ae630c9","analyzedAt":"2026-08-12T16:16:33.227Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}