dify_llm 1.8.2 → 1.9.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -3
  3. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +3 -0
  4. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +1 -1
  5. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  6. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  7. data/lib/ruby_llm/active_record/acts_as.rb +6 -6
  8. data/lib/ruby_llm/active_record/chat_methods.rb +41 -13
  9. data/lib/ruby_llm/active_record/message_methods.rb +11 -2
  10. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  11. data/lib/ruby_llm/aliases.json +62 -20
  12. data/lib/ruby_llm/attachment.rb +8 -0
  13. data/lib/ruby_llm/chat.rb +13 -2
  14. data/lib/ruby_llm/configuration.rb +6 -1
  15. data/lib/ruby_llm/connection.rb +3 -3
  16. data/lib/ruby_llm/content.rb +23 -0
  17. data/lib/ruby_llm/message.rb +11 -6
  18. data/lib/ruby_llm/model/info.rb +4 -0
  19. data/lib/ruby_llm/models.json +9410 -7793
  20. data/lib/ruby_llm/models.rb +14 -22
  21. data/lib/ruby_llm/provider.rb +23 -1
  22. data/lib/ruby_llm/providers/anthropic/chat.rb +22 -3
  23. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  24. data/lib/ruby_llm/providers/anthropic/media.rb +2 -1
  25. data/lib/ruby_llm/providers/anthropic/models.rb +15 -0
  26. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  27. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -18
  28. data/lib/ruby_llm/providers/bedrock/media.rb +2 -1
  29. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +15 -0
  30. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +2 -0
  31. data/lib/ruby_llm/providers/dify/chat.rb +16 -5
  32. data/lib/ruby_llm/providers/gemini/chat.rb +352 -69
  33. data/lib/ruby_llm/providers/gemini/media.rb +59 -1
  34. data/lib/ruby_llm/providers/gemini/tools.rb +146 -25
  35. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  36. data/lib/ruby_llm/providers/gemini.rb +2 -1
  37. data/lib/ruby_llm/providers/gpustack/media.rb +1 -0
  38. data/lib/ruby_llm/providers/ollama/media.rb +1 -0
  39. data/lib/ruby_llm/providers/openai/chat.rb +7 -2
  40. data/lib/ruby_llm/providers/openai/media.rb +2 -1
  41. data/lib/ruby_llm/providers/openai/streaming.rb +7 -2
  42. data/lib/ruby_llm/providers/openai/tools.rb +26 -6
  43. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  44. data/lib/ruby_llm/providers/openai.rb +1 -0
  45. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  46. data/lib/ruby_llm/providers/vertexai.rb +3 -0
  47. data/lib/ruby_llm/stream_accumulator.rb +10 -4
  48. data/lib/ruby_llm/tool.rb +126 -0
  49. data/lib/ruby_llm/transcription.rb +35 -0
  50. data/lib/ruby_llm/utils.rb +46 -0
  51. data/lib/ruby_llm/version.rb +1 -1
  52. data/lib/ruby_llm.rb +6 -0
  53. 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: 75dbb53612d3fa2c1089038bcf48fbbc0fe9425d37ffd8fccdfa56337daf97af
4
+ data.tar.gz: 316d3ef004a7387a6e723a02f8ab09729b167097127f9cb85ce5a864e6e4ef1e
5
5
  SHA512:
6
- metadata.gz: 305089c41bb76bf36aca07dfd95776b182debe2e059ce75dd34a77627516b2b95603de73780363cf5a9a2096df0109e433fdb420416067761daed700fad7c0b3
7
- data.tar.gz: e7478c5771d9d00046d77a2965b801a705e63734360c1871e8865ac9ed634882e529bcf5b241cfeed103e38c87dd009730e6cab92075074b915525525525802e
6
+ metadata.gz: bca45bf0d49f6e98e9ea00cf2e760537fbff5f861394080f8dec8baa634784289f23779476eedbda5c9d11e4323e2a67f6eadc84144c18cd4adc7d09e3b9cfe7
7
+ data.tar.gz: a12a868701ae4e70f6f397de290d3bc018da91993950366d956fd8d60f0c37cc8ef89f882a680db396336258107e032f881817224668b81d9d65b3f53c1f44d9
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
@@ -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
 
