Skip to content

Commit 19bc0d1

Browse files
committed
Let explicit selectors route unverifiable ARNs to BedrockInvokeModel
invoke_model? consulted anthropic_model? before the selector, so an application-inference-profile ARN with no provider_name metadata could never route to InvokeModel — even when an Array or Proc selector opted in explicitly. Since assume_model_exists chats carry no registry metadata, every ARN-routed request fell back to Converse regardless of the operator's configuration, and the refusal warning's own advice ("use an Array or Proc selector to opt in explicitly") was impossible to follow. Split vendor verification into two tiers: provably non-Anthropic ids (known vendor prefixes, bare or cross-region) are never routed under any selector; unverifiable ids (ARNs without metadata) route when an Array or Proc opts in explicitly. Only the blanket true selector still requires positive verification, since it expresses 'all Anthropic models' rather than a specific opt-in.
1 parent 07de0b8 commit 19bc0d1

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)