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 +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +1 -1
- data/lib/open_router/schema.rb +67 -25
- data/lib/open_router/tool_serializer.rb +9 -2
- data/lib/open_router/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '08f395125182c8bc6fdedec8b3c1057750344bc56f5988373a5a4deaaccf6585'
|
|
4
|
+
data.tar.gz: c98f3b9e15d8838b0fcc160c55a7b9ae0a4ade6fe05495d3663e016666c48914
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/lib/open_router/schema.rb
CHANGED
|
@@ -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:
|
|
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
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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]
|
|
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
|
|
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
|
|
data/lib/open_router/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-07-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|