@@ -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,22 +11,22 @@ 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
 
@@ -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
@@ -8,6 +8,9 @@
8
8
  "openrouter": "anthropic/claude-3.5-haiku",
9
9
  "bedrock": "anthropic.claude-3-5-haiku-20241022-v1:0"
10
10
  },
11
+ "claude-3-5-haiku-latest": {
12
+ "anthropic": "claude-3-5-haiku-latest"
13
+ },
11
14
  "claude-3-5-sonnet": {
12
15
  "anthropic": "claude-3-5-sonnet-20241022",
13
16
  "openrouter": "anthropic/claude-3.5-sonnet",
@@ -18,6 +21,9 @@
18
21
  "openrouter": "anthropic/claude-3.7-sonnet",
19
22
  "bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
20
23
  },
24
+ "claude-3-7-sonnet-latest": {
25
+ "anthropic": "claude-3-7-sonnet-latest"
26
+ },
21
27
  "claude-3-haiku": {
22
28
  "anthropic": "claude-3-haiku-20240307",
23
29
  "openrouter": "anthropic/claude-3-haiku",
@@ -31,11 +37,19 @@
31
37
  "claude-3-sonnet": {
32
38
  "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
33
39
  },
40
+ "claude-haiku-4-5": {
41
+ "anthropic": "claude-haiku-4-5-20251001",
42
+ "openrouter": "anthropic/claude-haiku-4.5",
43
+ "bedrock": "us.anthropic.claude-haiku-4-5-20251001-v1:0"
44
+ },
34
45
  "claude-opus-4": {
35
46
  "anthropic": "claude-opus-4-20250514",
36
47
  "openrouter": "anthropic/claude-opus-4",
37
48
  "bedrock": "us.anthropic.claude-opus-4-1-20250805-v1:0"
38
49
  },
50
+ "claude-opus-4-0": {
51
+ "anthropic": "claude-opus-4-0"
52
+ },
39
53
  "claude-opus-4-1": {
40
54
  "anthropic": "claude-opus-4-1-20250805",
41
55
  "openrouter": "anthropic/claude-opus-4.1",
@@ -46,30 +60,18 @@
46
60
  "openrouter": "anthropic/claude-sonnet-4",
47
61
  "bedrock": "us.anthropic.claude-sonnet-4-20250514-v1:0"
48
62
  },
63
+ "claude-sonnet-4-0": {
64
+ "anthropic": "claude-sonnet-4-0"
65
+ },
66
+ "claude-sonnet-4-5": {
67
+ "anthropic": "claude-sonnet-4-5-20250929",
68
+ "openrouter": "anthropic/claude-sonnet-4.5",
69
+ "bedrock": "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
70
+ },
49
71
  "deepseek-chat": {
50
72
  "deepseek": "deepseek-chat",
51
73
  "openrouter": "deepseek/deepseek-chat"
52
74
  },
53
- "gemini-1.5-flash": {
54
- "gemini": "gemini-1.5-flash",
55
- "vertexai": "gemini-1.5-flash"
56
- },
57
- "gemini-1.5-flash-002": {
58
- "gemini": "gemini-1.5-flash-002",
59
- "vertexai": "gemini-1.5-flash-002"
60
- },
61
- "gemini-1.5-flash-8b": {
62
- "gemini": "gemini-1.5-flash-8b",
63
- "vertexai": "gemini-1.5-flash-8b"
64
- },
65
- "gemini-1.5-pro": {
66
- "gemini": "gemini-1.5-pro",
67
- "vertexai": "gemini-1.5-pro"
68
- },
69
- "gemini-1.5-pro-002": {
70
- "gemini": "gemini-1.5-pro-002",
71
- "vertexai": "gemini-1.5-pro-002"
72
- },
73
75
  "gemini-2.0-flash": {
74
76
  "gemini": "gemini-2.0-flash",
75
77
  "vertexai": "gemini-2.0-flash"
@@ -93,6 +95,10 @@
93
95
  "openrouter": "google/gemini-2.5-flash",
94
96
  "vertexai": "gemini-2.5-flash"
95
97
  },
98
+ "gemini-2.5-flash-image": {
99
+ "gemini": "gemini-2.5-flash-image",
100
+ "openrouter": "google/gemini-2.5-flash-image"
101
+ },
96
102
  "gemini-2.5-flash-image-preview": {
97
103
  "gemini": "gemini-2.5-flash-image-preview",
98
104
  "openrouter": "google/gemini-2.5-flash-image-preview"
@@ -106,6 +112,14 @@
106
112
  "gemini": "gemini-2.5-flash-lite-preview-06-17",
107
113
  "openrouter": "google/gemini-2.5-flash-lite-preview-06-17"
108
114
  },
115
+ "gemini-2.5-flash-lite-preview-09-2025": {
116
+ "gemini": "gemini-2.5-flash-lite-preview-09-2025",
117
+ "openrouter": "google/gemini-2.5-flash-lite-preview-09-2025"
118
+ },
119
+ "gemini-2.5-flash-preview-09-2025": {
120
+ "gemini": "gemini-2.5-flash-preview-09-2025",
121
+ "openrouter": "google/gemini-2.5-flash-preview-09-2025"
122
+ },
109
123
  "gemini-2.5-pro": {
110
124
  "gemini": "gemini-2.5-pro",
111
125
  "openrouter": "google/gemini-2.5-pro",
@@ -219,6 +233,10 @@
219
233
  "openai": "gpt-5",
220
234
  "openrouter": "openai/gpt-5"
221
235
  },
236
+ "gpt-5-codex": {
237
+ "openai": "gpt-5-codex",
238
+ "openrouter": "openai/gpt-5-codex"
239
+ },
222
240
  "gpt-5-mini": {
223
241
  "openai": "gpt-5-mini",
224
242
  "openrouter": "openai/gpt-5-mini"
@@ -227,6 +245,22 @@
227
245
  "openai": "gpt-5-nano",
228
246
  "openrouter": "openai/gpt-5-nano"
229
247
  },
248
+ "gpt-5-pro": {
249
+ "openai": "gpt-5-pro",
250
+ "openrouter": "openai/gpt-5-pro"
251
+ },
252
+ "gpt-oss-120b": {
253
+ "openai": "gpt-oss-120b",
254
+ "openrouter": "openai/gpt-oss-120b"
255
+ },
256
+ "gpt-oss-20b": {
257
+ "openai": "gpt-oss-20b",
258
+ "openrouter": "openai/gpt-oss-20b"
259
+ },
260
+ "imagen-4.0-generate-001": {
261
+ "gemini": "imagen-4.0-generate-001",
262
+ "vertexai": "imagen-4.0-generate-001"
263
+ },
230
264
  "o1": {
231
265
  "openai": "o1",
232
266
  "openrouter": "openai/o1"
@@ -247,6 +281,10 @@
247
281
  "openai": "o3",
248
282
  "openrouter": "openai/o3"
249
283
  },
284
+ "o3-deep-research": {
285
+ "openai": "o3-deep-research",
286
+ "openrouter": "openai/o3-deep-research"
287
+ },
250
288
  "o3-mini": {
251
289
  "openai": "o3-mini",
252
290
  "openrouter": "openai/o3-mini"
@@ -259,6 +297,10 @@
259
297
  "openai": "o4-mini",
260
298
  "openrouter": "openai/o4-mini"
261
299
  },
300
+ "o4-mini-deep-research": {
301
+ "openai": "o4-mini-deep-research",
302
+ "openrouter": "openai/o4-mini-deep-research"
303
+ },
262
304
  "text-embedding-004": {
263
305
  "gemini": "text-embedding-004",
264
306
  "vertexai": "text-embedding-004"
@@ -71,6 +71,14 @@ module RubyLLM
71
71
  Base64.strict_encode64(content)
72
72
  end
73
73
 
74
+ def save(path)
75
+ return unless io_like?
76
+
77
+ File.open(path, 'w') do |f|
78
+ f.puts(@source.read)
79
+ end
80
+ end
81
+
74
82
  def for_llm
75
83
  case type
76
84
  when :text
data/lib/ruby_llm/chat.rb CHANGED
@@ -31,7 +31,7 @@ module RubyLLM
31
31
  end
32
32
 
33
33
  def ask(message = nil, with: nil, &)
34
- add_message role: :user, content: Content.new(message, with)
34
+ add_message role: :user, content: build_content(message, with)
35
35
  complete(&)
36
36
  end
37
37
 
@@ -193,7 +193,8 @@ module RubyLLM
193
193
  @on[:tool_call]&.call(tool_call)
194
194
  result = execute_tool tool_call
195
195
  @on[:tool_result]&.call(result)
196
- content = result.is_a?(Content) ? result : result.to_s
196
+ tool_payload = result.is_a?(Tool::Halt) ? result.content : result
197
+ content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
197
198
  message = add_message role: :tool, content:, tool_call_id: tool_call.id
198
199
  @on[:end_message]&.call(message)
199
200
 
@@ -208,5 +209,15 @@ module RubyLLM
208
209
  args = tool_call.arguments
209
210
  tool.call(args)
210
211
  end
212
+
213
+ def build_content(message, attachments)
214
+ return message if content_like?(message)
215
+
216
+ Content.new(message, attachments)
217
+ end
218
+
219
+ def content_like?(object)
220
+ object.is_a?(Content) || object.is_a?(Content::Raw)
221
+ end
211
222
  end
212
223
  end
@@ -10,6 +10,7 @@ module RubyLLM
10
10
  :openai_use_system_role,
11
11
  :anthropic_api_key,
12
12
  :gemini_api_key,
13
+ :gemini_api_base,
13
14
  :vertexai_project_id,
14
15
  :vertexai_location,
15
16
  :deepseek_api_key,
@@ -31,7 +32,9 @@ module RubyLLM
31
32
  :default_embedding_model,
32
33
  :default_moderation_model,
33
34
  :default_image_model,
35
+ :default_transcription_model,
34
36
  # Model registry
37
+ :model_registry_file,
35
38
  :model_registry_class,
36
39
  # Rails integration
37
40
  :use_new_acts_as,
@@ -49,7 +52,7 @@ module RubyLLM
49
52
  :log_stream_debug
50
53
 
51
54
  def initialize
52
- @request_timeout = 120
55
+ @request_timeout = 300
53
56
  @max_retries = 3
54
57
  @retry_interval = 0.1
55
58
  @retry_backoff_factor = 2
@@ -60,7 +63,9 @@ module RubyLLM
60
63
  @default_embedding_model = 'text-embedding-3-small'
61
64
  @default_moderation_model = 'omni-moderation-latest'
62
65
  @default_image_model = 'gpt-image-1'
66
+ @default_transcription_model = 'whisper-1'
63
67
 
68
+ @model_registry_file = File.expand_path('models.json', __dir__)
64
69
  @model_registry_class = 'Model'
65
70
  @use_new_acts_as = false
66
71
 
@@ -34,8 +34,7 @@ module RubyLLM
34
34
  end
35
35
 
36
36
  def post(url, payload, &)
37
- body = payload.is_a?(Hash) ? JSON.generate(payload, ascii_only: false) : payload
38
- @connection.post url, body do |req|
37
+ @connection.post url, payload do |req|
39
38
  req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
40
39
  yield req if block_given?
41
40
  end
@@ -77,7 +76,7 @@ module RubyLLM
77
76
  errors: true,
78
77
  headers: false,
79
78
  log_level: :debug do |logger|
80
- logger.filter(%r{[A-Za-z0-9+/=]{100,}}, 'data":"[BASE64 DATA]"')
79
+ logger.filter(%r{[A-Za-z0-9+/=]{100,}}, '[BASE64 DATA]')
81
80
  logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
82
81
  end
83
82
  end
@@ -94,6 +93,7 @@ module RubyLLM
94
93
  end
95
94
 
96
95
  def setup_middleware(faraday)
96
+ faraday.request :multipart
97
97
  faraday.request :json
98
98
  faraday.response :json
99
99
  faraday.adapter :net_http
@@ -48,3 +48,26 @@ module RubyLLM
48
48
  end
49
49
  end
50
50
  end
51
+
52
+ module RubyLLM
53
+ class Content
54
+ # Represents provider-specific payloads that should bypass RubyLLM formatting.
55
+ class Raw
56
+ attr_reader :value
57
+
58
+ def initialize(value)
59
+ raise ArgumentError, 'Raw content payload cannot be nil' if value.nil?
60
+
61
+ @value = value
62
+ end
63
+
64
+ def format
65
+ @value
66
+ end
67
+
68
+ def to_h
69
+ @value
70
+ end
71
+ end
72
+ end
73
+ end
@@ -5,18 +5,21 @@ module RubyLLM
5
5
  class Message
6
6
  ROLES = %i[system user assistant tool].freeze
7
7
 
8
- attr_reader :role, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id, :raw, :conversation_id
8
+ attr_reader :role, :model_id, :tool_calls, :tool_call_id, :input_tokens, :output_tokens,
9
+ :cached_tokens, :cache_creation_tokens, :raw, :conversation_id
9
10
  attr_writer :content
10
11
 
11
12
  def initialize(options = {})
12
13
  @role = options.fetch(:role).to_sym
13
14
  @content = normalize_content(options.fetch(:content))
15
+ @model_id = options[:model_id]
14
16
  @tool_calls = options[:tool_calls]
17
+ @tool_call_id = options[:tool_call_id]
18
+ @conversation_id = options[:conversation_id]
15
19
  @input_tokens = options[:input_tokens]
16
20
  @output_tokens = options[:output_tokens]
17
- @model_id = options[:model_id]
18
- @conversation_id = options[:conversation_id]
19
- @tool_call_id = options[:tool_call_id]
21
+ @cached_tokens = options[:cached_tokens]
22
+ @cache_creation_tokens = options[:cache_creation_tokens]
20
23
  @raw = options[:raw]
21
24
 
22
25
  ensure_valid_role
@@ -46,12 +49,14 @@ module RubyLLM
46
49
  {
47
50
  role: role,
48
51
  content: content,
52
+ model_id: model_id,
49
53
  tool_calls: tool_calls,
50
54
  tool_call_id: tool_call_id,
55
+ conversation_id: conversation_id,
51
56
  input_tokens: input_tokens,
52
57
  output_tokens: output_tokens,
53
- conversation_id: conversation_id,
54
- model_id: model_id
58
+ cached_tokens: cached_tokens,
59
+ cache_creation_tokens: cache_creation_tokens
55
60
  }.compact
56
61
  end
57
62
 
@@ -72,6 +72,10 @@ module RubyLLM
72
72
  pricing.text_tokens.output
73
73
  end
74
74
 
75
+ def provider_class
76
+ RubyLLM::Provider.resolve provider
77
+ end
78
+
75
79
  def type # rubocop:disable Metrics/PerceivedComplexity
76
80
  if modalities.output.include?('embeddings') && !modalities.output.include?('text')
77
81
  'embedding'