ruby_llm 1.14.0 → 1.14.1

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.
@@ -3,37 +3,10 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Anthropic
6
- # Determines capabilities and pricing for Anthropic models
6
+ # Provider-level capability checks used outside the model registry.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def determine_context_window(_model_id)
11
- 200_000
12
- end
13
-
14
- def determine_max_tokens(model_id)
15
- case model_id
16
- when /claude-3-7-sonnet/, /claude-3-5/ then 8_192
17
- else 4_096
18
- end
19
- end
20
-
21
- def get_input_price(model_id)
22
- PRICES.dig(model_family(model_id), :input) || default_input_price
23
- end
24
-
25
- def get_output_price(model_id)
26
- PRICES.dig(model_family(model_id), :output) || default_output_price
27
- end
28
-
29
- def supports_vision?(model_id)
30
- !model_id.match?(/claude-[12]/)
31
- end
32
-
33
- def supports_functions?(model_id)
34
- !model_id.match?(/claude-[12]/)
35
- end
36
-
37
10
  def supports_tool_choice?(_model_id)
38
11
  true
39
12
  end
@@ -41,111 +14,6 @@ module RubyLLM
41
14
  def supports_tool_parallel_control?(_model_id)
42
15
  true
43
16
  end
44
-
45
- def supports_json_mode?(model_id)
46
- !model_id.match?(/claude-[12]/)
47
- end
48
-
49
- def supports_structured_output?(model_id)
50
- match = model_id.match(/claude-(?:sonnet|opus|haiku)-(\d+)-(\d+)/)
51
- return false unless match
52
-
53
- major = match[1].to_i
54
- minor = match[2].to_i
55
- major > 4 || (major == 4 && minor >= 5)
56
- end
57
-
58
- def supports_extended_thinking?(model_id)
59
- model_id.match?(/claude-3-7-sonnet/)
60
- end
61
-
62
- def model_family(model_id)
63
- case model_id
64
- when /claude-3-7-sonnet/ then 'claude-3-7-sonnet'
65
- when /claude-3-5-sonnet/ then 'claude-3-5-sonnet'
66
- when /claude-3-5-haiku/ then 'claude-3-5-haiku'
67
- when /claude-3-opus/ then 'claude-3-opus'
68
- when /claude-3-sonnet/ then 'claude-3-sonnet'
69
- when /claude-3-haiku/ then 'claude-3-haiku'
70
- else 'claude-2'
71
- end
72
- end
73
-
74
- def model_type(_)
75
- 'chat'
76
- end
77
-
78
- PRICES = {
79
- 'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
80
- 'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
81
- 'claude-3-5-haiku': { input: 0.80, output: 4.0 },
82
- 'claude-3-opus': { input: 15.0, output: 75.0 },
83
- 'claude-3-haiku': { input: 0.25, output: 1.25 },
84
- 'claude-2': { input: 3.0, output: 15.0 }
85
- }.freeze
86
-
87
- def default_input_price
88
- 3.0
89
- end
90
-
91
- def default_output_price
92
- 15.0
93
- end
94
-
95
- def modalities_for(model_id)
96
- modalities = {
97
- input: ['text'],
98
- output: ['text']
99
- }
100
-
101
- unless model_id.match?(/claude-[12]/)
102
- modalities[:input] << 'image'
103
- modalities[:input] << 'pdf'
104
- end
105
-
106
- modalities
107
- end
108
-
109
- def capabilities_for(model_id)
110
- capabilities = ['streaming']
111
-
112
- unless model_id.match?(/claude-[12]/)
113
- capabilities << 'function_calling'
114
- capabilities << 'batch'
115
- end
116
-
117
- capabilities << 'structured_output' if supports_structured_output?(model_id)
118
- capabilities << 'reasoning' if model_id.match?(/claude-3-7-sonnet|claude-(?:sonnet|opus|haiku)-4/)
119
- capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/)
120
- capabilities
121
- end
122
-
123
- def pricing_for(model_id)
124
- family = model_family(model_id)
125
- prices = PRICES.fetch(family.to_sym, { input: default_input_price, output: default_output_price })
126
-
127
- standard_pricing = {
128
- input_per_million: prices[:input],
129
- output_per_million: prices[:output]
130
- }
131
-
132
- batch_pricing = {
133
- input_per_million: prices[:input] * 0.5,
134
- output_per_million: prices[:output] * 0.5
135
- }
136
-
137
- if model_id.match?(/claude-3-7/)
138
- standard_pricing[:reasoning_output_per_million] = prices[:output] * 2.5
139
- batch_pricing[:reasoning_output_per_million] = prices[:output] * 1.25
140
- end
141
-
142
- {
143
- text_tokens: {
144
- standard: standard_pricing,
145
- batch: batch_pricing
146
- }
147
- }
148
- end
149
17
  end
