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

Invalid JSON in native categorization: #{e.message}

Error message

Invalid JSON in native categorization: #{e.message}

What it means

Raised by Provider::Openai::AutoCategorizer#extract_categorizations_native when the native Responses-API message text exists but JSON.parse fails on it. The native path assumes strict JSON (the request asks for JSON output), unlike the generic path which uses parse_json_flexibly to strip code fences and prose. Any deviation — markdown fences around the JSON, a leading sentence, or truncated JSON — produces JSON::ParserError whose message is embedded into the raised error.

Source

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

      variations.each do |_key, synonyms|
        if synonyms.include?(input_lower) && synonyms.include?(category_lower)
          return true
        end
      end

      false
    end

    def extract_categorizations_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("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"]

View on GitHub (pinned to e69894adb9)

Solutions

  1. Route the response through the generic extractor's parse_json_flexibly (strip fences/prose) when native JSON.parse fails, instead of raising immediately.
  2. Ensure the native request actually sets strict JSON schema/response_format and that the endpoint honors it (first-party OpenAI does).
  3. Increase the response token budget so JSON is not truncated.
  4. Tighten the prompt: 'Respond with a single JSON object, no markdown, no commentary.'

Example fix

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

# after
begin
  JSON.parse(raw).dig("categorizations")
rescue JSON::ParserError => e
  parsed = parse_json_flexibly(raw) # reuses fence/prose stripping
  raise Provider::Openai::Error, "Invalid JSON in native categorization: #{e.message}" if parsed.nil?
  parsed.dig("categorizations")
end
Defensive patterns

Strategy: fallback

Try / catch

begin
  JSON.parse(raw)
rescue JSON::ParserError
  parsed = parse_json_flexibly(raw) # generic fence/prose stripping as second chance
  raise if parsed.nil?
  parsed
end

Prevention

When it happens

Trigger: Model wraps the JSON in ```json fences despite response_format; model prefixes 'Here is the JSON:' prose; JSON truncated mid-object because output tokens ran out; custom OpenAI-compatible provider ignoring strict JSON mode and emitting thinking prose.

Common situations: Routing non-OpenAI models (via uri_base) through the native responses path where JSON mode is not enforced; low max_response_tokens on large transaction batches; prompt changes that make the model chatty; gateway middleware appending text.

Understand the failure class

Related errors


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