open_router_enhanced 1.0.0 → 1.2.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.
@@ -14,7 +14,6 @@ gem "faraday_middleware"
14
14
  ```ruby
15
15
  # Define a schema
16
16
  user_schema = OpenRouter::Schema.define("user") do
17
- # Use JSON Schema keywords (camelCase): minLength, maxLength, etc.
18
17
  string :name, required: true, description: "User's full name", minLength: 2, maxLength: 100
19
18
  integer :age, required: true, description: "User's age", minimum: 0, maximum: 150
20
19
  string :email, required: true, description: "Valid email address"
@@ -30,33 +29,86 @@ response = client.complete(
30
29
  )
31
30
 
32
31
  # Access structured data
33
- user = response.structured_output # Hash or raises StructuredOutputError in strict mode
32
+ user = response.structured_output # Validated Hash or raises StructuredOutputError
34
33
  puts user["name"] # => "John Doe"
35
34
  puts user["age"] # => 30
36
35
  puts user["email"] # => "john@example.com"
37
- puts user["premium"] # => false
38
36
  ```
39
37
 
38
+ ## Key Concepts: JSON Content vs Structured Outputs
39
+
40
+ **Important: These are fundamentally different features.**
41
+
42
+ ### Regular JSON in Response Content
43
+
44
+ When you ask a model to "return JSON", you might get JSON in `response.content`, but this is just **text that happens to be formatted as JSON**:
45
+
46
+ ```ruby
47
+ response = client.complete([
48
+ { role: "user", content: "Return JSON with a name field" }
49
+ ])
50
+
51
+ # response.content might be: '{"name": "John"}'
52
+ # This is just a string - NO schema validation, NO guarantees, NO auto-healing
53
+ json_text = response.content
54
+ data = JSON.parse(json_text) # You must parse it yourself
55
+ ```
56
+
57
+ **Characteristics:**
58
+ - No schema validation
59
+ - No automatic healing if malformed
60
+ - You must parse JSON manually
61
+ - Model might return text, markdown, or mixed content
62
+ - No guarantees about structure or fields
63
+
64
+ ### Structured Outputs with Schema Validation
65
+
66
+ When you use `response_format: schema`, the gem ensures the response conforms to your schema:
67
+
68
+ ```ruby
69
+ schema = OpenRouter::Schema.define("user") do
70
+ string :name, required: true
71
+ integer :age, required: true
72
+ end
73
+
74
+ response = client.complete(
75
+ messages,
76
+ response_format: schema # This enables structured output mode
77
+ )
78
+
79
+ # response.structured_output => { "name" => "John", "age" => 30 }
80
+ # This is VALIDATED against your schema, with optional auto-healing
81
+ ```
82
+
83
+ **Characteristics:**
84
+ - Schema-driven validation
85
+ - Automatic healing (if enabled)
86
+ - Parsed and validated automatically
87
+ - Guaranteed structure matching your schema
88
+ - Support for models without native structured output capability
89
+
90
+ **Key Difference**: `response.content` is raw text; `response.structured_output` is validated, parsed data conforming to your schema.
91
+
40
92
  ## Schema Definition DSL
41
93
 
42
- The gem provides a fluent DSL for defining JSON schemas with validation rules:
94
+ The gem provides a fluent DSL for defining JSON schemas with validation rules. Use JSON Schema keywords in camelCase (`minLength`, `maxLength`, `minItems`, etc.):
43
95
 
44
96
  ### Basic Types
45
97
 
46
98
  ```ruby
47
99
  schema = OpenRouter::Schema.define("example") do
48
- # String properties - use JSON Schema keywords (camelCase)
100
+ # String properties
49
101
  string :name, required: true, description: "Name field"
50
102
  string :category, enum: ["A", "B", "C"], description: "Category selection"
51
103
  string :content, minLength: 10, maxLength: 1000
52
-
104
+
53
105
  # Numeric properties
54
106
  integer :count, minimum: 0, maximum: 100
55
107
  number :price, minimum: 0.01, description: "Price in USD"
56
-
108
+
57
109
  # Boolean properties
58
110
  boolean :active, description: "Active status"
59
-
111
+
60
112
  # Strict schema - no extra fields allowed
61
113
  no_additional_properties
62
114
  end
@@ -67,7 +119,7 @@ end
67
119
  ```ruby
