active_harness 0.2.6 → 0.2.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c32abcde2f732633681db8c314c0066d668a6e90818d0bb10324f7c4fcb26f20
4
- data.tar.gz: 32081332c6997ef6bc1296008b1841be30c333afa7ffd8d1701af1a7f182d718
3
+ metadata.gz: d7139860d4aa461b1d86eca144c4929d4bf362192099498eadf61401b85acf8d
4
+ data.tar.gz: 3f00f021d6fd660748767349fb6a00ba9187c6baf507d18b01ac668c52c67904
5
5
  SHA512:
6
- metadata.gz: 469e75529882ee17fb2610aad11823fab222cd3c060cb6a583990e1b22c4d9b8d1af5173b4158ef8ded2facb11b0ad6cb3fd9101be213e7327ca464106b720b6
7
- data.tar.gz: 291624ff7f7765ae5af466d8417cee75e66596b604afefc01263771c091c738f5739dd8ead25712075e4e1a32f28252c475b62ae0942e02ce62d2eb483f42eff
6
+ metadata.gz: 1c335d3ecce967dc10424ef0cbe15922561627539e7cbf5eb2a34ad7e672c964bd7fd796ed129229a41a7f32992c22241353e16b8e314aec7cd71878ea86148d
7
+ data.tar.gz: 930e5ad7ad85e5e269772e90c45453fb6672dc2f8752c3d546f9888d3d2c8cd2020a24190f7391f8217bf1c8e24cde67945788870c3b1d1ec60e01747c0d2698
@@ -134,8 +134,15 @@ module ActiveHarness
134
134
  @models = []
135
135
  end
136
136
 
137
- def use(provider:, model:, temperature: nil)
138
- @models << { provider: provider, model: model, temperature: temperature }.compact
137
+ def use(provider:, model:, temperature: nil, name: nil, retry_attempts: nil, retry_delay: nil)
138
+ @models << {
139
+ provider: provider,
140
+ model: model,
141
+ temperature: temperature,
142
+ name: name,
143
+ retry_attempts: retry_attempts,
144
+ retry_delay: retry_delay
145
+ }.compact
139
146
  end
140
147
 
141
148
  alias fallback use
@@ -1,36 +1,54 @@
1
1
  module ActiveHarness
2
2
  class Agent
3
- # Errors that allow retrying the next model in the chain
3
+ # Errors that allow retrying the next model in the chain.
4
+ # InvalidRequestError is included here so that a bad model name (or any
5
+ # per-model request failure) does not abort the entire chain — the next
6
+ # fallback model will be attempted instead.
4
7
  RETRYABLE_ERRORS = [
5
8
  Errors::TimeoutError,
6
9
  Errors::RateLimitError,
7
10
  Errors::ServerError,
8
- Errors::ProviderUnavailableError
11
+ Errors::ProviderUnavailableError,
12
+ Errors::InvalidRequestError
9
13
  ].freeze
10
14
 
11
- # Errors that abort the entire chain immediately
15
+ # Errors that abort the entire chain immediately.
16
+ # InvalidApiKeyError — the key is wrong for every model, retrying is pointless.
17
+ # SafetyBlockedError — the input itself is blocked; a different model won't help.
12
18
  STOP_ERRORS = [
13
- Errors::InvalidRequestError,
14
19
  Errors::InvalidApiKeyError,
15
20
  Errors::SafetyBlockedError
16
21
  ].freeze
17
22
 