150
18
  end
151
19
  end
@@ -11,21 +11,15 @@ module RubyLLM
11
11
  'v1/models'
12
12
  end
13
13
 
14
- def parse_list_models_response(response, slug, capabilities)
14
+ def parse_list_models_response(response, slug, _capabilities)
15
15
  Array(response.body['data']).map do |model_data|
16
16
  model_id = model_data['id']
17
17
 
18
18
  Model::Info.new(
19
19
  id: model_id,
20
- name: model_data['display_name'],
20
+ name: model_data['display_name'] || model_id,
21
21
  provider: slug,
22
- family: capabilities.model_family(model_id),
23
22
  created_at: Time.parse(model_data['created_at']),
24
- context_window: capabilities.determine_context_window(model_id),
25
- max_output_tokens: capabilities.determine_max_tokens(model_id),
26
- modalities: capabilities.modalities_for(model_id),
27
- capabilities: capabilities.capabilities_for(model_id),
28
- pricing: capabilities.pricing_for(model_id),
29
23
  metadata: {}
30
24
  )
31
25
  end
@@ -3,44 +3,10 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class DeepSeek
6
- # Determines capabilities and pricing for DeepSeek models
6
+ # Provider-level capability checks used outside the model registry.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def context_window_for(model_id)
11
- case model_id
12
- when /deepseek-(?:chat|reasoner)/ then 64_000
13
- else 32_768
14
- end
15
- end
16
-
17
- def max_tokens_for(model_id)
18
- case model_id
19
- when /deepseek-(?:chat|reasoner)/ then 8_192
20
- else 4_096
21
- end
22
- end
23
-
24
- def input_price_for(model_id)
25
- PRICES.dig(model_family(model_id), :input_miss) || default_input_price
26
- end
27
-
28
- def output_price_for(model_id)
29
- PRICES.dig(model_family(model_id), :output) || default_output_price
30
- end
31
-
32
- def cache_hit_price_for(model_id)
33
- PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
34
- end
35
-
36
- def supports_vision?(_model_id)
37
- false
38
- end
39
-
40
- def supports_functions?(model_id)
41
- model_id.match?(/deepseek-chat/)
42
- end
43
-
44
10
  def supports_tool_choice?(_model_id)
45
11
  true
46
12
  end
@@ -48,90 +14,6 @@ module RubyLLM
48
14
  def supports_tool_parallel_control?(_model_id)
49
15
  false
50
16
  end
