ruby_llm 1.14.0 → 1.15.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -5
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  5. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  6. data/lib/ruby_llm/active_record/acts_as_legacy.rb +52 -25
  7. data/lib/ruby_llm/active_record/chat_methods.rb +47 -23
  8. data/lib/ruby_llm/active_record/message_methods.rb +19 -14
  9. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  10. data/lib/ruby_llm/active_record/payload_helpers.rb +29 -0
  11. data/lib/ruby_llm/active_record/tool_call_methods.rb +5 -15
  12. data/lib/ruby_llm/agent.rb +3 -2
  13. data/lib/ruby_llm/aliases.json +53 -14
  14. data/lib/ruby_llm/attachment.rb +11 -27
  15. data/lib/ruby_llm/chat.rb +62 -21
  16. data/lib/ruby_llm/cost.rb +224 -0
  17. data/lib/ruby_llm/image.rb +37 -4
  18. data/lib/ruby_llm/message.rb +20 -0
  19. data/lib/ruby_llm/model/info.rb +17 -0
  20. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  21. data/lib/ruby_llm/models.json +26511 -24930
  22. data/lib/ruby_llm/models.rb +2 -1
  23. data/lib/ruby_llm/models_schema.json +3 -0
  24. data/lib/ruby_llm/provider.rb +10 -3
  25. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
  26. data/lib/ruby_llm/providers/anthropic/models.rb +2 -8
  27. data/lib/ruby_llm/providers/anthropic/tools.rb +4 -1
  28. data/lib/ruby_llm/providers/bedrock/chat.rb +24 -13
  29. data/lib/ruby_llm/providers/bedrock/streaming.rb +4 -1
  30. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -119
  31. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
  32. data/lib/ruby_llm/providers/gemini/chat.rb +8 -1
  33. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  34. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  35. data/lib/ruby_llm/providers/gemini/streaming.rb +4 -1
  36. data/lib/ruby_llm/providers/gemini/tools.rb +3 -1
  37. data/lib/ruby_llm/providers/mistral/capabilities.rb +6 -1
  38. data/lib/ruby_llm/providers/mistral/chat.rb +55 -4
  39. data/lib/ruby_llm/providers/openai/capabilities.rb +157 -195
  40. data/lib/ruby_llm/providers/openai/chat.rb +45 -6
  41. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  42. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  43. data/lib/ruby_llm/providers/openai/streaming.rb +5 -6
  44. data/lib/ruby_llm/providers/openrouter/chat.rb +30 -6
  45. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  46. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  47. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  48. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  49. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  50. data/lib/ruby_llm/railtie.rb +6 -0
  51. data/lib/ruby_llm/tokens.rb +8 -0
  52. data/lib/ruby_llm/tool.rb +24 -7
  53. data/lib/ruby_llm/version.rb +1 -1
  54. data/lib/ruby_llm.rb +2 -4
  55. data/lib/tasks/models.rake +13 -12
  56. metadata +21 -5
@@ -60,8 +60,7 @@ module RubyLLM
60
60
  return unless message_data
61
61
 
62
62
  usage = data['usage'] || {}
63
- cached_tokens = usage.dig('prompt_tokens_details', 'cached_tokens')
64
- thinking_tokens = usage.dig('completion_tokens_details', 'reasoning_tokens')
63
+ thinking_tokens = thinking_tokens(usage)
65
64
  thinking_text = extract_thinking_text(message_data)
66
65
  thinking_signature = extract_thinking_signature(message_data)
67
66
 
@@ -70,16 +69,41 @@ module RubyLLM
70
69
  content: message_data['content'],
71
70
  thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
72
71
  tool_calls: OpenAI::Tools.parse_tool_calls(message_data['tool_calls']),
73
- input_tokens: usage['prompt_tokens'],
74
- output_tokens: usage['completion_tokens'],
75
- cached_tokens: cached_tokens,
76
- cache_creation_tokens: 0,
72
+ input_tokens: input_tokens(usage),
73
+ output_tokens: output_tokens(usage),
74
+ cached_tokens: cache_read_tokens(usage),
75
+ cache_creation_tokens: cache_write_tokens(usage),
77
76
  thinking_tokens: thinking_tokens,
78
77
  model_id: data['model'],
79
78
  raw: response
