dify_llm 1.8.2 → 1.9.1

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/lib/generators/ruby_llm/generator_helpers.rb +31 -10
  4. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +5 -0
  6. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +7 -1
  7. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +1 -1
  8. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  9. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  10. data/lib/ruby_llm/active_record/acts_as.rb +22 -24
  11. data/lib/ruby_llm/active_record/chat_methods.rb +41 -13
  12. data/lib/ruby_llm/active_record/message_methods.rb +11 -2
  13. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  14. data/lib/ruby_llm/aliases.json +61 -32
  15. data/lib/ruby_llm/attachment.rb +44 -13
  16. data/lib/ruby_llm/chat.rb +13 -2
  17. data/lib/ruby_llm/configuration.rb +6 -1
  18. data/lib/ruby_llm/connection.rb +3 -3
  19. data/lib/ruby_llm/content.rb +23 -0
  20. data/lib/ruby_llm/message.rb +11 -6
  21. data/lib/ruby_llm/model/info.rb +4 -0
  22. data/lib/ruby_llm/models.json +9649 -8211
  23. data/lib/ruby_llm/models.rb +14 -22
  24. data/lib/ruby_llm/provider.rb +23 -1
  25. data/lib/ruby_llm/providers/anthropic/chat.rb +22 -3
  26. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  27. data/lib/ruby_llm/providers/anthropic/media.rb +3 -2
  28. data/lib/ruby_llm/providers/anthropic/models.rb +15 -0
  29. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  30. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -18
  31. data/lib/ruby_llm/providers/bedrock/media.rb +2 -1
  32. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +15 -0
  33. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +2 -0
  34. data/lib/ruby_llm/providers/dify/chat.rb +16 -5
  35. data/lib/ruby_llm/providers/gemini/chat.rb +352 -69
  36. data/lib/ruby_llm/providers/gemini/media.rb +59 -1
  37. data/lib/ruby_llm/providers/gemini/tools.rb +146 -25
  38. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  39. data/lib/ruby_llm/providers/gemini.rb +2 -1
  40. data/lib/ruby_llm/providers/gpustack/media.rb +1 -0
  41. data/lib/ruby_llm/providers/ollama/media.rb +1 -0
  42. data/lib/ruby_llm/providers/openai/chat.rb +7 -2
  43. data/lib/ruby_llm/providers/openai/media.rb +2 -1
  44. data/lib/ruby_llm/providers/openai/streaming.rb +7 -2
  45. data/lib/ruby_llm/providers/openai/tools.rb +26 -6
  46. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  47. data/lib/ruby_llm/providers/openai.rb +1 -0
  48. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  49. data/lib/ruby_llm/providers/vertexai.rb +11 -11
  50. data/lib/ruby_llm/railtie.rb +24 -22
  51. data/lib/ruby_llm/stream_accumulator.rb +10 -4
  52. data/lib/ruby_llm/tool.rb +126 -0
  53. data/lib/ruby_llm/transcription.rb +35 -0
  54. data/lib/ruby_llm/utils.rb +46 -0
  55. data/lib/ruby_llm/version.rb +1 -1
  56. data/lib/ruby_llm.rb +7 -0
  57. metadata +24 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7643dffaaba8bee51ea9d5929f3665be99f4e56fb84f5474fca20f8b8fd71fd5
4
- data.tar.gz: 1a5db0333d2df1e09d1b1841e209677d57401137908e58426626844a5e9a11d9
3
+ metadata.gz: d85ea7861e7fdc76f629a92f9aa4768d61d8ad6ad069d540c35943356ab622cc
4
+ data.tar.gz: f684857d9908d597c2f913e0f57fad58e71bab4be9698e29dee95ec49c571f74
5
5
  SHA512:
