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
data/lib/ruby_llm/chat.rb
CHANGED
|
@@ -30,6 +30,7 @@ module RubyLLM
|
|
|
30
30
|
tool_call: nil,
|
|
31
31
|
tool_result: nil
|
|
32
32
|
}
|
|
33
|
+
@callbacks = Hash.new { |callbacks, name| callbacks[name] = [] }
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def ask(message = nil, with: nil, &)
|
|
@@ -112,31 +113,47 @@ module RubyLLM
|
|
|
112
113
|
self
|
|
113
114
|
end
|
|
114
115
|
|
|
115
|
-
def on_new_message(&
|
|
116
|
-
|
|
117
|
-
self
|
|
116
|
+
def on_new_message(&)
|
|
117
|
+
set_legacy_callback(:new_message, :on_new_message, :before_message, &)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
def on_end_message(&
|
|
121
|
-
|
|
122
|
-
self
|
|
120
|
+
def on_end_message(&)
|
|
121
|
+
set_legacy_callback(:end_message, :on_end_message, :after_message, &)
|
|
123
122
|
end
|
|
124
123
|
|
|
125
|
-
def on_tool_call(&
|
|
126
|
-
|
|
127
|
-
self
|
|
124
|
+
def on_tool_call(&)
|
|
125
|
+
set_legacy_callback(:tool_call, :on_tool_call, :before_tool_call, &)
|
|
128
126
|
end
|
|
129
127
|
|
|
130
|
-
def on_tool_result(&
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
def on_tool_result(&)
|
|
129
|
+
set_legacy_callback(:tool_result, :on_tool_result, :after_tool_result, &)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def before_message(&)
|
|
133
|
+
add_callback(:before_message, &)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def after_message(&)
|
|
137
|
+
add_callback(:after_message, &)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def before_tool_call(&)
|
|
141
|
+
add_callback(:before_tool_call, &)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def after_tool_result(&)
|
|
145
|
+
add_callback(:after_tool_result, &)
|
|
133
146
|
end
|
|
134
147
|
|
|
135
148
|
def each(&)
|
|
136
149
|
messages.each(&)
|
|
137
150
|
end
|
|
138
151
|
|
|
139
|
-
def
|
|
152
|
+
def cost
|
|
153
|
+
Cost.aggregate(messages.map(&:cost))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def complete(&)
|
|
140
157
|
response = @provider.complete(
|
|
141
158
|
messages,
|
|
142
159
|
tools: @tools,
|
|
@@ -150,7 +167,7 @@ module RubyLLM
|
|
|
150
167
|
&wrap_streaming_block(&)
|
|
151
168
|
)
|
|
152
169
|
|
|
153
|
-
|
|
170
|
+
run_callbacks(:before_message, :new_message) unless block_given?
|
|
154
171
|
|
|
155
172
|
if @schema && response.content.is_a?(String) && !response.tool_call?
|
|
156
173
|
begin
|
|
@@ -161,7 +178,7 @@ module RubyLLM
|
|
|
161
178
|
end
|
|
162
179
|
|
|
163
180
|
add_message response
|
|
164
|
-
|
|
181
|
+
run_callbacks(:after_message, :end_message, response)
|
|
165
182
|
|
|
166
183
|
if response.tool_call?
|
|
167
184
|
handle_tool_calls(response, &)
|
|
@@ -221,28 +238,52 @@ module RubyLLM
|
|
|
221
238
|
sanitized.empty? ? 'response' : sanitized
|
|
222
239
|
end
|
|
223
240
|
|
|
241
|
+
def add_callback(name, &block)
|
|
242
|
+
@callbacks[name] << block if block
|
|
243
|
+
self
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def set_legacy_callback(name, legacy_name, additive_name, &block)
|
|
247
|
+
warn_legacy_callback_deprecation(legacy_name, additive_name) if block
|
|
248
|
+
|
|
249
|
+
@on[name] = block
|
|
250
|
+
self
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def warn_legacy_callback_deprecation(legacy_name, additive_name)
|
|
254
|
+
RubyLLM.logger.warn(
|
|
255
|
+
"`#{legacy_name}` is deprecated and will be removed in RubyLLM 2.0. " \
|
|
256
|
+
"Use `#{additive_name}` instead."
|
|
257
|
+
)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def run_callbacks(name, legacy_name, *args)
|
|
261
|
+
@callbacks[name].each { |callback| callback.call(*args) }
|
|
262
|
+
@on[legacy_name]&.call(*args)
|
|
263
|
+
end
|
|
264
|
+
|
|
224
265
|
def wrap_streaming_block(&block)
|
|
225
266
|
return nil unless block_given?
|
|
226
267
|
|
|
227
|
-
|
|
268
|
+
run_callbacks(:before_message, :new_message)
|
|
228
269
|
|
|
229
270
|
proc do |chunk|
|
|
230
271
|
block.call chunk
|
|
231
272
|
end
|
|
232
273
|
end
|
|
233
274
|
|
|
234
|
-
def handle_tool_calls(response, &)
|
|
275
|
+
def handle_tool_calls(response, &)
|
|
235
276
|
halt_result = nil
|
|
236
277
|
|
|
237
278
|
response.tool_calls.each_value do |tool_call|
|
|
238
|
-
|
|
239
|
-
|
|
279
|
+
run_callbacks(:before_message, :new_message)
|
|
280
|
+
run_callbacks(:before_tool_call, :tool_call, tool_call)
|
|
240
281
|
result = execute_tool tool_call
|
|
241
|
-
|
|
282
|
+
run_callbacks(:after_tool_result, :tool_result, result)
|
|
242
283
|
tool_payload = result.is_a?(Tool::Halt) ? result.content : result
|
|
243
284
|
content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
|
|
244
285
|
message = add_message role: :tool, content:, tool_call_id: tool_call.id
|
|
245
|
-
|
|
286
|
+
run_callbacks(:after_message, :end_message, message)
|
|
246
287
|
|
|
247
288
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
248
289
|
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Represents the cost of token usage for a model response.
|
|
5
|
+
class Cost
|
|
6
|
+
COMPONENTS = %i[input output cache_read cache_write thinking].freeze
|
|
7
|
+
PER_MILLION = 1_000_000.0
|
|
8
|
+
|
|
9
|
+
attr_reader :tokens, :model, :category
|
|
10
|
+
|
|
11
|
+
def self.aggregate(costs)
|
|
12
|
+
costs = costs.compact.select(&:tokens?)
|
|
13
|
+
return new(amounts: {}, has_tokens: false) if costs.empty?
|
|
14
|
+
|
|
15
|
+
missing = COMPONENTS.select do |component|
|
|
16
|
+
costs.any? { |cost| cost.missing?(component) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
amounts = COMPONENTS.to_h do |component|
|
|
20
|
+
[component, missing.include?(component) ? nil : aggregate_component(costs, component)]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
new(amounts:, missing:, has_tokens: true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# rubocop:disable Metrics/ParameterLists
|
|
27
|
+
def initialize(tokens: nil, model: nil, amounts: nil, missing: [], has_tokens: nil, category: :text_tokens,
|
|
28
|
+
input_details: nil)
|
|
29
|
+
@tokens = tokens
|
|
30
|
+
@model = normalize_model(model)
|
|
31
|
+
@amounts = amounts
|
|
32
|
+
@missing = missing
|
|
33
|
+
@has_tokens = has_tokens
|
|
34
|
+
@category = category.to_sym
|
|
35
|
+
@input_details = input_details
|
|
36
|
+
end
|
|
37
|
+
# rubocop:enable Metrics/ParameterLists
|
|
38
|
+
|
|
39
|
+
def input
|
|
40
|
+
amount_for(:input)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def output
|
|
44
|
+
amount_for(:output)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cache_read
|
|
48
|
+
amount_for(:cache_read)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cache_write
|
|
52
|
+
amount_for(:cache_write)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def thinking
|
|
56
|
+
amount_for(:thinking)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias reasoning thinking
|
|
60
|
+
|
|
61
|
+
alias cached_input cache_read
|
|
62
|
+
alias cache_creation cache_write
|
|
63
|
+
|
|
64
|
+
def total
|
|
65
|
+
return nil unless tokens?
|
|
66
|
+
return nil if COMPONENTS.any? { |component| missing?(component) }
|
|
67
|
+
|
|
68
|
+
costs = COMPONENTS.filter_map { |component| public_send(component) }
|
|
69
|
+
return nil if costs.empty?
|
|
70
|
+
|
|
71
|
+
costs.sum
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_h
|
|
75
|
+
{
|
|
76
|
+
input: input,
|
|
77
|
+
output: output,
|
|
78
|
+
cache_read: cache_read,
|
|
79
|
+
cache_write: cache_write,
|
|
80
|
+
thinking: thinking,
|
|
81
|
+
total: total
|
|
82
|
+
}.compact
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def tokens?
|
|
86
|
+
return @has_tokens unless @has_tokens.nil?
|
|
87
|
+
|
|
88
|
+
COMPONENTS.any? { |component| !tokens_for(component).nil? }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def missing?(component)
|
|
92
|
+
return @missing.include?(component) if aggregate?
|
|
93
|
+
return image_input_missing? if component == :input && detailed_image_input?
|
|
94
|
+
return false if component == :thinking && !thinking_priced_separately?
|
|
95
|
+
|
|
96
|
+
tokens = tokens_for(component)
|
|
97
|
+
tokens.to_i.positive? && price_for(component).nil?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private_class_method def self.aggregate_component(costs, component)
|
|
101
|
+
values = costs.filter_map { |cost| cost.public_send(component) }
|
|
102
|
+
values.empty? ? nil : values.sum
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def amount_for(component)
|
|
108
|
+
return @amounts[component] if aggregate?
|
|
109
|
+
return image_input_amount if component == :input && detailed_image_input?
|
|
110
|
+
|
|
111
|
+
token_count = tokens_for(component)
|
|
112
|
+
return nil if token_count.nil?
|
|
113
|
+
|
|
114
|
+
token_count = token_count.to_i
|
|
115
|
+
return 0.0 if token_count.zero?
|
|
116
|
+
|
|
117
|
+
price = price_for(component)
|
|
118
|
+
return nil unless price
|
|
119
|
+
|
|
120
|
+
token_count * price / PER_MILLION
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def aggregate?
|
|
124
|
+
!@amounts.nil?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def tokens_for(component)
|
|
128
|
+
return unless tokens
|
|
129
|
+
|
|
130
|
+
case component
|
|
131
|
+
when :input
|
|
132
|
+
tokens.input
|
|
133
|
+
when :output
|
|
134
|
+
tokens.output
|
|
135
|
+
when :cache_read
|
|
136
|
+
tokens.cache_read
|
|
137
|
+
when :cache_write
|
|
138
|
+
tokens.cache_write
|
|
139
|
+
when :thinking
|
|
140
|
+
tokens.thinking if thinking_priced_separately?
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def price_for(component)
|
|
145
|
+
case component
|
|
146
|
+
when :input
|
|
147
|
+
text_pricing.input
|
|
148
|
+
when :output
|
|
149
|
+
output_pricing.output
|
|
150
|
+
when :cache_read
|
|
151
|
+
text_pricing.cache_read_input
|
|
152
|
+
when :cache_write
|
|
153
|
+
text_pricing.cache_write_input
|
|
154
|
+
when :thinking
|
|
155
|
+
text_pricing.reasoning_output
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def text_pricing
|
|
160
|
+
model&.pricing&.text_tokens || RubyLLM::Model::PricingCategory.new
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def image_pricing
|
|
164
|
+
model&.pricing&.images || RubyLLM::Model::PricingCategory.new
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def output_pricing
|
|
168
|
+
image_cost? && image_pricing.output ? image_pricing : text_pricing
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def image_cost?
|
|
172
|
+
%i[image images].include?(category)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def detailed_image_input?
|
|
176
|
+
image_cost? && @input_details.is_a?(Hash) && image_input_parts.any? { |_, tokens, _| !tokens.nil? }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def image_input_amount
|
|
180
|
+
return nil if image_input_missing?
|
|
181
|
+
|
|
182
|
+
image_input_parts.filter_map do |_, token_count, price|
|
|
183
|
+
next if token_count.nil? || token_count.to_i.zero?
|
|
184
|
+
|
|
185
|
+
token_count.to_i * price / PER_MILLION
|
|
186
|
+
end.sum
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def image_input_missing?
|
|
190
|
+
image_input_parts.any? do |_, token_count, price|
|
|
191
|
+
token_count.to_i.positive? && price.nil?
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def image_input_parts
|
|
196
|
+
[
|
|
197
|
+
[:text, input_detail('text_tokens'), text_pricing.input],
|
|
198
|
+
[:image, input_detail('image_tokens'), image_pricing.input || text_pricing.input]
|
|
199
|
+
]
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def input_detail(key)
|
|
203
|
+
@input_details[key] || @input_details[key.to_sym]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def thinking_priced_separately?
|
|
207
|
+
reasoning_price = text_pricing.reasoning_output
|
|
208
|
+
return false unless reasoning_price
|
|
209
|
+
|
|
210
|
+
output_price = text_pricing.output
|
|
211
|
+
output_price.nil? || reasoning_price != output_price
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def normalize_model(model)
|
|
215
|
+
return RubyLLM.models.find(model.to_s) if model.is_a?(String) || model.is_a?(Symbol)
|
|
216
|
+
return model.to_llm if model.respond_to?(:to_llm)
|
|
217
|
+
return model if model.respond_to?(:pricing)
|
|
218
|
+
|
|
219
|
+
nil
|
|
220
|
+
rescue ModelNotFoundError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
data/lib/ruby_llm/image.rb
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
# Represents a generated image from an AI model.
|
|
5
5
|
class Image
|
|
6
|
-
attr_reader :url, :data, :mime_type, :revised_prompt, :model_id
|
|
6
|
+
attr_reader :url, :data, :mime_type, :revised_prompt, :model_id, :usage
|
|
7
7
|
|
|
8
|
-
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil)
|
|
8
|
+
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil, usage: {}) # rubocop:disable Metrics/ParameterLists
|
|
9
9
|
@url = url
|
|
10
10
|
@data = data
|
|
11
11
|
@mime_type = mime_type
|
|
12
12
|
@revised_prompt = revised_prompt
|
|
13
13
|
@model_id = model_id
|
|
14
|
+
@usage = usage
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def base64?
|
|
@@ -36,14 +37,46 @@ module RubyLLM
|
|
|
36
37
|
provider: nil,
|
|
37
38
|
assume_model_exists: false,
|
|
38
39
|
size: '1024x1024',
|
|
39
|
-
context: nil
|
|
40
|
+
context: nil,
|
|
41
|
+
with: nil,
|
|
42
|
+
mask: nil,
|
|
43
|
+
params: {})
|
|
40
44
|
config = context&.config || RubyLLM.config
|
|
41
45
|
model ||= config.default_image_model
|
|
42
46
|
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
43
47
|
config: config)
|
|
44
48
|
model_id = model.id
|
|
45
49
|
|
|
46
|
-
provider_instance.paint(prompt, model: model_id, size:)
|
|
50
|
+
provider_instance.paint(prompt, model: model_id, size:, with:, mask:, params:)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tokens
|
|
54
|
+
@tokens ||= Tokens.build(
|
|
55
|
+
input: usage_value('input_tokens'),
|
|
56
|
+
output: usage_value('output_tokens')
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cost
|
|
61
|
+
Cost.new(tokens:, model: model_info, category: :images, input_details: input_tokens_details)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def model_info
|
|
65
|
+
return unless model_id
|
|
66
|
+
|
|
67
|
+
@model_info ||= RubyLLM.models.find(model_id)
|
|
68
|
+
rescue ModelNotFoundError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def input_tokens_details
|
|
75
|
+
usage_value('input_tokens_details')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def usage_value(key)
|
|
79
|
+
usage[key] || usage[key.to_sym]
|
|
47
80
|
end
|
|
48
81
|
end
|
|
49
82
|
end
|
data/lib/ruby_llm/message.rb
CHANGED
|
@@ -64,6 +64,14 @@ module RubyLLM
|
|
|
64
64
|
tokens&.cache_creation
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
def cache_read_tokens
|
|
68
|
+
tokens&.cache_read
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def cache_write_tokens
|
|
72
|
+
tokens&.cache_write
|
|
73
|
+
end
|
|
74
|
+
|
|
67
75
|
def thinking_tokens
|
|
68
76
|
tokens&.thinking
|
|
69
77
|
end
|
|
@@ -72,6 +80,10 @@ module RubyLLM
|
|
|
72
80
|
tokens&.thinking
|
|
73
81
|
end
|
|
74
82
|
|
|
83
|
+
def cost(model: nil)
|
|
84
|
+
Cost.new(tokens:, model: model || model_info)
|
|
85
|
+
end
|
|
86
|
+
|
|
75
87
|
def to_h
|
|
76
88
|
{
|
|
77
89
|
role: role,
|
|
@@ -88,6 +100,14 @@ module RubyLLM
|
|
|
88
100
|
super - [:@raw]
|
|
89
101
|
end
|
|
90
102
|
|
|
103
|
+
def model_info
|
|
104
|
+
return unless model_id
|
|
105
|
+
|
|
106
|
+
@model_info ||= RubyLLM.models.find(model_id)
|
|
107
|
+
rescue ModelNotFoundError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
91
111
|
private
|
|
92
112
|
|
|
93
113
|
def normalize_content(content, role:, tool_calls:)
|
data/lib/ruby_llm/model/info.rb
CHANGED
|
@@ -77,6 +77,23 @@ module RubyLLM
|
|
|
77
77
|
pricing.text_tokens.output
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
def cache_read_input_price_per_million
|
|
81
|
+
pricing.text_tokens.cache_read_input
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def cache_write_input_price_per_million
|
|
85
|
+
pricing.text_tokens.cache_write_input
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
alias cached_input_price_per_million cache_read_input_price_per_million
|
|
89
|
+
alias cache_creation_input_price_per_million cache_write_input_price_per_million
|
|
90
|
+
|
|
91
|
+
def cost_for(tokens)
|
|
92
|
+
tokens = tokens.tokens if tokens.respond_to?(:tokens)
|
|
93
|
+
|
|
94
|
+
Cost.new(tokens:, model: self)
|
|
95
|
+
end
|
|
96
|
+
|
|
80
97
|
def provider_class
|
|
81
98
|
RubyLLM::Provider.resolve provider
|
|
82
99
|
end
|
|
@@ -19,10 +19,21 @@ module RubyLLM
|
|
|
19
19
|
standard&.output_per_million
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def
|
|
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
|