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

Tool call missing merchants

Error message

Tool call missing merchants

What it means

The model did call report_enhancements, but the parsed tool input is not a Hash with a "merchants" Array (string inputs are JSON.parse'd; string and symbol keys accepted). "Tool call missing merchants" means the tool arguments have the wrong shape — a renamed key, a bare array, or truncated JSON — so the enhancement list cannot be built.

Source

Thrown at app/models/provider/anthropic/provider_merchant_enhancer.rb:117

    def user_message
      <<~MESSAGE.strip_heredoc
        Enhance the following merchants by identifying each one's website URL:

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

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

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

    def build_response(enhanced)
      enhanced.map do |m|
        EnhancedMerchant.new(
          merchant_id: m["merchant_id"] || m[:merchant_id],
          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
    end

View on GitHub (pinned to e69894adb9)

Solutions

  1. Retry — single malformed responses usually do not repeat.
  2. Require "merchants" in the tool's input_schema with typed items so the model must use the exact key.
  3. Switch to a current model and shrink the batch so tool arguments are not truncated.
  4. Diff the raw tool input in the Langfuse trace against the schema to identify the renames.

Example fix

# tool schema — before
{ type: "object", properties: { merchants: { type: "array" } } }
# model sends { "results": [...] } => "Tool call missing merchants"

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

Strategy: retry

Try / catch

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

Prevention

When it happens

Trigger: The model returns {"enhanced": [...]} or a top-level array instead of {"merchants": [...]}; tool arguments get cut at max_tokens on larger batches and parse to an incomplete Hash; middleware on a proxy re-keys JSON tool input.

Common situations: Tool schema not marking "merchants" required, letting the model improvise key names; smaller/older models deviating; one-off malformed output; gateways that transform JSON keys.

Related errors


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