we-promise/sure · error · Provider::Openai::Error

No response from AI

Error message

No response from AI

What it means

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.

Source

Thrown at app/models/provider/openai/bank_statement_extractor.rb:113

      chunks << current_chunk.join("\n\n") if current_chunk.any?
      chunks
    end

    def process_chunk(text, is_first_chunk)
      params = {
        model: model,
        messages: [
          { role: "system", content: is_first_chunk ? instructions_with_metadata : instructions_transactions_only },
          { role: "user", content: "Extract transactions:\n\n#{text}" }
        ],
        response_format: { type: "json_object" }
      }

      response = client.chat(parameters: params)
      content = response.dig("choices", 0, "message", "content")

      raise Provider::Openai::Error, "No response from AI" if content.blank?

      parsed = parse_json_response(content)

      {
        transactions: normalize_transactions(parsed["transactions"] || []),
        period: {
          start_date: parsed.dig("statement_period", "start_date"),
          end_date: parsed.dig("statement_period", "end_date")
        },
        account_holder: parsed["account_holder"],
        account_number: parsed["account_number"],
        bank_name: parsed["bank_name"],
        opening_balance: parsed["opening_balance"],
        closing_balance: parsed["closing_balance"]
      }
    end

    def parse_json_response(content)

View on GitHub (pinned to e69894adb9)

Solutions

  1. Log response.dig("choices", 0, "finish_reason") — content_filter vs length tells you the remedy immediately.
  2. 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.
  3. For length: raise max tokens or shrink MAX_CHARS_PER_CHUNK so a single page fits.
  4. For gateways: verify with a minimal curl chat completion that the endpoint returns content for json_object mode at all.

Example fix

# before
content = response.dig("choices", 0, "message", "content")
raise Provider::Openai::Error, "No response from AI" if content.blank?

# after
choice = response.dig("choices", 0)
content = choice&.dig("message", "content")
if content.blank?
  raise Provider::Openai::Error, "No response from AI (finish_reason=#{choice&.dig("finish_reason")})"
end
Defensive patterns

Strategy: retry

Try / catch

attempts = 0
begin
  process_chunk(chunk, first)
rescue Provider::Openai::Error => e
  raise unless e.message.include?("No response from AI") && (attempts += 1) <= 1
  retry # transient empty completion; give the model a second chance
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/4fc35eb68ae37c89. Report an issue: GitHub.