open_router_enhanced 2.2.1 → 2.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f4936ab4591643976d7cf5f63b92fec61fef1fda2900ce6ed022ce35671bd3a
4
- data.tar.gz: 1ac1e6ea82c9eacc1bc28e4b99490507bcf1f335446f3ab1cbab2a2ca8d749b4
3
+ metadata.gz: '08f395125182c8bc6fdedec8b3c1057750344bc56f5988373a5a4deaaccf6585'
4
+ data.tar.gz: c98f3b9e15d8838b0fcc160c55a7b9ae0a4ade6fe05495d3663e016666c48914
5
5
  SHA512:
6
- metadata.gz: ef3a651dee0fec1b4d0508e88578c78ed1d259b5dc7c5fb5917d7af0690a2e69c12732f6fc5a0b423340da7afddbc4de80f08f8a27e616c9d7c15a1051508a39
7
- data.tar.gz: 017ec8a29aeb205e396265409c8f9d53f3f7280d025543e89b2c95510d6b21fe78f230461dcc5291a433c1b60f850425ba754dcfc1ab32e98517b1f522688692
6
+ metadata.gz: 5526e571d28a4c3d93726d67f4441c90890980c929b36938e2ed0f8137c29e0ac8773a7d438de4422119e0660522334447bab2585837333c2ae1651f79ef0cd9
7
+ data.tar.gz: 6977214b59795346e76855586eb04ff50f173e45ea6189ad1b514ad80ff5edabb1a68f433b8535977789516685eb321db6d820ca7fe18564821c9cd392a9ecf7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.3.0] - 2026-07-08
4
+
5
+ ### Fixed
6
+
7
+ - **Structured output schemas now honor optional keys.** `Schema#to_h` previously forced *every* property — at every nesting level — into the `required` array, so fields declared with `required: false` (the DSL default) were silently made mandatory in the prompt, the native `json_schema` payload, and the healer. `to_h` now emits the schema honestly, respecting the `required` arrays exactly as declared. Optional fields stay optional and the model may omit them.
8
+ - `to_h` and local validation now agree, so what is described to the model matches what is accepted.
9
+
10
+ ### Added
11
+
12
+ - `Schema#to_strict_h` — the provider-strict form for native `json_schema` decoding. It still lists every property in `required` (so strict providers don't `400` on nested `required: []`), but expresses optionality the correct way: fields not declared required are made **nullable** (their type gains `"null"`) rather than mandatory. The native serialization path uses this form automatically for strict schemas.
13
+
3
14
  ## [2.0.0] - 2025-12-28
4
15
 
5
16
  ### Overview
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- open_router_enhanced (2.2.1)
4
+ open_router_enhanced (2.3.0)
5
5
  activesupport (>= 6.0, < 9.0)
6
6
  dotenv (>= 2.0, < 4.0)
7
7
  faraday (>= 1.0, < 3.0)
@@ -28,40 +28,37 @@ module OpenRouter
28
28
  new(name, builder.to_h, strict:)
29
29
  end
30
30
 
31
- # Convert to the format expected by OpenRouter API
31
+ # Convert to the format expected by OpenRouter API.
32
+ #
33
+ # This is the *honest* representation: it respects the `required` arrays
34
+ # exactly as declared, so fields you left optional stay optional. It is what
35
+ # we describe to the model (prompt injection), what we validate against, and
36
+ # what we hand the healer — keeping "what we ask for" and "what we accept" in
37
+ # agreement. The provider-strict, all-required form lives in #to_strict_h and
38
+ # is used only when serializing a native `json_schema` request.
32
39
  def to_h
33
40
  {
34
41
  name: @name,
35
42
  strict: @strict,
36
- schema: enforce_all_required(@schema)
43
+ schema: @schema
37
44
  }
38
45
  end
39
46
 
47
+ # Provider-strict form for native `json_schema` decoding.
48
+ #
40
49
  # OpenRouter / OpenAI strict mode requires EVERY object — at every nesting
41
50
  # 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
51
+ # properties in its `required` array; a nested `required: []` gets a 400.
52
+ # We satisfy that WITHOUT silently making optional fields mandatory: any
53
+ # property that was not declared required is made nullable (its type gains
54
+ # "null"), which is exactly how strict mode expresses optionality. The model
55
+ # may then return `null` for it instead of being forced to invent a value.
56
+ def to_strict_h
57
+ {
58
+ name: @name,
59
+ strict: @strict,
60
+ schema: enforce_all_required(@schema)
61
+ }
65
62
  end
66
63
 
67
64
  # Get the pure JSON Schema (respects required flags) for testing/validation
@@ -137,6 +134,51 @@ module OpenRouter
137
134
 
138
135
  private
139
136
 
137
+ # Recursively force every object to list all its properties in `required`,
138
+ # keeping declared-optional properties optional by making them nullable.
139
+ # Reads each node's OWN existing `required` before overwriting, so nested
140
+ # optionality is preserved at every level.
141
+ def enforce_all_required(node)
142
+ case node
143
+ when Hash
144
+ transformed = node.each_with_object({}) { |(key, value), acc| acc[key] = enforce_all_required(value) }
145
+
146
+ props = transformed[:properties] || transformed["properties"]
147
+ if props.is_a?(Hash) && props.any?
148
+ key = transformed.key?(:properties) ? :required : "required"
149
+ already_required = Array(transformed[key]).map(&:to_s)
150
+ props.each do |prop_name, prop_def|
151
+ next if already_required.include?(prop_name.to_s)
152
+
153
+ props[prop_name] = make_nullable(prop_def)
154
+ end
155
+ transformed[key] = props.keys.map(&:to_s)
156
+ end
157
+
158
+ transformed
159
+ when Array
160
+ node.map { |element| enforce_all_required(element) }
161
+ else
162
+ node
163
+ end
164
+ end
165
+
166
+ # Add "null" to a property's type union so it can be omitted (as null) under
167
+ # strict mode. Leaves the property untouched if it has no determinable type.
168
+ def make_nullable(prop_def)
169
+ return prop_def unless prop_def.is_a?(Hash)
170
+
171
+ type_key = if prop_def.key?(:type) then :type
172
+ elsif prop_def.key?("type") then "type"
173
+ end
174
+ return prop_def unless type_key
175
+
176
+ types = Array(prop_def[type_key]).map(&:to_s)
177
+ return prop_def if types.include?("null")
178
+
179
+ prop_def.merge(type_key => types + ["null"])
180
+ end
181
+
140
182
  def validate_schema!
141
183
  raise ArgumentError, "Schema name is required" if @name.nil? || @name.empty?
142
184
  raise ArgumentError, "Schema must be a hash" unless @schema.is_a?(Hash)
@@ -94,17 +94,24 @@ module OpenRouter
94
94
  case response_format
95
95
  when Hash
96
96
  if response_format[:json_schema].is_a?(Schema)
97
- response_format.merge(json_schema: response_format[:json_schema].to_h)
97
+ response_format.merge(json_schema: native_schema_hash(response_format[:json_schema]))
98
98
  else
99
99
  response_format
100
100
  end
101
101
  when Schema
102
- { type: "json_schema", json_schema: response_format.to_h }
102
+ { type: "json_schema", json_schema: native_schema_hash(response_format) }
103
103
  else
104
104
  response_format
105
105
  end
106
106
  end
107
107
 
108
+ # Provider-side json_schema decoding requires every property in `required`.
109
+ # For strict schemas we emit the all-required-with-nullable-optionals form so
110
+ # the request doesn't 400; non-strict schemas are sent honestly.
111
+ def native_schema_hash(schema)
112
+ schema.strict ? schema.to_strict_h : schema.to_h
113
+ end
114
+
108
115
  def inject_schema_instructions!(messages, schema)
109
116
  return unless schema
110
117
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenRouter
4
- VERSION = "2.2.1"
4
+ VERSION = "2.3.0"
5
5
  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.1
4
+ version: 2.3.0
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-07-01 00:00:00.000000000 Z
11
+ date: 2026-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport