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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +157 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/model_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 +184 -0
- data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
- data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
- data/lib/ruby_llm/active_record/acts_as.rb +137 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +274 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +191 -0
- data/lib/ruby_llm/chat.rb +212 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +69 -0
- data/lib/ruby_llm/connection.rb +137 -0
- data/lib/ruby_llm/content.rb +50 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +76 -0
- data/lib/ruby_llm/image.rb +49 -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 +103 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +31418 -0
- data/lib/ruby_llm/models.rb +235 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +215 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -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 +91 -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 +107 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/dify/chat.rb +59 -0
- data/lib/ruby_llm/providers/dify/media.rb +37 -0
- data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
- data/lib/ruby_llm/providers/dify.rb +48 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +54 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +45 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
- data/lib/ruby_llm/providers/openai/chat.rb +83 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +80 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
- data/lib/ruby_llm/providers/openai/tools.rb +78 -0
- data/lib/ruby_llm/providers/openai.rb +42 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +41 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +83 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/utils.rb +45 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +97 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- 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
|