6
- metadata.gz: 305089c41bb76bf36aca07dfd95776b182debe2e059ce75dd34a77627516b2b95603de73780363cf5a9a2096df0109e433fdb420416067761daed700fad7c0b3
7
- data.tar.gz: e7478c5771d9d00046d77a2965b801a705e63734360c1871e8865ac9ed634882e529bcf5b241cfeed103e38c87dd009730e6cab92075074b915525525525802e
6
+ metadata.gz: 71a889822eeb0a384e5dd9639eb92db1dc1d9da7d6da02fcc97000596affdb023ae32d9bebc341bf6810050b2146198fdb90af544d99a8d70a2b84be05dfe87e
7
+ data.tar.gz: 912691efa352436b5cc6ba1d1b5fbf0890e48591399b5c6e98f44a7d6c2721f2164ae1acebe906d48f987ce0f4cb90ff261cef66c35e7f8982133c31c9bfebfb
data/README.md CHANGED
@@ -18,7 +18,7 @@ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="
18
18
  </div>
19
19
 
20
20
  > [!NOTE]
21
- > Using RubyLLM in production? [Share your story](https://tally.so/r/3Na02p)! Takes 5 minutes.
21
+ > Using RubyLLM? [Share your story](https://tally.so/r/3Na02p)! Takes 5 minutes.
22
22
 
23
23
  ---
24
24
 
@@ -69,6 +69,11 @@ RubyLLM.paint "a sunset over mountains in watercolor style"
69
69
  RubyLLM.embed "Ruby is elegant and expressive"
70
70
  ```
71
71
 
72
+ ```ruby
73
+ # Transcribe audio to text
74
+ RubyLLM.transcribe "meeting.wav"
75
+ ```
76
+
72
77
  ```ruby
73
78
  # Moderate content for safety
74
79
  RubyLLM.moderate "Check if this text is safe"
@@ -107,10 +112,10 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
107
112
 
108
113
  * **Chat:** Conversational AI with `RubyLLM.chat`
109
114
  * **Vision:** Analyze images and videos
110
- * **Audio:** Transcribe and understand speech
115
+ * **Audio:** Transcribe and understand speech with `RubyLLM.transcribe`
111
116
  * **Documents:** Extract from PDFs, CSVs, JSON, any file type
112
117
  * **Image generation:** Create images with `RubyLLM.paint`
113
- * **Embeddings:** Vector search with `RubyLLM.embed`
118
+ * **Embeddings:** Generate embeddings with `RubyLLM.embed`
114
119
  * **Moderation:** Content safety with `RubyLLM.moderate`
115
120
  * **Tools:** Let AI call your Ruby methods
116
121
  * **Structured output:** JSON schemas that just work
@@ -52,8 +52,10 @@ module RubyLLM
52
52
  def acts_as_chat_declaration
53
53
  params = []
54
54
 
55
- add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
56
- add_association_params(params, :model, model_table_name, model_model_name)
55
+ add_association_params(params, :messages, message_table_name, message_model_name,
56
+ owner_table: chat_table_name, plural: true)
57
+ add_association_params(params, :model, model_table_name, model_model_name,
58
+ owner_table: chat_table_name)
57
59
 
58
60
  "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
59
61
  end
@@ -61,9 +63,12 @@ module RubyLLM
61
63
  def acts_as_message_declaration
62
64
  params = []
63
65
 
64
- add_association_params(params, :chat, chat_table_name, chat_model_name)
65
- add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
66
- add_association_params(params, :model, model_table_name, model_model_name)
66
+ add_association_params(params, :chat, chat_table_name, chat_model_name,
67
+ owner_table: message_table_name)
68
+ add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name,
69
+ owner_table: message_table_name, plural: true)
70
+ add_association_params(params, :model, model_table_name, model_model_name,
71
+ owner_table: message_table_name)
67
72
 
68
73
  "acts_as_message#{" #{params.join(', ')}" if params.any?}"
69
74
  end
@@ -71,7 +76,8 @@ module RubyLLM
71
76
  def acts_as_model_declaration
72
77
  params = []
73
78
 
74
- add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
79
+ add_association_params(params, :chats, chat_table_name, chat_model_name,
80
+ owner_table: model_table_name, plural: true)
75
81
 
76
82
  "acts_as_model#{" #{params.join(', ')}" if params.any?}"
77
83
  end
@@ -79,7 +85,8 @@ module RubyLLM
79
85
  def acts_as_tool_call_declaration
80
86
  params = []
81
87
 
82
- add_association_params(params, :message, message_table_name, message_model_name)
88
+ add_association_params(params, :message, message_table_name, message_model_name,
89
+ owner_table: tool_call_table_name)
83
90
 
84
91
  "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
85
92
  end
@@ -120,6 +127,12 @@ module RubyLLM
120
127
  false
121
128
  end
122
129
 
130
+ def mysql?
131
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
132
+ rescue StandardError
133
+ false
134
+ end
135
+
123
136
  def table_exists?(table_name)
124
137
  ::ActiveRecord::Base.connection.table_exists?(table_name)
125
138
  rescue StandardError
@@ -128,13 +141,21 @@ module RubyLLM
128
141
 
129
142
  private
130
143
 
131
- def add_association_params(params, default_assoc, table_name, model_name, plural: false)
144
+ def add_association_params(params, default_assoc, table_name, model_name, owner_table:, plural: false) # rubocop:disable Metrics/ParameterLists
132
145
  assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
133
146
 
134
- return if assoc == default_assoc
147
+ default_foreign_key = "#{default_assoc}_id"
148
+ # has_many/has_one: foreign key is on the associated table pointing back to owner
149
+ # belongs_to: foreign key is on the owner table pointing to associated table
150
+ foreign_key = if plural || default_assoc.to_s.pluralize == default_assoc.to_s # has_many or has_one
151
+ "#{owner_table.singularize}_id"
152
+ else # belongs_to
153
+ "#{table_name.singularize}_id"
154
+ end
135
155
 
136
- params << "#{default_assoc}: :#{assoc}"
156
+ params << "#{default_assoc}: :#{assoc}" if assoc != default_assoc
137
157
  params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
158
+ params << "#{default_assoc}_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
138
159
  end
139
160
 
140
161
  # Convert namespaced model names to proper table names
@@ -3,8 +3,11 @@ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::M
3
3
  create_table :<%= message_table_name %> do |t|
4
4
  t.string :role, null: false
5
5
  t.text :content
6
+ t.json :content_raw
6
7
  t.integer :input_tokens
7
8
  t.integer :output_tokens
9
+ t.integer :cached_tokens
10
+ t.integer :cache_creation_tokens
8
11
  t.timestamps
9
12
  end
10
13
 
@@ -14,6 +14,11 @@ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Mig
14
14
  t.jsonb :capabilities, default: []
15
15
  t.jsonb :pricing, default: {}
16
16
  t.jsonb :metadata, default: {}
17
+ <% elsif mysql? %>
18
+ t.json :modalities
19
+ t.json :capabilities
20
+ t.json :pricing
21
+ t.json :metadata
17
22
  <% else %>
18
23
  t.json :modalities, default: {}
19
24
  t.json :capabilities, default: []
@@ -4,7 +4,13 @@ class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord:
4
4
  create_table :<%= tool_call_table_name %> do |t|
5
5
  t.string :tool_call_id, null: false
6
6
  t.string :name, null: false
7
- t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
7
+ <% if postgresql? %>
8
+ t.jsonb :arguments, default: {}
9
+ <% elsif mysql? %>
10
+ t.json :arguments
11
+ <% else %>
12
+ t.json :arguments, default: {}
13
+ <% end %>
8
14
  t.timestamps
9
15
  end
10
16
 
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  # Generator to upgrade existing RubyLLM apps to v1.7 with new Rails-like API
10
10
  class UpgradeToV17Generator < Rails::Generators::Base
11
11
  include Rails::Generators::Migration
12
- include RubyLLM::GeneratorHelpers
12
+ include RubyLLM::Generators::GeneratorHelpers
13
13
 
14
14
  namespace 'ruby_llm:upgrade_to_v1_7'
15
15
  source_root File.expand_path('templates', __dir__)
@@ -0,0 +1,15 @@
1
+ class AddRubyLlmV19Columns < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ unless column_exists?(:<%= message_table_name %>, :cached_tokens)
4
+ add_column :<%= message_table_name %>, :cached_tokens, :integer
5
+ end
6
+
7
+ unless column_exists?(:<%= message_table_name %>, :cache_creation_tokens)
8
+ add_column :<%= message_table_name %>, :cache_creation_tokens, :integer
9
+ end
10
+
11
+ unless column_exists?(:<%= message_table_name %>, :content_raw)
12
+ add_column :<%= message_table_name %>, :content_raw, :json
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module RubyLLM
8
+ module Generators
9
+ # Generator to add v1.9 columns (cached tokens + raw content support) to existing apps.
10
+ class UpgradeToV19Generator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include RubyLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'ruby_llm:upgrade_to_v1_9'
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ argument :model_mappings, type: :array, default: [], banner: 'message:MessageName'
18
+
19
+ desc 'Adds cached token columns and raw content storage fields introduced in v1.9.0'
20
+
21
+ def self.next_migration_number(dirname)
22
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
23
+ end
24
+
25
+ def create_migration_file
26
+ parse_model_mappings
27
+
28
+ migration_template 'add_v1_9_message_columns.rb.tt',
29
+ 'db/migrate/add_ruby_llm_v1_9_columns.rb',
30
+ migration_version: migration_version,
31
+ message_table_name: message_table_name
32
+ end
33
+
34
+ def show_next_steps
35
+ say_status :success, 'Upgrade prepared!', :green
36
+ say <<~INSTRUCTIONS
37
+
38
+ Next steps:
39
+ 1. Review the generated migration
40
+ 2. Run: rails db:migrate
41
+ 3. Restart your application server
42
+
43
+ 📚 See the v1.9.0 release notes for details on cached token tracking and raw content support.
44
+
45
+ INSTRUCTIONS
46
+ end
47
+ end
48
+ end
49
+ end
@@ -11,28 +11,28 @@ module RubyLLM
11
11
  super
12
12
  # Monkey-patch Models to use database when ActsAs is active
13
13
  RubyLLM::Models.class_eval do
14
- def load_models
14
+ def self.load_models
15
15
  read_from_database
16
16
  rescue StandardError => e
17
17
  RubyLLM.logger.debug "Failed to load models from database: #{e.message}, falling back to JSON"
18
18
  read_from_json
19
19
  end
20
20
 
21
- def load_from_database!
22
- @models = read_from_database
23
- end
24
-
25
- def read_from_database
21
+ def self.read_from_database
26
22
  model_class = RubyLLM.config.model_registry_class
27
23
  model_class = model_class.constantize if model_class.is_a?(String)
28
24
  model_class.all.map(&:to_llm)
29
25
  end
26
+
27
+ def load_from_database!
28
+ @models = self.class.read_from_database
29
+ end
30
30
  end
31
31
  end
32
32
 
33
33
  class_methods do # rubocop:disable Metrics/BlockLength
34
- def acts_as_chat(messages: :messages, message_class: nil,
35
- model: :model, model_class: nil)
34
+ def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
35
+ model: :model, model_class: nil, model_foreign_key: nil)
36
36
  include RubyLLM::ActiveRecord::ChatMethods
