activeagent 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -4
  3. data/lib/active_agent/base.rb +3 -2
  4. data/lib/active_agent/concerns/provider.rb +6 -2
  5. data/lib/active_agent/concerns/rescue.rb +39 -0
  6. data/lib/active_agent/concerns/streaming.rb +2 -1
  7. data/lib/active_agent/concerns/view.rb +1 -1
  8. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/api/traces_controller.rb +117 -0
  9. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/application_controller.rb +54 -0
  10. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/dashboard_controller.rb +126 -0
  11. data/lib/active_agent/dashboard/app/controllers/active_agent/dashboard/traces_controller.rb +103 -0
  12. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/agent_execution_job.rb +56 -0
  13. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/application_job.rb +14 -0
  14. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_cleanup_job.rb +49 -0
  15. data/lib/active_agent/dashboard/app/jobs/active_agent/dashboard/sandbox_provision_job.rb +65 -0
  16. data/lib/active_agent/dashboard/app/jobs/active_agent/process_telemetry_traces_job.rb +77 -0
  17. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent.rb +256 -0
  18. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_run.rb +113 -0
  19. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_template.rb +208 -0
  20. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/agent_version.rb +60 -0
  21. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/application_record.rb +46 -0
  22. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_action.rb +125 -0
  23. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/recording_snapshot.rb +83 -0
  24. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_run.rb +52 -0
  25. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/sandbox_session.rb +169 -0
  26. data/lib/active_agent/dashboard/app/models/active_agent/dashboard/session_recording.rb +193 -0
  27. data/lib/active_agent/dashboard/app/models/active_agent/telemetry_trace.rb +198 -0
  28. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/_trace_detail.html.erb +105 -0
  29. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/index.html.erb +135 -0
  30. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/metrics.html.erb +143 -0
  31. data/lib/active_agent/dashboard/app/views/active_agent/dashboard/traces/show.html.erb +36 -0
  32. data/lib/active_agent/dashboard/app/views/layouts/active_agent/dashboard/application.html.erb +94 -0
  33. data/lib/active_agent/dashboard/config/routes.rb +78 -0
  34. data/lib/active_agent/dashboard/engine.rb +39 -0
  35. data/lib/active_agent/dashboard.rb +151 -0
  36. data/lib/active_agent/providers/_base_provider.rb +2 -1
  37. data/lib/active_agent/providers/anthropic/request.rb +3 -1
  38. data/lib/active_agent/providers/anthropic/transforms.rb +88 -0
  39. data/lib/active_agent/providers/anthropic_provider.rb +14 -4
  40. data/lib/active_agent/providers/azure/_types.rb +5 -0
  41. data/lib/active_agent/providers/azure/options.rb +111 -0
  42. data/lib/active_agent/providers/azure_open_ai_provider.rb +2 -0
  43. data/lib/active_agent/providers/azure_openai_provider.rb +2 -0
  44. data/lib/active_agent/providers/azure_provider.rb +133 -0
  45. data/lib/active_agent/providers/azureopenai_provider.rb +2 -0
  46. data/lib/active_agent/providers/bedrock/_types.rb +8 -0
  47. data/lib/active_agent/providers/bedrock/bearer_client.rb +109 -0
  48. data/lib/active_agent/providers/bedrock/options.rb +77 -0
  49. data/lib/active_agent/providers/bedrock_provider.rb +84 -0
  50. data/lib/active_agent/providers/common/messages/_types.rb +6 -2
  51. data/lib/active_agent/providers/concerns/exception_handler.rb +1 -0
  52. data/lib/active_agent/providers/gemini/_types.rb +19 -0
  53. data/lib/active_agent/providers/gemini/options.rb +41 -0
  54. data/lib/active_agent/providers/gemini_provider.rb +94 -0
  55. data/lib/active_agent/providers/mock/request.rb +1 -4
  56. data/lib/active_agent/providers/open_ai/chat/transforms.rb +37 -1
  57. data/lib/active_agent/providers/open_ai/chat_provider.rb +5 -3
  58. data/lib/active_agent/providers/requesty/_types.rb +16 -0
  59. data/lib/active_agent/providers/requesty/options.rb +39 -0
  60. data/lib/active_agent/providers/requesty_provider.rb +63 -0
  61. data/lib/active_agent/providers/ruby_llm/_types.rb +77 -0
  62. data/lib/active_agent/providers/ruby_llm/embedding_request.rb +16 -0
  63. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +109 -0
  64. data/lib/active_agent/providers/ruby_llm/messages/assistant.rb +27 -0
  65. data/lib/active_agent/providers/ruby_llm/messages/base.rb +48 -0
  66. data/lib/active_agent/providers/ruby_llm/messages/system.rb +18 -0
  67. data/lib/active_agent/providers/ruby_llm/messages/tool.rb +24 -0
  68. data/lib/active_agent/providers/ruby_llm/messages/user.rb +18 -0
  69. data/lib/active_agent/providers/ruby_llm/options.rb +28 -0
  70. data/lib/active_agent/providers/ruby_llm/request.rb +30 -0
  71. data/lib/active_agent/providers/ruby_llm/tool_proxy.rb +45 -0
  72. data/lib/active_agent/providers/ruby_llm_provider.rb +407 -0
  73. data/lib/active_agent/railtie.rb +32 -1
  74. data/lib/active_agent/telemetry/configuration.rb +213 -0
  75. data/lib/active_agent/telemetry/instrumentation.rb +155 -0
  76. data/lib/active_agent/telemetry/reporter.rb +176 -0
  77. data/lib/active_agent/telemetry/span.rb +267 -0
  78. data/lib/active_agent/telemetry/tracer.rb +184 -0
  79. data/lib/active_agent/telemetry.rb +162 -0
  80. data/lib/active_agent/version.rb +1 -1
  81. data/lib/active_agent.rb +2 -0
  82. data/lib/generators/active_agent/dashboard/install/install_generator.rb +96 -0
  83. data/lib/generators/active_agent/dashboard/install/templates/initializer.rb +89 -0
  84. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_runs.rb +42 -0
  85. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_templates.rb +38 -0
  86. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agent_versions.rb +22 -0
  87. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_agents.rb +53 -0
  88. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_runs.rb +28 -0
  89. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_sandbox_sessions.rb +43 -0
  90. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_session_recordings.rb +44 -0
  91. data/lib/generators/active_agent/dashboard/install/templates/migrations/create_active_agent_telemetry_traces.rb +56 -0
  92. data/lib/generators/active_agent/dashboard/install_generator.rb +64 -0
  93. data/lib/generators/active_agent/dashboard/templates/active_agent_dashboard.rb.erb +30 -0
  94. data/lib/generators/active_agent/dashboard/templates/create_active_agent_telemetry_traces.rb.erb +30 -0
  95. metadata +120 -17