51
-
52
- def supports_json_mode?(_model_id)
53
- false
54
- end
55
-
56
- def format_display_name(model_id)
57
- case model_id
58
- when 'deepseek-chat' then 'DeepSeek V3'
59
- when 'deepseek-reasoner' then 'DeepSeek R1'
60
- else
61
- model_id.split('-')
62
- .map(&:capitalize)
63
- .join(' ')
64
- end
65
- end
66
-
67
- def model_type(_model_id)
68
- 'chat'
69
- end
70
-
71
- def model_family(model_id)
72
- case model_id
73
- when /deepseek-reasoner/ then :reasoner
74
- else :chat
75
- end
76
- end
77
-
78
- PRICES = {
79
- chat: {
80
- input_hit: 0.07,
81
- input_miss: 0.27,
82
- output: 1.10
83
- },
84
- reasoner: {
85
- input_hit: 0.14,
86
- input_miss: 0.55,
87
- output: 2.19
88
- }
89
- }.freeze
90
-
91
- def default_input_price
92
- 0.27
93
- end
94
-
95
- def default_output_price
96
- 1.10
97
- end
98
-
99
- def default_cache_hit_price
100
- 0.07
101
- end
102
-
103
- def modalities_for(_model_id)
104
- {
105
- input: ['text'],
106
- output: ['text']
107
- }
108
- end
109
-
110
- def capabilities_for(model_id)
111
- capabilities = ['streaming']
112
-
113
- capabilities << 'function_calling' if model_id.match?(/deepseek-chat/)
114
-
115
- capabilities
116
- end
117
-
118
- def pricing_for(model_id)
119
- family = model_family(model_id)
120
- prices = PRICES.fetch(family, { input_miss: default_input_price, output: default_output_price })
121
-
122
- standard_pricing = {
123
- input_per_million: prices[:input_miss],
124
- output_per_million: prices[:output]
125
- }
126
-
127
- standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
128
-
129
- {
130
- text_tokens: {
131
- standard: standard_pricing
132
- }
133
- }
134
- end
135
17
  end
136
18
  end
137
19
  end
@@ -3,13 +3,35 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Gemini
6
- # Determines capabilities and pricing for Google Gemini models
6
+ # Provider-level capability checks and narrow registry fallbacks.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
+ PRICES = {
11
+ flash_2: { input: 0.10, output: 0.40 }, # rubocop:disable Naming/VariableNumber
12
+ flash_lite_2: { input: 0.075, output: 0.30 }, # rubocop:disable Naming/VariableNumber
13
+ flash: { input: 0.075, output: 0.30 },
14
+ flash_8b: { input: 0.0375, output: 0.15 },
15
+ pro: { input: 1.25, output: 5.0 },
16
+ pro_2_5: { input: 0.12, output: 0.50 }, # rubocop:disable Naming/VariableNumber
17
+ gemini_embedding: { input: 0.002, output: 0.004 },
18
+ embedding: { input: 0.00, output: 0.00 },
19
+ imagen: { price: 0.03 },
20
+ aqa: { input: 0.00, output: 0.00 }
21
+ }.freeze
22
+
23
+ def supports_tool_choice?(_model_id)
24
+ true
25
+ end
26
+
27
+ def supports_tool_parallel_control?(_model_id)
28
+ false
29
+ end
30
+
10
31
  def context_window_for(model_id)
11
32
  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
33
+ when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/,
34
+ /gemini-1\.5-flash-8b/
13
35
  1_048_576
14
36
  when /gemini-1\.5-pro/ then 2_097_152
15
37
  when /gemini-embedding-exp/ then 8_192
@@ -23,7 +45,8 @@ module RubyLLM
23
45
  def max_tokens_for(model_id)
24
46
  case model_id
25
47
  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
48
+ when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/,
49
+ /gemini-1\.5-pro/
27
50
  8_192
28
51
  when /gemini-embedding-exp/ then nil
29
52
  when /text-embedding-004/, /embedding-001/ then 768
@@ -32,18 +55,24 @@ module RubyLLM
32
55
  end
33
56
  end
34
57
 
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
58
+ def critical_capabilities_for(model_id)
59
+ capabilities = []
60
+ capabilities << 'function_calling' if supports_functions?(model_id)
61
+ capabilities << 'structured_output' if supports_structured_output?(model_id)
62
+ capabilities << 'vision' if supports_vision?(model_id)
63
+ capabilities
40
64
  end
41
65
 
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
66
+ def pricing_for(model_id)
67
+ prices = PRICES.fetch(pricing_family(model_id), { input: 0.075, output: 0.30 })
68
+ {
69
+ text_tokens: {
70
+ standard: {
71
+ input_per_million: prices[:input] || prices[:price] || 0.075,
72
+ output_per_million: prices[:output] || prices[:price] || 0.30
73
+ }
74
+ }
75
+ }
47
76
  end
