{"record":{"id":"24a29a4564fa430f","repo":"we-promise/sure","slug":"no-message-content-found-in-response","errorCode":null,"errorMessage":"No message content found in response","messagePattern":"No message content found in response","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai/auto_categorizer.rb","lineNumber":347,"sourceCode":"      # Ensure string inputs for string operations\n      input_lower = input.to_s.downcase\n      category_lower = category.to_s.downcase\n\n      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)","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai/auto_categorizer.rb#L329-L365","documentation":"Raised by Provider::Openai::AutoCategorizer#extract_categorizations_native when a Responses-API reply contains no output item of type \"message\" whose content[0].text is present. The native path expects response[\"output\"] to be an array with a message item holding the JSON string; nil means the model produced only reasoning items, an empty output, or a refusal-shaped response. It fires before JSON parsing, so this is 'no answer text at all', distinct from error 351 (text present but unparseable).","triggerScenarios":"A reasoning model (o-series) exhausting output on reasoning items or hitting max_output_tokens before emitting the message; response.output filtered to tool/reasoning-only items; an API or gateway returning a nonstandard Responses-shaped body; model returning a refusal with empty content.","commonSituations":"Switching auto-categorization to an o1/o3-style model with a small token budget; an OpenAI-compatible gateway that mimics /v1/responses imperfectly; truncation from long transaction batches; upstream API changes to the Responses payload shape.","solutions":["Inspect the full response object (log response[\"output\"].map { |o| o[\"type\"] }) to see whether only reasoning items came back.","Raise max_output_tokens / reduce batch size (LLM_MAX_ITEMS_PER_CALL) so reasoning models finish and emit the message item.","Use a non-reasoning chat model (gpt-4.1) for native mode, or route the custom gateway through the generic extraction path.","If the output shape changed upstream, extend the finder to also accept o.dig(\"content\", 0, \"text\") from other item types."],"exampleFix":"# before\nmessage_output = response[\"output\"]&.find { |o| o[\"type\"] == \"message\" }\nraw = message_output&.dig(\"content\", 0, \"text\")\n\n# after\nmessage_output = response[\"output\"]&.find { |o| o[\"type\"] == \"message\" }\nraw = message_output&.dig(\"content\", 0, \"text\")\nif raw.nil?\n  Rails.logger.error(\"native output items: #{response.dig(\"output\")&.map { |o| o[\"type\"] }.inspect}\")\n  raise Provider::Openai::Error, \"No message content found in response\"\nend","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  categorizations = extractor.extract_categorizations_native(response)\nrescue Provider::Openai::Error => e\n  raise unless e.message.include?(\"No message content\")\n  categorizations = extractor.extract_categorizations_generic(fallback_chat_response)\nend","preventionTips":["Log output item types on every native-path failure to spot reasoning-only responses early.","Budget max_output_tokens generously for reasoning models so the message item is actually emitted.","Prefer non-reasoning chat models for structured extraction on the native path."],"tags":["openai","responses-api","llm","empty-response","auto-categorization"],"backgroundTag":"llm-empty-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}