{"record":{"id":"b2f46f14138f1530","repo":"we-promise/sure","slug":"model-did-not-invoke-tool-name","errorCode":null,"errorMessage":"Model did not invoke #{TOOL_NAME}","messagePattern":"Model did not invoke #(.+?)","errorType":"exception","errorClass":"Provider::Anthropic::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/anthropic/auto_categorizer.rb","lineNumber":133,"sourceCode":"    def user_message\n      <<~MESSAGE.strip_heredoc\n        Here are the user's available categories in JSON:\n\n        ```json\n        #{user_categories.to_json}\n        ```\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","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/anthropic/auto_categorizer.rb#L115-L151","documentation":"After the messages API call, AutoCategorizer#extract_categorizations scans response.content for a :tool_use block; if none exists it raises \"Model did not invoke report_categorizations\". The categorizer relies on Anthropic tool calling (tool_choice forcing the report_categorizations tool), so a response that is plain text, truncated before the tool call (max_tokens/stop_reason), or a refusal means the expected tool block is missing.","triggerScenarios":"The model replies with prose instead of calling the tool; the response is cut off by a max_tokens cap before emitting the tool_use block (stop_reason \"max_tokens\"); a weak model behind a custom proxy ignores forced tool_choice; prompt text inside transaction memo fields steers the model off-format.","commonSituations":"Downgrading to a cheaper/legacy model that handles forced tool calls poorly; very long transaction lists consuming output budget; custom OpenAI-compatible-to-Anthropic bridge that drops tool_choice; intermittent provider behavior needing one retry.","solutions":["Retry the call once — transient non-tool responses are common and usually succeed on the second attempt.","Verify the request actually forces the tool (tool_choice set for report_categorizations) and that the tool schema reaches the API — inspect the Langfuse trace created by auto_categorize.","Use a stronger/current default model (e.g. claude-sonnet-4-6) or reduce the batch size so the model finishes its tool call within max_tokens.","If routing through a custom proxy, confirm it forwards tool definitions and tool_choice faithfully."],"exampleFix":"# before\nresult = Provider::Anthropic::AutoCategorizer.new(client, model: model, transactions: txns, user_categories: cats, family: family).auto_categorize\n\n# after\nbegin\n  result = Provider::Anthropic::AutoCategorizer.new(client, model: model, transactions: txns, user_categories: cats, family: family).auto_categorize\nrescue Provider::Anthropic::Error => e\n  raise unless e.message.include?(\"did not invoke\")\n  retry_once { same_call } # transient: retry, then surface to the job\nend","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"attempts = 0\nbegin\n  result = provider.auto_categorize(transactions: txns, user_categories: cats, family: family)\nrescue Provider::Anthropic::Error => e\n  attempts += 1\n  retry if attempts < 2 # transient tool-less responses usually clear once\n  raise\nend","preventionTips":["Keep batches well under 25 and max_tokens generous so tool calls aren't truncated.","Default to a current strong model that honors forced tool_choice.","Check the Langfuse trace when it recurs — stop_reason max_tokens means raise the token cap, not retry."],"tags":["anthropic","llm","tool-use","auto-categorization","response-parsing"],"backgroundTag":"llm-tool-call-failure","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}