37
37
 
38
38
  class_attribute :messages_association_name, :model_association_name, :message_class, :model_class
@@ -45,12 +45,12 @@ module RubyLLM
45
45
  has_many messages,
46
46
  -> { order(created_at: :asc) },
47
47
  class_name: self.message_class,
48
- foreign_key: ActiveSupport::Inflector.foreign_key(table_name.singularize),
48
+ foreign_key: messages_foreign_key,
49
49
  dependent: :destroy
50
50
 
51
51
  belongs_to model,
52
52
  class_name: self.model_class,
53
- foreign_key: ActiveSupport::Inflector.foreign_key(model.to_s.singularize),
53
+ foreign_key: model_foreign_key,
54
54
  optional: true
55
55
 
56
56
  delegate :add_message, to: :to_llm
@@ -68,7 +68,7 @@ module RubyLLM
68
68
  end
69
69
  end
70
70
 
71
- def acts_as_model(chats: :chats, chat_class: nil)
71
+ def acts_as_model(chats: :chats, chat_class: nil, chats_foreign_key: nil)
72
72
  include RubyLLM::ActiveRecord::ModelMethods
73
73
 
74
74
  class_attribute :chats_association_name, :chat_class
@@ -80,18 +80,16 @@ module RubyLLM
80
80
  validates :provider, presence: true
