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
- Log the parsed JSON keys at the failure point to learn the model's actual field name.
- Add the observed key to the accepted list (e.g. parsed.dig("transactions")) or normalize nested shapes before the nil check.
- Make the prompt include a literal example response {"categorizations": [{"transaction_id": ..., "category_name": ...}]} — few-shot shape anchoring fixes most renames.
- 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
- Anchor the schema with a one-shot example response in the prompt when swapping models.
- Log parsed top-level keys whenever extraction fails so new field names get added deliberately.
- Keep the accepted-key list in sync with the models you route through the generic path.
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
- Invalid JSON in native categorization: #{e.message}
- Tool call missing categorizations
- No categories available for auto-categorization
- No message content found in response
- Could not parse JSON from response: #{raw.truncate(200)}
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/7e82d8c2797fca36.
Report an issue: GitHub.