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

Model did not invoke #{TOOL_NAME}

Error message

Model did not invoke #{TOOL_NAME}

What it means

BankStatementExtractor#extract_tool_input scans the Messages response content for a :tool_use block; if none exists it raises "Model did not invoke report_bank_statement". The extractor forces the report_bank_statement tool so the statement structure comes back as JSON; a prose answer, a refusal ('do not invent values' pushed too far), or a response truncated by max_tokens before the tool call leaves the block missing.

Source

Thrown at app/models/provider/anthropic/bank_statement_extractor.rb:151

      <<~INSTRUCTIONS
        Extract bank statement data from the attached PDF and return the result via the report_bank_statement tool.

        Rules:
          - Extract EVERY transaction in document order
          - Negative amounts for debits / expenses, positive for credits / deposits
          - Dates in YYYY-MM-DD
          - Use null for any field you cannot read; do not invent values
      INSTRUCTIONS
    end

    def stop_reason(response)
      raw = response.respond_to?(:stop_reason) ? response.stop_reason : nil
      raw.to_s.to_sym if raw
    end

    def extract_tool_input(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)
      input
    end

    def build_result(parsed)
      # Intentionally NOT deduplicated, unlike Provider::Openai's extractor. That
      # one chunks the PDF text with overlap and must drop transactions repeated
      # across adjacent chunks. We send the whole PDF as a single native document
      # block — no chunk artifacts — so deduping here would wrongly merge
      # legitimate same-day, same-amount rows (e.g. two identical purchases).
      # Preserve every transaction the model returns.
      transactions = Array(parsed["transactions"] || parsed[:transactions]).map { |t| normalize_transaction(t) }.compact

      {
        transactions: transactions,
        period: {

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry the extraction once — tool-less responses are frequently one-off.
  2. Check the Langfuse span (extract_bank_statement_api_call) for stop_reason: if max_tokens, raise max_tokens or split the PDF into smaller documents.
  3. Use a current strong model (claude-sonnet-4-6 class) that reliably honors forced tool_choice.
  4. Verify custom-endpoint proxies forward tool definitions and tool_choice.

Example fix

# before
result = extractor.extract

# after
begin
  result = extractor.extract
rescue Provider::Anthropic::Error => e
  retry if (e.message.include?("did not invoke") && (attempts += 1) < 2)
  raise
end
Defensive patterns

Strategy: retry

Try / catch

attempts = 0
begin
  result = extractor.extract
rescue Provider::Anthropic::Error => e
  attempts += 1
  retry if attempts < 2 && e.message.include?("did not invoke")
  raise
end

Prevention

When it happens

Trigger: The model narrates the statement in text instead of calling the tool; max_tokens cuts the response (large statements produce large tool arguments); a custom proxy drops tool_choice; degraded image-only PDFs make the model hedge with prose.

Common situations: Huge statements whose tool payload exceeds the output cap; weak/legacy model behind a custom endpoint; proxy translation layer losing tool parameters; transient model behavior fixed by a retry.

Related errors


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