From 8328547c3d8dc20dcdf867f2208fa48c1f3238aa Mon Sep 17 00:00:00 2001 From: Matt Webb Date: Wed, 10 Jun 2026 17:58:07 -0700 Subject: [PATCH] Support Bedrock application inference profile ARNs as model ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application inference profiles (used for cost-allocation tagging) are invoked by passing their ARN as the modelId. The Bedrock provider built the request path as "/model/#{@model.id}/converse" with the id raw, so an ARN's internal "/" (".../application-inference-profile/") was parsed as a path separator in both the request URL and the SigV4 canonical path — AWS then rejected it with "The provided model identifier is invalid". Percent-encode the model id's "/" so the ARN stays a single path segment. canonical_uri re-encodes per segment, which double-encodes it as SigV4 requires for non-S3 services, keeping the signed and sent paths consistent. No-op for ordinary model ids (which contain no "/"); ":" version suffixes are valid path-segment characters and are unaffected. --- lib/ruby_llm/protocols/converse/chat.rb | 8 ++++- lib/ruby_llm/protocols/converse/streaming.rb | 2 +- spec/ruby_llm/providers/bedrock_spec.rb | 38 ++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/lib/ruby_llm/protocols/converse/chat.rb b/lib/ruby_llm/protocols/converse/chat.rb index 38a9c9b5c..cb2545437 100644 --- a/lib/ruby_llm/protocols/converse/chat.rb +++ b/lib/ruby_llm/protocols/converse/chat.rb @@ -13,7 +13,13 @@ module Chat module_function def completion_url - "/model/#{@model.id}/converse" + "/model/#{escape_model_id(@model.id)}/converse" + end + + # Application inference profile ARNs contain '/', but Bedrock expects the model id + # to remain one path segment. + def escape_model_id(model_id) + model_id.to_s.gsub('/', '%2F') end # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument diff --git a/lib/ruby_llm/protocols/converse/streaming.rb b/lib/ruby_llm/protocols/converse/streaming.rb index 56168c955..c54c5e990 100644 --- a/lib/ruby_llm/protocols/converse/streaming.rb +++ b/lib/ruby_llm/protocols/converse/streaming.rb @@ -14,7 +14,7 @@ module Streaming private def stream_url - "/model/#{@model.id}/converse-stream" + "/model/#{escape_model_id(@model.id)}/converse-stream" end def stream_response(payload, additional_headers = {}, &block) diff --git a/spec/ruby_llm/providers/bedrock_spec.rb b/spec/ruby_llm/providers/bedrock_spec.rb index 995d26f26..91c45378b 100644 --- a/spec/ruby_llm/providers/bedrock_spec.rb +++ b/spec/ruby_llm/providers/bedrock_spec.rb @@ -160,4 +160,42 @@ def credential_provider(credentials = self.credentials) expect(provider.protocol_for(model)).to eq(RubyLLM::Protocols::Converse) end end + + describe 'model id path encoding' do + let(:converse) { RubyLLM::Protocols::Converse.allocate } + let(:arn) { 'arn:aws:bedrock:us-west-2:123:application-inference-profile/p' } + + def with_model(id) + converse.instance_variable_set(:@model, instance_double(RubyLLM::Model, id: id)) + end + + it 'keeps an application inference profile ARN as a single path segment in the converse URL' do + with_model(arn) + expect(converse.send(:completion_url)).to eq( + '/model/arn:aws:bedrock:us-west-2:123:application-inference-profile%2Fp/converse' + ) + end + + it 'encodes the ARN for the converse-stream URL too' do + with_model(arn) + expect(converse.send(:stream_url)).to eq( + '/model/arn:aws:bedrock:us-west-2:123:application-inference-profile%2Fp/converse-stream' + ) + end + + it 'leaves ordinary model ids (including a ":" version suffix) unchanged' do + with_model('us.anthropic.claude-sonnet-4-5-20250929-v1:0') + expect(converse.send(:completion_url)).to eq( + '/model/us.anthropic.claude-sonnet-4-5-20250929-v1:0/converse' + ) + end + + it 'signs the ARN as one segment (SigV4 canonical path double-encodes "/", not truncates)' do + with_model(arn) + path = URI.parse(converse.send(:completion_url)).path + expect(described_class.allocate.send(:canonical_uri, path)).to eq( + '/model/arn%3Aaws%3Abedrock%3Aus-west-2%3A123%3Aapplication-inference-profile%252Fp/converse' + ) + end + end end