18
23
  PROVIDERS = {
19
- openai: -> { Providers::OpenAI.new },
20
- openrouter: -> { Providers::OpenRouter.new },
21
- groq: -> { Providers::Groq.new },
22
- gemini: -> { Providers::Gemini.new },
23
- anthropic: -> { Providers::Anthropic.new }
24
+ openai: -> { Providers::OpenAI.new },
25
+ openrouter: -> { Providers::OpenRouter.new },
26
+ groq: -> { Providers::Groq.new },
27
+ gemini: -> { Providers::Gemini.new },
28
+ anthropic: -> { Providers::Anthropic.new },
29
+ xai: -> { Providers::XAI.new },
30
+ deepseek: -> { Providers::DeepSeek.new },
31
+ mistral: -> { Providers::Mistral.new },
32
+ ollama: -> { Providers::Ollama.new },
33
+ perplexity: -> { Providers::Perplexity.new },
34
+ gpustack: -> { Providers::GPUStack.new },
35
+ azure: -> { Providers::Azure.new },
36
+ bedrock: -> { Providers::Bedrock.new },
37
+ vertexai: -> { Providers::VertexAI.new },
38
+ custom: -> { Providers::Custom.new }
24
39
  }.freeze
25
40
 
26
41
  private
27
42
 
28
43
  def attempt_model(entry, system_prompt)
44
+ return attempt_via_ruby_llm(entry, system_prompt) if @config[:ruby_llm_backend]
45
+
29
46
  provider = resolve_provider(entry[:provider])
30
47
  messages = build_messages(system_prompt, @input)
31
48
  opts = { model: entry[:model], messages: messages }
32
49
  opts[:temperature] = entry[:temperature] if entry[:temperature]
33
50
  opts[:stream] = @stream if @stream
51
+ opts[:name] = entry[:name] if entry[:name]
34
52
  provider.call(**opts)
35
53
  end
36
54
 
@@ -0,0 +1,90 @@
1
+ module ActiveHarness
2
+ class Agent
3
+ # -------------------------------------------------------------------------
4
+ # RubyLLM backend DSL
5
+ #
6
+ # Allows an agent to delegate HTTP calls to the `ruby_llm` gem instead of
7
+ # ActiveHarness's built-in Net::HTTP providers.
8
+ #
9
+ # Usage:
10
+ #
11
+ # ruby_llm_backend do |params|
12
+ # RubyLLM.chat(
13
+ # model: params.model,
14
+ # provider: params.provider,
15
+ # assume_model_exists: true
16
+ # ).tap { |c| c.with_temperature(params.temperature) if params.temperature }
17
+ # end
18
+ #
19
+ # The block receives a BackendParams struct and must return a RubyLLM::Chat.
20
+ # ActiveHarness calls chat.ask(@input) and maps the result to its Result format.
21
+ #
22
+ # All existing features work unchanged:
23
+ # - model do / use / fallback (order of attempts)
24
+ # - retry_attempts / retry_delay (per-model retry policy)
25
+ # - fallback chain on error
26
+ # - hooks (:setup, :before_call, :retry, :failure, …)
27
+ # - streaming via stream: lambda
28
+ # -------------------------------------------------------------------------
29
+
30
+ # Passed to the ruby_llm_backend block for each model attempt.
31
+ BackendParams = Struct.new(:model, :provider, :temperature, keyword_init: true)
32
+
33
+ class << self
34
+ # Define the RubyLLM backend block for this agent class.
35
+ def ruby_llm_backend(&block)
36
+ agent_config[:ruby_llm_backend] = block
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # Called from attempt_model when ruby_llm_backend is configured.
43
+ def attempt_via_ruby_llm(entry, system_prompt)
44
+ require "ruby_llm"
45
+
46
+ backend = @config[:ruby_llm_backend]
47
+
48
+ params = BackendParams.new(
49
+ model: entry[:model],
50
+ provider: entry[:provider].to_s,
51
+ temperature: entry[:temperature]
52
+ )
53
+
54
+ chat = backend.call(params)
55
+ chat.with_instructions(system_prompt) if system_prompt
56
+
57
+ if @stream
58
+ response = chat.ask(@input) { |chunk| @stream.call(chunk.content) if chunk.content }
59
+ else
60
+ response = chat.ask(@input)
61
+ end
62
+
63
+ { content: response.content, usage: ruby_llm_usage(response) }
64
+ rescue ::RubyLLM::UnauthorizedError => e
65
+ raise Errors::InvalidApiKeyError, e.message
66
+ rescue ::RubyLLM::RateLimitError, ::RubyLLM::OverloadedError => e
67
+ raise Errors::RateLimitError, e.message
68
+ rescue ::RubyLLM::ServerError, ::RubyLLM::ServiceUnavailableError => e
69
+ raise Errors::ServerError, e.message
70
+ rescue ::RubyLLM::BadRequestError, ::RubyLLM::ContextLengthExceededError => e
71
+ raise Errors::InvalidRequestError, e.message
72
+ rescue ::RubyLLM::Error => e
73
+ raise Errors::ProviderError, e.message
74
+ rescue LoadError
75
+ raise Errors::ProviderUnavailableError,
76
+ "The `ruby_llm` gem is required. Add `gem \"ruby_llm\"` to your Gemfile."
77
+ end
78
+
79
+ def ruby_llm_usage(response)
80
+ t = response.tokens
81
+ return nil unless t
82
+
83
+ {
84
+ input_tokens: t.input,
85
+ output_tokens: t.output,
86
+ total_tokens: (t.input.to_i + t.output.to_i)
87
+ }.compact
88
+ end
89
+ end
90
+ end
@@ -61,9 +61,15 @@ module ActiveHarness
61
61
  run_hook(:before_call)
