ruby_llm 2.0.0.rc2 → 2.0.0.rc4

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +2 -2
  3. data/README.md +32 -21
  4. data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +34 -0
  5. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
  6. data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
  7. data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
  8. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
  9. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +19 -12
  10. data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +6 -6
  11. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +38 -6
  12. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +16 -14
  13. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +37 -8
  14. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +26 -5
  16. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +16 -1
  17. data/lib/ruby_llm/accounting/usage.rb +9 -0
  18. data/lib/ruby_llm/agent.rb +10 -9
  19. data/lib/ruby_llm/aliases.json +26 -4
  20. data/lib/ruby_llm/attachment.rb +5 -0
  21. data/lib/ruby_llm/batch.rb +2 -2
  22. data/lib/ruby_llm/chat.rb +6 -2
  23. data/lib/ruby_llm/embedding.rb +1 -1
  24. data/lib/ruby_llm/image.rb +1 -1
  25. data/lib/ruby_llm/message.rb +14 -5
  26. data/lib/ruby_llm/models.json +5364 -3027
  27. data/lib/ruby_llm/moderation.rb +1 -1
  28. data/lib/ruby_llm/ocr.rb +1 -1
  29. data/lib/ruby_llm/protocols/anthropic/chat.rb +1 -6
  30. data/lib/ruby_llm/protocols/bedrock/async_videos.rb +2 -1
  31. data/lib/ruby_llm/protocols/chat_completions/chat.rb +2 -11
  32. data/lib/ruby_llm/protocols/chat_completions/rerank.rb +8 -1
  33. data/lib/ruby_llm/protocols/cohere/rerank.rb +8 -1
  34. data/lib/ruby_llm/protocols/converse/chat.rb +0 -1
  35. data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +5 -0
  36. data/lib/ruby_llm/protocols/interactions/tools.rb +3 -1
  37. data/lib/ruby_llm/protocols/responses/chat.rb +0 -10
  38. data/lib/ruby_llm/providers/deepseek/responses.rb +0 -1
  39. data/lib/ruby_llm/providers/mistral/ocr.rb +5 -1
  40. data/lib/ruby_llm/providers/openrouter/chat.rb +1 -5
  41. data/lib/ruby_llm/rerank.rb +1 -1
  42. data/lib/ruby_llm/speech.rb +1 -1
  43. data/lib/ruby_llm/transcription.rb +1 -1
  44. data/lib/ruby_llm/version.rb +1 -1
  45. data/lib/ruby_llm/video_job.rb +1 -1
  46. data/lib/ruby_llm.rb +1 -1
  47. data/lib/tasks/ruby_llm.rake +1 -1
  48. metadata +11 -6
@@ -104,7 +104,7 @@ module RubyLLM
104
104
  empty_tokens = Tokens.new
105
105
  payload = {
106
106
  provider: provider_instance.slug,
107
- provider_class: provider_instance.class.display_name,
107
+ provider_class: provider_instance.name,
108
108
  model: model&.id,
109
109
  model_info: model,
110
110
  input: input,
data/lib/ruby_llm/ocr.rb CHANGED
@@ -62,7 +62,7 @@ module RubyLLM
62
62
  config: config)
