open_router_enhanced 2.2.0 → 2.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6c9c14171242103eaeab8219521180242f9e0ee0968c739f8db2148a17423a5
4
- data.tar.gz: '0906f33ab027e8cbf17ff60ab120ec65de39ec689c6e9a48025fb8df679f7d55'
3
+ metadata.gz: 9f4936ab4591643976d7cf5f63b92fec61fef1fda2900ce6ed022ce35671bd3a
4
+ data.tar.gz: 1ac1e6ea82c9eacc1bc28e4b99490507bcf1f335446f3ab1cbab2a2ca8d749b4
5
5
  SHA512:
6
- metadata.gz: 4cd127d4d6889e281e88e3f044f2444bd32e46f7ac4797d5c786b9a3fc5a8f792baf445c8dea481ab5018fae72646a221da05266bcb4134266735546e35a428d
7
- data.tar.gz: a5b80c88d5f2228f1891409d2edb335a1a2c0294b94ffc69fb2730d0a854f7010c5eece8ac026eb6ba99a60df583634f42c77e18aada4b0399886b6a744a3488
6
+ metadata.gz: ef3a651dee0fec1b4d0508e88578c78ed1d259b5dc7c5fb5917d7af0690a2e69c12732f6fc5a0b423340da7afddbc4de80f08f8a27e616c9d7c15a1051508a39
7
+ data.tar.gz: 017ec8a29aeb205e396265409c8f9d53f3f7280d025543e89b2c95510d6b21fe78f230461dcc5291a433c1b60f850425ba754dcfc1ab32e98517b1f522688692
data/CHANGELOG.md CHANGED
@@ -1,23 +1,5 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [2.2.0] - 2026-06-28
4
-
5
- ### Added
6
-
7
- - **`Routing` mixin** (`OpenRouter::Routing`) included in `Client`, providing two new meta-routing methods:
8
- - `pareto_complete(messages, min_coding_score: nil, **opts)` — routes to the cheapest model meeting a configurable quality bar via OpenRouter's Pareto Code Router (`openrouter/pareto-code`). `min_coding_score` is validated to `0.0–1.0`.
9
- - `fuse(messages, analysis_models: nil, judge: nil, preset: nil, max_tool_calls: nil, **opts)` — fans a prompt out to a panel of models and synthesises one answer via OpenRouter's Fusion router (`openrouter/fusion`). `analysis_models` (1–8) and `max_tool_calls` (1–16) are validated.
10
- - **`SubagentTool`** (`OpenRouter::SubagentTool`) — wraps OpenRouter's `openrouter:subagent` server tool so an orchestrator model can delegate self-contained subtasks to a cheaper worker model mid-generation. Constructor: `model:` (required worker model) plus optional `instructions:`, `max_completion_tokens:`, `temperature:`, and `reasoning:`. Pass it via the normal `tools:` array to `complete`.
11
- - **`Response#selected_model`** — alias for `#model`; returns the concrete model OpenRouter resolved for routing responses (e.g. Pareto, Auto, Fusion).
12
-
13
- ### Changed
14
-
15
- - Capability warning / strict-mode guards now exempt all `openrouter/`-prefixed meta-models (previously only `openrouter/auto` was exempt); this prevents spurious warnings or `CapabilityError` when using `pareto_complete` or `fuse` with tools or structured outputs.
16
-
17
- ### Notes
18
-
19
- - These three OpenRouter platform features are still evolving server-side. The gem builds and validates the requests; routing/synthesis/delegation behaviour is performed by OpenRouter. Fusion fans out to every panel model plus a judge, so it costs roughly 4–5× a single completion. `pareto_complete` may resolve to a reasoning model that consumes a small `max_tokens` budget entirely on reasoning (returning `nil` content with `finish_reason: "length"`) — budget `max_tokens` accordingly.
20
-
21
3
  ## [2.0.0] - 2025-12-28
22
4
 
23
5
  ### Overview
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- open_router_enhanced (2.2.0)
4
+ open_router_enhanced (2.2.1)
5
5
  activesupport (>= 6.0, < 9.0)
6
6
  dotenv (>= 2.0, < 4.0)
7
7
  faraday (>= 1.0, < 3.0)
