dify_llm 1.6.4

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 (129) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +157 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -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_legacy_migration.rb.tt +16 -0
  8. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  9. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  10. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
  12. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  13. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  14. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  15. data/lib/generators/ruby_llm/install_generator.rb +184 -0
  16. data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
  17. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
  18. data/lib/ruby_llm/active_record/acts_as.rb +137 -0
  19. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  20. data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
  21. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  22. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  23. data/lib/ruby_llm/aliases.json +274 -0
  24. data/lib/ruby_llm/aliases.rb +38 -0
  25. data/lib/ruby_llm/attachment.rb +191 -0
  26. data/lib/ruby_llm/chat.rb +212 -0
  27. data/lib/ruby_llm/chunk.rb +6 -0
  28. data/lib/ruby_llm/configuration.rb +69 -0
  29. data/lib/ruby_llm/connection.rb +137 -0
  30. data/lib/ruby_llm/content.rb +50 -0
  31. data/lib/ruby_llm/context.rb +29 -0
  32. data/lib/ruby_llm/embedding.rb +29 -0
  33. data/lib/ruby_llm/error.rb +76 -0
  34. data/lib/ruby_llm/image.rb +49 -0
  35. data/lib/ruby_llm/message.rb +76 -0
  36. data/lib/ruby_llm/mime_type.rb +67 -0
  37. data/lib/ruby_llm/model/info.rb +103 -0
  38. data/lib/ruby_llm/model/modalities.rb +22 -0
  39. data/lib/ruby_llm/model/pricing.rb +48 -0
  40. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  41. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  42. data/lib/ruby_llm/model.rb +7 -0
  43. data/lib/ruby_llm/models.json +31418 -0
  44. data/lib/ruby_llm/models.rb +235 -0
  45. data/lib/ruby_llm/models_schema.json +168 -0
  46. data/lib/ruby_llm/provider.rb +215 -0
  47. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  48. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  49. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  50. data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
  51. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  52. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  53. data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
  54. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  55. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  56. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  57. data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
  58. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  59. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  60. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  61. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
  62. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  63. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
  64. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  65. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  66. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  67. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  68. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  69. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  70. data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
  71. data/lib/ruby_llm/providers/dify/chat.rb +59 -0
  72. data/lib/ruby_llm/providers/dify/media.rb +37 -0
  73. data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
  74. data/lib/ruby_llm/providers/dify.rb +48 -0
  75. data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
  76. data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
  77. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  78. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  79. data/lib/ruby_llm/providers/gemini/media.rb +54 -0
  80. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  81. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  82. data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
  83. data/lib/ruby_llm/providers/gemini.rb +36 -0
  84. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  85. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  86. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  87. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  88. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  89. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  90. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  91. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  92. data/lib/ruby_llm/providers/mistral.rb +32 -0
  93. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  94. data/lib/ruby_llm/providers/ollama/media.rb +45 -0
  95. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  96. data/lib/ruby_llm/providers/ollama.rb +30 -0
  97. data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
  98. data/lib/ruby_llm/providers/openai/chat.rb +83 -0
  99. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  100. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  101. data/lib/ruby_llm/providers/openai/media.rb +80 -0
  102. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  103. data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
  104. data/lib/ruby_llm/providers/openai/tools.rb +78 -0
  105. data/lib/ruby_llm/providers/openai.rb +42 -0
  106. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  107. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  108. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  109. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  110. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  111. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  112. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  113. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  114. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  115. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  116. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  117. data/lib/ruby_llm/railtie.rb +41 -0
  118. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  119. data/lib/ruby_llm/streaming.rb +153 -0
  120. data/lib/ruby_llm/tool.rb +83 -0
  121. data/lib/ruby_llm/tool_call.rb +22 -0
  122. data/lib/ruby_llm/utils.rb +45 -0
  123. data/lib/ruby_llm/version.rb +5 -0
  124. data/lib/ruby_llm.rb +97 -0
  125. data/lib/tasks/models.rake +525 -0
  126. data/lib/tasks/release.rake +67 -0
  127. data/lib/tasks/ruby_llm.rake +15 -0
  128. data/lib/tasks/vcr.rake +92 -0
  129. metadata +291 -0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Dify
