Class: RailsConsoleAi::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_console_ai/configuration.rb

Constant Summary collapse

PROVIDERS =
%i[anthropic openai local bedrock].freeze
MODEL_FAMILIES =

Per-family model attributes, matched by substring so one entry covers every ID variant of a family: bare Anthropic IDs (claude-sonnet-5), dated snapshots (claude-haiku-4-5-20251001), and Bedrock inference profiles (us.anthropic.claude-sonnet-5, global.anthropic.claude-opus-4-6-v1).

input/output are $ per MTok (converted to per-token in .pricing_for). Cache pricing is derived: read = 0.1x input, write = 1.25x input. temperature: false marks families that reject the temperature parameter (removed on opus-4-7+, sonnet-5, and fable-5).

{
  'claude-fable-5'    => { input: 10.0, output: 50.0, max_tokens: 16_000, temperature: false },
  'claude-opus-5'     => { input: 5.0,  output: 25.0, max_tokens: 16_000, temperature: false },
  'claude-opus-4-8'   => { input: 5.0,  output: 25.0, max_tokens: 16_000, temperature: false },
  'claude-opus-4-7'   => { input: 5.0,  output: 25.0, max_tokens: 16_000, temperature: false },
  'claude-opus-4-6'   => { input: 5.0,  output: 25.0, max_tokens: 16_000, temperature: true },
  'claude-sonnet-5'   => { input: 3.0,  output: 15.0, max_tokens: 16_000, temperature: false },
  'claude-sonnet-4-6' => { input: 3.0,  output: 15.0, max_tokens: 16_000, temperature: true },
  'claude-haiku-4-5'  => { input: 1.0,  output: 5.0,  max_tokens: 16_000, temperature: true },
}.freeze
MODEL_FAMILY_KEYS =

Family keys sorted longest-first so a more specific family always wins if keys ever overlap (e.g. a future 'claude-sonnet-5-5' entry would match before 'claude-sonnet-5').

MODEL_FAMILIES.keys.sort_by { |k| -k.length }.freeze
DEFAULT_ERROR_HINTS =

Known environment-level failures the executor recognizes and explains to the LLM on the FIRST occurrence, so it doesn't burn rounds rediscovering them through trial and error. Each entry: { name:, pattern:, hint: }. The pattern is matched against the execution error AND the captured output (to catch errors rescued and printed by the generated code itself).

