riffer 0.52.1 → 0.54.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/.claude/rules/rbs-inline.md +2 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +22 -0
- data/Steepfile +19 -0
- data/docs/AGENTS.md +81 -4
- data/docs/CATALOG.md +205 -0
- data/docs/CONFIGURATION.md +35 -23
- data/docs/MESSAGES.md +1 -1
- data/docs/OVERVIEW.md +1 -1
- data/docs/SERIALIZATION.md +1 -0
- data/docs/TOOLS.md +15 -12
- data/docs/TRACING.md +2 -1
- data/docs/providers/AMAZON_BEDROCK.md +2 -0
- data/docs/providers/ANTHROPIC.md +2 -0
- data/docs/providers/AZURE_OPENAI.md +2 -0
- data/docs/providers/CUSTOM_PROVIDERS.md +15 -1
- data/docs/providers/GEMINI.md +11 -0
- data/docs/providers/MOCK_PROVIDER.md +2 -0
- data/docs/providers/OPENAI.md +2 -0
- data/docs/providers/OPENROUTER.md +2 -0
- data/docs-site/manifest.yml +3 -0
- data/lib/riffer/agent/config.rb +17 -0
- data/lib/riffer/agent/run.rb +2 -0
- data/lib/riffer/agent/serializer.rb +2 -0
- data/lib/riffer/agent/structured_output.rb +23 -3
- data/lib/riffer/agent.rb +7 -0
- data/lib/riffer/{config/pricing → catalog}/rates.rb +21 -6
- data/lib/riffer/catalog.rb +225 -0
- data/lib/riffer/config/pricing.rb +17 -27
- data/lib/riffer/config.rb +29 -0
- data/lib/riffer/params/any_of.rb +22 -0
- data/lib/riffer/params/format.rb +73 -0
- data/lib/riffer/params/param.rb +120 -17
- data/lib/riffer/params.rb +135 -24
- data/lib/riffer/providers/amazon_bedrock.rb +14 -0
- data/lib/riffer/providers/anthropic.rb +8 -0
- data/lib/riffer/providers/base.rb +67 -15
- data/lib/riffer/providers/gemini.rb +7 -0
- data/lib/riffer/providers/mock.rb +7 -0
- data/lib/riffer/providers/open_ai.rb +6 -0
- data/lib/riffer/providers/open_router.rb +6 -0
- data/lib/riffer/version.rb +1 -1
- data/lib/riffer.rb +2 -0
- data/rbs_collection.lock.yaml +8 -0
- data/rbs_collection.yaml +2 -0
- data/sig/generated/riffer/agent/config.rbs +10 -1
- data/sig/generated/riffer/agent/structured_output.rbs +6 -0
- data/sig/generated/riffer/agent.rbs +4 -0
- data/sig/generated/riffer/{config/pricing → catalog}/rates.rbs +10 -4
- data/sig/generated/riffer/catalog.rbs +77 -0
- data/sig/generated/riffer/config/pricing.rbs +12 -11
- data/sig/generated/riffer/config.rbs +17 -0
- data/sig/generated/riffer/params/any_of.rbs +13 -0
- data/sig/generated/riffer/params/format.rbs +39 -0
- data/sig/generated/riffer/params/param.rbs +22 -4
- data/sig/generated/riffer/params.rbs +34 -8
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +6 -0
- data/sig/generated/riffer/providers/anthropic.rbs +4 -0
- data/sig/generated/riffer/providers/base.rbs +22 -2
- data/sig/generated/riffer/providers/gemini.rbs +4 -0
- data/sig/generated/riffer/providers/mock.rbs +5 -0
- data/sig/generated/riffer/providers/open_ai.rbs +4 -0
- data/sig/generated/riffer/providers/open_router.rbs +4 -0
- data/sig/generated/riffer.rbs +1 -0
- data/sig/manifest.yaml +2 -1
- data/sig/manual/riffer/params/format.rbs +5 -0
- metadata +11 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "date"
|
|
5
|
+
require "ipaddr"
|
|
6
|
+
|
|
7
|
+
module Riffer::Params::Format
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
DATE = /\A(\d{4})-(\d{2})-(\d{2})\z/ #: Regexp
|
|
11
|
+
|
|
12
|
+
DATE_TIME = /\A(\d{4})-(\d{2})-(\d{2})[Tt]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)\z/ #: Regexp
|
|
13
|
+
|
|
14
|
+
# The offset is optional: Anthropic's grammar emits times without one.
|
|
15
|
+
TIME = /\A([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)?\z/ #: Regexp
|
|
16
|
+
|
|
17
|
+
DURATION = /\AP(?!\z)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|\d+W)\z/ #: Regexp
|
|
18
|
+
|
|
19
|
+
UUID = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/ #: Regexp
|
|
20
|
+
|
|
21
|
+
# The WHATWG HTML pattern: no single pattern matches RFC 5322 exactly, so
|
|
22
|
+
# this one only rules out values that are clearly not an address.
|
|
23
|
+
EMAIL = %r{\A[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\z} #: Regexp
|
|
24
|
+
|
|
25
|
+
HOSTNAME = /\A(?=.{1,253}\.?\z)[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.?\z/ #: Regexp
|
|
26
|
+
|
|
27
|
+
# The formats OpenAI, Anthropic and Bedrock all accept. OpenAI rejects uri.
|
|
28
|
+
CHECKS = {
|
|
29
|
+
"date-time" => ->(value) { calendar_date?(DATE_TIME, value) },
|
|
30
|
+
"time" => ->(value) { TIME.match?(value) },
|
|
31
|
+
"date" => ->(value) { calendar_date?(DATE, value) },
|
|
32
|
+
"duration" => ->(value) { DURATION.match?(value) },
|
|
33
|
+
"email" => ->(value) { EMAIL.match?(value) },
|
|
34
|
+
"hostname" => ->(value) { HOSTNAME.match?(value) },
|
|
35
|
+
"ipv4" => ->(value) { ip_address?(value, &:ipv4?) },
|
|
36
|
+
"ipv6" => ->(value) { ip_address?(value, &:ipv6?) },
|
|
37
|
+
"uuid" => ->(value) { UUID.match?(value) },
|
|
38
|
+
}.freeze #: Hash[String, ^(String) -> bool]
|
|
39
|
+
|
|
40
|
+
private_constant :DATE, :DATE_TIME, :TIME, :DURATION, :UUID, :EMAIL, :HOSTNAME, :CHECKS
|
|
41
|
+
|
|
42
|
+
NAMES = CHECKS.keys.freeze #: Array[String]
|
|
43
|
+
|
|
44
|
+
#--
|
|
45
|
+
#: (String, String) -> bool
|
|
46
|
+
def valid?(format, value)
|
|
47
|
+
CHECKS.fetch(format).call(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
#--
|
|
53
|
+
#: (Regexp, String) -> bool
|
|
54
|
+
def calendar_date?(pattern, value)
|
|
55
|
+
match = pattern.match(value)
|
|
56
|
+
return false unless match
|
|
57
|
+
|
|
58
|
+
# RFC 3339 dates are proleptic Gregorian; Date defaults to the Julian calendar before 1582.
|
|
59
|
+
Date.valid_date?(match[1].to_i, match[2].to_i, match[3].to_i, Date::GREGORIAN)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#--
|
|
63
|
+
#: (String) { (IPAddr) -> bool } -> bool
|
|
64
|
+
def ip_address?(value)
|
|
65
|
+
# IPAddr also accepts a prefix (10.0.0.0/24), a zone ID (fe80::1%eth0) and
|
|
66
|
+
# brackets ([::1]), none of which are part of an address.
|
|
67
|
+
return false if value.match?(%r{[/%\[\]]})
|
|
68
|
+
|
|
69
|
+
yield IPAddr.new(value)
|
|
70
|
+
rescue IPAddr::Error
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/riffer/params/param.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
4
6
|
class Riffer::Params::Param
|
|
5
7
|
TYPE_MAPPINGS = {
|
|
6
8
|
String => "string",
|
|
@@ -34,43 +36,99 @@ class Riffer::Params::Param
|
|
|
34
36
|
|
|
35
37
|
attr_reader :enum #: Array[untyped]? # @dynamic enum
|
|
36
38
|
|
|
39
|
+
attr_reader :format #: String? # @dynamic format
|
|
40
|
+
|
|
37
41
|
attr_reader :default #: untyped # @dynamic default
|
|
38
42
|
|
|
39
43
|
attr_reader :item_type #: Module? # @dynamic item_type
|
|
40
44
|
|
|
45
|
+
attr_reader :dimensions #: Integer # @dynamic dimensions
|
|
46
|
+
|
|
41
47
|
attr_reader :nested_params #: Riffer::Params? # @dynamic nested_params
|
|
42
48
|
|
|
49
|
+
attr_reader :variants #: Array[Riffer::Params]? # @dynamic variants
|
|
50
|
+
|
|
43
51
|
#--
|
|
44
52
|
#: (Symbol, Hash[Symbol, untyped], required: bool) -> Riffer::Params::Param
|
|
45
53
|
def self.from_json_schema(name, schema, required:)
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
variants = resolve_variants(schema[:anyOf]) if schema.key?(:anyOf)
|
|
55
|
+
ruby_type = variants ? Riffer::Params::AnyOf : json_type_to_ruby(schema[:type])
|
|
56
|
+
nested = resolve_nested_params(ruby_type, schema)
|
|
57
|
+
item_type, dimensions, items = resolve_typed_items(schema) if ruby_type == Array && !nested
|
|
48
58
|
|
|
49
59
|
new(
|
|
50
60
|
name: name,
|
|
51
61
|
type: ruby_type,
|
|
52
62
|
required: required,
|
|
53
63
|
description: schema[:description],
|
|
54
|
-
enum: schema[:enum],
|
|
64
|
+
enum: schema[:enum] || items&.dig(:enum),
|
|
65
|
+
format: schema[:format] || items&.dig(:format),
|
|
55
66
|
default: schema[:default],
|
|
56
67
|
item_type: item_type,
|
|
68
|
+
dimensions: dimensions || 1,
|
|
57
69
|
nested_params: nested,
|
|
70
|
+
variants: variants,
|
|
58
71
|
)
|
|
59
72
|
end
|
|
60
73
|
|
|
61
74
|
#--
|
|
62
|
-
#: (
|
|
63
|
-
def self.
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
#: (Array[Hash[Symbol, untyped]]) -> Array[Riffer::Params]
|
|
76
|
+
def self.resolve_variants(branches)
|
|
77
|
+
# Strict schemas add a null branch to optional fields; nullability comes from +required+.
|
|
78
|
+
variants = branches.reject { |branch| branch[:type] == "null" }.map do |branch|
|
|
79
|
+
raise Riffer::ArgumentError, "Unsupported anyOf branch: #{branch.to_json}" unless branch[:type] == "object"
|
|
66
80
|
|
|
81
|
+
Riffer::Params.from_json_schema(branch)
|
|
82
|
+
end
|
|
83
|
+
raise Riffer::ArgumentError, "anyOf requires at least two object branches" if variants.size < 2
|
|
84
|
+
|
|
85
|
+
variants
|
|
86
|
+
end
|
|
87
|
+
private_class_method :resolve_variants
|
|
88
|
+
|
|
89
|
+
#--
|
|
90
|
+
#: (Module, Hash[Symbol, untyped]) -> Riffer::Params?
|
|
91
|
+
def self.resolve_nested_params(ruby_type, schema)
|
|
92
|
+
if ruby_type == Hash && schema[:properties]
|
|
93
|
+
Riffer::Params.from_json_schema(schema)
|
|
94
|
+
elsif ruby_type == Array && schema[:items].is_a?(Hash) && schema[:items][:properties]
|
|
95
|
+
Riffer::Params.from_json_schema(schema[:items])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_class_method :resolve_nested_params
|
|
99
|
+
|
|
100
|
+
#--
|
|
101
|
+
#: (Hash[Symbol, untyped]) -> [Module, Integer, Hash[Symbol, untyped]]?
|
|
102
|
+
def self.resolve_typed_items(schema)
|
|
67
103
|
items = schema[:items]
|
|
68
|
-
return
|
|
69
|
-
return [nil, Riffer::Params.from_json_schema(items)] if items[:properties]
|
|
104
|
+
return unless items.is_a?(Hash)
|
|
70
105
|
|
|
71
|
-
|
|
106
|
+
dimensions = 1
|
|
107
|
+
# Riffer only writes nested arrays with enum/format and a single primitive type on the innermost
|
|
108
|
+
# items, and no nullable inner arrays; any other shape would read back with a different meaning.
|
|
109
|
+
while json_type_to_ruby(items[:type]) == Array
|
|
110
|
+
inner = items[:items]
|
|
111
|
+
unless items[:type] == "array" && inner.is_a?(Hash) && !items.keys.intersect?(%i[enum format])
|
|
112
|
+
raise Riffer::ArgumentError, "Unsupported nested array: #{items.to_json}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
items = inner
|
|
116
|
+
dimensions += 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
item_type = json_type_to_ruby(items[:type])
|
|
120
|
+
if dimensions > 1
|
|
121
|
+
if schema.keys.intersect?(%i[enum format])
|
|
122
|
+
raise Riffer::ArgumentError, "Unsupported nested array: #{schema.to_json}"
|
|
123
|
+
end
|
|
124
|
+
unless items[:type].is_a?(String) && PRIMITIVE_TYPES.include?(item_type)
|
|
125
|
+
raise Riffer::ArgumentError, "Unsupported nested array items: #{items.to_json}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
[item_type, dimensions, items]
|
|
72
130
|
end
|
|
73
|
-
private_class_method :
|
|
131
|
+
private_class_method :resolve_typed_items
|
|
74
132
|
|
|
75
133
|
#--
|
|
76
134
|
#: (untyped) -> Module
|
|
@@ -81,16 +139,27 @@ class Riffer::Params::Param
|
|
|
81
139
|
private_class_method :json_type_to_ruby
|
|
82
140
|
|
|
83
141
|
#--
|
|
84
|
-
#: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?default: untyped, ?item_type: Module?, ?nested_params: Riffer::Params?) -> void
|
|
85
|
-
def initialize(name:, type:, required:, description: nil, enum: nil,
|
|
142
|
+
#: (name: Symbol, type: Module, required: bool, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?item_type: Module?, ?dimensions: Integer, ?nested_params: Riffer::Params?, ?variants: Array[Riffer::Params]?) -> void
|
|
143
|
+
def initialize(name:, type:, required:, description: nil, enum: nil, format: nil, default: nil, item_type: nil,
|
|
144
|
+
dimensions: 1, nested_params: nil, variants: nil)
|
|
145
|
+
if type == Riffer::Params::AnyOf && variants.nil?
|
|
146
|
+
raise Riffer::ArgumentError, "#{name}: an AnyOf param requires variants"
|
|
147
|
+
end
|
|
148
|
+
|
|
86
149
|
@name = name.to_sym
|
|
87
150
|
@type = type
|
|
88
151
|
@required = required
|
|
89
152
|
@description = description
|
|
90
153
|
@enum = enum
|
|
154
|
+
# Frozen and deduplicated, so copies never share a mutable string.
|
|
155
|
+
@format = format.is_a?(String) ? -format : format
|
|
91
156
|
@default = default
|
|
92
157
|
@item_type = item_type
|
|
158
|
+
@dimensions = dimensions
|
|
93
159
|
@nested_params = nested_params
|
|
160
|
+
@variants = variants
|
|
161
|
+
# Checked here rather than in Params so from_json_schema enforces it too.
|
|
162
|
+
validate_format! if format
|
|
94
163
|
end
|
|
95
164
|
|
|
96
165
|
#--
|
|
@@ -102,6 +171,8 @@ class Riffer::Params::Param
|
|
|
102
171
|
[true, false].include?(value)
|
|
103
172
|
elsif type == Float
|
|
104
173
|
value.is_a?(Numeric)
|
|
174
|
+
elsif type == Riffer::Params::AnyOf
|
|
175
|
+
value.is_a?(Hash)
|
|
105
176
|
else
|
|
106
177
|
value.is_a?(type)
|
|
107
178
|
end
|
|
@@ -110,6 +181,8 @@ class Riffer::Params::Param
|
|
|
110
181
|
#--
|
|
111
182
|
#: () -> String
|
|
112
183
|
def type_name
|
|
184
|
+
return "object" if type == Riffer::Params::AnyOf
|
|
185
|
+
|
|
113
186
|
TYPE_MAPPINGS[type] || type.to_s.downcase
|
|
114
187
|
end
|
|
115
188
|
|
|
@@ -120,9 +193,21 @@ class Riffer::Params::Param
|
|
|
120
193
|
# Nullable so strict providers can distinguish absent from present.
|
|
121
194
|
nullable = strict && !required
|
|
122
195
|
|
|
196
|
+
if type == Riffer::Params::AnyOf && variants
|
|
197
|
+
schema = { anyOf: variants.map { |variant| variant.to_json_schema(strict: strict) } } #: Hash[Symbol, untyped]
|
|
198
|
+
schema[:anyOf] << { type: "null" } if nullable
|
|
199
|
+
schema[:description] = description if description
|
|
200
|
+
schema[:default] = default unless strict || default.nil?
|
|
201
|
+
return schema
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# A typed array's enum and format constrain its items, not the array.
|
|
205
|
+
value_enum = enum unless item_type
|
|
206
|
+
value_format = format unless item_type
|
|
207
|
+
|
|
123
208
|
# Providers like Anthropic reject a nullable type union combined with enum.
|
|
124
|
-
if nullable &&
|
|
125
|
-
schema = { anyOf: [{ type: type_name, enum:
|
|
209
|
+
if nullable && value_enum
|
|
210
|
+
schema = { anyOf: [{ type: type_name, enum: value_enum }, { type: "null" }] } #: Hash[Symbol, untyped]
|
|
126
211
|
schema[:description] = description if description
|
|
127
212
|
return schema
|
|
128
213
|
end
|
|
@@ -132,7 +217,8 @@ class Riffer::Params::Param
|
|
|
132
217
|
|
|
133
218
|
schema = { type: type } #: Hash[Symbol, untyped]
|
|
134
219
|
schema[:description] = description if description
|
|
135
|
-
schema[:enum] =
|
|
220
|
+
schema[:enum] = value_enum if value_enum
|
|
221
|
+
schema[:format] = value_format if value_format
|
|
136
222
|
# Strict providers reject the +default+ keyword; emit it only in
|
|
137
223
|
# non-strict mode, where it makes the schema a lossless round-trip
|
|
138
224
|
# source for +from_json_schema+.
|
|
@@ -141,7 +227,9 @@ class Riffer::Params::Param
|
|
|
141
227
|
if self.type == Array && nested_params
|
|
142
228
|
schema[:items] = nested_params.to_json_schema(strict: strict)
|
|
143
229
|
elsif self.type == Array && item_type
|
|
144
|
-
|
|
230
|
+
items = { type: TYPE_MAPPINGS[item_type], enum: enum, format: format }.compact #: Hash[Symbol, untyped]
|
|
231
|
+
(dimensions - 1).times { items = { type: "array", items: items } }
|
|
232
|
+
schema[:items] = items
|
|
145
233
|
elsif self.type == Hash && nested_params
|
|
146
234
|
# The nested schema carries its own type: "object", which would clobber a nullable union.
|
|
147
235
|
schema.merge!(nested_params.to_json_schema(strict: strict).except(:type))
|
|
@@ -160,10 +248,25 @@ class Riffer::Params::Param
|
|
|
160
248
|
def initialize_copy(source)
|
|
161
249
|
super
|
|
162
250
|
@nested_params = source.nested_params&.dup
|
|
251
|
+
@variants = source.variants&.map(&:dup)
|
|
163
252
|
@enum = Riffer::Helpers::DeepDup.call(source.enum)
|
|
164
253
|
@default = Riffer::Helpers::DeepDup.call(source.default)
|
|
165
254
|
end
|
|
166
255
|
|
|
256
|
+
#--
|
|
257
|
+
#: () -> void
|
|
258
|
+
def validate_format!
|
|
259
|
+
unless Riffer::Params::Format::NAMES.include?(format)
|
|
260
|
+
raise Riffer::ArgumentError,
|
|
261
|
+
"#{name}: format: must be a supported format (#{Riffer::Params::Format::NAMES.join(', ')}), " \
|
|
262
|
+
"got #{format.inspect}"
|
|
263
|
+
end
|
|
264
|
+
unless type == String || (type == Array && item_type == String)
|
|
265
|
+
raise Riffer::ArgumentError, "#{name}: format: requires a String or an Array whose innermost type is String"
|
|
266
|
+
end
|
|
267
|
+
raise Riffer::ArgumentError, "#{name}: format: cannot be used with enum:" if enum
|
|
268
|
+
end
|
|
269
|
+
|
|
167
270
|
#--
|
|
168
271
|
#: () -> void
|
|
169
272
|
def validate_strict_shape!
|
data/lib/riffer/params.rb
CHANGED
|
@@ -29,33 +29,45 @@ class Riffer::Params
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
#--
|
|
32
|
-
#: (Symbol,
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
#: (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
33
|
+
#: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
34
|
+
def required(name, type, description: nil, enum: nil, format: nil, of: nil, &)
|
|
35
|
+
item_type, dimensions = unwrap_of(of)
|
|
36
|
+
nested = build_nested(type, item_type, enum, &) unless type == Riffer::Params::AnyOf
|
|
37
|
+
variants = build_variants(of, enum, &) if type == Riffer::Params::AnyOf
|
|
35
38
|
@parameters << Riffer::Params::Param.new(
|
|
36
39
|
name: name,
|
|
37
40
|
type: type,
|
|
38
41
|
required: true,
|
|
39
42
|
description: description,
|
|
40
43
|
enum: enum,
|
|
41
|
-
|
|
44
|
+
format: format,
|
|
45
|
+
item_type: item_type,
|
|
46
|
+
dimensions: dimensions,
|
|
42
47
|
nested_params: nested,
|
|
48
|
+
variants: variants,
|
|
43
49
|
)
|
|
44
50
|
end
|
|
45
51
|
|
|
46
52
|
#--
|
|
47
|
-
#: (Symbol,
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
#: (Symbol, singleton(Riffer::Params::AnyOf), ?description: String?, ?default: untyped) { (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> void
|
|
54
|
+
#: (Symbol, Module, ?description: String?, ?enum: Array[untyped]?, ?format: String?, ?default: untyped, ?of: (Module | Array[untyped])?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> void
|
|
55
|
+
def optional(name, type, description: nil, enum: nil, format: nil, default: nil, of: nil, &)
|
|
56
|
+
item_type, dimensions = unwrap_of(of)
|
|
57
|
+
nested = build_nested(type, item_type, enum, &) unless type == Riffer::Params::AnyOf
|
|
58
|
+
variants = build_variants(of, enum, &) if type == Riffer::Params::AnyOf
|
|
50
59
|
@parameters << Riffer::Params::Param.new(
|
|
51
60
|
name: name,
|
|
52
61
|
type: type,
|
|
53
62
|
required: false,
|
|
54
63
|
description: description,
|
|
55
64
|
enum: enum,
|
|
65
|
+
format: format,
|
|
56
66
|
default: default,
|
|
57
|
-
item_type:
|
|
67
|
+
item_type: item_type,
|
|
68
|
+
dimensions: dimensions,
|
|
58
69
|
nested_params: nested,
|
|
70
|
+
variants: variants,
|
|
59
71
|
)
|
|
60
72
|
end
|
|
61
73
|
|
|
@@ -83,11 +95,17 @@ class Riffer::Params
|
|
|
83
95
|
next
|
|
84
96
|
end
|
|
85
97
|
|
|
86
|
-
|
|
98
|
+
# A typed array's enum and format constrain its items, which validate_typed_array checks.
|
|
99
|
+
if param.enum && !param.item_type && !param.enum.include?(value)
|
|
87
100
|
errors << "#{param.name} must be one of: #{param.enum.join(', ')}"
|
|
88
101
|
next
|
|
89
102
|
end
|
|
90
103
|
|
|
104
|
+
if param.format && !param.item_type && !Riffer::Params::Format.valid?(param.format, value)
|
|
105
|
+
errors << "#{param.name} must be a valid #{param.format}"
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
|
|
91
109
|
value = validate_nested(param, value, errors)
|
|
92
110
|
|
|
93
111
|
validated[param.name] = coerce_value(param.type, value)
|
|
@@ -131,9 +149,20 @@ class Riffer::Params
|
|
|
131
149
|
end
|
|
132
150
|
|
|
133
151
|
#--
|
|
134
|
-
#: (Module
|
|
135
|
-
def
|
|
152
|
+
#: ((Module | Array[untyped])?, ?Integer) -> [Module?, Integer]
|
|
153
|
+
def unwrap_of(of, dimensions = 1)
|
|
154
|
+
return [of, dimensions] unless of.is_a?(Array)
|
|
155
|
+
raise Riffer::ArgumentError, "of: must nest exactly one type, got #{of.inspect}" unless of.size == 1 && of.first
|
|
156
|
+
|
|
157
|
+
unwrap_of(of.first, dimensions + 1)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
#--
|
|
161
|
+
#: (Module, Module?, Array[untyped]?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
|
|
162
|
+
def build_nested(type, of, enum, &block)
|
|
136
163
|
raise Riffer::ArgumentError, "cannot use both of: and a block" if of && block
|
|
164
|
+
raise Riffer::ArgumentError, "enum: cannot be used with Hash" if enum && type == Hash
|
|
165
|
+
raise Riffer::ArgumentError, "enum: with Array requires of:" if enum && type == Array && !of
|
|
137
166
|
|
|
138
167
|
if of
|
|
139
168
|
raise Riffer::ArgumentError, "of: can only be used with Array type, got #{type}" unless type == Array
|
|
@@ -155,10 +184,26 @@ class Riffer::Params
|
|
|
155
184
|
nested
|
|
156
185
|
end
|
|
157
186
|
|
|
187
|
+
#--
|
|
188
|
+
#: ((Module | Array[untyped])?, Array[untyped]?) ?{ (Riffer::Params::AnyOf) [self: Riffer::Params::AnyOf] -> void } -> Array[Riffer::Params]
|
|
189
|
+
def build_variants(of, enum, &block)
|
|
190
|
+
raise Riffer::ArgumentError, "of: cannot be used with AnyOf" if of
|
|
191
|
+
raise Riffer::ArgumentError, "enum: cannot be used with AnyOf" if enum
|
|
192
|
+
raise Riffer::ArgumentError, "AnyOf requires a block defining its variants" unless block
|
|
193
|
+
|
|
194
|
+
any_of = Riffer::Params::AnyOf.new
|
|
195
|
+
any_of.instance_eval(&block)
|
|
196
|
+
raise Riffer::ArgumentError, "AnyOf requires at least two variants" if any_of.variants.size < 2
|
|
197
|
+
|
|
198
|
+
any_of.variants
|
|
199
|
+
end
|
|
200
|
+
|
|
158
201
|
#--
|
|
159
202
|
#: (Riffer::Params::Param, untyped, Array[String]) -> untyped
|
|
160
203
|
def validate_nested(param, value, errors)
|
|
161
|
-
if param.type ==
|
|
204
|
+
if param.type == Riffer::Params::AnyOf
|
|
205
|
+
validate_any_of(param, value, errors)
|
|
206
|
+
elsif param.type == Hash && param.nested_params
|
|
162
207
|
validate_nested_hash(param, value, errors)
|
|
163
208
|
elsif param.type == Array && param.nested_params
|
|
164
209
|
validate_nested_array_of_objects(param, value, errors)
|
|
@@ -183,6 +228,49 @@ class Riffer::Params
|
|
|
183
228
|
value
|
|
184
229
|
end
|
|
185
230
|
|
|
231
|
+
#--
|
|
232
|
+
#: (Riffer::Params::Param, Hash[Symbol, untyped], Array[String]) -> Hash[Symbol, untyped]
|
|
233
|
+
def validate_any_of(param, value, errors)
|
|
234
|
+
variants = param.variants
|
|
235
|
+
return value unless variants
|
|
236
|
+
|
|
237
|
+
variant_errors = [] #: Array[String]
|
|
238
|
+
variants.each.with_index(1) do |variant, number|
|
|
239
|
+
validated = variant.validate(value)
|
|
240
|
+
# Each variant's schema sets additionalProperties: false on every object
|
|
241
|
+
# it declares, so a key outside this variant rules it out.
|
|
242
|
+
unknown = undeclared_keys(value, validated)
|
|
243
|
+
return validated if unknown.empty?
|
|
244
|
+
|
|
245
|
+
variant_errors << "variant #{number} (#{unknown.map { |path| "#{path} is not allowed" }.join(', ')})"
|
|
246
|
+
rescue Riffer::ValidationError => e
|
|
247
|
+
# Enclosing params split errors on "; " to prefix their path, so one
|
|
248
|
+
# variant's errors must not contain it.
|
|
249
|
+
variant_errors << "variant #{number} (#{e.message.gsub('; ', ', ')})"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
errors << "#{param.name} must match one of the variants: #{variant_errors.join(', ')}"
|
|
253
|
+
value
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Validation drops keys a Params doesn't declare, at any depth, so they are
|
|
257
|
+
# the keys in the input that are missing from the output.
|
|
258
|
+
#--
|
|
259
|
+
#: (untyped, untyped, ?String?) -> Array[String]
|
|
260
|
+
def undeclared_keys(value, validated, path = nil)
|
|
261
|
+
case value
|
|
262
|
+
when Hash
|
|
263
|
+
value.flat_map do |key, child|
|
|
264
|
+
key_path = [path, key].compact.join(".")
|
|
265
|
+
validated.key?(key) ? undeclared_keys(child, validated[key], key_path) : [key_path]
|
|
266
|
+
end
|
|
267
|
+
when Array
|
|
268
|
+
value.each_with_index.flat_map { |item, i| undeclared_keys(item, validated[i], "#{path}[#{i}]") }
|
|
269
|
+
else
|
|
270
|
+
[]
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
186
274
|
#--
|
|
187
275
|
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
188
276
|
def validate_nested_array_of_objects(param, value, errors)
|
|
@@ -204,22 +292,33 @@ class Riffer::Params
|
|
|
204
292
|
end
|
|
205
293
|
|
|
206
294
|
#--
|
|
207
|
-
#: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
|
|
208
|
-
def validate_typed_array(param, value, errors)
|
|
295
|
+
#: (Riffer::Params::Param, Array[untyped], Array[String], ?String, ?Integer) -> Array[untyped]
|
|
296
|
+
def validate_typed_array(param, value, errors, path = param.name.to_s, dimensions = param.dimensions)
|
|
209
297
|
item_type = param.item_type
|
|
210
298
|
return value unless item_type
|
|
211
299
|
|
|
212
|
-
type_name = Riffer::Params::Param::TYPE_MAPPINGS[item_type]
|
|
213
|
-
valid_item = if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(item_type)
|
|
214
|
-
->(item) { [true, false].include?(item) }
|
|
215
|
-
elsif item_type == Float
|
|
216
|
-
->(item) { item.is_a?(Numeric) }
|
|
217
|
-
else
|
|
218
|
-
->(item) { item.is_a?(item_type) }
|
|
219
|
-
end
|
|
220
300
|
value.map.with_index do |item, i|
|
|
221
|
-
|
|
222
|
-
|
|
301
|
+
item_path = "#{path}[#{i}]"
|
|
302
|
+
|
|
303
|
+
if dimensions > 1
|
|
304
|
+
next validate_typed_array(param, item, errors, item_path, dimensions - 1) if item.is_a?(Array)
|
|
305
|
+
|
|
306
|
+
errors << "#{item_path} must be an array"
|
|
307
|
+
next item
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
unless valid_item?(item_type, item)
|
|
311
|
+
errors << "#{item_path} must be a #{Riffer::Params::Param::TYPE_MAPPINGS[item_type]}"
|
|
312
|
+
next item
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
if param.enum && !param.enum.include?(item)
|
|
316
|
+
errors << "#{item_path} must be one of: #{param.enum.join(', ')}"
|
|
317
|
+
next item
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
if param.format && !Riffer::Params::Format.valid?(param.format, item)
|
|
321
|
+
errors << "#{item_path} must be a valid #{param.format}"
|
|
223
322
|
next item
|
|
224
323
|
end
|
|
225
324
|
|
|
@@ -227,6 +326,18 @@ class Riffer::Params
|
|
|
227
326
|
end
|
|
228
327
|
end
|
|
229
328
|
|
|
329
|
+
#--
|
|
330
|
+
#: (Module, untyped) -> bool
|
|
331
|
+
def valid_item?(type, item)
|
|
332
|
+
if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(type)
|
|
333
|
+
[true, false].include?(item)
|
|
334
|
+
elsif type == Float
|
|
335
|
+
item.is_a?(Numeric)
|
|
336
|
+
else
|
|
337
|
+
item.is_a?(type)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
230
341
|
#--
|
|
231
342
|
#: (Module, untyped) -> untyped
|
|
232
343
|
def coerce_value(type, value)
|
|
@@ -78,6 +78,20 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
# Claude's fields, since application inference profile ARNs hide the model
|
|
82
|
+
# family; other families need a catalog entry.
|
|
83
|
+
#--
|
|
84
|
+
#: (Symbol) -> Hash[Symbol, untyped]
|
|
85
|
+
def default_reasoning_options(level)
|
|
86
|
+
fields =
|
|
87
|
+
if level == :off
|
|
88
|
+
{ thinking: { type: "disabled" } }
|
|
89
|
+
else
|
|
90
|
+
{ thinking: { type: "adaptive" }, output_config: { effort: level.to_s } }
|
|
91
|
+
end
|
|
92
|
+
{ additional_model_request_fields: fields }
|
|
93
|
+
end
|
|
94
|
+
|
|
81
95
|
#--
|
|
82
96
|
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
83
97
|
def build_request_params(messages, model, options)
|
|
@@ -56,6 +56,14 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
|
|
|
56
56
|
::Anthropic::Client.new(**{ api_key: Riffer.config.anthropic.api_key }.compact)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
#--
|
|
60
|
+
#: (Symbol) -> Hash[Symbol, untyped]
|
|
61
|
+
def default_reasoning_options(level)
|
|
62
|
+
return { thinking: { type: "disabled" } } if level == :off
|
|
63
|
+
|
|
64
|
+
{ thinking: { type: "adaptive" }, output_config: { effort: level.to_s } }
|
|
65
|
+
end
|
|
66
|
+
|
|
59
67
|
#--
|
|
60
68
|
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
61
69
|
def build_request_params(messages, model, options)
|