62
62
  attempts = []
63
63
 
64
+ cfg = ActiveHarness.config
65
+
64
66
  model_list.each do |entry|
67
+ retry_policy = Http::RetryPolicy.new(
68
+ max_attempts: entry[:retry_attempts] || cfg.retry_default_attempts,
69
+ base_delay: entry[:retry_delay] || cfg.retry_default_delay
70
+ )
65
71
  t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
66
- response = attempt_model(entry, @system_prompt)
72
+ response = retry_policy.run { attempt_model(entry, @system_prompt) }
67
73
  elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0).round(3)
68
74
  result = build_result(response, entry, attempts, elapsed)
69
75
  save_to_memory(result)
@@ -139,4 +145,5 @@ require_relative "agent/hooks"
139
145
  require_relative "agent/models"
140
146
  require_relative "agent/providers"
141
147
  require_relative "agent/output_parser"
148
+ require_relative "agent/ruby_llm_backend"
142
149
 
@@ -0,0 +1,173 @@
1
+ module ActiveHarness
2
+ # Central configuration object.
3
+ #
4
+ # Usage in config/initializers/active_harness.rb:
5
+ #
6
+ # ActiveHarness.configure do |config|
7
+ # config.openai_api_key = ENV["OPENAI_API_KEY"]
8
+ # config.openai_api_url = "https://api.openai.com/v1/chat/completions"
9
+ # # ...
10
+ # end
11
+ #
12
+ # If a value is not explicitly set, it is read from the corresponding
13
+ # environment variable so all existing ENV-based setups keep working.
14
+ class Configuration
15
+ # -------------------------------------------------------------------------
16
+ # Global
17
+ # -------------------------------------------------------------------------
18
+ attr_accessor :request_timeout
19
+
20
+ # Retry policy for a single model (exponential backoff).
21
+ # Set retry_default_attempts to 1 to disable retries entirely.
22
+ # Per-model values can be set via retry_attempts: / retry_delay: in the DSL.
23
+ attr_accessor :retry_default_attempts
24
+ attr_accessor :retry_default_delay
25
+
26
+ # -------------------------------------------------------------------------
27
+ # OpenAI
28
+ # -------------------------------------------------------------------------
29
+ attr_accessor :openai_api_key
30
+ attr_accessor :openai_api_url
31
+
32
+ # -------------------------------------------------------------------------
33
+ # Anthropic
34
+ # -------------------------------------------------------------------------
35
+ attr_accessor :anthropic_api_key
36
+ attr_accessor :anthropic_api_url
37
+
38
+ # -------------------------------------------------------------------------
39
+ # Google Gemini (OpenAI-compatible REST endpoint)
40
+ # -------------------------------------------------------------------------
41
+ attr_accessor :gemini_api_key
42
+ attr_accessor :gemini_api_url
43
+
44
+ # -------------------------------------------------------------------------
45
+ # Groq
46
+ # -------------------------------------------------------------------------
47
+ attr_accessor :groq_api_key
48
+ attr_accessor :groq_api_url
49
+
50
+ # -------------------------------------------------------------------------
51
+ # OpenRouter
52
+ # -------------------------------------------------------------------------
53
+ attr_accessor :openrouter_api_key
54
+ attr_accessor :openrouter_api_url
55
+ attr_accessor :openrouter_http_referer
56
+
57
+ # -------------------------------------------------------------------------
58
+ # xAI (Grok)
59
+ # -------------------------------------------------------------------------
60
+ attr_accessor :xai_api_key
61
+ attr_accessor :xai_api_url
62
+
63
+ # -------------------------------------------------------------------------
64
+ # DeepSeek
65
+ # -------------------------------------------------------------------------
66
+ attr_accessor :deepseek_api_key
67
+ attr_accessor :deepseek_api_url
68
+
69
+ # -------------------------------------------------------------------------
70
+ # Mistral
71
+ # -------------------------------------------------------------------------
72
+ attr_accessor :mistral_api_key
73
+ attr_accessor :mistral_api_url
74
+
75
+ # -------------------------------------------------------------------------
76
+ # Ollama (local — key is optional)
77
+ # -------------------------------------------------------------------------
78
+ attr_accessor :ollama_api_key
79
+ attr_accessor :ollama_api_base
80
+
81
+ # -------------------------------------------------------------------------
82
+ # Perplexity
83
+ # -------------------------------------------------------------------------
84
+ attr_accessor :perplexity_api_key
85
+ attr_accessor :perplexity_api_url
86
+
87
+ # -------------------------------------------------------------------------
88
+ # GPUStack (self-hosted — key is optional)
89
+ # -------------------------------------------------------------------------
90
+ attr_accessor :gpustack_api_key
91
+ attr_accessor :gpustack_api_base
92
+
93
+ # -------------------------------------------------------------------------
94
+ # Azure OpenAI Service
95
+ # -------------------------------------------------------------------------
96
+ attr_accessor :azure_api_key # api-key header (preferred)
97
+ attr_accessor :azure_ai_auth_token # Bearer token (alternative to api-key)
98
+ attr_accessor :azure_api_base # e.g. "https://my-resource.openai.azure.com"
99
+ attr_accessor :azure_api_version # e.g. "2024-05-01-preview"
100
+
101
+ # -------------------------------------------------------------------------
102
+ # Custom providers
103
+ #
104
+ # Register any OpenAI-compatible endpoint under an arbitrary name:
105
+ #
106
+ # ActiveHarness.configure do |config|
107
+ # config.custom["MyLocal"]["url"] = "http://localhost:8080/v1/chat/completions"
108
+ # config.custom["MyLocal"]["api_key"] = ENV["MYLOCAL_API_KEY"] # omit if no auth
109
+ #
110
+ # config.custom["SecondProvider"]["url"] = "https://second.example.com/v1/chat/completions"
111
+ # config.custom["SecondProvider"]["api_key"] = ENV["SECOND_API_KEY"]
112
+ # end
113
+ #
114
+ # Use in an agent:
115
+ # model do
116
+ # use provider: :custom, name: "MyLocal", model: "llama3.2"
117
+ # fallback provider: :custom, name: "SecondProvider", model: "mixtral"
118
+ # end
119
+ # -------------------------------------------------------------------------
120
+ def custom
121
+ @custom ||= Hash.new { |h, k| h[k] = {} }
122
+ end
123
+
124
+ # -------------------------------------------------------------------------
125
+ # Defaults — all keys fall back to the corresponding ENV variable so that
126
+ # existing ENV-based setups keep working without any changes.
127
+ # -------------------------------------------------------------------------
128
+ def initialize
129
+ @request_timeout = 10
130
+ @retry_default_attempts = 3
131
+ @retry_default_delay = 1.0
132
+
133
+ @openai_api_key = ENV["OPENAI_API_KEY"]
134
+ @openai_api_url = "https://api.openai.com/v1/chat/completions"
135
+
136
+ @anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
137
+ @anthropic_api_url = "https://api.anthropic.com/v1/messages"
138
+
139
+ @gemini_api_key = ENV["GEMINI_API_KEY"]
140
+ @gemini_api_url = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"
141
+
142
+ @groq_api_key = ENV["GROQ_API_KEY"]
143
+ @groq_api_url = "https://api.groq.com/openai/v1/chat/completions"
144
+
145
+ @openrouter_api_key = ENV["OPENROUTER_API_KEY"]
146
+ @openrouter_api_url = "https://openrouter.ai/api/v1/chat/completions"
147
+ @openrouter_http_referer = "https://github.com/the-teacher/ActiveHarness"
148
+
149
+ @xai_api_key = ENV["XAI_API_KEY"]
150
+ @xai_api_url = "https://api.x.ai/v1/chat/completions"
151
+
152
+ @deepseek_api_key = ENV["DEEPSEEK_API_KEY"]
153
+ @deepseek_api_url = "https://api.deepseek.com/v1/chat/completions"
154
+
155
+ @mistral_api_key = ENV["MISTRAL_API_KEY"]
156
+ @mistral_api_url = "https://api.mistral.ai/v1/chat/completions"
157
+
158
+ @ollama_api_key = ENV["OLLAMA_API_KEY"] # nil if not set — key is optional
159
+ @ollama_api_base = ENV.fetch("OLLAMA_API_BASE", "http://localhost:11434")
160
+
161
+ @perplexity_api_key = ENV["PERPLEXITY_API_KEY"]
162
+ @perplexity_api_url = "https://api.perplexity.ai/chat/completions"
163
+
164
+ @gpustack_api_key = ENV["GPUSTACK_API_KEY"] # nil if not set — key is optional
165
+ @gpustack_api_base = ENV["GPUSTACK_API_BASE"]
166
+
167
+ @azure_api_key = ENV["AZURE_API_KEY"]
168
+ @azure_ai_auth_token = ENV["AZURE_AI_AUTH_TOKEN"]
169
+ @azure_api_base = ENV["AZURE_API_BASE"]
170
+ @azure_api_version = ENV.fetch("AZURE_API_VERSION", "2024-05-01-preview")
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveHarness
2
+ module Http
3
+ # Wraps a block with automatic retry on transient errors.
4
+ # Uses exponential backoff: delay doubles after each failed attempt.
5
+ #
6
+ # Example:
7
+ # RetryPolicy.new(max_attempts: 3, base_delay: 0.5).run do
8
+ # http_client.post(...)
9
+ # end
10
+ #
11
+ # Disable retries entirely:
12
+ # RetryPolicy.new(max_attempts: 1).run { ... }
13
+ #
14
+ class RetryPolicy
15
+ RETRYABLE_ERRORS = [
16
+ Errors::TimeoutError,
17
+ Errors::RateLimitError,
18
+ Errors::ProviderUnavailableError,
19
+ Errors::ServerError
20
+ ].freeze
21
+
22
+ # @param max_attempts [Integer] total number of attempts (1 = no retries)
23
+ # @param base_delay [Float] seconds before 1st retry; doubles each round
24
+ # @param errors [Array] error classes that trigger a retry
25
+ def initialize(max_attempts: 3, base_delay: 1.0, errors: RETRYABLE_ERRORS)
26
+ @max_attempts = max_attempts
27
+ @base_delay = base_delay
28
+ @errors = errors
29
+ end
30
+
31
+ # @yieldreturn [Object] result of the block on success
32
+ # @raise last error after all attempts are exhausted
33
+ def run
34
+ attempt = 0
35
+ begin
36
+ attempt += 1
37
+ yield
38
+ rescue *@errors => e
39
+ raise if attempt >= @max_attempts
40
+
41
+ sleep(@base_delay * (2**(attempt - 1)))
42
+ retry
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -5,7 +5,6 @@ module ActiveHarness
5
5
  # Anthropic Claude — native Messages API (not OpenAI-compatible).
