Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions docs/_getting_started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ RubyLLM.configure do |config|
config.gpustack_api_base = ENV['GPUSTACK_API_BASE']
config.gpustack_api_key = ENV['GPUSTACK_API_KEY']

# AWS Bedrock (uses standard AWS credential chain if not set)
# AWS Bedrock - Static credentials
config.bedrock_api_key = ENV['AWS_ACCESS_KEY_ID']
config.bedrock_secret_key = ENV['AWS_SECRET_ACCESS_KEY']
config.bedrock_region = ENV['AWS_REGION'] # Required for Bedrock
Expand All @@ -90,6 +90,37 @@ end

These headers are optional and only needed for organization-specific billing or project tracking.

### AWS Bedrock Credential Provider

For dynamic credential management (IAM roles, assume role, credential
rotation), use an AWS SDK credential provider instead of static keys:

```ruby
require 'aws-sdk-core'

RubyLLM.configure do |config|
config.bedrock_region = 'us-east-1' # Required

# Use EC2 instance profile or ECS task role
config.bedrock_credential_provider = Aws::InstanceProfileCredentials.new

# Or assume a role
config.bedrock_credential_provider = Aws::AssumeRoleCredentials.new(
role_arn: 'arn:aws:iam::123456789012:role/MyBedrockRole',
role_session_name: 'ruby-llm-session'
)

# Or use shared credentials from ~/.aws/credentials
config.bedrock_credential_provider = Aws::SharedCredentials.new(
profile_name: 'my-profile'
)
end
```

When `bedrock_credential_provider` is set, it takes precedence over static
credentials (`bedrock_api_key`, `bedrock_secret_key`, `bedrock_session_token`).
The provider handles credential refresh automatically.

## Custom Endpoints

### OpenAI-Compatible APIs
Expand Down Expand Up @@ -335,6 +366,7 @@ RubyLLM.configure do |config|
config.bedrock_secret_key = String
config.bedrock_region = String
config.bedrock_session_token = String
config.bedrock_credential_provider = Object # Aws::CredentialProvider

# Default Models
config.default_model = String
Expand Down Expand Up @@ -363,4 +395,4 @@ Now that you've configured RubyLLM, you're ready to:

- [Start chatting with AI models]({% link _core_features/chat.md %})
- [Work with different providers and models]({% link _advanced/models.md %})
- [Set up Rails integration]({% link _advanced/rails.md %})
- [Set up Rails integration]({% link _advanced/rails.md %})
1 change: 1 addition & 0 deletions lib/ruby_llm/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Configuration
:bedrock_secret_key,
:bedrock_region,
:bedrock_session_token,
:bedrock_credential_provider,
:openrouter_api_key,
:ollama_api_base,
:gpustack_api_base,
Expand Down
43 changes: 35 additions & 8 deletions lib/ruby_llm/providers/bedrock.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,30 @@ def sign_request(url, method: :post, payload: nil)
end

def create_signer
Signing::Signer.new({
access_key_id: @config.bedrock_api_key,
secret_access_key: @config.bedrock_secret_key,
session_token: @config.bedrock_session_token,
region: @config.bedrock_region,
service: 'bedrock'
})
signer_options = {
region: @config.bedrock_region,
service: 'bedrock'
}

if @config.bedrock_credential_provider
validate_credential_provider!(@config.bedrock_credential_provider)
signer_options[:credentials_provider] = @config.bedrock_credential_provider
else
signer_options.merge!({
access_key_id: @config.bedrock_api_key,
secret_access_key: @config.bedrock_secret_key,
session_token: @config.bedrock_session_token
})
end

Signing::Signer.new(signer_options)
end

def validate_credential_provider!(provider)
return if provider.respond_to?(:credentials)

raise ConfigurationError,
'bedrock_credential_provider must respond to :credentials method'
end

def build_request(url, method: :post, payload: nil)
Expand All @@ -68,13 +85,23 @@ def build_headers(signature_headers, streaming: false)
)
end

def configuration_requirements
# If credential provider is configured, only region is required
# Otherwise, require static credentials (api_key, secret_key) + region
if @config.bedrock_credential_provider
%i[bedrock_region]
else
%i[bedrock_api_key bedrock_secret_key bedrock_region]
end
end

class << self
def capabilities
Bedrock::Capabilities
end

def configuration_requirements
%i[bedrock_api_key bedrock_secret_key bedrock_region]
%i[bedrock_region]
end
end
end
Expand Down
48 changes: 40 additions & 8 deletions spec/ruby_llm/providers/bedrock/models_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@

it 'adds us. prefix to model ID' do
# Mock a provider instance to test the region functionality
allow(RubyLLM.config).to receive(:bedrock_region).and_return('us-east-1')
allow(RubyLLM.config).to receive_messages(
bedrock_region: 'us-east-1',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)

Expand All @@ -63,7 +67,11 @@

it 'does not add us. prefix to model ID' do
# Mock a provider instance to test the region functionality
allow(RubyLLM.config).to receive(:bedrock_region).and_return('us-east-1')
allow(RubyLLM.config).to receive_messages(
bedrock_region: 'us-east-1',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)

Expand All @@ -88,7 +96,11 @@

it 'does not add us. prefix to model ID' do
# Mock a provider instance to test the region functionality
allow(RubyLLM.config).to receive(:bedrock_region).and_return('us-east-1')
allow(RubyLLM.config).to receive_messages(
bedrock_region: 'us-east-1',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)

Expand All @@ -112,7 +124,11 @@

it 'does not add us. prefix to model ID' do
# Mock a provider instance to test the region functionality
allow(RubyLLM.config).to receive(:bedrock_region).and_return('us-east-1')
allow(RubyLLM.config).to receive_messages(
bedrock_region: 'us-east-1',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)

Expand All @@ -125,7 +141,11 @@
# New specs for region-aware inference profile handling
describe '#model_id_with_region with region awareness' do
let(:provider_instance) do
allow(RubyLLM.config).to receive(:bedrock_region).and_return('eu-west-3')
allow(RubyLLM.config).to receive_messages(
bedrock_region: 'eu-west-3',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)
provider
Expand Down Expand Up @@ -163,7 +183,11 @@

context 'with AP region configured' do
let(:provider_instance) do
allow(RubyLLM.config).to receive(:bedrock_region).and_return('ap-south-1')
allow(RubyLLM.config).to receive_messages(
bedrock_region: 'ap-south-1',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)
provider
Expand All @@ -184,7 +208,11 @@

context 'with region prefix edge cases' do
it 'handles empty region gracefully' do
allow(RubyLLM.config).to receive(:bedrock_region).and_return('')
allow(RubyLLM.config).to receive_messages(
bedrock_region: '',
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)

Expand All @@ -208,7 +236,11 @@
}

regions_and_expected_prefixes.each do |region, expected_prefix|
allow(RubyLLM.config).to receive(:bedrock_region).and_return(region)
allow(RubyLLM.config).to receive_messages(
bedrock_region: region,
bedrock_api_key: 'test-key',
bedrock_secret_key: 'test-secret'
)
provider = RubyLLM::Providers::Bedrock.new(RubyLLM.config)
provider.extend(described_class)

Expand Down
Loading