81
81
  validates :name, presence: true
82
82
 
83
- has_many chats,
84
- class_name: self.chat_class,
85
- foreign_key: ActiveSupport::Inflector.foreign_key(table_name.singularize)
83
+ has_many chats, class_name: self.chat_class, foreign_key: chats_foreign_key
86
84
 
87
85
  define_method :chats_association do
88
86
  send(chats_association_name)
89
87
  end
90
88
  end
91
89
 
92
- def acts_as_message(chat: :chat, chat_class: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
93
- tool_calls: :tool_calls, tool_call_class: nil,
94
- model: :model, model_class: nil)
90
+ def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
91
+ tool_calls: :tool_calls, tool_call_class: nil, tool_calls_foreign_key: nil,
92
+ model: :model, model_class: nil, model_foreign_key: nil)
95
93
  include RubyLLM::ActiveRecord::MessageMethods
96
94
 
97
95
  class_attribute :chat_association_name, :tool_calls_association_name, :model_association_name,
@@ -106,12 +104,12 @@ module RubyLLM
106
104
 
107
105
  belongs_to chat,
108
106
  class_name: self.chat_class,
109
- foreign_key: ActiveSupport::Inflector.foreign_key(chat.to_s.singularize),
107
+ foreign_key: chat_foreign_key,
110
108
  touch: touch_chat
