ruby_llm 1.3.0rc1 → 1.3.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/lib/ruby_llm/active_record/acts_as.rb +66 -148
- data/lib/ruby_llm/aliases.json +170 -42
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +12 -4
- data/lib/ruby_llm/configuration.rb +5 -1
- data/lib/ruby_llm/connection.rb +28 -2
- data/lib/ruby_llm/content.rb +9 -40
- data/lib/ruby_llm/error.rb +1 -0
- data/lib/ruby_llm/image.rb +2 -3
- data/lib/ruby_llm/message.rb +2 -2
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +2220 -1915
- data/lib/ruby_llm/models.rb +20 -20
- data/lib/ruby_llm/provider.rb +1 -1
- data/lib/ruby_llm/providers/anthropic/media.rb +14 -3
- data/lib/ruby_llm/providers/anthropic/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/media.rb +7 -4
- data/lib/ruby_llm/providers/bedrock/models.rb +2 -2
- data/lib/ruby_llm/providers/gemini/images.rb +3 -2
- data/lib/ruby_llm/providers/gemini/media.rb +12 -24
- data/lib/ruby_llm/providers/gemini/models.rb +1 -1
- data/lib/ruby_llm/providers/ollama/media.rb +8 -4
- data/lib/ruby_llm/providers/openai/capabilities.rb +1 -1
- data/lib/ruby_llm/providers/openai/images.rb +3 -2
- data/lib/ruby_llm/providers/openai/media.rb +18 -8
- data/lib/ruby_llm/providers/openai/models.rb +1 -1
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
- data/lib/ruby_llm/streaming.rb +46 -11
- data/lib/ruby_llm/utils.rb +14 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/release.rake +32 -0
- metadata +40 -25
- data/lib/ruby_llm/attachments/audio.rb +0 -12
- data/lib/ruby_llm/attachments/image.rb +0 -9
- data/lib/ruby_llm/attachments/pdf.rb +0 -9
- data/lib/ruby_llm/attachments.rb +0 -78
- data/lib/ruby_llm/mime_types.rb +0 -713
- data/lib/ruby_llm/model_info.rb +0 -237
- data/lib/tasks/{models.rake → models_update.rake} +13 -13
data/lib/ruby_llm/models.rb
CHANGED
@@ -46,22 +46,25 @@ module RubyLLM
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
def resolve(model_id, provider: nil, assume_exists: false)
|
49
|
+
def resolve(model_id, provider: nil, assume_exists: false) # rubocop:disable Metrics/PerceivedComplexity
|
50
50
|
assume_exists = true if provider && Provider.providers[provider.to_sym].local?
|
51
51
|
|
52
52
|
if assume_exists
|
53
53
|
raise ArgumentError, 'Provider must be specified if assume_exists is true' unless provider
|
54
54
|
|
55
55
|
provider = Provider.providers[provider.to_sym] || raise(Error, "Unknown provider: #{provider.to_sym}")
|
56
|
-
model =
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
56
|
+
model = Model::Info.new(
|
57
|
+
id: model_id,
|
58
|
+
name: model_id.gsub('-', ' ').capitalize,
|
59
|
+
provider: provider.slug,
|
60
|
+
capabilities: %w[function_calling streaming],
|
61
|
+
modalities: { input: %w[text image], output: %w[text] },
|
62
|
+
metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
|
63
|
+
)
|
64
|
+
if RubyLLM.config.log_assume_model_exists
|
65
|
+
RubyLLM.logger.warn "Assuming model '#{model_id}' exists for provider '#{provider}'. " \
|
66
|
+
'Capabilities may not be accurately reflected.'
|
67
|
+
end
|
65
68
|
else
|
66
69
|
model = Models.find model_id, provider
|
67
70
|
provider = Provider.providers[model.provider.to_sym] || raise(Error, "Unknown provider: #{model.provider}")
|
@@ -84,15 +87,12 @@ module RubyLLM
|
|
84
87
|
def fetch_from_parsera
|
85
88
|
RubyLLM.logger.info 'Fetching models from Parsera API...'
|
86
89
|
|
87
|
-
connection =
|
90
|
+
connection = Connection.basic do |f|
|
88
91
|
f.request :json
|
89
|
-
f.response :json
|
90
|
-
f.response :raise_error
|
91
|
-
f.adapter Faraday.default_adapter
|
92
|
+
f.response :json, parser_options: { symbolize_names: true }
|
92
93
|
end
|
93
|
-
|
94
|
-
response
|
95
|
-
response.body.map { |data| ModelInfo.new(Utils.deep_symbolize_keys(data)) }
|
94
|
+
response = connection.get 'https://api.parsera.org/v1/llm-specs'
|
95
|
+
response.body.map { |data| Model::Info.new(data) }
|
96
96
|
end
|
97
97
|
|
98
98
|
def merge_models(provider_models, parsera_models)
|
@@ -130,10 +130,10 @@ module RubyLLM
|
|
130
130
|
end
|
131
131
|
|
132
132
|
def add_provider_metadata(parsera_model, provider_model)
|
133
|
-
# Create a new
|
133
|
+
# Create a new Model::Info with parsera data but include provider metadata
|
134
134
|
data = parsera_model.to_h
|
135
135
|
data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
|
136
|
-
|
136
|
+
Model::Info.new(data)
|
137
137
|
end
|
138
138
|
end
|
139
139
|
|
@@ -145,7 +145,7 @@ module RubyLLM
|
|
145
145
|
# Load models from the JSON file
|
146
146
|
def load_models
|
147
147
|
data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
|
148
|
-
JSON.parse(data).map { |model|
|
148
|
+
JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
|
149
149
|
rescue JSON::ParserError
|
150
150
|
[]
|
151
151
|
end
|
data/lib/ruby_llm/provider.rb
CHANGED
@@ -40,7 +40,7 @@ module RubyLLM
|
|
40
40
|
def paint(prompt, model:, size:, connection:)
|
41
41
|
payload = render_image_payload(prompt, model:, size:)
|
42
42
|
response = connection.post images_url, payload
|
43
|
-
parse_image_response
|
43
|
+
parse_image_response(response, model:)
|
44
44
|
end
|
45
45
|
|
46
46
|
def configured?(config = nil)
|
@@ -14,11 +14,15 @@ module RubyLLM
|
|
14
14
|
parts << format_text(content.text) if content.text
|
15
15
|
|
16
16
|
content.attachments.each do |attachment|
|
17
|
-
case attachment
|
18
|
-
when
|
17
|
+
case attachment.type
|
18
|
+
when :image
|
19
19
|
parts << format_image(attachment)
|
20
|
-
when
|
20
|
+
when :pdf
|
21
21
|
parts << format_pdf(attachment)
|
22
|
+
when :text
|
23
|
+
parts << format_text_file(attachment)
|
24
|
+
else
|
25
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
22
26
|
end
|
23
27
|
end
|
24
28
|
|
@@ -73,6 +77,13 @@ module RubyLLM
|
|
73
77
|
}
|
74
78
|
end
|
75
79
|
end
|
80
|
+
|
81
|
+
def format_text_file(text_file)
|
82
|
+
{
|
83
|
+
type: 'text',
|
84
|
+
text: Utils.format_text_file_for_llm(text_file)
|
85
|
+
}
|
86
|
+
end
|
76
87
|
end
|
77
88
|
end
|
78
89
|
end
|
@@ -4,6 +4,7 @@ module RubyLLM
|
|
4
4
|
module Providers
|
5
5
|
module Bedrock
|
6
6
|
# Media handling methods for the Bedrock API integration
|
7
|
+
# NOTE: Bedrock does not support url attachments
|
7
8
|
module Media
|
8
9
|
extend Anthropic::Media
|
9
10
|
|
@@ -16,13 +17,15 @@ module RubyLLM
|
|
16
17
|
parts << Anthropic::Media.format_text(content.text) if content.text
|
17
18
|
|
18
19
|
content.attachments.each do |attachment|
|
19
|
-
case attachment
|
20
|
-
when
|
20
|
+
case attachment.type
|
21
|
+
when :image
|
21
22
|
parts << format_image(attachment)
|
22
|
-
when
|
23
|
+
when :pdf
|
23
24
|
parts << format_pdf(attachment)
|
25
|
+
when :text
|
26
|
+
parts << Anthropic::Media.format_text_file(attachment)
|
24
27
|
else
|
25
|
-
raise
|
28
|
+
raise UnsupportedAttachmentError, attachment.type
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
@@ -30,7 +30,7 @@ module RubyLLM
|
|
30
30
|
models.select { |m| m['modelId'].include?('claude') }.map do |model_data|
|
31
31
|
model_id = model_data['modelId']
|
32
32
|
|
33
|
-
|
33
|
+
Model::Info.new(
|
34
34
|
id: model_id_with_region(model_id, model_data),
|
35
35
|
name: model_data['modelName'] || capabilities.format_display_name(model_id),
|
36
36
|
provider: slug,
|
@@ -56,7 +56,7 @@ module RubyLLM
|
|
56
56
|
def create_model_info(model_data, slug, _capabilities)
|
57
57
|
model_id = model_data['modelId']
|
58
58
|
|
59
|
-
|
59
|
+
Model::Info.new(
|
60
60
|
id: model_id_with_region(model_id, model_data),
|
61
61
|
name: model_data['modelName'] || model_id,
|
62
62
|
provider: slug,
|
@@ -24,7 +24,7 @@ module RubyLLM
|
|
24
24
|
}
|
25
25
|
end
|
26
26
|
|
27
|
-
def parse_image_response(response)
|
27
|
+
def parse_image_response(response, model:)
|
28
28
|
data = response.body
|
29
29
|
image_data = data['predictions']&.first
|
30
30
|
|
@@ -38,7 +38,8 @@ module RubyLLM
|
|
38
38
|
|
39
39
|
Image.new(
|
40
40
|
data: base64_data,
|
41
|
-
mime_type: mime_type
|
41
|
+
mime_type: mime_type,
|
42
|
+
model_id: model
|
42
43
|
)
|
43
44
|
end
|
44
45
|
end
|
@@ -14,43 +14,31 @@ module RubyLLM
|
|
14
14
|
parts << format_text(content.text) if content.text
|
15
15
|
|
16
16
|
content.attachments.each do |attachment|
|
17
|
-
case attachment
|
18
|
-
when
|
19
|
-
parts <<
|
20
|
-
when
|
21
|
-
|
22
|
-
|
23
|
-
parts <<
|
17
|
+
case attachment.type
|
18
|
+
when :text
|
19
|
+
parts << format_text_file(attachment)
|
20
|
+
when :unknown
|
21
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
22
|
+
else
|
23
|
+
parts << format_attachment(attachment)
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
parts
|
28
28
|
end
|
29
29
|
|
30
|
-
def
|
30
|
+
def format_attachment(attachment)
|
31
31
|
{
|
32
32
|
inline_data: {
|
33
|
-
mime_type:
|
34
|
-
data:
|
33
|
+
mime_type: attachment.mime_type,
|
34
|
+
data: attachment.encoded
|
35
35
|
}
|
36
36
|
}
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
39
|
+
def format_text_file(text_file)
|
40
40
|
{
|
41
|
-
|
42
|
-
mime_type: pdf.mime_type,
|
43
|
-
data: pdf.encoded
|
44
|
-
}
|
45
|
-
}
|
46
|
-
end
|
47
|
-
|
48
|
-
def format_audio(audio)
|
49
|
-
{
|
50
|
-
inline_data: {
|
51
|
-
mime_type: audio.mime_type,
|
52
|
-
data: audio.encoded
|
53
|
-
}
|
41
|
+
text: Utils.format_text_file_for_llm(text_file)
|
54
42
|
}
|
55
43
|
end
|
56
44
|
|
@@ -16,13 +16,17 @@ module RubyLLM
|
|
16
16
|
parts << format_text(content.text) if content.text
|
17
17
|
|
18
18
|
content.attachments.each do |attachment|
|
19
|
-
case attachment
|
20
|
-
when
|
19
|
+
case attachment.type
|
20
|
+
when :image
|
21
21
|
parts << Ollama::Media.format_image(attachment)
|
22
|
-
when
|
22
|
+
when :pdf
|
23
23
|
parts << format_pdf(attachment)
|
24
|
-
when
|
24
|
+
when :audio
|
25
25
|
parts << format_audio(attachment)
|
26
|
+
when :text
|
27
|
+
parts << format_text_file(attachment)
|
28
|
+
else
|
29
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
26
30
|
end
|
27
31
|
end
|
28
32
|
|
@@ -263,7 +263,7 @@ module RubyLLM
|
|
263
263
|
# Advanced capabilities
|
264
264
|
capabilities << 'reasoning' if model_id.match?(/o1/)
|
265
265
|
|
266
|
-
if model_id.match?(/gpt-4-turbo|gpt-4o
|
266
|
+
if model_id.match?(/gpt-4-turbo|gpt-4o/)
|
267
267
|
capabilities << 'image_generation' if model_id.match?(/vision/)
|
268
268
|
capabilities << 'speech_generation' if model_id.match?(/audio/)
|
269
269
|
capabilities << 'transcription' if model_id.match?(/audio/)
|
@@ -20,7 +20,7 @@ module RubyLLM
|
|
20
20
|
}
|
21
21
|
end
|
22
22
|
|
23
|
-
def parse_image_response(response)
|
23
|
+
def parse_image_response(response, model:)
|
24
24
|
data = response.body
|
25
25
|
image_data = data['data'].first
|
26
26
|
|
@@ -28,7 +28,8 @@ module RubyLLM
|
|
28
28
|
url: image_data['url'],
|
29
29
|
mime_type: 'image/png', # DALL-E typically returns PNGs
|
30
30
|
revised_prompt: image_data['revised_prompt'],
|
31
|
-
model_id:
|
31
|
+
model_id: model,
|
32
|
+
data: image_data['b64_json']
|
32
33
|
)
|
33
34
|
end
|
34
35
|
end
|
@@ -14,13 +14,17 @@ module RubyLLM
|
|
14
14
|
parts << format_text(content.text) if content.text
|
15
15
|
|
16
16
|
content.attachments.each do |attachment|
|
17
|
-
case attachment
|
18
|
-
when
|
17
|
+
case attachment.type
|
18
|
+
when :image
|
19
19
|
parts << format_image(attachment)
|
20
|
-
when
|
20
|
+
when :pdf
|
21
21
|
parts << format_pdf(attachment)
|
22
|
-
when
|
22
|
+
when :audio
|
23
23
|
parts << format_audio(attachment)
|
24
|
+
when :text
|
25
|
+
parts << format_text_file(attachment)
|
26
|
+
else
|
27
|
+
raise UnsupportedAttachmentError, attachment.type
|
24
28
|
end
|
25
29
|
end
|
26
30
|
|
@@ -31,8 +35,7 @@ module RubyLLM
|
|
31
35
|
{
|
32
36
|
type: 'image_url',
|
33
37
|
image_url: {
|
34
|
-
url: image.url? ? image.source : "data:#{image.mime_type};base64,#{image.encoded}"
|
35
|
-
detail: 'auto'
|
38
|
+
url: image.url? ? image.source : "data:#{image.mime_type};base64,#{image.encoded}"
|
36
39
|
}
|
37
40
|
}
|
38
41
|
end
|
@@ -41,18 +44,25 @@ module RubyLLM
|
|
41
44
|
{
|
42
45
|
type: 'file',
|
43
46
|
file: {
|
44
|
-
filename:
|
47
|
+
filename: pdf.filename,
|
45
48
|
file_data: "data:#{pdf.mime_type};base64,#{pdf.encoded}"
|
46
49
|
}
|
47
50
|
}
|
48
51
|
end
|
49
52
|
|
53
|
+
def format_text_file(text_file)
|
54
|
+
{
|
55
|
+
type: 'text',
|
56
|
+
text: Utils.format_text_file_for_llm(text_file)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
50
60
|
def format_audio(audio)
|
51
61
|
{
|
52
62
|
type: 'input_audio',
|
53
63
|
input_audio: {
|
54
64
|
data: audio.encoded,
|
55
|
-
format: audio.
|
65
|
+
format: audio.mime_type.split('/').last
|
56
66
|
}
|
57
67
|
}
|
58
68
|
end
|
@@ -37,7 +37,7 @@ module RubyLLM
|
|
37
37
|
# Convert OpenRouter's supported parameters to our capability format
|
38
38
|
capabilities = supported_parameters_to_capabilities(model_data['supported_parameters'])
|
39
39
|
|
40
|
-
|
40
|
+
Model::Info.new(
|
41
41
|
id: model_data['id'],
|
42
42
|
name: model_data['name'],
|
43
43
|
provider: slug,
|
data/lib/ruby_llm/streaming.rb
CHANGED
@@ -12,9 +12,18 @@ module RubyLLM
|
|
12
12
|
accumulator = StreamAccumulator.new
|
13
13
|
|
14
14
|
connection.post stream_url, payload do |req|
|
15
|
-
req.options.on_data
|
16
|
-
|
17
|
-
|
15
|
+
if req.options.respond_to?(:on_data)
|
16
|
+
# Handle Faraday 2.x streaming with on_data method
|
17
|
+
req.options.on_data = handle_stream do |chunk|
|
18
|
+
accumulator.add chunk
|
19
|
+
block.call chunk
|
20
|
+
end
|
21
|
+
else
|
22
|
+
# Handle Faraday 1.x streaming with :on_data key
|
23
|
+
req.options[:on_data] = handle_stream do |chunk|
|
24
|
+
accumulator.add chunk
|
25
|
+
block.call chunk
|
26
|
+
end
|
18
27
|
end
|
19
28
|
end
|
20
29
|
|
@@ -29,19 +38,45 @@ module RubyLLM
|
|
29
38
|
|
30
39
|
private
|
31
40
|
|
32
|
-
def to_json_stream(&
|
41
|
+
def to_json_stream(&)
|
33
42
|
buffer = String.new
|
34
43
|
parser = EventStreamParser::Parser.new
|
35
44
|
|
36
|
-
|
37
|
-
|
45
|
+
create_stream_processor(parser, buffer, &)
|
46
|
+
end
|
38
47
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
48
|
+
def create_stream_processor(parser, buffer, &)
|
49
|
+
if Faraday::VERSION.start_with?('1')
|
50
|
+
# Faraday 1.x: on_data receives (chunk, size)
|
51
|
+
legacy_stream_processor(parser, &)
|
52
|
+
else
|
53
|
+
# Faraday 2.x: on_data receives (chunk, bytes, env)
|
54
|
+
stream_processor(parser, buffer, &)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_stream_chunk(chunk, parser, _env, &)
|
59
|
+
RubyLLM.logger.debug "Received chunk: #{chunk}"
|
60
|
+
|
61
|
+
if error_chunk?(chunk)
|
62
|
+
handle_error_chunk(chunk, nil)
|
63
|
+
else
|
64
|
+
yield handle_sse(chunk, parser, nil, &)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def legacy_stream_processor(parser, &block)
|
69
|
+
proc do |chunk, _size|
|
70
|
+
process_stream_chunk(chunk, parser, nil, &block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def stream_processor(parser, buffer, &block)
|
75
|
+
proc do |chunk, _bytes, env|
|
76
|
+
if env&.status == 200
|
77
|
+
process_stream_chunk(chunk, parser, env, &block)
|
43
78
|
else
|
44
|
-
|
79
|
+
handle_failed_response(chunk, buffer, env)
|
45
80
|
end
|
46
81
|
end
|
47
82
|
end
|
data/lib/ruby_llm/utils.rb
CHANGED
@@ -5,17 +5,22 @@ module RubyLLM
|
|
5
5
|
module Utils
|
6
6
|
module_function
|
7
7
|
|
8
|
-
def
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
8
|
+
def format_text_file_for_llm(text_file)
|
9
|
+
"<file name='#{text_file.filename}' mime_type='#{text_file.mime_type}'>#{text_file.content}</file>"
|
10
|
+
end
|
11
|
+
|
12
|
+
def hash_get(hash, key)
|
13
|
+
hash[key.to_sym] || hash[key.to_s]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_safe_array(item)
|
17
|
+
case item
|
15
18
|
when Array
|
16
|
-
|
19
|
+
item
|
20
|
+
when Hash
|
21
|
+
[item]
|
17
22
|
else
|
18
|
-
|
23
|
+
Array(item)
|
19
24
|
end
|
20
25
|
end
|
21
26
|
end
|
data/lib/ruby_llm/version.rb
CHANGED