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
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
namespace :aliases do # rubocop:disable Metrics/BlockLength
|
6
|
+
desc 'Generate aliases.json from models in the registry'
|
7
|
+
task :generate do # rubocop:disable Metrics/BlockLength
|
8
|
+
require 'ruby_llm'
|
9
|
+
|
10
|
+
# Group models by provider
|
11
|
+
models = Hash.new { |h, k| h[k] = [] }
|
12
|
+
|
13
|
+
RubyLLM.models.all.each do |model|
|
14
|
+
models[model.provider] << model.id
|
15
|
+
end
|
16
|
+
|
17
|
+
aliases = {}
|
18
|
+
|
19
|
+
# OpenAI models
|
20
|
+
models['openai'].each do |model|
|
21
|
+
openrouter_model = "openai/#{model}"
|
22
|
+
next unless models['openrouter'].include?(openrouter_model)
|
23
|
+
|
24
|
+
alias_key = model.gsub('-latest', '')
|
25
|
+
aliases[alias_key] = {
|
26
|
+
'openai' => model,
|
27
|
+
'openrouter' => openrouter_model
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Anthropic models - group by base name and find latest
|
32
|
+
anthropic_latest = group_anthropic_models_by_base_name(models['anthropic'])
|
33
|
+
|
34
|
+
anthropic_latest.each do |base_name, latest_model|
|
35
|
+
# Check OpenRouter naming patterns for the BASE NAME (not the full dated model)
|
36
|
+
openrouter_variants = [
|
37
|
+
"anthropic/#{base_name}", # anthropic/claude-3-5-sonnet
|
38
|
+
"anthropic/#{base_name.gsub(/-(\d)/, '.\1')}", # anthropic/claude-3.5-sonnet
|
39
|
+
"anthropic/#{base_name.gsub(/claude-(\d+)-(\d+)/, 'claude-\1.\2')}", # claude-3-5 -> claude-3.5
|
40
|
+
"anthropic/#{base_name.gsub(/(\d+)-(\d+)/, '\1.\2')}" # any X-Y -> X.Y pattern
|
41
|
+
]
|
42
|
+
|
43
|
+
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
44
|
+
|
45
|
+
# Find corresponding Bedrock model
|
46
|
+
bedrock_model = find_best_bedrock_model(latest_model, models['bedrock'])
|
47
|
+
|
48
|
+
# Create alias if we have any match (OpenRouter OR Bedrock) OR if it's Anthropic-only
|
49
|
+
next unless openrouter_model || bedrock_model || models['anthropic'].include?(latest_model)
|
50
|
+
|
51
|
+
aliases[base_name] = {
|
52
|
+
'anthropic' => latest_model
|
53
|
+
}
|
54
|
+
|
55
|
+
aliases[base_name]['openrouter'] = openrouter_model if openrouter_model
|
56
|
+
aliases[base_name]['bedrock'] = bedrock_model if bedrock_model
|
57
|
+
end
|
58
|
+
|
59
|
+
# Also check if Bedrock has models that Anthropic doesn't
|
60
|
+
models['bedrock'].each do |bedrock_model|
|
61
|
+
next unless bedrock_model.start_with?('anthropic.')
|
62
|
+
|
63
|
+
# Extract the Claude model name
|
64
|
+
next unless bedrock_model =~ /anthropic\.(claude-[\d\.]+-[a-z]+)/
|
65
|
+
|
66
|
+
base_name = Regexp.last_match(1)
|
67
|
+
# Normalize to Anthropic naming convention
|
68
|
+
anthropic_name = base_name.gsub('.', '-')
|
69
|
+
|
70
|
+
# Skip if we already have an alias for this
|
71
|
+
next if aliases[anthropic_name]
|
72
|
+
|
73
|
+
# Check if this model exists in OpenRouter
|
74
|
+
openrouter_variants = [
|
75
|
+
"anthropic/#{anthropic_name}",
|
76
|
+
"anthropic/#{base_name}" # Keep the dots
|
77
|
+
]
|
78
|
+
|
79
|
+
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
80
|
+
|
81
|
+
aliases[anthropic_name] = {
|
82
|
+
'bedrock' => bedrock_model
|
83
|
+
}
|
84
|
+
|
85
|
+
aliases[anthropic_name]['anthropic'] = anthropic_name if models['anthropic'].include?(anthropic_name)
|
86
|
+
aliases[anthropic_name]['openrouter'] = openrouter_model if openrouter_model
|
87
|
+
end
|
88
|
+
|
89
|
+
# Gemini models
|
90
|
+
models['gemini'].each do |model|
|
91
|
+
# OpenRouter uses "google/" prefix and sometimes different naming
|
92
|
+
openrouter_variants = [
|
93
|
+
"google/#{model}",
|
94
|
+
"google/#{model.gsub('gemini-', 'gemini-').gsub('.', '-')}",
|
95
|
+
"google/#{model.gsub('gemini-', 'gemini-')}"
|
96
|
+
]
|
97
|
+
|
98
|
+
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
99
|
+
next unless openrouter_model
|
100
|
+
|
101
|
+
alias_key = model.gsub('-latest', '')
|
102
|
+
aliases[alias_key] = {
|
103
|
+
'gemini' => model,
|
104
|
+
'openrouter' => openrouter_model
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
# DeepSeek models
|
109
|
+
models['deepseek'].each do |model|
|
110
|
+
openrouter_model = "deepseek/#{model}"
|
111
|
+
next unless models['openrouter'].include?(openrouter_model)
|
112
|
+
|
113
|
+
alias_key = model.gsub('-latest', '')
|
114
|
+
aliases[alias_key] = {
|
115
|
+
'deepseek' => model,
|
116
|
+
'openrouter' => openrouter_model
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
# Write the result
|
121
|
+
sorted_aliases = aliases.sort.to_h
|
122
|
+
File.write('lib/ruby_llm/aliases.json', JSON.pretty_generate(sorted_aliases))
|
123
|
+
|
124
|
+
puts "Generated #{sorted_aliases.size} aliases"
|
125
|
+
end
|
126
|
+
|
127
|
+
def group_anthropic_models_by_base_name(anthropic_models) # rubocop:disable Rake/MethodDefinitionInTask
|
128
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
129
|
+
|
130
|
+
anthropic_models.each do |model|
|
131
|
+
base_name = extract_base_name(model)
|
132
|
+
grouped[base_name] << model
|
133
|
+
end
|
134
|
+
|
135
|
+
# Find the latest model for each base name
|
136
|
+
latest_models = {}
|
137
|
+
grouped.each do |base_name, model_list|
|
138
|
+
if model_list.size == 1
|
139
|
+
latest_models[base_name] = model_list.first
|
140
|
+
else
|
141
|
+
# Sort by date and take the latest
|
142
|
+
latest_model = model_list.max_by { |model| extract_date_from_model(model) }
|
143
|
+
latest_models[base_name] = latest_model
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
latest_models
|
148
|
+
end
|
149
|
+
|
150
|
+
def extract_base_name(model) # rubocop:disable Rake/MethodDefinitionInTask
|
151
|
+
# Remove date suffix (YYYYMMDD) from model name
|
152
|
+
if model =~ /^(.+)-(\d{8})$/
|
153
|
+
Regexp.last_match(1)
|
154
|
+
else
|
155
|
+
# Models without date suffix (like claude-2.0, claude-2.1)
|
156
|
+
model
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def extract_date_from_model(model) # rubocop:disable Rake/MethodDefinitionInTask
|
161
|
+
# Extract date for comparison, return '00000000' for models without dates
|
162
|
+
if model =~ /-(\d{8})$/
|
163
|
+
Regexp.last_match(1)
|
164
|
+
else
|
165
|
+
'00000000' # Ensures models without dates sort before dated ones
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable Metrics/PerceivedComplexity,Rake/MethodDefinitionInTask
|
170
|
+
# Special mapping for Claude 2.x models
|
171
|
+
base_pattern = case anthropic_model
|
172
|
+
when 'claude-2.0', 'claude-2'
|
173
|
+
'claude-v2'
|
174
|
+
when 'claude-2.1'
|
175
|
+
'claude-v2:1'
|
176
|
+
when 'claude-instant-v1', 'claude-instant'
|
177
|
+
'claude-instant'
|
178
|
+
else
|
179
|
+
# For Claude 3+ models, extract base name
|
180
|
+
extract_base_name(anthropic_model)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Find all matching Bedrock models by stripping provider prefix and comparing base name
|
184
|
+
matching_models = bedrock_models.select do |bedrock_model|
|
185
|
+
# Strip any provider prefix (anthropic. or us.anthropic.)
|
186
|
+
model_without_prefix = bedrock_model.sub(/^(?:us\.)?anthropic\./, '')
|
187
|
+
model_without_prefix.start_with?(base_pattern)
|
188
|
+
end
|
189
|
+
|
190
|
+
return nil if matching_models.empty?
|
191
|
+
|
192
|
+
# Get model info to check context window
|
193
|
+
begin
|
194
|
+
model_info = RubyLLM.models.find(anthropic_model)
|
195
|
+
target_context = model_info.context_window
|
196
|
+
rescue StandardError
|
197
|
+
target_context = nil
|
198
|
+
end
|
199
|
+
|
200
|
+
# If we have context window info, try to match it
|
201
|
+
if target_context
|
202
|
+
# Convert to k format (200000 -> 200k)
|
203
|
+
target_k = target_context / 1000
|
204
|
+
|
205
|
+
# Find models with this specific context window
|
206
|
+
with_context = matching_models.select do |m|
|
207
|
+
m.include?(":#{target_k}k") || m.include?(":0:#{target_k}k")
|
208
|
+
end
|
209
|
+
|
210
|
+
return with_context.first if with_context.any?
|
211
|
+
end
|
212
|
+
|
213
|
+
# Otherwise, pick the one with the highest context window or latest version
|
214
|
+
matching_models.min_by do |model|
|
215
|
+
# Extract context window if specified
|
216
|
+
context_priority = if model =~ /:(?:\d+:)?(\d+)k/
|
217
|
+
-Regexp.last_match(1).to_i # Negative for descending sort
|
218
|
+
else
|
219
|
+
0 # No context specified
|
220
|
+
end
|
221
|
+
|
222
|
+
# Extract version if present
|
223
|
+
version_priority = if model =~ /-v(\d+):/
|
224
|
+
-Regexp.last_match(1).to_i # Negative for descending sort (latest version first)
|
225
|
+
else
|
226
|
+
0
|
227
|
+
end
|
228
|
+
|
229
|
+
# Prefer models with explicit context windows
|
230
|
+
has_context_priority = model.include?('k') ? -1 : 0
|
231
|
+
|
232
|
+
[has_context_priority, context_priority, version_priority]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :release do
|
4
|
+
desc 'Verify cassettes are fresh enough for release'
|
5
|
+
task :verify_cassettes do
|
6
|
+
max_age_days = 1
|
7
|
+
cassette_dir = 'spec/fixtures/vcr_cassettes'
|
8
|
+
stale_cassettes = []
|
9
|
+
|
10
|
+
Dir.glob("#{cassette_dir}/**/*.yml").each do |cassette|
|
11
|
+
age_days = (Time.now - File.mtime(cassette)) / 86_400
|
12
|
+
|
13
|
+
next unless age_days > max_age_days
|
14
|
+
|
15
|
+
stale_cassettes << {
|
16
|
+
file: File.basename(cassette),
|
17
|
+
age: age_days.round(1)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
if stale_cassettes.any?
|
22
|
+
puts "\n❌ Found stale cassettes (older than #{max_age_days} days):"
|
23
|
+
stale_cassettes.each do |c|
|
24
|
+
puts " - #{c[:file]} (#{c[:age]} days old)"
|
25
|
+
end
|
26
|
+
puts "\nRun locally: bundle exec rspec"
|
27
|
+
exit 1
|
28
|
+
else
|
29
|
+
puts "✅ All cassettes are fresh (< #{max_age_days} days old)"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_llm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Carmine Paolino
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: base64
|
@@ -42,58 +41,72 @@ dependencies:
|
|
42
41
|
name: faraday
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
44
43
|
requirements:
|
45
|
-
- - "
|
44
|
+
- - ">="
|
46
45
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
46
|
+
version: 1.10.0
|
48
47
|
type: :runtime
|
49
48
|
prerelease: false
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
51
50
|
requirements:
|
52
|
-
- - "
|
51
|
+
- - ">="
|
53
52
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
53
|
+
version: 1.10.0
|
55
54
|
- !ruby/object:Gem::Dependency
|
56
55
|
name: faraday-multipart
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
58
57
|
requirements:
|
59
|
-
- - "
|
58
|
+
- - ">="
|
60
59
|
- !ruby/object:Gem::Version
|
61
60
|
version: '1'
|
62
61
|
type: :runtime
|
63
62
|
prerelease: false
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
65
64
|
requirements:
|
66
|
-
- - "
|
65
|
+
- - ">="
|
67
66
|
- !ruby/object:Gem::Version
|
68
67
|
version: '1'
|
69
68
|
- !ruby/object:Gem::Dependency
|
70
69
|
name: faraday-net_http
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
72
71
|
requirements:
|
73
|
-
- - "
|
72
|
+
- - ">="
|
74
73
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
74
|
+
version: '1'
|
76
75
|
type: :runtime
|
77
76
|
prerelease: false
|
78
77
|
version_requirements: !ruby/object:Gem::Requirement
|
79
78
|
requirements:
|
80
|
-
- - "
|
79
|
+
- - ">="
|
81
80
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
81
|
+
version: '1'
|
83
82
|
- !ruby/object:Gem::Dependency
|
84
83
|
name: faraday-retry
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: marcel
|
85
98
|
requirement: !ruby/object:Gem::Requirement
|
86
99
|
requirements:
|
87
100
|
- - "~>"
|
88
101
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
102
|
+
version: '1.0'
|
90
103
|
type: :runtime
|
91
104
|
prerelease: false
|
92
105
|
version_requirements: !ruby/object:Gem::Requirement
|
93
106
|
requirements:
|
94
107
|
- - "~>"
|
95
108
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
109
|
+
version: '1.0'
|
97
110
|
- !ruby/object:Gem::Dependency
|
98
111
|
name: zeitwerk
|
99
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -126,10 +139,7 @@ files:
|
|
126
139
|
- lib/ruby_llm/active_record/acts_as.rb
|
127
140
|
- lib/ruby_llm/aliases.json
|
128
141
|
- lib/ruby_llm/aliases.rb
|
129
|
-
- lib/ruby_llm/
|
130
|
-
- lib/ruby_llm/attachments/audio.rb
|
131
|
-
- lib/ruby_llm/attachments/image.rb
|
132
|
-
- lib/ruby_llm/attachments/pdf.rb
|
142
|
+
- lib/ruby_llm/attachment.rb
|
133
143
|
- lib/ruby_llm/chat.rb
|
134
144
|
- lib/ruby_llm/chunk.rb
|
135
145
|
- lib/ruby_llm/configuration.rb
|
@@ -140,8 +150,13 @@ files:
|
|
140
150
|
- lib/ruby_llm/error.rb
|
141
151
|
- lib/ruby_llm/image.rb
|
142
152
|
- lib/ruby_llm/message.rb
|
143
|
-
- lib/ruby_llm/
|
144
|
-
- lib/ruby_llm/
|
153
|
+
- lib/ruby_llm/mime_type.rb
|
154
|
+
- lib/ruby_llm/model.rb
|
155
|
+
- lib/ruby_llm/model/info.rb
|
156
|
+
- lib/ruby_llm/model/modalities.rb
|
157
|
+
- lib/ruby_llm/model/pricing.rb
|
158
|
+
- lib/ruby_llm/model/pricing_category.rb
|
159
|
+
- lib/ruby_llm/model/pricing_tier.rb
|
145
160
|
- lib/ruby_llm/models.json
|
146
161
|
- lib/ruby_llm/models.rb
|
147
162
|
- lib/ruby_llm/provider.rb
|
@@ -198,8 +213,10 @@ files:
|
|
198
213
|
- lib/ruby_llm/tool_call.rb
|
199
214
|
- lib/ruby_llm/utils.rb
|
200
215
|
- lib/ruby_llm/version.rb
|
201
|
-
- lib/tasks/
|
216
|
+
- lib/tasks/aliases.rake
|
202
217
|
- lib/tasks/models_docs.rake
|
218
|
+
- lib/tasks/models_update.rake
|
219
|
+
- lib/tasks/release.rake
|
203
220
|
- lib/tasks/vcr.rake
|
204
221
|
homepage: https://rubyllm.com
|
205
222
|
licenses:
|
@@ -211,7 +228,6 @@ metadata:
|
|
211
228
|
documentation_uri: https://rubyllm.com
|
212
229
|
bug_tracker_uri: https://github.com/crmne/ruby_llm/issues
|
213
230
|
rubygems_mfa_required: 'true'
|
214
|
-
post_install_message:
|
215
231
|
rdoc_options: []
|
216
232
|
require_paths:
|
217
233
|
- lib
|
@@ -226,8 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
226
242
|
- !ruby/object:Gem::Version
|
227
243
|
version: '0'
|
228
244
|
requirements: []
|
229
|
-
rubygems_version: 3.
|
230
|
-
signing_key:
|
245
|
+
rubygems_version: 3.6.7
|
231
246
|
specification_version: 4
|
232
247
|
summary: A single delightful Ruby way to work with AI.
|
233
248
|
test_files: []
|
data/lib/ruby_llm/attachments.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RubyLLM
|
4
|
-
# A module for handling attachments and file operations in RubyLLM.
|
5
|
-
module Attachments
|
6
|
-
# Base class for attachments
|
7
|
-
class Base
|
8
|
-
attr_reader :source, :filename
|
9
|
-
|
10
|
-
def initialize(source, filename: nil)
|
11
|
-
@source = source
|
12
|
-
@filename = filename ||
|
13
|
-
(@source.respond_to?(:original_filename) && @source.original_filename) ||
|
14
|
-
(@source.respond_to?(:path) && File.basename(@source.path)) ||
|
15
|
-
(@source.is_a?(String) && File.basename(@source.split('?').first)) || # Basic URL basename
|
16
|
-
nil
|
17
|
-
end
|
18
|
-
|
19
|
-
def url?
|
20
|
-
@source.is_a?(String) && @source.match?(%r{^https?://})
|
21
|
-
end
|
22
|
-
|
23
|
-
def file_path?
|
24
|
-
@source.is_a?(String) && !url?
|
25
|
-
end
|
26
|
-
|
27
|
-
def io_like?
|
28
|
-
@source.respond_to?(:read) && !file_path?
|
29
|
-
end
|
30
|
-
|
31
|
-
def content
|
32
|
-
return @content if defined?(@content) && !@content.nil?
|
33
|
-
|
34
|
-
if url?
|
35
|
-
fetch_content
|
36
|
-
elsif file_path?
|
37
|
-
load_content_from_path
|
38
|
-
elsif io_like?
|
39
|
-
load_content_from_io
|
40
|
-
else
|
41
|
-
RubyLLM.logger.warn "Attachment source is neither a String nor an IO-like object: #{@source}"
|
42
|
-
nil
|
43
|
-
end
|
44
|
-
|
45
|
-
@content
|
46
|
-
end
|
47
|
-
|
48
|
-
def type
|
49
|
-
self.class.name.demodulize.downcase
|
50
|
-
end
|
51
|
-
|
52
|
-
def encoded
|
53
|
-
Base64.strict_encode64(content)
|
54
|
-
end
|
55
|
-
|
56
|
-
def mime_type
|
57
|
-
RubyLLM::MimeTypes.detect_from_path(@filename)
|
58
|
-
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
def fetch_content
|
63
|
-
RubyLLM.logger.debug("Fetching content from URL: #{@source}")
|
64
|
-
response = Faraday.get(@source)
|
65
|
-
@content = response.body if response.success?
|
66
|
-
end
|
67
|
-
|
68
|
-
def load_content_from_path
|
69
|
-
@content = File.read(File.expand_path(@source))
|
70
|
-
end
|
71
|
-
|
72
|
-
def load_content_from_io
|
73
|
-
@source.rewind if source.respond_to? :rewind
|
74
|
-
@content = @source.read
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|