ruby_llm 1.15.0 → 1.16.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  4. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  5. data/lib/ruby_llm/active_record/acts_as.rb +1 -26
  6. data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
  7. data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
  8. data/lib/ruby_llm/active_record/message_methods.rb +70 -3
  9. data/lib/ruby_llm/agent.rb +1 -0
  10. data/lib/ruby_llm/aliases.json +78 -75
  11. data/lib/ruby_llm/aliases.rb +3 -0
  12. data/lib/ruby_llm/attachment.rb +34 -17
  13. data/lib/ruby_llm/chat.rb +176 -47
  14. data/lib/ruby_llm/configuration.rb +14 -1
  15. data/lib/ruby_llm/connection.rb +36 -7
  16. data/lib/ruby_llm/content.rb +15 -1
  17. data/lib/ruby_llm/deprecator.rb +24 -0
  18. data/lib/ruby_llm/embedding.rb +31 -1
  19. data/lib/ruby_llm/error.rb +11 -75
  20. data/lib/ruby_llm/error_middleware.rb +81 -0
  21. data/lib/ruby_llm/image.rb +2 -0
  22. data/lib/ruby_llm/instrumentation.rb +36 -0
  23. data/lib/ruby_llm/mime_type.rb +25 -0
  24. data/lib/ruby_llm/model/info.rb +36 -2
  25. data/lib/ruby_llm/model/pricing.rb +19 -9
  26. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  27. data/lib/ruby_llm/model_registry.rb +39 -0
  28. data/lib/ruby_llm/models.json +18225 -19144
  29. data/lib/ruby_llm/models.rb +95 -30
  30. data/lib/ruby_llm/provider.rb +11 -2
  31. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  32. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  33. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  34. data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
  35. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  36. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  37. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
  38. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  39. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  40. data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
  41. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  42. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  43. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  44. data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
  45. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  46. data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
  47. data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
  48. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  49. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  50. data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
  51. data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
  52. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  53. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  54. data/lib/ruby_llm/providers/mistral.rb +2 -2
  55. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  56. data/lib/ruby_llm/providers/openai/chat.rb +16 -1
  57. data/lib/ruby_llm/providers/openai/images.rb +9 -9
  58. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  59. data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
  60. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  61. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  62. data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
  63. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  64. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  65. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  66. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  67. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  68. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  69. data/lib/ruby_llm/providers/xai.rb +2 -2
  70. data/lib/ruby_llm/railtie.rb +5 -1
  71. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  72. data/lib/ruby_llm/streaming.rb +4 -0
  73. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  74. data/lib/ruby_llm/transcription.rb +2 -1
  75. data/lib/ruby_llm/utils.rb +39 -0
  76. data/lib/ruby_llm/version.rb +1 -1
  77. data/lib/ruby_llm.rb +9 -2
  78. data/lib/tasks/models.rake +32 -4
  79. data/lib/tasks/release.rake +50 -23
  80. metadata +17 -10
@@ -8,7 +8,7 @@ module RubyLLM
8
8
  include XAI::Models
9
9
 
10
10
  def api_base
11
- 'https://api.x.ai/v1'
11
+ @config.xai_api_base || 'https://api.x.ai/v1'
12
12
  end
13
13
 
14
14
  def headers
@@ -20,7 +20,7 @@ module RubyLLM
20
20
 
21
21
  class << self
22
22
  def configuration_options
23
- %i[xai_api_key]
23
+ %i[xai_api_key xai_api_base]
24
24
  end
25
25
 
26
26
  def configuration_requirements
@@ -10,6 +10,10 @@ if defined?(Rails::Railtie)
10
10
  end
11
11
  end
12
12
 
13
+ initializer 'ruby_llm.instrumentation' do
14
+ RubyLLM.config.instrumenter ||= ActiveSupport::Notifications
15
+ end
16
+
13
17
  initializer 'ruby_llm.active_record' do
14
18
  ActiveSupport.on_load :active_record do