6
+ # Media handling methods for the Gemini API integration
7
+ module Media
8
+ module_function
9
+
10
+ def format_files(content)
11
+ return nil unless content.is_a?(Content)
12
+
13
+ parts = []
14
+
15
+ content.attachments.each do |attachment|
16
+ case attachment.type
17
+ when :file_id
18
+ parts << format_document_type(attachment)
19
+ else
20
+ raise UnsupportedAttachmentError, attachment.class
21
+ end
22
+ end
23
+
24
+ parts
25
+ end
26
+
27
+ def format_document_type(attachment)
28
+ {
29
+ type: 'document',
30
+ transfer_method: 'local_file',
31
+ upload_file_id: attachment.upload_file_id
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Dify
6
+ # Streaming methods of the OpenAI API integration
7
+ module Streaming
8
+ module_function
9
+
10
+ def stream_url
11
+ completion_url
12
+ end
13
+
14
+ def build_chunk(data)
15
+ Chunk.new(
16
+ role: :assistant,
17
+ conversation_id: data['conversation_id'],
18
+ model_id: nil,
19
+ content: data['answer'],
20
+ tool_calls: nil,
21
+ input_tokens: data.dig('metadata', 'usage', 'prompt_tokens'),
22
+ output_tokens: data.dig('metadata', 'usage', 'completion_tokens')
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Dify API integration.
6
+ class Dify < Provider
7
+ include Dify::Chat
8
+ include Dify::Media
9
+ include Dify::Streaming
10
+
11
+ def api_base
12
+ @config.dify_api_base
13
+ end
14
+
15
+ def parse_error(response)
16
+ return if response.body.empty?
17
+
18
+ body = try_parse_json(response.body)
19
+ case body
20
+ when Hash
21
+ body['message']
22
+ else
23
+ body
24
+ end
25
+ end
26
+
27
+ def headers
28
+ {
29
+ 'Authorization' => "Bearer #{@config.dify_api_key}"
30
+ }
31
+ end
32
+
33
+ class << self
34
+ def capabilities
35
+ Dify::Capabilities
36
+ end
37
+
38
+ def local?
39
+ true
40
+ end
41
+
42
+ def configuration_requirements
43
+ %i[dify_api_base dify_api_key]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,276 @@
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
+ def context_window_for(model_id)
11
+ case model_id
12
+ 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
13
+ 1_048_576
14
+ when /gemini-1\.5-pro/ then 2_097_152
15
+ when /gemini-embedding-exp/ then 8_192
16
+ when /text-embedding-004/, /embedding-001/ then 2_048
17
+ when /aqa/ then 7_168
18
+ when /imagen-3/ then nil
19
+ else 32_768
20
+ end
21
+ end
22
+
23
+ def max_tokens_for(model_id)
24
+ case model_id
25
+ when /gemini-2\.5-pro-exp-03-25/ then 64_000
26
+ 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
27
+ 8_192
28
+ when /gemini-embedding-exp/ then nil
29
+ when /text-embedding-004/, /embedding-001/ then 768
30
+ when /imagen-3/ then 4
31
+ else 4_096
32
+ end
33
+ end
34
+
35
+ def input_price_for(model_id)
36
+ base_price = PRICES.dig(pricing_family(model_id), :input) || default_input_price
37
+ return base_price unless long_context_model?(model_id)
38
+
39
+ context_window_for(model_id) > 128_000 ? base_price * 2 : base_price
40
+ end
41
+
42
+ def output_price_for(model_id)
43
+ base_price = PRICES.dig(pricing_family(model_id), :output) || default_output_price
44
+ return base_price unless long_context_model?(model_id)
45
+
46
+ context_window_for(model_id) > 128_000 ? base_price * 2 : base_price
47
+ end
48
+
49
+ def supports_vision?(model_id)
50
+ return false if model_id.match?(/text-embedding|embedding-001|aqa/)
51
+
52
+ model_id.match?(/gemini|flash|pro|imagen/)
53
+ end
54
+
55
+ def supports_functions?(model_id)
56
+ return false if model_id.match?(/text-embedding|embedding-001|aqa|flash-lite|imagen|gemini-2\.0-flash-lite/)
57
+
58
+ model_id.match?(/gemini|pro|flash/)
59
+ end
60
+
61
+ def supports_json_mode?(model_id)
62
+ if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
63
+ return false
64
+ end
65
+
66
+ model_id.match?(/gemini|pro|flash/)
67
+ end
68
+
69
+ def format_display_name(model_id)
70
+ model_id
71
+ .delete_prefix('models/')
72
+ .split('-')
73
+ .map(&:capitalize)
74
+ .join(' ')
75
+ .gsub(/(\d+\.\d+)/, ' \1')
76
+ .gsub(/\s+/, ' ')
77
+ .gsub('Aqa', 'AQA')
78
+ .strip
79
+ end
80
+
81
+ def supports_caching?(model_id)
82
+ if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
83
+ return false
84
+ end
85
+
86
+ model_id.match?(/gemini|pro|flash/)
87
+ end
88
+
89
+ def supports_tuning?(model_id)
90
+ model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
91
+ end
92
+
93
+ def supports_audio?(model_id)
94
+ model_id.match?(/gemini|pro|flash/)
95
+ end
96
+
97
+ def model_type(model_id)
98
+ case model_id
99
+ when /text-embedding|embedding|gemini-embedding/ then 'embedding'
100
+ when /imagen/ then 'image'
101
+ else 'chat'
102
+ end
103
+ end
104
+
105
+ def model_family(model_id)
106
+ case model_id
107
+ when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
108
+ when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
109
+ when /gemini-2\.0-flash/ then 'gemini20_flash'
110
+ when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
111
+ when /gemini-1\.5-flash/ then 'gemini15_flash'
112
+ when /gemini-1\.5-pro/ then 'gemini15_pro'
113
+ when /gemini-embedding-exp/ then 'gemini_embedding_exp'
114
+ when /text-embedding-004/ then 'embedding4'
115
+ when /embedding-001/ then 'embedding1'
116
+ when /aqa/ then 'aqa'
117
+ when /imagen-3/ then 'imagen3'
118
+ else 'other'
119
+ end
120
+ end
121
+
122
+ def pricing_family(model_id)
123
+ case model_id
124
+ when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
125
+ when /gemini-2\.0-flash-lite/ then :flash_lite_2 # rubocop:disable Naming/VariableNumber
126
+ when /gemini-2\.0-flash/ then :flash_2 # rubocop:disable Naming/VariableNumber
127
+ when /gemini-1\.5-flash-8b/ then :flash_8b
128
+ when /gemini-1\.5-flash/ then :flash
129
+ when /gemini-1\.5-pro/ then :pro
130
+ when /gemini-embedding-exp/ then :gemini_embedding
131
+ when /text-embedding|embedding/ then :embedding
132
+ when /imagen/ then :imagen
133
+ when /aqa/ then :aqa
134
+ else :base
135
+ end
136
+ end
137
+
138
+ def long_context_model?(model_id)
139
+ model_id.match?(/gemini-1\.5-(?:pro|flash)|gemini-1\.5-flash-8b/)
140
+ end
141
+
142
+ def context_length(model_id)
143
+ context_window_for(model_id)
144
+ end
145
+
146
+ PRICES = {
147
+ flash_2: { # rubocop:disable Naming/VariableNumber
148
+ input: 0.10,
149
+ output: 0.40,
150
+ audio_input: 0.70,
151
+ cache: 0.025,
152
+ cache_storage: 1.00,
153
+ grounding_search: 35.00
154
+ },
155
+ flash_lite_2: { # rubocop:disable Naming/VariableNumber
156
+ input: 0.075,
157
+ output: 0.30
158
+ },
159
+ flash: {
160
+ input: 0.075,
161
+ output: 0.30,
162
+ cache: 0.01875,
163
+ cache_storage: 1.00,
164
+ grounding_search: 35.00
165
+ },
166
+ flash_8b: {
167
+ input: 0.0375,
168
+ output: 0.15,
169
+ cache: 0.01,
170
+ cache_storage: 0.25,
171
+ grounding_search: 35.00
172
+ },
173
+ pro: {
174
+ input: 1.25,
175
+ output: 5.0,
176
+ cache: 0.3125,
177
+ cache_storage: 4.50,
178
+ grounding_search: 35.00
179
+ },
180
+ pro_2_5: { # rubocop:disable Naming/VariableNumber
181
+ input: 0.12,
182
+ output: 0.50
183
+ },
184
+ gemini_embedding: {
185
+ input: 0.002,
186
+ output: 0.004
187
+ },
188
+ embedding: {
189
+ input: 0.00,
190
+ output: 0.00
191
+ },
192
+ imagen: {
193
+ price: 0.03
194
+ },
195
+ aqa: {
196
+ input: 0.00,
197
+ output: 0.00
198
+ }
199
+ }.freeze
200
+
201
+ def default_input_price
202
+ 0.075
203
+ end
204
+
205
+ def default_output_price
206
+ 0.30
207
+ end
208
+
209
+ def modalities_for(model_id)
210
+ modalities = {
211
+ input: ['text'],
212
+ output: ['text']
213
+ }
214
+
215
+ if supports_vision?(model_id)
216
+ modalities[:input] << 'image'
217
+ modalities[:input] << 'pdf'
218
+ end
219
+
220
+ modalities[:input] << 'audio' if model_id.match?(/audio/)
221
+ modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
222
+ modalities[:output] = ['image'] if model_id.match?(/imagen/)
223
+
224
+ modalities
225
+ end
226
+
227
+ def capabilities_for(model_id)
228
+ capabilities = ['streaming']
229
+
230
+ capabilities << 'function_calling' if supports_functions?(model_id)
231
+ capabilities << 'structured_output' if supports_json_mode?(model_id)
232
+ capabilities << 'batch' if model_id.match?(/embedding|flash/)
233
+ capabilities << 'caching' if supports_caching?(model_id)
234
+ capabilities << 'fine_tuning' if supports_tuning?(model_id)
235
+ capabilities
236
+ end
237
+
238
+ def pricing_for(model_id)
239
+ family = pricing_family(model_id)
240
+ prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
241
+
242
+ standard_pricing = {
243
+ input_per_million: prices[:input],
244
+ output_per_million: prices[:output]
245
+ }
246
+
247
+ standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
248
+
249
+ batch_pricing = {
250
+ input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
251
+ output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
252
+ }
253
+
254
+ if standard_pricing[:cached_input_per_million]
255
+ batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
256
+ end
257
+
258
+ pricing = {
259
+ text_tokens: {
260
+ standard: standard_pricing,
261
+ batch: batch_pricing
262
+ }
263
+ }
264
+
265
+ if model_id.match?(/embedding|gemini-embedding/)
266
+ pricing[:embeddings] = {
267
+ standard: { input_per_million: prices[:price] || 0.002 }
268
+ }
269
+ end
270
+
271
+ pricing
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,171 @@
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.id
16
+ payload = {
17
+ contents: format_messages(messages),
18
+ generationConfig: {}
19
+ }
20
+
21
+ payload[:generationConfig][:temperature] = temperature unless temperature.nil?
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'
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: Media.format_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: calculate_output_tokens(data),
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)
90
+ return nil unless schema
91
+
92
+ build_base_schema(schema).tap do |result|
93
+ result[:description] = schema[:description] if schema[:description]
94
+ apply_type_specific_attributes(result, schema)
95
+ end
96
+ end
97
+
98
+ def extract_content(data)
99
+ candidate = data.dig('candidates', 0)
100
+ return '' unless candidate
101
+
102
+ return '' if function_call?(candidate)
103
+
104
+ parts = candidate.dig('content', 'parts')
105
+ text_parts = parts&.select { |p| p['text'] }
106
+ return '' unless text_parts&.any?
107
+
108
+ text_parts.map { |p| p['text'] }.join
109
+ end
110
+
111
+ def function_call?(candidate)
112
+ parts = candidate.dig('content', 'parts')
113
+ parts&.any? { |p| p['functionCall'] }
114
+ end
115
+
116
+ def calculate_output_tokens(data)
117
+ candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0
118
+ thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
119
+ candidates + thoughts
120
+ end
121
+
122
+ def build_base_schema(schema)
123
+ case schema[:type]
124
+ when 'object'
125
+ build_object_schema(schema)
126
+ when 'array'
127
+ { type: 'ARRAY', items: schema[:items] ? convert_schema_to_gemini(schema[:items]) : { type: 'STRING' } }
128
+ when 'number'
129
+ { type: 'NUMBER' }
130
+ when 'integer'
131
+ { type: 'INTEGER' }
132
+ when 'boolean'
133
+ { type: 'BOOLEAN' }
134
+ else
135
+ { type: 'STRING' }
136
+ end
137
+ end
138
+
139
+ def build_object_schema(schema)
140
+ {
141
+ type: 'OBJECT',
142
+ properties: (schema[:properties] || {}).transform_values { |prop| convert_schema_to_gemini(prop) },
143
+ required: schema[:required] || []
144
+ }.tap do |object|
145
+ object[:propertyOrdering] = schema[:propertyOrdering] if schema[:propertyOrdering]
146
+ object[:nullable] = schema[:nullable] if schema.key?(:nullable)
147
+ end
148
+ end
149
+
150
+ def apply_type_specific_attributes(result, schema)
151
+ case schema[:type]
152
+ when 'string'
153
+ copy_attributes(result, schema, :enum, :format, :nullable)
154
+ when 'number', 'integer'
155
+ copy_attributes(result, schema, :format, :minimum, :maximum, :enum, :nullable)
156
+ when 'array'
157
+ copy_attributes(result, schema, :minItems, :maxItems, :nullable)
158
+ when 'boolean'
159
+ copy_attributes(result, schema, :nullable)
160
+ end
161
+ end
162
+
163
+ def copy_attributes(target, source, *attributes)
164
+ attributes.each do |attr|
165
+ target[attr] = source[attr] if attr == :nullable ? source.key?(attr) : source[attr]
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,37 @@
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
+ vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
21
+
22
+ Embedding.new(vectors:, model:, input_tokens: 0)
23
+ end
24
+
25
+ private
26
+
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
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
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
+ mime_type = image_data['mimeType'] || 'image/png'
36
+ base64_data = image_data['bytesBase64Encoded']
37
+
38
+ Image.new(
39
+ data: base64_data,
40
+ mime_type: mime_type,
41
+ model_id: model
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
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
+ return [format_text(content.to_json)] if content.is_a?(Hash) || content.is_a?(Array)
12
+ return [format_text(content)] unless content.is_a?(Content)
13
+
14
+ parts = []
15
+ parts << format_text(content.text) if content.text
16
+
17
+ content.attachments.each do |attachment|
18
+ case attachment.type
19
+ when :text
20
+ parts << format_text_file(attachment)
21
+ when :unknown
22
+ raise UnsupportedAttachmentError, attachment.mime_type
23
+ else
24
+ parts << format_attachment(attachment)
25
+ end
26
+ end
27
+
28
+ parts
29
+ end
30
+
31
+ def format_attachment(attachment)
32
+ {
33
+ inline_data: {
34
+ mime_type: attachment.mime_type,
35
+ data: attachment.encoded
36
+ }
37
+ }
38
+ end
39
+
40
+ def format_text_file(text_file)
41
+ {
42
+ text: text_file.for_llm
43
+ }
44
+ end
45
+
46
+ def format_text(text)
47
+ {
48
+ text: text
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end