we-promise/sure · error · Provider::Anthropic::Error

Model did not invoke #{TOOL_NAME}

Error message

Model did not invoke #{TOOL_NAME}

What it means

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.

Source

Thrown at app/models/provider/anthropic/auto_categorizer.rb:133

    def user_message
      <<~MESSAGE.strip_heredoc
        Here are the user's available categories in JSON:

        ```json
        #{user_categories.to_json}
        ```

        Auto-categorize the following transactions:

        ```json
        #{transactions.to_json}
        ```
      MESSAGE
    end

    def extract_categorizations(response)
      tool_use = Array(response.content).find { |block| block_type(block) == :tool_use }
      raise Provider::Anthropic::Error, "Model did not invoke #{TOOL_NAME}" unless tool_use

      input = block_input(tool_use)
      input = JSON.parse(input) if input.is_a?(String)
      categorizations = input.is_a?(Hash) ? (input["categorizations"] || input[:categorizations]) : nil

      raise Provider::Anthropic::Error, "Tool call missing categorizations" unless categorizations.is_a?(Array)
      categorizations
    end

    def build_response(categorizations)
      categorizations.map do |c|
        category_name = c["category_name"] || c[:category_name]
        AutoCategorization.new(
          transaction_id: c["transaction_id"] || c[:transaction_id],
          category_name: normalize_category(category_name)
        )
      end
    end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry the call once — transient non-tool responses are common and usually succeed on the second attempt.
  2. 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.
  3. 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.
  4. If routing through a custom proxy, confirm it forwards tool definitions and tool_choice faithfully.

Example fix

# before
result = Provider::Anthropic::AutoCategorizer.new(client, model: model, transactions: txns, user_categories: cats, family: family).auto_categorize

# after
begin
  result = Provider::Anthropic::AutoCategorizer.new(client, model: model, transactions: txns, user_categories: cats, family: family).auto_categorize
rescue Provider::Anthropic::Error => e
  raise unless e.message.include?("did not invoke")
  retry_once { same_call } # transient: retry, then surface to the job
end
Defensive patterns

Strategy: retry

Try / catch

attempts = 0
begin
  result = provider.auto_categorize(transactions: txns, user_categories: cats, family: family)
rescue Provider::Anthropic::Error => e
  attempts += 1
  retry if attempts < 2 # transient tool-less responses usually clear once
  raise
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/b2f46f14138f1530. Report an issue: GitHub.