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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +1 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +13 -0
  5. data/.rubocop_todo.yml +130 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +41 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/CONTRIBUTING.md +384 -0
  10. data/Gemfile +22 -0
  11. data/Gemfile.lock +138 -0
  12. data/LICENSE.txt +21 -0
  13. data/MIGRATION.md +556 -0
  14. data/README.md +1660 -0
  15. data/Rakefile +334 -0
  16. data/SECURITY.md +150 -0
  17. data/VCR_CONFIGURATION.md +80 -0
  18. data/docs/model_selection.md +637 -0
  19. data/docs/observability.md +430 -0
  20. data/docs/prompt_templates.md +422 -0
  21. data/docs/streaming.md +467 -0
  22. data/docs/structured_outputs.md +466 -0
  23. data/docs/tools.md +1016 -0
  24. data/examples/basic_completion.rb +122 -0
  25. data/examples/model_selection_example.rb +141 -0
  26. data/examples/observability_example.rb +199 -0
  27. data/examples/prompt_template_example.rb +184 -0
  28. data/examples/smart_completion_example.rb +89 -0
  29. data/examples/streaming_example.rb +176 -0
  30. data/examples/structured_outputs_example.rb +191 -0
  31. data/examples/tool_calling_example.rb +149 -0
  32. data/lib/open_router/client.rb +552 -0
  33. data/lib/open_router/http.rb +118 -0
  34. data/lib/open_router/json_healer.rb +263 -0
  35. data/lib/open_router/model_registry.rb +378 -0
  36. data/lib/open_router/model_selector.rb +462 -0
  37. data/lib/open_router/prompt_template.rb +290 -0
  38. data/lib/open_router/response.rb +371 -0
  39. data/lib/open_router/schema.rb +288 -0
  40. data/lib/open_router/streaming_client.rb +210 -0
  41. data/lib/open_router/tool.rb +221 -0
  42. data/lib/open_router/tool_call.rb +180 -0
  43. data/lib/open_router/usage_tracker.rb +277 -0
  44. data/lib/open_router/version.rb +5 -0
  45. data/lib/open_router.rb +123 -0
  46. data/sig/open_router.rbs +20 -0
  47. 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
+ ```