15
19
  require 'ruby_llm/active_record/payload_helpers'
@@ -25,7 +29,7 @@ if defined?(Rails::Railtie)
25
29
  require 'ruby_llm/active_record/acts_as_legacy'
26
30
  ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy
27
31
 
28
- Rails.logger.warn(
32
+ RubyLLM.deprecator.warn(
29
33
  "\n!!! RubyLLM's legacy acts_as API is deprecated and will be removed in RubyLLM 2.0.0. " \
30
34
  "Please consult the migration guide at https://rubyllm.com/upgrading-to-1-7/\n"
31
35
  )
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+ require 'securerandom'
5
+
3
6
  module RubyLLM
4
7
  # Assembles streaming responses from LLMs into complete messages.
5
8
  class StreamAccumulator
@@ -18,6 +21,7 @@ module RubyLLM
18
21
  @inside_think_tag = false
19
22
  @pending_think_tag = +''
20
23
  @latest_tool_call_id = nil
24
+ @tool_call_ids_by_index = {}
21
25
  end
22
26
 
23
27
  def add(chunk)
@@ -72,43 +76,54 @@ module RubyLLM
72
76
  end
73
77
  end
74
78
 
75
- def accumulate_tool_calls(new_tool_calls) # rubocop:disable Metrics/PerceivedComplexity
79
+ def accumulate_tool_calls(new_tool_calls)
76
80
  RubyLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if RubyLLM.config.log_stream_debug
77
- new_tool_calls.each_value do |tool_call|
81
+ new_tool_calls.each do |stream_key, tool_call|
78
82
  if tool_call.id
79
- tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
80
- tool_call_arguments = tool_call.arguments
81
- if tool_call_arguments.nil? || (tool_call_arguments.respond_to?(:empty?) && tool_call_arguments.empty?)
82
- tool_call_arguments = +''
83
- end
84
- @tool_calls[tool_call.id] = ToolCall.new(
85
- id: tool_call_id,
86
- name: tool_call.name,
87
- arguments: tool_call_arguments,
88
- thought_signature: tool_call.thought_signature
89
- )
90
- @latest_tool_call_id = tool_call.id
83
+ start_tool_call(stream_key, tool_call)
91
84
  else
92
- existing = @tool_calls[@latest_tool_call_id]
93
- if existing
94
- fragment = tool_call.arguments
95
- fragment = '' if fragment.nil?
96
- existing.arguments << fragment
97
- if tool_call.thought_signature && existing.thought_signature.nil?
98
- existing.thought_signature = tool_call.thought_signature
99
- end
100
- end
85
+ append_tool_call_fragment(stream_key, tool_call)
101
86
  end
102
87
  end
103
88
  end
104
89
 
105
- def find_tool_call(tool_call_id)
106
- if tool_call_id.nil?
107
- @tool_calls[@latest_tool_call]
108
- else
109
- @latest_tool_call_id = tool_call_id
110
- @tool_calls[tool_call_id]
111
- end
90
+ def start_tool_call(stream_key, tool_call)
91
+ tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
92
+ tool_call_key = tool_call.id
93
+
94
+ @tool_calls[tool_call_key] = ToolCall.new(
95
+ id: tool_call_id,
96
+ name: tool_call.name,
97
+ arguments: initial_tool_call_arguments(tool_call),
98
+ thought_signature: tool_call.thought_signature
99
+ )
100
+ @tool_call_ids_by_index[stream_key] = tool_call_key unless stream_key.nil?
101
+ @latest_tool_call_id = tool_call_key
102
+ end
103
+
104
+ def initial_tool_call_arguments(tool_call)
105
+ arguments = tool_call.arguments
106
+ return +'' if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
107
+
108
+ arguments
109
+ end
110
+
111
+ def append_tool_call_fragment(stream_key, tool_call)
112
+ existing = find_tool_call(stream_key)
113
+ return unless existing
114
+
115
+ fragment = tool_call.arguments
116
+ fragment = '' if fragment.nil?
117
+ existing.arguments << fragment
118
+ return unless tool_call.thought_signature && existing.thought_signature.nil?
119
+
120
+ existing.thought_signature = tool_call.thought_signature
121
+ end
122
+
123
+ def find_tool_call(stream_key)
124
+ return @tool_calls[@latest_tool_call_id] if stream_key.nil?
125
+
126
+ @tool_calls[@tool_call_ids_by_index[stream_key]] || @tool_calls[stream_key]
112
127
  end
113
128
 
114
129
  def count_tokens(chunk)
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'event_stream_parser'
4
+ require 'faraday'
5
+ require 'json'
6
+
3
7
  module RubyLLM
4
8
  # Handles streaming responses from AI providers.
5
9
  module Streaming
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Runs multiple tool calls concurrently with Ruby's built-in threads or optional fibers.
5
+ module ToolConcurrency
6
+ MODES = %i[threads fibers].freeze
7
+ Result = Struct.new(:index, :tool_call, :value, :error, keyword_init: true)
8
+
9
+ module_function
10
+
11
+ def modes
12
+ MODES
13
+ end
14
+
15
+ def supported?(name)
16
+ MODES.include?(name)
17
+ end
18
+
19
+ def run(mode, tool_calls, on_result: nil, &)
20
+ case mode
21
+ when :threads
22
+ run_with_threads(tool_calls, on_result:, &)
23
+ when :fibers
24
+ run_with_fibers(tool_calls, on_result:, &)
25
+ end
26
+ end
27
+
28
+ def run_with_threads(tool_calls, on_result:, &execute)
29
+ executor = rails_executor
30
+ queue = Queue.new
31
+ threads = tool_calls.each_value.with_index.map do |tool_call, index|
32
+ thread = Thread.new { queue << capture_result(index, tool_call, executor, execute) }
33
+ thread.report_on_exception = false
34
+ thread
35
+ end
36
+
37
+ collect_results(queue, threads.size, on_result:)
38
+ ensure
39
+ threads&.each(&:join)
40
+ end
41
+
42
+ def run_with_fibers(tool_calls, on_result:, &execute)
43
+ begin
44
+ require 'async'
45
+ require 'async/queue'
46
+ rescue LoadError
47
+ raise LoadError, "The 'async' gem is required for concurrent tool execution with fibers. " \
48
+ "Add `gem 'async'` to your Gemfile or use `concurrency: :threads`."
49
+ end
50
+
51
+ executor = rails_executor
52
+ Async do |task|
53
+ queue = Async::Queue.new
54
+ tasks = tool_calls.each_value.with_index.map do |tool_call, index|
55
+ task.async { queue << capture_result(index, tool_call, executor, execute) }
56
+ end
57
+
58
+ collect_results(queue, tasks.size, on_result:)
59
+ ensure
60
+ tasks&.each(&:wait)
61
+ end.wait
62
+ end
63
+
64
+ def collect_results(queue, count, on_result:)
65
+ results = Array.new(count)
66
+ errors = []
67
+
68
+ count.times do
69
+ result = queue.pop
70
+ if result.error
71
+ errors << result.error
72
+ else
73
+ results[result.index] = [result.tool_call, result.value]
74
+ on_result&.call(result.tool_call, result.value)
75
+ end
76
+ end
77
+
78
+ raise errors.first if errors.any?
79
+
80
+ results
81
+ end
82
+
83
+ def capture_result(index, tool_call, rails_executor, execute)
84
+ tool_call, value = run_tool_call(tool_call, rails_executor, execute)
85
+ Result.new(index:, tool_call:, value:)
86
+ rescue Exception => e # rubocop:disable Lint/RescueException
87
+ Result.new(index:, tool_call:, error: e)
88
+ end
89
+
90
+ def run_tool_call(tool_call, rails_executor, execute)
91
+ if rails_executor
92
+ rails_executor.wrap { [tool_call, execute.call(tool_call)] }
93
+ else
94
+ [tool_call, execute.call(tool_call)]
95
+ end
96
+ end
97
+
98
+ def rails_executor
99
+ defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.executor
100
+ end
101
+
102
+ private_class_method :run_with_threads, :run_with_fibers, :collect_results, :capture_result, :run_tool_call,
103
+ :rails_executor
104
+ end
105
+ end
@@ -3,7 +3,7 @@
3
3
  module RubyLLM
4
4
  # Represents a transcription of audio content.
5
5
  class Transcription
6
- attr_reader :text, :model, :language, :duration, :segments, :input_tokens, :output_tokens
6
+ attr_reader :text, :model, :language, :duration, :segments, :words, :input_tokens, :output_tokens
7
7
 
8
8
  def initialize(text:, model:, **attributes)
9
9
  @text = text
@@ -11,6 +11,7 @@ module RubyLLM
11
11
  @language = attributes[:language]
12
12
  @duration = attributes[:duration]
13
13
  @segments = attributes[:segments]
14
+ @words = attributes[:words]
14
15
  @input_tokens = attributes[:input_tokens]
15
16
  @output_tokens = attributes[:output_tokens]
16
17
  end
@@ -32,6 +32,45 @@ module RubyLLM
32
32
  value.is_a?(Date) ? value : Date.parse(value.to_s)
33
33
  end
34
34
 
35
+ def safe_constantize(name)
36
+ parts = name.to_s.split('::').reject(&:empty?)
37
+ return if parts.empty?
38
+
39
+ namespace = Object
40
+ until parts.empty?
41
+ const_name = parts.shift
42
+ return unless namespace.const_defined?(const_name, false)
43
+
44
+ namespace = namespace.const_get(const_name, false)
45
+ end
46
+ namespace
47
+ rescue NameError
48
+ nil
49
+ end
50
+
51
+ def parse_iso_date_prefix(value)
52
+ return value if value.is_a?(Date)
53
+
54
+ date = value.to_s.strip
55
+ return if date.empty?
56
+
57
+ case date
58
+ when /\A\d{4}-\d{2}-\d{2}\z/
59
+ Date.iso8601(date)
60
+ when /\A\d{4}-\d{2}\z/
61
+ Date.iso8601("#{date}-01")
62
+ when /\A\d{4}\z/
63
+ Date.iso8601("#{date}-01-01")
64
+ end
65
+ rescue ArgumentError
66
+ nil
67
+ end
68
+
69
+ def iso_date_prefix_to_utc_midnight_string(value)
70
+ date = parse_iso_date_prefix(value)
71
+ "#{date.strftime('%Y-%m-%d')} 00:00:00 UTC" if date
72
+ end
73
+
35
74
  def deep_merge(original, overrides)
36
75
  original.merge(overrides) do |_key, original_value, overrides_value|
37
76
  if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.15.0'
4
+ VERSION = '1.16.0'
5
5
  end
data/lib/ruby_llm.rb CHANGED
@@ -12,6 +12,7 @@ require 'securerandom'
12
12
  require 'date'
13
13
  require 'time'
14
14
  require 'zeitwerk'
15
+ require 'ruby_llm/error'
15
16
 
16
17
  loader = Zeitwerk::Loader.for_gem
17
18
  loader.inflector.inflect(
@@ -39,9 +40,15 @@ loader.setup
39
40
 
40
41
  # A delightful Ruby interface to modern AI language models.
41
42
  module RubyLLM
42
- class Error < StandardError; end
43
-
44
43
  class << self
44
+ def deprecator
45
+ @deprecator ||= Deprecator.new
46
+ end
47
+
48
+ def instrument(...)
49
+ Instrumentation.instrument(...)
50
+ end
51
+
45
52
  def context
46
53
  context_config = config.dup
47
54
  yield context_config if block_given?
@@ -348,7 +348,7 @@ def pricing_part(pricing_data, key, label)
348
348
  "#{label}: $#{format('%.2f', pricing_data[key])}"
349
349
  end
350
350
 
351
- def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
351
+ def generate_aliases # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
352
352
  models = Hash.new { |h, k| h[k] = [] }
353
353
 
354
354
  RubyLLM.models.all.each do |model|
@@ -467,12 +467,40 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
467
467
  }
468
468
  end
469
469
 
470
+ add_xai_aliases(aliases, models['xai'])
471
+
470
472
  sorted_aliases = aliases.sort.to_h
471
473
  File.write(RubyLLM::Aliases.aliases_file, JSON.pretty_generate(sorted_aliases))
472
474
 
473
475
  puts "Generated #{sorted_aliases.size} aliases"
474
476
  end
475
477
 
478
+ def add_xai_aliases(aliases, xai_models)
479
+ return unless xai_models.include?('grok-4.3')
480
+
481
+ %w[
482
+ grok-latest
483
+ grok-3
484
+ grok-3-latest
485
+ grok-3-mini
486
+ grok-3-mini-latest
487
+ grok-4
488
+ grok-4-latest
489
+ grok-4-fast
490
+ grok-4-fast-reasoning
491
+ grok-4-fast-reasoning-latest
492
+ grok-4-fast-non-reasoning
493
+ grok-4-fast-non-reasoning-latest
494
+ grok-4-1-fast
495
+ grok-4-1-fast-reasoning
496
+ grok-4-1-fast-reasoning-latest
497
+ grok-4-1-fast-non-reasoning
498
+ grok-4-1-fast-non-reasoning-latest
499
+ ].each do |alias_key|
500
+ aliases[alias_key] ||= { 'xai' => 'grok-4.3' }
501
+ end
502
+ end
503
+
476
504
  def group_anthropic_models_by_base_name(anthropic_models)
477
505
  grouped = Hash.new { |h, k| h[k] = [] }
478
506
 
@@ -519,12 +547,12 @@ def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable M
519
547
  when 'claude-instant-v1', 'claude-instant'
520
548
  'claude-instant'
521
549
  else
522
- extract_base_name(anthropic_model)
550
+ anthropic_model
523
551
  end
524
552
 
525
553
  matching_models = bedrock_models.select do |bedrock_model|
526
- model_without_prefix = bedrock_model.sub(/^(?:us\.)?anthropic\./, '')
527
- model_without_prefix.start_with?(base_pattern)
554
+ model_without_prefix = bedrock_model.sub(/^(?:(?:[a-z]{2}|global)\.)?anthropic\./, '')
555
+ model_without_prefix.match?(/\A#{Regexp.escape(base_pattern)}(?:-v\d+|:\d+k|$)/)
528
556
  end
529
557
 
530
558
  return nil if matching_models.empty?
@@ -1,5 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+ require 'yaml'
5
+
6
+ # Shared helpers for release-related Rake tasks.
7
+ module ReleaseTasks
8
+ module_function
9
+
10
+ def cassette_recorded_at_times(cassette)
11
+ data = YAML.safe_load_file(cassette, aliases: true)
12
+
13
+ Array(data['http_interactions']).filter_map do |interaction|
14
+ Time.parse(interaction['recorded_at'])
15
+ rescue ArgumentError, TypeError
16
+ nil
17
+ end
18
+ rescue Psych::Exception => e
19
+ abort "Could not parse VCR cassette #{cassette}: #{e.message}"
20
+ end
21
+
22
+ def cassette_recorded_at(cassette)
23
+ recorded_at_times = cassette_recorded_at_times(cassette)
24
+
25
+ abort "No recorded_at timestamps found in VCR cassette #{cassette}" if recorded_at_times.empty?
26
+
27
+ recorded_at_times.min
28
+ end
29
+
30
+ def find_stale_cassettes(cassette_dir, max_age_days)
31
+ Dir.glob("#{cassette_dir}/**/*.yml").filter_map do |cassette|
32
+ recorded_at = cassette_recorded_at(cassette)
33
+ age_days = (Time.now - recorded_at) / 86_400
34
+
35
+ next unless age_days > max_age_days
36
+
37
+ {
38
+ path: cassette,
39
+ file: File.basename(cassette),
40
+ age: age_days.round(1)
41
+ }
42
+ end
43
+ end
44
+ end
45
+
3
46
  namespace :release do # rubocop:disable Metrics/BlockLength
4
47
  desc 'Prepare for release'
5
48
  task :prepare do
@@ -13,18 +56,15 @@ namespace :release do # rubocop:disable Metrics/BlockLength
13
56
  max_age_days = 1
14
57
  cassette_dir = 'spec/fixtures/vcr_cassettes'
15
58
 
16
- stale_count = 0
17
- Dir.glob("#{cassette_dir}/**/*.yml").each do |cassette|
18
- age_days = (Time.now - File.mtime(cassette)) / 86_400
19
- next unless age_days > max_age_days
59
+ stale_cassettes = ReleaseTasks.find_stale_cassettes(cassette_dir, max_age_days)
20
60
 
21
- puts "Removing stale cassette: #{File.basename(cassette)} (#{age_days.round(1)} days old)"
22
- File.delete(cassette)
23
- stale_count += 1
61
+ stale_cassettes.each do |cassette|
62
+ puts "Removing stale cassette: #{cassette[:file]} (#{cassette[:age]} days old)"
63
+ File.delete(cassette[:path])
24
64
  end
25
65
 
26
- if stale_count.positive?
27
- puts "\nšŸ—‘ļø Removed #{stale_count} stale cassettes"
66
+ if stale_cassettes.any?
67
+ puts "\nšŸ—‘ļø Removed #{stale_cassettes.size} stale cassettes"
28
68
  puts 'šŸ”„ Re-recording cassettes...'
29
69
  run_test_queue_rspec || exit(1)
30
70
  puts 'āœ… Cassettes refreshed!'
@@ -37,25 +77,12 @@ namespace :release do # rubocop:disable Metrics/BlockLength
37
77
  task :verify_cassettes do
38
78
  max_age_days = 1
39
79
  cassette_dir = 'spec/fixtures/vcr_cassettes'
40
- stale_cassettes = []
41
-
42
- Dir.glob("#{cassette_dir}/**/*.yml").each do |cassette|
43
- age_days = (Time.now - File.mtime(cassette)) / 86_400
44
-
45
- next unless age_days > max_age_days
46
-
47
- stale_cassettes << {
48
- file: File.basename(cassette),
49
- age: age_days.round(1)
50
- }
51
- end
80
+ stale_cassettes = ReleaseTasks.find_stale_cassettes(cassette_dir, max_age_days)
52
81
 
53
82
  if stale_cassettes.any?
54
83
  puts "\nāŒ Found stale cassettes (older than #{max_age_days} days):"
55
- stale_files = []
56
84
  stale_cassettes.each do |c|
57
85
  puts " - #{c[:file]} (#{c[:age]} days old)"
58
- stale_files << File.join(cassette_dir, '**', c[:file])
59
86
  end
60
87
 
61
88
  puts "\nRun locally: bundle exec rake release:refresh_stale_cassettes"
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: 1.15.0
4
+ version: 1.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -135,13 +135,13 @@ dependencies:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
137
  version: '2'
138
- description: One beautiful Ruby API for GPT, Claude, Gemini, and more. Easily build
139
- chatbots, AI agents, RAG applications, and content generators. Features chat (text,
140
- images, audio, PDFs), image generation, embeddings, tools (function calling), structured
141
- output, Rails integration, and streaming. Works with OpenAI, Anthropic, Google Gemini,
142
- AWS Bedrock, DeepSeek, Mistral, Ollama (local models), OpenRouter, Perplexity, GPUStack,
143
- and any OpenAI-compatible API. Minimal dependencies - just Faraday, Zeitwerk, and
144
- Marcel.
138
+ description: A single, beautiful Ruby framework for all major AI providers. Easily
139
+ build chatbots, AI agents, RAG applications, content generators, and every AI workflow
140
+ you can think of. Features chat (text, images, audio, PDFs), image generation, embeddings,
141
+ tools (function calling), structured output, Rails integration, and streaming. Works
142
+ with OpenAI, Anthropic, Google Gemini, AWS Bedrock, DeepSeek, Mistral, Ollama (local
143
+ models), OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API. Minimal
144
+ dependencies - just Faraday, Zeitwerk, and Marcel.
145
145
  email:
146
146
  - carmine@paolino.me
147
147
  executables: []
@@ -241,9 +241,12 @@ files:
241
241
  - lib/ruby_llm/content.rb
242
242
  - lib/ruby_llm/context.rb
243
243
  - lib/ruby_llm/cost.rb
244
+ - lib/ruby_llm/deprecator.rb
244
245
  - lib/ruby_llm/embedding.rb
245
246
  - lib/ruby_llm/error.rb
247
+ - lib/ruby_llm/error_middleware.rb
246
248
  - lib/ruby_llm/image.rb
249
+ - lib/ruby_llm/instrumentation.rb
247
250
  - lib/ruby_llm/message.rb
248
251
  - lib/ruby_llm/mime_type.rb
249
252
  - lib/ruby_llm/model.rb
@@ -252,6 +255,7 @@ files:
252
255
  - lib/ruby_llm/model/pricing.rb
253
256
  - lib/ruby_llm/model/pricing_category.rb
254
257
  - lib/ruby_llm/model/pricing_tier.rb
258
+ - lib/ruby_llm/model_registry.rb
255
259
  - lib/ruby_llm/models.json
256
260
  - lib/ruby_llm/models.rb
257
261
  - lib/ruby_llm/models_schema.json
@@ -299,6 +303,7 @@ files:
299
303
  - lib/ruby_llm/providers/mistral/capabilities.rb
300
304
  - lib/ruby_llm/providers/mistral/chat.rb
301
305
  - lib/ruby_llm/providers/mistral/embeddings.rb
306
+ - lib/ruby_llm/providers/mistral/media.rb
302
307
  - lib/ruby_llm/providers/mistral/models.rb
303
308
  - lib/ruby_llm/providers/ollama.rb
304
309
  - lib/ruby_llm/providers/ollama/capabilities.rb
@@ -325,6 +330,7 @@ files:
325
330
  - lib/ruby_llm/providers/perplexity.rb
326
331
  - lib/ruby_llm/providers/perplexity/capabilities.rb
327
332
  - lib/ruby_llm/providers/perplexity/chat.rb
333
+ - lib/ruby_llm/providers/perplexity/media.rb
328
334
  - lib/ruby_llm/providers/perplexity/models.rb
329
335
  - lib/ruby_llm/providers/vertexai.rb
330
336
  - lib/ruby_llm/providers/vertexai/chat.rb
@@ -342,6 +348,7 @@ files:
342
348
  - lib/ruby_llm/tokens.rb
343
349
  - lib/ruby_llm/tool.rb
344
350
  - lib/ruby_llm/tool_call.rb
351
+ - lib/ruby_llm/tool_concurrency.rb
345
352
  - lib/ruby_llm/transcription.rb
346
353
  - lib/ruby_llm/utils.rb
347
354
  - lib/ruby_llm/version.rb
@@ -391,7 +398,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
391
398
  - !ruby/object:Gem::Version
392
399
  version: '0'
393
400
  requirements: []
394
- rubygems_version: 4.0.6
401
+ rubygems_version: 4.0.10
395
402
  specification_version: 4
396
- summary: One beautiful Ruby API for GPT, Claude, Gemini, and more.
403
+ summary: A single, beautiful Ruby framework for all major AI providers.
397
404
  test_files: []