48
77
 
49
78
  def supports_vision?(model_id)
@@ -52,25 +81,13 @@ module RubyLLM
52
81
  model_id.match?(/gemini|flash|pro|imagen/)
53
82
  end
54
83
 
55
- def supports_video?(model_id)
56
- model_id.match?(/gemini/)
57
- end
58
-
59
84
  def supports_functions?(model_id)
60
85
  return false if model_id.match?(/text-embedding|embedding-001|aqa|flash-lite|imagen|gemini-2\.0-flash-lite/)
61
86
 
62
87
  model_id.match?(/gemini|pro|flash/)
63
88
  end
64
89
 
65
- def supports_tool_choice?(_model_id)
66
- true
67
- end
68
-
69
- def supports_tool_parallel_control?(_model_id)
70
- false
71
- end
72
-
73
- def supports_json_mode?(model_id)
90
+ def supports_structured_output?(model_id)
74
91
  if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
75
92
  return false
76
93
  end
@@ -78,59 +95,6 @@ module RubyLLM
78
95
  model_id.match?(/gemini|pro|flash/)
79
96
  end
80
97
 
81
- def format_display_name(model_id)
82
- model_id
83
- .delete_prefix('models/')
84
- .split('-')
85
- .map(&:capitalize)
86
- .join(' ')
87
- .gsub(/(\d+\.\d+)/, ' \1')
88
- .gsub(/\s+/, ' ')
89
- .gsub('Aqa', 'AQA')
90
- .strip
91
- end
92
-
93
- def supports_caching?(model_id)
94
- if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
95
- return false
96
- end
97
-
98
- model_id.match?(/gemini|pro|flash/)
99
- end
100
-
101
- def supports_tuning?(model_id)
102
- model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
103
- end
104
-
105
- def supports_audio?(model_id)
106
- model_id.match?(/gemini|pro|flash/)
107
- end
108
-
109
- def model_type(model_id)
110
- case model_id
111
- when /text-embedding|embedding|gemini-embedding/ then 'embedding'
112
- when /imagen/ then 'image'
113
- else 'chat'
114
- end
115
- end
116
-
117
- def model_family(model_id)
118
- case model_id
119
- when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
120
- when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
121
- when /gemini-2\.0-flash/ then 'gemini20_flash'
122
- when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
123
- when /gemini-1\.5-flash/ then 'gemini15_flash'
124
- when /gemini-1\.5-pro/ then 'gemini15_pro'
125
- when /gemini-embedding-exp/ then 'gemini_embedding_exp'
126
- when /text-embedding-004/ then 'embedding4'
127
- when /embedding-001/ then 'embedding1'
128
- when /aqa/ then 'aqa'
129
- when /imagen-3/ then 'imagen3'
130
- else 'other'
131
- end
132
- end
133
-
134
98
  def pricing_family(model_id)
135
99
  case model_id
136
100
  when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
@@ -147,142 +111,8 @@ module RubyLLM
147
111
  end
148
112
  end
149
113
 
