open_router_enhanced 1.2.2 → 1.2.3

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.
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Dynamic Model Switching & Lookup Example
5
+ # =========================================
6
+ # This example demonstrates how to dynamically select and switch between models
7
+ # based on requirements, capabilities, cost, and performance considerations.
8
+ #
9
+ # Run with: ruby -I lib examples/dynamic_model_switching_example.rb
10
+
11
+ require "open_router"
12
+ require "json"
13
+
14
+ # Configure the client
15
+ OpenRouter.configure do |config|
16
+ config.access_token = ENV.fetch("OPENROUTER_API_KEY") do
17
+ abort "Please set OPENROUTER_API_KEY environment variable"
18
+ end
19
+ config.site_name = "Model Switching Examples"
20
+ end
21
+
22
+ client = OpenRouter::Client.new
23
+
24
+ puts "=" * 60
25
+ puts "DYNAMIC MODEL SWITCHING & LOOKUP"
26
+ puts "=" * 60
27
+
28
+ # -----------------------------------------------------------------------------
29
+ # Example 1: Browse Available Models
30
+ # -----------------------------------------------------------------------------
31
+ puts "\n1. BROWSING AVAILABLE MODELS"
32
+ puts "-" * 40
33
+
34
+ # Get all models from the registry
35
+ all_models = OpenRouter::ModelRegistry.all_models
36
+ puts "Total models available: #{all_models.count}"
37
+
38
+ # Show a few random models
39
+ puts "\nSample of available models:"
40
+ all_models.keys.sample(5).each do |model_id|
41
+ info = all_models[model_id]
42
+ puts " - #{model_id}"
43
+ puts " Cost: $#{info[:cost_per_1k_tokens][:input]}/1k input, $#{info[:cost_per_1k_tokens][:output]}/1k output"
44
+ puts " Context: #{info[:context_length]} tokens"
45
+ puts " Capabilities: #{info[:capabilities].join(", ")}"
46
+ end
47
+
48
+ # -----------------------------------------------------------------------------
49
+ # Example 2: Check Model Capabilities
50
+ # -----------------------------------------------------------------------------
51
+ puts "\n\n2. CHECKING MODEL CAPABILITIES"
52
+ puts "-" * 40
53
+
54
+ models_to_check = %w[
55
+ openai/gpt-4o-mini
56
+ anthropic/claude-3-5-sonnet
57
+ google/gemini-2.0-flash-001
58
+ ]
59
+
60
+ models_to_check.each do |model_id|
61
+ if OpenRouter::ModelRegistry.model_exists?(model_id)
62
+ info = OpenRouter::ModelRegistry.get_model_info(model_id)
63
+ puts "\n#{model_id}:"
64
+ puts " Function calling: #{info[:capabilities].include?(:function_calling)}"
65
+ puts " Structured outputs: #{info[:capabilities].include?(:structured_outputs)}"
66
+ puts " Vision: #{info[:capabilities].include?(:vision)}"
67
+ puts " Long context: #{info[:capabilities].include?(:long_context)}"
68
+ else
69
+ puts "\n#{model_id}: Not found in registry"
70
+ end
71
+ end
72
+
73
+ # -----------------------------------------------------------------------------
74
+ # Example 3: Find Cheapest Model with Capabilities
75
+ # -----------------------------------------------------------------------------
76
+ puts "\n\n3. FIND CHEAPEST MODEL FOR TASK"
77
+ puts "-" * 40
78
+
79
+ # Task: Need function calling, optimize for cost
80
+ model = OpenRouter::ModelSelector.new
81
+ .require(:function_calling)
82
+ .optimize_for(:cost)
83
+ .choose
84
+
85
+ puts "Cheapest model with function calling: #{model}"
86
+
87
+ if model
88
+ info = OpenRouter::ModelRegistry.get_model_info(model)
89
+ puts " Cost: $#{info[:cost_per_1k_tokens][:input]}/1k input"
90
+ end
91
+
92
+ # Task: Need vision + function calling
93
+ model = OpenRouter::ModelSelector.new
94
+ .require(:function_calling, :vision)
95
+ .optimize_for(:cost)
96
+ .choose
97
+
98
+ puts "\nCheapest model with vision + function calling: #{model || "None found"}"
99
+
100
+ # -----------------------------------------------------------------------------
101
+ # Example 4: Budget-Constrained Selection
102
+ # -----------------------------------------------------------------------------
103
+ puts "\n\n4. BUDGET-CONSTRAINED MODEL SELECTION"
104
+ puts "-" * 40
105
+
106
+ # Find models under specific price point
107
+ model = OpenRouter::ModelSelector.new
108
+ .require(:function_calling)
109
+ .within_budget(max_cost: 0.0005) # Under $0.50 per million tokens
110
+ .optimize_for(:performance)
111
+ .choose
112
+
113
+ puts "Best performing model under $0.50/M tokens: #{model || "None found"}"
114
+
115
+ # Get cost estimate for a typical request
116
+ if model
117
+ estimated_cost = OpenRouter::ModelSelector.new.estimate_cost(
118
+ model,
119
+ input_tokens: 1000,
120
+ output_tokens: 500
121
+ )
122
+ puts " Estimated cost for 1k in / 500 out: $#{"%.6f" % estimated_cost}"
123
+ end
124
+
125
+ # -----------------------------------------------------------------------------
126
+ # Example 5: Provider-Based Selection
127
+ # -----------------------------------------------------------------------------
128
+ puts "\n\n5. PROVIDER-BASED SELECTION"
129
+ puts "-" * 40
130
+
131
+ # Prefer Anthropic models
132
+ model = OpenRouter::ModelSelector.new
133
+ .require(:function_calling)
134
+ .prefer_providers("anthropic")
135
+ .optimize_for(:cost)
136
+ .choose
137
+
138
+ puts "Preferred Anthropic model: #{model || "None found"}"
139
+
140
+ # Require only OpenAI models
141
+ model = OpenRouter::ModelSelector.new
142
+ .require(:function_calling)
143
+ .require_providers("openai")
144
+ .optimize_for(:cost)
145
+ .choose
146
+
147
+ puts "Required OpenAI model: #{model || "None found"}"
148
+
149
+ # Avoid certain providers
150
+ model = OpenRouter::ModelSelector.new
151
+ .require(:function_calling)
152
+ .avoid_providers("google", "meta-llama")
153
+ .avoid_patterns("*-free", "*-preview")
154
+ .optimize_for(:cost)
155
+ .choose
156
+
157
+ puts "Model avoiding Google/Meta and free/preview: #{model || "None found"}"
158
+
159
+ # -----------------------------------------------------------------------------
160
+ # Example 6: Get Fallback Options
161
+ # -----------------------------------------------------------------------------
162
+ puts "\n\n6. FALLBACK MODEL SELECTION"
163
+ puts "-" * 40
164
+
165
+ # Get top 3 models for fallback strategy
166
+ models = OpenRouter::ModelSelector.new
167
+ .require(:function_calling)
168
+ .optimize_for(:cost)
169
+ .choose_with_fallbacks(limit: 3)
170
+
171
+ puts "Top 3 fallback models (by cost):"
172
+ models.each_with_index do |model_id, i|
173
+ info = OpenRouter::ModelRegistry.get_model_info(model_id)
174
+ puts " #{i + 1}. #{model_id} ($#{info[:cost_per_1k_tokens][:input]}/1k)"
175
+ end
176
+
177
+ # Graceful degradation - will drop requirements if needed
178
+ model = OpenRouter::ModelSelector.new
179
+ .require(:function_calling, :vision, :long_context)
180
+ .within_budget(max_cost: 0.0001) # Very tight budget
181
+ .optimize_for(:cost)
182
+ .choose_with_fallback # Drops requirements progressively
183
+
184
+ puts "\nWith graceful degradation: #{model || "None found"}"
185
+
186
+ # -----------------------------------------------------------------------------
187
+ # Example 7: Runtime Model Switching
188
+ # -----------------------------------------------------------------------------
189
+ puts "\n\n7. RUNTIME MODEL SWITCHING"
190
+ puts "-" * 40
191
+
192
+ def smart_complete(client, messages, requirements: {}, budget: nil)
193
+ # Build selector based on requirements
194
+ selector = OpenRouter::ModelSelector.new
195
+
196
+ requirements.each do |cap|
197
+ selector = selector.require(cap)
198
+ end
199
+
200
+ selector = selector.within_budget(max_cost: budget) if budget
201
+ selector = selector.optimize_for(:cost)
202
+
203
+ # Get model with fallbacks
204
+ models = selector.choose_with_fallbacks(limit: 3)
205
+
206
+ if models.empty?
207
+ puts " No models match requirements, using fallback strategy..."
208
+ models = [selector.choose_with_fallback].compact
209
+ end
210
+
211
+ return nil if models.empty?
212
+
213
+ # Try each model until one succeeds
214
+ models.each do |model|
215
+ puts " Trying: #{model}"
216
+ response = client.complete(messages, model: model)
217
+ puts " Success with: #{model}"
218
+ return response
219
+ rescue OpenRouter::Error => e
220
+ puts " Failed with #{model}: #{e.message}"
221
+ next
222
+ end
223
+
224
+ nil
225
+ end
226
+
227
+ # Simple request - use cheapest model
228
+ puts "\nSimple question:"
229
+ response = smart_complete(
230
+ client,
231
+ [{ role: "user", content: "What is 2+2?" }],
232
+ requirements: [:chat]
233
+ )
234
+ puts "Answer: #{response&.content&.slice(0, 100)}..."
235
+
236
+ # Complex request - need function calling
237
+ puts "\nRequest needing tools:"
238
+ smart_complete(
239
+ client,
240
+ [{ role: "user", content: "Help me plan a trip" }],
241
+ requirements: [:function_calling],
242
+ budget: 0.001
243
+ )
244
+
245
+ # -----------------------------------------------------------------------------
246
+ # Example 8: Context-Aware Model Selection
247
+ # -----------------------------------------------------------------------------
248
+ puts "\n\n8. CONTEXT-AWARE SELECTION"
249
+ puts "-" * 40
250
+
251
+ def select_model_for_content(content_length)
252
+ # Estimate tokens (rough: 4 chars per token)
253
+ estimated_tokens = content_length / 4
254
+
255
+ selector = OpenRouter::ModelSelector.new.require(:chat)
256
+
257
+ if estimated_tokens > 100_000
258
+ puts " Long content detected, requiring 200k+ context..."
259
+ selector = selector.min_context(200_000)
260
+ elsif estimated_tokens > 30_000
261
+ puts " Medium content, requiring 50k+ context..."
262
+ selector = selector.min_context(50_000)
263
+ end
264
+
265
+ selector.optimize_for(:cost).choose
266
+ end
267
+
268
+ # Test with different content sizes
269
+ [1000, 50_000, 500_000].each do |chars|
270
+ puts "\nContent size: #{chars} characters"
271
+ model = select_model_for_content(chars)
272
+ if model
273
+ info = OpenRouter::ModelRegistry.get_model_info(model)
274
+ puts " Selected: #{model} (#{info[:context_length]} context)"
275
+ end
276
+ end
277
+
278
+ # -----------------------------------------------------------------------------
279
+ # Example 9: Cost Comparison
280
+ # -----------------------------------------------------------------------------
281
+ puts "\n\n9. COST COMPARISON ACROSS MODELS"
282
+ puts "-" * 40
283
+
284
+ # Get several models with function calling
285
+ models = OpenRouter::ModelSelector.new
286
+ .require(:function_calling)
287
+ .optimize_for(:cost)
288
+ .choose_with_fallbacks(limit: 10)
289
+
290
+ puts "Cost comparison for 10k input + 2k output tokens:\n"
291
+
292
+ costs = models.map do |model_id|
293
+ cost = OpenRouter::ModelSelector.new.estimate_cost(
294
+ model_id,
295
+ input_tokens: 10_000,
296
+ output_tokens: 2_000
297
+ )
298
+ [model_id, cost]
299
+ end
300
+
301
+ costs.sort_by(&:last).each do |model_id, cost|
302
+ puts " $#{"%.4f" % cost} - #{model_id}"
303
+ end
304
+
305
+ # -----------------------------------------------------------------------------
306
+ # Example 10: View Selection Criteria
307
+ # -----------------------------------------------------------------------------
308
+ puts "\n\n10. INTROSPECTING SELECTION CRITERIA"
309
+ puts "-" * 40
310
+
311
+ selector = OpenRouter::ModelSelector.new
312
+ .require(:function_calling, :structured_outputs)
313
+ .within_budget(max_cost: 0.01)
314
+ .prefer_providers("anthropic", "openai")
315
+ .avoid_patterns("*-free")
316
+ .optimize_for(:performance)
317
+
318
+ criteria = selector.selection_criteria
319
+
320
+ puts "Current selection criteria:"
321
+ puts JSON.pretty_generate(criteria)
322
+
323
+ model = selector.choose
324
+ puts "\nSelected model: #{model}"
325
+
326
+ puts "\n#{"=" * 60}"
327
+ puts "Examples complete!"
328
+ puts "=" * 60
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Real-World Structured Outputs Example
5
+ # =====================================
6
+ # This example demonstrates practical uses of schemas for extracting
7
+ # structured data from unstructured text, with validation and error handling.
8
+ #
9
+ # Run with: ruby -I lib examples/real_world_schemas_example.rb
10
+
11
+ require "open_router"
12
+ require "json"
13
+
14
+ # Configure the client
15
+ OpenRouter.configure do |config|
16
+ config.access_token = ENV.fetch("OPENROUTER_API_KEY") do
17
+ abort "Please set OPENROUTER_API_KEY environment variable"
18
+ end
19
+ config.site_name = "Schema Examples"
20
+ end
21
+
22
+ client = OpenRouter::Client.new
23
+
24
+ # Use a model with native structured output support
25
+ MODEL = "openai/gpt-4o-mini"
26
+
27
+ puts "=" * 60
28
+ puts "REAL-WORLD STRUCTURED OUTPUTS EXAMPLES"
29
+ puts "=" * 60
30
+
31
+ # -----------------------------------------------------------------------------
32
+ # Example 1: Extract Job Posting Details
33
+ # -----------------------------------------------------------------------------
34
+ puts "\n1. EXTRACTING JOB POSTING DETAILS"
35
+ puts "-" * 40
36
+
37
+ job_posting_schema = OpenRouter::Schema.define("job_posting") do
38
+ string :title, required: true, description: "Job title"
39
+ string :company, required: true, description: "Company name"
40
+ string :location, required: true, description: "Job location (city, state or 'Remote')"
41
+ boolean :remote_friendly, description: "Whether remote work is allowed"
42
+ integer :salary_min, description: "Minimum salary in USD"
43
+ integer :salary_max, description: "Maximum salary in USD"
44
+ array :required_skills, required: true do
45
+ string
46
+ end
47
+ array :nice_to_have_skills do
48
+ string
49
+ end
50
+ string :experience_level, enum: %w[junior mid senior lead principal]
51
+ end
52
+
53
+ job_text = <<~TEXT
54
+ Senior Ruby Engineer at TechCorp
55
+
56
+ We're looking for an experienced Ruby developer to join our platform team
57
+ in San Francisco (hybrid - 2 days in office). Salary range: $180,000-$220,000.
58
+
59
+ Requirements:
60
+ - 5+ years Ruby experience
61
+ - Strong Rails knowledge
62
+ - PostgreSQL expertise
63
+ - Experience with background job processing
64
+
65
+ Nice to have:
66
+ - Kubernetes experience
67
+ - GraphQL
68
+ - Previous startup experience
69
+ TEXT
70
+
71
+ response = client.complete(
72
+ [{ role: "user", content: "Extract the job details from this posting:\n\n#{job_text}" }],
73
+ model: MODEL,
74
+ response_format: job_posting_schema
75
+ )
76
+
77
+ job = response.structured_output
78
+ puts "Title: #{job["title"]}"
79
+ puts "Company: #{job["company"]}"
80
+ puts "Location: #{job["location"]}"
81
+ puts "Remote: #{job["remote_friendly"]}"
82
+ puts "Salary: $#{job["salary_min"]&.to_s || "?"} - $#{job["salary_max"]&.to_s || "?"}"
83
+ puts "Skills: #{job["required_skills"]&.join(", ")}"
84
+ puts "Level: #{job["experience_level"]}"
85
+
86
+ # -----------------------------------------------------------------------------
87
+ # Example 2: Nested Schema - Order with Line Items
88
+ # -----------------------------------------------------------------------------
89
+ puts "\n\n2. NESTED SCHEMA: ORDER WITH LINE ITEMS"
90
+ puts "-" * 40
91
+
92
+ order_schema = OpenRouter::Schema.define("order") do
93
+ string :order_id, required: true
94
+ string :customer_name, required: true
95
+ string :customer_email, required: true
96
+
97
+ object :shipping_address, required: true do
98
+ string :street, required: true
99
+ string :city, required: true
100
+ string :state, required: true
101
+ string :zip_code, required: true
102
+ string :country, required: true
103
+ end
104
+
105
+ array :line_items, required: true do
106
+ object do
107
+ string :product_name, required: true
108
+ integer :quantity, required: true
109
+ number :unit_price, required: true
110
+ number :total, required: true
111
+ end
112
+ end
113
+
114
+ number :subtotal, required: true
115
+ number :tax, required: true
116
+ number :total, required: true
117
+ string :status, enum: %w[pending confirmed shipped delivered]
118
+ end
119
+
120
+ order_email = <<~TEXT
121
+ Order Confirmation #ORD-2024-5847
122
+
123
+ Dear John Smith (john.smith@email.com),
124
+
125
+ Thank you for your order! Here are the details:
126
+
127
+ Ship to:
128
+ 123 Main Street
129
+ Austin, TX 78701
130
+ United States
131
+
132
+ Items:
133
+ - Mechanical Keyboard (x1) - $149.99 each = $149.99
134
+ - USB-C Hub (x2) - $39.99 each = $79.98
135
+ - Mouse Pad XL (x1) - $24.99 each = $24.99
136
+
137
+ Subtotal: $254.96
138
+ Tax (8.25%): $21.03
139
+ Total: $275.99
140
+
141
+ Your order has been confirmed and will ship soon!
142
+ TEXT
143
+
144
+ response = client.complete(
145
+ [{ role: "user", content: "Parse this order confirmation email into structured data:\n\n#{order_email}" }],
146
+ model: MODEL,
147
+ response_format: order_schema
148
+ )
149
+
150
+ order = response.structured_output
151
+ puts "Order: #{order["order_id"]}"
152
+ puts "Customer: #{order["customer_name"]} (#{order["customer_email"]})"
153
+ puts "Ship to: #{order.dig("shipping_address", "city")}, #{order.dig("shipping_address", "state")}"
154
+ puts "\nLine Items:"
155
+ order["line_items"]&.each do |item|
156
+ puts " - #{item["product_name"]} x#{item["quantity"]} = $#{"%.2f" % item["total"]}"
157
+ end
158
+ puts "\nTotal: $#{"%.2f" % order["total"]} (including $#{"%.2f" % order["tax"]} tax)"
159
+
160
+ # -----------------------------------------------------------------------------
161
+ # Example 3: Schema Validation
162
+ # -----------------------------------------------------------------------------
163
+ puts "\n\n3. SCHEMA VALIDATION"
164
+ puts "-" * 40
165
+
166
+ # Create a simple schema
167
+ validation_schema = OpenRouter::Schema.define("contact") do
168
+ string :name, required: true
169
+ string :email, required: true
170
+ integer :age
171
+ end
172
+
173
+ # Valid data
174
+ valid_data = { "name" => "Alice", "email" => "alice@example.com", "age" => 30 }
175
+ puts "Valid data: #{validation_schema.validate(valid_data)}"
176
+
177
+ # Invalid data (missing required field)
178
+ invalid_data = { "name" => "Bob" }
179
+ puts "Invalid data (missing email): #{validation_schema.validate(invalid_data)}"
180
+
181
+ # Get detailed errors
182
+ errors = validation_schema.validation_errors(invalid_data)
183
+ puts "Validation errors: #{errors.inspect}" if errors.any?
184
+
185
+ # -----------------------------------------------------------------------------
186
+ # Example 4: Sentiment Analysis with Confidence
187
+ # -----------------------------------------------------------------------------
188
+ puts "\n\n4. SENTIMENT ANALYSIS WITH CONFIDENCE"
189
+ puts "-" * 40
190
+
191
+ sentiment_schema = OpenRouter::Schema.define("sentiment_analysis") do
192
+ string :sentiment, required: true, enum: %w[positive negative neutral mixed]
193
+ number :confidence, required: true, description: "Confidence score from 0.0 to 1.0"
194
+ array :key_phrases, required: true do
195
+ string
196
+ end
197
+ string :summary, required: true, description: "Brief explanation of the sentiment"
198
+ end
199
+
200
+ reviews = [
201
+ "This product exceeded my expectations! Fast shipping, great quality, will buy again.",
202
+ "Terrible experience. Arrived broken and customer service was unhelpful.",
203
+ "It's okay. Does what it says but nothing special. Fair price I guess."
204
+ ]
205
+
206
+ reviews.each_with_index do |review, i|
207
+ response = client.complete(
208
+ [{ role: "user", content: "Analyze the sentiment of this review:\n\n#{review}" }],
209
+ model: MODEL,
210
+ response_format: sentiment_schema
211
+ )
212
+
213
+ result = response.structured_output
214
+ puts "\nReview #{i + 1}: \"#{review[0..50]}...\""
215
+ puts " Sentiment: #{result["sentiment"]} (#{(result["confidence"] * 100).round}% confident)"
216
+ puts " Key phrases: #{result["key_phrases"]&.join(", ")}"
217
+ end
218
+
219
+ # -----------------------------------------------------------------------------
220
+ # Example 5: Using Auto-Healing for Non-Native Models
221
+ # -----------------------------------------------------------------------------
222
+ puts "\n\n5. AUTO-HEALING FOR MALFORMED RESPONSES"
223
+ puts "-" * 40
224
+
225
+ # Enable auto-healing in configuration
226
+ OpenRouter.configure do |config|
227
+ config.auto_heal_responses = true
228
+ config.healer_model = "openai/gpt-4o-mini"
229
+ config.max_heal_attempts = 2
230
+ end
231
+
232
+ simple_schema = OpenRouter::Schema.define("extraction") do
233
+ string :name, required: true
234
+ integer :count, required: true
235
+ end
236
+
237
+ # The response will attempt to heal if the model returns malformed JSON
238
+ response = client.complete(
239
+ [{ role: "user", content: "Extract: There are 42 widgets made by Acme Corp" }],
240
+ model: MODEL,
241
+ response_format: simple_schema
242
+ )
243
+
244
+ if response.valid_structured_output?
245
+ puts "Extraction successful: #{response.structured_output}"
246
+ else
247
+ puts "Validation failed: #{response.validation_errors}"
248
+ end
249
+
250
+ # Show response metadata
251
+ puts "\nResponse metadata:"
252
+ puts " Model: #{response.model}"
253
+ puts " Tokens: #{response.usage["total_tokens"]} total"
254
+ puts " Was healed: #{begin
255
+ response.healed?
256
+ rescue StandardError
257
+ "N/A"
258
+ end}"
259
+
260
+ puts "\n#{"=" * 60}"
261
+ puts "Examples complete!"
262
+ puts "=" * 60