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,466 @@
|
|
|
1
|
+
# Structured Outputs
|
|
2
|
+
|
|
3
|
+
The OpenRouter gem provides comprehensive support for structured outputs using JSON Schema validation. This feature ensures that AI model responses conform to specific formats, making them easy to parse and integrate into your applications.
|
|
4
|
+
|
|
5
|
+
**Important**: Add `faraday_middleware` to your Gemfile for proper JSON response parsing:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Gemfile
|
|
9
|
+
gem "faraday_middleware"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
# Define a schema
|
|
16
|
+
user_schema = OpenRouter::Schema.define("user") do
|
|
17
|
+
# Use JSON Schema keywords (camelCase): minLength, maxLength, etc.
|
|
18
|
+
string :name, required: true, description: "User's full name", minLength: 2, maxLength: 100
|
|
19
|
+
integer :age, required: true, description: "User's age", minimum: 0, maximum: 150
|
|
20
|
+
string :email, required: true, description: "Valid email address"
|
|
21
|
+
boolean :premium, description: "Premium account status"
|
|
22
|
+
no_additional_properties
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Use with completion
|
|
26
|
+
response = client.complete(
|
|
27
|
+
[{ role: "user", content: "Create a user profile for John Doe, age 30, john@example.com" }],
|
|
28
|
+
model: "openai/gpt-4o",
|
|
29
|
+
response_format: user_schema
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Access structured data
|
|
33
|
+
user = response.structured_output # Hash or raises StructuredOutputError in strict mode
|
|
34
|
+
puts user["name"] # => "John Doe"
|
|
35
|
+
puts user["age"] # => 30
|
|
36
|
+
puts user["email"] # => "john@example.com"
|
|
37
|
+
puts user["premium"] # => false
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Schema Definition DSL
|
|
41
|
+
|
|
42
|
+
The gem provides a fluent DSL for defining JSON schemas with validation rules:
|
|
43
|
+
|
|
44
|
+
### Basic Types
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
schema = OpenRouter::Schema.define("example") do
|
|
48
|
+
# String properties - use JSON Schema keywords (camelCase)
|
|
49
|
+
string :name, required: true, description: "Name field"
|
|
50
|
+
string :category, enum: ["A", "B", "C"], description: "Category selection"
|
|
51
|
+
string :content, minLength: 10, maxLength: 1000
|
|
52
|
+
|
|
53
|
+
# Numeric properties
|
|
54
|
+
integer :count, minimum: 0, maximum: 100
|
|
55
|
+
number :price, minimum: 0.01, description: "Price in USD"
|
|
56
|
+
|
|
57
|
+
# Boolean properties
|
|
58
|
+
boolean :active, description: "Active status"
|
|
59
|
+
|
|
60
|
+
# Strict schema - no extra fields allowed
|
|
61
|
+
no_additional_properties
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Complex Objects and Arrays
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
order_schema = OpenRouter::Schema.define("order") do
|
|
69
|
+
string :id, required: true, description: "Order ID"
|
|
70
|
+
|
|
71
|
+
# Nested object
|
|
72
|
+
object :customer, required: true do
|
|
73
|
+
string :name, required: true
|
|
74
|
+
string :email, required: true
|
|
75
|
+
object :address, required: true do
|
|
76
|
+
string :street, required: true
|
|
77
|
+
string :city, required: true
|
|
78
|
+
string :zip_code, required: true
|
|
79
|
+
end
|
|
80
|
+
no_additional_properties
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Array of objects - use explicit items hash for complex objects
|
|
84
|
+
array :items, required: true, description: "Order items", items: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
product_id: { type: "string" },
|
|
88
|
+
quantity: { type: "integer", minimum: 1 },
|
|
89
|
+
unit_price: { type: "number", minimum: 0 }
|
|
90
|
+
},
|
|
91
|
+
required: ["product_id", "quantity", "unit_price"],
|
|
92
|
+
additionalProperties: false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Simple array
|
|
96
|
+
array :tags, description: "Order tags", items: { type: "string" }
|
|
97
|
+
|
|
98
|
+
number :total, required: true, minimum: 0
|
|
99
|
+
no_additional_properties
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Note**: Use JSON Schema keywords (camelCase): `minLength`, `maxLength`, `minItems`, `maxItems`, `patternProperties`, etc.
|
|
104
|
+
|
|
105
|
+
### Advanced Features
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
advanced_schema = OpenRouter::Schema.define("advanced") do
|
|
109
|
+
# Enum constraints
|
|
110
|
+
string :type, required: true, enum: ["personal", "business"]
|
|
111
|
+
|
|
112
|
+
# Pattern matching for strings
|
|
113
|
+
string :phone, pattern: "^\\+?[1-9]\\d{1,14}$", description: "Phone number"
|
|
114
|
+
|
|
115
|
+
# Length constraints
|
|
116
|
+
string :description,
|
|
117
|
+
description: "Detailed description (minimum 50 characters for quality)",
|
|
118
|
+
minLength: 50, maxLength: 1000
|
|
119
|
+
|
|
120
|
+
# Rich descriptions for better model understanding
|
|
121
|
+
string :priority, enum: ["low", "medium", "high"],
|
|
122
|
+
description: "Priority level - use 'high' for urgent items"
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Schema from Hash
|
|
127
|
+
|
|
128
|
+
For complex schemas or when migrating from existing JSON schemas:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
api_response_schema = OpenRouter::Schema.from_hash("api_response", {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: {
|
|
134
|
+
success: { type: "boolean" },
|
|
135
|
+
data: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
users: {
|
|
139
|
+
type: "array",
|
|
140
|
+
items: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
id: { type: "integer" },
|
|
144
|
+
username: { type: "string", minLength: 3 },
|
|
145
|
+
profile: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
bio: { type: "string" },
|
|
149
|
+
avatar_url: { type: "string", format: "uri" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
required: ["id", "username"]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
pagination: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
page: { type: "integer", minimum: 1 },
|
|
162
|
+
total: { type: "integer", minimum: 0 },
|
|
163
|
+
has_more: { type: "boolean" }
|
|
164
|
+
},
|
|
165
|
+
required: ["page", "total", "has_more"]
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
required: ["success", "data"]
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Response Handling
|
|
173
|
+
|
|
174
|
+
### Strict vs Gentle Modes
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
response = client.complete(messages, response_format: schema)
|
|
178
|
+
|
|
179
|
+
# Strict mode (default) – parses JSON and validates; may raise StructuredOutputError
|
|
180
|
+
data = response.structured_output
|
|
181
|
+
|
|
182
|
+
# Gentle mode – best-effort JSON parse; returns nil on failure, no validation
|
|
183
|
+
data = response.structured_output(mode: :gentle) # => Hash or nil
|
|
184
|
+
|
|
185
|
+
# Check validity (may trigger healing if auto_heal_responses is true)
|
|
186
|
+
if response.valid_structured_output?
|
|
187
|
+
puts "Valid structured output"
|
|
188
|
+
else
|
|
189
|
+
puts "Errors: #{response.validation_errors.join(", ")}"
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Tip**: There is no `response.has_structured_output?` helper. To check "presence," either:
|
|
194
|
+
- Call `response.structured_output(mode: :gentle)` and test for `nil`, or
|
|
195
|
+
- Check that you provided `response_format` and response has content
|
|
196
|
+
|
|
197
|
+
### Native vs Forced Structured Outputs
|
|
198
|
+
|
|
199
|
+
If the model supports structured outputs natively, the gem sends your schema to the API directly. If not:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
# Configuration for unsupported models
|
|
203
|
+
OpenRouter.configure do |config|
|
|
204
|
+
config.auto_force_on_unsupported_models = true # default - inject format instructions
|
|
205
|
+
config.strict_mode = false # warn instead of raise on missing capability
|
|
206
|
+
config.default_structured_output_mode = :strict
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
When `auto_force_on_unsupported_models` is `true`, the gem:
|
|
211
|
+
1. Injects format instructions into messages (forced extraction)
|
|
212
|
+
2. Parses/extracts JSON from text, optionally healing it if enabled
|
|
213
|
+
|
|
214
|
+
If `false`, using structured outputs on unsupported models raises a `CapabilityError` in strict mode.
|
|
215
|
+
|
|
216
|
+
### Error Handling
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
begin
|
|
220
|
+
response = client.complete(messages, response_format: schema)
|
|
221
|
+
data = response.structured_output
|
|
222
|
+
rescue OpenRouter::StructuredOutputError => e
|
|
223
|
+
puts "Failed to parse structured output: #{e.message}"
|
|
224
|
+
# Fall back to regular content
|
|
225
|
+
content = response.content
|
|
226
|
+
rescue OpenRouter::SchemaValidationError => e
|
|
227
|
+
puts "Schema validation failed: #{e.message}"
|
|
228
|
+
# Data might still be accessible but invalid
|
|
229
|
+
data = response.structured_output
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Best Practices
|
|
234
|
+
|
|
235
|
+
### Schema Design
|
|
236
|
+
|
|
237
|
+
1. **Be Specific**: Provide clear descriptions for better model understanding
|
|
238
|
+
2. **Use Constraints**: Set appropriate min/max values, string lengths, and enums
|
|
239
|
+
3. **Required Fields**: Mark essential fields as required
|
|
240
|
+
4. **No Extra Properties**: Use `no_additional_properties` for strict schemas
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
# Good: Clear, constrained schema
|
|
244
|
+
product_schema = OpenRouter::Schema.define("product") do
|
|
245
|
+
string :name, required: true, description: "Product name (2-100 characters)",
|
|
246
|
+
minLength: 2, maxLength: 100
|
|
247
|
+
string :category, required: true, enum: ["electronics", "clothing", "books"],
|
|
248
|
+
description: "Product category"
|
|
249
|
+
number :price, required: true, minimum: 0.01, maximum: 999999.99,
|
|
250
|
+
description: "Price in USD"
|
|
251
|
+
integer :stock, required: true, minimum: 0,
|
|
252
|
+
description: "Current stock quantity"
|
|
253
|
+
no_additional_properties
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Model Selection
|
|
258
|
+
|
|
259
|
+
Different models have varying support for structured outputs:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# Select a model that supports structured outputs
|
|
263
|
+
model = OpenRouter::ModelSelector.new
|
|
264
|
+
.require(:structured_outputs)
|
|
265
|
+
.optimize_for(:cost)
|
|
266
|
+
.choose
|
|
267
|
+
|
|
268
|
+
response = client.complete(messages, model: model, response_format: schema)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Fallback Strategies
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
def safe_structured_completion(messages, schema, client)
|
|
275
|
+
# Try with structured output first
|
|
276
|
+
begin
|
|
277
|
+
response = client.complete(messages, response_format: schema)
|
|
278
|
+
return { data: response.structured_output, type: :structured }
|
|
279
|
+
rescue OpenRouter::StructuredOutputError
|
|
280
|
+
# Fall back to regular completion with instructions
|
|
281
|
+
fallback_messages = messages + [{
|
|
282
|
+
role: "system",
|
|
283
|
+
content: "Please respond with valid JSON matching this schema: #{schema.to_json_schema}"
|
|
284
|
+
}]
|
|
285
|
+
|
|
286
|
+
response = client.complete(fallback_messages)
|
|
287
|
+
begin
|
|
288
|
+
data = JSON.parse(response.content)
|
|
289
|
+
return { data: data, type: :parsed }
|
|
290
|
+
rescue JSON::ParserError
|
|
291
|
+
return { data: response.content, type: :text }
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Debugging
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
# API schema sent to OpenRouter (all properties appear required)
|
|
301
|
+
puts JSON.pretty_generate(user_schema.to_h)
|
|
302
|
+
|
|
303
|
+
# Raw JSON Schema for local validation
|
|
304
|
+
puts JSON.pretty_generate(user_schema.pure_schema)
|
|
305
|
+
|
|
306
|
+
response = client.complete(messages, response_format: user_schema)
|
|
307
|
+
puts response.content
|
|
308
|
+
puts response.structured_output.inspect
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Key Distinction**:
|
|
312
|
+
- `Schema#to_h` returns the OpenRouter payload respecting your DSL required flags
|
|
313
|
+
- `Schema#pure_schema` returns the raw JSON Schema for local validation (when using the json-schema gem)
|
|
314
|
+
|
|
315
|
+
### Validation (Optional)
|
|
316
|
+
|
|
317
|
+
If you have the `json-schema` gem installed:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# schema.pure_schema is the raw JSON Schema (respects your required fields)
|
|
321
|
+
if schema.validation_available?
|
|
322
|
+
ok = schema.validate(data) # => true/false
|
|
323
|
+
errors = schema.validation_errors(data) # => Array<String>
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Response Healing
|
|
328
|
+
|
|
329
|
+
The gem includes automatic healing for malformed JSON responses:
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# Configure healing globally
|
|
333
|
+
OpenRouter.configure do |config|
|
|
334
|
+
config.auto_heal_responses = true
|
|
335
|
+
config.healer_model = "openai/gpt-4o-mini"
|
|
336
|
+
config.max_heal_attempts = 2
|
|
337
|
+
end
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
**Notes**:
|
|
341
|
+
- In strict mode, `response.structured_output` may invoke the healer if JSON is invalid or schema validation fails and `auto_heal_responses` is `true`
|
|
342
|
+
- Healing sends a secondary request with instructions to fix JSON according to your schema
|
|
343
|
+
|
|
344
|
+
### Format Instructions
|
|
345
|
+
|
|
346
|
+
You can preview the system instructions the model receives when forcing:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
schema = OpenRouter::Schema.define("example") { string :title, required: true }
|
|
350
|
+
puts schema.get_format_instructions # or get_format_instructions(forced: true)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Common Patterns
|
|
354
|
+
|
|
355
|
+
### API Response Wrapper
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
api_wrapper_schema = OpenRouter::Schema.define("api_wrapper") do
|
|
359
|
+
boolean :success, required: true, description: "Whether the operation succeeded"
|
|
360
|
+
string :message, description: "Human-readable message"
|
|
361
|
+
object :data, description: "Response payload"
|
|
362
|
+
array :errors, description: "List of error messages", items: { type: "string" }
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Data Extraction
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
extraction_schema = OpenRouter::Schema.define("extraction") do
|
|
370
|
+
array :entities, required: true, description: "Extracted entities" do
|
|
371
|
+
items do
|
|
372
|
+
object do
|
|
373
|
+
string :type, required: true, enum: ["person", "organization", "location"]
|
|
374
|
+
string :name, required: true, description: "Entity name"
|
|
375
|
+
number :confidence, required: true, minimum: 0, maximum: 1
|
|
376
|
+
integer :start_pos, description: "Start position in text"
|
|
377
|
+
integer :end_pos, description: "End position in text"
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
object :summary, required: true do
|
|
383
|
+
string :main_topic, required: true
|
|
384
|
+
array :key_points, required: true, items: { type: "string" }
|
|
385
|
+
string :sentiment, enum: ["positive", "negative", "neutral"]
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Configuration Objects
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
config_schema = OpenRouter::Schema.define("config") do
|
|
394
|
+
object :database, required: true do
|
|
395
|
+
string :host, required: true
|
|
396
|
+
integer :port, required: true, minimum: 1, maximum: 65535
|
|
397
|
+
string :name, required: true
|
|
398
|
+
boolean :ssl, default: true
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
object :cache do
|
|
402
|
+
string :type, enum: ["redis", "memcached", "memory"], default: "memory"
|
|
403
|
+
integer :ttl, minimum: 1, default: 3600
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
array :features, items: { type: "string" }
|
|
407
|
+
no_additional_properties
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Troubleshooting
|
|
412
|
+
|
|
413
|
+
### Common Issues
|
|
414
|
+
|
|
415
|
+
1. **Schema Too Complex**: Large, deeply nested schemas may cause model confusion
|
|
416
|
+
2. **Conflicting Constraints**: Ensure min/max values and enums are logically consistent
|
|
417
|
+
3. **Model Limitations**: Not all models support structured outputs equally well
|
|
418
|
+
4. **JSON Parsing Errors**: Models may return malformed JSON despite schema constraints
|
|
419
|
+
|
|
420
|
+
### Solutions
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
# 1. Simplify complex schemas
|
|
424
|
+
simple_schema = OpenRouter::Schema.define("simple") do
|
|
425
|
+
# Flatten nested structures where possible
|
|
426
|
+
string :user_name, required: true
|
|
427
|
+
string :user_email, required: true
|
|
428
|
+
string :order_id, required: true
|
|
429
|
+
number :order_total, required: true
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# 2. Add extra validation in your code
|
|
433
|
+
def validate_response_data(data, custom_rules = {})
|
|
434
|
+
errors = []
|
|
435
|
+
|
|
436
|
+
# Custom business logic validation
|
|
437
|
+
errors << "Invalid email format" unless data["email"]&.include?("@")
|
|
438
|
+
errors << "Price too low" if data["price"].to_f < 0.01
|
|
439
|
+
|
|
440
|
+
errors
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# 3. Use model selection
|
|
444
|
+
best_model = OpenRouter::ModelSelector.new
|
|
445
|
+
.require(:structured_outputs)
|
|
446
|
+
.optimize_for(:performance)
|
|
447
|
+
.choose
|
|
448
|
+
|
|
449
|
+
# 4. Implement retry logic with fallbacks
|
|
450
|
+
def robust_structured_completion(messages, schema, max_retries: 3)
|
|
451
|
+
retries = 0
|
|
452
|
+
|
|
453
|
+
begin
|
|
454
|
+
response = client.complete(messages, response_format: schema)
|
|
455
|
+
response.structured_output
|
|
456
|
+
rescue OpenRouter::StructuredOutputError => e
|
|
457
|
+
retries += 1
|
|
458
|
+
if retries <= max_retries
|
|
459
|
+
sleep(retries * 0.5) # Back off
|
|
460
|
+
retry
|
|
461
|
+
else
|
|
462
|
+
raise e
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
```
|