68
120
  order_schema = OpenRouter::Schema.define("order") do
69
121
  string :id, required: true, description: "Order ID"
70
-
122
+
71
123
  # Nested object
72
124
  object :customer, required: true do
73
125
  string :name, required: true
@@ -79,7 +131,7 @@ order_schema = OpenRouter::Schema.define("order") do
79
131
  end
80
132
  no_additional_properties
81
133
  end
82
-
134
+
83
135
  # Array of objects - use explicit items hash for complex objects
84
136
  array :items, required: true, description: "Order items", items: {
85
137
  type: "object",
@@ -91,39 +143,36 @@ order_schema = OpenRouter::Schema.define("order") do
91
143
  required: ["product_id", "quantity", "unit_price"],
92
144
  additionalProperties: false
93
145
  }
94
-
146
+
95
147
  # Simple array
96
148
  array :tags, description: "Order tags", items: { type: "string" }
97
-
149
+
98
150
  number :total, required: true, minimum: 0
99
151
  no_additional_properties
100
152
  end
101
153
  ```
102
154
 
103
- **Note**: Use JSON Schema keywords (camelCase): `minLength`, `maxLength`, `minItems`, `maxItems`, `patternProperties`, etc.
104
-
105
155
  ### Advanced Features
106
156
 
107
157
  ```ruby
108
158
  advanced_schema = OpenRouter::Schema.define("advanced") do
109
159
  # Enum constraints
110
160
  string :type, required: true, enum: ["personal", "business"]
111
-
161
+
112
162
  # Pattern matching for strings
113
163
  string :phone, pattern: "^\\+?[1-9]\\d{1,14}$", description: "Phone number"
114
-
164
+
115
165
  # Length constraints
116
- string :description,
117
- description: "Detailed description (minimum 50 characters for quality)",
118
- minLength: 50, maxLength: 1000
119
-
166
+ string :description, minLength: 50, maxLength: 1000,
167
+ description: "Detailed description (minimum 50 characters)"
168
+
120
169
  # Rich descriptions for better model understanding
121
- string :priority, enum: ["low", "medium", "high"],
170
+ string :priority, enum: ["low", "medium", "high"],
122
171
  description: "Priority level - use 'high' for urgent items"
123
172
  end
124
173
  ```
125
174
 
126
- ## Schema from Hash
175
+ ### Schema from Hash
127
176
 
128
177
  For complex schemas or when migrating from existing JSON schemas:
129
178
 
@@ -141,77 +190,284 @@ api_response_schema = OpenRouter::Schema.from_hash("api_response", {
141
190
  type: "object",
142
191
  properties: {
143
192
  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
- }
193
+ username: { type: "string", minLength: 3 }
152
194
  },
153
195
  required: ["id", "username"]
154
196
  }
155
197
  }
156
198
  }
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
199
  }
167
200
  },
168
201
  required: ["success", "data"]
169
202
  })
170
203
  ```
171
204
 
172
- ## Response Handling
205
+ ## Native Response Healing (Server-Side)
206
+
207
+ OpenRouter provides a native response healing plugin that fixes malformed JSON **on the server** before it reaches your application. This is faster and free compared to client-side healing.
173
208
 
174
- ### Strict vs Gentle Modes
209
+ ### How It Works
210
+
211
+ When enabled, OpenRouter's response-healing plugin automatically fixes:
212
+ - Missing brackets, commas, and quotes
213
+ - Trailing commas
214
+ - Markdown-wrapped JSON (` ```json ... ``` `)
215
+ - Text mixed with JSON
216
+ - Unquoted object keys
217
+
218
+ ### Automatic Activation
219
+
220
+ The gem **automatically enables** native healing for non-streaming structured output requests:
175
221
 
176
222
  ```ruby
177
- response = client.complete(messages, response_format: schema)
223
+ # Native healing is automatically added for structured outputs
224
+ response = client.complete(
225
+ messages,
226
+ model: "openai/gpt-4o-mini",
227
+ response_format: schema # Triggers auto-add of response-healing plugin
228
+ )
229
+ ```
178
230
 
