ruby_llm-bedrock_invoke 0.1.0.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module BedrockInvoke
5
+ # Payload shaping for Anthropic's tool search tool on Bedrock InvokeModel.
6
+ #
7
+ # Bedrock exposes tool search only through InvokeModel (not Converse), as
8
+ # a beta: the request body needs `anthropic_beta: ["tool-search-tool-2025-10-19"]`
9
+ # and the *unversioned* `tool_search_tool_regex` tool type (the first-party
10
+ # API uses versioned types and no beta flag; no BM25 variant exists on
11
+ # Bedrock). Tools marked deferred still send their full definition every
12
+ # request with `defer_loading: true` — they are excluded from the cached
13
+ # prompt prefix until the model discovers them via the search tool.
14
+ module ToolSearch
15
+ BETA_FLAG = 'tool-search-tool-2025-10-19'
16
+ SEARCH_TOOL_TYPE = 'tool_search_tool_regex'
17
+ SEARCH_TOOL_NAME = 'tool_search_tool_regex'
18
+ SERVER_BLOCK_TYPES = %w[server_tool_use tool_search_tool_result].freeze
19
+
20
+ module_function
21
+
22
+ def deferred?(tool)
23
+ tool.respond_to?(:defer_loading?) && tool.defer_loading?
24
+ end
25
+
26
+ # +tools+ is RubyLLM's name => tool hash; returns name strings.
27
+ def deferred_names(tools)
28
+ tools.values.select { |tool| deferred?(tool) }.map { |tool| tool.name.to_s }
29
+ end
30
+
31
+ # Mutates a rendered Anthropic Messages payload: flags deferred tool
32
+ # definitions, prepends the search tool, and opts into the beta.
33
+ # Tools may be marked via the Deferred module or by carrying
34
+ # defer_loading in their own provider params. No-op when nothing is
35
+ # deferred.
36
+ def apply!(payload, tools)
37
+ names = deferred_names(tools)
38
+ definitions = payload[:tools]
39
+
40
+ if definitions.nil?
41
+ return payload if names.empty?
42
+
43
+ raise RubyLLM::BedrockInvoke::Error, 'Deferred tools present but payload has no tools'
44
+ end
45
+
46
+ definitions.each do |definition|
47
+ name = definition_value(definition, :name).to_s
48
+ definition[:defer_loading] = true if names.include?(name)
49
+ end
50
+
51
+ return payload unless definitions.any? { |d| deferred_definition?(d) }
52
+
53
+ inject_search_tool!(definitions)
54
+ payload[:anthropic_beta] = Array(payload[:anthropic_beta]) | [BETA_FLAG]
55
+ payload
56
+ end
57
+
58
+ # Both generations deep-merge user params (with_params /
59
+ # with_provider_options) over the rendered payload AFTER apply! runs,
60
+ # and deep_merge REPLACES arrays — silently dropping the beta flag.
61
+ # Called on the final merged payload right before the request goes out.
62
+ def ensure_beta!(payload)
63
+ definitions = payload[:tools] || payload['tools']
64
+ return payload unless definitions.is_a?(Array)
65
+ return payload unless definitions.any? { |d| deferred_definition?(d) || search_tool_definition?(d) }
66
+
67
+ betas = Array(payload.delete('anthropic_beta')) | Array(payload[:anthropic_beta]) | [BETA_FLAG]
68
+ payload[:anthropic_beta] = betas
69
+ payload
70
+ end
71
+
72
+ def server_block?(block)
73
+ SERVER_BLOCK_TYPES.include?(block_type(block))
74
+ end
75
+
76
+ def contains_server_blocks?(blocks)
77
+ Array(blocks).any? { |block| server_block?(block) }
78
+ end
79
+
80
+ def block_type(block)
81
+ return nil unless block.is_a?(Hash)
82
+
83
+ (block[:type] || block['type']).to_s
84
+ end
85
+
86
+ def deferred_definition?(definition)
87
+ return false unless definition.is_a?(Hash)
88
+
89
+ !!definition_value(definition, :defer_loading)
90
+ end
91
+
92
+ def search_tool_definition?(definition)
93
+ return false unless definition.is_a?(Hash)
94
+
95
+ definition_value(definition, :type) == SEARCH_TOOL_TYPE
96
+ end
97
+
98
+ def definition_value(definition, key)
99
+ definition[key] || definition[key.to_s]
100
+ end
101
+
102
+ def inject_search_tool!(definitions)
103
+ return if definitions.any? { |d| search_tool_definition?(d) }
104
+
105
+ collision = definitions.find { |d| definition_value(d, :name).to_s == SEARCH_TOOL_NAME }
106
+ if collision
107
+ raise RubyLLM::BedrockInvoke::Error,
108
+ "A tool named '#{SEARCH_TOOL_NAME}' collides with the injected tool search tool. Rename it."
109
+ end
110
+
111
+ definitions.unshift(type: SEARCH_TOOL_TYPE, name: SEARCH_TOOL_NAME)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module BedrockInvoke
5
+ VERSION = '0.1.0.pre.1'
6
+ end
7
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_llm'
4
+ require 'erb'
5
+
6
+ require_relative 'ruby_llm/bedrock_invoke/version'
7
+ require_relative 'ruby_llm/bedrock_invoke/signing'
8
+ require_relative 'ruby_llm/bedrock_invoke/event_stream'
9
+ require_relative 'ruby_llm/bedrock_invoke/tool_search'
10
+ require_relative 'ruby_llm/bedrock_invoke/deferred'
11
+ require_relative 'ruby_llm/bedrock_invoke/block_collector'
12
+ require_relative 'ruby_llm/bedrock_invoke/stream_state'
13
+ require_relative 'ruby_llm/bedrock_invoke/capabilities'
14
+
15
+ module RubyLLM
16
+ # Anthropic Claude on AWS Bedrock via InvokeModel — tool search (deferred
17
+ # tool loading), structured outputs, prompt caching and streaming inside
18
+ # the AWS data boundary, authenticated with the standard AWS credential chain.
19
+ module BedrockInvoke
20
+ class Error < StandardError; end
21
+
22
+ CONFIGURATION_OPTIONS = %i[
23
+ bedrock_invoke_region
24
+ bedrock_invoke_api_base
25
+ bedrock_invoke_credential_provider
26
+ ].freeze
27
+
28
+ # Where a response carried server-managed tool-search blocks
29
+ # (server_tool_use / tool_search_tool_result), the full content-block
30
+ # array is preserved for verbatim replay. On 1.x it rides in the message
31
+ # content as RawContent; on 2.0 (String-only content) it rides in this
32
+ # ivar and is re-injected by the protocol when rendering history.
33
+ RAW_BLOCKS_IVAR = :@bedrock_invoke_raw_blocks
34
+
35
+ # The preserved Anthropic content blocks for a message, or nil.
36
+ # Works on both RubyLLM generations.
37
+ def self.raw_blocks(message)
38
+ if defined?(RawContent) && message.content.is_a?(RawContent)
39
+ message.content.value
40
+ elsif message.instance_variable_defined?(RAW_BLOCKS_IVAR)
41
+ message.instance_variable_get(RAW_BLOCKS_IVAR)
42
+ end
43
+ end
44
+
45
+ # Bedrock routes /model/{modelId}/... with the id as a single path label;
46
+ # ARN-based ids (inference profiles, application inference profiles,
47
+ # imported models) contain '/' and ':' and must be percent-encoded the
48
+ # way the official AWS SDK encodes them.
49
+ def self.escape_model_id(model_id)
50
+ ERB::Util.url_encode(model_id.to_s)
51
+ end
52
+
53
+ # Bedrock Claude supports budget-based extended thinking only (the
54
+ # adaptive/effort API is not available on Bedrock). Assumed-exist models
55
+ # carry no registry reasoning_options, so the providers bypass the
56
+ # registry gates and emit this payload directly.
57
+ def self.budget_thinking_payload(thinking)
58
+ return nil unless thinking.respond_to?(:enabled?) && thinking.enabled?
59
+
60
+ effort = thinking.respond_to?(:effort) ? thinking.effort&.to_s : nil
61
+ return nil if effort == 'none'
62
+
63
+ budget = thinking.respond_to?(:budget) ? thinking.budget : thinking
64
+ unless budget.is_a?(Integer)
65
+ raise ArgumentError,
66
+ 'Bedrock InvokeModel supports budget-based thinking only; adaptive/effort thinking ' \
67
+ 'is not available on Bedrock. Use with_thinking(budget: N).'
68
+ end
69
+
70
+ { type: 'enabled', budget_tokens: budget }
71
+ end
72
+
73
+ # RubyLLM 2.0 split providers (where/auth) from protocols (wire format);
74
+ # 1.x providers are monolithic. Detect which world we're in at require time.
75
+ def self.v2_architecture?
76
+ defined?(RubyLLM::Protocol) ? true : false
77
+ end
78
+
79
+ def self.register!
80
+ register_configuration_options!
81
+ RubyLLM::Provider.register :bedrock_invoke, RubyLLM::Providers::BedrockInvoke
82
+ end
83
+
84
+ # 1.x Configuration#inspect redacts ivars by suffix (_key/_secret/_token...)
85
+ # but not _credential_provider, so a custom credentials object would print
86
+ # its secrets in consoles and error reporters. 2.0's redaction already
87
+ # covers the suffix.
88
+ module ConfigurationRedaction
89
+ def instance_variables
90
+ super.reject { |ivar| ivar.to_s.end_with?('_credential_provider') }
91
+ end
92
+ end
93
+
94
+ # 1.16+ and 2.0 register configuration_options automatically inside
95
+ # Provider.register; older 1.x needs the accessors added by hand.
96
+ def self.register_configuration_options!
97
+ RubyLLM::Configuration.prepend(ConfigurationRedaction) unless v2_architecture?
98
+ return if RubyLLM::Configuration.respond_to?(:register_provider_options)
99
+
100
+ missing = CONFIGURATION_OPTIONS.reject { |key| RubyLLM::Configuration.method_defined?(key) }
101
+ RubyLLM::Configuration.attr_accessor(*missing) if missing.any?
102
+ end
103
+ end
104
+ end
105
+
106
+ if RubyLLM::BedrockInvoke.v2_architecture?
107
+ require_relative 'ruby_llm/bedrock_invoke/protocol_v2'
108
+ require_relative 'ruby_llm/bedrock_invoke/provider_v2'
109
+ else
110
+ require_relative 'ruby_llm/bedrock_invoke/raw_content'
111
+ require_relative 'ruby_llm/bedrock_invoke/provider_v1'
112
+ end
113
+
114
+ RubyLLM::BedrockInvoke.register!
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby_llm-bedrock_invoke
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - Chris Petersen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: aws-eventstream
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: aws-sdk-core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: base64
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ruby_llm
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '1.13'
61
+ - - "<"
62
+ - !ruby/object:Gem::Version
63
+ version: '3'
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '1.13'
71
+ - - "<"
72
+ - !ruby/object:Gem::Version
73
+ version: '3'
74
+ description: Talks the native Anthropic Messages format to Bedrock InvokeModel / InvokeModelWithResponseStream,
75
+ unlocking Anthropic-specific features that the Converse API cannot express — tool
76
+ search with deferred tool loading, prompt caching via cache_control, and structured
77
+ outputs — while keeping data inside the AWS boundary. Authenticates with the standard
78
+ AWS credential chain (IRSA, Pod Identity, instance profiles, env vars) via aws-sdk-core.
79
+ email:
80
+ - chris@petersen.io
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - LICENSE
86
+ - README.md
87
+ - lib/ruby_llm-bedrock_invoke.rb
88
+ - lib/ruby_llm/bedrock_invoke/block_collector.rb
89
+ - lib/ruby_llm/bedrock_invoke/capabilities.rb
90
+ - lib/ruby_llm/bedrock_invoke/deferred.rb
91
+ - lib/ruby_llm/bedrock_invoke/event_stream.rb
92
+ - lib/ruby_llm/bedrock_invoke/protocol_v2.rb
93
+ - lib/ruby_llm/bedrock_invoke/provider_v1.rb
94
+ - lib/ruby_llm/bedrock_invoke/provider_v2.rb
95
+ - lib/ruby_llm/bedrock_invoke/raw_content.rb
96
+ - lib/ruby_llm/bedrock_invoke/signing.rb
97
+ - lib/ruby_llm/bedrock_invoke/stream_state.rb
98
+ - lib/ruby_llm/bedrock_invoke/tool_search.rb
99
+ - lib/ruby_llm/bedrock_invoke/version.rb
100
+ homepage: https://github.com/scientist-labs/ruby_llm-bedrock_invoke
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ homepage_uri: https://github.com/scientist-labs/ruby_llm-bedrock_invoke
105
+ source_code_uri: https://github.com/scientist-labs/ruby_llm-bedrock_invoke
106
+ rubygems_mfa_required: 'true'
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 3.1.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.6.9
122
+ specification_version: 4
123
+ summary: RubyLLM provider for Anthropic Claude via the AWS Bedrock InvokeModel API
124
+ test_files: []