rails_console_ai 0.34.0 → 0.36.0
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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +56 -2
- data/app/helpers/rails_console_ai/sessions_helper.rb +11 -4
- data/app/views/rails_console_ai/sessions/index.html.erb +1 -1
- data/app/views/rails_console_ai/sessions/show.html.erb +6 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +12 -3
- data/lib/rails_console_ai/channel/console.rb +120 -15
- data/lib/rails_console_ai/configuration.rb +97 -19
- data/lib/rails_console_ai/conversation_engine.rb +241 -63
- data/lib/rails_console_ai/line_editor.rb +142 -0
- data/lib/rails_console_ai/providers/anthropic.rb +53 -9
- data/lib/rails_console_ai/providers/base.rb +74 -4
- data/lib/rails_console_ai/providers/bedrock.rb +47 -3
- data/lib/rails_console_ai/providers/local.rb +16 -36
- data/lib/rails_console_ai/providers/openai.rb +30 -9
- data/lib/rails_console_ai/providers/openrouter.rb +100 -0
- data/lib/rails_console_ai/session_logger.rb +4 -0
- data/lib/rails_console_ai/slack_bot.rb +19 -10
- data/lib/rails_console_ai/slash_commands.rb +149 -0
- data/lib/rails_console_ai/sub_agent.rb +15 -5
- data/lib/rails_console_ai/tools/registry.rb +2 -2
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +15 -0
- metadata +4 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module RailsConsoleAi
|
|
2
2
|
class Configuration
|
|
3
|
-
PROVIDERS = %i[anthropic openai local bedrock].freeze
|
|
3
|
+
PROVIDERS = %i[anthropic openai openrouter local bedrock].freeze
|
|
4
4
|
|
|
5
5
|
# Per-family model attributes, matched by substring so one entry covers every
|
|
6
6
|
# ID variant of a family: bare Anthropic IDs (claude-sonnet-5), dated
|
|
@@ -11,17 +11,23 @@ module RailsConsoleAi
|
|
|
11
11
|
# Cache pricing is derived: read = 0.1x input, write = 1.25x input.
|
|
12
12
|
# temperature: false marks families that reject the `temperature` parameter
|
|
13
13
|
# (removed on opus-4-7+, sonnet-5, and fable-5).
|
|
14
|
+
# max_tokens is the OUTPUT cap we request; context is the total window.
|
|
14
15
|
MODEL_FAMILIES = {
|
|
15
|
-
'claude-fable-5' => { input: 10.0, output: 50.0, max_tokens: 16_000, temperature: false },
|
|
16
|
-
'claude-opus-5' => { input: 5.0, output: 25.0, max_tokens: 16_000, temperature: false },
|
|
17
|
-
'claude-opus-4-8' => { input: 5.0, output: 25.0, max_tokens: 16_000, temperature: false },
|
|
18
|
-
'claude-opus-4-7' => { input: 5.0, output: 25.0, max_tokens: 16_000, temperature: false },
|
|
19
|
-
'claude-opus-4-6' => { input: 5.0, output: 25.0, max_tokens: 16_000, temperature: true },
|
|
20
|
-
'claude-sonnet-5' => { input:
|
|
21
|
-
'claude-sonnet-4-6' => { input: 3.0, output: 15.0, max_tokens: 16_000, temperature: true },
|
|
22
|
-
'claude-haiku-4-5' => { input: 1.0, output: 5.0, max_tokens: 16_000, temperature: true },
|
|
16
|
+
'claude-fable-5' => { input: 10.0, output: 50.0, max_tokens: 16_000, context: 1_000_000, temperature: false },
|
|
17
|
+
'claude-opus-5' => { input: 5.0, output: 25.0, max_tokens: 16_000, context: 1_000_000, temperature: false },
|
|
18
|
+
'claude-opus-4-8' => { input: 5.0, output: 25.0, max_tokens: 16_000, context: 1_000_000, temperature: false },
|
|
19
|
+
'claude-opus-4-7' => { input: 5.0, output: 25.0, max_tokens: 16_000, context: 1_000_000, temperature: false },
|
|
20
|
+
'claude-opus-4-6' => { input: 5.0, output: 25.0, max_tokens: 16_000, context: 1_000_000, temperature: true },
|
|
21
|
+
'claude-sonnet-5' => { input: 2.0, output: 10.0, max_tokens: 16_000, context: 1_000_000, temperature: false },
|
|
22
|
+
'claude-sonnet-4-6' => { input: 3.0, output: 15.0, max_tokens: 16_000, context: 1_000_000, temperature: true },
|
|
23
|
+
'claude-haiku-4-5' => { input: 1.0, output: 5.0, max_tokens: 16_000, context: 200_000, temperature: true },
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
26
|
+
# Assumed context window for models with no family entry — local models and
|
|
27
|
+
# anything newer than this table. Deliberately small: under-guessing warns a
|
|
28
|
+
# little early, over-guessing means no warning before the request is rejected.
|
|
29
|
+
DEFAULT_CONTEXT_WINDOW = 200_000
|
|
30
|
+
|
|
25
31
|
# Family keys sorted longest-first so a more specific family always wins
|
|
26
32
|
# if keys ever overlap (e.g. a future 'claude-sonnet-5-5' entry would match
|
|
27
33
|
# before 'claude-sonnet-5').
|
|
@@ -30,13 +36,16 @@ module RailsConsoleAi
|
|
|
30
36
|
# Returns the family attributes for a model ID, or nil for unknown models.
|
|
31
37
|
def self.model_family(model_id)
|
|
32
38
|
return nil unless model_id
|
|
33
|
-
|
|
39
|
+
normalized = model_id.gsub(/(?<=\d)\.(?=\d)/, '-')
|
|
40
|
+
key = MODEL_FAMILY_KEYS.find { |k| normalized.include?(k) }
|
|
34
41
|
key && MODEL_FAMILIES[key]
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
# Per-token pricing for a model ID, matched by family. Returns
|
|
38
45
|
# { input:, output:, cache_read:, cache_write: } or nil for unknown models.
|
|
39
|
-
|
|
46
|
+
# Cache reads bill at 0.1x the base input rate; cache writes at 1.25x for the
|
|
47
|
+
# 5-minute cache and 2x for the 1-hour cache.
|
|
48
|
+
def self.pricing_for(model_id, cache_ttl: nil)
|
|
40
49
|
family = model_family(model_id)
|
|
41
50
|
return nil unless family
|
|
42
51
|
input = family[:input] / 1_000_000
|
|
@@ -44,10 +53,32 @@ module RailsConsoleAi
|
|
|
44
53
|
input: input,
|
|
45
54
|
output: family[:output] / 1_000_000,
|
|
46
55
|
cache_read: input * 0.1,
|
|
47
|
-
cache_write: input * 1.25,
|
|
56
|
+
cache_write: input * (cache_ttl.to_s == '1h' ? 2.0 : 1.25),
|
|
48
57
|
}
|
|
49
58
|
end
|
|
50
59
|
|
|
60
|
+
# The four usage buckets each bill at their own rate. The API's `input_tokens`
|
|
61
|
+
# is the UNCACHED remainder — cached tokens are reported separately and are not
|
|
62
|
+
# part of it (total prompt size is input + cache_read + cache_write), so
|
|
63
|
+
# discounting cache_read out of input double-counts and can drive a cost
|
|
64
|
+
# negative. Every estimated cost readout goes through here. Providers that
|
|
65
|
+
# report real dollars (OpenRouter) are preferred over this estimate.
|
|
66
|
+
def self.estimate_cost(model, input:, output:, cache_read: 0, cache_write: 0, cache_ttl: nil)
|
|
67
|
+
pricing = pricing_for(model, cache_ttl: cache_ttl)
|
|
68
|
+
return nil unless pricing
|
|
69
|
+
|
|
70
|
+
((input || 0) * pricing[:input]) +
|
|
71
|
+
((output || 0) * pricing[:output]) +
|
|
72
|
+
((cache_read || 0) * pricing[:cache_read]) +
|
|
73
|
+
((cache_write || 0) * pricing[:cache_write])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Total context window for a model ID, matched by family.
|
|
77
|
+
def self.context_window_for(model_id)
|
|
78
|
+
family = model_family(model_id)
|
|
79
|
+
(family && family[:context]) || DEFAULT_CONTEXT_WINDOW
|
|
80
|
+
end
|
|
81
|
+
|
|
51
82
|
# Known environment-level failures the executor recognizes and explains to the
|
|
52
83
|
# LLM on the FIRST occurrence, so it doesn't burn rounds rediscovering them
|
|
53
84
|
# through trial and error. Each entry: { name:, pattern:, hint: }.
|
|
@@ -69,22 +100,25 @@ module RailsConsoleAi
|
|
|
69
100
|
|
|
70
101
|
attr_accessor :provider, :api_key, :model, :thinking_model, :max_tokens,
|
|
71
102
|
:auto_execute, :temperature,
|
|
72
|
-
:timeout, :debug, :max_tool_rounds,
|
|
103
|
+
:timeout, :open_timeout, :max_retries, :debug, :max_tool_rounds,
|
|
73
104
|
:error_hints,
|
|
74
105
|
:token_nudge_threshold, :token_stop_threshold,
|
|
106
|
+
:cache_ttl,
|
|
75
107
|
:storage_adapter, :memories_enabled,
|
|
76
108
|
:session_logging, :connection_class,
|
|
77
109
|
:admin_username, :admin_password,
|
|
78
110
|
:authenticate,
|
|
79
111
|
:slack_bot_token, :slack_app_token, :slack_channel_ids, :slack_allowed_usernames,
|
|
80
112
|
:local_url, :local_model, :local_api_key,
|
|
113
|
+
:openrouter_url, :openrouter_app_name, :openrouter_site_url,
|
|
81
114
|
:bedrock_region,
|
|
82
115
|
:code_search_paths,
|
|
83
116
|
:channels,
|
|
84
117
|
:bypass_guards_for_methods,
|
|
85
118
|
:user_extra_info,
|
|
86
119
|
:sub_agent_max_rounds,
|
|
87
|
-
:sub_agent_model
|
|
120
|
+
:sub_agent_model,
|
|
121
|
+
:line_editor
|
|
88
122
|
|
|
89
123
|
def initialize
|
|
90
124
|
@provider = :anthropic
|
|
@@ -94,12 +128,29 @@ module RailsConsoleAi
|
|
|
94
128
|
@max_tokens = nil
|
|
95
129
|
@auto_execute = false
|
|
96
130
|
@temperature = 0.2
|
|
97
|
-
|
|
131
|
+
# Read timeout for one provider request. Adaptive thinking plus a large output
|
|
132
|
+
# cap means a single agentic call can legitimately run for minutes; the old 30s
|
|
133
|
+
# cut those off mid-generation, which loses the turn and the tokens already
|
|
134
|
+
# spent on it, and sends the user back to re-ask from a cold cache.
|
|
135
|
+
@timeout = 300
|
|
136
|
+
@open_timeout = 10 # establishing the connection, not generating the response
|
|
137
|
+
@max_retries = 2 # transient failures only — see Providers::Base#with_retries
|
|
98
138
|
@debug = false
|
|
99
139
|
@max_tool_rounds = 200
|
|
100
140
|
@error_hints = DEFAULT_ERROR_HINTS.dup
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
# Measured against total prompt tokens sent in one tool loop — uncached input
|
|
142
|
+
# plus cache reads plus cache writes. Not the API's `input_tokens` alone:
|
|
143
|
+
# that is only the uncached remainder, so with caching on it stays near zero
|
|
144
|
+
# regardless of conversation size and neither guard would ever fire.
|
|
145
|
+
@token_nudge_threshold = 500_000 # prompt tokens in one tool loop → nudge model to wrap up (nil disables)
|
|
146
|
+
@token_stop_threshold = 1_000_000 # prompt tokens in one tool loop → force a final answer (nil disables)
|
|
147
|
+
# Prompt cache lifetime: nil/'5m' for the 5-minute default, '1h' for the
|
|
148
|
+
# 1-hour cache. Within a tool loop, rounds are seconds apart and 5m is
|
|
149
|
+
# strictly cheaper (a read refreshes the entry, and the write costs 1.25x
|
|
150
|
+
# vs 2x). '1h' pays off when a human sits between turns for more than five
|
|
151
|
+
# minutes — long interactive console sessions and Slack threads — because
|
|
152
|
+
# a miss there resends the whole conversation at full price.
|
|
153
|
+
@cache_ttl = nil
|
|
103
154
|
@storage_adapter = nil
|
|
104
155
|
@memories_enabled = true
|
|
105
156
|
@session_logging = true
|
|
@@ -115,6 +166,9 @@ module RailsConsoleAi
|
|
|
115
166
|
@local_url = 'http://localhost:11434'
|
|
116
167
|
@local_model = 'qwen2.5:7b'
|
|
117
168
|
@local_api_key = nil
|
|
169
|
+
@openrouter_url = nil
|
|
170
|
+
@openrouter_app_name = nil
|
|
171
|
+
@openrouter_site_url = nil
|
|
118
172
|
@bedrock_region = nil
|
|
119
173
|
@code_search_paths = %w[app]
|
|
120
174
|
@channels = {}
|
|
@@ -122,6 +176,9 @@ module RailsConsoleAi
|
|
|
122
176
|
@user_extra_info = {}
|
|
123
177
|
@sub_agent_max_rounds = 15
|
|
124
178
|
@sub_agent_model = nil
|
|
179
|
+
# Interactive line editor: :auto prefers Reline (live "/" completion menu)
|
|
180
|
+
# and falls back to Readline. Force one with :reline or :readline.
|
|
181
|
+
@line_editor = :auto
|
|
125
182
|
end
|
|
126
183
|
|
|
127
184
|
def resolve_user_extra_info(username)
|
|
@@ -208,6 +265,8 @@ module RailsConsoleAi
|
|
|
208
265
|
ENV['ANTHROPIC_API_KEY']
|
|
209
266
|
when :openai
|
|
210
267
|
ENV['OPENAI_API_KEY']
|
|
268
|
+
when :openrouter
|
|
269
|
+
ENV['OPENROUTER_API_KEY']
|
|
211
270
|
when :local
|
|
212
271
|
@local_api_key || 'no-key'
|
|
213
272
|
when :bedrock
|
|
@@ -223,6 +282,8 @@ module RailsConsoleAi
|
|
|
223
282
|
'claude-sonnet-5'
|
|
224
283
|
when :openai
|
|
225
284
|
'gpt-5.3-codex'
|
|
285
|
+
when :openrouter
|
|
286
|
+
'anthropic/claude-sonnet-5'
|
|
226
287
|
when :local
|
|
227
288
|
@local_model
|
|
228
289
|
when :bedrock
|
|
@@ -234,7 +295,9 @@ module RailsConsoleAi
|
|
|
234
295
|
return @max_tokens if @max_tokens
|
|
235
296
|
|
|
236
297
|
family = self.class.model_family(resolved_model)
|
|
237
|
-
|
|
298
|
+
return family[:max_tokens] if family
|
|
299
|
+
|
|
300
|
+
@provider == :openrouter ? 16_000 : 4096
|
|
238
301
|
end
|
|
239
302
|
|
|
240
303
|
# Returns nil for model families that reject the `temperature` parameter
|
|
@@ -245,6 +308,13 @@ module RailsConsoleAi
|
|
|
245
308
|
@temperature
|
|
246
309
|
end
|
|
247
310
|
|
|
311
|
+
# Returns '1h' when the 1-hour prompt cache is requested, else nil (the
|
|
312
|
+
# 5-minute default). Providers that offer only one cache duration ignore it.
|
|
313
|
+
def resolved_cache_ttl
|
|
314
|
+
return nil unless @cache_ttl
|
|
315
|
+
@cache_ttl.to_s == '1h' ? '1h' : nil
|
|
316
|
+
end
|
|
317
|
+
|
|
248
318
|
def resolved_thinking_model
|
|
249
319
|
return @thinking_model if @thinking_model && !@thinking_model.empty?
|
|
250
320
|
|
|
@@ -253,6 +323,8 @@ module RailsConsoleAi
|
|
|
253
323
|
'claude-opus-5'
|
|
254
324
|
when :openai
|
|
255
325
|
'gpt-5.3-codex'
|
|
326
|
+
when :openrouter
|
|
327
|
+
'anthropic/claude-opus-5'
|
|
256
328
|
when :local
|
|
257
329
|
@local_model
|
|
258
330
|
when :bedrock
|
|
@@ -264,6 +336,12 @@ module RailsConsoleAi
|
|
|
264
336
|
@provider == :local ? [@timeout, 300].max : @timeout
|
|
265
337
|
end
|
|
266
338
|
|
|
339
|
+
ENV_KEYS = {
|
|
340
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
341
|
+
openai: 'OPENAI_API_KEY',
|
|
342
|
+
openrouter: 'OPENROUTER_API_KEY'
|
|
343
|
+
}.freeze
|
|
344
|
+
|
|
267
345
|
def validate!
|
|
268
346
|
unless PROVIDERS.include?(@provider)
|
|
269
347
|
raise ConfigurationError, "Unknown provider: #{@provider}. Valid: #{PROVIDERS.join(', ')}"
|
|
@@ -280,7 +358,7 @@ module RailsConsoleAi
|
|
|
280
358
|
end
|
|
281
359
|
else
|
|
282
360
|
unless resolved_api_key
|
|
283
|
-
env_var = @provider
|
|
361
|
+
env_var = ENV_KEYS[@provider] || 'API_KEY'
|
|
284
362
|
raise ConfigurationError, "No API key. Set config.api_key or #{env_var} env var."
|
|
285
363
|
end
|
|
286
364
|
end
|