179
- # Strict mode (default) – parses JSON and validates; may raise StructuredOutputError
180
- data = response.structured_output
231
+ ### Configuration
181
232
 
182
- # Gentle mode – best-effort JSON parse; returns nil on failure, no validation
183
- data = response.structured_output(mode: :gentle) # => Hash or nil
233
+ ```ruby
234
+ OpenRouter.configure do |config|
235
+ # Enable/disable automatic native healing (default: true)
236
+ config.auto_native_healing = true
237
+ end
184
238
 
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(", ")}"
239
+ # Or via environment variable
240
+ # OPENROUTER_AUTO_NATIVE_HEALING=false
241
+ ```
242
+
243
+ ### Manual Plugin Control
244
+
245
+ You can also manually specify plugins:
246
+
247
+ ```ruby
248
+ # Manually specify response-healing
249
+ response = client.complete(
250
+ messages,
251
+ model: "openai/gpt-4o-mini",
252
+ plugins: [{ id: "response-healing" }],
253
+ response_format: { type: "json_object" }
254
+ )
255
+
256
+ # Combine with other plugins
257
+ response = client.complete(
258
+ messages,
259
+ model: "openai/gpt-4o-mini",
260
+ plugins: [{ id: "web-search" }]
261
+ )
262
+ ```
263
+
264
+ ### Limitations
265
+
266
+ - **Non-streaming only**: Native healing doesn't work with streaming responses
267
+ - **Syntax only**: Fixes JSON syntax errors but not schema validation failures
268
+ - **Truncation**: May fail if response was truncated by `max_tokens`
269
+
270
+ For schema validation and more complex healing, use the client-side auto-healing feature below.
271
+
272
+ ## JSON Auto-Healing (Client-Side)
273
+
274
+ The gem can automatically repair malformed JSON responses or fix schema validation failures using a secondary LLM call.
275
+
276
+ ### When Auto-Healing Triggers
277
+
278
+ Auto-healing activates when **both** conditions are met:
279
+ 1. `config.auto_heal_responses = true` (configuration)
280
+ 2. One of these failures occurs:
281
+ - The model returns invalid JSON syntax (parse error)
282
+ - Valid JSON fails schema validation (missing required fields, wrong types, etc.)
283
+
284
+ ### How Auto-Healing Works
285
+
286
+ When healing is triggered:
287
+ 1. The gem detects the JSON problem (parse error or validation failure)
288
+ 2. Sends a **secondary API request** to the healer model
289
+ 3. Passes the malformed JSON and your schema to the healer
290
+ 4. The healer model fixes the JSON to match your schema
291
+ 5. Returns the corrected, validated response
292
+
293
+ **Important**: Healing uses additional API calls, which incurs extra cost.
294
+
295
+ ### Configuration
296
+
297
+ ```ruby
298
+ OpenRouter.configure do |config|
299
+ # Enable automatic healing (default: false)
300
+ config.auto_heal_responses = true
301
+
302
+ # Model to use for healing (should be reliable and cheap)
303
+ config.healer_model = "openai/gpt-4o-mini"
304
+
305
+ # Maximum healing attempts (default: 2)
306
+ config.max_heal_attempts = 2
190
307
  end
191
308
  ```
192
309
 
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
310
+ ### How to Know if Healing Occurred
196
311
 
197
- ### Native vs Forced Structured Outputs
312
+ Use the callback system to track healing:
198
313
 
