ruby_llm 1.15.0 ā 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.
- checksums.yaml +4 -4
- data/README.md +5 -4
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
- data/lib/ruby_llm/active_record/acts_as.rb +1 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
- data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
- data/lib/ruby_llm/active_record/message_methods.rb +70 -3
- data/lib/ruby_llm/agent.rb +1 -0
- data/lib/ruby_llm/aliases.json +78 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +34 -17
- data/lib/ruby_llm/chat.rb +176 -47
- data/lib/ruby_llm/configuration.rb +14 -1
- data/lib/ruby_llm/connection.rb +36 -7
- data/lib/ruby_llm/content.rb +15 -1
- data/lib/ruby_llm/deprecator.rb +24 -0
- data/lib/ruby_llm/embedding.rb +31 -1
- data/lib/ruby_llm/error.rb +11 -75
- data/lib/ruby_llm/error_middleware.rb +81 -0
- data/lib/ruby_llm/image.rb +2 -0
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +36 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_tier.rb +20 -9
- data/lib/ruby_llm/model_registry.rb +39 -0
- data/lib/ruby_llm/models.json +18225 -19144
- data/lib/ruby_llm/models.rb +95 -30
- data/lib/ruby_llm/provider.rb +11 -2
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
- data/lib/ruby_llm/providers/bedrock.rb +2 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
- data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
- data/lib/ruby_llm/providers/mistral/media.rb +55 -0
- data/lib/ruby_llm/providers/mistral/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral.rb +2 -2
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/openai/chat.rb +16 -1
- data/lib/ruby_llm/providers/openai/images.rb +9 -9
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
- data/lib/ruby_llm/providers/openai/tools.rb +2 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
- data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
- data/lib/ruby_llm/providers/perplexity.rb +2 -2
- data/lib/ruby_llm/providers/vertexai.rb +5 -1
- data/lib/ruby_llm/providers/xai/chat.rb +9 -0
- data/lib/ruby_llm/providers/xai/models.rb +15 -27
- data/lib/ruby_llm/providers/xai.rb +2 -2
- data/lib/ruby_llm/railtie.rb +5 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tool_concurrency.rb +105 -0
- data/lib/ruby_llm/transcription.rb +2 -1
- data/lib/ruby_llm/utils.rb +39 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +9 -2
- data/lib/tasks/models.rake +32 -4
- data/lib/tasks/release.rake +50 -23
- metadata +17 -10
|
@@ -8,7 +8,7 @@ module RubyLLM
|
|
|
8
8
|
include XAI::Models
|
|
9
9
|
|
|
10
10
|
def api_base
|
|
11
|
-
'https://api.x.ai/v1'
|
|
11
|
+
@config.xai_api_base || 'https://api.x.ai/v1'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def headers
|
|
@@ -20,7 +20,7 @@ module RubyLLM
|
|
|
20
20
|
|
|
21
21
|
class << self
|
|
22
22
|
def configuration_options
|
|
23
|
-
%i[xai_api_key]
|
|
23
|
+
%i[xai_api_key xai_api_base]
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def configuration_requirements
|
data/lib/ruby_llm/railtie.rb
CHANGED
|
@@ -10,6 +10,10 @@ if defined?(Rails::Railtie)
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
initializer 'ruby_llm.instrumentation' do
|
|
14
|
+
RubyLLM.config.instrumenter ||= ActiveSupport::Notifications
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
initializer 'ruby_llm.active_record' do
|
|
14
18
|
ActiveSupport.on_load :active_record do
|
|
15
19
|
require 'ruby_llm/active_record/payload_helpers'
|
|
@@ -25,7 +29,7 @@ if defined?(Rails::Railtie)
|
|
|
25
29
|
require 'ruby_llm/active_record/acts_as_legacy'
|
|
26
30
|
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
RubyLLM.deprecator.warn(
|
|
29
33
|
"\n!!! RubyLLM's legacy acts_as API is deprecated and will be removed in RubyLLM 2.0.0. " \
|
|
30
34
|
"Please consult the migration guide at https://rubyllm.com/upgrading-to-1-7/\n"
|
|
31
35
|
)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
3
6
|
module RubyLLM
|
|
4
7
|
# Assembles streaming responses from LLMs into complete messages.
|
|
5
8
|
class StreamAccumulator
|
|
@@ -18,6 +21,7 @@ module RubyLLM
|
|
|
18
21
|
@inside_think_tag = false
|
|
19
22
|
@pending_think_tag = +''
|
|
20
23
|
@latest_tool_call_id = nil
|
|
24
|
+
@tool_call_ids_by_index = {}
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def add(chunk)
|
|
@@ -72,43 +76,54 @@ module RubyLLM
|
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
|
|
75
|
-
def accumulate_tool_calls(new_tool_calls)
|
|
79
|
+
def accumulate_tool_calls(new_tool_calls)
|
|
76
80
|
RubyLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if RubyLLM.config.log_stream_debug
|
|
77
|
-
new_tool_calls.
|
|
81
|
+
new_tool_calls.each do |stream_key, tool_call|
|
|
78
82
|
if tool_call.id
|
|
79
|
-
|
|
80
|
-
tool_call_arguments = tool_call.arguments
|
|
81
|
-
if tool_call_arguments.nil? || (tool_call_arguments.respond_to?(:empty?) && tool_call_arguments.empty?)
|
|
82
|
-
tool_call_arguments = +''
|
|
83
|
-
end
|
|
84
|
-
@tool_calls[tool_call.id] = ToolCall.new(
|
|
85
|
-
id: tool_call_id,
|
|
86
|
-
name: tool_call.name,
|
|
87
|
-
arguments: tool_call_arguments,
|
|
88
|
-
thought_signature: tool_call.thought_signature
|
|
89
|
-
)
|
|
90
|
-
@latest_tool_call_id = tool_call.id
|
|
83
|
+
start_tool_call(stream_key, tool_call)
|
|
91
84
|
else
|
|
92
|
-
|
|
93
|
-
if existing
|
|
94
|
-
fragment = tool_call.arguments
|
|
95
|
-
fragment = '' if fragment.nil?
|
|
96
|
-
existing.arguments << fragment
|
|
97
|
-
if tool_call.thought_signature && existing.thought_signature.nil?
|
|
98
|
-
existing.thought_signature = tool_call.thought_signature
|
|
99
|
-
end
|
|
100
|
-
end
|
|
85
|
+
append_tool_call_fragment(stream_key, tool_call)
|
|
101
86
|
end
|
|
102
87
|
end
|
|
103
88
|
end
|
|
104
89
|
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
90
|
+
def start_tool_call(stream_key, tool_call)
|
|
91
|
+
tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
|
|
92
|
+
tool_call_key = tool_call.id
|
|
93
|
+
|
|
94
|
+
@tool_calls[tool_call_key] = ToolCall.new(
|
|
95
|
+
id: tool_call_id,
|
|
96
|
+
name: tool_call.name,
|
|
97
|
+
arguments: initial_tool_call_arguments(tool_call),
|
|
98
|
+
thought_signature: tool_call.thought_signature
|
|
99
|
+
)
|
|
100
|
+
@tool_call_ids_by_index[stream_key] = tool_call_key unless stream_key.nil?
|
|
101
|
+
@latest_tool_call_id = tool_call_key
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def initial_tool_call_arguments(tool_call)
|
|
105
|
+
arguments = tool_call.arguments
|
|
106
|
+
return +'' if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
|
|
107
|
+
|
|
108
|
+
arguments
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def append_tool_call_fragment(stream_key, tool_call)
|
|
112
|
+
existing = find_tool_call(stream_key)
|
|
113
|
+
return unless existing
|
|
114
|
+
|
|
115
|
+
fragment = tool_call.arguments
|
|
116
|
+
fragment = '' if fragment.nil?
|
|
117
|
+
existing.arguments << fragment
|
|
118
|
+
return unless tool_call.thought_signature && existing.thought_signature.nil?
|
|
119
|
+
|
|
120
|
+
existing.thought_signature = tool_call.thought_signature
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def find_tool_call(stream_key)
|
|
124
|
+
return @tool_calls[@latest_tool_call_id] if stream_key.nil?
|
|
125
|
+
|
|
126
|
+
@tool_calls[@tool_call_ids_by_index[stream_key]] || @tool_calls[stream_key]
|
|
112
127
|
end
|
|
113
128
|
|
|
114
129
|
def count_tokens(chunk)
|
data/lib/ruby_llm/streaming.rb
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Runs multiple tool calls concurrently with Ruby's built-in threads or optional fibers.
|
|
5
|
+
module ToolConcurrency
|
|
6
|
+
MODES = %i[threads fibers].freeze
|
|
7
|
+
Result = Struct.new(:index, :tool_call, :value, :error, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def modes
|
|
12
|
+
MODES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def supported?(name)
|
|
16
|
+
MODES.include?(name)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(mode, tool_calls, on_result: nil, &)
|
|
20
|
+
case mode
|
|
21
|
+
when :threads
|
|
22
|
+
run_with_threads(tool_calls, on_result:, &)
|
|
23
|
+
when :fibers
|
|
24
|
+
run_with_fibers(tool_calls, on_result:, &)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run_with_threads(tool_calls, on_result:, &execute)
|
|
29
|
+
executor = rails_executor
|
|
30
|
+
queue = Queue.new
|
|
31
|
+
threads = tool_calls.each_value.with_index.map do |tool_call, index|
|
|
32
|
+
thread = Thread.new { queue << capture_result(index, tool_call, executor, execute) }
|
|
33
|
+
thread.report_on_exception = false
|
|
34
|
+
thread
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
collect_results(queue, threads.size, on_result:)
|
|
38
|
+
ensure
|
|
39
|
+
threads&.each(&:join)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def run_with_fibers(tool_calls, on_result:, &execute)
|
|
43
|
+
begin
|
|
44
|
+
require 'async'
|
|
45
|
+
require 'async/queue'
|
|
46
|
+
rescue LoadError
|
|
47
|
+
raise LoadError, "The 'async' gem is required for concurrent tool execution with fibers. " \
|
|
48
|
+
"Add `gem 'async'` to your Gemfile or use `concurrency: :threads`."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
executor = rails_executor
|
|
52
|
+
Async do |task|
|
|
53
|
+
queue = Async::Queue.new
|
|
54
|
+
tasks = tool_calls.each_value.with_index.map do |tool_call, index|
|
|
55
|
+
task.async { queue << capture_result(index, tool_call, executor, execute) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
collect_results(queue, tasks.size, on_result:)
|
|
59
|
+
ensure
|
|
60
|
+
tasks&.each(&:wait)
|
|
61
|
+
end.wait
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def collect_results(queue, count, on_result:)
|
|
65
|
+
results = Array.new(count)
|
|
66
|
+
errors = []
|
|
67
|
+
|
|
68
|
+
count.times do
|
|
69
|
+
result = queue.pop
|
|
70
|
+
if result.error
|
|
71
|
+
errors << result.error
|
|
72
|
+
else
|
|
73
|
+
results[result.index] = [result.tool_call, result.value]
|
|
74
|
+
on_result&.call(result.tool_call, result.value)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
raise errors.first if errors.any?
|
|
79
|
+
|
|
80
|
+
results
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def capture_result(index, tool_call, rails_executor, execute)
|
|
84
|
+
tool_call, value = run_tool_call(tool_call, rails_executor, execute)
|
|
85
|
+
Result.new(index:, tool_call:, value:)
|
|
86
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
87
|
+
Result.new(index:, tool_call:, error: e)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run_tool_call(tool_call, rails_executor, execute)
|
|
91
|
+
if rails_executor
|
|
92
|
+
rails_executor.wrap { [tool_call, execute.call(tool_call)] }
|
|
93
|
+
else
|
|
94
|
+
[tool_call, execute.call(tool_call)]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def rails_executor
|
|
99
|
+
defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.executor
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private_class_method :run_with_threads, :run_with_fibers, :collect_results, :capture_result, :run_tool_call,
|
|
103
|
+
:rails_executor
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
# Represents a transcription of audio content.
|
|
5
5
|
class Transcription
|
|
6
|
-
attr_reader :text, :model, :language, :duration, :segments, :input_tokens, :output_tokens
|
|
6
|
+
attr_reader :text, :model, :language, :duration, :segments, :words, :input_tokens, :output_tokens
|
|
7
7
|
|
|
8
8
|
def initialize(text:, model:, **attributes)
|
|
9
9
|
@text = text
|
|
@@ -11,6 +11,7 @@ module RubyLLM
|
|
|
11
11
|
@language = attributes[:language]
|
|
12
12
|
@duration = attributes[:duration]
|
|
13
13
|
@segments = attributes[:segments]
|
|
14
|
+
@words = attributes[:words]
|
|
14
15
|
@input_tokens = attributes[:input_tokens]
|
|
15
16
|
@output_tokens = attributes[:output_tokens]
|
|
16
17
|
end
|
data/lib/ruby_llm/utils.rb
CHANGED
|
@@ -32,6 +32,45 @@ module RubyLLM
|
|
|
32
32
|
value.is_a?(Date) ? value : Date.parse(value.to_s)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def safe_constantize(name)
|
|
36
|
+
parts = name.to_s.split('::').reject(&:empty?)
|
|
37
|
+
return if parts.empty?
|
|
38
|
+
|
|
39
|
+
namespace = Object
|
|
40
|
+
until parts.empty?
|
|
41
|
+
const_name = parts.shift
|
|
42
|
+
return unless namespace.const_defined?(const_name, false)
|
|
43
|
+
|
|
44
|
+
namespace = namespace.const_get(const_name, false)
|
|
45
|
+
end
|
|
46
|
+
namespace
|
|
47
|
+
rescue NameError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_iso_date_prefix(value)
|
|
52
|
+
return value if value.is_a?(Date)
|
|
53
|
+
|
|
54
|
+
date = value.to_s.strip
|
|
55
|
+
return if date.empty?
|
|
56
|
+
|
|
57
|
+
case date
|
|
58
|
+
when /\A\d{4}-\d{2}-\d{2}\z/
|
|
59
|
+
Date.iso8601(date)
|
|
60
|
+
when /\A\d{4}-\d{2}\z/
|
|
61
|
+
Date.iso8601("#{date}-01")
|
|
62
|
+
when /\A\d{4}\z/
|
|
63
|
+
Date.iso8601("#{date}-01-01")
|
|
64
|
+
end
|
|
65
|
+
rescue ArgumentError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def iso_date_prefix_to_utc_midnight_string(value)
|
|
70
|
+
date = parse_iso_date_prefix(value)
|
|
71
|
+
"#{date.strftime('%Y-%m-%d')} 00:00:00 UTC" if date
|
|
72
|
+
end
|
|
73
|
+
|
|
35
74
|
def deep_merge(original, overrides)
|
|
36
75
|
original.merge(overrides) do |_key, original_value, overrides_value|
|
|
37
76
|
if original_value.is_a?(Hash) && overrides_value.is_a?(Hash)
|
data/lib/ruby_llm/version.rb
CHANGED
data/lib/ruby_llm.rb
CHANGED
|
@@ -12,6 +12,7 @@ require 'securerandom'
|
|
|
12
12
|
require 'date'
|
|
13
13
|
require 'time'
|
|
14
14
|
require 'zeitwerk'
|
|
15
|
+
require 'ruby_llm/error'
|
|
15
16
|
|
|
16
17
|
loader = Zeitwerk::Loader.for_gem
|
|
17
18
|
loader.inflector.inflect(
|
|
@@ -39,9 +40,15 @@ loader.setup
|
|
|
39
40
|
|
|
40
41
|
# A delightful Ruby interface to modern AI language models.
|
|
41
42
|
module RubyLLM
|
|
42
|
-
class Error < StandardError; end
|
|
43
|
-
|
|
44
43
|
class << self
|
|
44
|
+
def deprecator
|
|
45
|
+
@deprecator ||= Deprecator.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def instrument(...)
|
|
49
|
+
Instrumentation.instrument(...)
|
|
50
|
+
end
|
|
51
|
+
|
|
45
52
|
def context
|
|
46
53
|
context_config = config.dup
|
|
47
54
|
yield context_config if block_given?
|
data/lib/tasks/models.rake
CHANGED
|
@@ -348,7 +348,7 @@ def pricing_part(pricing_data, key, label)
|
|
|
348
348
|
"#{label}: $#{format('%.2f', pricing_data[key])}"
|
|
349
349
|
end
|
|
350
350
|
|
|
351
|
-
def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
|
|
351
|
+
def generate_aliases # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
352
352
|
models = Hash.new { |h, k| h[k] = [] }
|
|
353
353
|
|
|
354
354
|
RubyLLM.models.all.each do |model|
|
|
@@ -467,12 +467,40 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
|
|
|
467
467
|
}
|
|
468
468
|
end
|
|
469
469
|
|
|
470
|
+
add_xai_aliases(aliases, models['xai'])
|
|
471
|
+
|
|
470
472
|
sorted_aliases = aliases.sort.to_h
|
|
471
473
|
File.write(RubyLLM::Aliases.aliases_file, JSON.pretty_generate(sorted_aliases))
|
|
472
474
|
|
|
473
475
|
puts "Generated #{sorted_aliases.size} aliases"
|
|
474
476
|
end
|
|
475
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
|
+
|
|
476
504
|
def group_anthropic_models_by_base_name(anthropic_models)
|
|
477
505
|
grouped = Hash.new { |h, k| h[k] = [] }
|
|
478
506
|
|
|
@@ -519,12 +547,12 @@ def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable M
|
|
|
519
547
|
when 'claude-instant-v1', 'claude-instant'
|
|
520
548
|
'claude-instant'
|
|
521
549
|
else
|
|
522
|
-
|
|
550
|
+
anthropic_model
|
|
523
551
|
end
|
|
524
552
|
|
|
525
553
|
matching_models = bedrock_models.select do |bedrock_model|
|
|
526
|
-
model_without_prefix = bedrock_model.sub(/^(?:
|
|
527
|
-
model_without_prefix.
|
|
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|$)/)
|
|
528
556
|
end
|
|
529
557
|
|
|
530
558
|
return nil if matching_models.empty?
|
data/lib/tasks/release.rake
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
27
|
-
puts "\nšļø Removed #{
|
|
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.
|
|
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:
|
|
139
|
-
chatbots, AI agents, RAG applications,
|
|
140
|
-
images, audio, PDFs), image generation, embeddings,
|
|
141
|
-
output, Rails integration, and streaming. Works
|
|
142
|
-
AWS Bedrock, DeepSeek, Mistral, Ollama (local
|
|
143
|
-
and any OpenAI-compatible API. Minimal
|
|
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: []
|
|
@@ -241,9 +241,12 @@ files:
|
|
|
241
241
|
- lib/ruby_llm/content.rb
|
|
242
242
|
- lib/ruby_llm/context.rb
|
|
243
243
|
- lib/ruby_llm/cost.rb
|
|
244
|
+
- lib/ruby_llm/deprecator.rb
|
|
244
245
|
- lib/ruby_llm/embedding.rb
|
|
245
246
|
- lib/ruby_llm/error.rb
|
|
247
|
+
- lib/ruby_llm/error_middleware.rb
|
|
246
248
|
- lib/ruby_llm/image.rb
|
|
249
|
+
- lib/ruby_llm/instrumentation.rb
|
|
247
250
|
- lib/ruby_llm/message.rb
|
|
248
251
|
- lib/ruby_llm/mime_type.rb
|
|
249
252
|
- lib/ruby_llm/model.rb
|
|
@@ -252,6 +255,7 @@ files:
|
|
|
252
255
|
- lib/ruby_llm/model/pricing.rb
|
|
253
256
|
- lib/ruby_llm/model/pricing_category.rb
|
|
254
257
|
- lib/ruby_llm/model/pricing_tier.rb
|
|
258
|
+
- lib/ruby_llm/model_registry.rb
|
|
255
259
|
- lib/ruby_llm/models.json
|
|
256
260
|
- lib/ruby_llm/models.rb
|
|
257
261
|
- lib/ruby_llm/models_schema.json
|
|
@@ -299,6 +303,7 @@ files:
|
|
|
299
303
|
- lib/ruby_llm/providers/mistral/capabilities.rb
|
|
300
304
|
- lib/ruby_llm/providers/mistral/chat.rb
|
|
301
305
|
- lib/ruby_llm/providers/mistral/embeddings.rb
|
|
306
|
+
- lib/ruby_llm/providers/mistral/media.rb
|
|
302
307
|
- lib/ruby_llm/providers/mistral/models.rb
|
|
303
308
|
- lib/ruby_llm/providers/ollama.rb
|
|
304
309
|
- lib/ruby_llm/providers/ollama/capabilities.rb
|
|
@@ -325,6 +330,7 @@ files:
|
|
|
325
330
|
- lib/ruby_llm/providers/perplexity.rb
|
|
326
331
|
- lib/ruby_llm/providers/perplexity/capabilities.rb
|
|
327
332
|
- lib/ruby_llm/providers/perplexity/chat.rb
|
|
333
|
+
- lib/ruby_llm/providers/perplexity/media.rb
|
|
328
334
|
- lib/ruby_llm/providers/perplexity/models.rb
|
|
329
335
|
- lib/ruby_llm/providers/vertexai.rb
|
|
330
336
|
- lib/ruby_llm/providers/vertexai/chat.rb
|
|
@@ -342,6 +348,7 @@ files:
|
|
|
342
348
|
- lib/ruby_llm/tokens.rb
|
|
343
349
|
- lib/ruby_llm/tool.rb
|
|
344
350
|
- lib/ruby_llm/tool_call.rb
|
|
351
|
+
- lib/ruby_llm/tool_concurrency.rb
|
|
345
352
|
- lib/ruby_llm/transcription.rb
|
|
346
353
|
- lib/ruby_llm/utils.rb
|
|
347
354
|
- lib/ruby_llm/version.rb
|
|
@@ -391,7 +398,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
391
398
|
- !ruby/object:Gem::Version
|
|
392
399
|
version: '0'
|
|
393
400
|
requirements: []
|
|
394
|
-
rubygems_version: 4.0.
|
|
401
|
+
rubygems_version: 4.0.10
|
|
395
402
|
specification_version: 4
|
|
396
|
-
summary:
|
|
403
|
+
summary: A single, beautiful Ruby framework for all major AI providers.
|
|
397
404
|
test_files: []
|