ruby_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 (56) 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 +42 -11
  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 +9 -4
  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/gemini/chat.rb +352 -69
  35. data/lib/ruby_llm/providers/gemini/media.rb +59 -1
  36. data/lib/ruby_llm/providers/gemini/tools.rb +146 -25
  37. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  38. data/lib/ruby_llm/providers/gemini.rb +2 -1
  39. data/lib/ruby_llm/providers/gpustack/media.rb +1 -0
  40. data/lib/ruby_llm/providers/ollama/media.rb +1 -0
  41. data/lib/ruby_llm/providers/openai/chat.rb +7 -2
  42. data/lib/ruby_llm/providers/openai/media.rb +2 -1
  43. data/lib/ruby_llm/providers/openai/streaming.rb +7 -2
  44. data/lib/ruby_llm/providers/openai/tools.rb +26 -6
  45. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  46. data/lib/ruby_llm/providers/openai.rb +1 -0
  47. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  48. data/lib/ruby_llm/providers/vertexai.rb +11 -11
  49. data/lib/ruby_llm/railtie.rb +24 -22
  50. data/lib/ruby_llm/stream_accumulator.rb +10 -4
  51. data/lib/ruby_llm/tool.rb +126 -0
  52. data/lib/ruby_llm/transcription.rb +35 -0
  53. data/lib/ruby_llm/utils.rb +46 -0
  54. data/lib/ruby_llm/version.rb +1 -1
  55. data/lib/ruby_llm.rb +7 -0
  56. metadata +24 -3
@@ -8,16 +8,17 @@
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-sonnet": {
12
- "anthropic": "claude-3-5-sonnet-20241022",
13
- "openrouter": "anthropic/claude-3.5-sonnet",
14
- "bedrock": "anthropic.claude-3-5-sonnet-20240620-v1:0:200k"
11
+ "claude-3-5-haiku-latest": {
12
+ "anthropic": "claude-3-5-haiku-latest"
15
13
  },
16
14
  "claude-3-7-sonnet": {
17
15
  "anthropic": "claude-3-7-sonnet-20250219",
18
16
  "openrouter": "anthropic/claude-3.7-sonnet",
19
17
  "bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
20
18
  },