150
- def long_context_model?(model_id)
151
- model_id.match?(/gemini-1\.5-(?:pro|flash)|gemini-1\.5-flash-8b/)
152
- end
153
-
154
- def context_length(model_id)
155
- context_window_for(model_id)
156
- end
157
-
158
- PRICES = {
159
- flash_2: { # rubocop:disable Naming/VariableNumber
160
- input: 0.10,
161
- output: 0.40,
162
- audio_input: 0.70,
163
- cache: 0.025,
164
- cache_storage: 1.00,
165
- grounding_search: 35.00
166
- },
167
- flash_lite_2: { # rubocop:disable Naming/VariableNumber
168
- input: 0.075,
169
- output: 0.30
170
- },
171
- flash: {
172
- input: 0.075,
173
- output: 0.30,
174
- cache: 0.01875,
175
- cache_storage: 1.00,
176
- grounding_search: 35.00
177
- },
178
- flash_8b: {
179
- input: 0.0375,
180
- output: 0.15,
181
- cache: 0.01,
182
- cache_storage: 0.25,
183
- grounding_search: 35.00
184
- },
185
- pro: {
186
- input: 1.25,
187
- output: 5.0,
188
- cache: 0.3125,
189
- cache_storage: 4.50,
190
- grounding_search: 35.00
191
- },
192
- pro_2_5: { # rubocop:disable Naming/VariableNumber
193
- input: 0.12,
194
- output: 0.50
195
- },
196
- gemini_embedding: {
197
- input: 0.002,
198
- output: 0.004
199
- },
200
- embedding: {
201
- input: 0.00,
202
- output: 0.00
203
- },
204
- imagen: {
205
- price: 0.03
206
- },
207
- aqa: {
208
- input: 0.00,
209
- output: 0.00
210
- }
211
- }.freeze
212
-
213
- def default_input_price
214
- 0.075
215
- end
216
-
217
- def default_output_price
218
- 0.30
219
- end
220
-
221
- def modalities_for(model_id)
222
- modalities = {
223
- input: ['text'],
224
- output: ['text']
225
- }
226
-
227
- if supports_vision?(model_id)
228
- modalities[:input] << 'image'
229
- modalities[:input] << 'pdf'
230
- end
231
-
232
- modalities[:input] << 'video' if supports_video?(model_id)
233
- modalities[:input] << 'audio' if model_id.match?(/audio/)
234
- modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
235
- modalities[:output] = ['image'] if model_id.match?(/imagen/)
236
-
237
- modalities
238
- end
239
-
240
- def capabilities_for(model_id)
241
- capabilities = ['streaming']
242
-
243
- capabilities << 'function_calling' if supports_functions?(model_id)
244
- capabilities << 'structured_output' if supports_json_mode?(model_id)
245
- capabilities << 'batch' if model_id.match?(/embedding|flash/)
246
- capabilities << 'caching' if supports_caching?(model_id)
247
- capabilities << 'fine_tuning' if supports_tuning?(model_id)
248
- capabilities
249
- end
250
-
251
- def pricing_for(model_id)
252
- family = pricing_family(model_id)
253
- prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
254
-
255
- standard_pricing = {
256
- input_per_million: prices[:input],
257
- output_per_million: prices[:output]
258
- }
259
-
260
- standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
261
-
262
- batch_pricing = {
263
- input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
264
- output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
265
- }
266
-
267
- if standard_pricing[:cached_input_per_million]
268
- batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
269
- end
270
-
271
- pricing = {
272
- text_tokens: {
273
- standard: standard_pricing,
274
- batch: batch_pricing
275
- }
276
- }
277
-
278
- if model_id.match?(/embedding|gemini-embedding/)
279
- pricing[:embeddings] = {
280
- standard: { input_per_million: prices[:price] || 0.002 }
281
- }
282
- end
283
-
284
- pricing
285
- end
114
+ module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
115
+ :supports_vision?, :supports_functions?, :supports_structured_output?, :pricing_family
286
116
  end
287
117
  end
288
118
  end
@@ -17,14 +17,12 @@ module RubyLLM
17
17
 
18
18
  Model::Info.new(
19
19
  id: model_id,
20
- name: model_data['displayName'],
20
+ name: model_data['displayName'] || model_id,
21
21
  provider: slug,
22
- family: capabilities.model_family(model_id),
23
22
  created_at: nil,
24
23
  context_window: model_data['inputTokenLimit'] || capabilities.context_window_for(model_id),
25
24
  max_output_tokens: model_data['outputTokenLimit'] || capabilities.max_tokens_for(model_id),
26
- modalities: capabilities.modalities_for(model_id),
27
- capabilities: capabilities.capabilities_for(model_id),
25
+ capabilities: capabilities.critical_capabilities_for(model_id),
28
26
  pricing: capabilities.pricing_for(model_id),
29
27
  metadata: {
30
28
  version: model_data['version'],