{"record":{"id":"4b2aefb32b2b12dc","repo":"we-promise/sure","slug":"could-not-parse-json-from-response-raw-truncate","errorCode":null,"errorMessage":"Could not parse JSON from response: #{raw.truncate(200)}","messagePattern":"Could not parse JSON from response: #(.+?)","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai/auto_categorizer.rb","lineNumber":436,"sourceCode":"        end\n        # Try greedy match if non-greedy failed\n        begin\n          return JSON.parse($1)\n        rescue JSON::ParserError\n          # Continue to next strategy\n        end\n      end\n\n      # Strategy 4: Find any JSON object (last resort)\n      if cleaned =~ /(\\{[\\s\\S]*\\})/m\n        begin\n          return JSON.parse($1)\n        rescue JSON::ParserError\n          # Fall through to error\n        end\n      end\n\n      raise Provider::Openai::Error, \"Could not parse JSON from response: #{raw.truncate(200)}\"\n    end\n\n    # Strip thinking model tags (<think>...</think>) from response\n    # Some models like Qwen-thinking output reasoning in these tags before the actual response\n    def strip_thinking_tags(raw)\n      # Remove <think>...</think> blocks but keep content after them\n      # If no closing tag, the model may have been cut off - try to extract JSON from inside\n      if raw.include?(\"<think>\")\n        # Check if there's content after the thinking block\n        if raw =~ /<\\/think>\\s*([\\s\\S]*)/m\n          after_thinking = $1.strip\n          return after_thinking if after_thinking.present?\n        end\n        # If no content after </think> or no closing tag, look inside the thinking block\n        # The JSON might be the last thing in the thinking block\n        if raw =~ /<think>([\\s\\S]*)/m\n          return $1\n        end","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai/auto_categorizer.rb#L418-L454","documentation":"Raised by Provider::Openai::AutoCategorizer#parse_json_flexibly after all four recovery strategies fail: direct JSON.parse, stripping markdown code fences, unwrapping concatenated/fragments, and a last-resort regex grab of {...}. It includes the first 200 chars of the raw text, which is the key diagnostic — truncated JSON from token limits and pure-prose answers are the two dominant causes. strip_thinking_tags already handles <think>...</think> blocks before this point, so unclosed thinking from a cut-off reasoning model also lands here.","triggerScenarios":"Response truncated mid-JSON because max_tokens was hit (unterminated strings/braces); model answers in prose with no JSON at all; reasoning model's <think> block never closed because generation was cut; JSON containing an embedded regex-breaking brace imbalance (rare).","commonSituations":"Large transaction batches against small token budgets; gpt-4.1-mini or third-party models with weaker JSON discipline; Setting.llm_max_items_per_call raised without raising the response budget; custom gateways with lower default max_tokens than OpenAI.","solutions":["Read the truncated raw in the message: an abrupt cut (no closing brace) means raise the token budget or lower LLM_MAX_ITEMS_PER_CALL; prose means strengthen the prompt.","Add json_object response_format (or strict schema) to the request so the model cannot answer in prose.","Retry once with a smaller batch — halving items per call resolves most truncation cases.","If using a thinking model via custom provider, ensure the prompt forces the answer after </think> and the budget covers the reasoning."],"exampleFix":"# before\nraise Provider::Openai::Error, \"Could not parse JSON from response: #{raw.truncate(200)}\"\n\n# after\nif raw.truncated_json?\n  retry_with_smaller_batch # halve batch size and call again\nelse\n  raise Provider::Openai::Error, \"Could not parse JSON from response: #{raw.truncate(200)}\"\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  parsed = parse_json_flexibly(raw)\nrescue Provider::Openai::Error\n  parsed = retry_with(smaller_batch: true) # truncation is the usual cause; halve and re-ask\nend","preventionTips":["Set json_object response_format or a strict schema so prose answers cannot occur.","Size LLM_MAX_ITEMS_PER_CALL against the response token budget so JSON never truncates.","Watch the 200-char raw snippet: unclosed JSON = budget problem, prose = prompt problem."],"tags":["openai","json-parsing","llm-output","truncation","token-limits"],"backgroundTag":"llm-invalid-json","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}