@@ -9,7 +9,8 @@ require_relative "concerns/tool_choice_clearing"
9
9
  # @private
10
10
  GEM_LOADERS = {
11
11
  anthropic: [ "anthropic", "~> 1.12", "anthropic" ],
12
- openai: [ "openai", "~> 0.34", "openai" ]
12
+ openai: [ "openai", "~> 0.34", "openai" ],
13
+ ruby_llm: [ "ruby_llm", ">= 1.0", "ruby_llm" ]
13
14
  }
14
15
 
15
16
  # Requires a provider's gem dependency.
@@ -71,7 +71,9 @@ module ActiveAgent
71
71
  # @raise [ArgumentError] when gem model validation fails
72
72
  def initialize(**params)
73
73
  # Step 1: Extract custom fields that gem doesn't support
74
- @response_format = params.delete(:response_format)
74
+ # Read response_format without deleting - normalize_params will delete and convert it
75
+ # to output_config for json_schema, or drop it for other types.
76
+ @response_format = params[:response_format]
75
77
  @stream = params.delete(:stream)
76
78
  anthropic_beta = params.delete(:anthropic_beta)
77
79
 
@@ -31,6 +31,12 @@ module ActiveAgent
31
31
  params[:tools] = normalize_tools(params[:tools]) if params[:tools]
