{"record":{"id":"500e7efda09f0ae7","repo":"we-promise/sure","slug":"invalid-json-in-native-categorization-e-message","errorCode":null,"errorMessage":"Invalid JSON in native categorization: #{e.message}","messagePattern":"Invalid JSON in native categorization: #(.+?)","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai/auto_categorizer.rb","lineNumber":351,"sourceCode":"      variations.each do |_key, synonyms|\n        if synonyms.include?(input_lower) && synonyms.include?(category_lower)\n          return true\n        end\n      end\n\n      false\n    end\n\n    def extract_categorizations_native(response)\n      # Find the message output (not reasoning output)\n      message_output = response[\"output\"]&.find { |o| o[\"type\"] == \"message\" }\n      raw = message_output&.dig(\"content\", 0, \"text\")\n\n      raise Provider::Openai::Error, \"No message content found in response\" if raw.nil?\n\n      JSON.parse(raw).dig(\"categorizations\")\n    rescue JSON::ParserError => e\n      raise Provider::Openai::Error, \"Invalid JSON in native categorization: #{e.message}\"\n    end\n\n    def extract_categorizations_generic(response)\n      raw = response.dig(\"choices\", 0, \"message\", \"content\")\n      parsed = parse_json_flexibly(raw)\n\n      # Handle different response formats from various LLMs\n      categorizations = parsed.dig(\"categorizations\") ||\n                        parsed.dig(\"results\") ||\n                        (parsed.is_a?(Array) ? parsed : nil)\n\n      raise Provider::Openai::Error, \"Could not find categorizations in response\" if categorizations.nil?\n\n      # Normalize field names (some LLMs use different naming)\n      categorizations.map do |cat|\n        {\n          \"transaction_id\" => cat[\"transaction_id\"] || cat[\"id\"] || cat[\"txn_id\"],\n          \"category_name\" => cat[\"category_name\"] || cat[\"category\"] || cat[\"name\"]","sourceCodeStart":333,"sourceCodeEnd":369,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai/auto_categorizer.rb#L333-L369","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Route the response through the generic extractor's parse_json_flexibly (strip fences/prose) when native JSON.parse fails, instead of raising immediately.","Ensure the native request actually sets strict JSON schema/response_format and that the endpoint honors it (first-party OpenAI does).","Increase the response token budget so JSON is not truncated.","Tighten the prompt: 'Respond with a single JSON object, no markdown, no commentary.'"],"exampleFix":"# before\nJSON.parse(raw).dig(\"categorizations\")\nrescue JSON::ParserError => e\n  raise Provider::Openai::Error, \"Invalid JSON in native categorization: #{e.message}\"\n\n# after\nbegin\n  JSON.parse(raw).dig(\"categorizations\")\nrescue JSON::ParserError => e\n  parsed = parse_json_flexibly(raw) # reuses fence/prose stripping\n  raise Provider::Openai::Error, \"Invalid JSON in native categorization: #{e.message}\" if parsed.nil?\n  parsed.dig(\"categorizations\")\nend","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  JSON.parse(raw)\nrescue JSON::ParserError\n  parsed = parse_json_flexibly(raw) # generic fence/prose stripping as second chance\n  raise if parsed.nil?\n  parsed\nend","preventionTips":["Ensure the endpoint truly enforces JSON mode before relying on strict JSON.parse on model text.","Route custom gateways through the generic extractor, which tolerates fences and prose.","Include a literal JSON example in the prompt to anchor the output format."],"tags":["openai","json-parsing","llm-output","auto-categorization"],"backgroundTag":"llm-invalid-json","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}