111
109
 
112
110
  has_many tool_calls,
113
111
  class_name: self.tool_call_class,
114
- foreign_key: ActiveSupport::Inflector.foreign_key(table_name.singularize),
112
+ foreign_key: tool_calls_foreign_key,
115
113
  dependent: :destroy
116
114
 
117
115
  belongs_to :parent_tool_call,
@@ -126,7 +124,7 @@ module RubyLLM
126
124
 
127
125
  belongs_to model,
128
126
  class_name: self.model_class,
129
- foreign_key: ActiveSupport::Inflector.foreign_key(model.to_s.singularize),
127
+ foreign_key: model_foreign_key,
130
128
  optional: true
131
129
 
132
130
  delegate :tool_call?, :tool_result?, to: :to_llm
@@ -144,8 +142,8 @@ module RubyLLM
144
142
  end
145
143
  end
146
144
 
147
- def acts_as_tool_call(message: :message, message_class: nil,
148
- result: :result, result_class: nil)
145
+ def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
146
+ result: :result, result_class: nil, result_foreign_key: nil)
149
147
  class_attribute :message_association_name, :result_association_name, :message_class, :result_class
150
148
 
151
149
  self.message_association_name = message
@@ -155,11 +153,11 @@ module RubyLLM
155
153
 
156
154
  belongs_to message,
157
155
  class_name: self.message_class,
158
- foreign_key: ActiveSupport::Inflector.foreign_key(message.to_s.singularize)
156
+ foreign_key: message_foreign_key
159
157
 
160
158
  has_one result,
161
159
  class_name: self.result_class,
162
- foreign_key: ActiveSupport::Inflector.foreign_key(table_name.singularize),
160
+ foreign_key: result_foreign_key,
163
161
  dependent: :nullify
164
162
 
165
163
  define_method :message_association do
@@ -174,8 +174,16 @@ module RubyLLM
174
174
  end
175
175
 
176
176
  def create_user_message(content, with: nil)
177
- message_record = messages_association.create!(role: :user, content: content)
177
+ content_text, attachments, content_raw = prepare_content_for_storage(content)
178
+
179
+ message_record = messages_association.build(role: :user)
180
+ message_record.content = content_text
181
+ message_record.content_raw = content_raw if message_record.respond_to?(:content_raw=)
182
+ message_record.save!
183
+
178
184
  persist_content(message_record, with) if with.present?
185
+ persist_content(message_record, attachments) if attachments.present?
186
+
179
187
  message_record
180
188
  end
181
189
 
@@ -235,28 +243,25 @@ module RubyLLM
235
243
  @message = messages_association.create!(role: :assistant, content: '')
236
244
  end
237
245
 
238
- def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
246
+ # rubocop:disable Metrics/PerceivedComplexity
247
+ def persist_message_completion(message)
239
248
  return unless message
240
249
 
241
250
  tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
242
251
 
243
252
  transaction do
