Module: RubyLLM::BedrockInvoke::ToolSearch

Defined in:
lib/ruby_llm/bedrock_invoke/tool_search.rb

Overview

Payload shaping for Anthropic's tool search tool on Bedrock InvokeModel.

Bedrock exposes tool search only through InvokeModel (not Converse), as a beta: the request body needs anthropic_beta: ["tool-search-tool-2025-10-19"] and the unversioned tool_search_tool_regex tool type (the first-party API uses versioned types and no beta flag; no BM25 variant exists on Bedrock). Tools marked deferred still send their full definition every request with defer_loading: true — they are excluded from the cached prompt prefix until the model discovers them via the search tool.

Constant Summary collapse

BETA_FLAG =
'tool-search-tool-2025-10-19'
SEARCH_TOOL_TYPE =
'tool_search_tool_regex'
SEARCH_TOOL_NAME =
'tool_search_tool_regex'
SERVER_BLOCK_TYPES =
%w[server_tool_use tool_search_tool_result].freeze

Class Method Summary collapse

Class Method Details

.apply!(payload, tools) ⇒ Object

Mutates a rendered Anthropic Messages payload: flags deferred tool definitions, prepends the search tool, and opts into the beta. Tools may be marked via the Deferred module or by carrying defer_loading in their own provider params. No-op when nothing is deferred.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 36

def apply!(payload, tools)
  names = deferred_names(tools)
  definitions = payload[:tools]

  if definitions.nil?
    return payload if names.empty?

    raise RubyLLM::BedrockInvoke::Error, 'Deferred tools present but payload has no tools'
  end

  definitions.each do |definition|
    name = definition_value(definition, :name).to_s
    definition[:defer_loading] = true if names.include?(name)
  end

  return payload unless definitions.any? { |d| deferred_definition?(d) }

  inject_search_tool!(definitions)
  payload[:anthropic_beta] = Array(payload[:anthropic_beta]) | [BETA_FLAG]
  payload
end

.block_type(block) ⇒ Object



80
81
82
83
84
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 80

def block_type(block)
  return nil unless block.is_a?(Hash)

  (block[:type] || block['type']).to_s
end

.contains_server_blocks?(blocks) ⇒ Boolean

Returns:

  • (Boolean)


76
77
78
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 76

def contains_server_blocks?(blocks)
  Array(blocks).any? { |block| server_block?(block) }
end

.deferred?(tool) ⇒ Boolean

Returns:

  • (Boolean)


22
23
24
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 22

def deferred?(tool)
  tool.respond_to?(:defer_loading?) && tool.defer_loading?
end

.deferred_definition?(definition) ⇒ Boolean

Returns:

  • (Boolean)


86
87
88
89
90
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 86

def deferred_definition?(definition)
  return false unless definition.is_a?(Hash)

  !!definition_value(definition, :defer_loading)
end

.deferred_names(tools) ⇒ Object

tools is RubyLLM's name => tool hash; returns name strings.



27
28
29
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 27

def deferred_names(tools)
  tools.values.select { |tool| deferred?(tool) }.map { |tool| tool.name.to_s }
end

.definition_value(definition, key) ⇒ Object



98
99
100
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 98

def definition_value(definition, key)
  definition[key] || definition[key.to_s]
end

.ensure_beta!(payload) ⇒ Object

Both generations deep-merge user params (with_params / with_provider_options) over the rendered payload AFTER apply! runs, and deep_merge REPLACES arrays — silently dropping the beta flag. Called on the final merged payload right before the request goes out.



62
63
64
65
66
67
68
69
70
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 62

def ensure_beta!(payload)
  definitions = payload[:tools] || payload['tools']
  return payload unless definitions.is_a?(Array)
  return payload unless definitions.any? { |d| deferred_definition?(d) || search_tool_definition?(d) }

  betas = Array(payload.delete('anthropic_beta')) | Array(payload[:anthropic_beta]) | [BETA_FLAG]
  payload[:anthropic_beta] = betas
  payload
end

.inject_search_tool!(definitions) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 102

def inject_search_tool!(definitions)
  return if definitions.any? { |d| search_tool_definition?(d) }

  collision = definitions.find { |d| definition_value(d, :name).to_s == SEARCH_TOOL_NAME }
  if collision
    raise RubyLLM::BedrockInvoke::Error,
          "A tool named '#{SEARCH_TOOL_NAME}' collides with the injected tool search tool. Rename it."
  end

  definitions.unshift(type: SEARCH_TOOL_TYPE, name: SEARCH_TOOL_NAME)
end

.search_tool_definition?(definition) ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
95
96
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 92

def search_tool_definition?(definition)
  return false unless definition.is_a?(Hash)

  definition_value(definition, :type) == SEARCH_TOOL_TYPE
end

.server_block?(block) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
# File 'lib/ruby_llm/bedrock_invoke/tool_search.rb', line 72

def server_block?(block)
  SERVER_BLOCK_TYPES.include?(block_type(block))
end