199
- If the model supports structured outputs natively, the gem sends your schema to the API directly. If not:
314
+ ```ruby
315
+ client = OpenRouter::Client.new
316
+
317
+ client.on(:on_heal) do |healing_data|
318
+ if healing_data[:healed]
319
+ puts "Healing succeeded after #{healing_data[:attempts]} attempt(s)"
320
+ puts "Original: #{healing_data[:original_content]}"
321
+ puts "Healed: #{healing_data[:healed_content]}"
322
+ else
323
+ puts "Healing failed after #{healing_data[:attempts]} attempt(s)"
324
+ end
325
+ end
326
+ ```
327
+
328
+ ### When Healing Fails
329
+
330
+ If healing fails after `max_heal_attempts`:
331
+ - In **strict mode**: raises `StructuredOutputError`
332
+ - In **gentle mode**: `response.structured_output(mode: :gentle)` returns `nil`
333
+
334
+ ### Cost Considerations
335
+
336
+ Each healing attempt makes an additional API call:
337
+ - Original request: Uses your specified model
338
+ - Healing request: Uses the `healer_model` (typically cheaper)
339
+ - With `max_heal_attempts = 2`: Up to 3 total API calls (1 original + 2 healing)
340
+
341
+ **Best Practice**: Use a cheap, reliable model for healing (e.g., `openai/gpt-4o-mini`).
342
+
343
+ ## Native vs Forced Structured Outputs
344
+
345
+ The gem works with all models, but handles them differently based on their capabilities.
346
+
347
+ ### Native Structured Outputs
348
+
349
+ Models like GPT-4, Claude 3.5, and Gemini support structured outputs natively:
350
+
351
+ ```ruby
352
+ response = client.complete(
353
+ messages,
354
+ model: "openai/gpt-4o",
355
+ response_format: user_schema
356
+ )
357
+
358
+ # What happens:
359
+ # 1. Your schema is sent directly to the OpenRouter API
360
+ # 2. The model guarantees valid JSON conforming to your schema
361
+ # 3. No format instructions needed, no extraction required
362
+ # 4. Most reliable approach
363
+ ```
364
+
365
+ **Advantages:**
366
+ - Highest reliability
367
+ - Guaranteed valid JSON structure
368
+ - No additional prompting overhead
369
+ - Faster processing
370
+
371
+ ### Forced Structured Outputs (Automatic Fallback)
372
+
373
+ For models **without** native structured output support, the gem automatically "forces" structured output:
374
+
375
+ ```ruby
376
+ response = client.complete(
377
+ messages,
378
+ model: "some-model-without-native-support",
379
+ response_format: user_schema
380
+ )
381
+
382
+ # What happens automatically:
383
+ # 1. Gem detects model lacks native structured output capability
384
+ # 2. Injects format instructions into your messages
385
+ # 3. Adds system message: "Respond with valid JSON matching this schema: [schema]"
386
+ # 4. Model receives the injected instructions
387
+ # 5. Gem extracts and parses JSON from response text
388
+ # 6. If enabled, attempts auto-healing for invalid JSON
389
+ ```
390
+
391
+ **Characteristics:**
392
+ - Works with any model
393
+ - Less reliable than native support
394
+ - May require healing for malformed responses
395
+ - Adds prompting overhead
396
+
397
+ ### Configuration Options
200
398
 
201
399
  ```ruby
202
- # Configuration for unsupported models
203
400
  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
401
+ # Option 1: Auto-force (default) - works with all models
402
+ config.auto_force_on_unsupported_models = true
403
+
404
+ # Option 2: Strict - only allow native support
405
+ config.auto_force_on_unsupported_models = false
406
+ config.strict_mode = true # raises CapabilityError for unsupported models
407
+
408
+ # Option 3: Warn but allow
409
+ config.auto_force_on_unsupported_models = false
410
+ config.strict_mode = false # warns but continues with forcing
207
411
  end
208
412
  ```
209
413
 
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
414
+ ### How to Ensure Native Support
415
+
416
+ Use `ModelSelector` to find models with native structured output capability:
417
+
418
+ ```ruby
419
+ model = OpenRouter::ModelSelector.new
420
+ .require(:structured_outputs)
421
+ .optimize_for(:cost)
422
+ .choose
213
423
 
214
- If `false`, using structured outputs on unsupported models raises a `CapabilityError` in strict mode.
424
+ # This guarantees a model with native support
425
+ response = client.complete(messages, model: model, response_format: schema)
426
+ ```
427
+
428
+ ### Previewing Format Instructions
429
+
430
+ You can see the instructions injected when forcing:
431
+
432
+ ```ruby
433
+ schema = OpenRouter::Schema.define("example") do
434
+ string :title, required: true
435
+ end
436
+
437
+ # See what gets injected for unsupported models
438
+ puts schema.get_format_instructions(forced: true)
439
+ ```
440
+
441
+ ## Response Handling
442
+
443
+ ### Accessing Structured Data
444
+
445
+ ```ruby
446
+ response = client.complete(messages, response_format: schema)
447
+
448
+ # Strict mode (default) - validates and may raise StructuredOutputError
449
+ data = response.structured_output
450
+
451
+ # Gentle mode - best-effort parse, returns nil on failure, no validation
452
+ data = response.structured_output(mode: :gentle) # => Hash or nil
453
+ ```
454
+
455
+ ### Checking Validity
456
+
457
+ ```ruby
458
+ # Check if output is valid (may trigger healing if auto_heal_responses is true)
459
+ if response.valid_structured_output?
460
+ puts "Valid structured output"
461
+ data = response.structured_output
462
+ else
463
+ puts "Errors: #{response.validation_errors.join(", ")}"
464
+ # Handle validation failure
465
+ end
466
+ ```
467
+
468
+ **Note**: There is no `response.has_structured_output?` helper. To check presence:
469
+ - Use `response.structured_output(mode: :gentle)` and test for `nil`, or
470
+ - Check that you provided `response_format` and response has content
215
471
 
216
472
  ### Error Handling
217
473
 
@@ -221,12 +477,12 @@ begin
221
477
  data = response.structured_output
222
478
  rescue OpenRouter::StructuredOutputError => e
223
479
  puts "Failed to parse structured output: #{e.message}"
224
- # Fall back to regular content
480
+ # Healing failed or disabled - fall back to regular content
225
481
  content = response.content
226
482
  rescue OpenRouter::SchemaValidationError => e
227
483
  puts "Schema validation failed: #{e.message}"
228
- # Data might still be accessible but invalid
229
- data = response.structured_output
484
+ # Data might still be accessible in gentle mode
485
+ data = response.structured_output(mode: :gentle)
230
486
  end
231
487
  ```
232
488
 
@@ -242,13 +498,13 @@ end
242
498
  ```ruby
243
499
  # Good: Clear, constrained schema
244
500
  product_schema = OpenRouter::Schema.define("product") do
245
- string :name, required: true, description: "Product name (2-100 characters)",
501
+ string :name, required: true, description: "Product name (2-100 characters)",
246
502
  minLength: 2, maxLength: 100
247
- string :category, required: true, enum: ["electronics", "clothing", "books"],
503
+ string :category, required: true, enum: ["electronics", "clothing", "books"],
248
504
  description: "Product category"
249
- number :price, required: true, minimum: 0.01, maximum: 999999.99,
505
+ number :price, required: true, minimum: 0.01, maximum: 999999.99,
250
506
  description: "Price in USD"
251
- integer :stock, required: true, minimum: 0,
507
+ integer :stock, required: true, minimum: 0,
252
508
  description: "Current stock quantity"
253
509
  no_additional_properties
254
510
  end
@@ -256,14 +512,14 @@ end
256
512
 
257
513
  ### Model Selection
258
514
 
259
- Different models have varying support for structured outputs:
515
+ Use `ModelSelector` to find models with appropriate capabilities:
260
516
 
261
517
  ```ruby
262
- # Select a model that supports structured outputs
518
+ # Select a model that supports structured outputs natively
263
519
  model = OpenRouter::ModelSelector.new
264
- .require(:structured_outputs)
265
- .optimize_for(:cost)
266
- .choose
520
+ .require(:structured_outputs)
521
+ .optimize_for(:cost)
522
+ .choose
267
523
 
268
524
  response = client.complete(messages, model: model, response_format: schema)
269
525
  ```
@@ -272,17 +528,16 @@ response = client.complete(messages, model: model, response_format: schema)
272
528
 
273
529
  ```ruby
274
530
  def safe_structured_completion(messages, schema, client)
275
- # Try with structured output first
276
531
  begin
277
532
  response = client.complete(messages, response_format: schema)
278
533
  return { data: response.structured_output, type: :structured }
279
534
  rescue OpenRouter::StructuredOutputError
280
- # Fall back to regular completion with instructions
535
+ # Healing failed - fall back to manual parsing
281
536
  fallback_messages = messages + [{
282
- role: "system",
283
- content: "Please respond with valid JSON matching this schema: #{schema.to_json_schema}"
537
+ role: "system",
538
+ content: "Please respond with valid JSON matching this schema: #{schema.to_h[:schema]}"
284
539
  }]
285
-
540
+
286
541
  response = client.complete(fallback_messages)
287
542
  begin
288
543
  data = JSON.parse(response.content)
@@ -297,59 +552,33 @@ end
297
552
  ### Debugging
298
553
 
299
554
  ```ruby
