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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +194 -0
- data/lib/ruby_llm/bedrock_invoke/block_collector.rb +110 -0
- data/lib/ruby_llm/bedrock_invoke/capabilities.rb +38 -0
- data/lib/ruby_llm/bedrock_invoke/deferred.rb +37 -0
- data/lib/ruby_llm/bedrock_invoke/event_stream.rb +117 -0
- data/lib/ruby_llm/bedrock_invoke/protocol_v2.rb +162 -0
- data/lib/ruby_llm/bedrock_invoke/provider_v1.rb +215 -0
- data/lib/ruby_llm/bedrock_invoke/provider_v2.rb +71 -0
- data/lib/ruby_llm/bedrock_invoke/raw_content.rb +23 -0
- data/lib/ruby_llm/bedrock_invoke/signing.rb +106 -0
- data/lib/ruby_llm/bedrock_invoke/stream_state.rb +34 -0
- data/lib/ruby_llm/bedrock_invoke/tool_search.rb +115 -0
- data/lib/ruby_llm/bedrock_invoke/version.rb +7 -0
- data/lib/ruby_llm-bedrock_invoke.rb +114 -0
- metadata +124 -0
|
@@ -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,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: []
|