ruby_llm 1.14.1 → 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.
- checksums.yaml +4 -4
- data/README.md +1 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +52 -25
- data/lib/ruby_llm/active_record/chat_methods.rb +39 -22
- data/lib/ruby_llm/active_record/message_methods.rb +17 -1
- data/lib/ruby_llm/active_record/model_methods.rb +7 -9
- data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
- data/lib/ruby_llm/agent.rb +3 -2
- data/lib/ruby_llm/aliases.json +34 -4
- data/lib/ruby_llm/attachment.rb +11 -27
- data/lib/ruby_llm/chat.rb +62 -21
- data/lib/ruby_llm/cost.rb +224 -0
- data/lib/ruby_llm/image.rb +37 -4
- data/lib/ruby_llm/message.rb +20 -0
- data/lib/ruby_llm/model/info.rb +17 -0
- data/lib/ruby_llm/model/pricing_category.rb +13 -2
- data/lib/ruby_llm/models.json +25168 -20374
- data/lib/ruby_llm/models.rb +2 -1
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +10 -3
- data/lib/ruby_llm/providers/anthropic/tools.rb +4 -1
- data/lib/ruby_llm/providers/bedrock/chat.rb +24 -13
- data/lib/ruby_llm/providers/bedrock/streaming.rb +4 -1
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -1
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/streaming.rb +4 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +3 -1
- data/lib/ruby_llm/providers/mistral/capabilities.rb +6 -1
- data/lib/ruby_llm/providers/mistral/chat.rb +55 -4
- data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
- data/lib/ruby_llm/providers/openai/chat.rb +45 -6
- data/lib/ruby_llm/providers/openai/images.rb +58 -6
- data/lib/ruby_llm/providers/openai/streaming.rb +5 -6
- data/lib/ruby_llm/providers/openrouter/chat.rb +30 -6
- data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
- data/lib/ruby_llm/railtie.rb +6 -0
- data/lib/ruby_llm/tokens.rb +8 -0
- data/lib/ruby_llm/tool.rb +24 -7
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +2 -4
- data/lib/tasks/models.rake +13 -12
- metadata +19 -4
|
@@ -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
|
content_source = delta['content'] || data.dig('choices', 0, 'message', 'content')
|
|
19
18
|
content, thinking_from_blocks = OpenAI::Chat.extract_content_and_thinking(content_source)
|
|
@@ -27,11 +26,11 @@ module RubyLLM
|
|
|
27
26
|
signature: delta['reasoning_signature']
|
|
28
27
|
),
|
|
29
28
|
tool_calls: parse_tool_calls(delta['tool_calls'], parse_arguments: false),
|
|
30
|
-
input_tokens: usage
|
|
31
|
-
output_tokens: usage
|
|
32
|
-
cached_tokens:
|
|
33
|
-
cache_creation_tokens:
|
|
34
|
-
thinking_tokens:
|
|
29
|
+
input_tokens: OpenAI::Chat.input_tokens(usage),
|
|
30
|
+
output_tokens: OpenAI::Chat.output_tokens(usage),
|
|
31
|
+
cached_tokens: OpenAI::Chat.cache_read_tokens(usage),
|
|
32
|
+
cache_creation_tokens: OpenAI::Chat.cache_write_tokens(usage),
|
|
33
|
+
thinking_tokens: OpenAI::Chat.thinking_tokens(usage)
|
|
35
34
|
)
|
|
36
35
|
end
|
|
37
36
|
|
|
@@ -60,8 +60,7 @@ module RubyLLM
|
|
|
60
60
|
return unless message_data
|
|
61
61
|
|
|
62
62
|
usage = data['usage'] || {}
|
|
63
|
-
|
|
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
|
|
74
|
-
output_tokens: usage
|
|
75
|
-
cached_tokens:
|
|
76
|
-
cache_creation_tokens:
|
|
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: :
|
|
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
|
|
29
|
-
output_tokens: usage
|
|
30
|
-
cached_tokens:
|
|
31
|
-
cache_creation_tokens:
|
|
32
|
-
thinking_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
|
|
data/lib/ruby_llm/railtie.rb
CHANGED
|
@@ -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
|
data/lib/ruby_llm/tokens.rb
CHANGED
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 =
|
|
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 = {
|
data/lib/ruby_llm/version.rb
CHANGED
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)
|
data/lib/tasks/models.rake
CHANGED
|
@@ -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
|
|
334
|
-
parts = []
|
|
339
|
+
return parts.join(', ') if parts.any?
|
|
335
340
|
|
|
336
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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.
|
|
4
|
+
version: 1.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carmine Paolino
|
|
@@ -240,6 +240,7 @@ files:
|
|
|
240
240
|
- lib/ruby_llm/connection.rb
|
|
241
241
|
- lib/ruby_llm/content.rb
|
|
242
242
|
- lib/ruby_llm/context.rb
|
|
243
|
+
- lib/ruby_llm/cost.rb
|
|
243
244
|
- lib/ruby_llm/embedding.rb
|
|
244
245
|
- lib/ruby_llm/error.rb
|
|
245
246
|
- lib/ruby_llm/image.rb
|
|
@@ -354,14 +355,28 @@ licenses:
|
|
|
354
355
|
metadata:
|
|
355
356
|
homepage_uri: https://rubyllm.com
|
|
356
357
|
source_code_uri: https://github.com/crmne/ruby_llm
|
|
357
|
-
changelog_uri: https://github.com/crmne/ruby_llm/
|
|
358
|
+
changelog_uri: https://github.com/crmne/ruby_llm/releases
|
|
358
359
|
documentation_uri: https://rubyllm.com
|
|
359
360
|
bug_tracker_uri: https://github.com/crmne/ruby_llm/issues
|
|
360
361
|
funding_uri: https://github.com/sponsors/crmne
|
|
361
362
|
rubygems_mfa_required: 'true'
|
|
362
363
|
post_install_message: |
|
|
363
|
-
|
|
364
|
-
|
|
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/
|
|
365
380
|
rdoc_options: []
|
|
366
381
|
require_paths:
|
|
367
382
|
- lib
|