300
- # API schema sent to OpenRouter (all properties appear required)
555
+ # View the schema sent to OpenRouter API
301
556
  puts JSON.pretty_generate(user_schema.to_h)
302
557
 
303
- # Raw JSON Schema for local validation
558
+ # View the raw JSON Schema for local validation
304
559
  puts JSON.pretty_generate(user_schema.pure_schema)
305
560
 
561
+ # Inspect response
306
562
  response = client.complete(messages, response_format: user_schema)
307
563
  puts response.content
308
564
  puts response.structured_output.inspect
309
565
  ```
310
566
 
311
567
  **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)
568
+ - `Schema#to_h` - OpenRouter API payload (all properties marked required for API compatibility)
569
+ - `Schema#pure_schema` - Raw JSON Schema respecting your DSL `required` flags
314
570
 
315
- ### Validation (Optional)
571
+ ### Optional Validation
316
572
 
317
- If you have the `json-schema` gem installed:
573
+ If you have the `json-schema` gem installed, you can validate data locally:
318
574
 
319
575
  ```ruby
320
- # schema.pure_schema is the raw JSON Schema (respects your required fields)
321
576
  if schema.validation_available?
322
577
  ok = schema.validate(data) # => true/false
323
578
  errors = schema.validation_errors(data) # => Array<String>
324
579
  end
325
580
  ```
326
581
 
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
582
  ## Common Patterns
354
583
 
355
584
  ### API Response Wrapper
@@ -378,7 +607,7 @@ extraction_schema = OpenRouter::Schema.define("extraction") do
378
607
  end
379
608
  end
380
609
  end
381
-
610
+
382
611
  object :summary, required: true do
383
612
  string :main_topic, required: true
384
613
  array :key_points, required: true, items: { type: "string" }
@@ -397,12 +626,12 @@ config_schema = OpenRouter::Schema.define("config") do
397
626
  string :name, required: true
398
627
  boolean :ssl, default: true
399
628
  end
400
-
629
+
401
630
  object :cache do
402
631
  string :type, enum: ["redis", "memcached", "memory"], default: "memory"
403
632
  integer :ttl, minimum: 1, default: 3600
404
633
  end
405
-
634
+
406
635
  array :features, items: { type: "string" }
407
636
  no_additional_properties
408
637
  end
@@ -410,57 +639,148 @@ end
410
639
 
411
640
  ## Troubleshooting
412
641
 
642
+ ### Common Questions
643
+
644
+ **Q: Why is my JSON still invalid after healing?**
645
+
646
+ Healing can fail if:
647
+ - The original response is too corrupted to repair
648
+ - The healer model misunderstands your schema
649
+ - `max_heal_attempts` is too low
650
+
651
+ Solutions:
652
+ - Increase `max_heal_attempts` to 3-5
653
+ - Use a more capable healer model (e.g., `openai/gpt-4o`)
654
+ - Simplify your schema
655
+ - Check healing callbacks to see failure details
656
+
657
+ **Q: How do I know if forcing is being used vs native support?**
658
+
659
+ Check the model's capabilities:
660
+
661
+ ```ruby
662
+ model_info = OpenRouter::ModelRegistry.get_model_info("model-name")
663
+ if model_info[:capabilities].include?(:structured_outputs)
664
+ puts "Native support"
665
+ else
666
+ puts "Will use forcing (if auto_force_on_unsupported_models is true)"
667
+ end
668
+ ```
669
+
670
+ Or use callbacks:
671
+
672
+ ```ruby
673
+ client.on(:before_request) do |params|
674
+ if params[:messages].any? { |m| m[:content]&.include?("valid JSON") }
675
+ puts "Forcing detected - format instructions injected"
676
+ end
677
+ end
678
+ ```
679
+
680
+ **Q: What's the cost impact of healing?**
681
+
682
+ Each healing attempt is a separate API call:
683
+ - Original request: Your specified model (e.g., `gpt-4o`: $0.0025/1k input tokens)
684
+ - Healing request: Healer model (e.g., `gpt-4o-mini`: $0.00015/1k input tokens)
685
+
686
+ With `max_heal_attempts = 2` and both attempts failing, you pay for 3 API calls total.
687
+
688
+ **Best Practice**: Use a cheap healer model and monitor healing frequency with callbacks.
689
+
690
+ **Q: How do I disable healing for specific requests?**
691
+
692
+ Healing is global configuration. To disable for specific requests:
693
+
694
+ ```ruby
695
+ # Option 1: Use gentle mode (no validation, no healing)
696
+ data = response.structured_output(mode: :gentle)
697
+
698
+ # Option 2: Temporarily disable healing
699
+ original_heal_setting = OpenRouter.configuration.auto_heal_responses
700
+ OpenRouter.configuration.auto_heal_responses = false
701
+ response = client.complete(messages, response_format: schema)
702
+ OpenRouter.configuration.auto_heal_responses = original_heal_setting
703
+ ```
704
+
413
705
  ### Common Issues
