ruby_llm 1.14.1 → 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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -7
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  5. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  6. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  7. data/lib/ruby_llm/active_record/acts_as.rb +4 -26
  8. data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
  9. data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
  10. data/lib/ruby_llm/active_record/message_methods.rb +87 -4
  11. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  12. data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
  13. data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
  14. data/lib/ruby_llm/agent.rb +4 -2
  15. data/lib/ruby_llm/aliases.json +108 -75
  16. data/lib/ruby_llm/aliases.rb +3 -0
  17. data/lib/ruby_llm/attachment.rb +41 -40
  18. data/lib/ruby_llm/chat.rb +229 -59
  19. data/lib/ruby_llm/configuration.rb +14 -1
  20. data/lib/ruby_llm/connection.rb +36 -7
  21. data/lib/ruby_llm/content.rb +15 -1
  22. data/lib/ruby_llm/cost.rb +224 -0
  23. data/lib/ruby_llm/deprecator.rb +24 -0
  24. data/lib/ruby_llm/embedding.rb +31 -1
  25. data/lib/ruby_llm/error.rb +11 -75
  26. data/lib/ruby_llm/error_middleware.rb +81 -0
  27. data/lib/ruby_llm/image.rb +39 -4
  28. data/lib/ruby_llm/instrumentation.rb +36 -0
  29. data/lib/ruby_llm/message.rb +20 -0
  30. data/lib/ruby_llm/mime_type.rb +25 -0
  31. data/lib/ruby_llm/model/info.rb +53 -2
  32. data/lib/ruby_llm/model/pricing.rb +19 -9
  33. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  34. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  35. data/lib/ruby_llm/model_registry.rb +39 -0
  36. data/lib/ruby_llm/models.json +17817 -13942
  37. data/lib/ruby_llm/models.rb +97 -31
  38. data/lib/ruby_llm/models_schema.json +3 -0
  39. data/lib/ruby_llm/provider.rb +20 -4
  40. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  41. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  42. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  43. data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
  44. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  45. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  46. data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
  47. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  48. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  49. data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
  50. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  51. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  52. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  53. data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
  54. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  55. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  56. data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
  57. data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
  58. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  59. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  60. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
  61. data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
  62. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  63. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  64. data/lib/ruby_llm/providers/mistral.rb +2 -2
  65. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  66. data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
  67. data/lib/ruby_llm/providers/openai/chat.rb +61 -7
  68. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  69. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  70. data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
  71. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  72. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  73. data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
  74. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  75. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  76. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  77. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  78. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  79. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  80. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  81. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  82. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  83. data/lib/ruby_llm/providers/xai.rb +2 -2
  84. data/lib/ruby_llm/railtie.rb +11 -1
  85. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  86. data/lib/ruby_llm/streaming.rb +4 -0
  87. data/lib/ruby_llm/tokens.rb +8 -0
  88. data/lib/ruby_llm/tool.rb +24 -7
  89. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  90. data/lib/ruby_llm/transcription.rb +2 -1
  91. data/lib/ruby_llm/utils.rb +39 -0
  92. data/lib/ruby_llm/version.rb +1 -1
  93. data/lib/ruby_llm.rb +11 -6
  94. data/lib/tasks/models.rake +45 -16
  95. data/lib/tasks/release.rake +50 -23
  96. metadata +35 -13
@@ -5,7 +5,7 @@ module RubyLLM
5
5
  # Information about an AI model's capabilities, pricing, and metadata.
6
6
  class Info
7
7
  attr_reader :id, :name, :provider, :family, :created_at, :context_window, :max_output_tokens, :knowledge_cutoff,
8
- :modalities, :capabilities, :pricing, :metadata
8
+ :modalities, :capabilities, :pricing, :metadata, :reasoning_options
9
9
 
10
10
  # Create a default model with assumed capabilities
11
11
  def self.default(model_id, provider)
@@ -31,7 +31,9 @@ module RubyLLM
31
31
  @modalities = Modalities.new(data[:modalities] || {})