[
  {
    name: :decryption_failure,
    pattern: /OpenSSL::Cipher::CipherError|bad decrypt|ActiveRecord::Encryption::Errors/i,
    hint: "Decryption failed — this console process's encryption key (e.g. ENV['ENCRYPTION_KEY']) " \
          "appears to be missing, invalid, or a placeholder for the data you are reading. This is an " \
          "environment configuration issue, not a data issue, and it affects EVERY encrypted field in " \
          "this session. Retrying with different code (reloading records, toggling encryption flags, " \
          "decrypting other fields or records) will fail the same way. Do NOT retry. Report this " \
          "limitation to the user, tell them the encryption key needs to be configured for this " \
          "environment, and answer using only what you can determine without decrypting."
  }
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfiguration

Returns a new instance of Configuration.



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/rails_console_ai/configuration.rb', line 89

def initialize
  @provider     = :anthropic
  @api_key      = nil
  @model        = nil
  @thinking_model = nil
  @max_tokens   = nil
  @auto_execute = false
  @temperature  = 0.2
  @timeout      = 30
  @debug        = false
  @max_tool_rounds = 200
  @error_hints = DEFAULT_ERROR_HINTS.dup
  @token_nudge_threshold = 500_000    # input tokens in one tool loop → nudge model to wrap up (nil disables)
  @token_stop_threshold  = 1_000_000  # input tokens in one tool loop → force a final answer (nil disables)
  @storage_adapter  = nil
  @memories_enabled = true
  @session_logging  = true
  @connection_class = nil
  @admin_username   = nil
  @admin_password   = nil
  @authenticate     = nil
  @safety_guards    = nil
  @slack_bot_token  = nil
  @slack_app_token  = nil
  @slack_channel_ids = nil
  @slack_allowed_usernames = nil
  @local_url        = 'http://localhost:11434'
  @local_model      = 'qwen2.5:7b'
  @local_api_key    = nil
  @bedrock_region   = nil
  @code_search_paths = %w[app]
  @channels = {}
  @bypass_guards_for_methods = []
  @user_extra_info = {}
  @sub_agent_max_rounds = 15
  @sub_agent_model = nil
end

Instance Attribute Details

#admin_passwordObject

Returns the value of attribute admin_password.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def admin_password
  @admin_password
end

#admin_usernameObject

Returns the value of attribute admin_username.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def admin_username
  @admin_username
end

#api_keyObject

Returns the value of attribute api_key.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def api_key
  @api_key
end

#authenticateObject

Returns the value of attribute authenticate.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def authenticate
  @authenticate
end

#auto_executeObject

Returns the value of attribute auto_execute.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def auto_execute
  @auto_execute
end

#bedrock_regionObject

Returns the value of attribute bedrock_region.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def bedrock_region
  @bedrock_region
end

#bypass_guards_for_methodsObject

Returns the value of attribute bypass_guards_for_methods.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def bypass_guards_for_methods
  @bypass_guards_for_methods
end

#channelsObject

Returns the value of attribute channels.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def channels
  @channels
end

#code_search_pathsObject

Returns the value of attribute code_search_paths.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def code_search_paths
  @code_search_paths
end

#connection_classObject

Returns the value of attribute connection_class.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def connection_class
  @connection_class
end

#debugObject

Returns the value of attribute debug.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def debug
  @debug
end

#error_hintsObject

Returns the value of attribute error_hints.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def error_hints
  @error_hints
end

#local_api_keyObject

Returns the value of attribute local_api_key.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def local_api_key
  @local_api_key
end

#local_modelObject

Returns the value of attribute local_model.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def local_model
  @local_model
end

#local_urlObject

Returns the value of attribute local_url.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def local_url
  @local_url
end

#max_tokensObject

Returns the value of attribute max_tokens.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def max_tokens
  @max_tokens
end

#max_tool_roundsObject

Returns the value of attribute max_tool_rounds.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def max_tool_rounds
  @max_tool_rounds
end

#memories_enabledObject

Returns the value of attribute memories_enabled.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def memories_enabled
  @memories_enabled
end

#modelObject

Returns the value of attribute model.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def model
  @model
end

#providerObject

Returns the value of attribute provider.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def provider
  @provider
end

#session_loggingObject

Returns the value of attribute session_logging.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def session_logging
  @session_logging
end

#slack_allowed_usernamesObject

Returns the value of attribute slack_allowed_usernames.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def slack_allowed_usernames
  @slack_allowed_usernames
end

#slack_app_tokenObject

Returns the value of attribute slack_app_token.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def slack_app_token
  @slack_app_token
end

#slack_bot_tokenObject

Returns the value of attribute slack_bot_token.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def slack_bot_token
  @slack_bot_token
end

#slack_channel_idsObject

Returns the value of attribute slack_channel_ids.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def slack_channel_ids
  @slack_channel_ids
end

#storage_adapterObject

Returns the value of attribute storage_adapter.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def storage_adapter
  @storage_adapter
end

#sub_agent_max_roundsObject

Returns the value of attribute sub_agent_max_rounds.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def sub_agent_max_rounds
  @sub_agent_max_rounds
end

#sub_agent_modelObject

Returns the value of attribute sub_agent_model.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def sub_agent_model
  @sub_agent_model
end

#temperatureObject

Returns the value of attribute temperature.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def temperature
  @temperature
end

#thinking_modelObject

Returns the value of attribute thinking_model.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def thinking_model
  @thinking_model
end

#timeoutObject

Returns the value of attribute timeout.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def timeout
  @timeout
end

#token_nudge_thresholdObject

Returns the value of attribute token_nudge_threshold.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def token_nudge_threshold
  @token_nudge_threshold
end

#token_stop_thresholdObject

Returns the value of attribute token_stop_threshold.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def token_stop_threshold
  @token_stop_threshold
end

#user_extra_infoObject

Returns the value of attribute user_extra_info.



70
71
72
# File 'lib/rails_console_ai/configuration.rb', line 70

def user_extra_info
  @user_extra_info
end

Class Method Details

.model_family(model_id) ⇒ Object

Returns the family attributes for a model ID, or nil for unknown models.



31
32
33
34
35
# File 'lib/rails_console_ai/configuration.rb', line 31

def self.model_family(model_id)
  return nil unless model_id
  key = MODEL_FAMILY_KEYS.find { |k| model_id.include?(k) }
  key && MODEL_FAMILIES[key]
end

.pricing_for(model_id) ⇒ Object

Per-token pricing for a model ID, matched by family. Returns { input:, output:, cache_read:, cache_write: } or nil for unknown models.



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/rails_console_ai/configuration.rb', line 39

def self.pricing_for(model_id)
  family = model_family(model_id)
  return nil unless family
  input = family[:input] / 1_000_000
  {
    input: input,
    output: family[:output] / 1_000_000,
    cache_read: input * 0.1,
    cache_write: input * 1.25,
  }
end

Instance Method Details

#channel_setting(mode, key) ⇒ Object

Look up a per-channel setting with backward compatibility. Falls back to top-level slack_* config when channels hash doesn't have the key.



134
135
136
137
138
139
140
141
142
143
144
# File 'lib/rails_console_ai/configuration.rb', line 134

def channel_setting(mode, key)
  channel_cfg = @channels[mode.to_s] || {}
  value = channel_cfg[key.to_s]

  # Backward compatibility: slack_allowed_usernames → channels.slack.allowed_usernames
  if value.nil? && mode.to_s == 'slack' && key.to_s == 'allowed_usernames'
    value = @slack_allowed_usernames
  end

  value
end

#resolve_user_extra_info(username) ⇒ Object



127
128
129
130
# File 'lib/rails_console_ai/configuration.rb', line 127

def resolve_user_extra_info(username)
  return nil if @user_extra_info.nil? || @user_extra_info.empty? || username.nil?
  @user_extra_info[username.to_s.downcase]
end

#resolved_api_keyObject



200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/rails_console_ai/configuration.rb', line 200

def resolved_api_key
  return @api_key if @api_key && !@api_key.empty?

  case @provider
  when :anthropic
    ENV['ANTHROPIC_API_KEY']
  when :openai
    ENV['OPENAI_API_KEY']
  when :local
    @local_api_key || 'no-key'
  when :bedrock
    'aws-sdk'
  end
end

#resolved_max_tokensObject



230
231
232
233
234
235
# File 'lib/rails_console_ai/configuration.rb', line 230

def resolved_max_tokens
  return @max_tokens if @max_tokens

  family = self.class.model_family(resolved_model)
  family ? family[:max_tokens] : 4096
end

#resolved_modelObject



215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/rails_console_ai/configuration.rb', line 215

def resolved_model
  return @model if @model && !@model.empty?

  case @provider
  when :anthropic
    'claude-sonnet-5'
  when :openai
    'gpt-5.3-codex'
  when :local
    @local_model
  when :bedrock
    'us.anthropic.claude-sonnet-5'
  end
end

#resolved_temperatureObject

Returns nil for model families that reject the temperature parameter (opus-4-7+, sonnet-5, fable-5) so providers omit the field from the request.



239
240
241
242
243
# File 'lib/rails_console_ai/configuration.rb', line 239

def resolved_temperature
  family = self.class.model_family(resolved_model)
  return nil if family && family[:temperature] == false
  @temperature
end

#resolved_thinking_modelObject



245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/rails_console_ai/configuration.rb', line 245

def resolved_thinking_model
  return @thinking_model if @thinking_model && !@thinking_model.empty?

  case @provider
  when :anthropic
    'claude-opus-5'
  when :openai
    'gpt-5.3-codex'
  when :local
    @local_model
  when :bedrock
    'us.anthropic.claude-opus-5'
  end
end

#resolved_timeoutObject



260
261
262
# File 'lib/rails_console_ai/configuration.rb', line 260

def resolved_timeout
  @provider == :local ? [@timeout, 300].max : @timeout
end

#safety_guard(name, &block) ⇒ Object

Register a custom safety guard by name with an around-block.

config.safety_guard :mailers do |&execute|
ActionMailer::Base.perform_deliveries = false
execute.call
ensure
ActionMailer::Base.perform_deliveries = true
end


170
171
172
# File 'lib/rails_console_ai/configuration.rb', line 170

def safety_guard(name, &block)
  safety_guards.add(name, &block)
end

#safety_guardsObject



155
156
157
158
159
160
# File 'lib/rails_console_ai/configuration.rb', line 155

def safety_guards
  @safety_guards ||= begin
    require 'rails_console_ai/safety_guards'
    SafetyGuards.new
  end
end

#use_builtin_safety_guard(name, allow: nil) ⇒ Object

Register a built-in safety guard by name. Available: :database_writes, :http_mutations, :mailers

Options:

allow: Array of strings or regexps to allowlist for this guard.
- :http_mutations  → hosts (e.g. "s3.amazonaws.com", /googleapis\.com/)
- :database_writes → table names (e.g. "rails_console_ai_sessions")


181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/rails_console_ai/configuration.rb', line 181

def use_builtin_safety_guard(name, allow: nil)
  require 'rails_console_ai/safety_guards'
  guard_name = name.to_sym
  case guard_name
  when :database_writes
    safety_guards.add(:database_writes, &BuiltinGuards.database_writes)
  when :http_mutations
    safety_guards.add(:http_mutations, &BuiltinGuards.http_mutations)
  when :mailers
    safety_guards.add(:mailers, &BuiltinGuards.mailers)
  else
    raise ConfigurationError, "Unknown built-in safety guard: #{name}. Available: database_writes, http_mutations, mailers"
  end

  if allow
    Array(allow).each { |key| safety_guards.allow_global(guard_name, key) }
  end
end

#username_allowed?(mode, key, username) ⇒ Boolean

Check if a username is permitted by a channel setting. Returns true when the setting is nil (not configured = no restriction).

Returns:

  • (Boolean)


148
149
150
151
152
153
# File 'lib/rails_console_ai/configuration.rb', line 148

def username_allowed?(mode, key, username)
  list = channel_setting(mode, key)
  return true if list.nil?
  normalized = Array(list).map(&:to_s).map(&:downcase)
  normalized.include?('all') || normalized.include?(username.to_s.downcase)
end

#validate!Object



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/rails_console_ai/configuration.rb', line 264

def validate!
  unless PROVIDERS.include?(@provider)
    raise ConfigurationError, "Unknown provider: #{@provider}. Valid: #{PROVIDERS.join(', ')}"
  end

  if @provider == :local
    raise ConfigurationError, "No local_url configured for :local provider." unless @local_url && !@local_url.empty?
  elsif @provider == :bedrock
    begin
      require 'aws-sdk-bedrockruntime'
    rescue LoadError
      raise ConfigurationError,
        "aws-sdk-bedrockruntime gem is required for the :bedrock provider. Add it to your Gemfile."
    end
  else
    unless resolved_api_key
      env_var = @provider == :anthropic ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY'
      raise ConfigurationError, "No API key. Set config.api_key or #{env_var} env var."
    end
  end
end