Skip to content

Commit 5af967b

Browse files
authored
Add AWS Bedrock provider (#316)
* Add AWS Bedrock provider for Anthropic models Enables using Anthropic Claude models hosted on AWS Bedrock via the existing `anthropic` gem's BedrockClient. The provider inherits all functionality from AnthropicProvider (streaming, tool use, multimodal, structured output) and overrides only client construction for AWS authentication. Implementation: - BedrockProvider inherits AnthropicProvider, overrides client() to return Anthropic::BedrockClient with SigV4 signing - Bedrock::Options handles AWS credential resolution: explicit options, environment variables (AWS_REGION, AWS_ACCESS_KEY_ID, etc.), then AWS SDK default chain (profiles, IAM roles, instance metadata) - Reuses Anthropic request/response types — no protocol translation needed as BedrockClient handles this internally - Sensitive credentials (access key, secret key, session token, profile) excluded from Options#serialize Test coverage (28 tests, 37 assertions): - Provider class: service_name, inheritance, client construction, client memoization, prompt_request_type delegation to Anthropic - Options: initialization, all AWS ENV fallbacks, explicit-over-ENV precedence, default/custom retry and timeout, serialization security - Provider loading: require path resolution, convention-based lookup Follows the same pattern established by the Azure provider in #301. * Add bearer token authentication for AWS Bedrock API keys AWS Bedrock supports API key authentication via bearer tokens (Authorization: Bearer <token>), as an alternative to SigV4 signing. See: https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html The existing Anthropic::BedrockClient only supports SigV4, so this adds a BearerClient that subclasses Anthropic::Client directly, reusing its built-in auth_token support while copying the Bedrock-specific request transformations (URL path rewriting, anthropic_version injection). The provider now checks for a bearer token first (via aws_bearer_token option or AWS_BEARER_TOKEN_BEDROCK env var) and falls back to the existing SigV4 path when no token is present. Also adds extra_headers and anthropic_beta to Bedrock::Options, which are required by the inherited AnthropicProvider at runtime. * Add tests for bearer token authentication - Options: bearer token from explicit value, env var resolution, preference of explicit over env, and serialize exclusion - Provider: client returns BearerClient when token is present, falls back to Anthropic::BedrockClient otherwise, memoization - BearerClient: initialization, auth_token, region, base_url, inheritance, and resource accessors (messages, completions, beta) * Fix missing retry delay options and isolate provider tests from env Pass initial_retry_delay and max_retry_delay through to both BearerClient and Anthropic::BedrockClient constructors so user- configured retry backoff is no longer silently ignored. Also clear AWS_BEARER_TOKEN_BEDROCK in provider test setup/teardown so tests expecting the SigV4 client path aren't broken by the environment.
1 parent a64e094 commit 5af967b

11 files changed

Lines changed: 825 additions & 0 deletions

File tree

activeagent.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
2828
spec.add_development_dependency "jbuilder", "~> 2.14"
2929

3030
spec.add_development_dependency "anthropic", "~> 1.12"
31+
spec.add_development_dependency "aws-sdk-bedrockruntime"
3132
spec.add_development_dependency "openai", "~> 0.34"
3233
spec.add_development_dependency "ruby_llm", ">= 1.0"
3334

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "options"
4+
require_relative "bearer_client"
5+
require_relative "../anthropic/_types"
6+
7+
# Bedrock uses the same request/response types as Anthropic.
8+
# The BedrockClient handles all protocol translation internally.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# frozen_string_literal: true
2+
3+
module ActiveAgent
4+
module Providers
5+
module Bedrock
6+
# Client for AWS Bedrock using bearer token (API key) authentication.
7+
#
8+
# Subclasses Anthropic::Client directly to reuse its built-in bearer
9+
# token support via the +auth_token+ parameter, while adding Bedrock-
10+
# specific request transformations (URL path rewriting, anthropic_version
11+
# injection) copied from Anthropic::BedrockClient.
12+
#
13+
# This avoids Anthropic::BedrockClient which requires SigV4 credentials
14+
# and would fail when only a bearer token is available.
15+
#
16+
# @see https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html
17+
class BearerClient < ::Anthropic::Client
18+
BEDROCK_VERSION = "bedrock-2023-05-31"
19+
20+
# @return [String]
21+
attr_reader :aws_region
22+
23+
# @param aws_region [String] AWS region for the Bedrock endpoint
24+
# @param bearer_token [String] AWS Bedrock API key (bearer token)
25+
# @param base_url [String, nil] Override the default Bedrock endpoint
26+
# @param max_retries [Integer]
27+
# @param timeout [Float]
28+
# @param initial_retry_delay [Float]
29+
# @param max_retry_delay [Float]
30+
def initialize(
31+
aws_region:,
32+
bearer_token:,
33+
base_url: nil,
34+
max_retries: self.class::DEFAULT_MAX_RETRIES,
35+
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
36+
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
37+
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
38+
)
39+
@aws_region = aws_region
40+
41+
base_url ||= "https://bedrock-runtime.#{aws_region}.amazonaws.com"
42+
43+
super(
44+
auth_token: bearer_token,
45+
api_key: nil,
46+
base_url: base_url,
47+
max_retries: max_retries,
48+
timeout: timeout,
49+
initial_retry_delay: initial_retry_delay,
50+
max_retry_delay: max_retry_delay
51+
)
52+
53+
@messages = ::Anthropic::Resources::Messages.new(client: self)
54+
@completions = ::Anthropic::Resources::Completions.new(client: self)
55+
@beta = ::Anthropic::Resources::Beta.new(client: self)
56+
end
57+
58+
private
59+
60+
# Intercepts request building to apply Bedrock-specific transformations
61+
# before the parent class processes the request.
62+
def build_request(req, opts)
63+
fit_req_to_bedrock_specs!(req)
64+
req = super
65+
body = req.fetch(:body)
66+
req[:body] = StringIO.new(body.to_a.join) if body.is_a?(Enumerator)
67+
req
68+
end
69+
70+
# Rewrites Anthropic API paths to Bedrock endpoint paths and injects
71+
# the Bedrock anthropic_version field.
72+
#
73+
# Adapted from Anthropic::Helpers::Bedrock::Client#fit_req_to_bedrock_specs!
74+
def fit_req_to_bedrock_specs!(request_components)
75+
if (body = request_components[:body]).is_a?(Hash)
76+
body[:anthropic_version] ||= BEDROCK_VERSION
77+
body.transform_keys!("anthropic-beta": :anthropic_beta)
78+
end
79+
80+
case request_components[:path]
81+
in %r{^v1/messages/batches}
82+
raise NotImplementedError, "The Batch API is not supported in Bedrock yet"
83+
in %r{v1/messages/count_tokens}
84+
raise NotImplementedError, "Token counting is not supported in Bedrock yet"
85+
in %r{v1/models\?beta=true}
86+
raise NotImplementedError,
87+
"Please instead use https://docs.anthropic.com/en/api/claude-on-amazon-bedrock#list-available-models " \
88+
"to list available models on Bedrock."
89+
else
90+
end
91+
92+
if %w[
93+
v1/complete
94+
v1/messages
95+
v1/messages?beta=true
96+
].include?(request_components[:path]) && request_components[:method] == :post && body.is_a?(Hash)
97+
model = body.delete(:model)
98+
model = URI.encode_www_form_component(model.to_s)
99+
stream = body.delete(:stream) || false
100+
request_components[:path] =
101+
stream ? "model/#{model}/invoke-with-response-stream" : "model/#{model}/invoke"
102+
end
103+
104+
request_components
105+
end
106+
end
107+
end
108+
end
109+
end
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require "active_agent/providers/common/model"
4+
5+
module ActiveAgent
6+
module Providers
7+
module Bedrock
8+
# Configuration for AWS Bedrock provider.
9+
#
10+
# AWS credentials are resolved in order:
11+
# 1. Explicit options (aws_access_key, aws_secret_key)
12+
# 2. Environment variables (AWS_REGION, AWS_ACCESS_KEY_ID, etc.)
13+
# 3. AWS SDK default chain (profiles, IAM roles, instance metadata)
14+
#
15+
# Unlike the Anthropic provider, no API key is needed — authentication
16+
# is handled entirely through AWS credentials.
17+
#
18+
# @example Minimal config (uses SDK default chain)
19+
# Bedrock::Options.new(aws_region: "eu-west-2")
20+
#
21+
# @example Explicit credentials
22+
# Bedrock::Options.new(
23+
# aws_region: "eu-west-2",
24+
# aws_access_key: "AKIA...",
25+
# aws_secret_key: "..."
26+
# )
27+
#
28+
# @example With profile
29+
# Bedrock::Options.new(
30+
# aws_region: "eu-west-2",
31+
# aws_profile: "my-profile"
32+
# )
33+
class Options < Common::BaseModel
34+
attribute :aws_region, :string
35+
attribute :aws_access_key, :string
36+
attribute :aws_secret_key, :string
37+
attribute :aws_session_token, :string
38+
attribute :aws_profile, :string
39+
attribute :aws_bearer_token, :string
40+
attribute :base_url, :string
41+
attribute :anthropic_beta, :string
42+
43+
attribute :max_retries, :integer, default: ::Anthropic::Client::DEFAULT_MAX_RETRIES
44+
attribute :timeout, :float, default: ::Anthropic::Client::DEFAULT_TIMEOUT_IN_SECONDS
45+
attribute :initial_retry_delay, :float, default: ::Anthropic::Client::DEFAULT_INITIAL_RETRY_DELAY
46+
attribute :max_retry_delay, :float, default: ::Anthropic::Client::DEFAULT_MAX_RETRY_DELAY
47+
48+
def initialize(kwargs = {})
49+
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
50+
51+
super(**deep_compact(kwargs.except(:default_url_options).merge(
52+
aws_region: kwargs[:aws_region] || ENV["AWS_REGION"] || ENV["AWS_DEFAULT_REGION"],
53+
aws_access_key: kwargs[:aws_access_key] || ENV["AWS_ACCESS_KEY_ID"],
54+
aws_secret_key: kwargs[:aws_secret_key] || ENV["AWS_SECRET_ACCESS_KEY"],
55+
aws_session_token: kwargs[:aws_session_token] || ENV["AWS_SESSION_TOKEN"],
56+
aws_profile: kwargs[:aws_profile] || ENV["AWS_PROFILE"],
57+
aws_bearer_token: kwargs[:aws_bearer_token] || ENV["AWS_BEARER_TOKEN_BEDROCK"]
58+
)))
59+
end
60+
61+
# Bedrock handles authentication at the client level (SigV4 or bearer token),
62+
# so no extra headers are needed in request options.
63+
def extra_headers
64+
{}
65+
end
66+
67+
# Excludes sensitive AWS credentials from serialized output.
68+
# The provider's client() method reads credentials directly from options attributes.
69+
def serialize
70+
attributes.symbolize_keys.except(
71+
:aws_access_key, :aws_secret_key, :aws_session_token, :aws_profile, :aws_bearer_token
72+
)
73+
end
74+
end
75+
end
76+
end
77+
end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "anthropic_provider"
4+
require_relative "bedrock/_types"
5+
6+
module ActiveAgent
7+
module Providers
8+
# Provider for Anthropic models hosted on AWS Bedrock.
9+
#
10+
# Inherits all functionality from AnthropicProvider (streaming, tool use,
11+
# multimodal, JSON format emulation) and overrides only the client
12+
# construction to use Anthropic::BedrockClient for AWS authentication.
13+
#
14+
# @example Configuration in active_agent.yml
15+
# bedrock:
16+
# service: "Bedrock"
17+
# aws_region: "eu-west-2"
18+
# model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
19+
#
20+
# @example Agent usage
21+
# class SummaryAgent < ApplicationAgent
22+
# generate_with :bedrock, model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
23+
#
24+
# def summarize
25+
# prompt(message: params[:message])
26+
# end
27+
# end
28+
#
29+
# @see AnthropicProvider
30+
class BedrockProvider < AnthropicProvider
31+
# @return [String]
32+
def self.service_name
33+
"Bedrock"
34+
end
35+
36+
# @return [Class]
37+
def self.options_klass
38+
Bedrock::Options
39+
end
40+
41+
# @return [ActiveModel::Type::Value]
42+
def self.prompt_request_type
43+
Anthropic::RequestType.new
44+
end
45+
46+
# Returns a configured Bedrock client.
47+
#
48+
# When a bearer token is available (via +aws_bearer_token+ option or
49+
# +AWS_BEARER_TOKEN_BEDROCK+ env var), uses {Bedrock::BearerClient}
50+
# which sends an +Authorization: Bearer+ header.
51+
#
52+
# Otherwise, falls back to {Anthropic::BedrockClient} which handles
53+
# SigV4 signing, credential resolution, and Bedrock URL path rewriting.
54+
#
55+
# @return [Bedrock::BearerClient, Anthropic::Helpers::Bedrock::Client]
56+
def client
57+
@client ||= if options.aws_bearer_token.present?
58+
Bedrock::BearerClient.new(
59+
aws_region: options.aws_region,
60+
bearer_token: options.aws_bearer_token,
61+
base_url: options.base_url.presence,
62+
max_retries: options.max_retries,
63+
timeout: options.timeout,
64+
initial_retry_delay: options.initial_retry_delay,
65+
max_retry_delay: options.max_retry_delay
66+
)
67+
else
68+
::Anthropic::BedrockClient.new(
69+
aws_region: options.aws_region,
70+
aws_access_key: options.aws_access_key,
71+
aws_secret_key: options.aws_secret_key,
72+
aws_session_token: options.aws_session_token,
73+
aws_profile: options.aws_profile,
74+
base_url: options.base_url.presence,
75+
max_retries: options.max_retries,
76+
timeout: options.timeout,
77+
initial_retry_delay: options.initial_retry_delay,
78+
max_retry_delay: options.max_retry_delay
79+
)
80+
end
81+
end
82+
end
83+
end
84+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module Providers
4+
# Example agent using Anthropic models via AWS Bedrock.
5+
#
6+
# Demonstrates basic prompt generation with the Bedrock provider.
7+
# Configured to use Claude Sonnet via cross-region inference.
8+
#
9+
# @example Basic usage
10+
# response = Providers::BedrockAgent.ask(message: "Hello").generate_now
11+
# response.message.content #=> "Hi! How can I help you today?"
12+
# region agent
13+
class BedrockAgent < ApplicationAgent
14+
generate_with :bedrock, model: "eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
15+
16+
# @return [ActiveAgent::Generation]
17+
def ask
18+
prompt(message: params[:message])
19+
end
20+
end
21+
# endregion agent
22+
end

test/dummy/config/active_agent.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ ollama: &ollama
2424
service: "Ollama"
2525
model: "gpt-oss:20b"
2626
# endregion ollama_anchor
27+
# region bedrock_anchor
28+
bedrock: &bedrock
29+
service: "Bedrock"
30+
aws_region: <%= ENV.fetch("AWS_REGION", "us-east-1") %>
31+
# endregion bedrock_anchor
2732
# region mock_anchor
2833
mock: &mock
2934
service: "Mock"
@@ -55,6 +60,10 @@ development:
5560
anthropic:
5661
<<: *anthropic
5762
# endregion anthropic_dev_config
63+
# region bedrock_dev_config
64+
bedrock:
65+
<<: *bedrock
66+
# endregion bedrock_dev_config
5867
# region mock_dev_config
5968
mock:
6069
<<: *mock
@@ -77,6 +86,8 @@ test:
7786
<<: *ollama
7887
anthropic:
7988
<<: *anthropic
89+
bedrock:
90+
<<: *bedrock
8091
mock:
8192
<<: *mock
8293
ruby_llm:

0 commit comments

Comments
 (0)