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,637 @@
|
|
|
1
|
+
# Model Selection
|
|
2
|
+
|
|
3
|
+
The OpenRouter gem includes sophisticated model selection capabilities that help you automatically choose the best AI model based on your specific requirements. The system combines intelligent model registry caching with a fluent DSL for expressing selection criteria.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Find the cheapest model that supports function calling
|
|
9
|
+
model = OpenRouter::ModelSelector.new
|
|
10
|
+
.require(:function_calling)
|
|
11
|
+
.optimize_for(:cost)
|
|
12
|
+
.choose
|
|
13
|
+
|
|
14
|
+
# Use the selected model
|
|
15
|
+
response = client.complete(messages, model: model, tools: tools)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## ModelSelector API
|
|
19
|
+
|
|
20
|
+
The `ModelSelector` class provides a fluent interface for building complex model selection criteria.
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
selector = OpenRouter::ModelSelector.new
|
|
26
|
+
|
|
27
|
+
# Method chaining for requirements
|
|
28
|
+
model = selector.require(:function_calling, :vision)
|
|
29
|
+
.within_budget(max_cost: 0.01)
|
|
30
|
+
.prefer_providers("anthropic", "openai")
|
|
31
|
+
.optimize_for(:performance)
|
|
32
|
+
.choose
|
|
33
|
+
|
|
34
|
+
puts model # => "anthropic/claude-3.5-sonnet"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Optimization Strategies
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# Optimize for cost - choose cheapest input token cost
|
|
41
|
+
model = selector.optimize_for(:cost).choose
|
|
42
|
+
|
|
43
|
+
# Optimize for performance - prefer premium tier, then by cost
|
|
44
|
+
model = selector.optimize_for(:performance).choose
|
|
45
|
+
|
|
46
|
+
# Optimize for latest - prefer most recent model
|
|
47
|
+
model = selector.optimize_for(:latest).choose
|
|
48
|
+
|
|
49
|
+
# Optimize for context - prefer largest context window
|
|
50
|
+
model = selector.optimize_for(:context).choose
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Note**: Sorting is nil-safe for `created_at` and `context_length`.
|
|
54
|
+
|
|
55
|
+
### Capability Requirements
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# Require specific capabilities
|
|
59
|
+
selector.require(:function_calling) # Single capability
|
|
60
|
+
selector.require(:function_calling, :vision) # Multiple capabilities
|
|
61
|
+
selector.require(:structured_outputs, :long_context) # Mix and match
|
|
62
|
+
|
|
63
|
+
# Available capabilities:
|
|
64
|
+
# :function_calling - Tool/function calling support
|
|
65
|
+
# :structured_outputs - JSON schema response formatting
|
|
66
|
+
# :vision - Image input processing
|
|
67
|
+
# :long_context - Large context windows (100k+ tokens)
|
|
68
|
+
# :chat - Basic chat completion (all models have this)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Budget Constraints
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Set maximum costs per 1k tokens
|
|
75
|
+
selector.within_budget(max_cost: 0.01) # Input tokens only
|
|
76
|
+
selector.within_budget(max_cost: 0.01, max_output_cost: 0.02) # Both input and output
|
|
77
|
+
|
|
78
|
+
# Example: Find cheapest model under $0.005 per 1k input tokens
|
|
79
|
+
model = OpenRouter::ModelSelector.new
|
|
80
|
+
.within_budget(max_cost: 0.005)
|
|
81
|
+
.require(:function_calling)
|
|
82
|
+
.choose
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Context Length Requirements
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Require minimum context length
|
|
89
|
+
selector.min_context(50_000) # 50k tokens minimum
|
|
90
|
+
selector.min_context(200_000) # 200k tokens minimum
|
|
91
|
+
|
|
92
|
+
# Find model with large context for document processing
|
|
93
|
+
model = OpenRouter::ModelSelector.new
|
|
94
|
+
.min_context(100_000)
|
|
95
|
+
.optimize_for(:cost)
|
|
96
|
+
.choose
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Provider Preferences and Filtering
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# Soft preference - records preference but doesn't currently affect ordering
|
|
103
|
+
selector.prefer_providers("anthropic", "openai")
|
|
104
|
+
|
|
105
|
+
# Hard requirement - only these providers
|
|
106
|
+
selector.require_providers("anthropic")
|
|
107
|
+
|
|
108
|
+
# Avoid specific providers
|
|
109
|
+
selector.avoid_providers("google", "meta")
|
|
110
|
+
|
|
111
|
+
# Avoid model patterns (glob syntax)
|
|
112
|
+
selector.avoid_patterns("*-free", "*-preview", "*-alpha")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Note**: `prefer_providers` is available to record preferences but does not currently affect ordering. Filtering is enforced via `require_providers`/`avoid_providers`/`avoid_patterns`.
|
|
116
|
+
|
|
117
|
+
### Release Date Filtering
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# Only models released after a specific date
|
|
121
|
+
selector.newer_than(Date.new(2024, 1, 1))
|
|
122
|
+
selector.newer_than(Time.now - 30.days)
|
|
123
|
+
selector.newer_than(1704067200) # Unix timestamp
|
|
124
|
+
|
|
125
|
+
# Find latest models from this year
|
|
126
|
+
model = OpenRouter::ModelSelector.new
|
|
127
|
+
.newer_than(Date.new(2024, 1, 1))
|
|
128
|
+
.optimize_for(:latest)
|
|
129
|
+
.choose
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Selection Methods
|
|
133
|
+
|
|
134
|
+
### Single Model Selection
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Get just the model ID
|
|
138
|
+
model = selector.choose
|
|
139
|
+
# => "anthropic/claude-3.5-sonnet"
|
|
140
|
+
|
|
141
|
+
# Get model ID with detailed specs
|
|
142
|
+
model, specs = selector.choose(return_specs: true)
|
|
143
|
+
# => ["anthropic/claude-3.5-sonnet", { capabilities: [...], cost_per_1k_tokens: {...}, ... }]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Multiple Models with Fallbacks
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Get multiple models in order of preference
|
|
150
|
+
models = selector.choose_with_fallbacks(limit: 3)
|
|
151
|
+
# => ["anthropic/claude-3.5-sonnet", "openai/gpt-4o", "anthropic/claude-3-haiku"]
|
|
152
|
+
|
|
153
|
+
# Use first model, fall back to others if needed
|
|
154
|
+
models.each do |model|
|
|
155
|
+
begin
|
|
156
|
+
response = client.complete(messages, model: model)
|
|
157
|
+
break
|
|
158
|
+
rescue OpenRouter::ServerError => e
|
|
159
|
+
puts "Model #{model} failed: #{e.message}"
|
|
160
|
+
next
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Graceful Degradation
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# choose_with_fallback drops least important requirements progressively if no match
|
|
169
|
+
model = selector.require(:function_calling, :vision)
|
|
170
|
+
.within_budget(max_cost: 0.001) # Very strict budget
|
|
171
|
+
.choose_with_fallback
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Drop order**:
|
|
175
|
+
1. `released_after_date`
|
|
176
|
+
2. `performance_tier`
|
|
177
|
+
3. `max_output_cost`
|
|
178
|
+
4. `min_context_length`
|
|
179
|
+
5. `max_input_cost`
|
|
180
|
+
6. Keep only capability requirements
|
|
181
|
+
7. Otherwise choose the cheapest available model
|
|
182
|
+
|
|
183
|
+
## ModelRegistry
|
|
184
|
+
|
|
185
|
+
The underlying model registry provides direct access to model data and capabilities.
|
|
186
|
+
|
|
187
|
+
### Model Information
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# Get all available models
|
|
191
|
+
all_models = OpenRouter::ModelRegistry.all_models
|
|
192
|
+
# => { "anthropic/claude-3.5-sonnet" => { capabilities: [...], cost_per_1k_tokens: {...}, ...}, ... }
|
|
193
|
+
|
|
194
|
+
# Get specific model information
|
|
195
|
+
model_info = OpenRouter::ModelRegistry.get_model_info("anthropic/claude-3.5-sonnet")
|
|
196
|
+
puts model_info[:capabilities] # => [:chat, :function_calling, :structured_outputs, :vision]
|
|
197
|
+
puts model_info[:cost_per_1k_tokens] # => { input: 0.003, output: 0.015 }
|
|
198
|
+
puts model_info[:context_length] # => 200000
|
|
199
|
+
puts model_info[:performance_tier] # => :premium
|
|
200
|
+
|
|
201
|
+
# Check if model exists
|
|
202
|
+
if OpenRouter::ModelRegistry.model_exists?("anthropic/claude-3.5-sonnet")
|
|
203
|
+
puts "Model is available"
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Cost Estimation
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# Estimate costs for specific token usage
|
|
211
|
+
cost = OpenRouter::ModelRegistry.calculate_estimated_cost(
|
|
212
|
+
"anthropic/claude-3.5-sonnet",
|
|
213
|
+
input_tokens: 1000,
|
|
214
|
+
output_tokens: 500
|
|
215
|
+
)
|
|
216
|
+
puts "Estimated cost: $#{cost.round(4)}" # => "Estimated cost: $0.0105"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Filtering Models
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# Find models matching specific requirements
|
|
223
|
+
candidates = OpenRouter::ModelRegistry.models_meeting_requirements(
|
|
224
|
+
capabilities: [:function_calling, :vision],
|
|
225
|
+
max_input_cost: 0.01,
|
|
226
|
+
min_context_length: 50_000
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
candidates.each do |model_id, specs|
|
|
230
|
+
puts "#{model_id}: $#{specs[:cost_per_1k_tokens][:input]} per 1k tokens"
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Cache Management
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# Refresh model data from API (clears cache)
|
|
238
|
+
OpenRouter::ModelRegistry.refresh!
|
|
239
|
+
|
|
240
|
+
# Clear cache manually
|
|
241
|
+
OpenRouter::ModelRegistry.clear_cache!
|
|
242
|
+
|
|
243
|
+
# Check cache status
|
|
244
|
+
cached_data = OpenRouter::ModelRegistry.load_cached_models
|
|
245
|
+
puts cached_data ? "Cache loaded" : "No cache available"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Advanced Usage Patterns
|
|
249
|
+
|
|
250
|
+
### Cost-Aware Model Selection
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
def select_model_by_budget(requirements, max_monthly_cost:, estimated_monthly_tokens:)
|
|
254
|
+
max_cost_per_1k = (max_monthly_cost / estimated_monthly_tokens) * 1000
|
|
255
|
+
|
|
256
|
+
OpenRouter::ModelSelector.new
|
|
257
|
+
.require(*requirements)
|
|
258
|
+
.within_budget(max_cost: max_cost_per_1k)
|
|
259
|
+
.optimize_for(:cost)
|
|
260
|
+
.choose
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Usage
|
|
264
|
+
model = select_model_by_budget(
|
|
265
|
+
[:function_calling],
|
|
266
|
+
max_monthly_cost: 100.0, # $100 monthly budget
|
|
267
|
+
estimated_monthly_tokens: 1_000_000 # 1M tokens per month
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Capability-First Selection
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
def select_best_model_for_task(task_type)
|
|
275
|
+
requirements = case task_type
|
|
276
|
+
when :data_extraction
|
|
277
|
+
[:structured_outputs]
|
|
278
|
+
when :function_calling
|
|
279
|
+
[:function_calling]
|
|
280
|
+
when :document_analysis
|
|
281
|
+
[:vision, :long_context]
|
|
282
|
+
when :code_generation
|
|
283
|
+
[:function_calling, :long_context]
|
|
284
|
+
else
|
|
285
|
+
[]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
OpenRouter::ModelSelector.new
|
|
289
|
+
.require(*requirements)
|
|
290
|
+
.optimize_for(:performance)
|
|
291
|
+
.choose_with_fallback
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Provider Rotation
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
class ModelRotator
|
|
299
|
+
def initialize(providers:, requirements: [])
|
|
300
|
+
@providers = providers
|
|
301
|
+
@requirements = requirements
|
|
302
|
+
@current_index = 0
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def next_model
|
|
306
|
+
provider = @providers[@current_index % @providers.length]
|
|
307
|
+
@current_index += 1
|
|
308
|
+
|
|
309
|
+
OpenRouter::ModelSelector.new
|
|
310
|
+
.require(*@requirements)
|
|
311
|
+
.require_providers(provider)
|
|
312
|
+
.optimize_for(:cost)
|
|
313
|
+
.choose
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Usage
|
|
318
|
+
rotator = ModelRotator.new(
|
|
319
|
+
providers: ["anthropic", "openai", "google"],
|
|
320
|
+
requirements: [:function_calling]
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
3.times do
|
|
324
|
+
model = rotator.next_model
|
|
325
|
+
puts "Using model: #{model}"
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Performance Monitoring
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
class ModelPerformanceTracker
|
|
333
|
+
def initialize
|
|
334
|
+
@performance_data = {}
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def track_completion(model, input_tokens:, output_tokens:, duration:, success:)
|
|
338
|
+
@performance_data[model] ||= {
|
|
339
|
+
calls: 0,
|
|
340
|
+
successes: 0,
|
|
341
|
+
total_duration: 0,
|
|
342
|
+
total_cost: 0
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
data = @performance_data[model]
|
|
346
|
+
data[:calls] += 1
|
|
347
|
+
data[:successes] += 1 if success
|
|
348
|
+
data[:total_duration] += duration
|
|
349
|
+
|
|
350
|
+
cost = OpenRouter::ModelRegistry.calculate_estimated_cost(
|
|
351
|
+
model,
|
|
352
|
+
input_tokens: input_tokens,
|
|
353
|
+
output_tokens: output_tokens
|
|
354
|
+
)
|
|
355
|
+
data[:total_cost] += cost
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def best_performing_model(min_calls: 5)
|
|
359
|
+
eligible_models = @performance_data.select { |_, data| data[:calls] >= min_calls }
|
|
360
|
+
return nil if eligible_models.empty?
|
|
361
|
+
|
|
362
|
+
eligible_models.max_by do |_, data|
|
|
363
|
+
success_rate = data[:successes].to_f / data[:calls]
|
|
364
|
+
avg_duration = data[:total_duration] / data[:calls]
|
|
365
|
+
|
|
366
|
+
# Score based on success rate and speed (higher is better)
|
|
367
|
+
success_rate * 100 - avg_duration
|
|
368
|
+
end&.first
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Integration with Client
|
|
374
|
+
|
|
375
|
+
### Automatic Model Selection
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
class SmartOpenRouterClient < OpenRouter::Client
|
|
379
|
+
def initialize(*args, **kwargs)
|
|
380
|
+
super
|
|
381
|
+
@model_selector = OpenRouter::ModelSelector.new
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def smart_complete(messages, requirements: [], **options)
|
|
385
|
+
unless options[:model]
|
|
386
|
+
options[:model] = @model_selector
|
|
387
|
+
.require(*requirements)
|
|
388
|
+
.optimize_for(:cost)
|
|
389
|
+
.choose_with_fallback
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
complete(messages, **options)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Usage
|
|
397
|
+
client = SmartOpenRouterClient.new
|
|
398
|
+
response = client.smart_complete(
|
|
399
|
+
messages,
|
|
400
|
+
requirements: [:function_calling],
|
|
401
|
+
tools: tools
|
|
402
|
+
)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Fallback Chains
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
def complete_with_fallbacks(messages, **options)
|
|
409
|
+
models = OpenRouter::ModelSelector.new
|
|
410
|
+
.require(:function_calling)
|
|
411
|
+
.optimize_for(:cost)
|
|
412
|
+
.choose_with_fallbacks(limit: 3)
|
|
413
|
+
|
|
414
|
+
last_error = nil
|
|
415
|
+
|
|
416
|
+
models.each do |model|
|
|
417
|
+
begin
|
|
418
|
+
return client.complete(messages, model: model, **options)
|
|
419
|
+
rescue OpenRouter::ServerError => e
|
|
420
|
+
last_error = e
|
|
421
|
+
puts "Model #{model} failed: #{e.message}, trying next..."
|
|
422
|
+
next
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
raise last_error if last_error
|
|
427
|
+
raise OpenRouter::Error, "No models available"
|
|
428
|
+
end
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Configuration and Customization
|
|
432
|
+
|
|
433
|
+
### Custom Selection Criteria
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
class CustomModelSelector < OpenRouter::ModelSelector
|
|
437
|
+
def require_custom_capability(capability_name)
|
|
438
|
+
# Add custom logic for proprietary capability detection
|
|
439
|
+
new_requirements = @requirements.dup
|
|
440
|
+
new_requirements[:custom_capabilities] ||= []
|
|
441
|
+
new_requirements[:custom_capabilities] << capability_name
|
|
442
|
+
|
|
443
|
+
self.class.new(
|
|
444
|
+
requirements: new_requirements,
|
|
445
|
+
strategy: @strategy,
|
|
446
|
+
provider_preferences: @provider_preferences,
|
|
447
|
+
fallback_options: @fallback_options
|
|
448
|
+
)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
private
|
|
452
|
+
|
|
453
|
+
def meets_requirements?(specs, requirements)
|
|
454
|
+
# Call parent implementation first
|
|
455
|
+
return false unless super
|
|
456
|
+
|
|
457
|
+
# Add custom requirement checking
|
|
458
|
+
if requirements[:custom_capabilities]
|
|
459
|
+
# Your custom logic here
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
true
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Environment-Specific Defaults
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
# config/initializers/openrouter.rb
|
|
471
|
+
class OpenRouter::ModelSelector
|
|
472
|
+
class << self
|
|
473
|
+
def production_defaults
|
|
474
|
+
new.avoid_patterns("*-preview", "*-alpha", "*-beta")
|
|
475
|
+
.require_providers("anthropic", "openai") # Trusted providers only
|
|
476
|
+
.optimize_for(:performance)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def development_defaults
|
|
480
|
+
new.optimize_for(:cost)
|
|
481
|
+
.within_budget(max_cost: 0.001) # Keep costs low in dev
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def for_environment
|
|
485
|
+
Rails.env.production? ? production_defaults : development_defaults
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Usage in your app
|
|
491
|
+
model = OpenRouter::ModelSelector.for_environment
|
|
492
|
+
.require(:function_calling)
|
|
493
|
+
.choose
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Best Practices
|
|
497
|
+
|
|
498
|
+
### 1. Cache Selection Results
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
class CachedModelSelector
|
|
502
|
+
def initialize(ttl: 3600) # 1 hour cache
|
|
503
|
+
@cache = {}
|
|
504
|
+
@ttl = ttl
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def select(requirements_hash)
|
|
508
|
+
cache_key = requirements_hash.hash
|
|
509
|
+
cached_result = @cache[cache_key]
|
|
510
|
+
|
|
511
|
+
if cached_result && Time.now - cached_result[:timestamp] < @ttl
|
|
512
|
+
return cached_result[:model]
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
model = build_selector_from_hash(requirements_hash).choose
|
|
516
|
+
@cache[cache_key] = { model: model, timestamp: Time.now }
|
|
517
|
+
model
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
private
|
|
521
|
+
|
|
522
|
+
def build_selector_from_hash(hash)
|
|
523
|
+
selector = OpenRouter::ModelSelector.new
|
|
524
|
+
|
|
525
|
+
selector = selector.require(*hash[:capabilities]) if hash[:capabilities]
|
|
526
|
+
selector = selector.within_budget(max_cost: hash[:max_cost]) if hash[:max_cost]
|
|
527
|
+
selector = selector.optimize_for(hash[:strategy]) if hash[:strategy]
|
|
528
|
+
|
|
529
|
+
selector
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### 2. Monitor Model Availability
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
def check_model_health(models)
|
|
538
|
+
results = {}
|
|
539
|
+
|
|
540
|
+
models.each do |model|
|
|
541
|
+
begin
|
|
542
|
+
# Quick test completion
|
|
543
|
+
response = client.complete(
|
|
544
|
+
[{ role: "user", content: "Say 'OK'" }],
|
|
545
|
+
model: model,
|
|
546
|
+
max_tokens: 5
|
|
547
|
+
)
|
|
548
|
+
results[model] = response.content.include?("OK") ? :healthy : :unhealthy
|
|
549
|
+
rescue => e
|
|
550
|
+
results[model] = :error
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
results
|
|
555
|
+
end
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### 3. Cost Tracking
|
|
559
|
+
|
|
560
|
+
```ruby
|
|
561
|
+
def track_model_costs
|
|
562
|
+
@daily_costs ||= Hash.new(0)
|
|
563
|
+
|
|
564
|
+
before_action do
|
|
565
|
+
@request_start_time = Time.now
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
after_action do
|
|
569
|
+
if @selected_model && @input_tokens && @output_tokens
|
|
570
|
+
cost = OpenRouter::ModelRegistry.calculate_estimated_cost(
|
|
571
|
+
@selected_model,
|
|
572
|
+
input_tokens: @input_tokens,
|
|
573
|
+
output_tokens: @output_tokens
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
date_key = Date.current.to_s
|
|
577
|
+
@daily_costs[date_key] += cost
|
|
578
|
+
|
|
579
|
+
Rails.logger.info "Model cost: #{@selected_model} $#{cost.round(6)}"
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
## Troubleshooting
|
|
586
|
+
|
|
587
|
+
### Common Issues
|
|
588
|
+
|
|
589
|
+
1. **No Models Found**: Your requirements may be too restrictive
|
|
590
|
+
2. **Cache Stale**: Model data might be outdated
|
|
591
|
+
3. **Network Errors**: API connection issues when fetching model data
|
|
592
|
+
4. **Performance**: Selection taking too long with many models
|
|
593
|
+
|
|
594
|
+
### Solutions
|
|
595
|
+
|
|
596
|
+
```ruby
|
|
597
|
+
# 1. Debug selection criteria
|
|
598
|
+
selector = OpenRouter::ModelSelector.new
|
|
599
|
+
.require(:function_calling)
|
|
600
|
+
.within_budget(max_cost: 0.0001) # Very restrictive
|
|
601
|
+
|
|
602
|
+
puts "Selection criteria:"
|
|
603
|
+
puts selector.selection_criteria.inspect
|
|
604
|
+
|
|
605
|
+
# See what models meet each requirement step by step
|
|
606
|
+
all_models = OpenRouter::ModelRegistry.all_models
|
|
607
|
+
puts "Total models: #{all_models.size}"
|
|
608
|
+
|
|
609
|
+
with_capability = OpenRouter::ModelRegistry.models_meeting_requirements(
|
|
610
|
+
capabilities: [:function_calling]
|
|
611
|
+
)
|
|
612
|
+
puts "With function calling: #{with_capability.size}"
|
|
613
|
+
|
|
614
|
+
within_budget = with_capability.select do |_, specs|
|
|
615
|
+
specs[:cost_per_1k_tokens][:input] <= 0.0001
|
|
616
|
+
end
|
|
617
|
+
puts "Within budget: #{within_budget.size}"
|
|
618
|
+
|
|
619
|
+
# 2. Force cache refresh
|
|
620
|
+
OpenRouter::ModelRegistry.refresh!
|
|
621
|
+
|
|
622
|
+
# 3. Handle network errors gracefully
|
|
623
|
+
begin
|
|
624
|
+
models = OpenRouter::ModelRegistry.all_models
|
|
625
|
+
rescue OpenRouter::ModelRegistryError => e
|
|
626
|
+
puts "Using fallback model due to registry error: #{e.message}"
|
|
627
|
+
model = "anthropic/claude-3-haiku" # Known reliable fallback
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# 4. Optimize for performance
|
|
631
|
+
# Use more specific requirements to reduce search space
|
|
632
|
+
model = OpenRouter::ModelSelector.new
|
|
633
|
+
.require_providers("anthropic") # Limit search space
|
|
634
|
+
.require(:function_calling)
|
|
635
|
+
.optimize_for(:cost)
|
|
636
|
+
.choose
|
|
637
|
+
```
|