ruby_llm 1.14.1 → 1.16.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -7
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  5. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  6. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  7. data/lib/ruby_llm/active_record/acts_as.rb +4 -26
  8. data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
  9. data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
  10. data/lib/ruby_llm/active_record/message_methods.rb +87 -4
  11. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  12. data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
  13. data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
  14. data/lib/ruby_llm/agent.rb +4 -2
  15. data/lib/ruby_llm/aliases.json +108 -75
  16. data/lib/ruby_llm/aliases.rb +3 -0
  17. data/lib/ruby_llm/attachment.rb +41 -40
  18. data/lib/ruby_llm/chat.rb +229 -59
  19. data/lib/ruby_llm/configuration.rb +14 -1
  20. data/lib/ruby_llm/connection.rb +36 -7
  21. data/lib/ruby_llm/content.rb +15 -1
  22. data/lib/ruby_llm/cost.rb +224 -0
  23. data/lib/ruby_llm/deprecator.rb +24 -0
  24. data/lib/ruby_llm/embedding.rb +31 -1
  25. data/lib/ruby_llm/error.rb +11 -75
  26. data/lib/ruby_llm/error_middleware.rb +81 -0
  27. data/lib/ruby_llm/image.rb +39 -4
  28. data/lib/ruby_llm/instrumentation.rb +36 -0
  29. data/lib/ruby_llm/message.rb +20 -0
  30. data/lib/ruby_llm/mime_type.rb +25 -0
  31. data/lib/ruby_llm/model/info.rb +53 -2
  32. data/lib/ruby_llm/model/pricing.rb +19 -9
  33. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  34. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  35. data/lib/ruby_llm/model_registry.rb +39 -0
  36. data/lib/ruby_llm/models.json +17817 -13942
  37. data/lib/ruby_llm/models.rb +97 -31
  38. data/lib/ruby_llm/models_schema.json +3 -0
  39. data/lib/ruby_llm/provider.rb +20 -4
  40. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  41. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  42. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  43. data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
  44. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  45. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  46. data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
  47. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  48. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  49. data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
  50. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  51. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  52. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  53. data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
  54. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  55. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  56. data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
  57. data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
  58. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  59. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  60. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
  61. data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
  62. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  63. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  64. data/lib/ruby_llm/providers/mistral.rb +2 -2
  65. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  66. data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
  67. data/lib/ruby_llm/providers/openai/chat.rb +61 -7
  68. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  69. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  70. data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
  71. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  72. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  73. data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
  74. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  75. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  76. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  77. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  78. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  79. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  80. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  81. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  82. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  83. data/lib/ruby_llm/providers/xai.rb +2 -2
  84. data/lib/ruby_llm/railtie.rb +11 -1
  85. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  86. data/lib/ruby_llm/streaming.rb +4 -0
  87. data/lib/ruby_llm/tokens.rb +8 -0
  88. data/lib/ruby_llm/tool.rb +24 -7
  89. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  90. data/lib/ruby_llm/transcription.rb +2 -1
  91. data/lib/ruby_llm/utils.rb +39 -0
  92. data/lib/ruby_llm/version.rb +1 -1
  93. data/lib/ruby_llm.rb +11 -6
  94. data/lib/tasks/models.rake +45 -16
  95. data/lib/tasks/release.rake +50 -23
  96. metadata +35 -13
@@ -329,25 +329,26 @@ 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
- def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
351
+ def generate_aliases # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
351
352
  models = Hash.new { |h, k| h[k] = [] }
352
353
 
353
354
  RubyLLM.models.all.each do |model|
@@ -466,12 +467,40 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
466
467
  }
467
468
  end
468
469
 
470
+ add_xai_aliases(aliases, models['xai'])
471
+
469
472
  sorted_aliases = aliases.sort.to_h
470
473
  File.write(RubyLLM::Aliases.aliases_file, JSON.pretty_generate(sorted_aliases))
471
474
 
472
475
  puts "Generated #{sorted_aliases.size} aliases"
473
476
  end
474
477
 
