ruby_llm 1.6.2 → 1.6.3
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 +73 -91
- data/lib/ruby_llm/active_record/acts_as.rb +2 -10
- data/lib/ruby_llm/aliases.json +4 -0
- data/lib/ruby_llm/aliases.rb +7 -25
- data/lib/ruby_llm/chat.rb +2 -10
- data/lib/ruby_llm/configuration.rb +1 -12
- data/lib/ruby_llm/content.rb +0 -2
- data/lib/ruby_llm/embedding.rb +1 -2
- data/lib/ruby_llm/error.rb +0 -8
- data/lib/ruby_llm/image.rb +0 -4
- data/lib/ruby_llm/message.rb +2 -4
- data/lib/ruby_llm/model/info.rb +0 -10
- data/lib/ruby_llm/model/pricing.rb +0 -3
- data/lib/ruby_llm/model/pricing_category.rb +0 -2
- data/lib/ruby_llm/model/pricing_tier.rb +0 -1
- data/lib/ruby_llm/models.json +623 -452
- data/lib/ruby_llm/models.rb +5 -13
- data/lib/ruby_llm/provider.rb +1 -5
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +0 -1
- data/lib/ruby_llm/providers/anthropic.rb +1 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -2
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
- data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
- data/lib/ruby_llm/providers/bedrock.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +26 -101
- data/lib/ruby_llm/providers/gemini/chat.rb +5 -7
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
- data/lib/ruby_llm/providers/gemini/images.rb +0 -1
- data/lib/ruby_llm/providers/gemini/media.rb +0 -1
- data/lib/ruby_llm/providers/gemini/models.rb +1 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
- data/lib/ruby_llm/providers/gpustack/chat.rb +0 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +3 -4
- data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
- data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
- data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/mistral/models.rb +0 -1
- data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
- data/lib/ruby_llm/providers/ollama/media.rb +0 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +0 -15
- data/lib/ruby_llm/providers/openai/chat.rb +0 -3
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/openai/media.rb +0 -1
- data/lib/ruby_llm/providers/openai.rb +1 -3
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
- data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
- data/lib/ruby_llm/providers/perplexity.rb +1 -5
- data/lib/ruby_llm/railtie.rb +0 -1
- data/lib/ruby_llm/stream_accumulator.rb +1 -3
- data/lib/ruby_llm/streaming.rb +15 -24
- data/lib/ruby_llm/tool.rb +2 -19
- data/lib/ruby_llm/tool_call.rb +0 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +0 -2
- data/lib/tasks/aliases.rake +5 -35
- data/lib/tasks/models_docs.rake +1 -11
- data/lib/tasks/models_update.rake +1 -1
- data/lib/tasks/vcr.rake +0 -7
- metadata +1 -1
@@ -23,9 +23,6 @@ module RubyLLM
|
|
23
23
|
data = response.body
|
24
24
|
input_tokens = data.dig('usage', 'prompt_tokens') || 0
|
25
25
|
vectors = data['data'].map { |d| d['embedding'] }
|
26
|
-
|
27
|
-
# If we only got one embedding AND the input was a single string (not an array),
|
28
|
-
# return it as a single vector
|
29
26
|
vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
|
30
27
|
|
31
28
|
Embedding.new(vectors:, model:, input_tokens:)
|
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
module RubyLLM
|
4
4
|
module Providers
|
5
|
-
# OpenAI API integration.
|
6
|
-
# and OpenAI's unique streaming format. Supports GPT-4, GPT-3.5,
|
7
|
-
# and other OpenAI models.
|
5
|
+
# OpenAI API integration.
|
8
6
|
class OpenAI < Provider
|
9
7
|
include OpenAI::Chat
|
10
8
|
include OpenAI::Embeddings
|
@@ -13,13 +13,11 @@ module RubyLLM
|
|
13
13
|
|
14
14
|
def parse_list_models_response(response, slug, _capabilities)
|
15
15
|
Array(response.body['data']).map do |model_data| # rubocop:disable Metrics/BlockLength
|
16
|
-
# Extract modalities directly from architecture
|
17
16
|
modalities = {
|
18
17
|
input: Array(model_data.dig('architecture', 'input_modalities')),
|
19
18
|
output: Array(model_data.dig('architecture', 'output_modalities'))
|
20
19
|
}
|
21
20
|
|
22
|
-
# Construct pricing from API data, only adding non-zero values
|
23
21
|
pricing = { text_tokens: { standard: {} } }
|
24
22
|
|
25
23
|
pricing_types = {
|
@@ -34,7 +32,6 @@ module RubyLLM
|
|
34
32
|
pricing[:text_tokens][:standard][target_key] = value * 1_000_000 if value.positive?
|
35
33
|
end
|
36
34
|
|
37
|
-
# Convert OpenRouter's supported parameters to our capability format
|
38
35
|
capabilities = supported_parameters_to_capabilities(model_data['supported_parameters'])
|
39
36
|
|
40
37
|
Model::Info.new(
|
@@ -63,23 +60,11 @@ module RubyLLM
|
|
63
60
|
return [] unless params
|
64
61
|
|
65
62
|
capabilities = []
|
66
|
-
|
67
|
-
# Standard capabilities mapping
|
68
|
-
capabilities << 'streaming' # Assume all OpenRouter models support streaming
|
69
|
-
|
70
|
-
# Function calling capability
|
63
|
+
capabilities << 'streaming'
|
71
64
|
capabilities << 'function_calling' if params.include?('tools') || params.include?('tool_choice')
|
72
|
-
|
73
|
-
# Structured output capability
|
74
65
|
capabilities << 'structured_output' if params.include?('response_format')
|
75
|
-
|
76
|
-
# Batch capability
|
77
66
|
capabilities << 'batch' if params.include?('batch')
|
78
|
-
|
79
|
-
# Additional mappings based on params
|
80
|
-
# Handles advanced model capabilities that might be inferred from supported params
|
81
67
|
capabilities << 'predicted_outputs' if params.include?('logit_bias') && params.include?('top_k')
|
82
|
-
|
83
68
|
capabilities
|
84
69
|
end
|
85
70
|
end
|
@@ -34,17 +34,13 @@ module RubyLLM
|
|
34
34
|
|
35
35
|
# If response is HTML (Perplexity returns HTML for auth errors)
|
36
36
|
if body.include?('<html>') && body.include?('<title>')
|
37
|
-
# Extract title content
|
38
37
|
title_match = body.match(%r{<title>(.+?)</title>})
|
39
38
|
if title_match
|
40
|
-
# Clean up the title - remove status code if present
|
41
39
|
message = title_match[1]
|
42
|
-
message = message.sub(/^\d+\s+/, '')
|
40
|
+
message = message.sub(/^\d+\s+/, '')
|
43
41
|
return message
|
44
42
|
end
|
45
43
|
end
|
46
|
-
|
47
|
-
# Fall back to parent's implementation
|
48
44
|
super
|
49
45
|
end
|
50
46
|
end
|
data/lib/ruby_llm/railtie.rb
CHANGED
@@ -2,8 +2,6 @@
|
|
2
2
|
|
3
3
|
module RubyLLM
|
4
4
|
# Assembles streaming responses from LLMs into complete messages.
|
5
|
-
# Handles the complexities of accumulating content and tool calls
|
6
|
-
# from partial chunks while tracking token usage.
|
7
5
|
class StreamAccumulator
|
8
6
|
attr_reader :content, :model_id, :tool_calls
|
9
7
|
|
@@ -48,7 +46,7 @@ module RubyLLM
|
|
48
46
|
arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
|
49
47
|
JSON.parse(tc.arguments)
|
50
48
|
elsif tc.arguments.is_a?(String)
|
51
|
-
{}
|
49
|
+
{}
|
52
50
|
else
|
53
51
|
tc.arguments
|
54
52
|
end
|
data/lib/ruby_llm/streaming.rb
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RubyLLM
|
4
|
-
# Handles streaming responses from AI providers.
|
5
|
-
# chunked responses, accumulate content, and handle provider-specific streaming formats.
|
6
|
-
# Each provider implements provider-specific parsing while sharing common stream handling
|
7
|
-
# patterns.
|
4
|
+
# Handles streaming responses from AI providers.
|
8
5
|
module Streaming
|
9
6
|
module_function
|
10
7
|
|
@@ -12,17 +9,14 @@ module RubyLLM
|
|
12
9
|
accumulator = StreamAccumulator.new
|
13
10
|
|
14
11
|
response = connection.post stream_url, payload do |req|
|
15
|
-
# Merge additional headers, with existing headers taking precedence
|
16
12
|
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
17
|
-
if
|
18
|
-
|
19
|
-
req.options.on_data = handle_stream do |chunk|
|
13
|
+
if faraday_1?
|
14
|
+
req.options[:on_data] = handle_stream do |chunk|
|
20
15
|
accumulator.add chunk
|
21
16
|
block.call chunk
|
22
17
|
end
|
23
18
|
else
|
24
|
-
|
25
|
-
req.options[:on_data] = handle_stream do |chunk|
|
19
|
+
req.options.on_data = handle_stream do |chunk|
|
26
20
|
accumulator.add chunk
|
27
21
|
block.call chunk
|
28
22
|
end
|
@@ -42,6 +36,10 @@ module RubyLLM
|
|
42
36
|
|
43
37
|
private
|
44
38
|
|
39
|
+
def faraday_1?
|
40
|
+
Faraday::VERSION.start_with?('1')
|
41
|
+
end
|
42
|
+
|
45
43
|
def to_json_stream(&)
|
46
44
|
buffer = +''
|
47
45
|
parser = EventStreamParser::Parser.new
|
@@ -50,11 +48,9 @@ module RubyLLM
|
|
50
48
|
end
|
51
49
|
|
52
50
|
def create_stream_processor(parser, buffer, &)
|
53
|
-
if
|
54
|
-
# Faraday 1.x: on_data receives (chunk, size)
|
51
|
+
if faraday_1?
|
55
52
|
legacy_stream_processor(parser, &)
|
56
53
|
else
|
57
|
-
# Faraday 2.x: on_data receives (chunk, bytes, env)
|
58
54
|
stream_processor(parser, buffer, &)
|
59
55
|
end
|
60
56
|
end
|
@@ -94,12 +90,10 @@ module RubyLLM
|
|
94
90
|
status, _message = parse_streaming_error(error_data)
|
95
91
|
parsed_data = JSON.parse(error_data)
|
96
92
|
|
97
|
-
|
98
|
-
error_response = if env
|
99
|
-
env.merge(body: parsed_data, status: status)
|
100
|
-
else
|
101
|
-
# For Faraday v1, create a simple object that responds to .status and .body
|
93
|
+
error_response = if faraday_1?
|
102
94
|
Struct.new(:body, :status).new(parsed_data, status)
|
95
|
+
else
|
96
|
+
env.merge(body: parsed_data, status: status)
|
103
97
|
end
|
104
98
|
|
105
99
|
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
@@ -137,12 +131,10 @@ module RubyLLM
|
|
137
131
|
status, _message = parse_streaming_error(data)
|
138
132
|
parsed_data = JSON.parse(data)
|
139
133
|
|
140
|
-
|
141
|
-
error_response = if env
|
142
|
-
env.merge(body: parsed_data, status: status)
|
143
|
-
else
|
144
|
-
# For Faraday v1, create a simple object that responds to .status and .body
|
134
|
+
error_response = if faraday_1?
|
145
135
|
Struct.new(:body, :status).new(parsed_data, status)
|
136
|
+
else
|
137
|
+
env.merge(body: parsed_data, status: status)
|
146
138
|
end
|
147
139
|
|
148
140
|
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
@@ -150,7 +142,6 @@ module RubyLLM
|
|
150
142
|
RubyLLM.logger.debug "Failed to parse error event: #{e.message}"
|
151
143
|
end
|
152
144
|
|
153
|
-
# Default implementation - providers should override this method
|
154
145
|
def parse_streaming_error(data)
|
155
146
|
error_data = JSON.parse(data)
|
156
147
|
[500, error_data['message'] || 'Unknown streaming error']
|
data/lib/ruby_llm/tool.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module RubyLLM
|
4
|
-
# Parameter definition for Tool methods.
|
5
|
-
# descriptions, and whether parameters are required.
|
4
|
+
# Parameter definition for Tool methods.
|
6
5
|
class Parameter
|
7
6
|
attr_reader :name, :type, :description, :required
|
8
7
|
|
@@ -14,23 +13,7 @@ module RubyLLM
|
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
17
|
-
# Base class for creating tools that AI models can use
|
18
|
-
# interface for defining parameters and implementing tool behavior.
|
19
|
-
#
|
20
|
-
# Example:
|
21
|
-
# require 'tzinfo'
|
22
|
-
#
|
23
|
-
# class TimeInfo < RubyLLM::Tool
|
24
|
-
# description 'Gets the current time in various timezones'
|
25
|
-
# param :timezone, desc: "Timezone name (e.g., 'UTC', 'America/New_York')"
|
26
|
-
#
|
27
|
-
# def execute(timezone:)
|
28
|
-
# time = TZInfo::Timezone.get(timezone).now.strftime('%Y-%m-%d %H:%M:%S')
|
29
|
-
# "Current time in #{timezone}: #{time}"
|
30
|
-
# rescue StandardError => e
|
31
|
-
# { error: e.message }
|
32
|
-
# end
|
33
|
-
# end
|
16
|
+
# Base class for creating tools that AI models can use
|
34
17
|
class Tool
|
35
18
|
# Stops conversation continuation after tool execution
|
36
19
|
class Halt
|
data/lib/ruby_llm/tool_call.rb
CHANGED
@@ -2,15 +2,6 @@
|
|
2
2
|
|
3
3
|
module RubyLLM
|
4
4
|
# Represents a function call from an AI model to a Tool.
|
5
|
-
# Encapsulates the function name, arguments, and execution results
|
6
|
-
# in a clean Ruby interface.
|
7
|
-
#
|
8
|
-
# Example:
|
9
|
-
# tool_call = ToolCall.new(
|
10
|
-
# id: "call_123",
|
11
|
-
# name: "calculator",
|
12
|
-
# arguments: { expression: "2 + 2" }
|
13
|
-
# )
|
14
5
|
class ToolCall
|
15
6
|
attr_reader :id, :name, :arguments
|
16
7
|
|
data/lib/ruby_llm/version.rb
CHANGED
data/lib/ruby_llm.rb
CHANGED
@@ -30,8 +30,6 @@ loader.ignore("#{__dir__}/generators")
|
|
30
30
|
loader.setup
|
31
31
|
|
32
32
|
# A delightful Ruby interface to modern AI language models.
|
33
|
-
# Provides a unified way to interact with models from OpenAI, Anthropic and others
|
34
|
-
# with a focus on developer happiness and convention over configuration.
|
35
33
|
module RubyLLM
|
36
34
|
class Error < StandardError; end
|
37
35
|
|
data/lib/tasks/aliases.rake
CHANGED
@@ -28,11 +28,9 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
28
28
|
}
|
29
29
|
end
|
30
30
|
|
31
|
-
# Anthropic models - group by base name and find latest
|
32
31
|
anthropic_latest = group_anthropic_models_by_base_name(models['anthropic'])
|
33
32
|
|
34
33
|
anthropic_latest.each do |base_name, latest_model|
|
35
|
-
# Check OpenRouter naming patterns for the BASE NAME (not the full dated model)
|
36
34
|
openrouter_variants = [
|
37
35
|
"anthropic/#{base_name}", # anthropic/claude-3-5-sonnet
|
38
36
|
"anthropic/#{base_name.gsub(/-(\d)/, '.\1')}", # anthropic/claude-3.5-sonnet
|
@@ -42,10 +40,8 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
42
40
|
|
43
41
|
openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
|
44
42
|
|
45
|
-
# Find corresponding Bedrock model
|
46
43
|
bedrock_model = find_best_bedrock_model(latest_model, models['bedrock'])
|
47
44
|
|
48
|
-
# Create alias if we have any match (OpenRouter OR Bedrock) OR if it's Anthropic-only
|
49
45
|
next unless openrouter_model || bedrock_model || models['anthropic'].include?(latest_model)
|
50
46
|
|
51
47
|
aliases[base_name] = {
|
@@ -56,21 +52,16 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
56
52
|
aliases[base_name]['bedrock'] = bedrock_model if bedrock_model
|
57
53
|
end
|
58
54
|
|
59
|
-
# Also check if Bedrock has models that Anthropic doesn't
|
60
55
|
models['bedrock'].each do |bedrock_model|
|
61
56
|
next unless bedrock_model.start_with?('anthropic.')
|
62
57
|
|
63
|
-
# Extract the Claude model name
|
64
58
|
next unless bedrock_model =~ /anthropic\.(claude-[\d\.]+-[a-z]+)/
|
65
59
|
|
66
60
|
base_name = Regexp.last_match(1)
|
67
|
-
# Normalize to Anthropic naming convention
|
68
61
|
anthropic_name = base_name.tr('.', '-')
|
69
62
|
|
70
|
-
# Skip if we already have an alias for this
|
71
63
|
next if aliases[anthropic_name]
|
72
64
|
|
73
|
-
# Check if this model exists in OpenRouter
|
74
65
|
openrouter_variants = [
|
75
66
|
"anthropic/#{anthropic_name}",
|
76
67
|
"anthropic/#{base_name}" # Keep the dots
|
@@ -86,9 +77,7 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
86
77
|
aliases[anthropic_name]['openrouter'] = openrouter_model if openrouter_model
|
87
78
|
end
|
88
79
|
|
89
|
-
# Gemini models
|
90
80
|
models['gemini'].each do |model|
|
91
|
-
# OpenRouter uses "google/" prefix and sometimes different naming
|
92
81
|
openrouter_variants = [
|
93
82
|
"google/#{model}",
|
94
83
|
"google/#{model.gsub('gemini-', 'gemini-').tr('.', '-')}",
|
@@ -105,7 +94,6 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
105
94
|
}
|
106
95
|
end
|
107
96
|
|
108
|
-
# DeepSeek models
|
109
97
|
models['deepseek'].each do |model|
|
110
98
|
openrouter_model = "deepseek/#{model}"
|
111
99
|
next unless models['openrouter'].include?(openrouter_model)
|
@@ -117,9 +105,8 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
117
105
|
}
|
118
106
|
end
|
119
107
|
|
120
|
-
# Write the result
|
121
108
|
sorted_aliases = aliases.sort.to_h
|
122
|
-
File.write(
|
109
|
+
File.write(RubyLLM::Aliases.aliases_file, JSON.pretty_generate(sorted_aliases))
|
123
110
|
|
124
111
|
puts "Generated #{sorted_aliases.size} aliases"
|
125
112
|
end
|
@@ -132,13 +119,11 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
132
119
|
grouped[base_name] << model
|
133
120
|
end
|
134
121
|
|
135
|
-
# Find the latest model for each base name
|
136
122
|
latest_models = {}
|
137
123
|
grouped.each do |base_name, model_list|
|
138
124
|
if model_list.size == 1
|
139
125
|
latest_models[base_name] = model_list.first
|
140
126
|
else
|
141
|
-
# Sort by date and take the latest
|
142
127
|
latest_model = model_list.max_by { |model| extract_date_from_model(model) }
|
143
128
|
latest_models[base_name] = latest_model
|
144
129
|
end
|
@@ -148,26 +133,22 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
148
133
|
end
|
149
134
|
|
150
135
|
def extract_base_name(model) # rubocop:disable Rake/MethodDefinitionInTask
|
151
|
-
# Remove date suffix (YYYYMMDD) from model name
|
152
136
|
if model =~ /^(.+)-(\d{8})$/
|
153
137
|
Regexp.last_match(1)
|
154
138
|
else
|
155
|
-
# Models without date suffix (like claude-2.0, claude-2.1)
|
156
139
|
model
|
157
140
|
end
|
158
141
|
end
|
159
142
|
|
160
143
|
def extract_date_from_model(model) # rubocop:disable Rake/MethodDefinitionInTask
|
161
|
-
# Extract date for comparison, return '00000000' for models without dates
|
162
144
|
if model =~ /-(\d{8})$/
|
163
145
|
Regexp.last_match(1)
|
164
146
|
else
|
165
|
-
'00000000'
|
147
|
+
'00000000'
|
166
148
|
end
|
167
149
|
end
|
168
150
|
|
169
151
|
def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable Metrics/PerceivedComplexity,Rake/MethodDefinitionInTask
|
170
|
-
# Special mapping for Claude 2.x models
|
171
152
|
base_pattern = case anthropic_model
|
172
153
|
when 'claude-2.0', 'claude-2'
|
173
154
|
'claude-v2'
|
@@ -176,20 +157,16 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
176
157
|
when 'claude-instant-v1', 'claude-instant'
|
177
158
|
'claude-instant'
|
178
159
|
else
|
179
|
-
# For Claude 3+ models, extract base name
|
180
160
|
extract_base_name(anthropic_model)
|
181
161
|
end
|
182
162
|
|
183
|
-
# Find all matching Bedrock models by stripping provider prefix and comparing base name
|
184
163
|
matching_models = bedrock_models.select do |bedrock_model|
|
185
|
-
# Strip any provider prefix (anthropic. or us.anthropic.)
|
186
164
|
model_without_prefix = bedrock_model.sub(/^(?:us\.)?anthropic\./, '')
|
187
165
|
model_without_prefix.start_with?(base_pattern)
|
188
166
|
end
|
189
167
|
|
190
168
|
return nil if matching_models.empty?
|
191
169
|
|
192
|
-
# Get model info to check context window
|
193
170
|
begin
|
194
171
|
model_info = RubyLLM.models.find(anthropic_model)
|
195
172
|
target_context = model_info.context_window
|
@@ -197,12 +174,9 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
197
174
|
target_context = nil
|
198
175
|
end
|
199
176
|
|
200
|
-
# If we have context window info, try to match it
|
201
177
|
if target_context
|
202
|
-
# Convert to k format (200000 -> 200k)
|
203
178
|
target_k = target_context / 1000
|
204
179
|
|
205
|
-
# Find models with this specific context window
|
206
180
|
with_context = matching_models.select do |m|
|
207
181
|
m.include?(":#{target_k}k") || m.include?(":0:#{target_k}k")
|
208
182
|
end
|
@@ -210,25 +184,21 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
|
|
210
184
|
return with_context.first if with_context.any?
|
211
185
|
end
|
212
186
|
|
213
|
-
# Otherwise, pick the one with the highest context window or latest version
|
214
187
|
matching_models.min_by do |model|
|
215
|
-
# Extract context window if specified
|
216
188
|
context_priority = if model =~ /:(?:\d+:)?(\d+)k/
|
217
|
-
-Regexp.last_match(1).to_i
|
189
|
+
-Regexp.last_match(1).to_i
|
218
190
|
else
|
219
|
-
0
|
191
|
+
0
|
220
192
|
end
|
221
193
|
|
222
|
-
# Extract version if present
|
223
194
|
version_priority = if model =~ /-v(\d+):/
|
224
|
-
-Regexp.last_match(1).to_i
|
195
|
+
-Regexp.last_match(1).to_i
|
225
196
|
else
|
226
197
|
0
|
227
198
|
end
|
228
199
|
|
229
200
|
# Prefer models with explicit context windows
|
230
201
|
has_context_priority = model.include?('k') ? -1 : 0
|
231
|
-
|
232
202
|
[has_context_priority, context_priority, version_priority]
|
233
203
|
end
|
234
204
|
end
|
data/lib/tasks/models_docs.rake
CHANGED
@@ -6,12 +6,8 @@ require 'fileutils'
|
|
6
6
|
namespace :models do
|
7
7
|
desc 'Generate available models documentation'
|
8
8
|
task :docs do
|
9
|
-
FileUtils.mkdir_p('docs')
|
10
|
-
|
11
|
-
# Generate markdown content
|
9
|
+
FileUtils.mkdir_p('docs')
|
12
10
|
output = generate_models_markdown
|
13
|
-
|
14
|
-
# Write the output
|
15
11
|
File.write('docs/_reference/available-models.md', output)
|
16
12
|
puts 'Generated docs/_reference/available-models.md'
|
17
13
|
end
|
@@ -121,7 +117,6 @@ end
|
|
121
117
|
def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
|
122
118
|
sections = []
|
123
119
|
|
124
|
-
# Models that support vision/images
|
125
120
|
vision_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('image') }
|
126
121
|
if vision_models.any?
|
127
122
|
sections << <<~SECTION
|
@@ -133,7 +128,6 @@ def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
|
|
133
128
|
SECTION
|
134
129
|
end
|
135
130
|
|
136
|
-
# Models that support audio
|
137
131
|
audio_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('audio') }
|
138
132
|
if audio_models.any?
|
139
133
|
sections << <<~SECTION
|
@@ -145,7 +139,6 @@ def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
|
|
145
139
|
SECTION
|
146
140
|
end
|
147
141
|
|
148
|
-
# Models that support PDFs
|
149
142
|
pdf_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('pdf') }
|
150
143
|
if pdf_models.any?
|
151
144
|
sections << <<~SECTION
|
@@ -157,7 +150,6 @@ def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
|
|
157
150
|
SECTION
|
158
151
|
end
|
159
152
|
|
160
|
-
# Models for embeddings
|
161
153
|
embedding_models = RubyLLM.models.select { |m| (m.modalities.output || []).include?('embeddings') }
|
162
154
|
if embedding_models.any?
|
163
155
|
sections << <<~SECTION
|
@@ -179,7 +171,6 @@ def models_table(models)
|
|
179
171
|
alignment = [':--', ':--', '--:', '--:', ':--']
|
180
172
|
|
181
173
|
rows = models.sort_by { |m| [m.provider, m.name] }.map do |model|
|
182
|
-
# Format pricing information
|
183
174
|
pricing = standard_pricing_display(model)
|
184
175
|
|
185
176
|
[
|
@@ -203,7 +194,6 @@ def models_table(models)
|
|
203
194
|
end
|
204
195
|
|
205
196
|
def standard_pricing_display(model)
|
206
|
-
# Access pricing data using to_h to get the raw hash
|
207
197
|
pricing_data = model.pricing.to_h[:text_tokens]&.dig(:standard) || {}
|
208
198
|
|
209
199
|
if pricing_data.any?
|
@@ -61,7 +61,7 @@ def refresh_models
|
|
61
61
|
end
|
62
62
|
|
63
63
|
def validate_models!(models)
|
64
|
-
schema_path =
|
64
|
+
schema_path = RubyLLM::Models.schema_file
|
65
65
|
models_data = models.all.map(&:to_h)
|
66
66
|
|
67
67
|
validation_errors = JSON::Validator.fully_validate(schema_path, models_data)
|
data/lib/tasks/vcr.rake
CHANGED
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'dotenv/load'
|
4
4
|
|
5
|
-
# Helper functions at the top level
|
6
5
|
def record_all_cassettes(cassette_dir)
|
7
|
-
# Re-record all cassettes
|
8
6
|
FileUtils.rm_rf(cassette_dir)
|
9
7
|
FileUtils.mkdir_p(cassette_dir)
|
10
8
|
|
@@ -14,10 +12,8 @@ def record_all_cassettes(cassette_dir)
|
|
14
12
|
end
|
15
13
|
|
16
14
|
def record_for_providers(providers, cassette_dir)
|
17
|
-
# Get the list of available providers from RubyLLM itself
|
18
15
|
all_providers = RubyLLM::Provider.providers.keys.map(&:to_s)
|
19
16
|
|
20
|
-
# Check for valid providers
|
21
17
|
if providers.empty?
|
22
18
|
puts "Please specify providers or 'all'. Example: rake vcr:record[openai,anthropic]"
|
23
19
|
puts "Available providers: #{all_providers.join(', ')}"
|
@@ -31,7 +27,6 @@ def record_for_providers(providers, cassette_dir)
|
|
31
27
|
return
|
32
28
|
end
|
33
29
|
|
34
|
-
# Find and delete matching cassettes
|
35
30
|
cassettes_to_delete = find_matching_cassettes(cassette_dir, providers)
|
36
31
|
|
37
32
|
if cassettes_to_delete.empty?
|
@@ -54,9 +49,7 @@ def find_matching_cassettes(dir, providers)
|
|
54
49
|
Dir.glob("#{dir}/**/*.yml").each do |file|
|
55
50
|
basename = File.basename(file)
|
56
51
|
|
57
|
-
# Precise matching to avoid cross-provider confusion
|
58
52
|
providers.each do |provider|
|
59
|
-
# Match only exact provider prefixes
|
60
53
|
next unless basename =~ /^[^_]*_#{provider}_/ || # For first section like "chat_openai_"
|
61
54
|
basename =~ /_#{provider}_[^_]+_/ # For middle sections like "_openai_gpt4_"
|
62
55
|
|