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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -91
  3. data/lib/ruby_llm/active_record/acts_as.rb +14 -13
  4. data/lib/ruby_llm/aliases.json +8 -0
  5. data/lib/ruby_llm/aliases.rb +7 -25
  6. data/lib/ruby_llm/chat.rb +5 -12
  7. data/lib/ruby_llm/configuration.rb +1 -12
  8. data/lib/ruby_llm/content.rb +0 -2
  9. data/lib/ruby_llm/embedding.rb +1 -2
  10. data/lib/ruby_llm/error.rb +0 -8
  11. data/lib/ruby_llm/image.rb +0 -4
  12. data/lib/ruby_llm/message.rb +2 -4
  13. data/lib/ruby_llm/model/info.rb +0 -10
  14. data/lib/ruby_llm/model/pricing.rb +0 -3
  15. data/lib/ruby_llm/model/pricing_category.rb +0 -2
  16. data/lib/ruby_llm/model/pricing_tier.rb +0 -1
  17. data/lib/ruby_llm/models.json +623 -452
  18. data/lib/ruby_llm/models.rb +5 -13
  19. data/lib/ruby_llm/provider.rb +1 -5
  20. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
  21. data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
  22. data/lib/ruby_llm/providers/anthropic/tools.rb +1 -2
  23. data/lib/ruby_llm/providers/anthropic.rb +1 -2
  24. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -2
  25. data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
  26. data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
  27. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
  28. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
  29. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
  30. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
  31. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
  32. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
  33. data/lib/ruby_llm/providers/bedrock.rb +1 -2
  34. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
  35. data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
  36. data/lib/ruby_llm/providers/gemini/capabilities.rb +26 -101
  37. data/lib/ruby_llm/providers/gemini/chat.rb +57 -31
  38. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
  39. data/lib/ruby_llm/providers/gemini/images.rb +0 -1
  40. data/lib/ruby_llm/providers/gemini/media.rb +0 -1
  41. data/lib/ruby_llm/providers/gemini/models.rb +1 -2
  42. data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
  43. data/lib/ruby_llm/providers/gpustack/chat.rb +0 -1
  44. data/lib/ruby_llm/providers/gpustack/models.rb +3 -4
  45. data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
  46. data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
  47. data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
  48. data/lib/ruby_llm/providers/mistral/models.rb +0 -1
  49. data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
  50. data/lib/ruby_llm/providers/ollama/media.rb +0 -1
  51. data/lib/ruby_llm/providers/openai/capabilities.rb +0 -15
  52. data/lib/ruby_llm/providers/openai/chat.rb +0 -3
  53. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
  54. data/lib/ruby_llm/providers/openai/media.rb +0 -1
  55. data/lib/ruby_llm/providers/openai.rb +1 -3
  56. data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
  57. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
  58. data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
  59. data/lib/ruby_llm/providers/perplexity.rb +1 -5
  60. data/lib/ruby_llm/railtie.rb +0 -1
  61. data/lib/ruby_llm/stream_accumulator.rb +1 -3
  62. data/lib/ruby_llm/streaming.rb +15 -24
  63. data/lib/ruby_llm/tool.rb +2 -19
  64. data/lib/ruby_llm/tool_call.rb +0 -9
  65. data/lib/ruby_llm/version.rb +1 -1
  66. data/lib/ruby_llm.rb +0 -2
  67. data/lib/tasks/models.rake +514 -0
  68. data/lib/tasks/release.rake +37 -2
  69. data/lib/tasks/vcr.rake +0 -7
  70. metadata +2 -4
  71. data/lib/tasks/aliases.rake +0 -235
  72. data/lib/tasks/models_docs.rake +0 -224
  73. 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
@@ -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
- puts "\nRun locally: bundle exec rspec"
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.2
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/aliases.rake
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