32
32
  params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice]
33
33
 
34
+ # Normalize json_schema response_format → output_config (Anthropic's native structured output field)
35
+ if params[:response_format]
36
+ output_config = normalize_response_format(params.delete(:response_format))
37
+ params[:output_config] = output_config if output_config
38
+ end
39
+
34
40
  # Handle mcps parameter (common format) -> transforms to mcp_servers (provider format)
35
41
  if params[:mcps]
36
42
  params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps))
@@ -159,6 +165,87 @@ module ActiveAgent
159
165
  end
160
166
  end
161
167
 
168
+ # Normalizes response_format to Anthropic output_config structure.
169
+ #
170
+ # Supported (ActiveAgent common format):
171
+ # { type: "json_schema", json_schema: { name: "...", schema: {...}, strict: true } }
172
+ # → { format: { type: "json_schema", schema: {...} } }
173
+ #
174
+ # Notes:
175
+ # - Anthropic requires `additionalProperties: false` on all object schemas.
176
+ # This is auto-injected into any object schemas that don't have it set.
177
+ # - Anthropic does not use OpenAI's `name` or `strict` fields in output_config.format.
178
+ # - json_object is not handled here; it remains prompt-emulated.
179
+ # - text is not handled here; Anthropic returns plain text by default.
180
+ #
181
+ # @param format [Hash, Symbol, String] ActiveAgent common response_format
182
+ # @return [Hash] Anthropic output_config hash, or nil if not applicable
183
+ def normalize_response_format(format)
184
+ case format
185
+ when Hash
186
+ format_hash = format.deep_symbolize_keys
187
+
188
+ if format_hash[:type].to_s == "json_schema"
189
+ schema = format_hash[:json_schema]&.dig(:schema)
190
+ if schema
191
+ schema = inject_additional_properties(schema.deep_dup)
192
+ end
193
+ {
194
+ format: {
195
+ type: "json_schema",
196
+ schema: schema
197
+ }.compact
198
+ }
199
+ elsif format_hash[:type].to_s == "json_object"
200
+ # json_object is not handled here; it remains prompt-emulated.
201
+ nil
202
+ elsif format_hash[:type].to_s == "text"
203
+ # text is not handled here; it remains prompt-emulated.
204
+ nil
205
+ else
206
+ # Pass through (already properly structured or Anthropic native format)
207
+ format_hash
208
+ end
209
+ when Symbol, String
210
+ # Bare :json_schema without a schema cannot use native output_config.
211
+ # Anthropic requires a valid JSON Schema — return nil to fall back.
212
+ nil
213
+ else
214
+ format
215
+ end
216
+ end
217
+
218
+ # Recursively injects `additionalProperties: false` into all object schemas.
219
+ #
220
+ # Anthropic's structured output API requires this field on all object types.
221
+ # Only inserts when not already explicitly set by the user.
222
+ #
223
+ # @param schema [Hash] JSON Schema hash
224
+ # @return [Hash] schema with additionalProperties injected
225
+ def inject_additional_properties(schema)
226
+ return schema unless schema.is_a?(Hash)
227
+
228
+ if schema[:type] == "object" && !schema.key?(:additionalProperties)
229
+ schema[:additionalProperties] = false
230
+ end
231
+
232
+ # Recurse into nested schemas
233
+ schema[:properties]&.each_value { |prop| inject_additional_properties(prop) }
234
+ schema[:items]&.then { |items| inject_additional_properties(items) }
235
+ schema[:anyOf]&.each { |s| inject_additional_properties(s) }
236
+ schema[:oneOf]&.each { |s| inject_additional_properties(s) }
237
+ schema[:allOf]&.each { |s| inject_additional_properties(s) }
238
+
239
+ if schema[:definitions]
240
+ schema[:definitions].each_value { |defn| inject_additional_properties(defn) }
241
+ end
242
+ if schema[:"$defs"]
243
+ schema[:"$defs"].each_value { |defn| inject_additional_properties(defn) }
244
+ end
245
+
246
+ schema
247
+ end
248
+
162
249
  # Merges consecutive same-role messages into single messages with multiple content blocks.
