{"record":{"id":"ae6c3fcee02588cf","repo":"we-promise/sure","slug":"tool-call-missing-categorizations","errorCode":null,"errorMessage":"Tool call missing categorizations","messagePattern":"Tool call missing categorizations","errorType":"exception","errorClass":"Provider::Anthropic::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/anthropic/auto_categorizer.rb","lineNumber":139,"sourceCode":"        ```\n\n        Auto-categorize the following transactions:\n\n        ```json\n        #{transactions.to_json}\n        ```\n      MESSAGE\n    end\n\n    def extract_categorizations(response)\n      tool_use = Array(response.content).find { |block| block_type(block) == :tool_use }\n      raise Provider::Anthropic::Error, \"Model did not invoke #{TOOL_NAME}\" unless tool_use\n\n      input = block_input(tool_use)\n      input = JSON.parse(input) if input.is_a?(String)\n      categorizations = input.is_a?(Hash) ? (input[\"categorizations\"] || input[:categorizations]) : nil\n\n      raise Provider::Anthropic::Error, \"Tool call missing categorizations\" unless categorizations.is_a?(Array)\n      categorizations\n    end\n\n    def build_response(categorizations)\n      categorizations.map do |c|\n        category_name = c[\"category_name\"] || c[:category_name]\n        AutoCategorization.new(\n          transaction_id: c[\"transaction_id\"] || c[:transaction_id],\n          category_name: normalize_category(category_name)\n        )\n      end\n    end\n\n    def normalize_category(value)\n      return nil if value.nil?\n      str = value.to_s.strip\n      return nil if str.empty? || str.casecmp(\"null\").zero?\n","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/anthropic/auto_categorizer.rb#L121-L157","documentation":"The model did invoke the report_categorizations tool, but block_input parsed to a Hash without a usable \"categorizations\" Array (string inputs are JSON.parse'd first, symbol and string keys are both accepted). This raises \"Tool call missing categorizations\" — the tool was called but its arguments have the wrong shape, e.g. a bare array, a differently-named key, or an empty payload.","triggerScenarios":"The model calls the tool with {\"results\": [...]} or a top-level array instead of {\"categorizations\": [...]}; a partially-truncated tool argument at the max_tokens boundary parses to an incomplete Hash; a proxy/gateway rewrites or mangles tool input JSON.","commonSituations":"Schema drift between the tool definition and what a newer/older model produces; forced tool_choice with an under-specified tool schema; flaky one-off malformed output that a retry fixes; gateways that re-serialize tool arguments and drop keys.","solutions":["Retry the request — isolated malformed tool args are usually transient.","Tighten the tool's input schema for the categorizations field (type, required) so the model is steered to the exact key.","Switch to a current, stronger model (claude-sonnet-4-6 class) known to follow the tool schema; lower batch size to avoid truncated arguments.","Compare the raw tool arguments in the Langfuse trace against the expected schema to find the exact divergence."],"exampleFix":"# tool definition (conceptual) — before\n{ name: \"report_categorizations\", input_schema: { type: \"object\" } }\n# model returns { \"results\": [...] } => \"Tool call missing categorizations\"\n\n# after\n{ name: \"report_categorizations\",\n  input_schema: { type: \"object\", required: [\"categorizations\"],\n    properties: { categorizations: { type: \"array\", items: { type: \"object\" } } } } }","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"attempts = 0\nbegin\n  result = categorizer.auto_categorize\nrescue Provider::Anthropic::Error => e\n  attempts += 1\n  retry if attempts < 2 && e.message.include?(\"missing categorizations\")\n  raise\nend","preventionTips":["Make the tool schema strict: categorizations required, items typed.","Pin a current model; weaker models drift from schemas more often.","Compare the raw tool input in Langfuse against the schema when it repeats — fix the schema, not the retry count."],"tags":["anthropic","llm","tool-use","schema-mismatch","auto-categorization"],"backgroundTag":"llm-malformed-tool-response","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}