32
32
  @capabilities = data[:capabilities] || []
33
33
  @pricing = Pricing.new(data[:pricing] || {})
34
- @metadata = data[:metadata] || {}
34
+ @metadata = data[:metadata]&.dup || {}
35
+ @reasoning_options = normalize_reasoning_options(reasoning_options_from(data))
36
+ store_reasoning_options_metadata
35
37
  end
36
38
 
37
39
  def supports?(capability)
@@ -61,6 +63,14 @@ module RubyLLM
61
63
  modalities.input.include?('image')
62
64
  end
63
65
 
66
+ def reasoning_option(type)
67
+ reasoning_options.find { |option| option[:type] == type.to_s }
68
+ end
69
+
70
+ def reasoning_option_values(type)
71
+ Array(reasoning_option(type)&.fetch(:values, nil))
72
+ end
73
+
64
74
  def supports_video?
65
75
  modalities.input.include?('video')
66
76
  end
@@ -77,6 +87,23 @@ module RubyLLM
77
87
  pricing.text_tokens.output
78
88
  end
79
89
 
90
+ def cache_read_input_price_per_million
91
+ pricing.text_tokens.cache_read_input
92
+ end
93
+
94
+ def cache_write_input_price_per_million
95
+ pricing.text_tokens.cache_write_input
96
+ end
97
+
98
+ alias cached_input_price_per_million cache_read_input_price_per_million
99
+ alias cache_creation_input_price_per_million cache_write_input_price_per_million
100
+
101
+ def cost_for(tokens)
102
+ tokens = tokens.tokens if tokens.respond_to?(:tokens)
103
+
104
+ Cost.new(tokens:, model: self)
105
+ end
106
+
80
107
  def provider_class
81
108
  RubyLLM::Provider.resolve provider
82
109
  end
@@ -108,6 +135,30 @@ module RubyLLM
108
135
  metadata: metadata
109
136
  }
110
137
  end
138
+
139
+ private
140
+
141
+ def reasoning_options_from(data)
142
+ data[:reasoning_options] || metadata[:reasoning_options] || metadata['reasoning_options']
143
+ end
144
+
145
+ def store_reasoning_options_metadata
146
+ return unless reasoning_options.any?
147
+
148
+ metadata.delete('reasoning_options')
149
+ metadata[:reasoning_options] = reasoning_options
150
+ end
151
+
152
+ def normalize_reasoning_options(options)
153
+ Array(options).filter_map do |option|
154
+ next unless option.is_a?(Hash)
155
+
156
+ normalized = option.to_h.transform_keys(&:to_sym)
157
+ normalized[:type] = normalized[:type].to_s if normalized[:type]
158
+ normalized[:values] = Array(normalized[:values]).map(&:to_s) if normalized.key?(:values)
159
+ normalized
160
+ end
161
+ end
111
162
  end
112
163
  end
113
164
  end
@@ -4,24 +4,30 @@ module RubyLLM
4
4
  module Model
5
5
  # A collection that manages and provides access to different categories of pricing information
6
6
  class Pricing
7
+ CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
8
+
7
9
  def initialize(data)
8
10
  @data = {}
9
11
 
10
- %i[text_tokens images audio_tokens embeddings].each do |category|
12
+ CATEGORIES.each do |category|
11
13
  @data[category] = PricingCategory.new(data[category]) if data[category] && !empty_pricing?(data[category])
12
14
  end
13
15
  end
14
16
 
15
- def method_missing(method, *args)
16
- if respond_to_missing?(method)
17
- @data[method.to_sym] || PricingCategory.new
18
- else
19
- super
20
- end
17
+ def text_tokens
18
+ category(:text_tokens)
19
+ end
20
+
21
+ def images
22
+ category(:images)
21
23
  end
22
24
 
