ruby_llm 1.2.0 → 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 +22 -19
- data/lib/ruby_llm/configuration.rb +30 -1
- 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 +25766 -2154
- 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 +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 +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 +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/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 +164 -121
- 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
@@ -4,7 +4,7 @@ module RubyLLM
|
|
4
4
|
module Providers
|
5
5
|
module Gemini
|
6
6
|
# Determines capabilities and pricing for Google Gemini models
|
7
|
-
module Capabilities
|
7
|
+
module Capabilities
|
8
8
|
module_function
|
9
9
|
|
10
10
|
# Returns the context window size (input token limit) for the given model
|
@@ -144,7 +144,7 @@ module RubyLLM
|
|
144
144
|
# Returns the model family identifier
|
145
145
|
# @param model_id [String] the model identifier
|
146
146
|
# @return [String] the model family identifier
|
147
|
-
def model_family(model_id)
|
147
|
+
def model_family(model_id)
|
148
148
|
case model_id
|
149
149
|
when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
|
150
150
|
when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
|
@@ -164,7 +164,7 @@ module RubyLLM
|
|
164
164
|
# Returns the pricing family identifier for the model
|
165
165
|
# @param model_id [String] the model identifier
|
166
166
|
# @return [Symbol] the pricing family identifier
|
167
|
-
def pricing_family(model_id)
|
167
|
+
def pricing_family(model_id)
|
168
168
|
case model_id
|
169
169
|
when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
|
170
170
|
when /gemini-2\.0-flash-lite/ then :flash_lite_2 # rubocop:disable Naming/VariableNumber
|
@@ -261,6 +261,87 @@ module RubyLLM
|
|
261
261
|
def default_output_price
|
262
262
|
0.30 # Default to Flash pricing
|
263
263
|
end
|
264
|
+
|
265
|
+
def modalities_for(model_id)
|
266
|
+
modalities = {
|
267
|
+
input: ['text'],
|
268
|
+
output: ['text']
|
269
|
+
}
|
270
|
+
|
271
|
+
# Vision support
|
272
|
+
if supports_vision?(model_id)
|
273
|
+
modalities[:input] << 'image'
|
274
|
+
modalities[:input] << 'pdf'
|
275
|
+
end
|
276
|
+
|
277
|
+
# Audio support
|
278
|
+
modalities[:input] << 'audio' if model_id.match?(/audio/)
|
279
|
+
|
280
|
+
# Embedding output
|
281
|
+
modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
|
282
|
+
|
283
|
+
modalities
|
284
|
+
end
|
285
|
+
|
286
|
+
def capabilities_for(model_id)
|
287
|
+
capabilities = ['streaming']
|
288
|
+
|
289
|
+
# Function calling
|
290
|
+
capabilities << 'function_calling' if supports_functions?(model_id)
|
291
|
+
|
292
|
+
# JSON mode
|
293
|
+
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
294
|
+
|
295
|
+
# Batch processing
|
296
|
+
capabilities << 'batch' if model_id.match?(/embedding|flash/)
|
297
|
+
|
298
|
+
# Caching
|
299
|
+
capabilities << 'caching' if supports_caching?(model_id)
|
300
|
+
|
301
|
+
# Tuning
|
302
|
+
capabilities << 'fine_tuning' if supports_tuning?(model_id)
|
303
|
+
|
304
|
+
capabilities
|
305
|
+
end
|
306
|
+
|
307
|
+
def pricing_for(model_id)
|
308
|
+
family = pricing_family(model_id)
|
309
|
+
prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
|
310
|
+
|
311
|
+
standard_pricing = {
|
312
|
+
input_per_million: prices[:input],
|
313
|
+
output_per_million: prices[:output]
|
314
|
+
}
|
315
|
+
|
316
|
+
# Add cached pricing if available
|
317
|
+
standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
|
318
|
+
|
319
|
+
# Batch pricing (typically 50% discount)
|
320
|
+
batch_pricing = {
|
321
|
+
input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
|
322
|
+
output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
|
323
|
+
}
|
324
|
+
|
325
|
+
if standard_pricing[:cached_input_per_million]
|
326
|
+
batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
|
327
|
+
end
|
328
|
+
|
329
|
+
pricing = {
|
330
|
+
text_tokens: {
|
331
|
+
standard: standard_pricing,
|
332
|
+
batch: batch_pricing
|
333
|
+
}
|
334
|
+
}
|
335
|
+
|
336
|
+
# Add embedding pricing if applicable
|
337
|
+
if model_id.match?(/embedding|gemini-embedding/)
|
338
|
+
pricing[:embeddings] = {
|
339
|
+
standard: { input_per_million: prices[:price] || 0.002 }
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
pricing
|
344
|
+
end
|
264
345
|
end
|
265
346
|
end
|
266
347
|
end
|
@@ -5,32 +5,24 @@ module RubyLLM
|
|
5
5
|
module Gemini
|
6
6
|
# Chat methods for the Gemini API implementation
|
7
7
|
module Chat
|
8
|
+
module_function
|
9
|
+
|
8
10
|
def completion_url
|
9
11
|
"models/#{@model}:generateContent"
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
@model = model
|
14
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false) # rubocop:disable Lint/UnusedMethodArgument
|
15
|
+
@model = model # Store model for completion_url/stream_url
|
14
16
|
payload = {
|
15
17
|
contents: format_messages(messages),
|
16
18
|
generationConfig: {
|
17
19
|
temperature: temperature
|
18
20
|
}
|
19
21
|
}
|
20
|
-
|
21
22
|
payload[:tools] = format_tools(tools) if tools.any?
|
22
|
-
|
23
|
-
# Store tools for use in generate_completion
|
24
|
-
@tools = tools
|
25
|
-
|
26
|
-
if block_given?
|
27
|
-
stream_response payload, &block
|
28
|
-
else
|
29
|
-
sync_response payload
|
30
|
-
end
|
23
|
+
payload
|
31
24
|
end
|
32
25
|
|
33
|
-
# Format methods can be private
|
34
26
|
private
|
35
27
|
|
36
28
|
def format_messages(messages)
|
@@ -50,9 +42,8 @@ module RubyLLM
|
|
50
42
|
end
|
51
43
|
end
|
52
44
|
|
53
|
-
def format_parts(msg)
|
45
|
+
def format_parts(msg)
|
54
46
|
if msg.tool_call?
|
55
|
-
# Handle function calls
|
56
47
|
[{
|
57
48
|
functionCall: {
|
58
49
|
name: msg.tool_calls.values.first.name,
|
@@ -60,7 +51,6 @@ module RubyLLM
|
|
60
51
|
}
|
61
52
|
}]
|
62
53
|
elsif msg.tool_result?
|
63
|
-
# Handle function responses
|
64
54
|
[{
|
65
55
|
functionResponse: {
|
66
56
|
name: msg.tool_call_id,
|
@@ -70,27 +60,8 @@ module RubyLLM
|
|
70
60
|
}
|
71
61
|
}
|
72
62
|
}]
|
73
|
-
elsif msg.content.is_a?(Array)
|
74
|
-
# Handle multi-part content (text, images, etc.)
|
75
|
-
msg.content.map { |part| format_part(part) }
|
76
|
-
else
|
77
|
-
# Simple text content
|
78
|
-
[{ text: msg.content.to_s }]
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def format_part(part) # rubocop:disable Metrics/MethodLength
|
83
|
-
case part[:type]
|
84
|
-
when 'text'
|
85
|
-
{ text: part[:text] }
|
86
|
-
when 'image'
|
87
|
-
Media.format_image(part)
|
88
|
-
when 'pdf'
|
89
|
-
Media.format_pdf(part)
|
90
|
-
when 'audio'
|
91
|
-
Media.format_audio(part)
|
92
63
|
else
|
93
|
-
|
64
|
+
Media.format_content(msg.content)
|
94
65
|
end
|
95
66
|
end
|
96
67
|
|
@@ -108,7 +79,7 @@ module RubyLLM
|
|
108
79
|
)
|
109
80
|
end
|
110
81
|
|
111
|
-
def extract_content(data)
|
82
|
+
def extract_content(data)
|
112
83
|
candidate = data.dig('candidates', 0)
|
113
84
|
return '' unless candidate
|
114
85
|
|
@@ -5,47 +5,31 @@ module RubyLLM
|
|
5
5
|
module Gemini
|
6
6
|
# Embeddings methods for the Gemini API integration
|
7
7
|
module Embeddings
|
8
|
-
|
9
|
-
def embed(text, model:) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
10
|
-
payload = {
|
11
|
-
content: {
|
12
|
-
parts: format_text_for_embedding(text)
|
13
|
-
}
|
14
|
-
}
|
8
|
+
module_function
|
15
9
|
|
16
|
-
|
17
|
-
|
10
|
+
def embedding_url(model:)
|
11
|
+
"models/#{model}:batchEmbedContents"
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_embedding_payload(text, model:, dimensions:)
|
15
|
+
{ requests: [text].flatten.map { |t| single_embedding_payload(t, model:, dimensions:) } }
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
single_payload = { content: { parts: [{ text: t.to_s }] } }
|
23
|
-
single_response = post(url, single_payload)
|
24
|
-
single_response.body.dig('embedding', 'values')
|
25
|
-
end
|
18
|
+
def parse_embedding_response(response, model:)
|
19
|
+
vectors = response.body['embeddings']&.map { |e| e['values'] }
|
20
|
+
vectors in [vectors]
|
26
21
|
|
27
|
-
|
28
|
-
vectors: embeddings,
|
29
|
-
model: model,
|
30
|
-
input_tokens: response.body.dig('usageMetadata', 'promptTokenCount') || 0
|
31
|
-
)
|
32
|
-
else
|
33
|
-
Embedding.new(
|
34
|
-
vectors: response.body.dig('embedding', 'values'),
|
35
|
-
model: model,
|
36
|
-
input_tokens: response.body.dig('usageMetadata', 'promptTokenCount') || 0
|
37
|
-
)
|
38
|
-
end
|
22
|
+
Embedding.new(vectors:, model:, input_tokens: 0)
|
39
23
|
end
|
40
24
|
|
41
25
|
private
|
42
26
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
27
|
+
def single_embedding_payload(text, model:, dimensions:)
|
28
|
+
{
|
29
|
+
model: "models/#{model}",
|
30
|
+
content: { parts: [{ text: text.to_s }] },
|
31
|
+
outputDimensionality: dimensions
|
32
|
+
}.compact
|
49
33
|
end
|
50
34
|
end
|
51
35
|
end
|
@@ -9,7 +9,7 @@ module RubyLLM
|
|
9
9
|
"models/#{@model}:predict"
|
10
10
|
end
|
11
11
|
|
12
|
-
def render_image_payload(prompt, model:, size:)
|
12
|
+
def render_image_payload(prompt, model:, size:)
|
13
13
|
RubyLLM.logger.debug "Ignoring size #{size}. Gemini does not support image size customization."
|
14
14
|
@model = model
|
15
15
|
{
|
@@ -24,7 +24,7 @@ module RubyLLM
|
|
24
24
|
}
|
25
25
|
end
|
26
26
|
|
27
|
-
def parse_image_response(response)
|
27
|
+
def parse_image_response(response)
|
28
28
|
data = response.body
|
29
29
|
image_data = data['predictions']&.first
|
30
30
|
|
@@ -4,131 +4,60 @@ module RubyLLM
|
|
4
4
|
module Providers
|
5
5
|
module Gemini
|
6
6
|
# Media handling methods for the Gemini API integration
|
7
|
-
module Media
|
7
|
+
module Media
|
8
8
|
module_function
|
9
9
|
|
10
|
-
def
|
11
|
-
|
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
|
-
|
23
|
-
|
24
|
-
{
|
25
|
-
inline_data: {
|
26
|
-
mime_type: mime_type_for_image(source),
|
27
|
-
data: encode_image_file(source)
|
28
|
-
}
|
29
|
-
}
|
30
|
-
end
|
31
|
-
elsif source.is_a?(Hash)
|
32
|
-
if source[:url]
|
33
|
-
# Handle URL in hash
|
34
|
-
{
|
35
|
-
inline_data: {
|
36
|
-
mime_type: source[:media_type] || mime_type_for_image(source[:url]),
|
37
|
-
data: fetch_and_encode_image(source[:url])
|
38
|
-
}
|
39
|
-
}
|
40
|
-
else
|
41
|
-
# Handle data in hash
|
42
|
-
{
|
43
|
-
inline_data: {
|
44
|
-
mime_type: source[:media_type] || 'image/jpeg',
|
45
|
-
data: source[:data]
|
46
|
-
}
|
47
|
-
}
|
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)
|
22
|
+
when Attachments::Audio
|
23
|
+
parts << format_audio(attachment)
|
48
24
|
end
|
49
25
|
end
|
50
|
-
end
|
51
26
|
|
52
|
-
|
53
|
-
source = part[:source]
|
54
|
-
|
55
|
-
if source.is_a?(String) && source.start_with?('http')
|
56
|
-
# Handle URL
|
57
|
-
{
|
58
|
-
inline_data: {
|
59
|
-
mime_type: 'application/pdf',
|
60
|
-
data: fetch_and_encode_pdf(source)
|
61
|
-
}
|
62
|
-
}
|
63
|
-
else
|
64
|
-
# Handle file path or data
|
65
|
-
{
|
66
|
-
inline_data: {
|
67
|
-
mime_type: 'application/pdf',
|
68
|
-
data: part[:content] ? Base64.strict_encode64(part[:content]) : encode_pdf_file(source)
|
69
|
-
}
|
70
|
-
}
|
71
|
-
end
|
27
|
+
parts
|
72
28
|
end
|
73
29
|
|
74
|
-
def
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
{
|
80
|
-
file_data: {
|
81
|
-
mime_type: mime_type_for_audio(source),
|
82
|
-
file_uri: source
|
83
|
-
}
|
30
|
+
def format_image(image)
|
31
|
+
{
|
32
|
+
inline_data: {
|
33
|
+
mime_type: image.mime_type,
|
34
|
+
data: image.encoded
|
84
35
|
}
|
85
|
-
|
86
|
-
# Handle file path or data
|
87
|
-
content = part[:content] || File.read(source)
|
88
|
-
{
|
89
|
-
inline_data: {
|
90
|
-
mime_type: mime_type_for_audio(source),
|
91
|
-
data: Base64.strict_encode64(content)
|
92
|
-
}
|
93
|
-
}
|
94
|
-
end
|
36
|
+
}
|
95
37
|
end
|
96
38
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def mime_type_for_audio(path)
|
108
|
-
ext = File.extname(path).downcase.delete('.')
|
109
|
-
case ext
|
110
|
-
when 'mp3' then 'audio/mpeg'
|
111
|
-
when 'ogg' then 'audio/ogg'
|
112
|
-
else 'audio/wav'
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
def fetch_and_encode_image(url)
|
117
|
-
response = Faraday.get(url)
|
118
|
-
Base64.strict_encode64(response.body)
|
119
|
-
end
|
120
|
-
|
121
|
-
def fetch_and_encode_pdf(url)
|
122
|
-
response = Faraday.get(url)
|
123
|
-
Base64.strict_encode64(response.body)
|
39
|
+
def format_pdf(pdf)
|
40
|
+
{
|
41
|
+
inline_data: {
|
42
|
+
mime_type: pdf.mime_type,
|
43
|
+
data: pdf.encoded
|
44
|
+
}
|
45
|
+
}
|
124
46
|
end
|
125
47
|
|
126
|
-
def
|
127
|
-
|
48
|
+
def format_audio(audio)
|
49
|
+
{
|
50
|
+
inline_data: {
|
51
|
+
mime_type: audio.mime_type,
|
52
|
+
data: audio.encoded
|
53
|
+
}
|
54
|
+
}
|
128
55
|
end
|
129
56
|
|
130
|
-
def
|
131
|
-
|
57
|
+
def format_text(text)
|
58
|
+
{
|
59
|
+
text: text
|
60
|
+
}
|
132
61
|
end
|
133
62
|
end
|
134
63
|
end
|
@@ -5,39 +5,33 @@ module RubyLLM
|
|
5
5
|
module Gemini
|
6
6
|
# Models methods for the Gemini API integration
|
7
7
|
module Models
|
8
|
-
|
8
|
+
module_function
|
9
|
+
|
9
10
|
def models_url
|
10
11
|
'models'
|
11
12
|
end
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
def parse_list_models_response(response, slug, capabilities) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
16
|
-
(response.body['models'] || []).map do |model|
|
14
|
+
def parse_list_models_response(response, slug, capabilities)
|
15
|
+
Array(response.body['models']).map do |model_data|
|
17
16
|
# Extract model ID without "models/" prefix
|
18
|
-
model_id =
|
17
|
+
model_id = model_data['name'].gsub('models/', '')
|
19
18
|
|
20
19
|
ModelInfo.new(
|
21
20
|
id: model_id,
|
22
|
-
|
23
|
-
display_name: model['displayName'],
|
21
|
+
name: model_data['displayName'],
|
24
22
|
provider: slug,
|
25
|
-
type: capabilities.model_type(model_id),
|
26
23
|
family: capabilities.model_family(model_id),
|
24
|
+
created_at: nil, # Gemini API doesn't provide creation date
|
25
|
+
context_window: model_data['inputTokenLimit'] || capabilities.context_window_for(model_id),
|
26
|
+
max_output_tokens: model_data['outputTokenLimit'] || capabilities.max_tokens_for(model_id),
|
27
|
+
modalities: capabilities.modalities_for(model_id),
|
28
|
+
capabilities: capabilities.capabilities_for(model_id),
|
29
|
+
pricing: capabilities.pricing_for(model_id),
|
27
30
|
metadata: {
|
28
|
-
version:
|
29
|
-
description:
|
30
|
-
|
31
|
-
|
32
|
-
supported_generation_methods: model['supportedGenerationMethods']
|
33
|
-
},
|
34
|
-
context_window: model['inputTokenLimit'] || capabilities.context_window_for(model_id),
|
35
|
-
max_tokens: model['outputTokenLimit'] || capabilities.max_tokens_for(model_id),
|
36
|
-
supports_vision: capabilities.supports_vision?(model_id),
|
37
|
-
supports_functions: capabilities.supports_functions?(model_id),
|
38
|
-
supports_json_mode: capabilities.supports_json_mode?(model_id),
|
39
|
-
input_price_per_million: capabilities.input_price_for(model_id),
|
40
|
-
output_price_per_million: capabilities.output_price_for(model_id)
|
31
|
+
version: model_data['version'],
|
32
|
+
description: model_data['description'],
|
33
|
+
supported_generation_methods: model_data['supportedGenerationMethods']
|
34
|
+
}
|
41
35
|
)
|
42
36
|
end
|
43
37
|
end
|
@@ -15,13 +15,13 @@ module RubyLLM
|
|
15
15
|
|
16
16
|
module_function
|
17
17
|
|
18
|
-
def api_base
|
18
|
+
def api_base(_config)
|
19
19
|
'https://generativelanguage.googleapis.com/v1beta'
|
20
20
|
end
|
21
21
|
|
22
|
-
def headers
|
22
|
+
def headers(config)
|
23
23
|
{
|
24
|
-
'x-goog-api-key' =>
|
24
|
+
'x-goog-api-key' => config.gemini_api_key
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Ollama
|
6
|
+
# Chat methods of the Ollama API integration
|
7
|
+
module Chat
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def format_messages(messages)
|
11
|
+
messages.map do |msg|
|
12
|
+
{
|
13
|
+
role: format_role(msg.role),
|
14
|
+
content: Ollama::Media.format_content(msg.content),
|
15
|
+
tool_calls: format_tool_calls(msg.tool_calls),
|
16
|
+
tool_call_id: msg.tool_call_id
|
17
|
+
}.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def format_role(role)
|
22
|
+
# Ollama doesn't use the new OpenAI convention for system prompts
|
23
|
+
role.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Ollama
|
6
|
+
# Handles formatting of media content (images, audio) for OpenAI APIs
|
7
|
+
module Media
|
8
|
+
extend OpenAI::Media
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def format_content(content)
|
13
|
+
return content unless content.is_a?(Content)
|
14
|
+
|
15
|
+
parts = []
|
16
|
+
parts << format_text(content.text) if content.text
|
17
|
+
|
18
|
+
content.attachments.each do |attachment|
|
19
|
+
case attachment
|
20
|
+
when Attachments::Image
|
21
|
+
parts << Ollama::Media.format_image(attachment)
|
22
|
+
when Attachments::PDF
|
23
|
+
parts << format_pdf(attachment)
|
24
|
+
when Attachments::Audio
|
25
|
+
parts << format_audio(attachment)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
parts
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_image(image)
|
33
|
+
{
|
34
|
+
type: 'image_url',
|
35
|
+
image_url: {
|
36
|
+
url: "data:#{image.mime_type};base64,#{image.encoded}",
|
37
|
+
detail: 'auto'
|
38
|
+
}
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# Ollama API integration.
|
6
|
+
module Ollama
|
7
|
+
extend OpenAI
|
8
|
+
extend Ollama::Chat
|
9
|
+
extend Ollama::Media
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def api_base(config)
|
14
|
+
config.ollama_api_base
|
15
|
+
end
|
16
|
+
|
17
|
+
def headers(_config)
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
|
21
|
+
def slug
|
22
|
+
'ollama'
|
23
|
+
end
|
24
|
+
|
25
|
+
def configuration_requirements
|
26
|
+
%i[ollama_api_base]
|
27
|
+
end
|
28
|
+
|
29
|
+
def local?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|