80
79
  )
81
80
  end
82
81
 
82
+ def input_tokens(usage)
83
+ return usage['prompt_cache_miss_tokens'] if usage['prompt_cache_miss_tokens']
84
+
85
+ prompt_tokens = usage['prompt_tokens']
86
+ return unless prompt_tokens
87
+
88
+ [prompt_tokens.to_i - cache_read_tokens(usage).to_i - cache_write_tokens(usage).to_i, 0].max
89
+ end
90
+
91
+ def output_tokens(usage)
92
+ OpenAI::Chat.output_tokens(usage)
93
+ end
94
+
95
+ def cache_read_tokens(usage)
96
+ usage.dig('prompt_tokens_details', 'cached_tokens') || usage['prompt_cache_hit_tokens']
97
+ end
98
+
99
+ def cache_write_tokens(usage)
100
+ usage.dig('prompt_tokens_details', 'cache_write_tokens') || 0
101
+ end
102
+
103
+ def thinking_tokens(usage)
104
+ OpenAI::Chat.thinking_tokens(usage)
105
+ end
106
+
83
107
  def format_messages(messages)
84
108
  messages.map do |msg|
85
109
  {
@@ -9,11 +9,11 @@ module RubyLLM
9
9
  module Images
10
10
  module_function
11
11
 
12
- def images_url
12
+ def images_url(with: nil, mask: nil) # rubocop:disable Lint/UnusedMethodArgument
13
13
  'chat/completions'
14
14
  end
15
15
 
16
- def render_image_payload(prompt, model:, size:)
16
+ def render_image_payload(prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists
17
17
  RubyLLM.logger.debug { "Ignoring size #{size}. OpenRouter image generation does not support size parameter." }
18
18
  {
19
19
  model: model,
@@ -23,7 +23,7 @@ module RubyLLM
23
23
  pricing_types = {
24
24
  prompt: :input_per_million,
25
25
  completion: :output_per_million,
26
- input_cache_read: :cached_input_per_million,
26
+ input_cache_read: :cache_read_input_per_million,
27
27
  internal_reasoning: :reasoning_output_per_million
28
28
  }
29
29
 
@@ -13,7 +13,6 @@ module RubyLLM
13
13
 
14
14
  def build_chunk(data)
15
15
  usage = data['usage'] || {}
16
- cached_tokens = usage.dig('prompt_tokens_details', 'cached_tokens')
17
16
  delta = data.dig('choices', 0, 'delta') || {}
18
17
 
19
18
  Chunk.new(
@@ -25,11 +24,11 @@ module RubyLLM
25
24
  signature: extract_thinking_signature(delta)
26
25
  ),
27
26
  tool_calls: OpenAI::Tools.parse_tool_calls(delta['tool_calls'], parse_arguments: false),
28
- input_tokens: usage['prompt_tokens'],
29
- output_tokens: usage['completion_tokens'],
30
- cached_tokens: cached_tokens,
31
- cache_creation_tokens: 0,
32
- thinking_tokens: usage.dig('completion_tokens_details', 'reasoning_tokens')
27
+ input_tokens: OpenRouter::Chat.input_tokens(usage),
28
+ output_tokens: OpenRouter::Chat.output_tokens(usage),
29
+ cached_tokens: OpenRouter::Chat.cache_read_tokens(usage),
30
+ cache_creation_tokens: OpenRouter::Chat.cache_write_tokens(usage),
31
+ thinking_tokens: OpenRouter::Chat.thinking_tokens(usage)
33
32
  )
34
33
  end
35
34
 
@@ -3,63 +3,55 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Perplexity
6
- # Determines capabilities and pricing for Perplexity models
6
+ # Provider-level capability checks and narrow registry fallbacks.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def context_window_for(model_id)
11
- case model_id
12
- when /sonar-pro/ then 200_000
13
- else 128_000
14
- end
15
- end
10
+ PRICES = {
11
+ sonar: { input: 1.0, output: 1.0 },
12
+ sonar_pro: { input: 3.0, output: 15.0 },
13
+ sonar_reasoning: { input: 1.0, output: 5.0 },
14
+ sonar_reasoning_pro: { input: 2.0, output: 8.0 },
15
+ sonar_deep_research: {
16
+ input: 2.0,
17
+ output: 8.0,
18
+ reasoning_output: 3.0
19
+ }
20
+ }.freeze
16
21
 
17
- def max_tokens_for(model_id)
18
- case model_id
19
- when /sonar-(?:pro|reasoning-pro)/ then 8_192
20
- else 4_096
21
- end
22
+ def supports_tool_choice?(_model_id)
23
+ false
22
24
  end
23
25
 
24
- def input_price_for(model_id)
25
- PRICES.dig(model_family(model_id), :input) || 1.0
26
+ def supports_tool_parallel_control?(_model_id)
27
+ false
26
28
  end
27
29
 
28
- def output_price_for(model_id)
29
- PRICES.dig(model_family(model_id), :output) || 1.0
30
+ def context_window_for(model_id)
31
+ model_id.match?(/sonar-pro/) ? 200_000 : 128_000
30
32
  end
31
33
 
32
- def supports_vision?(model_id)
33
- case model_id
34
- when /sonar-reasoning-pro/, /sonar-reasoning/, /sonar-pro/, /sonar/ then true
35
- else false
36
- end
34
+ def max_tokens_for(model_id)
35
+ model_id.match?(/sonar-(?:pro|reasoning-pro)/) ? 8_192 : 4_096
37
36
  end
38
37
 
39
- def supports_functions?(_model_id)
40
- false
38
+ def critical_capabilities_for(model_id)
39
+ capabilities = []
40
+ capabilities << 'vision' if model_id.match?(/sonar(?:-pro|-reasoning(?:-pro)?)?$/)
41
+ capabilities << 'reasoning' if model_id.match?(/reasoning|deep-research/)
42
+ capabilities
41
43
  end
42
44
 
43
- def supports_json_mode?(_model_id)
44
- true
45
- end
45
+ def pricing_for(model_id)
46
+ prices = PRICES.fetch(model_family(model_id), { input: 1.0, output: 1.0 })
46
47
 
47
- def format_display_name(model_id)
48
- case model_id
49
- when 'sonar' then 'Sonar'
50
- when 'sonar-pro' then 'Sonar Pro'
51
- when 'sonar-reasoning' then 'Sonar Reasoning'
52
- when 'sonar-reasoning-pro' then 'Sonar Reasoning Pro'
53
- when 'sonar-deep-research' then 'Sonar Deep Research'
54
- else
55
- model_id.split('-')
56
- .map(&:capitalize)
57
- .join(' ')
58
- end
59
- end
48
+ standard = {
49
+ input_per_million: prices[:input],
50
+ output_per_million: prices[:output]
51
+ }
52
+ standard[:reasoning_output_per_million] = prices[:reasoning_output] if prices[:reasoning_output]
60
53
 
61
- def model_type(_model_id)
62
- 'chat'
54
+ { text_tokens: { standard: standard } }
63
55
  end
64
56
 
65
57
  def model_family(model_id)
@@ -73,64 +65,7 @@ module RubyLLM
73
65
  end
74
66
  end
75
67
 
76
- def modalities_for(_model_id)
77
- {
78
- input: ['text'],
79
- output: ['text']
80
- }
81
- end
82
-
83
- def capabilities_for(model_id)
84
- capabilities = %w[streaming json_mode]
85
- capabilities << 'vision' if supports_vision?(model_id)
86
- capabilities
87
- end
88
-
89
- def pricing_for(model_id)
90
- family = model_family(model_id)
91
- prices = PRICES.fetch(family, { input: 1.0, output: 1.0 })
92
-
93
- standard_pricing = {
94
- input_per_million: prices[:input],
95
- output_per_million: prices[:output]
96
- }
97
-
98
- standard_pricing[:citation_per_million] = prices[:citation] if prices[:citation]
99
- standard_pricing[:reasoning_per_million] = prices[:reasoning] if prices[:reasoning]
100
- standard_pricing[:search_per_thousand] = prices[:search_queries] if prices[:search_queries]
101
-
102
- {
103
- text_tokens: {
104
- standard: standard_pricing
105
- }
106
- }
107
- end
108
-
109
- PRICES = {
110
- sonar: {
111
- input: 1.0,
112
- output: 1.0
113
- },
114
- sonar_pro: {
115
- input: 3.0,
116
- output: 15.0
117
- },
118
- sonar_reasoning: {
119
- input: 1.0,
120
- output: 5.0
121
- },
122
- sonar_reasoning_pro: {
123
- input: 2.0,
124
- output: 8.0
125
- },
126
- sonar_deep_research: {
127
- input: 2.0,
128
- output: 8.0,
129
- citation: 2.0,
130
- reasoning: 3.0,
131
- search_queries: 5.0
132
- }
133
- }.freeze
68
+ module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for, :model_family
134
69
  end
135
70
  end
136
71
  end
@@ -5,33 +5,31 @@ module RubyLLM
5
5
  class Perplexity
6
6
  # Models methods of the Perplexity API integration
7
7
  module Models
8
+ MODEL_IDS = %w[
9
+ sonar
10
+ sonar-pro
11
+ sonar-reasoning
12
+ sonar-reasoning-pro
13
+ sonar-deep-research
14
+ ].freeze
15
+
8
16
  def list_models(**)
9
17
  slug = 'perplexity'
10
- capabilities = Perplexity::Capabilities
11
- parse_list_models_response(nil, slug, capabilities)
18
+ parse_list_models_response(nil, slug, Perplexity::Capabilities)
12
19
  end
13
20
 
14
21
  def parse_list_models_response(_response, slug, capabilities)
15
- [
16
- create_model_info('sonar', slug, capabilities),
17
- create_model_info('sonar-pro', slug, capabilities),
18
- create_model_info('sonar-reasoning', slug, capabilities),
19
- create_model_info('sonar-reasoning-pro', slug, capabilities),
20
- create_model_info('sonar-deep-research', slug, capabilities)
21
- ]
22
+ MODEL_IDS.map { |id| create_model_info(id, slug, capabilities) }
22
23
  end
23
24
 
24
25
  def create_model_info(id, slug, capabilities)
25
26
  Model::Info.new(
26
27
  id: id,
27
- name: capabilities.format_display_name(id),
28
+ name: id,
28
29
  provider: slug,
29
- family: capabilities.model_family(id).to_s,
30
- created_at: Time.now,
31
30
  context_window: capabilities.context_window_for(id),
32
31
  max_output_tokens: capabilities.max_tokens_for(id),
33
- modalities: capabilities.modalities_for(id),
34
- capabilities: capabilities.capabilities_for(id),
32
+ capabilities: capabilities.critical_capabilities_for(id),
35
33
  pricing: capabilities.pricing_for(id),
36
34
  metadata: {}
37
35
  )
@@ -12,6 +12,12 @@ if defined?(Rails::Railtie)
12
12
 
13
13
  initializer 'ruby_llm.active_record' do
14
14
  ActiveSupport.on_load :active_record do
15
+ require 'ruby_llm/active_record/payload_helpers'
16
+ require 'ruby_llm/active_record/chat_methods'
17
+ require 'ruby_llm/active_record/message_methods'
18
+ require 'ruby_llm/active_record/model_methods'
19
+ require 'ruby_llm/active_record/tool_call_methods'
20
+
15
21
  if RubyLLM.config.use_new_acts_as
16
22
  require 'ruby_llm/active_record/acts_as'
17
23
  ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
@@ -43,5 +43,13 @@ module RubyLLM
43
43
  def reasoning
44
44
  thinking
45
45
  end
46
+
47
+ def cache_read
48
+ cached
49
+ end
50
+
51
+ def cache_write
52
+ cache_creation
53
+ end
46
54
  end
47
55
  end
data/lib/ruby_llm/tool.rb CHANGED
@@ -7,10 +7,10 @@ module RubyLLM
7
7
  class Parameter
8
8
  attr_reader :name, :type, :description, :required
9
9
 
10
- def initialize(name, type: 'string', desc: nil, required: true)
10
+ def initialize(name, type: 'string', desc: nil, description: nil, required: true)
11
11
  @name = name
12
12
  @type = type
13
- @description = desc
13
+ @description = desc || description
14
14
  @required = required
15
15
  end
16
16
  end
@@ -30,6 +30,8 @@ module RubyLLM
30
30
  end
31
31
  end
32
32
 
33
+ POSITIONAL_PARAMETER_KINDS = %i[req opt rest].freeze
34
+
33
35
  class << self
34
36
  attr_reader :params_schema_definition
35
37
 
@@ -38,6 +40,7 @@ module RubyLLM
38
40
 
39
41
  @description = text
40
42
  end
43
+ alias desc description
41
44
 
42
45
  def param(name, **options)
43
46
  parameters[name] = Parameter.new(name, **options)
@@ -94,6 +97,8 @@ module RubyLLM
94
97
  definition.json_schema
95
98
  elsif parameters.any?
96
99
  SchemaDefinition.from_parameters(parameters)&.json_schema
100
+ else
101
+ SchemaDefinition.from_parameters(inferred_parameters, allow_empty: true)&.json_schema
97
102
  end
98
103
  end
99
104
  end
@@ -127,9 +132,10 @@ module RubyLLM
127
132
  end
128
133
 
129
134
  def validate_keyword_arguments(arguments)
130
- required_keywords, optional_keywords, accepts_extra_keywords = execute_keyword_signature
135
+ required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments =
136
+ execute_keyword_signature
131
137
 
132
- return nil if required_keywords.empty? && optional_keywords.empty?
138
+ return nil if required_keywords.empty? && optional_keywords.empty? && accepts_positional_arguments
133
139
 
134
140
  argument_keys = arguments.keys
135
141
  missing_keyword = first_missing_keyword(required_keywords, argument_keys)
@@ -148,8 +154,11 @@ module RubyLLM
148
154
  required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
149
155
  optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
150
156
  accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
157
+ accepts_positional_arguments = keyword_signature.any? do |kind, _|
158
+ POSITIONAL_PARAMETER_KINDS.include?(kind)
159
+ end
151
160
 
152
- [required_keywords, optional_keywords, accepts_extra_keywords]
161
+ [required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments]
153
162
  end
154
163
 
155
164
  def first_missing_keyword(required_keywords, argument_keys)
@@ -160,11 +169,19 @@ module RubyLLM
160
169
  (argument_keys - allowed_keywords).first
161
170
  end
162
171
 
172
+ def inferred_parameters
173
+ required_keywords, optional_keywords, = execute_keyword_signature
174
+
175
+ (required_keywords + optional_keywords).to_h do |name|
176
+ [name, Parameter.new(name, required: required_keywords.include?(name))]
177
+ end
178
+ end
179
+
163
180
  # Wraps schema handling for tool parameters, supporting JSON Schema hashes,
164
181
  # RubyLLM::Schema instances/classes, and DSL blocks.
165
182
  class SchemaDefinition
166
- def self.from_parameters(parameters)
167
- return nil if parameters.nil? || parameters.empty?
183
+ def self.from_parameters(parameters, allow_empty: false)
184
+ return nil if parameters.nil? || (parameters.empty? && !allow_empty)
168
185
 
169
186
  properties = parameters.to_h do |name, param|
170
187
  schema = {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.14.0'
4
+ VERSION = '1.15.0'
5
5
  end
data/lib/ruby_llm.rb CHANGED
@@ -33,6 +33,7 @@ loader.inflector.inflect(
33
33
  )
34
34
  loader.ignore("#{__dir__}/tasks")
35
35
  loader.ignore("#{__dir__}/generators")
36
+ loader.ignore("#{__dir__}/ruby_llm/active_record")
36
37
  loader.ignore("#{__dir__}/ruby_llm/railtie.rb")
37
38
  loader.setup
38
39
 
@@ -107,7 +108,4 @@ RubyLLM::Provider.register :perplexity, RubyLLM::Providers::Perplexity
107
108
  RubyLLM::Provider.register :vertexai, RubyLLM::Providers::VertexAI
108
109
  RubyLLM::Provider.register :xai, RubyLLM::Providers::XAI
109
110
 
110
- if defined?(Rails::Railtie)
111
- require 'ruby_llm/railtie'
112
- require 'ruby_llm/active_record/acts_as'
113
- end
111
+ require 'ruby_llm/railtie' if defined?(Rails::Railtie)
@@ -329,22 +329,23 @@ end
329
329
 
330
330
  def standard_pricing_display(model)
331
331
  pricing_data = model.pricing.to_h[:text_tokens]&.dig(:standard) || {}
332
+ parts = [
333
+ pricing_part(pricing_data, :input_per_million, 'In'),
334
+ pricing_part(pricing_data, :output_per_million, 'Out'),
335
+ pricing_part(pricing_data, %i[cache_read_input_per_million cached_input_per_million], 'Cache Read'),
336
+ pricing_part(pricing_data, %i[cache_write_input_per_million cache_creation_input_per_million], 'Cache Write')
337
+ ].compact
332
338
 
333
- if pricing_data.any?
334
- parts = []
339
+ return parts.join(', ') if parts.any?
335
340
 
336
- parts << "In: $#{format('%.2f', pricing_data[:input_per_million])}" if pricing_data[:input_per_million]
337
-
338
- parts << "Out: $#{format('%.2f', pricing_data[:output_per_million])}" if pricing_data[:output_per_million]
339
-
340
- if pricing_data[:cached_input_per_million]
341
- parts << "Cache: $#{format('%.2f', pricing_data[:cached_input_per_million])}"
342
- end
341
+ '-'
342
+ end
343
343
 
344
- return parts.join(', ') if parts.any?
345
- end
344
+ def pricing_part(pricing_data, key, label)
345
+ key = Array(key).find { |candidate| pricing_data[candidate] }
346
+ return unless key
346
347
 
347
- '-'
348
+ "#{label}: $#{format('%.2f', pricing_data[key])}"
348
349
  end
349
350
 
350
351
  def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
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.14.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -228,6 +228,7 @@ files:
228
228
  - lib/ruby_llm/active_record/chat_methods.rb
229
229
  - lib/ruby_llm/active_record/message_methods.rb
230
230
  - lib/ruby_llm/active_record/model_methods.rb
231
+ - lib/ruby_llm/active_record/payload_helpers.rb
231
232
  - lib/ruby_llm/active_record/tool_call_methods.rb
232
233
  - lib/ruby_llm/agent.rb
233
234
  - lib/ruby_llm/aliases.json
@@ -239,6 +240,7 @@ files:
239
240
  - lib/ruby_llm/connection.rb
240
241
  - lib/ruby_llm/content.rb
241
242
  - lib/ruby_llm/context.rb
243
+ - lib/ruby_llm/cost.rb
242
244
  - lib/ruby_llm/embedding.rb
243
245
  - lib/ruby_llm/error.rb
244
246
  - lib/ruby_llm/image.rb
@@ -353,14 +355,28 @@ licenses:
353
355
  metadata:
354
356
  homepage_uri: https://rubyllm.com
355
357
  source_code_uri: https://github.com/crmne/ruby_llm
356
- changelog_uri: https://github.com/crmne/ruby_llm/commits/main
358
+ changelog_uri: https://github.com/crmne/ruby_llm/releases
357
359
  documentation_uri: https://rubyllm.com
358
360
  bug_tracker_uri: https://github.com/crmne/ruby_llm/issues
359
361
  funding_uri: https://github.com/sponsors/crmne
360
362
  rubygems_mfa_required: 'true'
361
363
  post_install_message: |
362
- Upgrading from RubyLLM < 1.14.x? Check the upgrade guide for new features and migration instructions
363
- --> https://rubyllm.com/upgrading/
364
+ RubyLLM 1.15 upgrade note:
365
+
366
+ Token accounting is now normalized across providers. `input_tokens` means
367
+ standard input tokens; prompt cache reads and writes are exposed separately
368
+ as `cache_read_tokens` and `cache_write_tokens`.
369
+
370
+ Need request-side input activity?
371
+ input_tokens + cache_read_tokens + cache_write_tokens
372
+
373
+ New cost helpers:
374
+ response.cost.total
375
+ chat.cost.total
376
+ agent.cost.total
377
+
378
+ Upgrading from RubyLLM < 1.15? Read the full upgrade guide:
379
+ https://rubyllm.com/upgrading/
364
380
  rdoc_options: []
365
381
  require_paths:
366
382
  - lib
@@ -375,7 +391,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
375
391
  - !ruby/object:Gem::Version
376
392
  version: '0'
377
393
  requirements: []
378
- rubygems_version: 4.0.3
394
+ rubygems_version: 4.0.6
379
395
  specification_version: 4
380
396
  summary: One beautiful Ruby API for GPT, Claude, Gemini, and more.
381
397
  test_files: []