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

No message content found in response

Error message

No message content found in response

What it means

Raised by Provider::Openai::AutoMerchantDetector#extract_merchants_native when a Responses-API reply has no output item of type "message" with content[0].text — the merchant-detection twin of error 350. The native path digs response["output"] for the message item and takes its first content text as the JSON string; nil means only reasoning/tool items, an empty output array, or a nonconforming gateway response. The subsequent JSON.parse has its own error (Invalid JSON in native merchant detection), so this specifically means no text was found at all.

Source

Thrown at app/models/provider/openai/auto_merchant_detector.rb:305

      if user_merchants.present?
        # Try exact match first
        exact_match = user_merchants.find { |m| m[:name] == value }
        return exact_match[:name] if exact_match

        # Try case-insensitive match
        case_match = user_merchants.find { |m| m[:name].to_s.downcase == value.to_s.downcase }
        return case_match[:name] if case_match
      end

      value
    end

    def extract_merchants_native(response)
      # Find the message output (not reasoning output)
      message_output = response["output"]&.find { |o| o["type"] == "message" }
      raw = message_output&.dig("content", 0, "text")

      raise Provider::Openai::Error, "No message content found in response" if raw.nil?

      JSON.parse(raw).dig("merchants")
    rescue JSON::ParserError => e
      raise Provider::Openai::Error, "Invalid JSON in native merchant detection: #{e.message}"
    end

    def extract_merchants_generic(response)
      raw = response.dig("choices", 0, "message", "content")
      parsed = parse_json_flexibly(raw)

      # Handle different response formats from various LLMs
      merchants = parsed.dig("merchants") ||
                  parsed.dig("results") ||
                  (parsed.is_a?(Array) ? parsed : nil)

      raise Provider::Openai::Error, "Could not find merchants in response" if merchants.nil?

      # Normalize field names (some LLMs use different naming)

View on GitHub (pinned to e69894adb9)

Solutions

  1. Log response["output"].map { |o| o["type"] } to confirm whether reasoning-only output is the cause.
  2. Increase the output token budget or shrink the transaction batch for merchant detection.
  3. Use a standard chat model (gpt-4.1) for the native path or route the gateway through generic extraction.
  4. Extend the finder to tolerate adjacent shapes (e.g. summary items carrying content) if the gateway emits them.

Example fix

# before
raw = message_output&.dig("content", 0, "text")
raise Provider::Openai::Error, "No message content found in response" if raw.nil?

# after
raw = message_output&.dig("content", 0, "text")
if raw.nil?
  Rails.logger.warn("merchant native output types: #{response.dig("output")&.map { |o| o["type"] }.inspect} status=#{response.dig("status")}")
  raise Provider::Openai::Error, "No message content found in response"
end
Defensive patterns

Strategy: fallback

Try / catch

begin
  merchants = extractor.extract_merchants_native(response)
rescue Provider::Openai::Error => e
  raise unless e.message.include?("No message content")
  merchants = [] # degrade gracefully; user keeps manual merchant matching
end

Prevention

When it happens

Trigger: o-series reasoning model spending the entire output budget on reasoning items; custom OpenAI-compatible gateway returning a Responses body without a message item; model refusal with empty content; max_output_tokens truncation before the message item is emitted.

Common situations: Merchant detection switched to a reasoning model with a tight budget; gateway partially implementing /v1/responses; long merchant lists pushing output past the limit; upstream Responses payload changes.

Related errors


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