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

Could not find categorizations in response

Error message

Could not find categorizations in response

What it means

Raised by Provider::Openai::AutoCategorizer#extract_categorizations_generic when the chat-completions content parses to JSON but contains none of the accepted shapes: a categorizations key, a results key, or a top-level array. This path exists precisely because different models name things differently; when the model returns, say, {"transactions": [...]} or a per-transaction object keyed by id, the extractor gives up. It runs after parse_json_flexibly succeeded, so the JSON itself was valid.

Source

Thrown at app/models/provider/openai/auto_categorizer.rb:363

      raw = message_output&.dig("content", 0, "text")

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

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

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

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

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

      # Normalize field names (some LLMs use different naming)
      categorizations.map do |cat|
        {
          "transaction_id" => cat["transaction_id"] || cat["id"] || cat["txn_id"],
          "category_name" => cat["category_name"] || cat["category"] || cat["name"]
        }
      end
    end

    # Flexible JSON parsing that handles common LLM output issues
    def parse_json_flexibly(raw)
      return {} if raw.blank?

      # Strip thinking model tags if present (e.g., <think>...</think>)
      # The actual JSON output comes after the thinking block
      cleaned = strip_thinking_tags(raw)

View on GitHub (pinned to e69894adb9)

Solutions

  1. Log the parsed JSON keys at the failure point to learn the model's actual field name.
  2. Add the observed key to the accepted list (e.g. parsed.dig("transactions")) or normalize nested shapes before the nil check.
  3. Make the prompt include a literal example response {"categorizations": [{"transaction_id": ..., "category_name": ...}]} — few-shot shape anchoring fixes most renames.
  4. For custom gateways, prefer a model/endpoint that follows the demonstrated schema, or use JSON schema/response_format strictness.

Example fix

# before
categorizations = parsed.dig("categorizations") ||
                  parsed.dig("results") ||
                  (parsed.is_a?(Array) ? parsed : nil)

# after
categorizations = parsed.dig("categorizations") ||
                  parsed.dig("results") ||
                  parsed.dig("transactions") ||
                  (parsed.is_a?(Array) ? parsed : nil)
Defensive patterns

Strategy: fallback

Try / catch

begin
  extract_categorizations_generic(response)
rescue Provider::Openai::Error => e
  raise unless e.message.include?("Could not find categorizations")
  [] # treat as 'model had no categorizations' and surface partial results
end

Prevention

When it happens

Trigger: Model returns {"transactions": [{id, category}]} instead of categorizations/results; model nests one level deeper ({"output": {"categorizations": ...}}); model answers with an object of transaction_id -> category_name pairs; empty object {} for a batch the model 'had no opinion' about.

Common situations: Swapping the LLM behind a custom uri_base (Llama/Qwen/DeepSeek naming habits differ from GPT); prompt edits that paraphrase the requested schema; models returning an error note as JSON like {"error": "too many transactions"}.

Related errors


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