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

Tool call missing merchants

Error message

Tool call missing merchants

What it means

The model invoked report_merchants, but its parsed input is not a Hash containing a "merchants" Array (string inputs are JSON.parse'd; string and symbol keys both accepted). "Tool call missing merchants" means the tool arguments came back with the wrong shape — a bare array, a renamed key, or truncated JSON — so no merchant list can be built.

Source

Thrown at app/models/provider/anthropic/auto_merchant_detector.rb:141

        ```

        Transactions to analyze:

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

    def extract_merchants(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)
      merchants = input.is_a?(Hash) ? (input["merchants"] || input[:merchants]) : nil

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

    def build_response(merchants)
      merchants.map do |m|
        AutoDetectedMerchant.new(
          transaction_id: m["transaction_id"] || m[:transaction_id],
          business_name: normalize_merchant_name(m["business_name"] || m[:business_name]),
          business_url: normalize_value(m["business_url"] || m[:business_url])
        )
      end
    end

    def normalize_value(value)
      return nil if value.nil?
      str = value.to_s.strip
      return nil if str.empty? || str.casecmp("null").zero?
      str

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry — isolated malformed args are typically transient.
  2. Make the tool schema strict: require the "merchants" property with item types so the model must use the exact key.
  3. Upgrade to a current model (claude-sonnet-4-6 class) and reduce batch size to prevent truncated arguments.
  4. Inspect the tool input in the Langfuse trace to see the exact shape the model sent.

Example fix

# tool schema — before
{ type: "object", properties: { merchants: { type: "array" } } } # not required
# model omits "merchants" => "Tool call missing merchants"

# after
{ type: "object", required: ["merchants"],
  properties: { merchants: { type: "array", items: { type: "object",
    properties: { transaction_id: { type: "string" }, business_name: { type: "string" }, business_url: { type: "string" } } } } } }
Defensive patterns

Strategy: retry

Try / catch

attempts = 0
begin
  result = detector.auto_detect_merchants
rescue Provider::Anthropic::Error => e
  attempts += 1
  retry if attempts < 2 && e.message.include?("missing merchants")
  raise
end

Prevention

When it happens

Trigger: Model returns {"detected_merchants": [...]} or a top-level array instead of {"merchants": [...]}; tool arguments get cut at max_tokens and JSON.parse yields a partial Hash; a proxy rewrites tool input keys.

Common situations: Loose tool schema letting the model pick its own key name; older/smaller models deviating from schema; one-off malformed responses; gateway middleware normalizing JSON keys (e.g. camelize).

Related errors


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