ruby_llm 0.1.0.pre30 → 0.1.0.pre31
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/.github/workflows/{gem-push.yml → cicd.yml} +32 -4
- data/.rspec_status +27 -0
- data/lib/ruby_llm/active_record/acts_as.rb +5 -5
- data/lib/ruby_llm/chat.rb +2 -2
- data/lib/ruby_llm/configuration.rb +3 -1
- data/lib/ruby_llm/content.rb +79 -0
- data/lib/ruby_llm/embedding.rb +9 -3
- data/lib/ruby_llm/message.rb +9 -1
- data/lib/ruby_llm/models.json +14 -14
- data/lib/ruby_llm/provider.rb +39 -14
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
- data/lib/ruby_llm/providers/anthropic.rb +8 -234
- data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -2
- data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/gemini/models.rb +20 -0
- data/lib/ruby_llm/providers/gemini.rb +5 -10
- data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/openai/chat.rb +68 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/openai/models.rb +40 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
- data/lib/ruby_llm/providers/openai/tools.rb +69 -0
- data/lib/ruby_llm/providers/openai.rb +15 -197
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +4 -2
- data/ruby_llm.gemspec +2 -0
- metadata +48 -8
- data/.github/workflows/test.yml +0 -35
- data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -79
- data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
- data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
- data/lib/ruby_llm/model_capabilities/openai.rb +0 -189
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Gemini
|
6
|
+
# Determines capabilities and pricing for Google Gemini models
|
7
|
+
module Capabilities # rubocop:disable Metrics/ModuleLength
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def context_window_for(model_id)
|
11
|
+
case model_id
|
12
|
+
when /gemini-2\.0-flash/, /gemini-1\.5-flash/ then 1_048_576
|
13
|
+
when /gemini-1\.5-pro/ then 2_097_152
|
14
|
+
when /text-embedding/, /embedding-001/ then 2_048
|
15
|
+
when /aqa/ then 7_168
|
16
|
+
else 32_768 # Sensible default for unknown models
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def max_tokens_for(model_id)
|
21
|
+
case model_id
|
22
|
+
when /gemini-2\.0-flash/, /gemini-1\.5/ then 8_192
|
23
|
+
when /text-embedding/, /embedding-001/ then 768 # Output dimension size for embeddings
|
24
|
+
when /aqa/ then 1_024
|
25
|
+
else 4_096 # Sensible default
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def input_price_for(model_id)
|
30
|
+
base_price = PRICES.dig(pricing_family(model_id), :input) || default_input_price
|
31
|
+
return base_price unless long_context_model?(model_id)
|
32
|
+
|
33
|
+
# Double the price for prompts longer than 128k tokens
|
34
|
+
context_length(model_id) > 128_000 ? base_price * 2 : base_price
|
35
|
+
end
|
36
|
+
|
37
|
+
def output_price_for(model_id)
|
38
|
+
base_price = PRICES.dig(pricing_family(model_id), :output) || default_output_price
|
39
|
+
return base_price unless long_context_model?(model_id)
|
40
|
+
|
41
|
+
# Double the price for prompts longer than 128k tokens
|
42
|
+
context_length(model_id) > 128_000 ? base_price * 2 : base_price
|
43
|
+
end
|
44
|
+
|
45
|
+
def supports_vision?(model_id)
|
46
|
+
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
|
47
|
+
return false if model_id.match?(/gemini-1\.0/)
|
48
|
+
|
49
|
+
model_id.match?(/gemini-[12]\.[05]/)
|
50
|
+
end
|
51
|
+
|
52
|
+
def supports_functions?(model_id)
|
53
|
+
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
|
54
|
+
return false if model_id.match?(/flash-lite/)
|
55
|
+
return false if model_id.match?(/gemini-1\.0/)
|
56
|
+
|
57
|
+
model_id.match?(/gemini-[12]\.[05]-(?:pro|flash)(?!-lite)/)
|
58
|
+
end
|
59
|
+
|
60
|
+
def supports_json_mode?(model_id)
|
61
|
+
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
|
62
|
+
return false if model_id.match?(/gemini-1\.0/)
|
63
|
+
|
64
|
+
model_id.match?(/gemini-\d/)
|
65
|
+
end
|
66
|
+
|
67
|
+
def format_display_name(model_id)
|
68
|
+
model_id
|
69
|
+
.delete_prefix('models/')
|
70
|
+
.split('-')
|
71
|
+
.map(&:capitalize)
|
72
|
+
.join(' ')
|
73
|
+
.gsub(/(\d+\.\d+)/, ' \1') # Add space before version numbers
|
74
|
+
.gsub(/\s+/, ' ') # Clean up multiple spaces
|
75
|
+
.gsub(/Aqa/, 'AQA') # Special case for AQA
|
76
|
+
.strip
|
77
|
+
end
|
78
|
+
|
79
|
+
def supports_caching?(model_id)
|
80
|
+
return false if model_id.match?(/flash-lite|gemini-1\.0/)
|
81
|
+
|
82
|
+
model_id.match?(/gemini-[12]\.[05]/)
|
83
|
+
end
|
84
|
+
|
85
|
+
def supports_tuning?(model_id)
|
86
|
+
model_id.match?(/gemini-1\.5-flash/)
|
87
|
+
end
|
88
|
+
|
89
|
+
def supports_audio?(model_id)
|
90
|
+
model_id.match?(/gemini-[12]\.[05]/)
|
91
|
+
end
|
92
|
+
|
93
|
+
def model_type(model_id)
|
94
|
+
case model_id
|
95
|
+
when /text-embedding|embedding/ then 'embedding'
|
96
|
+
when /imagen/ then 'image'
|
97
|
+
else 'chat'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def model_family(model_id) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
102
|
+
case model_id
|
103
|
+
when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
|
104
|
+
when /gemini-2\.0-flash/ then 'gemini20_flash'
|
105
|
+
when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
|
106
|
+
when /gemini-1\.5-flash/ then 'gemini15_flash'
|
107
|
+
when /gemini-1\.5-pro/ then 'gemini15_pro'
|
108
|
+
when /gemini-1\.0-pro/ then 'gemini10_pro'
|
109
|
+
when /text-embedding-004/ then 'embedding4'
|
110
|
+
when /embedding-001/ then 'embedding1'
|
111
|
+
when /aqa/ then 'aqa'
|
112
|
+
else 'other'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def pricing_family(model_id) # rubocop:disable Metrics/CyclomaticComplexity
|
117
|
+
case model_id
|
118
|
+
when /gemini-2\.0-flash-lite/ then :flash_lite_2 # rubocop:disable Naming/VariableNumber
|
119
|
+
when /gemini-2\.0-flash/ then :flash_2 # rubocop:disable Naming/VariableNumber
|
120
|
+
when /gemini-1\.5-flash-8b/ then :flash_8b
|
121
|
+
when /gemini-1\.5-flash/ then :flash
|
122
|
+
when /gemini-1\.5-pro/ then :pro
|
123
|
+
when /gemini-1\.0-pro/ then :pro_1_0 # rubocop:disable Naming/VariableNumber
|
124
|
+
when /text-embedding|embedding/ then :embedding
|
125
|
+
else :base
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def long_context_model?(model_id)
|
132
|
+
model_id.match?(/gemini-1\.5-(?:pro|flash)/)
|
133
|
+
end
|
134
|
+
|
135
|
+
def context_length(model_id)
|
136
|
+
context_window_for(model_id)
|
137
|
+
end
|
138
|
+
|
139
|
+
PRICES = {
|
140
|
+
flash_2: { # Gemini 2.0 Flash # rubocop:disable Naming/VariableNumber
|
141
|
+
input: 0.10,
|
142
|
+
output: 0.40,
|
143
|
+
audio_input: 0.70,
|
144
|
+
cache: 0.025,
|
145
|
+
cache_storage: 1.00
|
146
|
+
},
|
147
|
+
flash_lite_2: { # Gemini 2.0 Flash Lite # rubocop:disable Naming/VariableNumber
|
148
|
+
input: 0.075,
|
149
|
+
output: 0.30,
|
150
|
+
cache: 0.01875,
|
151
|
+
cache_storage: 1.00
|
152
|
+
},
|
153
|
+
flash: { # Gemini 1.5 Flash
|
154
|
+
input: 0.075,
|
155
|
+
output: 0.30,
|
156
|
+
cache: 0.01875,
|
157
|
+
cache_storage: 1.00
|
158
|
+
},
|
159
|
+
flash_8b: { # Gemini 1.5 Flash 8B
|
160
|
+
input: 0.0375,
|
161
|
+
output: 0.15,
|
162
|
+
cache: 0.01,
|
163
|
+
cache_storage: 0.25
|
164
|
+
},
|
165
|
+
pro: { # Gemini 1.5 Pro
|
166
|
+
input: 1.25,
|
167
|
+
output: 5.0,
|
168
|
+
cache: 0.3125,
|
169
|
+
cache_storage: 4.50
|
170
|
+
},
|
171
|
+
pro_1_0: { # Gemini 1.0 Pro # rubocop:disable Naming/VariableNumber
|
172
|
+
input: 0.50,
|
173
|
+
output: 1.50
|
174
|
+
},
|
175
|
+
embedding: { # Text Embedding models
|
176
|
+
input: 0.00,
|
177
|
+
output: 0.00
|
178
|
+
}
|
179
|
+
}.freeze
|
180
|
+
|
181
|
+
def default_input_price
|
182
|
+
0.075 # Default to Flash pricing
|
183
|
+
end
|
184
|
+
|
185
|
+
def default_output_price
|
186
|
+
0.30 # Default to Flash pricing
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Gemini
|
6
|
+
# Models methods of the Gemini API integration
|
7
|
+
module Models
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def parse_list_models_response(response)
|
11
|
+
response.body['data']&.each do |model|
|
12
|
+
model['id'] = model['id'].delete_prefix('models/')
|
13
|
+
end
|
14
|
+
|
15
|
+
OpenAI::Models.parse_list_models_response(response)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -3,8 +3,11 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module Providers
|
5
5
|
# Gemini API integration.
|
6
|
-
|
7
|
-
|
6
|
+
module Gemini
|
7
|
+
extend OpenAI
|
8
|
+
extend Gemini::Models
|
9
|
+
|
10
|
+
module_function
|
8
11
|
|
9
12
|
def api_base
|
10
13
|
'https://generativelanguage.googleapis.com/v1beta/openai'
|
@@ -15,14 +18,6 @@ module RubyLLM
|
|
15
18
|
'Authorization' => "Bearer #{RubyLLM.config.gemini_api_key}"
|
16
19
|
}
|
17
20
|
end
|
18
|
-
|
19
|
-
def parse_list_models_response(response)
|
20
|
-
response.body['data']&.each do |model|
|
21
|
-
model['id'] = model['id'].delete_prefix('models/')
|
22
|
-
end
|
23
|
-
|
24
|
-
super(response)
|
25
|
-
end
|
26
21
|
end
|
27
22
|
end
|
28
23
|
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module OpenAI
|
6
|
+
# Determines capabilities and pricing for OpenAI models
|
7
|
+
module Capabilities # rubocop:disable Metrics/ModuleLength
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def context_window_for(model_id)
|
11
|
+
case model_id
|
12
|
+
when /o[13]-mini/, /o3-mini-2025/ then 200_000
|
13
|
+
when /o1-2024/ then 200_000
|
14
|
+
when /gpt-4o/, /gpt-4-turbo/ then 128_000
|
15
|
+
when /gpt-4-0[0-9]{3}/ then 8_192
|
16
|
+
when /gpt-3.5-turbo-instruct/ then 4_096
|
17
|
+
when /gpt-3.5/ then 16_385
|
18
|
+
else 4_096
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def max_tokens_for(model_id) # rubocop:disable Metrics/CyclomaticComplexity
|
23
|
+
case model_id
|
24
|
+
when /o1-2024/, /o3-mini/ then 100_000
|
25
|
+
when /o1-mini-2024/ then 65_536
|
26
|
+
when /gpt-4o-2024-05-13/ then 4_096
|
27
|
+
when /gpt-4o/, /gpt-4o-mini/ then 16_384
|
28
|
+
when /gpt-4o-realtime/ then 4_096
|
29
|
+
when /gpt-4-0[0-9]{3}/ then 8_192
|
30
|
+
when /gpt-3.5-turbo/ then 4_096
|
31
|
+
else 4_096
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def input_price_for(model_id)
|
36
|
+
PRICES.dig(model_family(model_id), :input) || default_input_price
|
37
|
+
end
|
38
|
+
|
39
|
+
def output_price_for(model_id)
|
40
|
+
PRICES.dig(model_family(model_id), :output) || default_output_price
|
41
|
+
end
|
42
|
+
|
43
|
+
def supports_vision?(model_id)
|
44
|
+
model_id.match?(/gpt-4o|o1/) || model_id.match?(/gpt-4-(?!0314|0613)/)
|
45
|
+
end
|
46
|
+
|
47
|
+
def supports_functions?(model_id)
|
48
|
+
!model_id.include?('instruct')
|
49
|
+
end
|
50
|
+
|
51
|
+
def supports_audio?(model_id)
|
52
|
+
model_id.match?(/audio-preview|realtime-preview|whisper|tts/)
|
53
|
+
end
|
54
|
+
|
55
|
+
def supports_json_mode?(model_id)
|
56
|
+
model_id.match?(/gpt-4-\d{4}-preview/) ||
|
57
|
+
model_id.include?('turbo') ||
|
58
|
+
model_id.match?(/gpt-3.5-turbo-(?!0301|0613)/)
|
59
|
+
end
|
60
|
+
|
61
|
+
def format_display_name(model_id)
|
62
|
+
model_id.then { |id| humanize(id) }
|
63
|
+
.then { |name| apply_special_formatting(name) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def model_type(model_id)
|
67
|
+
case model_id
|
68
|
+
when /text-embedding|embedding/ then 'embedding'
|
69
|
+
when /dall-e/ then 'image'
|
70
|
+
when /tts|whisper/ then 'audio'
|
71
|
+
when /omni-moderation/ then 'moderation'
|
72
|
+
else 'chat'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def supports_structured_output?(model_id)
|
77
|
+
model_id.match?(/gpt-4o|o[13]-mini|o1/)
|
78
|
+
end
|
79
|
+
|
80
|
+
def model_family(model_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
|
81
|
+
case model_id
|
82
|
+
when /o3-mini/ then 'o3_mini'
|
83
|
+
when /o1-mini/ then 'o1_mini'
|
84
|
+
when /o1/ then 'o1'
|
85
|
+
when /gpt-4o-audio/ then 'gpt4o_audio'
|
86
|
+
when /gpt-4o-realtime/ then 'gpt4o_realtime'
|
87
|
+
when /gpt-4o-mini-audio/ then 'gpt4o_mini_audio'
|
88
|
+
when /gpt-4o-mini-realtime/ then 'gpt4o_mini_realtime'
|
89
|
+
when /gpt-4o-mini/ then 'gpt4o_mini'
|
90
|
+
when /gpt-4o/ then 'gpt4o'
|
91
|
+
when /gpt-4-turbo/ then 'gpt4_turbo'
|
92
|
+
when /gpt-4/ then 'gpt4'
|
93
|
+
when /gpt-3.5-turbo-instruct/ then 'gpt35_instruct'
|
94
|
+
when /gpt-3.5/ then 'gpt35'
|
95
|
+
when /dall-e-3/ then 'dalle3'
|
96
|
+
when /dall-e-2/ then 'dalle2'
|
97
|
+
when /text-embedding-3-large/ then 'embedding3_large'
|
98
|
+
when /text-embedding-3-small/ then 'embedding3_small'
|
99
|
+
when /text-embedding-ada/ then 'embedding2'
|
100
|
+
when /tts-1-hd/ then 'tts1_hd'
|
101
|
+
when /tts-1/ then 'tts1'
|
102
|
+
when /whisper/ then 'whisper1'
|
103
|
+
when /omni-moderation/ then 'moderation'
|
104
|
+
when /babbage/ then 'babbage'
|
105
|
+
when /davinci/ then 'davinci'
|
106
|
+
else 'other'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
PRICES = {
|
113
|
+
o1: { input: 15.0, cached_input: 7.5, output: 60.0 },
|
114
|
+
o1_mini: { input: 1.10, cached_input: 0.55, output: 4.40 },
|
115
|
+
o3_mini: { input: 1.10, cached_input: 0.55, output: 4.40 },
|
116
|
+
gpt4o: { input: 2.50, cached_input: 1.25, output: 10.0 },
|
117
|
+
gpt4o_audio: {
|
118
|
+
text_input: 2.50,
|
119
|
+
audio_input: 40.0,
|
120
|
+
text_output: 10.0,
|
121
|
+
audio_output: 80.0
|
122
|
+
},
|
123
|
+
gpt4o_realtime: {
|
124
|
+
text_input: 5.0,
|
125
|
+
cached_text_input: 2.50,
|
126
|
+
audio_input: 40.0,
|
127
|
+
cached_audio_input: 2.50,
|
128
|
+
text_output: 20.0,
|
129
|
+
audio_output: 80.0
|
130
|
+
},
|
131
|
+
gpt4o_mini: { input: 0.15, cached_input: 0.075, output: 0.60 },
|
132
|
+
gpt4o_mini_audio: {
|
133
|
+
text_input: 0.15,
|
134
|
+
audio_input: 10.0,
|
135
|
+
text_output: 0.60,
|
136
|
+
audio_output: 20.0
|
137
|
+
},
|
138
|
+
gpt4o_mini_realtime: {
|
139
|
+
text_input: 0.60,
|
140
|
+
cached_text_input: 0.30,
|
141
|
+
audio_input: 10.0,
|
142
|
+
cached_audio_input: 0.30,
|
143
|
+
text_output: 2.40,
|
144
|
+
audio_output: 20.0
|
145
|
+
},
|
146
|
+
gpt4_turbo: { input: 10.0, output: 30.0 },
|
147
|
+
gpt4: { input: 30.0, output: 60.0 },
|
148
|
+
gpt35: { input: 0.50, output: 1.50 },
|
149
|
+
gpt35_instruct: { input: 1.50, output: 2.0 },
|
150
|
+
embedding3_large: { price: 0.13 },
|
151
|
+
embedding3_small: { price: 0.02 },
|
152
|
+
embedding2: { price: 0.10 },
|
153
|
+
davinci: { input: 2.0, output: 2.0 },
|
154
|
+
babbage: { input: 0.40, output: 0.40 },
|
155
|
+
tts1: { price: 15.0 },
|
156
|
+
tts1_hd: { price: 30.0 },
|
157
|
+
whisper1: { price: 0.006 }
|
158
|
+
}.freeze
|
159
|
+
|
160
|
+
def default_input_price
|
161
|
+
0.50
|
162
|
+
end
|
163
|
+
|
164
|
+
def default_output_price
|
165
|
+
1.50
|
166
|
+
end
|
167
|
+
|
168
|
+
def humanize(id)
|
169
|
+
id.tr('-', ' ')
|
170
|
+
.split(' ')
|
171
|
+
.map(&:capitalize)
|
172
|
+
.join(' ')
|
173
|
+
end
|
174
|
+
|
175
|
+
def apply_special_formatting(name) # rubocop:disable Metrics/MethodLength
|
176
|
+
name
|
177
|
+
.gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
|
178
|
+
.gsub(/^Gpt /, 'GPT-')
|
179
|
+
.gsub(/^O([13]) /, 'O\1-')
|
180
|
+
.gsub(/^Chatgpt /, 'ChatGPT-')
|
181
|
+
.gsub(/^Tts /, 'TTS-')
|
182
|
+
.gsub(/^Dall E /, 'DALL-E-')
|
183
|
+
.gsub(/3\.5 /, '3.5-')
|
184
|
+
.gsub(/4 /, '4-')
|
185
|
+
.gsub(/4o (?=Mini|Preview|Turbo|Audio)/, '4o-')
|
186
|
+
.gsub(/\bHd\b/, 'HD')
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module OpenAI
|
6
|
+
# Chat methods of the OpenAI API integration
|
7
|
+
module Chat
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def completion_url
|
11
|
+
'chat/completions'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false) # rubocop:disable Metrics/MethodLength
|
15
|
+
{
|
16
|
+
model: model,
|
17
|
+
messages: format_messages(messages),
|
18
|
+
temperature: temperature,
|
19
|
+
stream: stream
|
20
|
+
}.tap do |payload|
|
21
|
+
if tools.any?
|
22
|
+
payload[:tools] = tools.map { |_, tool| tool_for(tool) }
|
23
|
+
payload[:tool_choice] = 'auto'
|
24
|
+
end
|
25
|
+
payload[:stream_options] = { include_usage: true } if stream
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_completion_response(response) # rubocop:disable Metrics/MethodLength
|
30
|
+
data = response.body
|
31
|
+
return if data.empty?
|
32
|
+
|
33
|
+
message_data = data.dig('choices', 0, 'message')
|
34
|
+
return unless message_data
|
35
|
+
|
36
|
+
Message.new(
|
37
|
+
role: :assistant,
|
38
|
+
content: message_data['content'],
|
39
|
+
tool_calls: parse_tool_calls(message_data['tool_calls']),
|
40
|
+
input_tokens: data['usage']['prompt_tokens'],
|
41
|
+
output_tokens: data['usage']['completion_tokens'],
|
42
|
+
model_id: data['model']
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_messages(messages)
|
47
|
+
messages.map do |msg|
|
48
|
+
{
|
49
|
+
role: format_role(msg.role),
|
50
|
+
content: msg.content,
|
51
|
+
tool_calls: format_tool_calls(msg.tool_calls),
|
52
|
+
tool_call_id: msg.tool_call_id
|
53
|
+
}.compact
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def format_role(role)
|
58
|
+
case role
|
59
|
+
when :system
|
60
|
+
'developer'
|
61
|
+
else
|
62
|
+
role.to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module OpenAI
|
6
|
+
# Embeddings methods of the OpenAI API integration
|
7
|
+
module Embeddings
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def embedding_url
|
11
|
+
'embeddings'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_embedding_payload(text, model:)
|
15
|
+
{
|
16
|
+
model: model,
|
17
|
+
input: text
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_embedding_response(response)
|
22
|
+
data = response.body
|
23
|
+
model_id = data['model']
|
24
|
+
input_tokens = data.dig('usage', 'prompt_tokens') || 0
|
25
|
+
vectors = data['data'].map { |d| d['embedding'] }
|
26
|
+
|
27
|
+
# If we only got one embedding, return it as a single vector
|
28
|
+
vectors = vectors.size == 1 ? vectors.first : vectors
|
29
|
+
|
30
|
+
Embedding.new(
|
31
|
+
vectors: vectors,
|
32
|
+
model: model_id,
|
33
|
+
input_tokens: input_tokens
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module OpenAI
|
6
|
+
# Models methods of the OpenAI 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) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
15
|
+
(response.body['data'] || []).map do |model|
|
16
|
+
ModelInfo.new(
|
17
|
+
id: model['id'],
|
18
|
+
created_at: model['created'] ? Time.at(model['created']) : nil,
|
19
|
+
display_name: capabilities.format_display_name(model['id']),
|
20
|
+
provider: slug,
|
21
|
+
type: capabilities.model_type(model['id']),
|
22
|
+
family: capabilities.model_family(model['id']),
|
23
|
+
metadata: {
|
24
|
+
object: model['object'],
|
25
|
+
owned_by: model['owned_by']
|
26
|
+
},
|
27
|
+
context_window: capabilities.context_window_for(model['id']),
|
28
|
+
max_tokens: capabilities.max_tokens_for(model['id']),
|
29
|
+
supports_vision: capabilities.supports_vision?(model['id']),
|
30
|
+
supports_functions: capabilities.supports_functions?(model['id']),
|
31
|
+
supports_json_mode: capabilities.supports_json_mode?(model['id']),
|
32
|
+
input_price_per_million: capabilities.input_price_for(model['id']),
|
33
|
+
output_price_per_million: capabilities.output_price_for(model['id'])
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module OpenAI
|
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 handle_stream(&block) # rubocop:disable Metrics/MethodLength
|
15
|
+
to_json_stream do |data|
|
16
|
+
block.call(
|
17
|
+
Chunk.new(
|
18
|
+
role: :assistant,
|
19
|
+
model_id: data['model'],
|
20
|
+
content: data.dig('choices', 0, 'delta', 'content'),
|
21
|
+
tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false),
|
22
|
+
input_tokens: data.dig('usage', 'prompt_tokens'),
|
23
|
+
output_tokens: data.dig('usage', 'completion_tokens')
|
24
|
+
)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module OpenAI
|
6
|
+
# Tools methods of the OpenAI API integration
|
7
|
+
module Tools
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def tool_for(tool) # rubocop:disable Metrics/MethodLength
|
11
|
+
{
|
12
|
+
type: 'function',
|
13
|
+
function: {
|
14
|
+
name: tool.name,
|
15
|
+
description: tool.description,
|
16
|
+
parameters: {
|
17
|
+
type: 'object',
|
18
|
+
properties: tool.parameters.transform_values { |param| param_schema(param) },
|
19
|
+
required: tool.parameters.select { |_, p| p.required }.keys
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def param_schema(param)
|
26
|
+
{
|
27
|
+
type: param.type,
|
28
|
+
description: param.description
|
29
|
+
}.compact
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_tool_calls(tool_calls) # rubocop:disable Metrics/MethodLength
|
33
|
+
return nil unless tool_calls&.any?
|
34
|
+
|
35
|
+
tool_calls.map do |_, tc|
|
36
|
+
{
|
37
|
+
id: tc.id,
|
38
|
+
type: 'function',
|
39
|
+
function: {
|
40
|
+
name: tc.name,
|
41
|
+
arguments: JSON.generate(tc.arguments)
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_tool_calls(tool_calls, parse_arguments: true) # rubocop:disable Metrics/MethodLength
|
48
|
+
return nil unless tool_calls&.any?
|
49
|
+
|
50
|
+
tool_calls.to_h do |tc|
|
51
|
+
[
|
52
|
+
tc['id'],
|
53
|
+
ToolCall.new(
|
54
|
+
id: tc['id'],
|
55
|
+
name: tc.dig('function', 'name'),
|
56
|
+
arguments: if parse_arguments
|
57
|
+
JSON.parse(tc.dig('function',
|
58
|
+
'arguments'))
|
59
|
+
else
|
60
|
+
tc.dig('function', 'arguments')
|
61
|
+
end
|
62
|
+
)
|
63
|
+
]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|