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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -133
  3. data/lib/ruby_llm/active_record/acts_as.rb +212 -33
  4. data/lib/ruby_llm/aliases.json +48 -6
  5. data/lib/ruby_llm/attachments/audio.rb +12 -0
  6. data/lib/ruby_llm/attachments/image.rb +9 -0
  7. data/lib/ruby_llm/attachments/pdf.rb +9 -0
  8. data/lib/ruby_llm/attachments.rb +78 -0
  9. data/lib/ruby_llm/chat.rb +22 -19
  10. data/lib/ruby_llm/configuration.rb +30 -1
  11. data/lib/ruby_llm/connection.rb +95 -0
  12. data/lib/ruby_llm/content.rb +51 -72
  13. data/lib/ruby_llm/context.rb +30 -0
  14. data/lib/ruby_llm/embedding.rb +13 -5
  15. data/lib/ruby_llm/error.rb +1 -1
  16. data/lib/ruby_llm/image.rb +13 -5
  17. data/lib/ruby_llm/message.rb +12 -4
  18. data/lib/ruby_llm/mime_types.rb +713 -0
  19. data/lib/ruby_llm/model_info.rb +208 -27
  20. data/lib/ruby_llm/models.json +25766 -2154
  21. data/lib/ruby_llm/models.rb +95 -14
  22. data/lib/ruby_llm/provider.rb +48 -90
  23. data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
  24. data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
  25. data/lib/ruby_llm/providers/anthropic/media.rb +44 -34
  26. data/lib/ruby_llm/providers/anthropic/models.rb +15 -15
  27. data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
  28. data/lib/ruby_llm/providers/anthropic.rb +3 -3
  29. data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
  30. data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
  31. data/lib/ruby_llm/providers/bedrock/media.rb +56 -0
  32. data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
  33. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
  34. data/lib/ruby_llm/providers/bedrock.rb +14 -25
  35. data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
  36. data/lib/ruby_llm/providers/deepseek.rb +3 -3
  37. data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
  38. data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
  39. data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
  40. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  41. data/lib/ruby_llm/providers/gemini/media.rb +39 -110
  42. data/lib/ruby_llm/providers/gemini/models.rb +16 -22
  43. data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
  44. data/lib/ruby_llm/providers/gemini.rb +3 -3
  45. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  46. data/lib/ruby_llm/providers/ollama/media.rb +44 -0
  47. data/lib/ruby_llm/providers/ollama.rb +34 -0
  48. data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
  49. data/lib/ruby_llm/providers/openai/chat.rb +6 -4
  50. data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
  51. data/lib/ruby_llm/providers/openai/media.rb +38 -21
  52. data/lib/ruby_llm/providers/openai/models.rb +16 -17
  53. data/lib/ruby_llm/providers/openai/tools.rb +9 -5
  54. data/lib/ruby_llm/providers/openai.rb +7 -5
  55. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  56. data/lib/ruby_llm/providers/openrouter.rb +31 -0
  57. data/lib/ruby_llm/stream_accumulator.rb +4 -4
  58. data/lib/ruby_llm/streaming.rb +3 -3
  59. data/lib/ruby_llm/utils.rb +22 -0
  60. data/lib/ruby_llm/version.rb +1 -1
  61. data/lib/ruby_llm.rb +15 -5
  62. data/lib/tasks/models.rake +69 -33
  63. data/lib/tasks/models_docs.rake +164 -121
  64. data/lib/tasks/vcr.rake +4 -2
  65. metadata +23 -14
  66. data/lib/tasks/browser_helper.rb +0 -97
  67. data/lib/tasks/capability_generator.rb +0 -123
  68. data/lib/tasks/capability_scraper.rb +0 -224
  69. data/lib/tasks/cli_helper.rb +0 -22
  70. data/lib/tasks/code_validator.rb +0 -29
  71. 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 # rubocop:disable Metrics/ModuleLength
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) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
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) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
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 complete(messages, tools:, temperature:, model:, &block) # rubocop:disable Metrics/MethodLength
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) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
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
- { text: part.to_s }
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) # rubocop:disable Metrics/CyclomaticComplexity
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
- # Must be public for Provider module
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
- url = "models/#{model}:embedContent"
17
- response = post(url, payload)
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
- if text.is_a?(Array)
20
- # We need to make separate calls for each text with Gemini
21
- embeddings = text.map do |t|
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
- Embedding.new(
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 format_text_for_embedding(text)
44
- if text.is_a?(Array)
45
- text.map { |t| { text: t.to_s } }
46
- else
47
- [{ text: text.to_s }]
48
- end
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:) # rubocop:disable Metrics/MethodLength
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) # rubocop:disable Metrics/MethodLength
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 # rubocop:disable Metrics/ModuleLength
7
+ module Media
8
8
  module_function
9
9
 
10
- def format_image(part) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/PerceivedComplexity
11
- source = part[:source]
10
+ def format_content(content)
11
+ return [format_text(content)] unless content.is_a?(Content)
12
12
 
13
- if source.is_a?(String)
14
- if source.start_with?('http')
15
- # Handle URL
16
- {
17
- inline_data: {
18
- mime_type: mime_type_for_image(source),
19
- data: fetch_and_encode_image(source)
20
- }
21
- }
22
- else
23
- # Handle file path
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
- def format_pdf(part) # rubocop:disable Metrics/MethodLength
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 format_audio(part) # rubocop:disable Metrics/MethodLength
75
- source = part[:source]
76
-
77
- if source.is_a?(String) && source.start_with?('http')
78
- # Handle URL
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
- else
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 mime_type_for_image(path)
98
- ext = File.extname(path).downcase.delete('.')
99
- case ext
100
- when 'png' then 'image/png'
101
- when 'gif' then 'image/gif'
102
- when 'webp' then 'image/webp'
103
- else 'image/jpeg'
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 encode_image_file(path)
127
- Base64.strict_encode64(File.read(path))
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 encode_pdf_file(path)
131
- Base64.strict_encode64(File.read(path))
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
- # Methods needed by Provider - must be public
8
+ module_function
9
+
9
10
  def models_url
10
11
  'models'
11
12
  end
12
13
 
13
- private
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 = model['name'].gsub('models/', '')
17
+ model_id = model_data['name'].gsub('models/', '')
19
18
 
20
19
  ModelInfo.new(
21
20
  id: model_id,
22
- created_at: nil,
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: model['version'],
29
- description: model['description'],
30
- input_token_limit: model['inputTokenLimit'],
31
- output_token_limit: model['outputTokenLimit'],
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,7 +15,7 @@ module RubyLLM
15
15
  end
16
16
 
17
17
  # Extract tool calls from response data
18
- def extract_tool_calls(data) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
18
+ def extract_tool_calls(data)
19
19
  return nil unless data
20
20
 
21
21
  # Get the first candidate
@@ -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' => RubyLLM.config.gemini_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