open_router_enhanced 1.0.0
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/.env.example +1 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/.rubocop_todo.yml +130 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +41 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +384 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +556 -0
- data/README.md +1660 -0
- data/Rakefile +334 -0
- data/SECURITY.md +150 -0
- data/VCR_CONFIGURATION.md +80 -0
- data/docs/model_selection.md +637 -0
- data/docs/observability.md +430 -0
- data/docs/prompt_templates.md +422 -0
- data/docs/streaming.md +467 -0
- data/docs/structured_outputs.md +466 -0
- data/docs/tools.md +1016 -0
- data/examples/basic_completion.rb +122 -0
- data/examples/model_selection_example.rb +141 -0
- data/examples/observability_example.rb +199 -0
- data/examples/prompt_template_example.rb +184 -0
- data/examples/smart_completion_example.rb +89 -0
- data/examples/streaming_example.rb +176 -0
- data/examples/structured_outputs_example.rb +191 -0
- data/examples/tool_calling_example.rb +149 -0
- data/lib/open_router/client.rb +552 -0
- data/lib/open_router/http.rb +118 -0
- data/lib/open_router/json_healer.rb +263 -0
- data/lib/open_router/model_registry.rb +378 -0
- data/lib/open_router/model_selector.rb +462 -0
- data/lib/open_router/prompt_template.rb +290 -0
- data/lib/open_router/response.rb +371 -0
- data/lib/open_router/schema.rb +288 -0
- data/lib/open_router/streaming_client.rb +210 -0
- data/lib/open_router/tool.rb +221 -0
- data/lib/open_router/tool_call.rb +180 -0
- data/lib/open_router/usage_tracker.rb +277 -0
- data/lib/open_router/version.rb +5 -0
- data/lib/open_router.rb +123 -0
- data/sig/open_router.rbs +20 -0
- metadata +186 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module OpenRouter
|
|
6
|
+
class ModelSelectionError < Error; end
|
|
7
|
+
|
|
8
|
+
# ModelSelector provides a fluent DSL interface for selecting the best AI model
|
|
9
|
+
# based on specific requirements. It wraps the ModelRegistry functionality
|
|
10
|
+
# with an intuitive, chainable API.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# selector = OpenRouter::ModelSelector.new
|
|
14
|
+
# model = selector.optimize_for(:cost)
|
|
15
|
+
# .require(:function_calling, :vision)
|
|
16
|
+
# .within_budget(max_cost: 0.01)
|
|
17
|
+
# .choose
|
|
18
|
+
#
|
|
19
|
+
# @example With provider preferences
|
|
20
|
+
# model = OpenRouter::ModelSelector.new
|
|
21
|
+
# .prefer_providers("anthropic", "openai")
|
|
22
|
+
# .require(:function_calling)
|
|
23
|
+
# .choose
|
|
24
|
+
#
|
|
25
|
+
# @example With fallback selection
|
|
26
|
+
# models = OpenRouter::ModelSelector.new
|
|
27
|
+
# .optimize_for(:performance)
|
|
28
|
+
# .choose_with_fallbacks(limit: 3)
|
|
29
|
+
class ModelSelector
|
|
30
|
+
# Available optimization strategies
|
|
31
|
+
STRATEGIES = {
|
|
32
|
+
cost: { sort_by: :cost, pick_newer: false },
|
|
33
|
+
performance: { sort_by: :performance, pick_newer: false },
|
|
34
|
+
latest: { sort_by: :date, pick_newer: true },
|
|
35
|
+
context: { sort_by: :context_length, pick_newer: false }
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
def initialize(requirements: {}, strategy: :cost, provider_preferences: {}, fallback_options: {})
|
|
39
|
+
@requirements = requirements.dup
|
|
40
|
+
@strategy = strategy
|
|
41
|
+
@provider_preferences = provider_preferences.dup
|
|
42
|
+
@fallback_options = fallback_options.dup
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set the optimization strategy for model selection
|
|
46
|
+
#
|
|
47
|
+
# @param strategy [Symbol] The optimization strategy (:cost, :performance, :latest, :context)
|
|
48
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
49
|
+
# @raise [ArgumentError] If strategy is not supported
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# selector.optimize_for(:cost) # Choose cheapest model
|
|
53
|
+
# selector.optimize_for(:performance) # Choose highest performance tier
|
|
54
|
+
# selector.optimize_for(:latest) # Choose newest model
|
|
55
|
+
# selector.optimize_for(:context) # Choose model with largest context window
|
|
56
|
+
def optimize_for(strategy)
|
|
57
|
+
unless STRATEGIES.key?(strategy)
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"Unknown strategy: #{strategy}. Available: #{STRATEGIES.keys.join(", ")}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
new_requirements = @requirements.dup
|
|
63
|
+
|
|
64
|
+
# Apply strategy-specific requirements
|
|
65
|
+
case strategy
|
|
66
|
+
when :performance
|
|
67
|
+
new_requirements[:performance_tier] = :premium
|
|
68
|
+
when :latest
|
|
69
|
+
new_requirements[:pick_newer] = true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
self.class.new(
|
|
73
|
+
requirements: new_requirements,
|
|
74
|
+
strategy:,
|
|
75
|
+
provider_preferences: @provider_preferences,
|
|
76
|
+
fallback_options: @fallback_options
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Require specific capabilities from the selected model
|
|
81
|
+
#
|
|
82
|
+
# @param capabilities [Array<Symbol>] Required capabilities
|
|
83
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
84
|
+
#
|
|
85
|
+
# @example
|
|
86
|
+
# selector.require(:function_calling)
|
|
87
|
+
# selector.require(:function_calling, :vision, :structured_outputs)
|
|
88
|
+
def require(*capabilities)
|
|
89
|
+
new_requirements = @requirements.dup
|
|
90
|
+
new_requirements[:capabilities] = Array(new_requirements[:capabilities]) + capabilities
|
|
91
|
+
new_requirements[:capabilities].uniq!
|
|
92
|
+
|
|
93
|
+
self.class.new(
|
|
94
|
+
requirements: new_requirements,
|
|
95
|
+
strategy: @strategy,
|
|
96
|
+
provider_preferences: @provider_preferences,
|
|
97
|
+
fallback_options: @fallback_options
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Set budget constraints for model selection
|
|
102
|
+
#
|
|
103
|
+
# @param max_cost [Float] Maximum cost per 1k input tokens
|
|
104
|
+
# @param max_output_cost [Float] Maximum cost per 1k output tokens (optional)
|
|
105
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# selector.within_budget(max_cost: 0.01)
|
|
109
|
+
# selector.within_budget(max_cost: 0.01, max_output_cost: 0.02)
|
|
110
|
+
def within_budget(max_cost: nil, max_output_cost: nil)
|
|
111
|
+
new_requirements = @requirements.dup
|
|
112
|
+
new_requirements[:max_input_cost] = max_cost if max_cost
|
|
113
|
+
new_requirements[:max_output_cost] = max_output_cost if max_output_cost
|
|
114
|
+
|
|
115
|
+
self.class.new(
|
|
116
|
+
requirements: new_requirements,
|
|
117
|
+
strategy: @strategy,
|
|
118
|
+
provider_preferences: @provider_preferences,
|
|
119
|
+
fallback_options: @fallback_options
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Set minimum context length requirement
|
|
124
|
+
#
|
|
125
|
+
# @param tokens [Integer] Minimum context length in tokens
|
|
126
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
127
|
+
#
|
|
128
|
+
# @example
|
|
129
|
+
# selector.min_context(100_000) # Require at least 100k context
|
|
130
|
+
def min_context(tokens)
|
|
131
|
+
new_requirements = @requirements.dup
|
|
132
|
+
new_requirements[:min_context_length] = tokens
|
|
133
|
+
|
|
134
|
+
self.class.new(
|
|
135
|
+
requirements: new_requirements,
|
|
136
|
+
strategy: @strategy,
|
|
137
|
+
provider_preferences: @provider_preferences,
|
|
138
|
+
fallback_options: @fallback_options
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Require models released after a specific date
|
|
143
|
+
#
|
|
144
|
+
# @param date [Date, Time, Integer] Cutoff date (Date/Time object or Unix timestamp)
|
|
145
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
146
|
+
#
|
|
147
|
+
# @example
|
|
148
|
+
# selector.newer_than(Date.new(2024, 1, 1))
|
|
149
|
+
# selector.newer_than(Time.now - 30.days)
|
|
150
|
+
def newer_than(date)
|
|
151
|
+
new_requirements = @requirements.dup
|
|
152
|
+
new_requirements[:released_after_date] = date
|
|
153
|
+
|
|
154
|
+
self.class.new(
|
|
155
|
+
requirements: new_requirements,
|
|
156
|
+
strategy: @strategy,
|
|
157
|
+
provider_preferences: @provider_preferences,
|
|
158
|
+
fallback_options: @fallback_options
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Set provider preferences (soft preference - won't exclude other providers)
|
|
163
|
+
#
|
|
164
|
+
# @param providers [Array<String>] Preferred provider names in order of preference
|
|
165
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
166
|
+
#
|
|
167
|
+
# @example
|
|
168
|
+
# selector.prefer_providers("anthropic", "openai")
|
|
169
|
+
def prefer_providers(*providers)
|
|
170
|
+
new_provider_preferences = @provider_preferences.dup
|
|
171
|
+
new_provider_preferences[:preferred] = providers.flatten
|
|
172
|
+
|
|
173
|
+
self.class.new(
|
|
174
|
+
requirements: @requirements,
|
|
175
|
+
strategy: @strategy,
|
|
176
|
+
provider_preferences: new_provider_preferences,
|
|
177
|
+
fallback_options: @fallback_options
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Require specific providers (hard filter - only these providers)
|
|
182
|
+
#
|
|
183
|
+
# @param providers [Array<String>] Required provider names
|
|
184
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
185
|
+
#
|
|
186
|
+
# @example
|
|
187
|
+
# selector.require_providers("anthropic")
|
|
188
|
+
def require_providers(*providers)
|
|
189
|
+
new_provider_preferences = @provider_preferences.dup
|
|
190
|
+
new_provider_preferences[:required] = providers.flatten
|
|
191
|
+
|
|
192
|
+
self.class.new(
|
|
193
|
+
requirements: @requirements,
|
|
194
|
+
strategy: @strategy,
|
|
195
|
+
provider_preferences: new_provider_preferences,
|
|
196
|
+
fallback_options: @fallback_options
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Avoid specific providers (blacklist)
|
|
201
|
+
#
|
|
202
|
+
# @param providers [Array<String>] Provider names to avoid
|
|
203
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
204
|
+
#
|
|
205
|
+
# @example
|
|
206
|
+
# selector.avoid_providers("google")
|
|
207
|
+
def avoid_providers(*providers)
|
|
208
|
+
new_provider_preferences = @provider_preferences.dup
|
|
209
|
+
new_provider_preferences[:avoided] = providers.flatten
|
|
210
|
+
|
|
211
|
+
self.class.new(
|
|
212
|
+
requirements: @requirements,
|
|
213
|
+
strategy: @strategy,
|
|
214
|
+
provider_preferences: new_provider_preferences,
|
|
215
|
+
fallback_options: @fallback_options
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Avoid models matching specific patterns
|
|
220
|
+
#
|
|
221
|
+
# @param patterns [Array<String>] Glob patterns to avoid
|
|
222
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
223
|
+
#
|
|
224
|
+
# @example
|
|
225
|
+
# selector.avoid_patterns("*-free", "*-preview")
|
|
226
|
+
def avoid_patterns(*patterns)
|
|
227
|
+
new_provider_preferences = @provider_preferences.dup
|
|
228
|
+
new_provider_preferences[:avoided_patterns] = patterns.flatten
|
|
229
|
+
|
|
230
|
+
self.class.new(
|
|
231
|
+
requirements: @requirements,
|
|
232
|
+
strategy: @strategy,
|
|
233
|
+
provider_preferences: new_provider_preferences,
|
|
234
|
+
fallback_options: @fallback_options
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Configure fallback behavior
|
|
239
|
+
#
|
|
240
|
+
# @param max_fallbacks [Integer] Maximum number of fallback models to include
|
|
241
|
+
# @param strategy [Symbol] Fallback strategy (:similar, :cheaper, :different_provider)
|
|
242
|
+
# @return [ModelSelector] Returns self for method chaining
|
|
243
|
+
#
|
|
244
|
+
# @example
|
|
245
|
+
# selector.with_fallbacks(max: 3, strategy: :similar)
|
|
246
|
+
def with_fallbacks(max: 3, strategy: :similar)
|
|
247
|
+
new_fallback_options = { max_fallbacks: max, strategy: }
|
|
248
|
+
|
|
249
|
+
self.class.new(
|
|
250
|
+
requirements: @requirements,
|
|
251
|
+
strategy: @strategy,
|
|
252
|
+
provider_preferences: @provider_preferences,
|
|
253
|
+
fallback_options: new_fallback_options
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Select the best model based on configured requirements
|
|
258
|
+
#
|
|
259
|
+
# @param return_specs [Boolean] Whether to return model specs along with model ID
|
|
260
|
+
# @return [String, Array] Model ID or [model_id, specs] tuple if return_specs is true
|
|
261
|
+
# @return [nil] If no models match requirements
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# model = selector.choose
|
|
265
|
+
# model, specs = selector.choose(return_specs: true)
|
|
266
|
+
def choose(return_specs: false)
|
|
267
|
+
# Get all models that meet basic requirements
|
|
268
|
+
candidates = filter_by_providers(ModelRegistry.models_meeting_requirements(@requirements))
|
|
269
|
+
|
|
270
|
+
return nil if candidates.empty?
|
|
271
|
+
|
|
272
|
+
# Apply strategy-specific sorting
|
|
273
|
+
best_match = apply_strategy_sorting(candidates)
|
|
274
|
+
|
|
275
|
+
return nil unless best_match
|
|
276
|
+
|
|
277
|
+
return_specs ? best_match : best_match.first
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Select the best model with fallback options
|
|
281
|
+
#
|
|
282
|
+
# @param limit [Integer] Maximum number of models to return (including primary choice)
|
|
283
|
+
# @return [Array<String>] Array of model IDs in order of preference
|
|
284
|
+
# @return [Array] Empty array if no models match requirements
|
|
285
|
+
#
|
|
286
|
+
# @example
|
|
287
|
+
# models = selector.choose_with_fallbacks(limit: 3)
|
|
288
|
+
# # => ["gpt-4", "claude-3-opus", "gpt-3.5-turbo"]
|
|
289
|
+
def choose_with_fallbacks(limit: 3)
|
|
290
|
+
candidates = filter_by_providers(ModelRegistry.models_meeting_requirements(@requirements))
|
|
291
|
+
|
|
292
|
+
return [] if candidates.empty?
|
|
293
|
+
|
|
294
|
+
# Apply strategy-specific sorting to get ordered list
|
|
295
|
+
sorted_candidates = apply_strategy_sorting_all(candidates)
|
|
296
|
+
|
|
297
|
+
# Return up to `limit` models
|
|
298
|
+
sorted_candidates.first(limit).map(&:first)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Choose with graceful degradation if no models meet all requirements
|
|
302
|
+
#
|
|
303
|
+
# @return [String, nil] Model ID or nil if no models available at all
|
|
304
|
+
#
|
|
305
|
+
# @example
|
|
306
|
+
# model = selector.choose_with_fallback
|
|
307
|
+
def choose_with_fallback
|
|
308
|
+
# Try with all requirements first
|
|
309
|
+
result = choose
|
|
310
|
+
return result if result
|
|
311
|
+
|
|
312
|
+
# Try dropping least important requirements progressively
|
|
313
|
+
fallback_requirements = @requirements.dup
|
|
314
|
+
|
|
315
|
+
# Drop requirements in order of importance (least to most important)
|
|
316
|
+
%i[
|
|
317
|
+
released_after_date
|
|
318
|
+
performance_tier
|
|
319
|
+
max_output_cost
|
|
320
|
+
min_context_length
|
|
321
|
+
max_input_cost
|
|
322
|
+
].each do |requirement|
|
|
323
|
+
next unless fallback_requirements.key?(requirement)
|
|
324
|
+
|
|
325
|
+
fallback_requirements.delete(requirement)
|
|
326
|
+
candidates = filter_by_providers(ModelRegistry.models_meeting_requirements(fallback_requirements))
|
|
327
|
+
|
|
328
|
+
unless candidates.empty?
|
|
329
|
+
result = apply_strategy_sorting(candidates)
|
|
330
|
+
return result&.first
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Last resort: just pick any model that meets capability requirements
|
|
335
|
+
if fallback_requirements[:capabilities]
|
|
336
|
+
basic_requirements = { capabilities: fallback_requirements[:capabilities] }
|
|
337
|
+
candidates = filter_by_providers(ModelRegistry.models_meeting_requirements(basic_requirements))
|
|
338
|
+
result = apply_strategy_sorting(candidates) unless candidates.empty?
|
|
339
|
+
return result&.first if result
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Final fallback: cheapest available model
|
|
343
|
+
all_candidates = filter_by_providers(ModelRegistry.all_models)
|
|
344
|
+
return nil if all_candidates.empty?
|
|
345
|
+
|
|
346
|
+
all_candidates.min_by { |_, specs| specs[:cost_per_1k_tokens][:input] }&.first
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Get detailed information about the current selection criteria
|
|
350
|
+
#
|
|
351
|
+
# @return [Hash] Hash containing requirements, strategy, and provider preferences
|
|
352
|
+
def selection_criteria
|
|
353
|
+
{
|
|
354
|
+
requirements: deep_dup(@requirements),
|
|
355
|
+
strategy: @strategy,
|
|
356
|
+
provider_preferences: deep_dup(@provider_preferences),
|
|
357
|
+
fallback_options: deep_dup(@fallback_options)
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Estimate cost for a given model with expected token usage
|
|
362
|
+
#
|
|
363
|
+
# @param model [String] Model ID
|
|
364
|
+
# @param input_tokens [Integer] Expected input tokens
|
|
365
|
+
# @param output_tokens [Integer] Expected output tokens
|
|
366
|
+
# @return [Float] Estimated cost in dollars
|
|
367
|
+
def estimate_cost(model, input_tokens: 1000, output_tokens: 1000)
|
|
368
|
+
ModelRegistry.calculate_estimated_cost(model, input_tokens:, output_tokens:)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
private
|
|
372
|
+
|
|
373
|
+
# Deep duplicate a hash or array to avoid shared references
|
|
374
|
+
def deep_dup(obj)
|
|
375
|
+
case obj
|
|
376
|
+
when Hash
|
|
377
|
+
obj.transform_values { |v| deep_dup(v) }
|
|
378
|
+
when Array
|
|
379
|
+
obj.map { |item| deep_dup(item) }
|
|
380
|
+
else
|
|
381
|
+
obj
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Filter candidates by provider preferences
|
|
386
|
+
def filter_by_providers(candidates)
|
|
387
|
+
return candidates if @provider_preferences.empty?
|
|
388
|
+
|
|
389
|
+
filtered = candidates.dup
|
|
390
|
+
|
|
391
|
+
# Apply required providers filter (hard requirement)
|
|
392
|
+
if @provider_preferences[:required]
|
|
393
|
+
required_providers = @provider_preferences[:required]
|
|
394
|
+
filtered = filtered.select do |model_id, _|
|
|
395
|
+
provider = extract_provider_from_model_id(model_id)
|
|
396
|
+
required_providers.include?(provider)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Apply avoided providers filter
|
|
401
|
+
if @provider_preferences[:avoided]
|
|
402
|
+
avoided_providers = @provider_preferences[:avoided]
|
|
403
|
+
filtered = filtered.reject do |model_id, _|
|
|
404
|
+
provider = extract_provider_from_model_id(model_id)
|
|
405
|
+
avoided_providers.include?(provider)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Apply avoided patterns filter
|
|
410
|
+
if @provider_preferences[:avoided_patterns]
|
|
411
|
+
patterns = @provider_preferences[:avoided_patterns]
|
|
412
|
+
filtered = filtered.reject do |model_id, _|
|
|
413
|
+
patterns.any? { |pattern| File.fnmatch(pattern, model_id) }
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
filtered
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Extract provider name from model ID (e.g., "anthropic/claude-3" -> "anthropic")
|
|
421
|
+
def extract_provider_from_model_id(model_id)
|
|
422
|
+
model_id.split("/").first
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Apply strategy-specific sorting and return best match
|
|
426
|
+
def apply_strategy_sorting(candidates)
|
|
427
|
+
case @strategy
|
|
428
|
+
when :cost
|
|
429
|
+
candidates.min_by { |_, specs| specs[:cost_per_1k_tokens][:input] }
|
|
430
|
+
when :performance
|
|
431
|
+
# Prefer premium tier, then by cost within tier
|
|
432
|
+
candidates.min_by do |_, specs|
|
|
433
|
+
[specs[:performance_tier] == :premium ? 0 : 1, specs[:cost_per_1k_tokens][:input]]
|
|
434
|
+
end
|
|
435
|
+
when :latest
|
|
436
|
+
candidates.max_by { |_, specs| (specs[:created_at] || 0).to_i }
|
|
437
|
+
when :context
|
|
438
|
+
candidates.max_by { |_, specs| (specs[:context_length] || 0).to_i }
|
|
439
|
+
else
|
|
440
|
+
candidates.min_by { |_, specs| specs[:cost_per_1k_tokens][:input] }
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Apply strategy-specific sorting and return all sorted candidates
|
|
445
|
+
def apply_strategy_sorting_all(candidates)
|
|
446
|
+
case @strategy
|
|
447
|
+
when :cost
|
|
448
|
+
candidates.sort_by { |_, specs| specs[:cost_per_1k_tokens][:input] }
|
|
449
|
+
when :performance
|
|
450
|
+
candidates.sort_by do |_, specs|
|
|
451
|
+
[specs[:performance_tier] == :premium ? 0 : 1, specs[:cost_per_1k_tokens][:input]]
|
|
452
|
+
end
|
|
453
|
+
when :latest
|
|
454
|
+
candidates.sort_by { |_, specs| -(specs[:created_at] || 0).to_i }
|
|
455
|
+
when :context
|
|
456
|
+
candidates.sort_by { |_, specs| -(specs[:context_length] || 0).to_i }
|
|
457
|
+
else
|
|
458
|
+
candidates.sort_by { |_, specs| specs[:cost_per_1k_tokens][:input] }
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|