ruby_llm 1.6.2 ā 1.6.4
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 +73 -91
- data/lib/ruby_llm/active_record/acts_as.rb +14 -13
- data/lib/ruby_llm/aliases.json +8 -0
- data/lib/ruby_llm/aliases.rb +7 -25
- data/lib/ruby_llm/chat.rb +5 -12
- data/lib/ruby_llm/configuration.rb +1 -12
- data/lib/ruby_llm/content.rb +0 -2
- data/lib/ruby_llm/embedding.rb +1 -2
- data/lib/ruby_llm/error.rb +0 -8
- data/lib/ruby_llm/image.rb +0 -4
- data/lib/ruby_llm/message.rb +2 -4
- data/lib/ruby_llm/model/info.rb +0 -10
- data/lib/ruby_llm/model/pricing.rb +0 -3
- data/lib/ruby_llm/model/pricing_category.rb +0 -2
- data/lib/ruby_llm/model/pricing_tier.rb +0 -1
- data/lib/ruby_llm/models.json +623 -452
- data/lib/ruby_llm/models.rb +5 -13
- data/lib/ruby_llm/provider.rb +1 -5
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +1 -2
- data/lib/ruby_llm/providers/anthropic.rb +1 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -2
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
- data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
- data/lib/ruby_llm/providers/bedrock.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +26 -101
- data/lib/ruby_llm/providers/gemini/chat.rb +57 -31
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
- data/lib/ruby_llm/providers/gemini/images.rb +0 -1
- data/lib/ruby_llm/providers/gemini/media.rb +0 -1
- data/lib/ruby_llm/providers/gemini/models.rb +1 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
- data/lib/ruby_llm/providers/gpustack/chat.rb +0 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +3 -4
- data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
- data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
- data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/mistral/models.rb +0 -1
- data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
- data/lib/ruby_llm/providers/ollama/media.rb +0 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +0 -15
- data/lib/ruby_llm/providers/openai/chat.rb +0 -3
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/openai/media.rb +0 -1
- data/lib/ruby_llm/providers/openai.rb +1 -3
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
- data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
- data/lib/ruby_llm/providers/perplexity.rb +1 -5
- data/lib/ruby_llm/railtie.rb +0 -1
- data/lib/ruby_llm/stream_accumulator.rb +1 -3
- data/lib/ruby_llm/streaming.rb +15 -24
- data/lib/ruby_llm/tool.rb +2 -19
- data/lib/ruby_llm/tool_call.rb +0 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +0 -2
- data/lib/tasks/models.rake +514 -0
- data/lib/tasks/release.rake +37 -2
- data/lib/tasks/vcr.rake +0 -7
- metadata +2 -4
- data/lib/tasks/aliases.rake +0 -235
- data/lib/tasks/models_docs.rake +0 -224
- data/lib/tasks/models_update.rake +0 -108
@@ -0,0 +1,514 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dotenv/load'
|
4
|
+
require 'ruby_llm'
|
5
|
+
require 'json'
|
6
|
+
require 'json-schema'
|
7
|
+
require 'fileutils'
|
8
|
+
|
9
|
+
desc 'Update models, docs, and aliases'
|
10
|
+
task models: ['models:update', 'models:docs', 'models:aliases']
|
11
|
+
|
12
|
+
namespace :models do
|
13
|
+
desc 'Update available models from providers (API keys needed)'
|
14
|
+
task :update do
|
15
|
+
puts 'Configuring RubyLLM...'
|
16
|
+
configure_from_env
|
17
|
+
refresh_models
|
18
|
+
display_model_stats
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Generate available models documentation'
|
22
|
+
task :docs do
|
23
|
+
FileUtils.mkdir_p('docs/_reference')
|
24
|
+
output = generate_models_markdown
|
25
|
+
File.write('docs/_reference/available-models.md', output)
|
26
|
+
puts 'Generated docs/_reference/available-models.md'
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Generate model aliases from registry'
|
30
|
+
task :aliases do
|
31
|
+
generate_aliases
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Keep aliases:generate for backwards compatibility
|
36
|
+
namespace :aliases do
|
37
|
+
task generate: ['models:aliases']
|
38
|
+
end
|
39
|
+
|
40
|
+
def configure_from_env
|
41
|
+
RubyLLM.configure do |config|
|
42
|
+
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
|
43
|
+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
|
44
|
+
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
45
|
+
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
|
46
|
+
config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
|
47
|
+
config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
|
48
|
+
config.mistral_api_key = ENV.fetch('MISTRAL_API_KEY', nil)
|
49
|
+
configure_bedrock(config)
|
50
|
+
config.request_timeout = 30
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def configure_bedrock(config)
|
55
|
+
config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
|
56
|
+
config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
|
57
|
+
config.bedrock_region = ENV.fetch('AWS_REGION', nil)
|
58
|
+
config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
|
59
|
+
end
|
60
|
+
|
61
|
+
def refresh_models
|
62
|
+
initial_count = RubyLLM.models.all.size
|
63
|
+
puts "Refreshing models (#{initial_count} cached)..."
|
64
|
+
|
65
|
+
models = RubyLLM.models.refresh!
|
66
|
+
|
67
|
+
if models.all.empty? && initial_count.zero?
|
68
|
+
puts 'Error: Failed to fetch models.'
|
69
|
+
exit(1)
|
70
|
+
elsif models.all.size == initial_count && initial_count.positive?
|
71
|
+
puts 'Warning: Model list unchanged.'
|
72
|
+
else
|
73
|
+
puts 'Validating models...'
|
74
|
+
validate_models!(models)
|
75
|
+
|
76
|
+
puts "Saving models.json (#{models.all.size} models)"
|
77
|
+
models.save_models
|
78
|
+
end
|
79
|
+
|
80
|
+
@models = models
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate_models!(models)
|
84
|
+
schema_path = RubyLLM::Models.schema_file
|
85
|
+
models_data = models.all.map(&:to_h)
|
86
|
+
|
87
|
+
validation_errors = JSON::Validator.fully_validate(schema_path, models_data)
|
88
|
+
|
89
|
+
unless validation_errors.empty?
|
90
|
+
# Save failed models for inspection
|
91
|
+
failed_path = File.expand_path('../ruby_llm/models.failed.json', __dir__)
|
92
|
+
File.write(failed_path, JSON.pretty_generate(models_data))
|
93
|
+
|
94
|
+
puts 'ERROR: Models validation failed:'
|
95
|
+
puts "\nValidation errors:"
|
96
|
+
validation_errors.first(10).each { |error| puts " - #{error}" }
|
97
|
+
puts " ... and #{validation_errors.size - 10} more errors" if validation_errors.size > 10
|
98
|
+
puts "-> Failed models saved to: #{failed_path}"
|
99
|
+
exit(1)
|
100
|
+
end
|
101
|
+
|
102
|
+
puts 'ā Models validation passed'
|
103
|
+
end
|
104
|
+
|
105
|
+
def display_model_stats
|
106
|
+
puts "\nModel count:"
|
107
|
+
provider_counts = @models.all.group_by(&:provider).transform_values(&:count)
|
108
|
+
|
109
|
+
RubyLLM::Provider.providers.each do |sym, provider_class|
|
110
|
+
name = provider_class.name
|
111
|
+
count = provider_counts[sym.to_s] || 0
|
112
|
+
status = status(sym)
|
113
|
+
puts " #{name}: #{count} models #{status}"
|
114
|
+
end
|
115
|
+
|
116
|
+
puts 'Refresh complete.'
|
117
|
+
end
|
118
|
+
|
119
|
+
def status(provider_sym)
|
120
|
+
provider_class = RubyLLM::Provider.providers[provider_sym]
|
121
|
+
if provider_class.local?
|
122
|
+
' (LOCAL - SKIP)'
|
123
|
+
elsif provider_class.configured?(RubyLLM.config)
|
124
|
+
' (OK)'
|
125
|
+
else
|
126
|
+
' (NOT CONFIGURED)'
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def generate_models_markdown
|
131
|
+
<<~MARKDOWN
|
132
|
+
---
|
133
|
+
layout: default
|
134
|
+
title: Available Models
|
135
|
+
nav_order: 1
|
136
|
+
description: Browse hundreds of AI models from every major provider. Always up-to-date, automatically generated.
|
137
|
+
redirect_from:
|
138
|
+
- /guides/available-models
|
139
|
+
---
|
140
|
+
|
141
|
+
# {{ page.title }}
|
142
|
+
{: .no_toc }
|
143
|
+
|
144
|
+
{{ page.description }}
|
145
|
+
{: .fs-6 .fw-300 }
|
146
|
+
|
147
|
+
## Table of contents
|
148
|
+
{: .no_toc .text-delta }
|
149
|
+
|
150
|
+
1. TOC
|
151
|
+
{:toc}
|
152
|
+
|
153
|
+
---
|
154
|
+
|
155
|
+
After reading this guide, you will know:
|
156
|
+
|
157
|
+
* How RubyLLM's model registry works and where data comes from
|
158
|
+
* How to find models by provider, capability, or purpose
|
159
|
+
* What information is available for each model
|
160
|
+
* How to use model aliases for simpler configuration
|
161
|
+
|
162
|
+
## How Model Data Works
|
163
|
+
|
164
|
+
RubyLLM's model registry combines data from multiple sources:
|
165
|
+
|
166
|
+
- **OpenAI, Anthropic, DeepSeek, Gemini**: Data from [Parsera](https://api.parsera.org/v1/llm-specs)
|
167
|
+
- **OpenRouter**: Direct from OpenRouter's API
|
168
|
+
- **Other providers**: Defined in `capabilities.rb` files
|
169
|
+
|
170
|
+
## Contributing Model Updates
|
171
|
+
|
172
|
+
**For major providers** (OpenAI, Anthropic, DeepSeek, Gemini): File issues with [Parsera](https://github.com/parsera-labs/api-llm-specs/issues) for public model data corrections.
|
173
|
+
|
174
|
+
**For other providers**: Edit `lib/ruby_llm/providers/<provider>/capabilities.rb` then run `rake models:update`.
|
175
|
+
|
176
|
+
See the [Contributing Guide](https://github.com/crmne/ruby_llm/blob/main/CONTRIBUTING.md) for details.
|
177
|
+
|
178
|
+
## Last Updated
|
179
|
+
{: .d-inline-block }
|
180
|
+
|
181
|
+
#{Time.now.utc.strftime('%Y-%m-%d')}
|
182
|
+
{: .label .label-green }
|
183
|
+
|
184
|
+
## Models by Provider
|
185
|
+
|
186
|
+
#{generate_provider_sections}
|
187
|
+
|
188
|
+
## Models by Capability
|
189
|
+
|
190
|
+
#{generate_capability_sections}
|
191
|
+
|
192
|
+
## Models by Modality
|
193
|
+
|
194
|
+
#{generate_modality_sections}
|
195
|
+
MARKDOWN
|
196
|
+
end
|
197
|
+
|
198
|
+
def generate_provider_sections
|
199
|
+
RubyLLM::Provider.providers.filter_map do |provider, provider_class|
|
200
|
+
models = RubyLLM.models.by_provider(provider)
|
201
|
+
next if models.none?
|
202
|
+
|
203
|
+
<<~PROVIDER
|
204
|
+
### #{provider_class.name} (#{models.count})
|
205
|
+
|
206
|
+
#{models_table(models)}
|
207
|
+
PROVIDER
|
208
|
+
end.join("\n\n")
|
209
|
+
end
|
210
|
+
|
211
|
+
def generate_capability_sections
|
212
|
+
capabilities = {
|
213
|
+
'Function Calling' => RubyLLM.models.select(&:function_calling?),
|
214
|
+
'Structured Output' => RubyLLM.models.select(&:structured_output?),
|
215
|
+
'Streaming' => RubyLLM.models.select { |m| m.capabilities.include?('streaming') },
|
216
|
+
'Batch Processing' => RubyLLM.models.select { |m| m.capabilities.include?('batch') }
|
217
|
+
}
|
218
|
+
|
219
|
+
capabilities.filter_map do |capability, models|
|
220
|
+
next if models.none?
|
221
|
+
|
222
|
+
<<~CAPABILITY
|
223
|
+
### #{capability} (#{models.count})
|
224
|
+
|
225
|
+
#{models_table(models)}
|
226
|
+
CAPABILITY
|
227
|
+
end.join("\n\n")
|
228
|
+
end
|
229
|
+
|
230
|
+
def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
|
231
|
+
sections = []
|
232
|
+
|
233
|
+
vision_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('image') }
|
234
|
+
if vision_models.any?
|
235
|
+
sections << <<~SECTION
|
236
|
+
### Vision Models (#{vision_models.count})
|
237
|
+
|
238
|
+
Models that can process images:
|
239
|
+
|
240
|
+
#{models_table(vision_models)}
|
241
|
+
SECTION
|
242
|
+
end
|
243
|
+
|
244
|
+
audio_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('audio') }
|
245
|
+
if audio_models.any?
|
246
|
+
sections << <<~SECTION
|
247
|
+
### Audio Input Models (#{audio_models.count})
|
248
|
+
|
249
|
+
Models that can process audio:
|
250
|
+
|
251
|
+
#{models_table(audio_models)}
|
252
|
+
SECTION
|
253
|
+
end
|
254
|
+
|
255
|
+
pdf_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('pdf') }
|
256
|
+
if pdf_models.any?
|
257
|
+
sections << <<~SECTION
|
258
|
+
### PDF Models (#{pdf_models.count})
|
259
|
+
|
260
|
+
Models that can process PDF documents:
|
261
|
+
|
262
|
+
#{models_table(pdf_models)}
|
263
|
+
SECTION
|
264
|
+
end
|
265
|
+
|
266
|
+
embedding_models = RubyLLM.models.select { |m| (m.modalities.output || []).include?('embeddings') }
|
267
|
+
if embedding_models.any?
|
268
|
+
sections << <<~SECTION
|
269
|
+
### Embedding Models (#{embedding_models.count})
|
270
|
+
|
271
|
+
Models that generate embeddings:
|
272
|
+
|
273
|
+
#{models_table(embedding_models)}
|
274
|
+
SECTION
|
275
|
+
end
|
276
|
+
|
277
|
+
sections.join("\n\n")
|
278
|
+
end
|
279
|
+
|
280
|
+
def models_table(models)
|
281
|
+
return '*No models found*' if models.none?
|
282
|
+
|
283
|
+
headers = ['Model', 'Provider', 'Context', 'Max Output', 'Standard Pricing (per 1M tokens)']
|
284
|
+
alignment = [':--', ':--', '--:', '--:', ':--']
|
285
|
+
|
286
|
+
rows = models.sort_by { |m| [m.provider, m.name] }.map do |model|
|
287
|
+
pricing = standard_pricing_display(model)
|
288
|
+
|
289
|
+
[
|
290
|
+
model.id,
|
291
|
+
model.provider,
|
292
|
+
model.context_window || '-',
|
293
|
+
model.max_output_tokens || '-',
|
294
|
+
pricing
|
295
|
+
]
|
296
|
+
end
|
297
|
+
|
298
|
+
table = []
|
299
|
+
table << "| #{headers.join(' | ')} |"
|
300
|
+
table << "| #{alignment.join(' | ')} |"
|
301
|
+
|
302
|
+
rows.each do |row|
|
303
|
+
table << "| #{row.join(' | ')} |"
|
304
|
+
end
|
305
|
+
|
306
|
+
table.join("\n")
|
307
|
+
end
|
308
|
+
|
309
|
+
def standard_pricing_display(model)
|
310
|
+
pricing_data = model.pricing.to_h[:text_tokens]&.dig(:standard) || {}
|
311
|
+
|
312
|
+
if pricing_data.any?
|
313
|
+
parts = []
|
314
|
+
|
315
|
+
parts << "In: $#{format('%.2f', pricing_data[:input_per_million])}" if pricing_data[:input_per_million]
|
316
|
+
|
317
|
+
parts << "Out: $#{format('%.2f', pricing_data[:output_per_million])}" if pricing_data[:output_per_million]
|
318
|
+
|
319
|
+
if pricing_data[:cached_input_per_million]
|
320
|
+
parts << "Cache: $#{format('%.2f', pricing_data[:cached_input_per_million])}"
|
321
|
+
end
|
322
|
+
|
323
|
+
return parts.join(', ') if parts.any?
|
324
|
+
end
|
325
|
+
|
326
|
+
'-'
|
327
|
+
end
|
328
|
+
|
329
|
+
def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
|
330
|
+
models = Hash.new { |h, k| h[k] = [] }
|
331
|
+
|
332
|
+
RubyLLM.models.all.each do |model|
|
333
|
+
models[model.provider] << model.id
|
334
|
+
end
|
335
|
+
|
336
|
+
aliases = {}
|
337
|
+
|
338
|
+
# OpenAI models
|
339
|
+
models['openai'].each do |model|
|
340
|
+
openrouter_model = "openai/#{model}"
|
341
|
+
next unless models['openrouter'].include?(openrouter_model)
|
342
|
+
|
343
|
+
alias_key = model.gsub('-latest', '')
|
344
|
+
aliases[alias_key] = {
|
345
|
+
'openai' => model,
|
346
|
+
'openrouter' => openrouter_model
|
347
|
+
}
|
348
|
+
end
|
349
|
+
|
350
|
+
anthropic_latest = group_anthropic_models_by_base_name(models['anthropic'])
|
351
|
+
|
352
|
+
anthropic_latest.each do |base_name, latest_model|
|
353
|
+
openrouter_variants = [
|
354
|
+
"anthropic/#{base_name}",
|
355
|
+
"anthropic/#{base_name.gsub(/-(\d)/, '.\1')}",
|
356
|
+
"anthropic/#{base_name.gsub(/claude-(\d+)-(\d+)/, 'claude-\1.\2')}",
|
357
|
+
"anthropic/#{base_name.gsub(/(\d+)-(\d+)/, '\1.\2')}"
|
358
|
+
]
|
359
|
+
|
360
|
+
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
361
|
+
bedrock_model = find_best_bedrock_model(latest_model, models['bedrock'])
|
362
|
+
|
363
|
+
next unless openrouter_model || bedrock_model || models['anthropic'].include?(latest_model)
|
364
|
+
|
365
|
+
aliases[base_name] = { 'anthropic' => latest_model }
|
366
|
+
aliases[base_name]['openrouter'] = openrouter_model if openrouter_model
|
367
|
+
aliases[base_name]['bedrock'] = bedrock_model if bedrock_model
|
368
|
+
end
|
369
|
+
|
370
|
+
models['bedrock'].each do |bedrock_model|
|
371
|
+
next unless bedrock_model.start_with?('anthropic.')
|
372
|
+
next unless bedrock_model =~ /anthropic\.(claude-[\d\.]+-[a-z]+)/
|
373
|
+
|
374
|
+
base_name = Regexp.last_match(1)
|
375
|
+
anthropic_name = base_name.tr('.', '-')
|
376
|
+
|
377
|
+
next if aliases[anthropic_name]
|
378
|
+
|
379
|
+
openrouter_variants = [
|
380
|
+
"anthropic/#{anthropic_name}",
|
381
|
+
"anthropic/#{base_name}"
|
382
|
+
]
|
383
|
+
|
384
|
+
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
385
|
+
|
386
|
+
aliases[anthropic_name] = { 'bedrock' => bedrock_model }
|
387
|
+
aliases[anthropic_name]['anthropic'] = anthropic_name if models['anthropic'].include?(anthropic_name)
|
388
|
+
aliases[anthropic_name]['openrouter'] = openrouter_model if openrouter_model
|
389
|
+
end
|
390
|
+
|
391
|
+
models['gemini'].each do |model|
|
392
|
+
openrouter_variants = [
|
393
|
+
"google/#{model}",
|
394
|
+
"google/#{model.gsub('gemini-', 'gemini-').tr('.', '-')}",
|
395
|
+
"google/#{model.gsub('gemini-', 'gemini-')}"
|
396
|
+
]
|
397
|
+
|
398
|
+
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
399
|
+
next unless openrouter_model
|
400
|
+
|
401
|
+
alias_key = model.gsub('-latest', '')
|
402
|
+
aliases[alias_key] = {
|
403
|
+
'gemini' => model,
|
404
|
+
'openrouter' => openrouter_model
|
405
|
+
}
|
406
|
+
end
|
407
|
+
|
408
|
+
models['deepseek'].each do |model|
|
409
|
+
openrouter_model = "deepseek/#{model}"
|
410
|
+
next unless models['openrouter'].include?(openrouter_model)
|
411
|
+
|
412
|
+
alias_key = model.gsub('-latest', '')
|
413
|
+
aliases[alias_key] = {
|
414
|
+
'deepseek' => model,
|
415
|
+
'openrouter' => openrouter_model
|
416
|
+
}
|
417
|
+
end
|
418
|
+
|
419
|
+
sorted_aliases = aliases.sort.to_h
|
420
|
+
File.write(RubyLLM::Aliases.aliases_file, JSON.pretty_generate(sorted_aliases))
|
421
|
+
|
422
|
+
puts "Generated #{sorted_aliases.size} aliases"
|
423
|
+
end
|
424
|
+
|
425
|
+
def group_anthropic_models_by_base_name(anthropic_models)
|
426
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
427
|
+
|
428
|
+
anthropic_models.each do |model|
|
429
|
+
base_name = extract_base_name(model)
|
430
|
+
grouped[base_name] << model
|
431
|
+
end
|
432
|
+
|
433
|
+
latest_models = {}
|
434
|
+
grouped.each do |base_name, model_list|
|
435
|
+
if model_list.size == 1
|
436
|
+
latest_models[base_name] = model_list.first
|
437
|
+
else
|
438
|
+
latest_model = model_list.max_by { |model| extract_date_from_model(model) }
|
439
|
+
latest_models[base_name] = latest_model
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
latest_models
|
444
|
+
end
|
445
|
+
|
446
|
+
def extract_base_name(model)
|
447
|
+
if model =~ /^(.+)-(\d{8})$/
|
448
|
+
Regexp.last_match(1)
|
449
|
+
else
|
450
|
+
model
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def extract_date_from_model(model)
|
455
|
+
if model =~ /-(\d{8})$/
|
456
|
+
Regexp.last_match(1)
|
457
|
+
else
|
458
|
+
'00000000'
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable Metrics/PerceivedComplexity
|
463
|
+
base_pattern = case anthropic_model
|
464
|
+
when 'claude-2.0', 'claude-2'
|
465
|
+
'claude-v2'
|
466
|
+
when 'claude-2.1'
|
467
|
+
'claude-v2:1'
|
468
|
+
when 'claude-instant-v1', 'claude-instant'
|
469
|
+
'claude-instant'
|
470
|
+
else
|
471
|
+
extract_base_name(anthropic_model)
|
472
|
+
end
|
473
|
+
|
474
|
+
matching_models = bedrock_models.select do |bedrock_model|
|
475
|
+
model_without_prefix = bedrock_model.sub(/^(?:us\.)?anthropic\./, '')
|
476
|
+
model_without_prefix.start_with?(base_pattern)
|
477
|
+
end
|
478
|
+
|
479
|
+
return nil if matching_models.empty?
|
480
|
+
|
481
|
+
begin
|
482
|
+
model_info = RubyLLM.models.find(anthropic_model)
|
483
|
+
target_context = model_info.context_window
|
484
|
+
rescue StandardError
|
485
|
+
target_context = nil
|
486
|
+
end
|
487
|
+
|
488
|
+
if target_context
|
489
|
+
target_k = target_context / 1000
|
490
|
+
|
491
|
+
with_context = matching_models.select do |m|
|
492
|
+
m.include?(":#{target_k}k") || m.include?(":0:#{target_k}k")
|
493
|
+
end
|
494
|
+
|
495
|
+
return with_context.first if with_context.any?
|
496
|
+
end
|
497
|
+
|
498
|
+
matching_models.min_by do |model|
|
499
|
+
context_priority = if model =~ /:(?:\d+:)?(\d+)k/
|
500
|
+
-Regexp.last_match(1).to_i
|
501
|
+
else
|
502
|
+
0
|
503
|
+
end
|
504
|
+
|
505
|
+
version_priority = if model =~ /-v(\d+):/
|
506
|
+
-Regexp.last_match(1).to_i
|
507
|
+
else
|
508
|
+
0
|
509
|
+
end
|
510
|
+
|
511
|
+
has_context_priority = model.include?('k') ? -1 : 0
|
512
|
+
[has_context_priority, context_priority, version_priority]
|
513
|
+
end
|
514
|
+
end
|
data/lib/tasks/release.rake
CHANGED
@@ -1,6 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
namespace :release do
|
3
|
+
namespace :release do # rubocop:disable Metrics/BlockLength
|
4
|
+
desc 'Prepare for release'
|
5
|
+
task :prepare do
|
6
|
+
Rake::Task['release:refresh_stale_cassettes'].invoke
|
7
|
+
sh 'overcommit --run'
|
8
|
+
Rake::Task['models'].invoke
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Remove stale cassettes and re-record them'
|
12
|
+
task :refresh_stale_cassettes do
|
13
|
+
max_age_days = 1
|
14
|
+
cassette_dir = 'spec/fixtures/vcr_cassettes'
|
15
|
+
|
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
|
20
|
+
|
21
|
+
puts "Removing stale cassette: #{File.basename(cassette)} (#{age_days.round(1)} days old)"
|
22
|
+
File.delete(cassette)
|
23
|
+
stale_count += 1
|
24
|
+
end
|
25
|
+
|
26
|
+
if stale_count.positive?
|
27
|
+
puts "\nšļø Removed #{stale_count} stale cassettes"
|
28
|
+
puts 'š Re-recording cassettes...'
|
29
|
+
system('bundle exec rspec') || exit(1)
|
30
|
+
puts 'ā
Cassettes refreshed!'
|
31
|
+
else
|
32
|
+
puts 'ā
No stale cassettes found'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
4
36
|
desc 'Verify cassettes are fresh enough for release'
|
5
37
|
task :verify_cassettes do
|
6
38
|
max_age_days = 1
|
@@ -20,10 +52,13 @@ namespace :release do
|
|
20
52
|
|
21
53
|
if stale_cassettes.any?
|
22
54
|
puts "\nā Found stale cassettes (older than #{max_age_days} days):"
|
55
|
+
stale_files = []
|
23
56
|
stale_cassettes.each do |c|
|
24
57
|
puts " - #{c[:file]} (#{c[:age]} days old)"
|
58
|
+
stale_files << File.join(cassette_dir, '**', c[:file])
|
25
59
|
end
|
26
|
-
|
60
|
+
|
61
|
+
puts "\nRun locally: bundle exec rake release:refresh_stale_cassettes"
|
27
62
|
exit 1
|
28
63
|
else
|
29
64
|
puts "ā
All cassettes are fresh (< #{max_age_days} days old)"
|
data/lib/tasks/vcr.rake
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'dotenv/load'
|
4
4
|
|
5
|
-
# Helper functions at the top level
|
6
5
|
def record_all_cassettes(cassette_dir)
|
7
|
-
# Re-record all cassettes
|
8
6
|
FileUtils.rm_rf(cassette_dir)
|
9
7
|
FileUtils.mkdir_p(cassette_dir)
|
10
8
|
|
@@ -14,10 +12,8 @@ def record_all_cassettes(cassette_dir)
|
|
14
12
|
end
|
15
13
|
|
16
14
|
def record_for_providers(providers, cassette_dir)
|
17
|
-
# Get the list of available providers from RubyLLM itself
|
18
15
|
all_providers = RubyLLM::Provider.providers.keys.map(&:to_s)
|
19
16
|
|
20
|
-
# Check for valid providers
|
21
17
|
if providers.empty?
|
22
18
|
puts "Please specify providers or 'all'. Example: rake vcr:record[openai,anthropic]"
|
23
19
|
puts "Available providers: #{all_providers.join(', ')}"
|
@@ -31,7 +27,6 @@ def record_for_providers(providers, cassette_dir)
|
|
31
27
|
return
|
32
28
|
end
|
33
29
|
|
34
|
-
# Find and delete matching cassettes
|
35
30
|
cassettes_to_delete = find_matching_cassettes(cassette_dir, providers)
|
36
31
|
|
37
32
|
if cassettes_to_delete.empty?
|
@@ -54,9 +49,7 @@ def find_matching_cassettes(dir, providers)
|
|
54
49
|
Dir.glob("#{dir}/**/*.yml").each do |file|
|
55
50
|
basename = File.basename(file)
|
56
51
|
|
57
|
-
# Precise matching to avoid cross-provider confusion
|
58
52
|
providers.each do |provider|
|
59
|
-
# Match only exact provider prefixes
|
60
53
|
next unless basename =~ /^[^_]*_#{provider}_/ || # For first section like "chat_openai_"
|
61
54
|
basename =~ /_#{provider}_[^_]+_/ # For middle sections like "_openai_gpt4_"
|
62
55
|
|
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.6.
|
4
|
+
version: 1.6.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carmine Paolino
|
@@ -236,9 +236,7 @@ files:
|
|
236
236
|
- lib/ruby_llm/tool_call.rb
|
237
237
|
- lib/ruby_llm/utils.rb
|
238
238
|
- lib/ruby_llm/version.rb
|
239
|
-
- lib/tasks/
|
240
|
-
- lib/tasks/models_docs.rake
|
241
|
-
- lib/tasks/models_update.rake
|
239
|
+
- lib/tasks/models.rake
|
242
240
|
- lib/tasks/release.rake
|
243
241
|
- lib/tasks/vcr.rake
|
244
242
|
homepage: https://rubyllm.com
|