163
250
  #
164
251
  # Required by Anthropic API - consecutive messages with the same role must be combined.
@@ -437,6 +524,7 @@ module ActiveAgent
437
524
  msg.delete(:model)
438
525
  msg.delete(:stop_reason)
439
526
  msg.delete(:stop_sequence)
527
+ msg.delete(:stop_details)
440
528
  msg.delete(:type)
441
529
  msg.delete(:usage)
442
530
  end
@@ -189,9 +189,14 @@ module ActiveAgent
189
189
  when :ping
190
190
  # No-Op Keep Awake
191
191
  when :overloaded_error
192
- # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
192
+ # TODO: https://docs.claude.com/en/docs/build-with-claude/streaming#error-events
193
+
194
+ # Higher-level convenience events from anthropic gem's MessageStream
195
+ when :text, :input_json, :citation, :thinking, :signature
196
+ # No-Op; Already handled via :content_block_delta
197
+
193
198
  else
194
- # No-Op: Looks like internal tracking from gem wrapper
199
+ # No-Op; Internal tracking from gem wrapper
195
200
  return if api_response_chunk.respond_to?(:snapshot)
196
201
  raise "Unexpected chunk type: #{api_response_chunk.type}"
197
202
  end
@@ -296,11 +301,16 @@ module ActiveAgent
296
301
  def process_prompt_finished_extract_function_calls
297
302
  message_stack.pluck(:content).flatten.select { _1 in { type: "tool_use" } }.map do |api_function_call|
298
303
  json_buf = api_function_call.delete(:json_buf)
299
- api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf
304
+ api_function_call[:input] = JSON.parse(json_buf, symbolize_names: true) if json_buf.present?
300
305
 
301
306
  # Handle case where :input is still a JSON string (gem >= 1.14.0)
307
+ # For tools with no parameters, input may be an empty string
302
308
  if api_function_call[:input].is_a?(String)
303
- api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
309
+ if api_function_call[:input].present?
310
+ api_function_call[:input] = JSON.parse(api_function_call[:input], symbolize_names: true)
311
+ else
312
+ api_function_call[:input] = {}
313
+ end
304
314
  end
305
315
 
