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.
- checksums.yaml +4 -4
- data/LICENSE +22 -0
- data/README.md +172 -0
- data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install_generator.rb +121 -0
- data/lib/ruby_llm/active_record/acts_as.rb +382 -0
- data/lib/ruby_llm/aliases.json +217 -0
- data/lib/ruby_llm/aliases.rb +56 -0
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +219 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +75 -0
- data/lib/ruby_llm/connection.rb +126 -0
- data/lib/ruby_llm/content.rb +52 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +30 -0
- data/lib/ruby_llm/error.rb +84 -0
- data/lib/ruby_llm/image.rb +53 -0
- data/lib/ruby_llm/message.rb +76 -0
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +29924 -0
- data/lib/ruby_llm/models.rb +218 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +219 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
- data/lib/ruby_llm/providers/anthropic.rb +37 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +65 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
- data/lib/ruby_llm/providers/bedrock.rb +83 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +139 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/gemini/images.rb +48 -0
- data/lib/ruby_llm/providers/gemini/media.rb +55 -0
- data/lib/ruby_llm/providers/gemini/models.rb +41 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +58 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
- data/lib/ruby_llm/providers/gpustack.rb +33 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
- data/lib/ruby_llm/providers/mistral/models.rb +49 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +50 -0
- data/lib/ruby_llm/providers/ollama.rb +29 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
- data/lib/ruby_llm/providers/openai/chat.rb +86 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +81 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/response.rb +115 -0
- data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +190 -0
- data/lib/ruby_llm/providers/openai/tools.rb +100 -0
- data/lib/ruby_llm/providers/openai.rb +44 -0
- data/lib/ruby_llm/providers/openai_base.rb +44 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +52 -0
- data/lib/ruby_llm/railtie.rb +17 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +162 -0
- data/lib/ruby_llm/tool.rb +100 -0
- data/lib/ruby_llm/tool_call.rb +31 -0
- data/lib/ruby_llm/utils.rb +49 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +98 -0
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +224 -0
- data/lib/tasks/models_update.rake +108 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +99 -0
- 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
|