23
- def respond_to_missing?(method, include_private = false)
24
- %i[text_tokens images audio_tokens embeddings].include?(method.to_sym) || super
25
+ def audio_tokens
26
+ category(:audio_tokens)
27
+ end
28
+
29
+ def embeddings
30
+ category(:embeddings)
25
31
  end
26
32
 
27
33
  def to_h
@@ -30,6 +36,10 @@ module RubyLLM
30
36
 
31
37
  private
32
38
 
39
+ def category(name)
40
+ @data[name] || PricingCategory.new
41
+ end
42
+
33
43
  def empty_pricing?(data)
34
44
  return true unless data
35
45
 
@@ -19,10 +19,21 @@ module RubyLLM
19
19
  standard&.output_per_million
20
20
  end
21
21
 
22
- def cached_input
23
- standard&.cached_input_per_million
22
+ def cache_read_input
23
+ standard&.cache_read_input_per_million || standard&.cached_input_per_million
24
24
  end
25
25
 
26
+ def cache_write_input
27
+ standard&.cache_write_input_per_million || standard&.cache_creation_input_per_million
28
+ end
29
+
30
+ def reasoning_output
31
+ standard&.reasoning_output_per_million
32
+ end
33
+
34
+ alias cached_input cache_read_input
35
+ alias cache_creation_input cache_write_input
36
+
26
37
  def [](key)
27
38
  key == :batch ? batch : standard
28
39
  end
@@ -2,8 +2,18 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Model
5
- # A dynamic class for storing non-zero pricing values with flexible attribute access
5
+ # Stores non-zero pricing values for a single pricing tier.
6
6
  class PricingTier
7
+ ATTRIBUTES = %i[
8
+ input_per_million
9
+ output_per_million
10
+ cache_read_input_per_million
11
+ cache_write_input_per_million
12
+ cached_input_per_million
13
+ cache_creation_input_per_million
14
+ reasoning_output_per_million
15
+ ].freeze
16
+
7
17
  def initialize(data = {})
8
18
  @values = {}
9
19
 
@@ -12,17 +22,18 @@ module RubyLLM
12
22
  end
13
23
  end
14
24
 
15
- def method_missing(method, *args)
16
- if method.to_s.end_with?('=')
17
- key = method.to_s.chomp('=').to_sym
18
- @values[key] = args.first if args.first && args.first != 0.0
19
- elsif @values.key?(method)
20
- @values[method]
25
+ ATTRIBUTES.each do |attribute|
26
+ define_method(attribute) do
27
+ @values[attribute]
28
+ end
29
+
30
+ define_method("#{attribute}=") do |value|
31
+ @values[attribute] = value if value && value != 0.0
21
32
  end
22
33
  end
23
34
 
24
- def respond_to_missing?(method, include_private = false)
25
- method.to_s.end_with?('=') || @values.key?(method.to_sym) || super
35
+ def [](key)
36
+ @values[key.to_sym]
26
37
  end
27
38
 
28
39
  def to_h
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Sources for model registry data.
5
+ module ModelRegistry
6
+ # Reads model registry data from the configured JSON file.
7
+ class JsonSource
8
+ def initialize(file = nil)
9
+ @file = file
10
+ end
11
+
12
+ def read
13
+ Models.read_from_json(@file || RubyLLM.config.model_registry_file)
14
+ end
15
+ end
16
+
17
+ # Reads model registry data from the configured Active Record model class.
18
+ class ActiveRecordSource
19
+ def read
20
+ model_class = resolve_model_class
21
+ return [] unless model_class.respond_to?(:table_exists?) && model_class.table_exists?
22
+
23
+ model_class.all.map(&:to_llm)
24
+ rescue StandardError => e
25
+ RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
26
+ []
27
+ end
28
+
29
+ private
30
+
31
+ def resolve_model_class
32
+ model_class = RubyLLM.config.model_registry_class
33
+ return model_class unless model_class.is_a?(String)
34
+
35
+ model_class.split('::').inject(Object) { |scope, name| scope.const_get(name) }
36
+ end
37
+ end
38
+ end
39
+ end