306
316
  api_function_call
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "../open_ai/chat/_types"
5
+ require_relative "../open_ai/embedding/_types"
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../open_ai/options"
4
+
5
+ module ActiveAgent
6
+ module Providers
7
+ module Azure
8
+ # Configuration options for Azure OpenAI Service.
9
+ #
10
+ # Azure OpenAI uses a different authentication and endpoint structure than standard OpenAI:
11
+ # - Endpoint: https://{resource}.openai.azure.com/openai/deployments/{deployment}/
12
+ # - Authentication: api-key header instead of Authorization: Bearer
13
+ # - API Version: Required query parameter
14
+ #
15
+ # You can configure Azure OpenAI in two ways:
16
+ #
17
+ # 1. Using azure_resource and deployment_id (standard Azure OpenAI):
18
+ # @example
19
+ # options = Azure::Options.new(
20
+ # api_key: ENV["AZURE_OPENAI_API_KEY"],
21
+ # azure_resource: "mycompany",
22
+ # deployment_id: "gpt-4-deployment",
23
+ # api_version: "2024-10-21"
24
+ # )
25
+ #
26
+ # 2. Using a direct host/base_url (for custom domains or Azure AI Foundry):
27
+ # @example
28
+ # options = Azure::Options.new(
29
+ # api_key: ENV["AZURE_OPENAI_API_KEY"],
30
+ # host: "https://mycompany.cognitiveservices.azure.com/openai/deployments/gpt-4",
31
+ # api_version: "2024-10-21"
32
+ # )
33
+ class Options < ActiveAgent::Providers::OpenAI::Options
34
+ DEFAULT_API_VERSION = "2024-10-21"
35
+
36
+ attribute :azure_resource, :string
37
+ attribute :deployment_id, :string
38
+ attribute :api_version, :string, fallback: DEFAULT_API_VERSION
39
+
40
+ validates :azure_resource, presence: true, unless: :explicit_host_provided?
41
+ validates :deployment_id, presence: true, unless: :explicit_host_provided?
42
+
43
+ def initialize(kwargs = {})
44
+ kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
45
+ kwargs[:api_version] ||= resolve_api_version(kwargs)
46
+ # Store explicit host before super processes kwargs
47
+ # host is aliased to base_url in parent, so check both
48
+ @explicit_host = kwargs[:host] || kwargs[:base_url]
49
+ super(kwargs)
50
+ end
51
+
52
+ # Returns Azure-specific headers for authentication.
53
+ #
54
+ # Azure uses api-key header instead of Authorization: Bearer.
55
+ #
56
+ # @return [Hash] headers including api-key
57
+ def extra_headers
58
+ { "api-key" => api_key }
59
+ end
60
+
61
+ # Returns Azure-specific query parameters.
62
+ #
63
+ # Azure requires api-version as a query parameter.
64
+ #
65
+ # @return [Hash] query parameters including api-version
66
+ def extra_query
67
+ { "api-version" => api_version }
68
+ end
69
+
70
+ # Builds the base URL for Azure OpenAI API requests.
71
+ #
72
+ # If a direct host/base_url is provided, uses that directly.
73
+ # Otherwise, constructs the URL from azure_resource and deployment_id.
74
+ #
75
+ # @return [String] the Azure OpenAI endpoint URL
76
+ def base_url
77
+ if @explicit_host.present?
78
+ @explicit_host
79
+ elsif azure_resource.present? && deployment_id.present?
80
+ "https://#{azure_resource}.openai.azure.com/openai/deployments/#{deployment_id}"
81
+ else
82
+ raise ArgumentError, "Either host or azure_resource + deployment_id must be provided"
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def explicit_host_provided?
89
+ @explicit_host.present?
90
+ end
91
+
92
+ def resolve_api_key(kwargs)
93
+ kwargs[:api_key] ||
94
+ kwargs[:access_token] ||
95
+ ENV["AZURE_OPENAI_API_KEY"] ||
96
+ ENV["AZURE_OPENAI_ACCESS_TOKEN"]
97
+ end
98
+
99
+ def resolve_api_version(kwargs)
100
+ kwargs[:api_version] ||
101
+ ENV["AZURE_OPENAI_API_VERSION"] ||
102
+ DEFAULT_API_VERSION
103
+ end
104
+
105
+ # Not used as part of Azure OpenAI
106
+ def resolve_organization_id(_settings) = nil
107
+ def resolve_project_id(_settings) = nil
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,2 @@
1
+ # Azure OpenAI, alias for AzureOpenAI service name resolution
2
+ require_relative "azure_provider"
@@ -0,0 +1,2 @@
1
+ # Azure OpenAI, alias for :azure_openai provider reference
2
+ require_relative "azure_provider"
@@ -0,0 +1,133 @@
1
+ require_relative "_base_provider"
2
+
3
+ require_gem!(:openai, __FILE__)
4
+
5
+ require_relative "open_ai_provider"
6
+ require_relative "azure/_types"
7
+
8
+ module ActiveAgent
9
+ module Providers
10
+ # Provider for Azure OpenAI Service via OpenAI-compatible API.
11
+ #
12
+ # Azure OpenAI uses the same API structure as OpenAI but with different
13
+ # authentication (api-key header) and endpoint configuration (resource + deployment).
14
+ #
15
+ # @example Configuration in active_agent.yml
16
+ # azure_openai:
17
+ # service: "AzureOpenAI"
18
+ # api_key: <%= ENV["AZURE_OPENAI_API_KEY"] %>
19
+ # azure_resource: "mycompany"
20
+ # deployment_id: "gpt-4-deployment"
21
+ # api_version: "2024-10-21"
22
+ #
23
+ # @see OpenAI::ChatProvider
24
+ class AzureProvider < OpenAI::ChatProvider
25
+ # @return [String]
26
+ def self.service_name
27
+ "AzureOpenAI"
28
+ end
29
+
30
+ # @return [Class]
31
+ def self.options_klass
32
+ Azure::Options
33
+ end
34
+
35
+ # @return [ActiveModel::Type::Value]
36
+ def self.prompt_request_type
37
+ OpenAI::Chat::RequestType.new
38
+ end
39
+
40
+ # @return [ActiveModel::Type::Value]
41
+ def self.embed_request_type
42
+ OpenAI::Embedding::RequestType.new
43
+ end
44
+
45
+ # Returns a configured Azure OpenAI client.
46
+ #
47
+ # Uses a custom client subclass that handles Azure-specific authentication
48
+ # (api-key header instead of Authorization: Bearer).
49
+ #
50
+ # @return [AzureClient] the configured Azure client
51
+ def client
52
+ @client ||= AzureClient.new(
53
+ api_key: options.api_key,
54
+ base_url: options.base_url,
55
+ api_version: options.api_version,
56
+ max_retries: options.max_retries,
57
+ timeout: options.timeout,
58
+ initial_retry_delay: options.initial_retry_delay,
59
+ max_retry_delay: options.max_retry_delay
60
+ )
61
+ end
62
+
63
+ # Custom OpenAI client for Azure OpenAI Service.
64
+ #
65
+ # Azure uses different authentication headers (api-key instead of Authorization: Bearer)
66
+ # and requires api-version as a query parameter on all requests.
67
+ class AzureClient < ::OpenAI::Client
68
+ # @return [String]
69
+ attr_reader :api_version
70
+
71
+ # Creates a new Azure OpenAI client.
72
+ #
73
+ # @param api_key [String] Azure OpenAI API key
74
+ # @param base_url [String] Azure endpoint URL
75
+ # @param api_version [String] API version (e.g., "2024-10-21")
76
+ # @param max_retries [Integer] Maximum retry attempts
77
+ # @param timeout [Float] Request timeout in seconds
78
+ # @param initial_retry_delay [Float] Initial delay between retries
79
+ # @param max_retry_delay [Float] Maximum delay between retries
80
+ def initialize(
81
+ api_key:,
82
+ base_url:,
83
+ api_version:,
84
+ max_retries: self.class::DEFAULT_MAX_RETRIES,
85
+ timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
86
+ initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
87
+ max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
88
+ )
89
+ @api_version = api_version
90
+
91
+ super(
92
+ api_key: api_key,
93
+ base_url: base_url,
94
+ max_retries: max_retries,
95
+ timeout: timeout,
96
+ initial_retry_delay: initial_retry_delay,
97
+ max_retry_delay: max_retry_delay
98
+ )
99
+ end
100
+
101
+ private
102
+
103
+ # Azure uses api-key header instead of Authorization: Bearer.
104
+ #
105
+ # @return [Hash{String=>String}]
106
+ def auth_headers
107
+ return {} if @api_key.nil?
108
+
109
+ { "api-key" => @api_key }
110
+ end
111
+
112
+ # Builds request with Azure-specific query parameters.
113
+ #
114
+ # Injects api-version into extra_query for all requests.
115
+ #
116
+ # @param req [Hash] Request parameters
117
+ # @param opts [Hash] Request options
118
+ # @return [Hash] Built request
119
+ def build_request(req, opts)
120
+ # Inject api-version into extra_query
121
+ opts = opts.dup
122
+ opts[:extra_query] = (opts[:extra_query] || {}).merge("api-version" => @api_version)
123
+
124
+ super(req, opts)
125
+ end
126
+ end
127
+ end
128
+
129
+ # Aliases for provider loading with different service name variations
130
+ AzureOpenAIProvider = AzureProvider
131
+ AzureOpenaiProvider = AzureProvider
132
+ end
133
+ end
@@ -0,0 +1,2 @@
1
+ # Azure OpenAI, alternative naming for consistency with other provider aliases
2
+ require_relative "azure_provider"
@@ -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.
@@ -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
@@ -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