Skip to content

Commit 1aeb08d

Browse files
authored
Merge pull request #10 from appfolio/bedrock-invoke-model-arn-selector-optin
Let explicit selectors route unverifiable ARNs to BedrockInvokeModel
2 parents 07de0b8 + 19bc0d1 commit 1aeb08d

2 files changed

Lines changed: 74 additions & 9 deletions

File tree

lib/ruby_llm/providers/bedrock.rb

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,26 +127,51 @@ def model_supports_top_k?(model)
127127
# Returns true if the InvokeModel protocol should be used for this model.
128128
# `bedrock_use_invoke_model` can be:
129129
# - false / nil → always Converse (default)
130-
# - true → InvokeModel for all Anthropic models
130+
# - true → InvokeModel for all verifiably Anthropic models
131131
# - Array → InvokeModel when model.id is in the list
132132
# - Proc/lambda → InvokeModel when the callable returns truthy for model
133+
#
134+
# Vendor verification interacts with the selector in two tiers:
135+
# - Ids that are provably non-Anthropic (a known vendor prefix like amazon./meta.,
136+
# with or without a cross-region geo prefix) are never routed, under any selector —
137+
# the InvokeModel payload is Anthropic Messages format and would be rejected.
138+
# - Ids that cannot be verified either way — chiefly application-inference-profile
139+
# ARNs, whose Model::Info carries no provider_name metadata on the
140+
# assume_model_exists path — are routed when the selector opts in explicitly
141+
# (Array or Proc). An operator naming the exact id IS the verification. Only the
142+
# blanket `true` selector requires positive verification via anthropic_model?,
143+
# because it expresses "all Anthropic models", not "this specific model".
133144
def invoke_model?(model)
134145
selector = @config.bedrock_use_invoke_model
135146
return false unless selector
136-
return false unless anthropic_model?(model)
147+
return false if non_anthropic_model?(model)
137148

138149
case selector
139150
when true
140-
true
151+
anthropic_model?(model)
141152
when Array
142153
selector.include?(model.id)
143154
else
144155
selector.respond_to?(:call) ? selector.call(model) : false
145156
end
146157
end
147158

148-
NON_ANTHROPIC_PREFIXES = %w[amazon. meta. ai21. cohere. mistral. writer. stability.].freeze
149-
private_constant :NON_ANTHROPIC_PREFIXES
159+
NON_ANTHROPIC_VENDORS = %w[amazon meta ai21 cohere mistral writer stability].freeze
160+
private_constant :NON_ANTHROPIC_VENDORS
161+
162+
# Matches known non-Anthropic vendor ids in both bare ("amazon.nova-pro-v1:0") and
163+
# cross-region ("us.amazon.nova-pro-v1:0") forms.
164+
NON_ANTHROPIC_PATTERN = /\A(?:[a-z0-9-]+\.)?(?:#{NON_ANTHROPIC_VENDORS.join('|')})\./
165+
private_constant :NON_ANTHROPIC_PATTERN
166+
167+
# True only when the id provably belongs to a non-Anthropic vendor. ARNs return false:
168+
# they don't encode the vendor, so they are "unverifiable", not "non-Anthropic".
169+
def non_anthropic_model?(model)
170+
id = model.id.to_s
171+
return false if id.start_with?('arn:')
172+
173+
NON_ANTHROPIC_PATTERN.match?(id)
174+
end
150175

151176
def anthropic_model?(model)
152177
id = model.id.to_s
@@ -163,13 +188,15 @@ def anthropic_model?(model)
163188
# Application-inference-profile ARNs do not encode the underlying vendor in the ARN
164189
# string itself. When available, consult model.metadata[:provider_name] (populated
165190
# by Bedrock::Models for registered foundation models) to confirm the profile is
166-
# Anthropic-backed. If that field is absent (e.g. a runtime-only ARN the registry
167-
# has never seen), log a warning and refuse to route rather than silently forwarding
168-
# an incompatible payload to a non-Anthropic model.
191+
# Anthropic-backed. If that field is absent (e.g. an assume_model_exists chat, whose
192+
# Model::Info carries no registry metadata), log a warning and refuse to route rather
193+
# than silently forwarding an incompatible payload to a non-Anthropic model — the
194+
# operator can still opt in explicitly via an Array or Proc selector (see
195+
# invoke_model?).
169196
return arn_anthropic_model?(model, id) if id.start_with?('arn:')
170197

171198
# Block known non-Anthropic Bedrock vendor prefixes (Nova, Llama, Jurassic, etc.).
172-
return false if NON_ANTHROPIC_PREFIXES.any? { |pfx| id.start_with?(pfx) }
199+
return false if NON_ANTHROPIC_PATTERN.match?(id)
173200

174201
provider = model.respond_to?(:provider) ? model.provider : nil
175202
provider.to_s == 'anthropic'

spec/ruby_llm/providers/bedrock_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,44 @@ def model_double(id, metadata: {}, provider: 'bedrock')
235235
end
236236
end
237237

238+
context 'with an unverifiable ARN id (no provider_name metadata)' do # rubocop:disable RSpec/MultipleMemoizedHelpers
239+
before { allow(RubyLLM.logger).to receive(:warn) }
240+
241+
it 'routes to BedrockInvokeModel when an Array selector lists the ARN' do
242+
provider = build_bedrock(use_invoke_model: [arn_id])
243+
expect(provider.protocol_for(model_double(arn_id))).to be(RubyLLM::Protocols::BedrockInvokeModel)
244+
end
245+
246+
it 'routes to BedrockInvokeModel when a Proc selector returns true' do
247+
provider = build_bedrock(use_invoke_model: ->(_m) { true })
248+
expect(provider.protocol_for(model_double(arn_id))).to be(RubyLLM::Protocols::BedrockInvokeModel)
249+
end
250+
251+
it 'routes to Converse when a Proc selector returns false' do
252+
provider = build_bedrock(use_invoke_model: ->(_m) { false })
253+
expect(provider.protocol_for(model_double(arn_id))).to be(RubyLLM::Protocols::Converse)
254+
end
255+
256+
it 'still requires positive verification under the blanket true selector' do
257+
provider = build_bedrock(use_invoke_model: true)
258+
expect(provider.protocol_for(model_double(arn_id))).to be(RubyLLM::Protocols::Converse)
259+
expect(RubyLLM.logger).to have_received(:warn).with(/cannot verify.*Anthropic-backed/)
260+
end
261+
end
262+
263+
context 'with provably non-Anthropic ids under explicit selectors' do # rubocop:disable RSpec/MultipleMemoizedHelpers
264+
it 'never routes a bare vendor id, even when an Array selector lists it' do
265+
provider = build_bedrock(use_invoke_model: [nova_id])
266+
expect(provider.protocol_for(model_double(nova_id))).to be(RubyLLM::Protocols::Converse)
267+
end
268+
269+
it 'never routes a cross-region vendor id, even when a Proc selector returns true' do
270+
provider = build_bedrock(use_invoke_model: ->(_m) { true })
271+
expect(provider.protocol_for(model_double(us_nova_id))).to be(RubyLLM::Protocols::Converse)
272+
expect(provider.protocol_for(model_double(llama_id))).to be(RubyLLM::Protocols::Converse)
273+
end
274+
end
275+
238276
describe 'anthropic_model? directly' do # rubocop:disable RSpec/MultipleMemoizedHelpers
239277
let(:bedrock) { build_bedrock }
240278

0 commit comments

Comments
 (0)