19
+ "claude-3-7-sonnet-latest": {
20
+ "anthropic": "claude-3-7-sonnet-latest"
21
+ },
21
22
  "claude-3-haiku": {
22
23
  "anthropic": "claude-3-haiku-20240307",
23
24
  "openrouter": "anthropic/claude-3-haiku",
@@ -31,11 +32,19 @@
31
32
  "claude-3-sonnet": {
32
33
  "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
33
34
  },
35
+ "claude-haiku-4-5": {
36
+ "anthropic": "claude-haiku-4-5-20251001",
37
+ "openrouter": "anthropic/claude-haiku-4.5",
38
+ "bedrock": "us.anthropic.claude-haiku-4-5-20251001-v1:0"
39
+ },
34
40
  "claude-opus-4": {
35
41
  "anthropic": "claude-opus-4-20250514",
36
42
  "openrouter": "anthropic/claude-opus-4",
37
43
  "bedrock": "us.anthropic.claude-opus-4-1-20250805-v1:0"
38
44
  },
45
+ "claude-opus-4-0": {
46
+ "anthropic": "claude-opus-4-0"
47
+ },
39
48
  "claude-opus-4-1": {
40
49
  "anthropic": "claude-opus-4-1-20250805",
41
50
  "openrouter": "anthropic/claude-opus-4.1",
@@ -46,30 +55,18 @@
46
55
  "openrouter": "anthropic/claude-sonnet-4",
47
56
  "bedrock": "us.anthropic.claude-sonnet-4-20250514-v1:0"
48
57
  },
58
+ "claude-sonnet-4-0": {
59
+ "anthropic": "claude-sonnet-4-0"
60
+ },
61
+ "claude-sonnet-4-5": {
62
+ "anthropic": "claude-sonnet-4-5-20250929",
63
+ "openrouter": "anthropic/claude-sonnet-4.5",
64
+ "bedrock": "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
65
+ },
49
66
  "deepseek-chat": {
50
67
  "deepseek": "deepseek-chat",
51
68
  "openrouter": "deepseek/deepseek-chat"
52
69
  },
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
70
  "gemini-2.0-flash": {
74
71
  "gemini": "gemini-2.0-flash",
75
72
  "vertexai": "gemini-2.0-flash"
@@ -93,6 +90,10 @@
93
90
  "openrouter": "google/gemini-2.5-flash",
94
91
  "vertexai": "gemini-2.5-flash"
95
92
  },
93
+ "gemini-2.5-flash-image": {
94
+ "gemini": "gemini-2.5-flash-image",
95
+ "openrouter": "google/gemini-2.5-flash-image"
96
+ },
96
97
  "gemini-2.5-flash-image-preview": {
97
98
  "gemini": "gemini-2.5-flash-image-preview",
98
99
  "openrouter": "google/gemini-2.5-flash-image-preview"
@@ -106,6 +107,14 @@
106
107
  "gemini": "gemini-2.5-flash-lite-preview-06-17",
107
108
  "openrouter": "google/gemini-2.5-flash-lite-preview-06-17"
108
109
  },
110
+ "gemini-2.5-flash-lite-preview-09-2025": {
111
+ "gemini": "gemini-2.5-flash-lite-preview-09-2025",
112
+ "openrouter": "google/gemini-2.5-flash-lite-preview-09-2025"
113
+ },
114
+ "gemini-2.5-flash-preview-09-2025": {
115
+ "gemini": "gemini-2.5-flash-preview-09-2025",
116
+ "openrouter": "google/gemini-2.5-flash-preview-09-2025"
117
+ },
109
118
  "gemini-2.5-pro": {
110
119
  "gemini": "gemini-2.5-pro",
111
120
  "openrouter": "google/gemini-2.5-pro",
@@ -219,6 +228,10 @@
219
228
  "openai": "gpt-5",
220
229
  "openrouter": "openai/gpt-5"
221
230
  },
231
+ "gpt-5-codex": {
232
+ "openai": "gpt-5-codex",
233
+ "openrouter": "openai/gpt-5-codex"
234
+ },
222
235
  "gpt-5-mini": {
223
236
  "openai": "gpt-5-mini",
224
237
  "openrouter": "openai/gpt-5-mini"
@@ -227,18 +240,26 @@
227
240
  "openai": "gpt-5-nano",
228
241
  "openrouter": "openai/gpt-5-nano"
229
242
  },
243
+ "gpt-5-pro": {
244
+ "openai": "gpt-5-pro",
245
+ "openrouter": "openai/gpt-5-pro"
246
+ },
247
+ "gpt-oss-120b": {
248
+ "openai": "gpt-oss-120b",
249
+ "openrouter": "openai/gpt-oss-120b"
250
+ },
251
+ "gpt-oss-20b": {
252
+ "openai": "gpt-oss-20b",
253
+ "openrouter": "openai/gpt-oss-20b"
254
+ },
255
+ "imagen-4.0-generate-001": {
256
+ "gemini": "imagen-4.0-generate-001",
257
+ "vertexai": "imagen-4.0-generate-001"
258
+ },
230
259
  "o1": {
231
260
  "openai": "o1",
232
261
  "openrouter": "openai/o1"
233
262
  },
234
- "o1-mini": {
235
- "openai": "o1-mini",
236
- "openrouter": "openai/o1-mini"
237
- },
238
- "o1-mini-2024-09-12": {
239
- "openai": "o1-mini-2024-09-12",
240
- "openrouter": "openai/o1-mini-2024-09-12"
241
- },
242
263
  "o1-pro": {
243
264
  "openai": "o1-pro",
244
265
  "openrouter": "openai/o1-pro"
@@ -247,6 +268,10 @@
247
268
  "openai": "o3",
248
269
  "openrouter": "openai/o3"
249
270
  },
271
+ "o3-deep-research": {
272
+ "openai": "o3-deep-research",
273
+ "openrouter": "openai/o3-deep-research"
274
+ },
250
275
  "o3-mini": {
251
276
  "openai": "o3-mini",
252
277
  "openrouter": "openai/o3-mini"
@@ -259,6 +284,10 @@
259
284
  "openai": "o4-mini",
260
285
  "openrouter": "openai/o4-mini"
261
286
  },
287
+ "o4-mini-deep-research": {
288
+ "openai": "o4-mini-deep-research",
289
+ "openrouter": "openai/o4-mini-deep-research"
290
+ },
262
291
  "text-embedding-004": {
263
292
  "gemini": "text-embedding-004",
264
293
  "vertexai": "text-embedding-004"
@@ -7,17 +7,8 @@ module RubyLLM
7
7
 
8
8
  def initialize(source, filename: nil)
9
9
  @source = source
10
- if url?
11
- @source = URI source
12
- @filename = filename || File.basename(@source.path).to_s
13
- elsif path?
14
- @source = Pathname.new source
15
- @filename = filename || @source.basename.to_s
16
- elsif active_storage?
17
- @filename = filename || extract_filename_from_active_storage
18
- else
19
- @filename = filename
20
- end
10
+ @source = source_type_cast
11
+ @filename = filename || source_filename
21
12
 
22
13
  determine_mime_type
23
14
  end
@@ -65,6 +56,14 @@ module RubyLLM
65
56
  Base64.strict_encode64(content)
66
57
  end
67
58
 
59
+ def save(path)
60
+ return unless io_like?
61
+
62
+ File.open(path, 'w') do |f|
63
+ f.puts(@source.read)
64
+ end
65
+ end
66
+
68
67
  def for_llm
69
68
  case type
70
69
  when :text
@@ -158,6 +157,38 @@ module RubyLLM
158
157
  end
159
158
  end
160
159
 
160
+ def source_type_cast
161
+ if url?
162
+ URI(@source)
163
+ elsif path?
164
+ Pathname.new(@source)
165
+ else
166
+ @source
167
+ end
168
+ end
169
+
170
+ def source_filename
171
+ if url?
172
+ File.basename(@source.path).to_s
173
+ elsif path?
174
+ @source.basename.to_s
175
+ elsif io_like?
176
+ extract_filename_from_io
177
+ elsif active_storage?
178
+ extract_filename_from_active_storage
179
+ end
180
+ end
181
+
182
+ def extract_filename_from_io
183
+ if defined?(ActionDispatch::Http::UploadedFile) && @source.is_a?(ActionDispatch::Http::UploadedFile)
184
+ @source.original_filename.to_s
185
+ elsif @source.respond_to?(:path)
186
+ File.basename(@source.path).to_s
187
+ else
188
+ 'attachment'
189
+ end
190
+ end
191
+
161
192
  def extract_filename_from_active_storage # rubocop:disable Metrics/PerceivedComplexity
162
193
  return 'attachment' unless defined?(ActiveStorage)
163
194
 
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,
@@ -28,7 +29,9 @@ module RubyLLM
28
29
  :default_embedding_model,
29
30
  :default_moderation_model,
30
31
  :default_image_model,
32
+ :default_transcription_model,
31
33
  # Model registry
34
+ :model_registry_file,
32
35
  :model_registry_class,
33
36
  # Rails integration
34
37
  :use_new_acts_as,
@@ -46,7 +49,7 @@ module RubyLLM
46
49
  :log_stream_debug
47
50
 
48
51
  def initialize
49
- @request_timeout = 120
52
+ @request_timeout = 300
50
53
  @max_retries = 3
51
54
  @retry_interval = 0.1
52
55
  @retry_backoff_factor = 2
@@ -57,7 +60,9 @@ module RubyLLM
57
60
  @default_embedding_model = 'text-embedding-3-small'
58
61
  @default_moderation_model = 'omni-moderation-latest'
59
62
  @default_image_model = 'gpt-image-1'
63
+ @default_transcription_model = 'whisper-1'
60
64
 
65
+ @model_registry_file = File.expand_path('models.json', __dir__)
61
66
  @model_registry_class = 'Model'
62
67
  @use_new_acts_as = false
63
68
 
@@ -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
@@ -66,7 +65,7 @@ module RubyLLM
66
65
  errors: true,
67
66
  headers: false,
68
67
  log_level: :debug do |logger|
69
- logger.filter(%r{[A-Za-z0-9+/=]{100,}}, 'data":"[BASE64 DATA]"')
68
+ logger.filter(%r{[A-Za-z0-9+/=]{100,}}, '[BASE64 DATA]')
70
69
  logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
71
70
  end
72
71
  end
@@ -83,6 +82,7 @@ module RubyLLM
83
82
  end
84
83
 
85
84
  def setup_middleware(faraday)
85
+ faraday.request :multipart
86
86
  faraday.request :json
87
87
  faraday.response :json
88
88
  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,17 +5,20 @@ 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
8
+ attr_reader :role, :model_id, :tool_calls, :tool_call_id, :input_tokens, :output_tokens,
9
+ :cached_tokens, :cache_creation_tokens, :raw
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]
15
18
  @input_tokens = options[:input_tokens]
16
19
  @output_tokens = options[:output_tokens]
17
- @model_id = options[:model_id]
18
- @tool_call_id = options[:tool_call_id]
20
+ @cached_tokens = options[:cached_tokens]
21
+ @cache_creation_tokens = options[:cache_creation_tokens]
19
22
  @raw = options[:raw]
20
23
 
21
24
  ensure_valid_role
@@ -45,11 +48,13 @@ module RubyLLM
45
48
  {
46
49
  role: role,
47
50
  content: content,
51
+ model_id: model_id,
48
52
  tool_calls: tool_calls,
49
53
  tool_call_id: tool_call_id,
50
54
  input_tokens: input_tokens,
51
55
  output_tokens: output_tokens,
52
- model_id: model_id
56
+ cached_tokens: cached_tokens,
57
+ cache_creation_tokens: cache_creation_tokens
53
58
  }.compact
54
59
  end
55
60
 
@@ -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'