{"record":{"id":"4fc35eb68ae37c89","repo":"we-promise/sure","slug":"no-response-from-ai","errorCode":null,"errorMessage":"No response from AI","messagePattern":"No response from AI","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai/bank_statement_extractor.rb","lineNumber":113,"sourceCode":"\n      chunks << current_chunk.join(\"\\n\\n\") if current_chunk.any?\n      chunks\n    end\n\n    def process_chunk(text, is_first_chunk)\n      params = {\n        model: model,\n        messages: [\n          { role: \"system\", content: is_first_chunk ? instructions_with_metadata : instructions_transactions_only },\n          { role: \"user\", content: \"Extract transactions:\\n\\n#{text}\" }\n        ],\n        response_format: { type: \"json_object\" }\n      }\n\n      response = client.chat(parameters: params)\n      content = response.dig(\"choices\", 0, \"message\", \"content\")\n\n      raise Provider::Openai::Error, \"No response from AI\" if content.blank?\n\n      parsed = parse_json_response(content)\n\n      {\n        transactions: normalize_transactions(parsed[\"transactions\"] || []),\n        period: {\n          start_date: parsed.dig(\"statement_period\", \"start_date\"),\n          end_date: parsed.dig(\"statement_period\", \"end_date\")\n        },\n        account_holder: parsed[\"account_holder\"],\n        account_number: parsed[\"account_number\"],\n        bank_name: parsed[\"bank_name\"],\n        opening_balance: parsed[\"opening_balance\"],\n        closing_balance: parsed[\"closing_balance\"]\n      }\n    end\n\n    def parse_json_response(content)","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai/bank_statement_extractor.rb#L95-L131","documentation":"Raised by Provider::Openai::BankStatementExtractor#process_chunk when client.chat returns but choices[0].message.content is blank. The request already sets response_format json_object, so an HTTP-level failure would have raised earlier; a 200 with empty content means the model produced nothing usable in that slot — content-filter refusal (null content with a finish_reason like content_filter), truncation at max tokens (finish_reason length), or a gateway returning a choices array whose message carries only role/finish fields.","triggerScenarios":"OpenAI content moderation refusing the chunk (statements with gambling/adult merchant strings can trip it) returning content: null; output cut by max_tokens with content present but empty after filtering; a custom OpenAI-compatible provider returning an empty content field on error instead of a failing status; empty assistant message on finish_reason length.","commonSituations":"Statements containing unusual merchant names or obfuscated strings; tight token budgets on dense statement pages; third-party gateways that answer 200 with empty content when overloaded; temperature/tool-usage settings on the gateway causing empty completions.","solutions":["Log response.dig(\"choices\", 0, \"finish_reason\") — content_filter vs length tells you the remedy immediately.","For content_filter: rephrase/split the chunk, or catch and surface a user-facing 'statement could not be processed' message; do not retry the same text.","For length: raise max tokens or shrink MAX_CHARS_PER_CHUNK so a single page fits.","For gateways: verify with a minimal curl chat completion that the endpoint returns content for json_object mode at all."],"exampleFix":"# before\ncontent = response.dig(\"choices\", 0, \"message\", \"content\")\nraise Provider::Openai::Error, \"No response from AI\" if content.blank?\n\n# after\nchoice = response.dig(\"choices\", 0)\ncontent = choice&.dig(\"message\", \"content\")\nif content.blank?\n  raise Provider::Openai::Error, \"No response from AI (finish_reason=#{choice&.dig(\"finish_reason\")})\"\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"attempts = 0\nbegin\n  process_chunk(chunk, first)\nrescue Provider::Openai::Error => e\n  raise unless e.message.include?(\"No response from AI\") && (attempts += 1) <= 1\n  retry # transient empty completion; give the model a second chance\nend","preventionTips":["Inspect finish_reason in dev logs — content_filter and length need different fixes, and content_filter must not be blindly retried.","Keep chunks under the response token budget so JSON-mode answers are never cut to empty.","Confirm custom gateways honor response_format json_object before relying on non-empty content."],"tags":["openai","llm","empty-response","content-filter","bank-statement"],"backgroundTag":"llm-empty-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}