ruby_llm 1.1.2 → 1.3.0rc1
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 +212 -33
- data/lib/ruby_llm/aliases.json +48 -6
- data/lib/ruby_llm/attachments/audio.rb +12 -0
- data/lib/ruby_llm/attachments/image.rb +9 -0
- data/lib/ruby_llm/attachments/pdf.rb +9 -0
- data/lib/ruby_llm/attachments.rb +78 -0
- data/lib/ruby_llm/chat.rb +26 -10
- data/lib/ruby_llm/configuration.rb +32 -2
- data/lib/ruby_llm/connection.rb +95 -0
- data/lib/ruby_llm/content.rb +51 -72
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +1 -1
- data/lib/ruby_llm/image.rb +13 -5
- data/lib/ruby_llm/message.rb +12 -4
- data/lib/ruby_llm/mime_types.rb +713 -0
- data/lib/ruby_llm/model_info.rb +208 -27
- data/lib/ruby_llm/models.json +25782 -2102
- 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 +44 -34
- data/lib/ruby_llm/providers/anthropic/models.rb +15 -15
- 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 +56 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +51 -52
- 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 +2 -2
- data/lib/ruby_llm/providers/gemini/media.rb +39 -110
- data/lib/ruby_llm/providers/gemini/models.rb +16 -22
- 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 +44 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +79 -4
- 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/media.rb +38 -21
- data/lib/ruby_llm/providers/openai/models.rb +16 -17
- 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 +3 -3
- data/lib/ruby_llm/utils.rb +22 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/models.rake +69 -33
- data/lib/tasks/models_docs.rake +167 -112
- data/lib/tasks/vcr.rake +4 -2
- metadata +23 -14
- 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/ruby_llm/models.rb
CHANGED
@@ -12,7 +12,6 @@ module RubyLLM
|
|
12
12
|
class Models
|
13
13
|
include Enumerable
|
14
14
|
|
15
|
-
# Delegate class methods to the singleton instance
|
16
15
|
class << self
|
17
16
|
def instance
|
18
17
|
@instance ||= new
|
@@ -26,21 +25,48 @@ module RubyLLM
|
|
26
25
|
File.expand_path('models.json', __dir__)
|
27
26
|
end
|
28
27
|
|
29
|
-
def refresh!
|
30
|
-
|
28
|
+
def refresh!
|
29
|
+
# Collect models from both sources
|
30
|
+
provider_models = fetch_from_providers
|
31
|
+
parsera_models = fetch_from_parsera
|
31
32
|
|
32
|
-
#
|
33
|
-
|
34
|
-
RubyLLM.logger.info "Refreshing models from #{configured.map(&:slug).join(', ')}" if configured.any?
|
35
|
-
RubyLLM.logger.info "Skipping #{skipped.map(&:slug).join(', ')} - providers not configured" if skipped.any?
|
33
|
+
# Merge with parsera data taking precedence
|
34
|
+
merged_models = merge_models(provider_models, parsera_models)
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
@instance = new(merged_models)
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch_from_providers
|
40
|
+
configured = Provider.configured_providers(RubyLLM.config).filter(&:remote?)
|
41
|
+
|
42
|
+
RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:slug).join(', ')}"
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
+
configured.flat_map do |provider|
|
45
|
+
provider.list_models(connection: provider.connection(RubyLLM.config))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def resolve(model_id, provider: nil, assume_exists: false)
|
50
|
+
assume_exists = true if provider && Provider.providers[provider.to_sym].local?
|
51
|
+
|
52
|
+
if assume_exists
|
53
|
+
raise ArgumentError, 'Provider must be specified if assume_exists is true' unless provider
|
54
|
+
|
55
|
+
provider = Provider.providers[provider.to_sym] || raise(Error, "Unknown provider: #{provider.to_sym}")
|
56
|
+
model = Struct.new(:id, :provider, :capabilities, :modalities, :supports_vision?, :supports_functions?)
|
57
|
+
.new(model_id,
|
58
|
+
provider,
|
59
|
+
%w[function_calling streaming],
|
60
|
+
RubyLLM::Modalities.new({ input: %w[text image], output: %w[text] }),
|
61
|
+
true,
|
62
|
+
true)
|
63
|
+
RubyLLM.logger.warn "Assuming model '#{model_id}' exists for provider '#{provider}'. " \
|
64
|
+
'Capabilities may not be accurately reflected.'
|
65
|
+
else
|
66
|
+
model = Models.find model_id, provider
|
67
|
+
provider = Provider.providers[model.provider.to_sym] || raise(Error, "Unknown provider: #{model.provider}")
|
68
|
+
end
|
69
|
+
[model, provider]
|
44
70
|
end
|
45
71
|
|
46
72
|
def method_missing(method, ...)
|
@@ -54,6 +80,61 @@ module RubyLLM
|
|
54
80
|
def respond_to_missing?(method, include_private = false)
|
55
81
|
instance.respond_to?(method, include_private) || super
|
56
82
|
end
|
83
|
+
|
84
|
+
def fetch_from_parsera
|
85
|
+
RubyLLM.logger.info 'Fetching models from Parsera API...'
|
86
|
+
|
87
|
+
connection = Faraday.new('https://api.parsera.org') do |f|
|
88
|
+
f.request :json
|
89
|
+
f.response :json
|
90
|
+
f.response :raise_error
|
91
|
+
f.adapter Faraday.default_adapter
|
92
|
+
end
|
93
|
+
|
94
|
+
response = connection.get('/v1/llm-specs')
|
95
|
+
response.body.map { |data| ModelInfo.new(Utils.deep_symbolize_keys(data)) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def merge_models(provider_models, parsera_models)
|
99
|
+
# Create lookups for both sets of models
|
100
|
+
parsera_by_key = index_by_key(parsera_models)
|
101
|
+
provider_by_key = index_by_key(provider_models)
|
102
|
+
|
103
|
+
# All keys from both sources
|
104
|
+
all_keys = parsera_by_key.keys | provider_by_key.keys
|
105
|
+
|
106
|
+
# Merge data, with parsera taking precedence
|
107
|
+
models = all_keys.map do |key|
|
108
|
+
if (parsera_model = parsera_by_key[key])
|
109
|
+
# Parsera has this model - use it as the base
|
110
|
+
if (provider_model = provider_by_key[key])
|
111
|
+
# Both sources have this model, add provider metadata
|
112
|
+
add_provider_metadata(parsera_model, provider_model)
|
113
|
+
else
|
114
|
+
# Only parsera has this model
|
115
|
+
parsera_model
|
116
|
+
end
|
117
|
+
else
|
118
|
+
# Only provider has this model
|
119
|
+
provider_by_key[key]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
models.sort_by { |m| [m.provider, m.id] }
|
124
|
+
end
|
125
|
+
|
126
|
+
def index_by_key(models)
|
127
|
+
models.each_with_object({}) do |model, hash|
|
128
|
+
hash["#{model.provider}:#{model.id}"] = model
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_provider_metadata(parsera_model, provider_model)
|
133
|
+
# Create a new ModelInfo with parsera data but include provider metadata
|
134
|
+
data = parsera_model.to_h
|
135
|
+
data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
|
136
|
+
ModelInfo.new(data)
|
137
|
+
end
|
57
138
|
end
|
58
139
|
|
59
140
|
# Initialize with optional pre-filtered models
|
@@ -64,7 +145,7 @@ module RubyLLM
|
|
64
145
|
# Load models from the JSON file
|
65
146
|
def load_models
|
66
147
|
data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
|
67
|
-
JSON.parse(data).map { |model| ModelInfo.new(
|
148
|
+
JSON.parse(data).map { |model| ModelInfo.new(Utils.deep_symbolize_keys(model)) }
|
68
149
|
rescue JSON::ParserError
|
69
150
|
[]
|
70
151
|
end
|
data/lib/ruby_llm/provider.rb
CHANGED
@@ -7,15 +7,11 @@ module RubyLLM
|
|
7
7
|
module Provider
|
8
8
|
# Common functionality for all LLM providers. Implements the core provider
|
9
9
|
# interface so specific providers only need to implement a few key methods.
|
10
|
-
module Methods
|
10
|
+
module Methods
|
11
11
|
extend Streaming
|
12
12
|
|
13
|
-
def complete(messages, tools:, temperature:, model:, &
|
14
|
-
normalized_temperature =
|
15
|
-
capabilities.normalize_temperature(temperature, model)
|
16
|
-
else
|
17
|
-
temperature
|
18
|
-
end
|
13
|
+
def complete(messages, tools:, temperature:, model:, connection:, &)
|
14
|
+
normalized_temperature = maybe_normalize_temperature(temperature, model)
|
19
15
|
|
20
16
|
payload = render_payload(messages,
|
21
17
|
tools: tools,
|
@@ -24,112 +20,62 @@ module RubyLLM
|
|
24
20
|
stream: block_given?)
|
25
21
|
|
26
22
|
if block_given?
|
27
|
-
stream_response payload, &
|
23
|
+
stream_response connection, payload, &
|
28
24
|
else
|
29
|
-
sync_response payload
|
25
|
+
sync_response connection, payload
|
30
26
|
end
|
31
27
|
end
|
32
28
|
|
33
|
-
def list_models
|
34
|
-
response = connection.get
|
35
|
-
req.headers.merge! headers
|
36
|
-
end
|
37
|
-
|
29
|
+
def list_models(connection:)
|
30
|
+
response = connection.get models_url
|
38
31
|
parse_list_models_response response, slug, capabilities
|
39
32
|
end
|
40
33
|
|
41
|
-
def embed(text, model:)
|
42
|
-
payload = render_embedding_payload
|
43
|
-
response = post
|
44
|
-
parse_embedding_response
|
34
|
+
def embed(text, model:, connection:, dimensions:)
|
35
|
+
payload = render_embedding_payload(text, model:, dimensions:)
|
36
|
+
response = connection.post(embedding_url(model:), payload)
|
37
|
+
parse_embedding_response(response, model:)
|
45
38
|
end
|
46
39
|
|
47
|
-
def paint(prompt, model:, size:)
|
40
|
+
def paint(prompt, model:, size:, connection:)
|
48
41
|
payload = render_image_payload(prompt, model:, size:)
|
49
|
-
|
50
|
-
response
|
51
|
-
parse_image_response(response)
|
42
|
+
response = connection.post images_url, payload
|
43
|
+
parse_image_response response
|
52
44
|
end
|
53
45
|
|
54
|
-
def configured?
|
55
|
-
|
46
|
+
def configured?(config = nil)
|
47
|
+
config ||= RubyLLM.config
|
48
|
+
missing_configs(config).empty?
|
56
49
|
end
|
57
50
|
|
58
|
-
|
59
|
-
|
60
|
-
def missing_configs
|
51
|
+
def missing_configs(config)
|
61
52
|
configuration_requirements.select do |key|
|
62
|
-
value =
|
53
|
+
value = config.send(key)
|
63
54
|
value.nil? || value.empty?
|
64
55
|
end
|
65
56
|
end
|
66
57
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
config_block = <<~RUBY
|
71
|
-
RubyLLM.configure do |config|
|
72
|
-
#{missing_configs.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
|
73
|
-
end
|
74
|
-
RUBY
|
75
|
-
|
76
|
-
raise ConfigurationError,
|
77
|
-
"#{slug} provider is not configured. Add this to your initialization:\n\n#{config_block}"
|
58
|
+
def local?
|
59
|
+
false
|
78
60
|
end
|
79
61
|
|
80
|
-
def
|
81
|
-
|
82
|
-
parse_completion_response response
|
62
|
+
def remote?
|
63
|
+
!local?
|
83
64
|
end
|
84
65
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
66
|
+
private
|
67
|
+
|
68
|
+
def maybe_normalize_temperature(temperature, model)
|
69
|
+
if capabilities.respond_to?(:normalize_temperature)
|
70
|
+
capabilities.normalize_temperature(temperature, model)
|
71
|
+
else
|
72
|
+
temperature
|
89
73
|
end
|
90
74
|
end
|
91
75
|
|
92
|
-
def connection
|
93
|
-
|
94
|
-
|
95
|
-
@connection ||= Faraday.new(api_base) do |f| # rubocop:disable Metrics/BlockLength
|
96
|
-
f.options.timeout = RubyLLM.config.request_timeout
|
97
|
-
|
98
|
-
f.response :logger,
|
99
|
-
RubyLLM.logger,
|
100
|
-
bodies: true,
|
101
|
-
response: true,
|
102
|
-
errors: true,
|
103
|
-
headers: false,
|
104
|
-
log_level: :debug do |logger|
|
105
|
-
logger.filter(%r{"[A-Za-z0-9+/=]{100,}"}, 'data":"[BASE64 DATA]"')
|
106
|
-
logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
|
107
|
-
end
|
108
|
-
|
109
|
-
f.request :retry, {
|
110
|
-
max: RubyLLM.config.max_retries,
|
111
|
-
interval: RubyLLM.config.retry_interval,
|
112
|
-
interval_randomness: RubyLLM.config.retry_interval_randomness,
|
113
|
-
backoff_factor: RubyLLM.config.retry_backoff_factor,
|
114
|
-
exceptions: [
|
115
|
-
Errno::ETIMEDOUT,
|
116
|
-
Timeout::Error,
|
117
|
-
Faraday::TimeoutError,
|
118
|
-
Faraday::ConnectionFailed,
|
119
|
-
Faraday::RetriableResponse,
|
120
|
-
RubyLLM::RateLimitError,
|
121
|
-
RubyLLM::ServerError,
|
122
|
-
RubyLLM::ServiceUnavailableError,
|
123
|
-
RubyLLM::OverloadedError
|
124
|
-
],
|
125
|
-
retry_statuses: [429, 500, 502, 503, 504, 529]
|
126
|
-
}
|
127
|
-
|
128
|
-
f.request :json
|
129
|
-
f.response :json
|
130
|
-
f.adapter Faraday.default_adapter
|
131
|
-
f.use :llm_errors, provider: self
|
132
|
-
end
|
76
|
+
def sync_response(connection, payload)
|
77
|
+
response = connection.post completion_url, payload
|
78
|
+
parse_completion_response response
|
133
79
|
end
|
134
80
|
end
|
135
81
|
|
@@ -141,7 +87,7 @@ module RubyLLM
|
|
141
87
|
maybe_json
|
142
88
|
end
|
143
89
|
|
144
|
-
def parse_error(response)
|
90
|
+
def parse_error(response)
|
145
91
|
return if response.body.empty?
|
146
92
|
|
147
93
|
body = try_parse_json(response.body)
|
@@ -167,6 +113,10 @@ module RubyLLM
|
|
167
113
|
nil
|
168
114
|
end
|
169
115
|
|
116
|
+
def connection(config)
|
117
|
+
@connection ||= Connection.new(self, config)
|
118
|
+
end
|
119
|
+
|
170
120
|
class << self
|
171
121
|
def extended(base)
|
172
122
|
base.extend(Methods)
|
@@ -186,8 +136,16 @@ module RubyLLM
|
|
186
136
|
@providers ||= {}
|
187
137
|
end
|
188
138
|
|
189
|
-
def
|
190
|
-
providers.select { |
|
139
|
+
def local_providers
|
140
|
+
providers.select { |_slug, provider| provider.local? }
|
141
|
+
end
|
142
|
+
|
143
|
+
def remote_providers
|
144
|
+
providers.select { |_slug, provider| provider.remote? }
|
145
|
+
end
|
146
|
+
|
147
|
+
def configured_providers(config = nil)
|
148
|
+
providers.select { |_slug, provider| provider.configured?(config) }.values
|
191
149
|
end
|
192
150
|
end
|
193
151
|
end
|
@@ -73,13 +73,13 @@ module RubyLLM
|
|
73
73
|
# @return [Symbol] the model family identifier
|
74
74
|
def model_family(model_id)
|
75
75
|
case model_id
|
76
|
-
when /claude-3-7-sonnet/ then
|
77
|
-
when /claude-3-5-sonnet/ then
|
78
|
-
when /claude-3-5-haiku/ then
|
79
|
-
when /claude-3-opus/ then
|
80
|
-
when /claude-3-sonnet/ then
|
81
|
-
when /claude-3-haiku/ then
|
82
|
-
else
|
76
|
+
when /claude-3-7-sonnet/ then 'claude-3-7-sonnet'
|
77
|
+
when /claude-3-5-sonnet/ then 'claude-3-5-sonnet'
|
78
|
+
when /claude-3-5-haiku/ then 'claude-3-5-haiku'
|
79
|
+
when /claude-3-opus/ then 'claude-3-opus'
|
80
|
+
when /claude-3-sonnet/ then 'claude-3-sonnet'
|
81
|
+
when /claude-3-haiku/ then 'claude-3-haiku'
|
82
|
+
else 'claude-2'
|
83
83
|
end
|
84
84
|
end
|
85
85
|
|
@@ -92,12 +92,12 @@ module RubyLLM
|
|
92
92
|
|
93
93
|
# Pricing information for Anthropic models (per million tokens)
|
94
94
|
PRICES = {
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
95
|
+
'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
|
96
|
+
'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
|
97
|
+
'claude-3-5-haiku': { input: 0.80, output: 4.0 },
|
98
|
+
'claude-3-opus': { input: 15.0, output: 75.0 },
|
99
|
+
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
100
|
+
'claude-2': { input: 3.0, output: 15.0 }
|
101
101
|
}.freeze
|
102
102
|
|
103
103
|
# Default input price if model not found in PRICES
|
@@ -111,6 +111,69 @@ module RubyLLM
|
|
111
111
|
def default_output_price
|
112
112
|
15.0
|
113
113
|
end
|
114
|
+
|
115
|
+
def modalities_for(model_id)
|
116
|
+
modalities = {
|
117
|
+
input: ['text'],
|
118
|
+
output: ['text']
|
119
|
+
}
|
120
|
+
|
121
|
+
# All Claude 3+ models support vision
|
122
|
+
unless model_id.match?(/claude-[12]/)
|
123
|
+
modalities[:input] << 'image'
|
124
|
+
modalities[:input] << 'pdf'
|
125
|
+
end
|
126
|
+
|
127
|
+
modalities
|
128
|
+
end
|
129
|
+
|
130
|
+
def capabilities_for(model_id)
|
131
|
+
capabilities = ['streaming']
|
132
|
+
|
133
|
+
# Function calling for Claude 3+
|
134
|
+
if model_id.match?(/claude-3/)
|
135
|
+
capabilities << 'function_calling'
|
136
|
+
capabilities << 'structured_output'
|
137
|
+
capabilities << 'batch'
|
138
|
+
end
|
139
|
+
|
140
|
+
# Extended thinking (reasoning) for Claude 3.7
|
141
|
+
capabilities << 'reasoning' if model_id.match?(/claude-3-7/)
|
142
|
+
|
143
|
+
# Citations
|
144
|
+
capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/)
|
145
|
+
|
146
|
+
capabilities
|
147
|
+
end
|
148
|
+
|
149
|
+
def pricing_for(model_id)
|
150
|
+
family = model_family(model_id)
|
151
|
+
prices = PRICES.fetch(family.to_sym, { input: default_input_price, output: default_output_price })
|
152
|
+
|
153
|
+
standard_pricing = {
|
154
|
+
input_per_million: prices[:input],
|
155
|
+
output_per_million: prices[:output]
|
156
|
+
}
|
157
|
+
|
158
|
+
# Batch is typically half the price
|
159
|
+
batch_pricing = {
|
160
|
+
input_per_million: prices[:input] * 0.5,
|
161
|
+
output_per_million: prices[:output] * 0.5
|
162
|
+
}
|
163
|
+
|
164
|
+
# Add reasoning output pricing for 3.7 models
|
165
|
+
if model_id.match?(/claude-3-7/)
|
166
|
+
standard_pricing[:reasoning_output_per_million] = prices[:output] * 2.5
|
167
|
+
batch_pricing[:reasoning_output_per_million] = prices[:output] * 1.25
|
168
|
+
end
|
169
|
+
|
170
|
+
{
|
171
|
+
text_tokens: {
|
172
|
+
standard: standard_pricing,
|
173
|
+
batch: batch_pricing
|
174
|
+
}
|
175
|
+
}
|
176
|
+
end
|
114
177
|
end
|
115
178
|
end
|
116
179
|
end
|
@@ -5,7 +5,7 @@ module RubyLLM
|
|
5
5
|
module Anthropic
|
6
6
|
# Chat methods of the OpenAI API integration
|
7
7
|
module Chat
|
8
|
-
|
8
|
+
module_function
|
9
9
|
|
10
10
|
def completion_url
|
11
11
|
'/v1/messages'
|
@@ -41,12 +41,12 @@ module RubyLLM
|
|
41
41
|
messages: chat_messages.map { |msg| format_message(msg) },
|
42
42
|
temperature: temperature,
|
43
43
|
stream: stream,
|
44
|
-
max_tokens: RubyLLM.models.find(model)
|
44
|
+
max_tokens: RubyLLM.models.find(model)&.max_tokens || 4096
|
45
45
|
}
|
46
46
|
end
|
47
47
|
|
48
48
|
def add_optional_fields(payload, system_content:, tools:)
|
49
|
-
payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
|
49
|
+
payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any?
|
50
50
|
payload[:system] = system_content unless system_content.empty?
|
51
51
|
end
|
52
52
|
|
@@ -55,7 +55,7 @@ module RubyLLM
|
|
55
55
|
content_blocks = data['content'] || []
|
56
56
|
|
57
57
|
text_content = extract_text_content(content_blocks)
|
58
|
-
tool_use = find_tool_use(content_blocks)
|
58
|
+
tool_use = Tools.find_tool_use(content_blocks)
|
59
59
|
|
60
60
|
build_message(data, text_content, tool_use)
|
61
61
|
end
|
@@ -69,7 +69,7 @@ module RubyLLM
|
|
69
69
|
Message.new(
|
70
70
|
role: :assistant,
|
71
71
|
content: content,
|
72
|
-
tool_calls: parse_tool_calls(tool_use),
|
72
|
+
tool_calls: Tools.parse_tool_calls(tool_use),
|
73
73
|
input_tokens: data.dig('usage', 'input_tokens'),
|
74
74
|
output_tokens: data.dig('usage', 'output_tokens'),
|
75
75
|
model_id: data['model']
|
@@ -78,9 +78,9 @@ module RubyLLM
|
|
78
78
|
|
79
79
|
def format_message(msg)
|
80
80
|
if msg.tool_call?
|
81
|
-
format_tool_call(msg)
|
81
|
+
Tools.format_tool_call(msg)
|
82
82
|
elsif msg.tool_result?
|
83
|
-
format_tool_result(msg)
|
83
|
+
Tools.format_tool_result(msg)
|
84
84
|
else
|
85
85
|
format_basic_message(msg)
|
86
86
|
end
|
@@ -99,13 +99,6 @@ module RubyLLM
|
|
99
99
|
else 'assistant'
|
100
100
|
end
|
101
101
|
end
|
102
|
-
|
103
|
-
def format_text_block(content)
|
104
|
-
{
|
105
|
-
type: 'text',
|
106
|
-
text: content
|
107
|
-
}
|
108
|
-
end
|
109
102
|
end
|
110
103
|
end
|
111
104
|
end
|
@@ -7,62 +7,72 @@ module RubyLLM
|
|
7
7
|
module Media
|
8
8
|
module_function
|
9
9
|
|
10
|
-
def format_content(content)
|
11
|
-
return content unless content.is_a?(
|
10
|
+
def format_content(content)
|
11
|
+
return [format_text(content)] unless content.is_a?(Content)
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
part
|
13
|
+
parts = []
|
14
|
+
parts << format_text(content.text) if content.text
|
15
|
+
|
16
|
+
content.attachments.each do |attachment|
|
17
|
+
case attachment
|
18
|
+
when Attachments::Image
|
19
|
+
parts << format_image(attachment)
|
20
|
+
when Attachments::PDF
|
21
|
+
parts << format_pdf(attachment)
|
23
22
|
end
|
24
23
|
end
|
24
|
+
|
25
|
+
parts
|
25
26
|
end
|
26
27
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
def format_text(text)
|
29
|
+
{
|
30
|
+
type: 'text',
|
31
|
+
text: text
|
32
|
+
}
|
31
33
|
end
|
32
34
|
|
33
|
-
def
|
34
|
-
|
35
|
+
def format_image(image)
|
36
|
+
if image.url?
|
37
|
+
{
|
38
|
+
type: 'image',
|
39
|
+
source: {
|
40
|
+
type: 'url',
|
41
|
+
url: image.source
|
42
|
+
}
|
43
|
+
}
|
44
|
+
else
|
45
|
+
{
|
46
|
+
type: 'image',
|
47
|
+
source: {
|
48
|
+
type: 'base64',
|
49
|
+
media_type: image.mime_type,
|
50
|
+
data: image.encoded
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
35
55
|
|
36
|
-
|
37
|
-
|
56
|
+
def format_pdf(pdf)
|
57
|
+
if pdf.url?
|
38
58
|
{
|
39
59
|
type: 'document',
|
40
60
|
source: {
|
41
|
-
type: 'url',
|
42
|
-
url: source
|
61
|
+
type: 'url',
|
62
|
+
url: pdf.source
|
43
63
|
}
|
44
64
|
}
|
45
65
|
else
|
46
|
-
# For local files
|
47
|
-
data = Base64.strict_encode64(part[:content])
|
48
|
-
|
49
66
|
{
|
50
67
|
type: 'document',
|
51
68
|
source: {
|
52
69
|
type: 'base64',
|
53
|
-
media_type:
|
54
|
-
data:
|
70
|
+
media_type: pdf.mime_type,
|
71
|
+
data: pdf.encoded
|
55
72
|
}
|
56
73
|
}
|
57
74
|
end
|
58
75
|
end
|
59
|
-
|
60
|
-
def format_text_block(text)
|
61
|
-
{
|
62
|
-
type: 'text',
|
63
|
-
text: text
|
64
|
-
}
|
65
|
-
end
|
66
76
|
end
|
67
77
|
end
|
68
78
|
end
|
@@ -5,28 +5,28 @@ module RubyLLM
|
|
5
5
|
module Anthropic
|
6
6
|
# Models methods of the Anthropic API integration
|
7
7
|
module Models
|
8
|
-
|
8
|
+
module_function
|
9
9
|
|
10
10
|
def models_url
|
11
11
|
'/v1/models'
|
12
12
|
end
|
13
13
|
|
14
|
-
def parse_list_models_response(response, slug, capabilities)
|
15
|
-
(response.body['data']
|
14
|
+
def parse_list_models_response(response, slug, capabilities)
|
15
|
+
Array(response.body['data']).map do |model_data|
|
16
|
+
model_id = model_data['id']
|
17
|
+
|
16
18
|
ModelInfo.new(
|
17
|
-
id:
|
18
|
-
|
19
|
-
display_name: model['display_name'],
|
19
|
+
id: model_id,
|
20
|
+
name: model_data['display_name'],
|
20
21
|
provider: slug,
|
21
|
-
|
22
|
-
|
23
|
-
context_window: capabilities.determine_context_window(
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
output_price_per_million: capabilities.get_output_price(model['id'])
|
22
|
+
family: capabilities.model_family(model_id),
|
23
|
+
created_at: Time.parse(model_data['created_at']),
|
24
|
+
context_window: capabilities.determine_context_window(model_id),
|
25
|
+
max_output_tokens: capabilities.determine_max_tokens(model_id),
|
26
|
+
modalities: capabilities.modalities_for(model_id),
|
27
|
+
capabilities: capabilities.capabilities_for(model_id),
|
28
|
+
pricing: capabilities.pricing_for(model_id),
|
29
|
+
metadata: {}
|
30
30
|
)
|
31
31
|
end
|
32
32
|
end
|
@@ -5,7 +5,7 @@ module RubyLLM
|
|
5
5
|
module Anthropic
|
6
6
|
# Tools methods of the Anthropic API integration
|
7
7
|
module Tools
|
8
|
-
|
8
|
+
module_function
|
9
9
|
|
10
10
|
def find_tool_use(blocks)
|
11
11
|
blocks.find { |c| c['type'] == 'tool_use' }
|
@@ -17,7 +17,7 @@ module RubyLLM
|
|
17
17
|
{
|
18
18
|
role: 'assistant',
|
19
19
|
content: [
|
20
|
-
|
20
|
+
Media.format_text(msg.content),
|
21
21
|
format_tool_use_block(tool_call)
|
22
22
|
]
|
23
23
|
}
|