478
+ def add_xai_aliases(aliases, xai_models)
479
+ return unless xai_models.include?('grok-4.3')
480
+
481
+ %w[
482
+ grok-latest
483
+ grok-3
484
+ grok-3-latest
485
+ grok-3-mini
486
+ grok-3-mini-latest
487
+ grok-4
488
+ grok-4-latest
489
+ grok-4-fast
490
+ grok-4-fast-reasoning
491
+ grok-4-fast-reasoning-latest
492
+ grok-4-fast-non-reasoning
493
+ grok-4-fast-non-reasoning-latest
494
+ grok-4-1-fast
495
+ grok-4-1-fast-reasoning
496
+ grok-4-1-fast-reasoning-latest
497
+ grok-4-1-fast-non-reasoning
498
+ grok-4-1-fast-non-reasoning-latest
499
+ ].each do |alias_key|
500
+ aliases[alias_key] ||= { 'xai' => 'grok-4.3' }
501
+ end
502
+ end
503
+
475
504
  def group_anthropic_models_by_base_name(anthropic_models)
476
505
  grouped = Hash.new { |h, k| h[k] = [] }
477
506
 
@@ -518,12 +547,12 @@ def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable M
518
547
  when 'claude-instant-v1', 'claude-instant'
519
548
  'claude-instant'
520
549
  else
521
- extract_base_name(anthropic_model)
550
+ anthropic_model
522
551
  end
523
552
 
524
553
  matching_models = bedrock_models.select do |bedrock_model|