data/README.md CHANGED
@@ -45,7 +45,6 @@ The [OpenRouter API](https://openrouter.ai/docs) is a single unified interface f
45
45
  - [Tool Calling](#tool-calling)
46
46
  - [Structured Outputs](#structured-outputs)
47
47
  - [Smart Model Selection](#smart-model-selection)
48
- - [Routing (Pareto & Fusion)](#routing-pareto--fusion)
49
48
  - [Prompt Templates](#prompt-templates)
50
49
  - [Streaming](#streaming)
51
50
  - [Usage Tracking](#usage-tracking)
@@ -384,95 +383,6 @@ models = OpenRouter::ModelSelector.new
384
383
 
385
384
  **[Complete Model Selection Documentation](docs/model_selection.md)**
386
385
 
387
- ### Routing (Pareto & Fusion)
388
-
389
- OpenRouter offers two meta-routing modes that automatically pick or synthesize answers across models.
390
-
391
- #### Pareto Code Router
392
-
393
- Routes each request to the cheapest model that meets a configurable quality bar — useful when you want cost-optimised code completions without picking a specific model.
394
-
395
- ```ruby
396
- # Cheapest model meeting default quality threshold
397
- response = client.pareto_complete([
398
- { role: "user", content: "Write a binary search in Ruby" }
399
- ])
400
-
401
- # Require a higher quality bar (0.0–1.0, higher = better)
402
- response = client.pareto_complete(
403
- [{ role: "user", content: "Implement a red-black tree" }],
404
- min_coding_score: 0.8,
405
- max_tokens: 1000
406
- )
407
-
408
- # Which model actually answered?
409
- puts response.selected_model # => "anthropic/claude-3.5-haiku"
410
- puts response.content
411
- ```
412
-
413
- #### Fusion Router
414
-
415
- Fans a prompt out to a panel of models in parallel, then synthesises one answer with a judge model. Costs roughly 4–5× a single completion but can outperform any individual model.
416
-
417
- ```ruby
418
- # Default panel (OpenRouter chooses)
419
- response = client.fuse([
420
- { role: "user", content: "What is the best approach to distributed consensus?" }
421
- ])
422
-
423
- # Custom panel + explicit judge
424
- response = client.fuse(
425
- [{ role: "user", content: "Review this architecture" }],
426
- analysis_models: [
427
- "anthropic/claude-3.5-sonnet",
428
- "openai/gpt-4o",
429
- "google/gemini-2.0-flash-001"
430
- ],
431
- judge: "anthropic/claude-opus-4-5",
432
- max_tokens: 2000
433
- )
434
-
435
- # Curated preset panels
436
- response = client.fuse(messages, preset: "general-budget")
437
-
438
- # selected_model reports the synthesis/judge model that produced the answer,
439
- # e.g. "anthropic/claude-opus-4-5" — not the "openrouter/fusion" router alias.
440
- puts response.selected_model
441
- puts response.content
442
- ```
443
-
444
- > **Note:** Fusion fans out to every panel model plus a judge, so it costs roughly 4–5× a single completion. `min_coding_score` for Pareto is validated to `0.0–1.0`; `analysis_models` (1–8) and `max_tool_calls` (1–16) for Fusion are validated client-side.
445
-
446
- #### `SubagentTool`
447
-
448
- Wraps OpenRouter's built-in `openrouter:subagent` server tool so an LLM can spawn its own sub-completions during a tool-calling loop.
449
-
450
- ```ruby
451
- subagent = OpenRouter::SubagentTool.new(
452
- model: "anthropic/claude-3.5-haiku", # required: the cheaper worker model
453
- instructions: "Complete the task exactly as described. Be concise.", # optional
454
- max_completion_tokens: 512 # optional (also: temperature:, reasoning:)
455
- )
456
-
457
- response = client.complete(
458
- [{ role: "user", content: "Summarize the attached changelog into release notes." }],
459
- model: "openai/gpt-4o",
460
- tools: [subagent],
461
- tool_choice: "auto"
462
- )
463
- ```
464
-
465
- > The orchestrator decides whether to delegate. The gem's job is to build and send a valid `openrouter:subagent` tool; OpenRouter runs the worker server-side and feeds its result back into the orchestrator's generation.
466
-
467
- #### `Response#selected_model`
468
-
469
- All routing methods (`complete`, `pareto_complete`, `fuse`) return a `Response` object. Use `#selected_model` (alias for `#model`) to see which model OpenRouter ultimately used:
470
-
471
- ```ruby
472
- response = client.pareto_complete(messages)
473
- puts response.selected_model # e.g. "mistralai/codestral-2501"
474
- ```
475
-
476
386
  ### Prompt Templates
477
387
 
478
388
  Create reusable, parameterized prompts with variable interpolation.
@@ -43,7 +43,7 @@ module OpenRouter
43
43
  @callbacks[event].each do |callback|
44
44
  callback.call(data)
45
45
  rescue StandardError => e
46
- OpenRouter.log_warning("[OpenRouter] Callback error for #{event}: #{e.message}")
46
+ warn "[OpenRouter] Callback error for #{event}: #{e.message}"
47
47
  end
48
48
  end
49
49
  end
@@ -8,7 +8,6 @@ require_relative "callbacks"
8
8
  require_relative "parameter_builder"
9
9
  require_relative "tool_serializer"
10
10
  require_relative "request_handler"
11
- require_relative "routing"
12
11
 
13
12
  module OpenRouter
14
13
  class ServerError < StandardError; end
@@ -19,7 +18,6 @@ module OpenRouter
19
18
  include OpenRouter::ParameterBuilder
20
19
  include OpenRouter::ToolSerializer
21
20
  include OpenRouter::RequestHandler
22
- include OpenRouter::Routing
23
21
 
24
22
  attr_reader :callbacks, :usage_tracker, :configuration
25
23
 
@@ -60,7 +58,10 @@ module OpenRouter
60
58
  opts = normalize_options(options, kwargs)
61
59
  parameters = prepare_base_parameters(messages, opts, stream)
62
60
  forced_extraction = configure_tools_and_structured_outputs!(parameters, opts)
63
- configure_plugins!(parameters, opts.response_format, stream)
61
+ # Gate the response-healing plugin on what's actually on the wire — it requires
62
+ # response_format to be present, so keying off opts.response_format (intent) would
63
+ # attach it even when no response_format was sent and produce a 400.
64
+ configure_plugins!(parameters, parameters[:response_format], stream)
64
65
  validate_vision_support(opts.model, messages)
65
66
 
66
67
  trigger_callbacks(:before_request, parameters)
@@ -68,7 +69,7 @@ module OpenRouter
68
69
  raw_response = execute_request(parameters)
69
70
  validate_response!(raw_response, stream)
70
71
 
71
- response = build_response(raw_response, opts.response_format, forced_extraction)
72
+ response = build_response(raw_response, opts.response_format, forced_extraction, strict: resolve_strict(opts.strict))
72
73
 
73
74
  model_for_tracking = opts.model.is_a?(String) ? opts.model : opts.model.first
74
75
  @usage_tracker&.track(response, model: model_for_tracking)
@@ -195,6 +196,13 @@ module OpenRouter
195
196
 
196
197
  private
197
198
 
199
+ # Per-call `strict:` wins; otherwise fall back to the configured default.
200
+ def resolve_strict(strict)
201
+ return strict unless strict.nil?
202
+
203
+ configuration.structured_output_strict
204
+ end
205
+
198
206
  def normalize_options(options, kwargs)
199
207
  case options
200
208
  when CompletionOptions
@@ -147,11 +147,18 @@ module OpenRouter
147
147
  # Client-side options (not sent to API)
148
148
  # ═══════════════════════════════════════════════════════════════════════════
149
149
 
150
- # @return [Boolean, nil] Override forced extraction mode for structured outputs
151
- # true: Force extraction via system message injection
152
- # false: Use native structured output
153
- # nil: Auto-determine based on model capability
154
- attr_accessor :force_structured_output
150
+ # @return [Boolean, nil] Request provider-side native json_schema enforcement.
151
+ # true: Send response_format: { type: "json_schema", ... } (grammar-constrained,
152
+ # only supported by some models/providers — can 400 on unsupported ones).
153
+ # false/nil (default): Send response_format: { type: "json_object" } and describe
154
+ # the schema in the prompt. Widely supported; validated/healed client-side.
155
+ attr_accessor :native
156
+
157
+ # @return [Boolean, nil] Validation strictness for structured outputs.
158
+ # true: Raise StructuredOutputError if the response doesn't match the schema.
159
+ # false/nil (default): Best-effort — return the parsed JSON as-is.
160
+ # When nil, falls back to configuration.structured_output_strict.
161
+ attr_accessor :strict
155
162
 
156
163
  # All supported parameters with their defaults
157
164
  DEFAULTS = {
@@ -193,11 +200,12 @@ module OpenRouter
193
200
  # Responses API
194
201
  reasoning: nil,
195
202
  # Client-side
196
- force_structured_output: nil
203
+ native: nil,
204
+ strict: nil
197
205
  }.freeze
198
206
 
199
207
  # Parameters that are client-side only (not sent to API)
200
- CLIENT_SIDE_PARAMS = %i[force_structured_output extras].freeze
208
+ CLIENT_SIDE_PARAMS = %i[native strict extras].freeze
201
209
 
202
210
  # Initialize with keyword arguments
203
211
  #
@@ -133,7 +133,7 @@ module OpenRouter
133
133
  rescue StandardError => e
134
134
  # If the healing call itself fails, we can't proceed.
135
135
  # Return the original broken content to let the loop fail naturally.
136
- OpenRouter.log_warning("[OpenRouter Warning] JSON healing request failed: #{e.message}")
136
+ warn "[OpenRouter Warning] JSON healing request failed: #{e.message}"
137
137
 
138
138
  # Trigger callback for failed healing
139
139
  if @client.respond_to?(:trigger_callbacks)
@@ -58,8 +58,9 @@ module OpenRouter
58
58
  raise ServerError, "Empty response from OpenRouter. Might be worth retrying once or twice."
59
59
  end
60
60
 
61
- def build_response(raw_response, response_format, forced_extraction)
62
- response = Response.new(raw_response, response_format: response_format, forced_extraction: forced_extraction)
61
+ def build_response(raw_response, response_format, forced_extraction, strict: false)
62
+ response = Response.new(raw_response, response_format: response_format, forced_extraction: forced_extraction,
63
+ strict: strict)
63
64
  response.client = self
64
65
  response
65
66
  end
@@ -69,7 +70,7 @@ module OpenRouter
69
70
  end
70
71
 
71
72
  def warn_if_unsupported(model, capability, feature_name)
72
- return if model.is_a?(Array) || model.to_s.start_with?("openrouter/")
73
+ return if model.is_a?(Array) || model == "openrouter/auto"
73
74
  return if ModelRegistry.has_capability?(model, capability)
74
75
 
75
76
  if configuration.strict_mode
@@ -80,7 +81,7 @@ module OpenRouter
80
81
  warning_key = "#{model}:#{capability}"
81
82
  return if @capability_warnings_shown.include?(warning_key)
82
83
 
83
- OpenRouter.log_warning("[OpenRouter Warning] Model '#{model}' may not support #{feature_name} (missing :#{capability} capability). The request will still be attempted.")
84
+ warn "[OpenRouter Warning] Model '#{model}' may not support #{feature_name} (missing :#{capability} capability). The request will still be attempted."
84
85
  @capability_warnings_shown << warning_key
85
86
  end
86
87
 
@@ -10,13 +10,14 @@ module OpenRouter
10
10
  class Response
11
11
  include OpenRouter::ResponseParsing
12
12
 
13
- attr_reader :raw_response, :response_format, :forced_extraction
13
+ attr_reader :raw_response, :response_format, :forced_extraction, :strict
14
14
  attr_accessor :client
15
15
 
16
- def initialize(raw_response, response_format: nil, forced_extraction: false)
16
+ def initialize(raw_response, response_format: nil, forced_extraction: false, strict: false)
17
17
  @raw_response = raw_response.is_a?(Hash) ? raw_response.with_indifferent_access : {}
18
18
  @response_format = response_format
19
19
  @forced_extraction = forced_extraction
20
+ @strict = strict
20
21
  @client = nil
21
22
  end
22
23
 
@@ -80,14 +81,7 @@ module OpenRouter
80
81
 
81
82
  # Structured output methods
82
83
  def structured_output(mode: nil, auto_heal: nil)
83
- # Use global default mode if not specified
84
- if mode.nil?
85
- mode = if @client&.configuration.respond_to?(:default_structured_output_mode)
86
- @client.configuration.default_structured_output_mode || :strict
87
- else
88
- :strict
89
- end
90
- end
84
+ mode ||= default_structured_output_mode
91
85
  # Validate mode parameter
92
86
  raise ArgumentError, "Invalid mode: #{mode}. Must be :strict or :gentle." unless %i[strict gentle].include?(mode)
93
87
 
@@ -104,6 +98,10 @@ module OpenRouter
104
98
 
105
99
  result = parse_and_heal_structured_output(auto_heal: should_heal)
106
100
 
101
+ # In the json_object path (lenient extraction) a parse failure yields nil rather
102
+ # than raising. Strict mode must surface that as an error rather than returning nil.
103
+ raise StructuredOutputError, "Failed to parse structured output from response" if result.nil?
104
+
107
105
  # Only validate after parsing if healing is disabled (healing handles its own validation)
108
106
  if result && !should_heal
109
107
  schema_obj = extract_schema_from_response_format
@@ -185,10 +183,6 @@ module OpenRouter
185
183
  @raw_response["model"]
186
184
  end
187
185
 
188
- # Alias for #model — returns the concrete model the API/router used.
189
- # Useful for Pareto, Auto, and Fusion routing ("which model answered?").
190
- alias selected_model model
191
-
192
186
  def created
193
187
  @raw_response["created"]
194
188
  end
@@ -260,5 +254,13 @@ module OpenRouter
260
254
  def error_message
261
255
  @raw_response.dig("error", "message")
262
256
  end
257
+
258
+ private
259
+
260
+ # :strict raises on schema mismatch; :gentle returns best-effort. Driven by the
261
+ # `strict:` flag resolved at request time (per-call option or configured default).
262
+ def default_structured_output_mode
263
+ @strict ? :strict : :gentle
264
+ end
263
265
  end
264
266
  end
@@ -30,24 +30,40 @@ module OpenRouter
30
30
 
31
31
  # Convert to the format expected by OpenRouter API
32
32
  def to_h
33
- # Apply OpenRouter-specific transformations
34
- openrouter_schema = @schema.dup
35
-
36
- # OpenRouter/Azure requires ALL properties to be in the required array
37
- # even if they are logically optional. This is a deviation from JSON Schema spec
38
- # but necessary for compatibility.
39
- if openrouter_schema[:properties]&.any?
40
- all_properties = openrouter_schema[:properties].keys.map(&:to_s)
41
- openrouter_schema[:required] = all_properties
42
- end
43
-
44
33
  {
45
34
  name: @name,
46
35
  strict: @strict,
47
- schema: openrouter_schema
36
+ schema: enforce_all_required(@schema)
48
37
  }
49
38
  end
50
39
 
40
+ # OpenRouter / OpenAI strict mode requires EVERY object — at every nesting
41
+ # level, including nested objects and array items — to list all of its
42
+ # properties in its `required` array. Forcing this only at the top level
43
+ # makes nested objects come back with `required: []`, which strict providers
44
+ # reject with a 400. Walk the schema and enforce it recursively.
45
+ #
46
+ # (Optional fields are expressed in strict mode by adding "null" to the
47
+ # property's type union, not by omitting them from `required`.)
48
+ def enforce_all_required(node)
49
+ case node
50
+ when Hash
51
+ transformed = node.each_with_object({}) { |(key, value), acc| acc[key] = enforce_all_required(value) }
52
+
53
+ props = transformed[:properties] || transformed["properties"]
54
+ if props.is_a?(Hash) && props.any?
55
+ key = transformed.key?(:properties) ? :required : "required"
56
+ transformed[key] = props.keys.map(&:to_s)
57
+ end
58
+
59
+ transformed
60
+ when Array
61
+ node.map { |element| enforce_all_required(element) }
62
+ else
63
+ node
64
+ end
65
+ end
66
+
51
67
  # Get the pure JSON Schema (respects required flags) for testing/validation
52
68
  def pure_schema
53
69
  @schema
@@ -135,7 +135,7 @@ module OpenRouter
135
135
  @streaming_callbacks[event].each do |callback|
136
136
  callback.call(data)
137
137
  rescue StandardError => e
138
- OpenRouter.log_warning("[OpenRouter] Streaming callback error for #{event}: #{e.message}")
138
+ warn "[OpenRouter] Streaming callback error for #{event}: #{e.message}"
139
139
  end
140
140
  end
141
141
 
@@ -20,43 +20,35 @@ module OpenRouter
20
20
  parameters[:tool_choice] = opts.tool_choice if opts.tool_choice
21
21
  end
22
22
 
23
- # Returns forced_extraction boolean
23
+ # Configure the structured-output request.
24
+ #
25
+ # Default (native: false): ask the provider for a plain JSON object — the most
26
+ # widely supported response_format across models/providers — and describe the
27
+ # schema in the prompt. The model's capability registry never gates the request.
28
+ #
29
+ # Opt-in (native: true): send response_format: { type: "json_schema", ... } for
30
+ # grammar-constrained decoding. Only some models/providers support this; it can
31
+ # 400 on the rest, which is why it is explicit rather than auto-detected.
32
+ #
33
+ # Returns the lenient-extraction flag (true when the schema was injected into the
34
+ # prompt, so the response may need JSON extracted from surrounding text).
24
35
  def configure_structured_outputs!(parameters, opts)
25
36
  return false unless opts.response_format?
26
37
 
27
- force_extraction = determine_forced_extraction_mode(opts.model, opts.force_structured_output)
38
+ schema = extract_schema(opts.response_format)
28
39
 
29
- if force_extraction
30
- handle_forced_structured_output!(parameters, opts.model, opts.response_format)
31
- true
32
- else
33
- handle_native_structured_output!(parameters, opts.model, opts.response_format)
34
- false
35
- end
36
- end
37
-
38
- def determine_forced_extraction_mode(model, force_structured_output)
39
- return force_structured_output unless force_structured_output.nil?
40
-
41
- if model.is_a?(String) &&
42
- !model.start_with?("openrouter/") &&
43
- !ModelRegistry.has_capability?(model, :structured_outputs) &&
44
- configuration.auto_force_on_unsupported_models
45
- OpenRouter.log_warning("[OpenRouter] Model '#{model}' doesn't support native structured outputs. Automatically using forced extraction mode.")
46
- true
47
- else
48
- false
40
+ if opts.native && schema
41
+ warn_if_unsupported(opts.model, :structured_outputs, "structured outputs")
42
+ parameters[:response_format] = serialize_response_format(opts.response_format)
43
+ return false
49
44
  end
50
- end
51
45
 
52
- def handle_forced_structured_output!(parameters, model, response_format)
53
- warn_if_unsupported(model, :structured_outputs, "structured outputs") if configuration.strict_mode
54
- inject_schema_instructions!(parameters[:messages], response_format)
55
- end
46
+ # Default json_object path.
47
+ parameters[:response_format] = { type: "json_object" }
48
+ return false unless schema
56
49
 
57
- def handle_native_structured_output!(parameters, model, response_format)
58
- warn_if_unsupported(model, :structured_outputs, "structured outputs")
59
- parameters[:response_format] = serialize_response_format(response_format)
50
+ inject_schema_instructions!(parameters[:messages], schema)
51
+ true
60
52
  end
61
53
 
62
54
  # Serialize tools to Chat Completions API format: { type: "function", function: { name:, parameters: } }
@@ -113,8 +105,7 @@ module OpenRouter
113
105
  end
114
106
  end
115
107
 
116
- def inject_schema_instructions!(messages, response_format)
117
- schema = extract_schema(response_format)
108
+ def inject_schema_instructions!(messages, schema)
118
109
  return unless schema
119
110
 
120
111
  instruction_content = if schema.respond_to?(:get_format_instructions)
@@ -126,18 +117,15 @@ module OpenRouter
126
117
  messages << { role: "system", content: instruction_content }
127
118
  end
128
119
 
120
+ # Pull the schema out of a response_format. Returns nil for a plain
121
+ # { type: "json_object" } directive (no schema to describe or validate).
129
122
  def extract_schema(response_format)
130
123
  case response_format
131
124
  when Schema
132
125
  response_format
133
126
  when Hash
134
- if response_format[:json_schema].is_a?(Schema)
135
- response_format[:json_schema]
136
- elsif response_format[:json_schema].is_a?(Hash)
137
- response_format[:json_schema]
138
- else
139
- response_format
140
- end
127
+ json_schema = response_format[:json_schema] || response_format["json_schema"]
128
+ json_schema if json_schema.is_a?(Schema) || json_schema.is_a?(Hash)
141
129
  end
142
130
  end
143
131
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "2.2.0"
4
+ VERSION = "2.2.1"
5
5
  end
data/lib/open_router.rb CHANGED
@@ -19,7 +19,6 @@ end
19
19
  require_relative "open_router/http"
20
20
  require_relative "open_router/completion_options"
21
21
  require_relative "open_router/tool"
22
- require_relative "open_router/subagent_tool"
23
22
  require_relative "open_router/tool_call_base"
24
23
  require_relative "open_router/tool_call"
25
24
  require_relative "open_router/schema"
@@ -31,7 +30,6 @@ require_relative "open_router/model_registry"
31
30
  require_relative "open_router/model_selector"
32
31
  require_relative "open_router/prompt_template"
33
32
  require_relative "open_router/usage_tracker"
34
- require_relative "open_router/routing"
35
33
  require_relative "open_router/client"
36
34
  require_relative "open_router/streaming_client"
37
35
  require_relative "open_router/version"
@@ -56,15 +54,11 @@ module OpenRouter
56
54
  # Capability validation configuration
57
55
  attr_accessor :strict_mode
58
56
 
59
- # Automatic forcing configuration
60
- attr_accessor :auto_force_on_unsupported_models
61
-
62
- # Default structured output mode configuration
63
- attr_accessor :default_structured_output_mode
64
-
65
- # Optional logger. When set, gem warnings are routed through it (e.g. Rails.logger).
66
- # When nil (default), warnings go to Kernel.warn → $stderr.
67
- attr_accessor :logger
57
+ # Default validation strictness for structured outputs.
58
+ # false (default): best-effort, return parsed JSON as-is.
59
+ # true: raise StructuredOutputError when the response doesn't match the schema.
60
+ # Overridden per-call by the `strict:` option.
61
+ attr_accessor :structured_output_strict
68
62
 
69
63
  DEFAULT_API_VERSION = "v1"
70
64
  DEFAULT_REQUEST_TIMEOUT = 120
@@ -99,11 +93,8 @@ module OpenRouter
99
93
  # Capability validation defaults
100
94
  self.strict_mode = ENV.fetch("OPENROUTER_STRICT_MODE", "false").downcase == "true"
101
95
 
102
- # Auto forcing defaults
103
- self.auto_force_on_unsupported_models = ENV.fetch("OPENROUTER_AUTO_FORCE", "true").downcase == "true"
104
-
105
- # Default structured output mode
106
- self.default_structured_output_mode = ENV.fetch("OPENROUTER_DEFAULT_MODE", "strict").to_sym
96
+ # Default structured output validation strictness (loose/best-effort by default)
97
+ self.structured_output_strict = ENV.fetch("OPENROUTER_STRUCTURED_STRICT", "false").downcase == "true"
107
98
  end
108
99
 
109
100
  def access_token
@@ -136,12 +127,4 @@ module OpenRouter
136
127
  def self.configure
137
128
  yield(configuration)
138
129
  end
139
-
140
- def self.log_warning(message)
141
- if configuration.logger
142
- configuration.logger.warn(message)
143
- else
144
- Kernel.warn(message)
145
- end
146
- end
147
130
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open_router_enhanced
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Stiens
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-29 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -134,8 +134,6 @@ files:
134
134
  - docs/responses_api.md
135
135
  - docs/streaming.md
136
136
  - docs/structured_outputs.md
137
- - docs/superpowers/plans/2026-06-27-openrouter-routing-features.md
138
- - docs/superpowers/specs/2026-06-27-openrouter-routing-features-design.md
139
137
  - docs/tools.md
140
138
  - examples/basic_completion.rb
141
139
  - examples/dynamic_model_switching_example.rb
@@ -164,10 +162,8 @@ files:
164
162
  - lib/open_router/response_parsing.rb
165
163
  - lib/open_router/responses_response.rb
166
164
  - lib/open_router/responses_tool_call.rb
167
- - lib/open_router/routing.rb
168
165
  - lib/open_router/schema.rb
169
166
  - lib/open_router/streaming_client.rb
170
- - lib/open_router/subagent_tool.rb
171
167
  - lib/open_router/tool.rb
172
168
  - lib/open_router/tool_call.rb
173
169
  - lib/open_router/tool_call_base.rb