ruby_llm 2.0.0.rc2 → 2.0.0.rc3
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/README.md +2 -2
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +34 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +332 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +170 -0
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +14 -11
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +6 -6
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +31 -5
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +16 -14
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +36 -7
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +15 -5
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +14 -0
- data/lib/ruby_llm/accounting/usage.rb +9 -0
- data/lib/ruby_llm/agent.rb +10 -9
- data/lib/ruby_llm/aliases.json +26 -4
- data/lib/ruby_llm/attachment.rb +5 -0
- data/lib/ruby_llm/chat.rb +4 -0
- data/lib/ruby_llm/message.rb +14 -5
- data/lib/ruby_llm/models.json +3820 -1346
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +2 -1
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +8 -1
- data/lib/ruby_llm/protocols/cohere/rerank.rb +8 -1
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +5 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +3 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +0 -1
- data/lib/ruby_llm/providers/mistral/ocr.rb +5 -1
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/ruby_llm.rake +1 -1
- metadata +7 -2
|
@@ -12,7 +12,9 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
12
12
|
|
|
13
13
|
included do
|
|
14
14
|
class_attribute :ruby_llm_upgrade_kind, instance_writer: false
|
|
15
|
+
self.enumerate_columns_in_select_statements = true
|
|
15
16
|
default_scope { RubyLLMUpgrade.visible(self) }
|
|
17
|
+
after_initialize { @ruby_llm_upgrade_epoch = RubyLLMUpgrade.epoch }
|
|
16
18
|
around_save :guard_ruby_llm_upgrade, prepend: true
|
|
17
19
|
around_destroy :guard_ruby_llm_upgrade, prepend: true
|
|
18
20
|
before_save { RubyLLMUpgrade.prepare_model(self) if ruby_llm_upgrade_kind == :chat }
|
|
@@ -27,6 +29,9 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
27
29
|
|
|
28
30
|
class << self
|
|
29
31
|
def install(chat:, message:, tool_call:)
|
|
32
|
+
if version == 2 && message.table_exists? && message.column_names.include?('ruby_llm_content')
|
|
33
|
+
message.alias_attribute :content, :ruby_llm_content
|
|
34
|
+
end
|
|
30
35
|
{ chat => :chat, message => :message, tool_call => :tool_call }.each do |model, kind|
|
|
31
36
|
next if model < Records
|
|
32
37
|
|
|
@@ -37,11 +42,13 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
37
42
|
return if chat.instance_variable_defined?(:@ruby_llm_upgrade_operations)
|
|
38
43
|
|
|
39
44
|
operations = guarded_operations(chat, OPERATIONS, claim: true)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
%i[persist_usage_entry persist_new_message persist_message_completion].each do |name|
|
|
46
|
+
next unless chat.private_method_defined?(name)
|
|
47
|
+
|
|
48
|
+
operations.define_method(name) do |*args, **options, &block|
|
|
49
|
+
RubyLLMUpgrade.guard(self) { super(*args, **options, &block) }
|
|
43
50
|
end
|
|
44
|
-
operations.send(:private,
|
|
51
|
+
operations.send(:private, name)
|
|
45
52
|
end
|
|
46
53
|
chat.prepend operations
|
|
47
54
|
chat.instance_variable_set(:@ruby_llm_upgrade_operations, operations)
|
|
@@ -73,7 +80,9 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
73
80
|
def prepare_model(chat)
|
|
74
81
|
return unless available?
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
state = current_state
|
|
84
|
+
settings = state.settings
|
|
85
|
+
return if version == 1 && settings['online'] && state.status == 'preparing'
|
|
77
86
|
if version == 1
|
|
78
87
|
return unless ActiveRecord::Base.connection.column_exists?(settings.fetch('chat_table'), :ruby_llm_model_id)
|
|
79
88
|
return unless ActiveRecord::Base.connection.table_exists?('ruby_llm_models')
|
|
@@ -87,16 +96,30 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
87
96
|
states = records(TABLE)
|
|
88
97
|
states.uncached do
|
|
89
98
|
states.transaction do
|
|
90
|
-
|
|
99
|
+
if ActiveRecord::Base.connection.adapter_name == 'SQLite' && claim
|
|
100
|
+
states.update_all('epoch = epoch')
|
|
101
|
+
end
|
|
102
|
+
state = states.lock(lock_clause).first!
|
|
91
103
|
check_version(state)
|
|
92
104
|
protect(record, state, claim: claim)
|
|
105
|
+
if record.instance_variable_get(:@ruby_llm_upgrade_epoch) != state[:epoch].to_i
|
|
106
|
+
raise ActiveRecord::ReadOnlyRecord, 'Reload the conversation after switching RubyLLM versions'
|
|
107
|
+
end
|
|
93
108
|
yield
|
|
94
109
|
end
|
|
95
110
|
end
|
|
96
111
|
end
|
|
97
112
|
|
|
113
|
+
def epoch
|
|
114
|
+
available? ? current_state[:epoch].to_i : 0
|
|
115
|
+
end
|
|
116
|
+
|
|
98
117
|
private
|
|
99
118
|
|
|
119
|
+
def lock_clause
|
|
120
|
+
ActiveRecord::Base.connection.adapter_name == 'Mysql2' ? 'LOCK IN SHARE MODE' : 'FOR SHARE'
|
|
121
|
+
end
|
|
122
|
+
|
|
100
123
|
def guarded_operations(model, names, claim:)
|
|
101
124
|
Module.new do
|
|
102
125
|
names.each do |name|
|
|
@@ -141,10 +164,15 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
141
164
|
|
|
142
165
|
def records(table)
|
|
143
166
|
@records ||= {}
|
|
144
|
-
@records[table] ||= Class.new(ActiveRecord::Base) do
|
|
167
|
+
model = @records[table] ||= Class.new(ActiveRecord::Base) do
|
|
145
168
|
self.table_name = table
|
|
146
169
|
self.inheritance_column = :_type_disabled
|
|
147
170
|
end
|
|
171
|
+
if !model.column_names.include?(VERSION_COLUMN) &&
|
|
172
|
+
ActiveRecord::Base.connection.column_exists?(table, VERSION_COLUMN)
|
|
173
|
+
model.reset_column_information
|
|
174
|
+
end
|
|
175
|
+
model
|
|
148
176
|
end
|
|
149
177
|
|
|
150
178
|
def current_state
|
|
@@ -156,6 +184,7 @@ module RubyLLMUpgrade # :nodoc: all
|
|
|
156
184
|
end
|
|
157
185
|
|
|
158
186
|
def check_version(state)
|
|
187
|
+
return if version == 1 && state.active_version == 1 && state.settings['online'] && state.status == 'preparing'
|
|
159
188
|
return if state.active_version == version && %w[active finalized].include?(state.status)
|
|
160
189
|
|
|
161
190
|
raise ActiveRecord::ReadOnlyRecord,
|
|
@@ -25,7 +25,7 @@ module RubyLLM
|
|
|
25
25
|
class_option :phase, type: :string, enum: %w[prepare backfill finish cleanup],
|
|
26
26
|
desc: 'Generate one upgrade phase; cleanup runs in a later deployment'
|
|
27
27
|
class_option :mode, type: :string, enum: %w[rename copy], default: 'rename',
|
|
28
|
-
desc: 'Copy
|
|
28
|
+
desc: 'Copy prepares and backfills online, with protected 1.16 rollback until cleanup'
|
|
29
29
|
|
|
30
30
|
argument :model_mappings,
|
|
31
31
|
type: :array,
|
|
@@ -62,15 +62,25 @@ module RubyLLM
|
|
|
62
62
|
|
|
63
63
|
Copy mode retains the legacy tables and a separate chat model reference.
|
|
64
64
|
Install the generated RubyLLMUpgrade concern and initializer in BOTH the
|
|
65
|
-
1.16
|
|
65
|
+
running 1.16 build and the 2.0 build BEFORE running prepare. Restart
|
|
66
|
+
all affected 1.16 processes so they load the guards and explicit column
|
|
67
|
+
selects. The guards protect whole conversations
|
|
66
68
|
changed by 2.0. Direct SQL, bulk updates/deletes and attachment purges
|
|
67
69
|
bypass these guards; review those application paths before upgrading.
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
Rehearse prepare, backfill and finish on a production database copy.
|
|
72
|
+
Run prepare and backfill from the 2.0 build while 1.16 serves traffic.
|
|
73
|
+
Stop affected traffic, workers, scheduled jobs and retries BEFORE finish.
|
|
74
|
+
Finish catches up intervening writes, validates and activates 2.0.
|
|
75
|
+
Then load models and restart the 2.0 application.
|
|
72
76
|
Keep the old model/tool-call classes in the 1.16 build.
|
|
73
77
|
|
|
78
|
+
PostgreSQL, MySQL and SQLite support this workflow. Schema changes can
|
|
79
|
+
block writes; SQLite also serializes backfill and app writes.
|
|
80
|
+
Use a direct connection or session-mode pool for copy migrations.
|
|
81
|
+
db:migrate runs ALL pending phases. Stop at the backfill timestamp to
|
|
82
|
+
defer finish, or pause AI before running all three together.
|
|
83
|
+
|
|
74
84
|
With all affected processes stopped, use the 2.0 build to run:
|
|
75
85
|
bin/rails ruby_llm:upgrade:rollback # activates the protected 1.16 view
|
|
76
86
|
bin/rails ruby_llm:upgrade:resume # reconciles 1.16 writes, activates 2.0
|
|
@@ -6,12 +6,22 @@ module RubyLLM
|
|
|
6
6
|
TABLE = :ruby_llm_v2_upgrades
|
|
7
7
|
VERSION_COLUMN = :ruby_llm_version
|
|
8
8
|
|
|
9
|
+
def self.for(connection: ::ActiveRecord::Base.connection)
|
|
10
|
+
require_relative 'online_copy_migration'
|
|
11
|
+
OnlineCopyMigration.new(connection:)
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
def initialize(connection: ::ActiveRecord::Base.connection)
|
|
10
15
|
@connection = connection
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
def prepare(settings)
|
|
14
19
|
create_state_table unless @connection.table_exists?(TABLE)
|
|
20
|
+
prepare_state(settings)
|
|
21
|
+
yield if block_given?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def prepare_state(settings)
|
|
15
25
|
existing = states.first
|
|
16
26
|
if existing
|
|
17
27
|
raise 'This database has a different RubyLLM copy upgrade' unless existing.settings == settings.stringify_keys
|
|
@@ -24,6 +34,9 @@ module RubyLLM
|
|
|
24
34
|
states.create!(settings: settings.stringify_keys)
|
|
25
35
|
end
|
|
26
36
|
end
|
|
37
|
+
private :prepare_state
|
|
38
|
+
|
|
39
|
+
def online? = false
|
|
27
40
|
|
|
28
41
|
def copy_table(source, target)
|
|
29
42
|
return if @connection.table_exists?(target)
|
|
@@ -146,6 +159,7 @@ module RubyLLM
|
|
|
146
159
|
table.integer :active_version, null: false, default: 1
|
|
147
160
|
table.string :status, null: false, default: 'preparing'
|
|
148
161
|
table.boolean :needs_reconcile, null: false, default: false
|
|
162
|
+
table.bigint :epoch, null: false, default: 0
|
|
149
163
|
table.json :settings, null: false
|
|
150
164
|
end
|
|
151
165
|
end
|
|
@@ -163,6 +163,7 @@ module RubyLLM
|
|
|
163
163
|
# A request that produced several results, like a multi-image
|
|
164
164
|
# generation, is billed once: the first result carries the call.
|
|
165
165
|
billed = result.is_a?(Array) ? result.first : result
|
|
166
|
+
billed.model_info = message_model(billed) if billed.is_a?(Message)
|
|
166
167
|
pending = @pending.dup
|
|
167
168
|
if pending.empty?
|
|
168
169
|
attach_to_result(billed)
|
|
@@ -187,6 +188,14 @@ module RubyLLM
|
|
|
187
188
|
|
|
188
189
|
private
|
|
189
190
|
|
|
191
|
+
def message_model(message)
|
|
192
|
+
return @model_info if message.model.nil? || message.model == @model_info&.id
|
|
193
|
+
|
|
194
|
+
RubyLLM.models.find(message.model, provider: @provider.slug, config: @config)
|
|
195
|
+
rescue ModelNotFoundError
|
|
196
|
+
@model_info
|
|
197
|
+
end
|
|
198
|
+
|
|
190
199
|
# A request that never reached the provider, or that the provider
|
|
191
200
|
# refused before running it, cannot have been billed; possibly-billed
|
|
192
201
|
# failures keep their usage unknown.
|
data/lib/ruby_llm/agent.rb
CHANGED
|
@@ -53,8 +53,7 @@ module RubyLLM
|
|
|
53
53
|
:@input_names => [],
|
|
54
54
|
:@fallbacks => [],
|
|
55
55
|
:@fallback_options => {},
|
|
56
|
-
:@rescue_handlers => []
|
|
57
|
-
:@instructions => []
|
|
56
|
+
:@rescue_handlers => []
|
|
58
57
|
}.freeze
|
|
59
58
|
# Simple value options: a class-level getter/setter macro whose value the
|
|
60
59
|
# agent forwards to the matching Chat#with_* when it builds its chat.
|
|
@@ -177,15 +176,16 @@ module RubyLLM
|
|
|
177
176
|
# "Today is #{Date.current}"
|
|
178
177
|
# end
|
|
179
178
|
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
# persist when the record is created unless
|
|
179
|
+
# The class's own declarations take precedence over its conventional
|
|
180
|
+
# template. Inherited declarations are used only when neither exists.
|
|
181
|
+
# In Rails mode, declarations persist when the record is created unless
|
|
182
|
+
# <tt>persist: false</tt>;
|
|
183
183
|
# ::find always reapplies them without rewriting history. Called with no
|
|
184
184
|
# arguments, returns the declarations.
|
|
185
185
|
def instructions(text = nil, append: false, persist: true, cache_until_here: false, **prompt_locals, &block)
|
|
186
186
|
return instruction_declarations if text.nil? && prompt_locals.empty? && !block_given?
|
|
187
187
|
|
|
188
|
-
instruction_declarations << {
|
|
188
|
+
(@instruction_declarations ||= []) << {
|
|
189
189
|
value: block || text || { prompt: 'instructions', locals: prompt_locals },
|
|
190
190
|
append: append,
|
|
191
191
|
persist: persist,
|
|
@@ -611,6 +611,8 @@ module RubyLLM
|
|
|
611
611
|
end
|
|
612
612
|
|
|
613
613
|
def copy_inherited_config_to(subclass)
|
|
614
|
+
subclass.instance_variable_set(:@inherited_instruction_declarations, instruction_declarations.dup)
|
|
615
|
+
|
|
614
616
|
DUPED_INHERITED_CONFIG.each do |ivar, default|
|
|
615
617
|
value = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : default
|
|
616
618
|
subclass.instance_variable_set(ivar, value.respond_to?(:dup) ? value.dup : value)
|
|
@@ -767,8 +769,7 @@ module RubyLLM
|
|
|
767
769
|
end
|
|
768
770
|
|
|
769
771
|
def instructions_config
|
|
770
|
-
return instruction_declarations if instruction_declarations
|
|
771
|
-
return [] unless default_instructions_prompt_exists?
|
|
772
|
+
return instruction_declarations if @instruction_declarations&.any? || !default_instructions_prompt_exists?
|
|
772
773
|
|
|
773
774
|
[{
|
|
774
775
|
value: { prompt: 'instructions', locals: {} },
|
|
@@ -779,7 +780,7 @@ module RubyLLM
|
|
|
779
780
|
end
|
|
780
781
|
|
|
781
782
|
def instruction_declarations
|
|
782
|
-
@instruction_declarations
|
|
783
|
+
@instruction_declarations || @inherited_instruction_declarations || []
|
|
783
784
|
end
|
|
784
785
|
|
|
785
786
|
def rails_chat_record?(chat)
|
data/lib/ruby_llm/aliases.json
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"claude-3-haiku": {
|
|
3
|
-
"bedrock": "anthropic.claude-3-haiku-20240307-v1:0",
|
|
4
|
-
"openrouter": "anthropic/claude-3-haiku"
|
|
5
|
-
},
|
|
6
2
|
"claude-fable-5": {
|
|
7
3
|
"anthropic": "claude-fable-5",
|
|
8
4
|
"openrouter": "anthropic/claude-fable-5",
|
|
@@ -471,6 +467,14 @@
|
|
|
471
467
|
"openrouter": "openai/gpt-image-2",
|
|
472
468
|
"azure": "gpt-image-2"
|
|
473
469
|
},
|
|
470
|
+
"gpt-image-2.5-flare": {
|
|
471
|
+
"openai": "gpt-image-2.5-flare",
|
|
472
|
+
"openrouter": "openai/gpt-image-2.5-flare"
|
|
473
|
+
},
|
|
474
|
+
"gpt-image-2.5-sunburst": {
|
|
475
|
+
"openai": "gpt-image-2.5-sunburst",
|
|
476
|
+
"openrouter": "openai/gpt-image-2.5-sunburst"
|
|
477
|
+
},
|
|
474
478
|
"gpt-oss-120b": {
|
|
475
479
|
"vertexai": "openai/gpt-oss-120b-maas",
|
|
476
480
|
"openrouter": "openai/gpt-oss-120b"
|
|
@@ -532,6 +536,24 @@
|
|
|
532
536
|
"grok-4-latest": {
|
|
533
537
|
"xai": "grok-4.3"
|
|
534
538
|
},
|
|
539
|
+
"grok-4.1-fast-non-reasoning": {
|
|
540
|
+
"vertexai": "xai/grok-4.1-fast-non-reasoning"
|
|
541
|
+
},
|
|
542
|
+
"grok-4.1-fast-reasoning": {
|
|
543
|
+
"vertexai": "xai/grok-4.1-fast-reasoning"
|
|
544
|
+
},
|
|
545
|
+
"grok-4.20-non-reasoning": {
|
|
546
|
+
"vertexai": "xai/grok-4.20-non-reasoning"
|
|
547
|
+
},
|
|
548
|
+
"grok-4.20-reasoning": {
|
|
549
|
+
"vertexai": "xai/grok-4.20-reasoning"
|
|
550
|
+
},
|
|
551
|
+
"grok-4.3": {
|
|
552
|
+
"vertexai": "xai/grok-4.3"
|
|
553
|
+
},
|
|
554
|
+
"grok-4.6": {
|
|
555
|
+
"vertexai": "xai/grok-4.6"
|
|
556
|
+
},
|
|
535
557
|
"grok-latest": {
|
|
536
558
|
"xai": "grok-4.3"
|
|
537
559
|
},
|
data/lib/ruby_llm/attachment.rb
CHANGED
|
@@ -67,6 +67,11 @@ module RubyLLM
|
|
|
67
67
|
#
|
|
68
68
|
# +config:+ is the Configuration a URL source is downloaded with, and
|
|
69
69
|
# defaults to the global one.
|
|
70
|
+
#
|
|
71
|
+
# Paths and URLs must be trusted and authorized by the application.
|
|
72
|
+
# They can be read or fetched during construction to detect the MIME type.
|
|
73
|
+
# Validate upload parameters before passing them here: an unchecked String
|
|
74
|
+
# can access local files or internal network endpoints.
|
|
70
75
|
def initialize(source, filename: nil, config: nil)
|
|
71
76
|
@config = config
|
|
72
77
|
@source = source
|
data/lib/ruby_llm/chat.rb
CHANGED
|
@@ -145,6 +145,10 @@ module RubyLLM
|
|
|
145
145
|
# treating it as a final answer. Attach files with +with:+.
|
|
146
146
|
# A given block receives streamed Chunk objects as they arrive.
|
|
147
147
|
#
|
|
148
|
+
# String attachments read local paths or fetch URLs. Only pass trusted,
|
|
149
|
+
# authorized sources; validate user uploads before calling this method.
|
|
150
|
+
# See Attachment.new.
|
|
151
|
+
#
|
|
148
152
|
# chat.ask "What's the best way to learn Ruby?"
|
|
149
153
|
# chat.ask "What's in this image?", with: "ruby_conf.jpg"
|
|
150
154
|
# chat.ask "Analyze these files", with: ["diagram.png", "report.pdf"]
|
data/lib/ruby_llm/message.rb
CHANGED
|
@@ -35,6 +35,8 @@ module RubyLLM
|
|
|
35
35
|
# The ID of the model that produced the message, +nil+ on user messages.
|
|
36
36
|
attr_reader :model
|
|
37
37
|
|
|
38
|
+
attr_writer :model_info # :nodoc:
|
|
39
|
+
|
|
38
40
|
# The tool calls the assistant requested, as a Hash of ToolCall objects
|
|
39
41
|
# keyed by call ID, or +nil+.
|
|
40
42
|
attr_reader :tool_calls
|
|
@@ -238,12 +240,19 @@ module RubyLLM
|
|
|
238
240
|
}.merge(tokens.to_h).compact
|
|
239
241
|
end
|
|
240
242
|
|
|
241
|
-
# Returns the Model
|
|
242
|
-
#
|
|
243
|
+
# Returns the response's Model from its provider's registry, falling
|
|
244
|
+
# back to the requested model when the response ID is unknown.
|
|
245
|
+
# Restored messages use the last successful attempt's provider and model.
|
|
246
|
+
# Messages without request context look up #model, or return +nil+ if unknown.
|
|
243
247
|
def model_info
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
return @model_info if @model_info
|
|
249
|
+
|
|
250
|
+
entry = ruby_llm_usage_entries.reverse.find(&:succeeded?)
|
|
251
|
+
@model_info = if entry&.model
|
|
252
|
+
RubyLLM.models.find(entry.model, provider: entry.provider)
|
|
253
|
+
elsif model
|
|
254
|
+
RubyLLM.models.find(model)
|
|
255
|
+
end
|
|
247
256
|
rescue ModelNotFoundError
|
|
248
257
|
nil
|
|
249
258
|
end
|