525
- model_without_prefix = bedrock_model.sub(/^(?:us\.)?anthropic\./, '')
526
- model_without_prefix.start_with?(base_pattern)
554
+ model_without_prefix = bedrock_model.sub(/^(?:(?:[a-z]{2}|global)\.)?anthropic\./, '')
555
+ model_without_prefix.match?(/\A#{Regexp.escape(base_pattern)}(?:-v\d+|:\d+k|$)/)
527
556
  end
528
557
 
529
558
  return nil if matching_models.empty?
@@ -1,5 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+ require 'yaml'
5
+
6
+ # Shared helpers for release-related Rake tasks.
7
+ module ReleaseTasks
8
+ module_function
9
+
10
+ def cassette_recorded_at_times(cassette)
11
+ data = YAML.safe_load_file(cassette, aliases: true)
12
+
13
+ Array(data['http_interactions']).filter_map do |interaction|
14
+ Time.parse(interaction['recorded_at'])
15
+ rescue ArgumentError, TypeError
16
+ nil
17
+ end
18
+ rescue Psych::Exception => e
19
+ abort "Could not parse VCR cassette #{cassette}: #{e.message}"
20
+ end
21
+
22
+ def cassette_recorded_at(cassette)
23
+ recorded_at_times = cassette_recorded_at_times(cassette)
24
+
25
+ abort "No recorded_at timestamps found in VCR cassette #{cassette}" if recorded_at_times.empty?
26
+
27
+ recorded_at_times.min
28
+ end
29
+
30
+ def find_stale_cassettes(cassette_dir, max_age_days)
31
+ Dir.glob("#{cassette_dir}/**/*.yml").filter_map do |cassette|
32
+ recorded_at = cassette_recorded_at(cassette)
33
+ age_days = (Time.now - recorded_at) / 86_400
34
+
35
+ next unless age_days > max_age_days
36
+
37
+ {
38
+ path: cassette,
39
+ file: File.basename(cassette),
40
+ age: age_days.round(1)
41
+ }
42
+ end
43
+ end
44
+ end
45
+
3
46
  namespace :release do # rubocop:disable Metrics/BlockLength
4
47
  desc 'Prepare for release'
5
48
  task :prepare do
@@ -13,18 +56,15 @@ namespace :release do # rubocop:disable Metrics/BlockLength
13
56
  max_age_days = 1
14
57
  cassette_dir = 'spec/fixtures/vcr_cassettes'
15
58
 
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
59
+ stale_cassettes = ReleaseTasks.find_stale_cassettes(cassette_dir, max_age_days)
20
60
 
21
- puts "Removing stale cassette: #{File.basename(cassette)} (#{age_days.round(1)} days old)"
22
- File.delete(cassette)
23
- stale_count += 1
61
+ stale_cassettes.each do |cassette|
62
+ puts "Removing stale cassette: #{cassette[:file]} (#{cassette[:age]} days old)"
63
+ File.delete(cassette[:path])
24
64
  end
25
65
 
26
- if stale_count.positive?
27
- puts "\nšŸ—‘ļø Removed #{stale_count} stale cassettes"
66
+ if stale_cassettes.any?
67
+ puts "\nšŸ—‘ļø Removed #{stale_cassettes.size} stale cassettes"
28
68
  puts 'šŸ”„ Re-recording cassettes...'
29
69
  run_test_queue_rspec || exit(1)
30
70
  puts 'āœ… Cassettes refreshed!'
@@ -37,25 +77,12 @@ namespace :release do # rubocop:disable Metrics/BlockLength
37
77
  task :verify_cassettes do
38
78
  max_age_days = 1
39
79
  cassette_dir = 'spec/fixtures/vcr_cassettes'
40
- stale_cassettes = []
41
-
42
- Dir.glob("#{cassette_dir}/**/*.yml").each do |cassette|
43
- age_days = (Time.now - File.mtime(cassette)) / 86_400
44
-
45
- next unless age_days > max_age_days
46
-
47
- stale_cassettes << {
48
- file: File.basename(cassette),
49
- age: age_days.round(1)
50
- }
51
- end
80
+ stale_cassettes = ReleaseTasks.find_stale_cassettes(cassette_dir, max_age_days)
52
81
 
53
82
  if stale_cassettes.any?
54
83
  puts "\nāŒ Found stale cassettes (older than #{max_age_days} days):"
55
- stale_files = []
56
84
  stale_cassettes.each do |c|
57
85
  puts " - #{c[:file]} (#{c[:age]} days old)"
58
- stale_files << File.join(cassette_dir, '**', c[:file])
59
86
  end
60
87
 
61
88
  puts "\nRun locally: bundle exec rake release:refresh_stale_cassettes"
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.1
4
+ version: 1.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -135,13 +135,13 @@ dependencies:
135
135
  - - "~>"
136
136
  - !ruby/object:Gem::Version
137
137
  version: '2'
138
- description: One beautiful Ruby API for GPT, Claude, Gemini, and more. Easily build
139
- chatbots, AI agents, RAG applications, and content generators. Features chat (text,
140
- images, audio, PDFs), image generation, embeddings, tools (function calling), structured
141
- output, Rails integration, and streaming. Works with OpenAI, Anthropic, Google Gemini,
142
- AWS Bedrock, DeepSeek, Mistral, Ollama (local models), OpenRouter, Perplexity, GPUStack,
143
- and any OpenAI-compatible API. Minimal dependencies - just Faraday, Zeitwerk, and
144
- Marcel.
138
+ description: A single, beautiful Ruby framework for all major AI providers. Easily
139
+ build chatbots, AI agents, RAG applications, content generators, and every AI workflow
140
+ you can think of. Features chat (text, images, audio, PDFs), image generation, embeddings,
141
+ tools (function calling), structured output, Rails integration, and streaming. Works
142
+ with OpenAI, Anthropic, Google Gemini, AWS Bedrock, DeepSeek, Mistral, Ollama (local
143
+ models), OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API. Minimal
144
+ dependencies - just Faraday, Zeitwerk, and Marcel.
145
145
  email:
146
146
  - carmine@paolino.me
147
147
  executables: []
@@ -240,9 +240,13 @@ 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
244
+ - lib/ruby_llm/deprecator.rb
243
245
  - lib/ruby_llm/embedding.rb
244
246
  - lib/ruby_llm/error.rb
247
+ - lib/ruby_llm/error_middleware.rb
245
248
  - lib/ruby_llm/image.rb
249
+ - lib/ruby_llm/instrumentation.rb
246
250
  - lib/ruby_llm/message.rb
247
251
  - lib/ruby_llm/mime_type.rb
248
252
  - lib/ruby_llm/model.rb
@@ -251,6 +255,7 @@ files:
251
255
  - lib/ruby_llm/model/pricing.rb
252
256
  - lib/ruby_llm/model/pricing_category.rb
253
257
  - lib/ruby_llm/model/pricing_tier.rb
258
+ - lib/ruby_llm/model_registry.rb
254
259
  - lib/ruby_llm/models.json
255
260
  - lib/ruby_llm/models.rb
256
261
  - lib/ruby_llm/models_schema.json
@@ -298,6 +303,7 @@ files:
298
303
  - lib/ruby_llm/providers/mistral/capabilities.rb
299
304
  - lib/ruby_llm/providers/mistral/chat.rb
300
305
  - lib/ruby_llm/providers/mistral/embeddings.rb
306
+ - lib/ruby_llm/providers/mistral/media.rb
301
307
  - lib/ruby_llm/providers/mistral/models.rb
302
308
  - lib/ruby_llm/providers/ollama.rb
303
309
  - lib/ruby_llm/providers/ollama/capabilities.rb
@@ -324,6 +330,7 @@ files:
324
330
  - lib/ruby_llm/providers/perplexity.rb
325
331
  - lib/ruby_llm/providers/perplexity/capabilities.rb
326
332
  - lib/ruby_llm/providers/perplexity/chat.rb
333
+ - lib/ruby_llm/providers/perplexity/media.rb
327
334
  - lib/ruby_llm/providers/perplexity/models.rb
328
335
  - lib/ruby_llm/providers/vertexai.rb
329
336
  - lib/ruby_llm/providers/vertexai/chat.rb
@@ -341,6 +348,7 @@ files:
341
348
  - lib/ruby_llm/tokens.rb
342
349
  - lib/ruby_llm/tool.rb
343
350
  - lib/ruby_llm/tool_call.rb
351
+ - lib/ruby_llm/tool_concurrency.rb
344
352
  - lib/ruby_llm/transcription.rb
345
353
  - lib/ruby_llm/utils.rb
346
354
  - lib/ruby_llm/version.rb
@@ -354,14 +362,28 @@ licenses:
354
362
  metadata:
355
363
  homepage_uri: https://rubyllm.com
356
364
  source_code_uri: https://github.com/crmne/ruby_llm
357
- changelog_uri: https://github.com/crmne/ruby_llm/commits/main
365
+ changelog_uri: https://github.com/crmne/ruby_llm/releases
358
366
  documentation_uri: https://rubyllm.com
359
367
  bug_tracker_uri: https://github.com/crmne/ruby_llm/issues
360
368
  funding_uri: https://github.com/sponsors/crmne
361
369
  rubygems_mfa_required: 'true'
362
370
  post_install_message: |
363
- Upgrading from RubyLLM < 1.14.x? Check the upgrade guide for new features and migration instructions
364
- --> https://rubyllm.com/upgrading/
371
+ RubyLLM 1.15 upgrade note:
372
+
373
+ Token accounting is now normalized across providers. `input_tokens` means
374
+ standard input tokens; prompt cache reads and writes are exposed separately
375
+ as `cache_read_tokens` and `cache_write_tokens`.
376
+
377
+ Need request-side input activity?
378
+ input_tokens + cache_read_tokens + cache_write_tokens
379
+
380
+ New cost helpers:
381
+ response.cost.total
382
+ chat.cost.total
383
+ agent.cost.total
384
+
385
+ Upgrading from RubyLLM < 1.15? Read the full upgrade guide:
386
+ https://rubyllm.com/upgrading/
365
387
  rdoc_options: []
366
388
  require_paths:
367
389
  - lib
@@ -376,7 +398,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
376
398
  - !ruby/object:Gem::Version
377
399
  version: '0'
378
400
  requirements: []
379
- rubygems_version: 4.0.6
401
+ rubygems_version: 4.0.10
380
402
  specification_version: 4
381
- summary: One beautiful Ruby API for GPT, Claude, Gemini, and more.
403
+ summary: A single, beautiful Ruby framework for all major AI providers.
382
404
  test_files: []