ruby_llm 1.2.0 → 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/README.md +80 -133
- data/lib/ruby_llm/active_record/acts_as.rb +144 -47
- data/lib/ruby_llm/aliases.json +187 -17
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +31 -20
- data/lib/ruby_llm/configuration.rb +34 -1
- data/lib/ruby_llm/connection.rb +121 -0
- data/lib/ruby_llm/content.rb +27 -79
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +2 -1
- data/lib/ruby_llm/image.rb +15 -8
- data/lib/ruby_llm/message.rb +14 -6
- 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 +26279 -2362
- data/lib/ruby_llm/models.rb +95 -14
- data/lib/ruby_llm/provider.rb +48 -90
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
- data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
- data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
- data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
- data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
- data/lib/ruby_llm/providers/anthropic.rb +3 -3
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
- data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
- data/lib/ruby_llm/providers/bedrock.rb +14 -25
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
- data/lib/ruby_llm/providers/deepseek.rb +3 -3
- data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
- data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
- data/lib/ruby_llm/providers/gemini/images.rb +4 -3
- data/lib/ruby_llm/providers/gemini/media.rb +28 -111
- data/lib/ruby_llm/providers/gemini/models.rb +17 -23
- data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
- data/lib/ruby_llm/providers/gemini.rb +3 -3
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +48 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
- data/lib/ruby_llm/providers/openai/chat.rb +6 -4
- data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
- data/lib/ruby_llm/providers/openai/images.rb +3 -2
- data/lib/ruby_llm/providers/openai/media.rb +48 -21
- data/lib/ruby_llm/providers/openai/models.rb +17 -18
- data/lib/ruby_llm/providers/openai/tools.rb +9 -5
- data/lib/ruby_llm/providers/openai.rb +7 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +31 -0
- data/lib/ruby_llm/stream_accumulator.rb +4 -4
- data/lib/ruby_llm/streaming.rb +48 -13
- data/lib/ruby_llm/utils.rb +27 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +164 -121
- data/lib/tasks/models_update.rake +79 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +4 -2
- metadata +56 -32
- data/lib/ruby_llm/model_info.rb +0 -56
- data/lib/tasks/browser_helper.rb +0 -97
- data/lib/tasks/capability_generator.rb +0 -123
- data/lib/tasks/capability_scraper.rb +0 -224
- data/lib/tasks/cli_helper.rb +0 -22
- data/lib/tasks/code_validator.rb +0 -29
- data/lib/tasks/model_updater.rb +0 -66
- data/lib/tasks/models.rake +0 -43
data/lib/ruby_llm.rb
CHANGED
@@ -16,7 +16,9 @@ loader.inflector.inflect(
|
|
16
16
|
'openai' => 'OpenAI',
|
17
17
|
'api' => 'API',
|
18
18
|
'deepseek' => 'DeepSeek',
|
19
|
-
'bedrock' => 'Bedrock'
|
19
|
+
'bedrock' => 'Bedrock',
|
20
|
+
'openrouter' => 'OpenRouter',
|
21
|
+
'pdf' => 'PDF'
|
20
22
|
)
|
21
23
|
loader.ignore("#{__dir__}/tasks")
|
22
24
|
loader.ignore("#{__dir__}/ruby_llm/railtie")
|
@@ -30,8 +32,14 @@ module RubyLLM
|
|
30
32
|
class Error < StandardError; end
|
31
33
|
|
32
34
|
class << self
|
33
|
-
def
|
34
|
-
|
35
|
+
def context
|
36
|
+
context_config = config.dup
|
37
|
+
yield context_config if block_given?
|
38
|
+
Context.new(context_config)
|
39
|
+
end
|
40
|
+
|
41
|
+
def chat(...)
|
42
|
+
Chat.new(...)
|
35
43
|
end
|
36
44
|
|
37
45
|
def embed(...)
|
@@ -60,9 +68,9 @@ module RubyLLM
|
|
60
68
|
|
61
69
|
def logger
|
62
70
|
@logger ||= Logger.new(
|
63
|
-
|
71
|
+
config.log_file,
|
64
72
|
progname: 'RubyLLM',
|
65
|
-
level:
|
73
|
+
level: config.log_level
|
66
74
|
)
|
67
75
|
end
|
68
76
|
end
|
@@ -73,6 +81,8 @@ RubyLLM::Provider.register :anthropic, RubyLLM::Providers::Anthropic
|
|
73
81
|
RubyLLM::Provider.register :gemini, RubyLLM::Providers::Gemini
|
74
82
|
RubyLLM::Provider.register :deepseek, RubyLLM::Providers::DeepSeek
|
75
83
|
RubyLLM::Provider.register :bedrock, RubyLLM::Providers::Bedrock
|
84
|
+
RubyLLM::Provider.register :openrouter, RubyLLM::Providers::OpenRouter
|
85
|
+
RubyLLM::Provider.register :ollama, RubyLLM::Providers::Ollama
|
76
86
|
|
77
87
|
if defined?(Rails::Railtie)
|
78
88
|
require 'ruby_llm/railtie'
|
@@ -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
|
data/lib/tasks/models_docs.rake
CHANGED
@@ -1,168 +1,211 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'dotenv/load'
|
4
4
|
require 'fileutils'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
provider
|
11
|
-
context_window
|
12
|
-
max_tokens
|
13
|
-
family
|
14
|
-
input_price_per_million
|
15
|
-
output_price_per_million
|
16
|
-
].freeze
|
17
|
-
|
18
|
-
def to_markdown_table(models) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
19
|
-
to_display_hash = ->(model) { model.to_h.slice(*MODEL_KEYS_TO_DISPLAY) }
|
20
|
-
model_hashes = Array(models).map { |model| to_display_hash.call(model) }
|
21
|
-
|
22
|
-
# Create abbreviated headers
|
23
|
-
headers = {
|
24
|
-
id: 'ID',
|
25
|
-
type: 'Type',
|
26
|
-
display_name: 'Name',
|
27
|
-
provider: 'Provider',
|
28
|
-
context_window: 'Context',
|
29
|
-
max_tokens: 'MaxTok',
|
30
|
-
family: 'Family',
|
31
|
-
input_price_per_million: 'In$/M',
|
32
|
-
output_price_per_million: 'Out$/M'
|
33
|
-
}
|
6
|
+
namespace :models do
|
7
|
+
desc 'Generate available models documentation'
|
8
|
+
task :docs do
|
9
|
+
FileUtils.mkdir_p('docs/guides') # ensure output directory exists
|
34
10
|
|
35
|
-
|
36
|
-
|
37
|
-
alignments = {
|
38
|
-
id: ':--',
|
39
|
-
type: ':--',
|
40
|
-
display_name: ':--',
|
41
|
-
provider: ':--',
|
42
|
-
context_window: '--:',
|
43
|
-
max_tokens: '--:',
|
44
|
-
family: ':--',
|
45
|
-
input_price_per_million: '--:',
|
46
|
-
output_price_per_million: '--:'
|
47
|
-
}
|
11
|
+
# Generate markdown content
|
12
|
+
output = generate_models_markdown
|
48
13
|
|
49
|
-
|
50
|
-
|
14
|
+
# Write the output
|
15
|
+
File.write('docs/guides/available-models.md', output)
|
16
|
+
puts 'Generated docs/guides/available-models.md'
|
17
|
+
end
|
18
|
+
end
|
51
19
|
|
52
|
-
|
53
|
-
|
20
|
+
def generate_models_markdown
|
21
|
+
<<~MARKDOWN
|
22
|
+
---
|
23
|
+
layout: default
|
24
|
+
title: Available Models
|
25
|
+
parent: Guides
|
26
|
+
nav_order: 10
|
27
|
+
permalink: /guides/available-models
|
28
|
+
---
|
54
29
|
|
55
|
-
|
56
|
-
|
30
|
+
# Available Models
|
31
|
+
{: .no_toc }
|
57
32
|
|
58
|
-
|
59
|
-
|
60
|
-
values = MODEL_KEYS_TO_DISPLAY.map do |key|
|
61
|
-
if model_hash[key].is_a?(Float)
|
62
|
-
format('%.2f', model_hash[key])
|
63
|
-
else
|
64
|
-
model_hash[key]
|
65
|
-
end
|
66
|
-
end
|
33
|
+
This guide lists all models available in RubyLLM, automatically generated from the current model registry.
|
34
|
+
{: .fs-6 .fw-300 }
|
67
35
|
|
68
|
-
|
69
|
-
|
36
|
+
## Table of contents
|
37
|
+
{: .no_toc .text-delta }
|
38
|
+
|
39
|
+
1. TOC
|
40
|
+
{:toc}
|
41
|
+
|
42
|
+
---
|
43
|
+
|
44
|
+
## Contributing
|
45
|
+
|
46
|
+
The model list is automatically generated from the model registry. To add or update models:
|
47
|
+
|
48
|
+
1. Edit the appropriate `capabilities.rb` file in `lib/ruby_llm/providers/<provider>/`
|
49
|
+
2. Run `rake models:update` to refresh the model registry
|
50
|
+
3. Submit a pull request with the updated `models.json`
|
51
|
+
|
52
|
+
See [Contributing Guide](/CONTRIBUTING.md) for more details.
|
53
|
+
|
54
|
+
## Last Updated
|
55
|
+
{: .d-inline-block }
|
56
|
+
|
57
|
+
#{Time.now.utc.strftime('%Y-%m-%d')}
|
58
|
+
{: .label .label-green }
|
59
|
+
|
60
|
+
## Models by Provider
|
61
|
+
|
62
|
+
#{generate_provider_sections}
|
63
|
+
|
64
|
+
## Models by Capability
|
70
65
|
|
71
|
-
|
66
|
+
#{generate_capability_sections}
|
67
|
+
|
68
|
+
## Models by Modality
|
69
|
+
|
70
|
+
#{generate_modality_sections}
|
71
|
+
MARKDOWN
|
72
72
|
end
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
def generate_provider_sections
|
75
|
+
RubyLLM::Provider.providers.keys.map do |provider|
|
76
|
+
models = RubyLLM.models.by_provider(provider)
|
77
|
+
next if models.none?
|
78
|
+
|
79
|
+
<<~PROVIDER
|
80
|
+
### #{provider.to_s.capitalize} (#{models.count})
|
78
81
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
parent: Guides
|
84
|
-
nav_order: 10
|
85
|
-
permalink: /guides/available-models
|
86
|
-
---
|
82
|
+
#{models_table(models)}
|
83
|
+
PROVIDER
|
84
|
+
end.compact.join("\n\n")
|
85
|
+
end
|
87
86
|
|
88
|
-
|
89
|
-
|
87
|
+
def generate_capability_sections
|
88
|
+
capabilities = {
|
89
|
+
'Function Calling' => RubyLLM.models.select(&:function_calling?),
|
90
|
+
'Structured Output' => RubyLLM.models.select(&:structured_output?),
|
91
|
+
'Streaming' => RubyLLM.models.select { |m| m.capabilities.include?('streaming') },
|
92
|
+
# 'Reasoning' => RubyLLM.models.select { |m| m.capabilities.include?('reasoning') },
|
93
|
+
'Batch Processing' => RubyLLM.models.select { |m| m.capabilities.include?('batch') }
|
94
|
+
}
|
90
95
|
|
91
|
-
|
92
|
-
|
96
|
+
capabilities.map do |capability, models|
|
97
|
+
next if models.none?
|
93
98
|
|
94
|
-
|
95
|
-
{
|
99
|
+
<<~CAPABILITY
|
100
|
+
### #{capability} (#{models.count})
|
96
101
|
|
97
|
-
|
98
|
-
|
102
|
+
#{models_table(models)}
|
103
|
+
CAPABILITY
|
104
|
+
end.compact.join("\n\n")
|
105
|
+
end
|
99
106
|
|
100
|
-
|
107
|
+
def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
|
108
|
+
sections = []
|
101
109
|
|
102
|
-
|
110
|
+
# Models that support vision/images
|
111
|
+
vision_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('image') }
|
112
|
+
if vision_models.any?
|
113
|
+
sections << <<~SECTION
|
114
|
+
### Vision Models (#{vision_models.count})
|
103
115
|
|
104
|
-
|
116
|
+
Models that can process images:
|
105
117
|
|
106
|
-
|
107
|
-
|
108
|
-
|
118
|
+
#{models_table(vision_models)}
|
119
|
+
SECTION
|
120
|
+
end
|
109
121
|
|
110
|
-
|
122
|
+
# Models that support audio
|
123
|
+
audio_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('audio') }
|
124
|
+
if audio_models.any?
|
125
|
+
sections << <<~SECTION
|
126
|
+
### Audio Input Models (#{audio_models.count})
|
111
127
|
|
112
|
-
|
128
|
+
Models that can process audio:
|
113
129
|
|
114
|
-
|
130
|
+
#{models_table(audio_models)}
|
131
|
+
SECTION
|
132
|
+
end
|
115
133
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
134
|
+
# Models that support PDFs
|
135
|
+
pdf_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('pdf') }
|
136
|
+
if pdf_models.any?
|
137
|
+
sections << <<~SECTION
|
138
|
+
### PDF Models (#{pdf_models.count})
|
120
139
|
|
121
|
-
|
140
|
+
Models that can process PDF documents:
|
122
141
|
|
123
|
-
|
142
|
+
#{models_table(pdf_models)}
|
143
|
+
SECTION
|
144
|
+
end
|
124
145
|
|
125
|
-
|
126
|
-
|
146
|
+
# Models for embeddings
|
147
|
+
embedding_models = RubyLLM.models.select { |m| (m.modalities.output || []).include?('embeddings') }
|
148
|
+
if embedding_models.any?
|
149
|
+
sections << <<~SECTION
|
150
|
+
### Embedding Models (#{embedding_models.count})
|
127
151
|
|
128
|
-
|
129
|
-
{: .label .label-green }
|
152
|
+
Models that generate embeddings:
|
130
153
|
|
131
|
-
|
154
|
+
#{models_table(embedding_models)}
|
155
|
+
SECTION
|
156
|
+
end
|
132
157
|
|
133
|
-
|
158
|
+
sections.join("\n\n")
|
159
|
+
end
|
134
160
|
|
135
|
-
|
161
|
+
def models_table(models)
|
162
|
+
return '*No models found*' if models.none?
|
136
163
|
|
137
|
-
|
164
|
+
headers = ['Model', 'ID', 'Provider', 'Context', 'Max Output', 'Standard Pricing (per 1M tokens)']
|
165
|
+
alignment = [':--', ':--', ':--', '--:', '--:', ':--']
|
138
166
|
|
139
|
-
|
167
|
+
rows = models.sort_by { |m| [m.provider, m.name] }.map do |model|
|
168
|
+
# Format pricing information
|
169
|
+
pricing = standard_pricing_display(model)
|
140
170
|
|
141
|
-
|
171
|
+
[
|
172
|
+
model.name,
|
173
|
+
model.id,
|
174
|
+
model.provider,
|
175
|
+
model.context_window || '-',
|
176
|
+
model.max_output_tokens || '-',
|
177
|
+
pricing
|
178
|
+
]
|
179
|
+
end
|
142
180
|
|
143
|
-
|
181
|
+
table = []
|
182
|
+
table << "| #{headers.join(' | ')} |"
|
183
|
+
table << "| #{alignment.join(' | ')} |"
|
144
184
|
|
145
|
-
|
185
|
+
rows.each do |row|
|
186
|
+
table << "| #{row.join(' | ')} |"
|
187
|
+
end
|
146
188
|
|
147
|
-
|
189
|
+
table.join("\n")
|
190
|
+
end
|
148
191
|
|
149
|
-
|
192
|
+
def standard_pricing_display(model)
|
193
|
+
# Access pricing data using to_h to get the raw hash
|
194
|
+
pricing_data = model.pricing.to_h[:text_tokens]&.dig(:standard) || {}
|
150
195
|
|
151
|
-
|
196
|
+
if pricing_data.any?
|
197
|
+
parts = []
|
152
198
|
|
153
|
-
|
154
|
-
models = RubyLLM.models.by_provider(provider)
|
155
|
-
next if models.none?
|
199
|
+
parts << "In: $#{format('%.2f', pricing_data[:input_per_million])}" if pricing_data[:input_per_million]
|
156
200
|
|
157
|
-
|
158
|
-
### #{provider.to_s.capitalize} Models (#{models.count})
|
201
|
+
parts << "Out: $#{format('%.2f', pricing_data[:output_per_million])}" if pricing_data[:output_per_million]
|
159
202
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
MARKDOWN
|
203
|
+
if pricing_data[:cached_input_per_million]
|
204
|
+
parts << "Cache: $#{format('%.2f', pricing_data[:cached_input_per_million])}"
|
205
|
+
end
|
164
206
|
|
165
|
-
|
166
|
-
puts 'Generated docs/guides/available-models.md'
|
207
|
+
return parts.join(', ') if parts.any?
|
167
208
|
end
|
209
|
+
|
210
|
+
'-'
|
168
211
|
end
|