ruby_llm_community 0.0.1 → 0.0.2

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +22 -0
  3. data/README.md +172 -0
  4. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  5. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
  8. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  9. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  10. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  12. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  13. data/lib/ruby_llm/active_record/acts_as.rb +382 -0
  14. data/lib/ruby_llm/aliases.json +217 -0
  15. data/lib/ruby_llm/aliases.rb +56 -0
  16. data/lib/ruby_llm/attachment.rb +164 -0
  17. data/lib/ruby_llm/chat.rb +219 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +75 -0
  20. data/lib/ruby_llm/connection.rb +126 -0
  21. data/lib/ruby_llm/content.rb +52 -0
  22. data/lib/ruby_llm/context.rb +29 -0
  23. data/lib/ruby_llm/embedding.rb +30 -0
  24. data/lib/ruby_llm/error.rb +84 -0
  25. data/lib/ruby_llm/image.rb +53 -0
  26. data/lib/ruby_llm/message.rb +76 -0
  27. data/lib/ruby_llm/mime_type.rb +67 -0
  28. data/lib/ruby_llm/model/info.rb +101 -0
  29. data/lib/ruby_llm/model/modalities.rb +22 -0
  30. data/lib/ruby_llm/model/pricing.rb +51 -0
  31. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  32. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  33. data/lib/ruby_llm/model.rb +7 -0
  34. data/lib/ruby_llm/models.json +29924 -0
  35. data/lib/ruby_llm/models.rb +218 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +219 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  44. data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
  45. data/lib/ruby_llm/providers/anthropic.rb +37 -0
  46. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +65 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
  49. data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
  50. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  51. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
  52. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
  53. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  54. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
  55. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  56. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  57. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  58. data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
  59. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  60. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  61. data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
  62. data/lib/ruby_llm/providers/gemini/chat.rb +139 -0
  63. data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
  64. data/lib/ruby_llm/providers/gemini/images.rb +48 -0
  65. data/lib/ruby_llm/providers/gemini/media.rb +55 -0
  66. data/lib/ruby_llm/providers/gemini/models.rb +41 -0
  67. data/lib/ruby_llm/providers/gemini/streaming.rb +58 -0
  68. data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
  69. data/lib/ruby_llm/providers/gemini.rb +36 -0
  70. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  71. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  72. data/lib/ruby_llm/providers/gpustack.rb +33 -0
  73. data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
  74. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  75. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  76. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  77. data/lib/ruby_llm/providers/mistral.rb +32 -0
  78. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  79. data/lib/ruby_llm/providers/ollama/media.rb +50 -0
  80. data/lib/ruby_llm/providers/ollama.rb +29 -0
  81. data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
  82. data/lib/ruby_llm/providers/openai/chat.rb +86 -0
  83. data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
  84. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  85. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  86. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  87. data/lib/ruby_llm/providers/openai/response.rb +115 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +190 -0
  90. data/lib/ruby_llm/providers/openai/tools.rb +100 -0
  91. data/lib/ruby_llm/providers/openai.rb +44 -0
  92. data/lib/ruby_llm/providers/openai_base.rb +44 -0
  93. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  94. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  95. data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
  96. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  97. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  98. data/lib/ruby_llm/providers/perplexity.rb +52 -0
  99. data/lib/ruby_llm/railtie.rb +17 -0
  100. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  101. data/lib/ruby_llm/streaming.rb +162 -0
  102. data/lib/ruby_llm/tool.rb +100 -0
  103. data/lib/ruby_llm/tool_call.rb +31 -0
  104. data/lib/ruby_llm/utils.rb +49 -0
  105. data/lib/ruby_llm/version.rb +5 -0
  106. data/lib/ruby_llm.rb +98 -0
  107. data/lib/tasks/aliases.rake +235 -0
  108. data/lib/tasks/models_docs.rake +224 -0
  109. data/lib/tasks/models_update.rake +108 -0
  110. data/lib/tasks/release.rake +32 -0
  111. data/lib/tasks/vcr.rake +99 -0
  112. metadata +128 -7
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Determines capabilities and pricing for Google Gemini models
7
+ module Capabilities
8
+ module_function
9
+
10
+ # Returns the context window size (input token limit) for the given model
11
+ # @param model_id [String] the model identifier
12
+ # @return [Integer] the context window size in tokens
13
+ def context_window_for(model_id)
14
+ case model_id
15
+ when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/ # rubocop:disable Layout/LineLength
16
+ 1_048_576
17
+ when /gemini-1\.5-pro/ then 2_097_152
18
+ when /gemini-embedding-exp/ then 8_192
19
+ when /text-embedding-004/, /embedding-001/ then 2_048
20
+ when /aqa/ then 7_168
21
+ when /imagen-3/ then nil # No token limit for image generation
22
+ else 32_768 # Sensible default for unknown models
23
+ end
24
+ end
25
+
26
+ # Returns the maximum output tokens for the given model
27
+ # @param model_id [String] the model identifier
28
+ # @return [Integer] the maximum output tokens
29
+ def max_tokens_for(model_id)
30
+ case model_id
31
+ when /gemini-2\.5-pro-exp-03-25/ then 64_000
32
+ when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/, /gemini-1\.5-pro/ # rubocop:disable Layout/LineLength
33
+ 8_192
34
+ when /gemini-embedding-exp/ then nil # Elastic, supports 3072, 1536, or 768
35
+ when /text-embedding-004/, /embedding-001/ then 768 # Output dimension size for embeddings
36
+ when /aqa/ then 1_024
37
+ when /imagen-3/ then 4 # Output images
38
+ else 4_096 # Sensible default
39
+ end
40
+ end
41
+
42
+ # Returns the input price per million tokens for the given model
43
+ # @param model_id [String] the model identifier
44
+ # @return [Float] the price per million tokens in USD
45
+ def input_price_for(model_id)
46
+ base_price = PRICES.dig(pricing_family(model_id), :input) || default_input_price
47
+ return base_price unless long_context_model?(model_id)
48
+
49
+ # Apply different pricing for prompts longer than 128k tokens
50
+ context_window_for(model_id) > 128_000 ? base_price * 2 : base_price
51
+ end
52
+
53
+ # Returns the output price per million tokens for the given model
54
+ # @param model_id [String] the model identifier
55
+ # @return [Float] the price per million tokens in USD
56
+ def output_price_for(model_id)
57
+ base_price = PRICES.dig(pricing_family(model_id), :output) || default_output_price
58
+ return base_price unless long_context_model?(model_id)
59
+
60
+ # Apply different pricing for prompts longer than 128k tokens
61
+ context_window_for(model_id) > 128_000 ? base_price * 2 : base_price
62
+ end
63
+
64
+ # Determines if the model supports vision (image/video) inputs
65
+ # @param model_id [String] the model identifier
66
+ # @return [Boolean] true if the model supports vision inputs
67
+ def supports_vision?(model_id)
68
+ return false if model_id.match?(/text-embedding|embedding-001|aqa/)
69
+
70
+ model_id.match?(/gemini|flash|pro|imagen/)
71
+ end
72
+
73
+ # Determines if the model supports function calling
74
+ # @param model_id [String] the model identifier
75
+ # @return [Boolean] true if the model supports function calling
76
+ def supports_functions?(model_id)
77
+ return false if model_id.match?(/text-embedding|embedding-001|aqa|flash-lite|imagen|gemini-2\.0-flash-lite/)
78
+
79
+ model_id.match?(/gemini|pro|flash/)
80
+ end
81
+
82
+ # Determines if the model supports JSON mode
83
+ # @param model_id [String] the model identifier
84
+ # @return [Boolean] true if the model supports JSON mode
85
+ def supports_json_mode?(model_id)
86
+ if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
87
+ return false
88
+ end
89
+
90
+ model_id.match?(/gemini|pro|flash/)
91
+ end
92
+
93
+ # Formats the model ID into a human-readable display name
94
+ # @param model_id [String] the model identifier
95
+ # @return [String] the formatted display name
96
+ def format_display_name(model_id)
97
+ model_id
98
+ .delete_prefix('models/')
99
+ .split('-')
100
+ .map(&:capitalize)
101
+ .join(' ')
102
+ .gsub(/(\d+\.\d+)/, ' \1') # Add space before version numbers
103
+ .gsub(/\s+/, ' ') # Clean up multiple spaces
104
+ .gsub('Aqa', 'AQA') # Special case for AQA
105
+ .strip
106
+ end
107
+
108
+ # Determines if the model supports context caching
109
+ # @param model_id [String] the model identifier
110
+ # @return [Boolean] true if the model supports caching
111
+ def supports_caching?(model_id)
112
+ if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
113
+ return false
114
+ end
115
+
116
+ model_id.match?(/gemini|pro|flash/)
117
+ end
118
+
119
+ # Determines if the model supports tuning
120
+ # @param model_id [String] the model identifier
121
+ # @return [Boolean] true if the model supports tuning
122
+ def supports_tuning?(model_id)
123
+ model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
124
+ end
125
+
126
+ # Determines if the model supports audio inputs
127
+ # @param model_id [String] the model identifier
128
+ # @return [Boolean] true if the model supports audio inputs
129
+ def supports_audio?(model_id)
130
+ model_id.match?(/gemini|pro|flash/)
131
+ end
132
+
133
+ # Returns the type of model (chat, embedding, image)
134
+ # @param model_id [String] the model identifier
135
+ # @return [String] the model type
136
+ def model_type(model_id)
137
+ case model_id
138
+ when /text-embedding|embedding|gemini-embedding/ then 'embedding'
139
+ when /imagen/ then 'image'
140
+ else 'chat'
141
+ end
142
+ end
143
+
144
+ # Returns the model family identifier
145
+ # @param model_id [String] the model identifier
146
+ # @return [String] the model family identifier
147
+ def model_family(model_id)
148
+ case model_id
149
+ when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
150
+ when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
151
+ when /gemini-2\.0-flash/ then 'gemini20_flash'
152
+ when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
153
+ when /gemini-1\.5-flash/ then 'gemini15_flash'
154
+ when /gemini-1\.5-pro/ then 'gemini15_pro'
155
+ when /gemini-embedding-exp/ then 'gemini_embedding_exp'
156
+ when /text-embedding-004/ then 'embedding4'
157
+ when /embedding-001/ then 'embedding1'
158
+ when /aqa/ then 'aqa'
159
+ when /imagen-3/ then 'imagen3'
160
+ else 'other'
161
+ end
162
+ end
163
+
164
+ # Returns the pricing family identifier for the model
165
+ # @param model_id [String] the model identifier
166
+ # @return [Symbol] the pricing family identifier
167
+ def pricing_family(model_id)
168
+ case model_id
169
+ when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
170
+ when /gemini-2\.0-flash-lite/ then :flash_lite_2 # rubocop:disable Naming/VariableNumber
171
+ when /gemini-2\.0-flash/ then :flash_2 # rubocop:disable Naming/VariableNumber
172
+ when /gemini-1\.5-flash-8b/ then :flash_8b
173
+ when /gemini-1\.5-flash/ then :flash
174
+ when /gemini-1\.5-pro/ then :pro
175
+ when /gemini-embedding-exp/ then :gemini_embedding
176
+ when /text-embedding|embedding/ then :embedding
177
+ when /imagen/ then :imagen
178
+ when /aqa/ then :aqa
179
+ else :base
180
+ end
181
+ end
182
+
183
+ # Determines if the model supports long context
184
+ # @param model_id [String] the model identifier
185
+ # @return [Boolean] true if the model supports long context
186
+ def long_context_model?(model_id)
187
+ model_id.match?(/gemini-1\.5-(?:pro|flash)|gemini-1\.5-flash-8b/)
188
+ end
189
+
190
+ # Returns the context length for the model
191
+ # @param model_id [String] the model identifier
192
+ # @return [Integer] the context length in tokens
193
+ def context_length(model_id)
194
+ context_window_for(model_id)
195
+ end
196
+
197
+ # Pricing information for Gemini models (per 1M tokens in USD)
198
+ PRICES = {
199
+ flash_2: { # Gemini 2.0 Flash # rubocop:disable Naming/VariableNumber
200
+ input: 0.10,
201
+ output: 0.40,
202
+ audio_input: 0.70,
203
+ cache: 0.025,
204
+ cache_storage: 1.00,
205
+ grounding_search: 35.00 # per 1K requests after 1.5K free
206
+ },
207
+ flash_lite_2: { # Gemini 2.0 Flash Lite # rubocop:disable Naming/VariableNumber
208
+ input: 0.075,
209
+ output: 0.30
210
+ },
211
+ flash: { # Gemini 1.5 Flash
212
+ input: 0.075,
213
+ output: 0.30,
214
+ cache: 0.01875,
215
+ cache_storage: 1.00,
216
+ grounding_search: 35.00 # per 1K requests up to 5K per day
217
+ },
218
+ flash_8b: { # Gemini 1.5 Flash 8B
219
+ input: 0.0375,
220
+ output: 0.15,
221
+ cache: 0.01,
222
+ cache_storage: 0.25,
223
+ grounding_search: 35.00 # per 1K requests up to 5K per day
224
+ },
225
+ pro: { # Gemini 1.5 Pro
226
+ input: 1.25,
227
+ output: 5.0,
228
+ cache: 0.3125,
229
+ cache_storage: 4.50,
230
+ grounding_search: 35.00 # per 1K requests up to 5K per day
231
+ },
232
+ pro_2_5: { # Gemini 2.5 Pro Experimental # rubocop:disable Naming/VariableNumber
233
+ input: 0.12,
234
+ output: 0.50
235
+ },
236
+ gemini_embedding: { # Gemini Embedding Experimental
237
+ input: 0.002,
238
+ output: 0.004
239
+ },
240
+ embedding: { # Text Embedding models
241
+ input: 0.00,
242
+ output: 0.00
243
+ },
244
+ imagen: { # Imagen 3
245
+ price: 0.03 # per image
246
+ },
247
+ aqa: { # AQA model
248
+ input: 0.00,
249
+ output: 0.00
250
+ }
251
+ }.freeze
252
+
253
+ # Default input price for unknown models
254
+ # @return [Float] the default input price per million tokens
255
+ def default_input_price
256
+ 0.075 # Default to Flash pricing
257
+ end
258
+
259
+ # Default output price for unknown models
260
+ # @return [Float] the default output price per million tokens
261
+ def default_output_price
262
+ 0.30 # Default to Flash pricing
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
+ # Image output for imagen models
284
+ modalities[:output] = ['image'] if model_id.match?(/imagen/)
285
+
286
+ modalities
287
+ end
288
+
289
+ def capabilities_for(model_id)
290
+ capabilities = ['streaming']
291
+
292
+ # Function calling
293
+ capabilities << 'function_calling' if supports_functions?(model_id)
294
+
295
+ # JSON mode
296
+ capabilities << 'structured_output' if supports_json_mode?(model_id)
297
+
298
+ # Batch processing
299
+ capabilities << 'batch' if model_id.match?(/embedding|flash/)
300
+
301
+ # Caching
302
+ capabilities << 'caching' if supports_caching?(model_id)
303
+
304
+ # Tuning
305
+ capabilities << 'fine_tuning' if supports_tuning?(model_id)
306
+
307
+ capabilities
308
+ end
309
+
310
+ def pricing_for(model_id)
311
+ family = pricing_family(model_id)
312
+ prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
313
+
314
+ standard_pricing = {
315
+ input_per_million: prices[:input],
316
+ output_per_million: prices[:output]
317
+ }
318
+
319
+ # Add cached pricing if available
320
+ standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
321
+
322
+ # Batch pricing (typically 50% discount)
323
+ batch_pricing = {
324
+ input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
325
+ output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
326
+ }
327
+
328
+ if standard_pricing[:cached_input_per_million]
329
+ batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
330
+ end
331
+
332
+ pricing = {
333
+ text_tokens: {
334
+ standard: standard_pricing,
335
+ batch: batch_pricing
336
+ }
337
+ }
338
+
339
+ # Add embedding pricing if applicable
340
+ if model_id.match?(/embedding|gemini-embedding/)
341
+ pricing[:embeddings] = {
342
+ standard: { input_per_million: prices[:price] || 0.002 }
343
+ }
344
+ end
345
+
346
+ pricing
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Chat methods for the Gemini API implementation
7
+ module Chat
8
+ module_function
9
+
10
+ def completion_url
11
+ "models/#{@model}:generateContent"
12
+ end
13
+
14
+ def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
15
+ @model = model # Store model for completion_url/stream_url
16
+ payload = {
17
+ contents: format_messages(messages),
18
+ generationConfig: {
19
+ temperature: temperature
20
+ }
21
+ }
22
+
23
+ if schema
24
+ payload[:generationConfig][:responseMimeType] = 'application/json'
25
+ payload[:generationConfig][:responseSchema] = convert_schema_to_gemini(schema)
26
+ end
27
+
28
+ payload[:tools] = format_tools(tools) if tools.any?
29
+ payload
30
+ end
31
+
32
+ private
33
+
34
+ def format_messages(messages)
35
+ messages.map do |msg|
36
+ {
37
+ role: format_role(msg.role),
38
+ parts: format_parts(msg)
39
+ }
40
+ end
41
+ end
42
+
43
+ def format_role(role)
44
+ case role
45
+ when :assistant then 'model'
46
+ when :system, :tool then 'user' # Gemini doesn't have system, use user role, function responses use user role
47
+ else role.to_s
48
+ end
49
+ end
50
+
51
+ def format_parts(msg)
52
+ if msg.tool_call?
53
+ [{
54
+ functionCall: {
55
+ name: msg.tool_calls.values.first.name,
56
+ args: msg.tool_calls.values.first.arguments
57
+ }
58
+ }]
59
+ elsif msg.tool_result?
60
+ [{
61
+ functionResponse: {
62
+ name: msg.tool_call_id,
63
+ response: {
64
+ name: msg.tool_call_id,
65
+ content: msg.content
66
+ }
67
+ }
68
+ }]
69
+ else
70
+ Media.format_content(msg.content)
71
+ end
72
+ end
73
+
74
+ def parse_completion_response(response)
75
+ data = response.body
76
+ tool_calls = extract_tool_calls(data)
77
+
78
+ Message.new(
79
+ role: :assistant,
80
+ content: extract_content(data),
81
+ tool_calls: tool_calls,
82
+ input_tokens: data.dig('usageMetadata', 'promptTokenCount'),
83
+ output_tokens: data.dig('usageMetadata', 'candidatesTokenCount'),
84
+ model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0],
85
+ raw: response
86
+ )
87
+ end
88
+
89
+ def convert_schema_to_gemini(schema) # rubocop:disable Metrics/PerceivedComplexity
90
+ return nil unless schema
91
+
92
+ case schema[:type]
93
+ when 'object'
94
+ {
95
+ type: 'OBJECT',
96
+ properties: schema[:properties]&.transform_values { |prop| convert_schema_to_gemini(prop) } || {},
97
+ required: schema[:required] || []
98
+ }
99
+ when 'array'
100
+ {
101
+ type: 'ARRAY',
102
+ items: schema[:items] ? convert_schema_to_gemini(schema[:items]) : { type: 'STRING' }
103
+ }
104
+ when 'string'
105
+ result = { type: 'STRING' }
106
+ result[:enum] = schema[:enum] if schema[:enum]
107
+ result
108
+ when 'number', 'integer'
109
+ { type: 'NUMBER' }
110
+ when 'boolean'
111
+ { type: 'BOOLEAN' }
112
+ else
113
+ { type: 'STRING' }
114
+ end
115
+ end
116
+
117
+ def extract_content(data)
118
+ candidate = data.dig('candidates', 0)
119
+ return '' unless candidate
120
+
121
+ # Content will be empty for function calls
122
+ return '' if function_call?(candidate)
123
+
124
+ # Extract text content
125
+ parts = candidate.dig('content', 'parts')
126
+ text_parts = parts&.select { |p| p['text'] }
127
+ return '' unless text_parts&.any?
128
+
129
+ text_parts.map { |p| p['text'] }.join
130
+ end
131
+
132
+ def function_call?(candidate)
133
+ parts = candidate.dig('content', 'parts')
134
+ parts&.any? { |p| p['functionCall'] }
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Embeddings methods for the Gemini API integration
7
+ module Embeddings
8
+ module_function
9
+
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
17
+
18
+ def parse_embedding_response(response, model:, text:)
19
+ vectors = response.body['embeddings']&.map { |e| e['values'] }
20
+ # If we only got one embedding AND the input was a single string (not an array),
21
+ # return it as a single vector
22
+ vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
23
+
24
+ Embedding.new(vectors:, model:, input_tokens: 0)
25
+ end
26
+
27
+ private
28
+
29
+ def single_embedding_payload(text, model:, dimensions:)
30
+ {
31
+ model: "models/#{model}",
32
+ content: { parts: [{ text: text.to_s }] },
33
+ outputDimensionality: dimensions
34
+ }.compact
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Image generation methods for the Gemini API implementation
7
+ module Images
8
+ def images_url
9
+ "models/#{@model}:predict"
10
+ end
11
+
12
+ def render_image_payload(prompt, model:, size:)
13
+ RubyLLM.logger.debug "Ignoring size #{size}. Gemini does not support image size customization."
14
+ @model = model
15
+ {
16
+ instances: [
17
+ {
18
+ prompt: prompt
19
+ }
20
+ ],
21
+ parameters: {
22
+ sampleCount: 1
23
+ }
24
+ }
25
+ end
26
+
27
+ def parse_image_response(response, model:)
28
+ data = response.body
29
+ image_data = data['predictions']&.first
30
+
31
+ unless image_data&.key?('bytesBase64Encoded')
32
+ raise Error, 'Unexpected response format from Gemini image generation API'
33
+ end
34
+
35
+ # Extract mime type and base64 data
36
+ mime_type = image_data['mimeType'] || 'image/png'
37
+ base64_data = image_data['bytesBase64Encoded']
38
+
39
+ Image.new(
40
+ data: base64_data,
41
+ mime_type: mime_type,
42
+ model_id: model
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Media handling methods for the Gemini API integration
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content)
11
+ # Convert Hash/Array back to JSON string for API
12
+ return [format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
13
+ return [format_text(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.type
20
+ when :text
21
+ parts << format_text_file(attachment)
22
+ when :unknown
23
+ raise UnsupportedAttachmentError, attachment.mime_type
24
+ else
25
+ parts << format_attachment(attachment)
26
+ end
27
+ end
28
+
29
+ parts
30
+ end
31
+
32
+ def format_attachment(attachment)
33
+ {
34
+ inline_data: {
35
+ mime_type: attachment.mime_type,
36
+ data: attachment.encoded
37
+ }
38
+ }
39
+ end
40
+
41
+ def format_text_file(text_file)
42
+ {
43
+ text: Utils.format_text_file_for_llm(text_file)
44
+ }
45
+ end
46
+
47
+ def format_text(text)
48
+ {
49
+ text: text
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Gemini
6
+ # Models methods for the Gemini API integration
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def parse_list_models_response(response, slug, capabilities)
15
+ Array(response.body['models']).map do |model_data|
16
+ # Extract model ID without "models/" prefix
17
+ model_id = model_data['name'].gsub('models/', '')
18
+
19
+ Model::Info.new(
20
+ id: model_id,
21
+ name: model_data['displayName'],
22
+ provider: slug,
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),
30
+ metadata: {
31
+ version: model_data['version'],
32
+ description: model_data['description'],
33
+ supported_generation_methods: model_data['supportedGenerationMethods']
34
+ }
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end