6
6
  # https://docs.anthropic.com/en/api/messages
7
7
  class Anthropic < Base
8
- API_URL = URI("https://api.anthropic.com/v1/messages")
9
8
  ANTHROPIC_VERSION = "2023-06-01"
10
9
  DEFAULT_MAX_TOKENS = 1024
11
10
 
@@ -20,7 +19,7 @@ module ActiveHarness
20
19
  }
21
20
  body[:system] = system_msg if system_msg
22
21
 
23
- raw = post_json(API_URL,
22
+ raw = post_json(URI(config.anthropic_api_url),
24
23
  headers: {
25
24
  "Content-Type" => "application/json",
26
25
  "x-api-key" => api_key,
@@ -52,8 +51,8 @@ module ActiveHarness
52
51
  end
53
52
 
54
53
  def api_key
55
- key = ENV["ANTHROPIC_API_KEY"].to_s
56
- raise Errors::InvalidApiKeyError, "ANTHROPIC_API_KEY is not set" if key.empty?
54
+ key = config.anthropic_api_key.to_s
55
+ raise Errors::InvalidApiKeyError, "anthropic_api_key is not configured" if key.empty?
57
56
  key
58
57
  end
59
58
 
@@ -0,0 +1,97 @@
1
+ require "uri"
2
+
3
+ module ActiveHarness
4
+ module Providers
5
+ # Azure OpenAI Service — deployment-based API.
6
+ # https://learn.microsoft.com/en-us/azure/ai-services/openai/reference
7
+ #
8
+ # The `model:` parameter is treated as the **deployment name** you created
9
+ # in the Azure portal (not the underlying model name).
10
+ #
11
+ # Required config (or ENV fallback):
12
+ # config.azure_api_base — "https://my-resource.openai.azure.com"
13
+ # config.azure_api_key — your resource API key
14
+ # (alternatively: config.azure_ai_auth_token for OAuth bearer)
15
+ #
16
+ # Optional config:
17
+ # config.azure_api_version — defaults to "2024-05-01-preview"
18
+ #
19
+ # Resulting endpoint:
20
+ # POST {azure_api_base}/openai/deployments/{deployment}/chat/completions
21
+ # ?api-version={azure_api_version}
22
+ #
23
+ # Example agent config:
24
+ # model do
25
+ # use provider: :azure, model: "my-gpt4o-deployment", temperature: 0.7
26
+ # end
27
+ class Azure < Base
28
+ def call(model:, messages:, temperature: 0.7)
29
+ url = build_url(model)
30
+
31
+ raw = post_json(url,
32
+ headers: {
33
+ "Content-Type" => "application/json"
34
+ }.merge(auth_header),
35
+ body: { messages: messages, temperature: temperature }
36
+ )
37
+ data = parse!(raw)
38
+ handle_error!(data)
39
+
40
+ {
41
+ content: data.dig("choices", 0, "message", "content").to_s.strip,
42
+ provider: :azure,
43
+ model: data["model"] || model,
44
+ usage: extract_usage_openai(data)
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ def build_url(deployment)
51
+ URI("#{api_base}/openai/deployments/#{deployment}/chat/completions?api-version=#{config.azure_api_version}")
52
+ end
53
+
54
+ def api_base
55
+ base = config.azure_api_base.to_s
56
+ raise Errors::InvalidRequestError, "azure_api_base is not configured" if base.empty?
57
+ base.chomp("/")
58
+ end
59
+
60
+ # Azure accepts either a resource API key (header: api-key)
61
+ # or an OAuth2 bearer token (header: Authorization).
62
+ def auth_header
63
+ if (key = config.azure_api_key.to_s) && !key.empty?
64
+ { "api-key" => key }
65
+ elsif (token = config.azure_ai_auth_token.to_s) && !token.empty?
66
+ { "Authorization" => "Bearer #{token}" }
67
+ else
68
+ raise Errors::InvalidApiKeyError,
69
+ "Neither azure_api_key nor azure_ai_auth_token is configured"
70
+ end
71
+ end
72
+
73
+ def handle_error!(data)
74
+ return unless data["error"]
75
+
76
+ msg = data.dig("error", "message").to_s
77
+ code = data.dig("error", "code").to_s
78
+ type = data.dig("error", "innererror", "code").to_s
79
+ metadata = data["error"].reject { |k, _| %w[message code innererror].include?(k) }
80
+ metadata = nil if metadata.empty?
81
+
82
+ case code
83
+ when "401", "invalid_api_key", "unauthorized", "AccessDenied"
84
+ raise Errors::InvalidApiKeyError.new(msg, error_code: code, metadata: metadata)
85
+ when "429", "TooManyRequests"
86
+ raise Errors::RateLimitError.new(msg, error_code: code, metadata: metadata)
87
+ when "ContentFilter", "content_filter"
88
+ raise Errors::SafetyBlockedError.new(msg, error_code: code, metadata: metadata)
89
+ when "500", "502", "503", "504", "ServiceUnavailable"
90
+ raise Errors::ProviderUnavailableError.new(msg, error_code: code, metadata: metadata)
91
+ else
92
+ raise Errors::InvalidRequestError.new(msg, error_code: code, metadata: metadata)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -8,6 +8,10 @@ module ActiveHarness
8
8
 
9
9
  private
10
10
 
11
+ def config
12
+ ActiveHarness.config
13
+ end
14
+
11
15
  def post_json(url, headers:, body:, timeout: 30)
12
16
  HTTP.post(url, headers: headers, body: body.to_json, timeout: timeout)
13
17
  end
@@ -0,0 +1,29 @@
1
+ module ActiveHarness
2
+ module Providers
3
+ # AWS Bedrock — stub provider.
4
+ #
5
+ # Bedrock requires AWS Signature V4 request signing, which is non-trivial
6
+ # to implement and carries AWS SDK dependencies. This stub raises a clear
7
+ # error so that the agent falls through to the next model in its fallback chain.
8
+ #
9
+ # To use Bedrock in production, please look for a dedicated gem, for example:
10
+ # gem "active_harness-bedrock" (not yet released — contributions welcome)
11
+ #
12
+ # Example agent config (will fall through to the next fallback):
13
+ # model do
14
+ # use provider: :bedrock, model: "anthropic.claude-3-5-sonnet-20241022-v2:0"
15
+ # fallback provider: :anthropic, model: "claude-3-5-sonnet-20241022"
16
+ # end
17
+ class Bedrock < Base
18
+ STUB_MESSAGE = <<~MSG.strip
19
+ ActiveHarness: AWS Bedrock provider is not built-in.
20
+ Bedrock requires AWS Signature V4 signing — please use a dedicated gem.
21
+ Falling through to the next model in the fallback chain.
22
+ MSG
23
+
24
+ def call(model:, messages:, temperature: 0.7) # rubocop:disable Lint/UnusedMethodArgument
25
+ raise Errors::ProviderUnavailableError, STUB_MESSAGE
26
+ end
27
+ end
28
+ end
29
+ end