63
63
  payload = {
64
64
  provider: provider_instance.slug,
65
- provider_class: provider_instance.class.display_name,
65
+ provider_class: provider_instance.name,
66
66
  model: model.id,
67
67
  model_info: model,
68
68
  pages: pages,
@@ -39,13 +39,12 @@ module RubyLLM
39
39
  warn_unsupported_citations(model) if citations && !model.supports?(:citations)
40
40
  tool_prefs ||= {}
41
41
  system_messages, chat_messages = separate_messages(messages)
42
- explicit_boundaries = cache_boundaries?(messages, caching:)
43
42
  system_content = build_system_content(system_messages, caching:)
44
43
 
45
44
  build_base_payload(chat_messages, model, stream, thinking, citations: citations, caching:,
46
45
  max_output_tokens:).tap do |payload|
47
46
  add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
48
- payload[:cache_control] = prompt_cache_control(caching) if caching && !explicit_boundaries
47
+ payload[:cache_control] = prompt_cache_control(caching) if caching
49
48
  end
50
49
  end
51
50
 
@@ -429,10 +428,6 @@ module RubyLLM
429
428
  content_blocks.concat(Media.format_content(msg.content, msg.attachments, citations: citations))
430
429
  end
431
430
 
432
- def cache_boundaries?(messages, caching: nil)
433
- caching != false && messages.any?(&:cache_until_here?)
434
- end
435
-
436
431
  def cache_boundary?(message, caching: nil)
437
432
  caching != false && message.cache_until_here?
438
433
  end
@@ -64,7 +64,8 @@ module RubyLLM
64
64
  prefix = job.raw.dig('outputDataConfig', 's3OutputDataConfig', 's3Uri')
65
65
  raise Error, 'Bedrock video job has no output S3 URI' unless prefix
66
66
 
67
- uris = @provider.list_file_uris("#{prefix.sub(%r{/+\z}, '')}/")
67
+ prefix_length = (prefix.rindex(%r{[^/]}) || -1) + 1
68
+ uris = @provider.list_file_uris("#{prefix[0, prefix_length]}/")
68
69
  videos = uris.select { |uri| uri.downcase.end_with?('.mp4') }
69
70
  raise Error, 'Expected exactly one MP4 in the Bedrock video output' unless videos.one?
70
71
 
@@ -87,7 +87,7 @@ module RubyLLM
87
87
  payload[:reasoning_effort] = effort if effort
88
88
 
89
89
  payload[:stream_options] = { include_usage: true } if stream
90
- apply_prompt_cache_params(payload, messages, caching)
90
+ apply_prompt_cache_params(payload, caching)
91
91
  payload
92
92
  end
93
93
 
@@ -250,11 +250,10 @@ module RubyLLM
250
250
  end
251
251
  end
252
252
 
253
- def apply_prompt_cache_params(payload, messages, caching)
253
+ def apply_prompt_cache_params(payload, caching)
254
254
  return unless openai_prompt_caching?
255
255
 
256
256
  payload.merge!(prompt_cache_params(caching)) if caching
257
- force_explicit_cache_mode(payload) if caching != false && cache_boundaries?(messages)
258
257
  end
259
258
 
260
259
  def openai_prompt_caching?
@@ -290,14 +289,6 @@ module RubyLLM
290
289
  retention
291
290
  end
292
291
 
293
- def force_explicit_cache_mode(payload)
294
- payload[:prompt_cache_options] = { mode: 'explicit' }.merge(payload[:prompt_cache_options] || {})
295
- end
296
-
297
- def cache_boundaries?(messages)
298
- messages.any?(&:cache_until_here?)
299
- end
300
-
301
292
  def prompt_cache_options(caching)
302
293
  options = caching.to_h.transform_keys(&:to_sym)
303
294
  unsupported = options.keys - PROMPT_CACHE_OPTIONS
@@ -38,10 +38,13 @@ module RubyLLM
38
38
  def parse_rerank_results(data, documents = [])
39
39
  Array(data['results']).map do |result|
40
40
  index = result['index']
41
+ unless valid_rerank_index?(index, documents)
42
+ raise Error, 'Rerank endpoint returned an invalid document index'
43
+ end
41
44
 
42
45
  RubyLLM::Rerank::Result.new(
43
46
  index: index,
44
- document: rerank_document(result['document']) || (index && documents[index]),
47
+ document: rerank_document(result['document']) || documents[index],
45
48
  score: result['relevance_score']
46
49
  )
47
50
  end
@@ -50,6 +53,10 @@ module RubyLLM
50
53
  def rerank_document(document)
51
54
  document.is_a?(Hash) ? document['text'] : document
52
55
  end
56
+
57
+ def valid_rerank_index?(index, documents)
58
+ index.is_a?(Integer) && index.between?(0, documents.length - 1)
59
+ end
53
60
  end
54
61
  end
55
62
  end
@@ -38,14 +38,21 @@ module RubyLLM
38
38
  def parse_rerank_results(data, documents)
39
39
  Array(data['results']).map do |result|
40
40
  index = result['index']
41
+ unless valid_rerank_index?(index, documents)
42
+ raise Error, 'Cohere reranking returned an invalid document index'
43
+ end
41
44
 
42
45
  RubyLLM::Rerank::Result.new(
43
46
  index: index,
44
- document: result.dig('document', 'text') || (index && documents[index]),
47
+ document: result.dig('document', 'text') || documents[index],
45
48
  score: result['relevance_score']
46
49
  )
47
50
  end
48
51
  end
52
+
53
+ def valid_rerank_index?(index, documents)
54
+ index.is_a?(Integer) && index.between?(0, documents.length - 1)
55
+ end
49
56
  end
50
57
  end
51
58
  end
@@ -266,7 +266,6 @@ module RubyLLM
266
266
 
267
267
  def automatic_cache_target(system_messages, chat_messages, caching)
268
268
  return unless caching
269
- return if (system_messages + chat_messages).any?(&:cache_until_here?)
270
269
 
271
270
  (chat_messages.reverse + system_messages.reverse).find { |msg| cacheable_message?(msg) }
272
271
  end
@@ -60,6 +60,11 @@ module RubyLLM
60
60
  metadata = responses.first.fetch('metadata')
61
61
  return if responses.size < metadata.fetch('embedding_count')
62
62
 
63
+ positions = responses.map { |inline| inline.dig('metadata', 'embedding_index') }
64
+ unless positions.sort == (0...responses.size).to_a
65
+ return [index, nil, batch_failure(key, 'Invalid or duplicate embedding record positions')]
66
+ end
67
+
63
68
  vectors = embedding_batch_vectors(responses)
64
69
  return [index, nil, batch_failure(key, 'Gemini returned no embedding')] unless vectors
65
70
 
@@ -31,7 +31,9 @@ module RubyLLM
31
31
  end
32
32
 
33
33
  def parse_interaction_arguments(arguments)
34
- arguments.is_a?(String) ? JSON.parse(arguments) : arguments || {}
34
+ return {} if arguments.nil? || (arguments.is_a?(String) && arguments.empty?)
35
+
36
+ arguments.is_a?(String) ? JSON.parse(arguments) : arguments
35
37
  rescue JSON::ParserError => e
36
38
  raise ToolCallParseError.new(finish_reason: :tool_calls), cause: e
37
39
  end
@@ -12,7 +12,6 @@ module RubyLLM
12
12
  OPENAI_INLINE_FILE_LIMIT = 50 * 1024 * 1024
13
13
  OPENAI_FILE_UPLOAD_LIMIT = 512 * 1024 * 1024
14
14
  PROMPT_CACHE_OPTIONS = %i[key ttl mode retention].freeze
15
- CACHE_BREAKPOINT_ROLES = %i[user system].freeze
16
15
 
17
16
  module_function
18
17
 
@@ -47,7 +46,6 @@ module RubyLLM
47
46
  payload[:reasoning] = { effort: effort } if effort
48
47
  payload[:reasoning] = (payload[:reasoning] || {}).merge(summary: 'auto') if thinking&.display == :summarized
49
48
  payload.merge!(prompt_cache_params(caching)) if caching
50
- force_explicit_cache_mode(payload) if caching != false && cache_boundaries?(messages)
51
49
 
52
50
  payload
53
51
  end
@@ -225,14 +223,6 @@ module RubyLLM
225
223
  retention
226
224
  end
227
225
 
228
- def force_explicit_cache_mode(payload)
229
- payload[:prompt_cache_options] = { mode: 'explicit' }.merge(payload[:prompt_cache_options] || {})
230
- end
231
-
232
- def cache_boundaries?(messages)
233
- messages.any? { |msg| msg.cache_until_here? && CACHE_BREAKPOINT_ROLES.include?(msg.role) }
234
- end
235
-
236
226
  def prompt_cache_options(caching)
237
227
  options = caching.to_h.transform_keys(&:to_sym)
238
228
  unsupported = options.keys - PROMPT_CACHE_OPTIONS
@@ -8,7 +8,6 @@ module RubyLLM
8
8
  # instead of summaries, both in responses and in the stream.
9
9
  class Responses < Protocols::Responses
10
10
  SERVER_TOOL_ALIASES = {
11
- web_search: { tool: { type: 'web_search' } },
12
11
  apply_patch: { tool: { type: 'custom', name: 'apply_patch' } }
13
12
  }.freeze
14
13
 
@@ -22,7 +22,7 @@ module RubyLLM
22
22
  end
23
23
 
24
24
  def ocr_document_part(attachment)
25
- reference = attachment.url? ? attachment.source.to_s : attachment.for_llm
25
+ reference = attachment.url? ? attachment.source.to_s : ocr_data_uri(attachment)
26
26
 
27
27
  if attachment.image?
28
28
  { type: 'image_url', image_url: reference }
@@ -31,6 +31,10 @@ module RubyLLM
31
31
  end
32
32
  end
33
33
 
34
+ def ocr_data_uri(attachment)
35
+ "data:#{attachment.mime_type};base64,#{attachment.encoded}"
36
+ end
37
+
34
38
  def parse_ocr_response(response, model:)
35
39
  data = response.body
36
40
 
@@ -47,7 +47,7 @@ module RubyLLM
47
47
 
48
48
  reasoning = build_reasoning(thinking)
49
49
  payload[:reasoning] = reasoning if reasoning
50
- payload[:cache_control] = prompt_cache_control(caching) if caching && !cache_boundaries?(messages)
50
+ payload[:cache_control] = prompt_cache_control(caching) if caching
51
51
  payload
52
52
  end
53
53
 
@@ -142,10 +142,6 @@ module RubyLLM
142
142
  keys.map { |key| ":#{key}" }.join(', ')
143
143
  end
144
144
 
145
- def cache_boundaries?(messages)
146
- messages.any?(&:cache_until_here?)
147
- end
148
-
149
145
  def openai_prompt_caching?
150
146
  false
151
147
  end
@@ -81,7 +81,7 @@ module RubyLLM
81
81
  empty_tokens = Tokens.new
82
82
  payload = {
83
83
  provider: provider_instance.slug,
84
- provider_class: provider_instance.class.display_name,
84
+ provider_class: provider_instance.name,
85
85
  model: model.id,
86
86
  model_info: model,
87
87
  query: query,
@@ -91,7 +91,7 @@ module RubyLLM
91
91
 
92
92
  payload = {
93
93
  provider: provider_instance.slug,
94
- provider_class: provider_instance.class.display_name,
94
+ provider_class: provider_instance.name,
95
95
  model: model.id,
96
96
  model_info: model,
97
97
  input: input,
@@ -132,7 +132,7 @@ module RubyLLM
132
132
  empty_tokens = Tokens.new
133
133
  payload = {
134
134
  provider: provider_instance.slug,
135
- provider_class: provider_instance.class.display_name,
135
+ provider_class: provider_instance.name,
136
136
  model: model.id,
137
137
  model_info: model,
138
138
  language: language,
@@ -2,5 +2,5 @@
2
2
 
3
3
  module RubyLLM
4
4
  # The version of the ruby_llm gem, as a string.
5
- VERSION = '2.0.0.rc2'
5
+ VERSION = '2.0.0.rc4'
6
6
  end
@@ -50,7 +50,7 @@ module RubyLLM
50
50
  config: config)
51
51
  payload = {
52
52
  provider: provider_instance.slug,
53
- provider_class: provider_instance.class.display_name,
53
+ provider_class: provider_instance.name,
54
54
  model: model.id,
55
55
  model_info: model,
56
56
  prompt: prompt,
data/lib/ruby_llm.rb CHANGED
@@ -44,7 +44,7 @@ loader.setup
44
44
 
45
45
  # RubyLLM is an AI framework for Ruby and Rails. Build conversations and
46
46
  # agents, generate media, process documents, and work with model providers
47
- # through one Ruby API. The guides at https://rubyllm.com/next/ introduce
47
+ # through one Ruby API. The guides at https://rubyllm.com/ introduce
48
48
  # each feature; this reference documents its classes, arguments, and results.
49
49
  #
50
50
  # RubyLLM.configure do |config|
@@ -7,7 +7,7 @@ namespace :ruby_llm do
7
7
  task action => :environment do
8
8
  require 'generators/ruby_llm/upgrade/upgrade_migration'
9
9
 
10
- RubyLLM::Generators::UpgradeMigration.new.public_send(action)
10
+ RubyLLM::Generators::UpgradeMigration.for.public_send(action)
11
11
  puts "RubyLLM copy upgrade: #{action} completed"
12
12
  end
13
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.rc2
4
+ version: 2.0.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -275,6 +275,11 @@ files:
275
275
  - lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt
276
276
  - lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt
277
277
  - lib/generators/ruby_llm/tool/tool_generator.rb
278
+ - lib/generators/ruby_llm/upgrade/legacy_content_sql.rb
279
+ - lib/generators/ruby_llm/upgrade/online_copy_migration.rb
280
+ - lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb
281
+ - lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb
282
+ - lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb
278
283
  - lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt
279
284
  - lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt
280
285
  - lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt
@@ -607,24 +612,24 @@ metadata:
607
612
  homepage_uri: https://rubyllm.com
608
613
  source_code_uri: https://github.com/crmne/ruby_llm
609
614
  changelog_uri: https://github.com/crmne/ruby_llm/releases
610
- documentation_uri: https://rubyllm.com/next/
615
+ documentation_uri: https://rubyllm.com/
611
616
  bug_tracker_uri: https://github.com/crmne/ruby_llm/issues
612
617
  funding_uri: https://github.com/sponsors/crmne
613
618
  rubygems_mfa_required: 'true'
614
619
  post_install_message: |
615
- RubyLLM 2.0.0.rc2
620
+ RubyLLM 2.0.0.rc4
616
621
 
617
622
  2.0 renames several APIs and changes what message content returns. Coming
618
623
  from 1.x? Read the upgrade guide before you boot:
619
624
 
620
- https://rubyllm.com/next/upgrading/
625
+ https://rubyllm.com/upgrading/
621
626
 
622
627
  The Rails upgrade uses forward-only preparation, backfill, and finish
623
628
  phases, with cleanup later. Rename mode is the default; optional copy
624
629
  mode supports a controlled return to 1.16. Read its requirements in the
625
630
  upgrade guide and rehearse on a recent production snapshot.
626
631
 
627
- Agent skill: https://rubyllm.com/next/ai-coding-assistants/
632
+ Agent skill: https://rubyllm.com/ai-coding-assistants/
628
633
  rdoc_options: []
629
634
  require_paths:
630
635
  - lib
@@ -639,7 +644,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
639
644
  - !ruby/object:Gem::Version
640
645
  version: '0'
641
646
  requirements: []
642
- rubygems_version: 4.0.16
647
+ rubygems_version: 4.0.20
643
648
  specification_version: 4
644
649
  summary: 'Build AI features the Ruby way: a delightful Ruby AI framework for every
645
650
  major AI provider.'