414
706
 
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
707
+ **1. Schema Too Complex**
708
+
709
+ Large, deeply nested schemas may cause model confusion.
419
710
 
420
- ### Solutions
711
+ Solution: Flatten structures where possible:
421
712
 
422
713
  ```ruby
423
- # 1. Simplify complex schemas
714
+ # Instead of deep nesting:
715
+ # user -> profile -> settings -> notifications -> email -> frequency
716
+
717
+ # Use flatter structure:
424
718
  simple_schema = OpenRouter::Schema.define("simple") do
425
- # Flatten nested structures where possible
426
719
  string :user_name, required: true
427
- string :user_email, required: true
428
- string :order_id, required: true
429
- number :order_total, required: true
720
+ string :notification_email_frequency, enum: ["daily", "weekly", "never"]
430
721
  end
722
+ ```
431
723
 
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
724
+ **2. Conflicting Constraints**
725
+
726
+ Ensure min/max values and enums are logically consistent:
727
+
728
+ ```ruby
729
+ # Bad: impossible constraints
730
+ string :code, minLength: 10, maxLength: 5 # impossible!
731
+
732
+ # Good: logical constraints
733
+ string :code, minLength: 5, maxLength: 10
734
+ ```
442
735
 
443
- # 3. Use model selection
444
- best_model = OpenRouter::ModelSelector.new
445
- .require(:structured_outputs)
446
- .optimize_for(:performance)
447
- .choose
736
+ **3. Model Limitations**
448
737
 
449
- # 4. Implement retry logic with fallbacks
738
+ Not all models support structured outputs equally well.
739
+
740
+ Solution: Use `ModelSelector` to find capable models:
741
+
742
+ ```ruby
743
+ model = OpenRouter::ModelSelector.new
744
+ .require(:structured_outputs)
745
+ .optimize_for(:performance)
746
+ .choose
747
+ ```
748
+
749
+ **4. JSON Parsing Errors**
750
+
751
+ Models may return malformed JSON despite constraints.
752
+
753
+ Solutions:
754
+ - Enable auto-healing: `config.auto_heal_responses = true`
755
+ - Use models with native structured output support
756
+ - Implement retry logic:
757
+
758
+ ```ruby
450
759
  def robust_structured_completion(messages, schema, max_retries: 3)
451
760
  retries = 0
452
-
761
+
453
762
  begin
454
763
  response = client.complete(messages, response_format: schema)
455
764
  response.structured_output
456
765
  rescue OpenRouter::StructuredOutputError => e
457
766
  retries += 1
458
767
  if retries <= max_retries
459
- sleep(retries * 0.5) # Back off
768
+ sleep(retries * 0.5) # Exponential backoff
460
769
  retry
461
770
  else
462
771
  raise e
463
772
  end
464
773
  end
465
774
  end
466
- ```
775
+ ```
776
+
777
+ ### Getting Help
778
+
779
+ For additional assistance:
780
+ - Check the [examples directory](../examples/) for working code samples
781
+ - Review the [observability documentation](observability.md) for callback debugging
782
+ - Open an issue on [GitHub](https://github.com/estiens/open_router_enhanced/issues) with:
783
+ - Your schema definition
784
+ - The model used
785
+ - Error messages and stack traces
786
+ - Whether healing is enabled