244
- content = message.content
245
- attachments_to_persist = nil
246
-
247
- if content.is_a?(RubyLLM::Content)
248
- attachments_to_persist = content.attachments if content.attachments.any?
249
- content = content.text
250
- elsif content.is_a?(Hash) || content.is_a?(Array)
251
- content = content.to_json
252
- end
253
+ content_text, attachments_to_persist, content_raw = prepare_content_for_storage(message.content)
253
254
 
254
255
  attrs = {
255
256
  role: message.role,
256
- content: content,
257
+ content: content_text,
257
258
  input_tokens: message.input_tokens,
258
259
  output_tokens: message.output_tokens
259
260
  }
261
+ attrs[:cached_tokens] = message.cached_tokens if @message.has_attribute?(:cached_tokens)
262
+ if @message.has_attribute?(:cache_creation_tokens)
263
+ attrs[:cache_creation_tokens] = message.cache_creation_tokens
264
+ end
260
265
 
261
266
  # Add model association dynamically
262
267
  attrs[self.class.model_association_name] = model_association
@@ -266,12 +271,15 @@ module RubyLLM
266
271
  attrs[parent_tool_call_assoc.foreign_key] = tool_call_id
267
272
  end
268
273
 
269
- @message.update!(attrs)
274
+ @message.assign_attributes(attrs)
275
+ @message.content_raw = content_raw if @message.respond_to?(:content_raw=)
276
+ @message.save!
270
277
 
271
278
  persist_content(@message, attachments_to_persist) if attachments_to_persist
272
279
  persist_tool_calls(message.tool_calls) if message.tool_calls.present?
273
280
  end
274
281
  end
282
+ # rubocop:enable Metrics/PerceivedComplexity
275
283
 
276
284
  def persist_tool_calls(tool_calls)
277
285
  tool_calls.each_value do |tool_call|
@@ -331,6 +339,26 @@ module RubyLLM
331
339
  RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
332
340
  nil
333
341
  end
342
+
343
+ def prepare_content_for_storage(content)
344
+ attachments = nil
345
+ content_raw = nil
346
+ content_text = content
347
+
348
+ case content
349
+ when RubyLLM::Content::Raw
350
+ content_raw = content.value
351
+ content_text = nil
352
+ when RubyLLM::Content
353
+ attachments = content.attachments if content.attachments.any?
354
+ content_text = content.text
355
+ when Hash, Array
356
+ content_raw = content
357
+ content_text = nil
358
+ end
359
+
360
+ [content_text, attachments, content_raw]
361
+ end
334
362
  end
335
363
  end
336
364
  end
@@ -11,6 +11,9 @@ module RubyLLM
11
11
  end
12
12
 
13
13
  def to_llm
14
+ cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
15
+ cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
16
+
14
17
  RubyLLM::Message.new(
15
18
  role: role.to_sym,
16
19
  content: extract_content,
@@ -18,6 +21,8 @@ module RubyLLM
18
21
  tool_call_id: extract_tool_call_id,
19
22
  input_tokens: input_tokens,
20
23
  output_tokens: output_tokens,
24
+ cached_tokens: cached,
25
+ cache_creation_tokens: cache_creation,
21
26
  model_id: model_association&.model_id
22
27
  )
23
28
  end
@@ -42,9 +47,13 @@ module RubyLLM
42
47
  end
43
48
 
44
49
  def extract_content
45
- return content unless respond_to?(:attachments) && attachments.attached?
50
+ return RubyLLM::Content::Raw.new(content_raw) if has_attribute?(:content_raw) && content_raw.present?
51
+
52
+ content_value = self[:content]
53
+
54
+ return content_value unless respond_to?(:attachments) && attachments.attached?
46
55
 
47
- RubyLLM::Content.new(content).tap do |content_obj|
56
+ RubyLLM::Content.new(content_value).tap do |content_obj|
48
57
  @_tempfiles = []
49
58
 
50
59
  attachments.each do |attachment|
@@ -77,7 +77,7 @@ module RubyLLM
77
77
  delegate :supports?, :supports_vision?, :supports_functions?, :type,
78
78
  :input_price_per_million, :output_price_per_million,
79
79
  :function_calling?, :structured_output?, :batch?,
80
- :reasoning?, :citations?, :streaming?,
80
+ :reasoning?, :citations?, :streaming?, :provider_class,
81
81
  to: :to_llm
82
82
  end
83
83
  end