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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +33 -15
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/docs/tools.md +234 -1
- data/examples/dynamic_model_switching_example.rb +328 -0
- data/examples/real_world_schemas_example.rb +262 -0
- data/examples/responses_api_example.rb +324 -0
- data/examples/tool_loop_example.rb +317 -0
- data/lib/open_router/streaming_client.rb +8 -4
- data/lib/open_router/version.rb +1 -1
- metadata +6 -2
|
@@ -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
|