riffer 0.43.0 → 0.45.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +15 -0
  4. data/Steepfile +1 -0
  5. data/docs/AGENTS.md +23 -2
  6. data/docs/CONFIGURATION.md +32 -0
  7. data/docs/MESSAGES.md +22 -10
  8. data/docs/OVERVIEW.md +1 -0
  9. data/docs/TESTING.md +129 -0
  10. data/docs/TOOLS.md +23 -2
  11. data/docs/TRACING.md +1 -1
  12. data/docs/providers/AMAZON_BEDROCK.md +17 -0
  13. data/docs/providers/CUSTOM_PROVIDERS.md +1 -1
  14. data/docs/providers/GEMINI.md +2 -3
  15. data/docs-site/manifest.yml +3 -0
  16. data/lib/riffer/config.rb +94 -0
  17. data/lib/riffer/files/downloader.rb +78 -0
  18. data/lib/riffer/files/resolver.rb +97 -0
  19. data/lib/riffer/files.rb +5 -0
  20. data/lib/riffer/helpers/identifier.rb +11 -3
  21. data/lib/riffer/messages/file_part.rb +61 -9
  22. data/lib/riffer/providers/amazon_bedrock.rb +10 -7
  23. data/lib/riffer/providers/anthropic.rb +4 -0
  24. data/lib/riffer/providers/base.rb +8 -0
  25. data/lib/riffer/providers/finish_reason.rb +1 -1
  26. data/lib/riffer/providers/gemini.rb +16 -6
  27. data/lib/riffer/providers/open_ai.rb +36 -23
  28. data/lib/riffer/providers/open_router.rb +27 -7
  29. data/lib/riffer/registrable.rb +105 -22
  30. data/lib/riffer/testing/minitest.rb +21 -0
  31. data/lib/riffer/testing/rspec.rb +11 -0
  32. data/lib/riffer/testing.rb +130 -0
  33. data/lib/riffer/version.rb +1 -1
  34. data/lib/riffer.rb +31 -0
  35. data/sig/_private/minitest.rbs +9 -0
  36. data/sig/_private/riffer/testing/minitest.rbs +6 -0
  37. data/sig/_private/rspec.rbs +7 -0
  38. data/sig/_private/zeitwerk.rbs +2 -0
  39. data/sig/generated/riffer/config.rbs +64 -0
  40. data/sig/generated/riffer/files/downloader.rbs +16 -0
  41. data/sig/generated/riffer/files/resolver.rbs +36 -0
  42. data/sig/generated/riffer/files.rbs +4 -0
  43. data/sig/generated/riffer/helpers/identifier.rbs +8 -0
  44. data/sig/generated/riffer/messages/file_part.rbs +38 -6
  45. data/sig/generated/riffer/providers/amazon_bedrock.rbs +4 -0
  46. data/sig/generated/riffer/providers/base.rbs +4 -0
  47. data/sig/generated/riffer/providers/gemini.rbs +4 -0
  48. data/sig/generated/riffer/providers/open_ai.rbs +12 -3
  49. data/sig/generated/riffer/providers/open_router.rbs +14 -2
  50. data/sig/generated/riffer/registrable.rbs +57 -9
  51. data/sig/generated/riffer/testing/rspec.rbs +2 -0
  52. data/sig/generated/riffer/testing.rbs +75 -0
  53. data/sig/generated/riffer.rbs +32 -0
  54. data/sig/manifest.yaml +1 -0
  55. data/sig/manual/riffer/testing.rbs +5 -0
  56. metadata +31 -1
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Registry of a class's named direct subclasses, keyed by identifier. Extend it
4
+ # Registry of a class's direct subclasses, keyed by identifier. Extend it
5
5
  # onto a base class to look up subclasses in constant time via +find+ and +all+.
6
+ # Subclasses join implicitly by inheriting; +register+ adds one explicitly, for
7
+ # ephemeral classes a test suite builds and tears down. Registration is not
8
+ # synchronized — register during boot or from a single-threaded test, before
9
+ # concurrent lookups begin.
6
10
  #
7
11
  # class Riffer::Tool
8
12
  # extend Riffer::Registrable
@@ -13,12 +17,15 @@
13
17
  # @rbs module-self Class
14
18
  module Riffer::Registrable
15
19
  # @rbs @identifier_registry: Hash[String, Class]?
20
+ # @rbs @explicit_registrations: Hash[String, Class]?
16
21
 
17
22
  # Finds a registered subclass by identifier, or +nil+ when none matches.
18
- # Only *named direct* subclasses are registered: grandchildren are not
19
- # visible to a grandparent's +find+ (call +find+ on their direct parent
20
- # instead), anonymous classes are never registered, and duplicate identifiers
21
- # raise Riffer::DuplicateIdentifierError at first lookup.
23
+ # Implicit registration covers only *named direct* subclasses: grandchildren
24
+ # are not visible to a grandparent's +find+ (call +find+ on their direct
25
+ # parent instead), anonymous classes are never registered implicitly, and a
26
+ # subclass whose name no longer resolves back to it is dropped at the next
27
+ # registry rebuild. Duplicate identifiers raise
28
+ # Riffer::DuplicateIdentifierError at first lookup.
22
29
  #
23
30
  #--
24
31
  #: (String | Symbol) -> Class?
@@ -26,10 +33,8 @@ module Riffer::Registrable
26
33
  identifier_registry[identifier.to_s]
27
34
  end
28
35
 
29
- # Returns all registered subclasses. Only *named direct* subclasses are
30
- # registered: grandchildren are not included (call +all+ on their direct
31
- # parent instead), anonymous classes are never registered, and duplicate
32
- # identifiers raise Riffer::DuplicateIdentifierError at first lookup.
36
+ # Returns all registered subclasses, implicit and explicit. Carries the same
37
+ # registration rules as +find+.
33
38
  #
34
39
  #--
35
40
  #: () -> Array[Class]
@@ -37,6 +42,45 @@ module Riffer::Registrable
37
42
  identifier_registry.values
38
43
  end
39
44
 
45
+ # Registers a direct subclass under its +identifier+, whether or not it is
46
+ # named — unlike implicit registration, it survives a name that no longer
47
+ # resolves, so an ephemeral class stays findable until +unregister+. Prefer
48
+ # Riffer::Testing for ordinary test setup, which stubs and cleans up
49
+ # automatically.
50
+ #
51
+ # Raises Riffer::ArgumentError when the identifier is blank or the class is
52
+ # not a direct subclass, and Riffer::DuplicateIdentifierError when the
53
+ # identifier is already taken — including by this same class.
54
+ #
55
+ #--
56
+ #: (Class) -> void
57
+ def register(klass)
58
+ unless klass.superclass.equal?(self)
59
+ raise Riffer::ArgumentError, "#{klass} must be a direct subclass of #{self} to register"
60
+ end
61
+
62
+ key = identifier_key(klass)
63
+ raise Riffer::ArgumentError, "#{klass} must declare a non-blank identifier to register" if key.strip.empty?
64
+
65
+ existing = identifier_registry[key]
66
+ raise_duplicate_identifier!(key, existing, klass) if existing
67
+
68
+ explicit_registrations[key] = klass
69
+ @identifier_registry = nil
70
+ end
71
+
72
+ # Removes an explicit registration of +klass+, leaving implicit registrations
73
+ # untouched.
74
+ #--
75
+ #: (Class) -> void
76
+ def unregister(klass)
77
+ key, = explicit_registrations.find { |_key, registered| registered.equal?(klass) }
78
+ return if key.nil?
79
+
80
+ explicit_registrations.delete(key)
81
+ @identifier_registry = nil
82
+ end
83
+
40
84
  private
41
85
 
42
86
  # Ruby invokes +inherited+ with +self+ bound to the direct superclass — the
@@ -55,27 +99,66 @@ module Riffer::Registrable
55
99
  @identifier_registry ||= build_identifier_registry
56
100
  end
57
101
 
102
+ #--
103
+ #: () -> Hash[String, Class]
104
+ def explicit_registrations
105
+ @explicit_registrations ||= {}
106
+ end
107
+
58
108
  #--
59
109
  #: () -> Hash[String, Class]
60
110
  def build_identifier_registry
61
- registry = {} #: Hash[String, Class]
62
- subclasses.each_with_object(registry) do |subclass, acc|
63
- # Anonymous classes are skipped even with an explicit identifier — the
64
- # MCP factory and serializer shells synthesize short-lived anonymous
65
- # classes whose registration would flake with GC timing.
66
- next if Riffer::Helpers::Identifier.for(subclass).empty?
67
-
68
- candidate = subclass #: untyped
69
- key = candidate.identifier.to_s
111
+ subclasses.each_with_object(explicit_registrations.dup) do |subclass, acc|
112
+ next unless live?(subclass)
113
+
114
+ key = identifier_key(subclass)
70
115
  next if key.strip.empty?
71
116
 
72
117
  existing = acc[key]
73
- if existing
74
- raise Riffer::DuplicateIdentifierError,
75
- "Duplicate identifier #{key.inspect} for #{existing} and #{subclass}"
76
- end
118
+ raise_duplicate_identifier!(key, existing, subclass) if existing && !existing.equal?(subclass)
77
119
 
78
120
  acc[key] = subclass
79
121
  end.freeze
80
122
  end
123
+
124
+ # Class#subclasses keeps returning superseded generations of a reloaded or
125
+ # stubbed class, so a subclass counts only while its own name still resolves
126
+ # back to it. An anonymous class has no name to resolve and is skipped even
127
+ # with an explicit identifier — the MCP factory and serializer shells
128
+ # synthesize short-lived anonymous classes whose registration would flake
129
+ # with GC timing.
130
+ #--
131
+ #: (Class) -> bool
132
+ def live?(subclass)
133
+ real_name = Riffer::Helpers::Identifier.real_name(subclass)
134
+ return false if real_name.nil?
135
+
136
+ # Module#autoload? does not traverse a qualified path, so each segment is
137
+ # resolved against its own owner: probing a pending autoload would trigger
138
+ # the load, whose +inherited+ hook busts the memo this build is populating.
139
+ root = Object #: Module
140
+ resolved = real_name.split("::").reduce(root) do |owner, segment|
141
+ return false if owner.autoload?(segment, false)
142
+
143
+ owner.const_get(segment, false) #: Module
144
+ end
145
+
146
+ resolved.equal?(subclass)
147
+ rescue NameError
148
+ false
149
+ end
150
+
151
+ #--
152
+ #: (Class) -> String
153
+ def identifier_key(klass)
154
+ candidate = klass #: untyped
155
+ candidate.identifier.to_s
156
+ end
157
+
158
+ #--
159
+ #: (String, Class, Class) -> void
160
+ def raise_duplicate_identifier!(key, existing, klass)
161
+ raise Riffer::DuplicateIdentifierError,
162
+ "Duplicate identifier #{key.inspect} for #{existing} and #{klass}"
163
+ end
81
164
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs-inline stays disabled here: the generated signature would name Minitest,
4
+ # which a shipped signature must never reference. The hand-written stub lives in
5
+ # sig/_private/riffer/testing/minitest.rbs.
6
+
7
+ require "riffer"
8
+
9
+ # Wiring only — minitest is never a riffer dependency; a consumer requires this
10
+ # file from a test_helper that has already loaded the framework.
11
+ module Riffer::Testing::MinitestCleanup
12
+ # Minitest reserves +after_teardown+ for library extensions; +teardown+
13
+ # belongs to the test author.
14
+ def after_teardown
15
+ Riffer::Testing.reset!
16
+ super
17
+ end
18
+ end
19
+
20
+ Minitest::Test.include(Riffer::Testing)
21
+ Minitest::Test.include(Riffer::Testing::MinitestCleanup)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "riffer"
5
+
6
+ # Wiring only — RSpec is never a riffer dependency; a consumer requires this
7
+ # file from a spec_helper that has already loaded the framework.
8
+ RSpec.configure do |config|
9
+ config.include Riffer::Testing
10
+ config.after { Riffer::Testing.reset! }
11
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Builds throwaway agents and tools a test suite can resolve by identifier or
5
+ # constant name, and removes them again. Require <tt>riffer/testing/rspec</tt>
6
+ # or <tt>riffer/testing/minitest</tt> to get +stub_agent+/+stub_tool+ in every
7
+ # example plus per-test cleanup; otherwise include this module and call +reset!+
8
+ # from your own teardown, or call the methods on the module directly.
9
+ #
10
+ # Tracking is not synchronized — stub from a single-threaded test, before
11
+ # concurrent lookups begin.
12
+ module Riffer::Testing
13
+ extend self
14
+
15
+ # @rbs self.@registrations: Array[[Class, String?]]?
16
+
17
+ CONST_NAME_PATTERN = /\A[A-Z][A-Za-z0-9_]*\z/ #: Regexp
18
+ private_constant :CONST_NAME_PATTERN
19
+
20
+ # Builds an agent class, evaluates the optional body in it, and makes it
21
+ # resolvable until the next +reset!+. A +name+ assigns a top-level constant
22
+ # and derives the identifier; the body may set +identifier+ like any other
23
+ # agent config.
24
+ #
25
+ # agent = stub_agent("SupportAgent") { model "mock/gpt-5-mini" }
26
+ #
27
+ # Raises Riffer::ArgumentError when the constant is already defined or the
28
+ # stub ends up with no identifier, and Riffer::DuplicateIdentifierError when
29
+ # another agent already holds the identifier.
30
+ #
31
+ #--
32
+ #: (?(String | Symbol)?, ?base: singleton(Riffer::Agent)) ?{ () [self: singleton(Riffer::Agent)] -> void } -> singleton(Riffer::Agent)
33
+ def stub_agent(name = nil, base: Riffer::Agent, &body)
34
+ build_stub(name, base: base, &body) #: singleton(Riffer::Agent)
35
+ end
36
+
37
+ # Builds a tool class, evaluates the optional body in it, and makes it
38
+ # resolvable until the next +reset!+. A +name+ assigns a top-level constant
39
+ # and derives the identifier; the body may set +identifier+ like any other
40
+ # tool config.
41
+ #
42
+ # tool = stub_tool("KbSearch") { def call(context:, **) = text("stubbed") }
43
+ #
44
+ # Raises Riffer::ArgumentError when the constant is already defined or the
45
+ # stub ends up with no identifier, and Riffer::DuplicateIdentifierError when
46
+ # another tool already holds the identifier.
47
+ #
48
+ #--
49
+ #: (?(String | Symbol)?, ?base: singleton(Riffer::Tool)) ?{ () [self: singleton(Riffer::Tool)] -> void } -> singleton(Riffer::Tool)
50
+ def stub_tool(name = nil, base: Riffer::Tool, &body)
51
+ build_stub(name, base: base, &body) #: singleton(Riffer::Tool)
52
+ end
53
+
54
+ # Removes every stub built since the last reset — its registration and any
55
+ # constant it created — newest first, and forgets them. A no-op when nothing
56
+ # has been stubbed.
57
+ #--
58
+ #: () -> void
59
+ def reset!
60
+ tracked = Riffer::Testing.registrations
61
+ tracked.reverse_each do |stub, const_name|
62
+ registrable = stub.superclass #: untyped
63
+ registrable.unregister(stub)
64
+ remove_stub_const(const_name, stub) if const_name
65
+ end
66
+ tracked.clear
67
+ end
68
+
69
+ # The stub classes awaiting cleanup. Lives on the module rather than the
70
+ # caller so an including test case and a direct
71
+ # <tt>Riffer::Testing.stub_*</tt> call share one list.
72
+ #--
73
+ #: () -> Array[[Class, String?]]
74
+ def self.registrations # :nodoc:
75
+ @registrations ||= []
76
+ end
77
+
78
+ private
79
+
80
+ #--
81
+ #: ((String | Symbol)?, base: Class) ?{ () [self: untyped] -> void } -> Class
82
+ def build_stub(name, base:, &body)
83
+ const_name = validate_const_name(name)
84
+ stub = Class.new(base)
85
+ configurable = stub #: untyped
86
+ configurable.identifier(Riffer::Helpers::Identifier.derive(const_name)) if const_name
87
+ configurable.class_eval(&body) if body
88
+ if configurable.identifier.to_s.strip.empty?
89
+ raise Riffer::ArgumentError, "a stub needs a name, or an identifier set in its block"
90
+ end
91
+
92
+ # The class must be registered while still anonymous: naming it first makes
93
+ # it implicitly live, and +register+ rejects an identifier the registry
94
+ # already resolves — even to this same class.
95
+ registrable = base #: untyped
96
+ registrable.register(stub)
97
+ Object.const_set(const_name, stub) if const_name
98
+ Riffer::Testing.registrations << [stub, const_name]
99
+
100
+ stub
101
+ end
102
+
103
+ #--
104
+ #: ((String | Symbol)?) -> String?
105
+ def validate_const_name(name)
106
+ return nil if name.nil?
107
+
108
+ const_name = name.to_s
109
+ unless CONST_NAME_PATTERN.match?(const_name)
110
+ raise Riffer::ArgumentError, "#{const_name.inspect} is not a simple top-level constant name"
111
+ end
112
+
113
+ # True of a pending autoload too — the name is taken either way.
114
+ if Object.const_defined?(const_name, false)
115
+ raise Riffer::ArgumentError, "#{const_name} is already defined; use stub_const to replace a real class"
116
+ end
117
+
118
+ const_name
119
+ end
120
+
121
+ # A test may have removed or replaced the constant itself, so never clobber
122
+ # one that no longer points at the stub.
123
+ #--
124
+ #: (String, Class) -> void
125
+ def remove_stub_const(const_name, stub)
126
+ return unless Object.const_defined?(const_name, false) && Object.const_get(const_name, false).equal?(stub)
127
+
128
+ Object.send(:remove_const, const_name)
129
+ end
130
+ end
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.43.0" #: String
5
+ VERSION = "0.45.0" #: String
6
6
  end
data/lib/riffer.rb CHANGED
@@ -10,6 +10,13 @@ loader.inflector.inflect(
10
10
  "azure_open_ai" => "AzureOpenAI",
11
11
  "open_router" => "OpenRouter",
12
12
  )
13
+ # Test-framework wiring a consumer requires by hand; neither file defines the
14
+ # constant its path implies, and both reference framework constants riffer
15
+ # never loads itself.
16
+ loader.ignore(
17
+ "#{__dir__}/riffer/testing/rspec.rb",
18
+ "#{__dir__}/riffer/testing/minitest.rb",
19
+ )
13
20
  loader.setup
14
21
 
15
22
  module Riffer
@@ -34,6 +41,30 @@ module Riffer
34
41
  # Raised when a tool encounters an expected execution error.
35
42
  class ToolExecutionError < Error; end
36
43
 
44
+ # Base error for a file attachment that can't be resolved for the provider.
45
+ class FileError < Error; end
46
+
47
+ # Raised when the provider can't accept the file at all.
48
+ class FileUnsupportedError < FileError; end
49
+
50
+ # Raised when a download is required but Riffer.config.files.allow_downloads is false.
51
+ class FileDownloadsDisabledError < FileError; end
52
+
53
+ # Raised when a message carries more files than Riffer.config.files.max_per_message allows.
54
+ class TooManyFilesError < FileError; end
55
+
56
+ # Raised when there's an issue downloading a file
57
+ class FileDownloadError < FileError; end
58
+
59
+ # Raised when a file size exceeds Riffer.config.files.max_bytes
60
+ class FileTooLargeError < FileError; end
61
+
62
+ # Raised when a downloaded or inline file's sha256 doesn't match.
63
+ class FileChecksumMismatchError < FileError; end
64
+
65
+ # Raised when a FilePart's data isn't valid base64
66
+ class FileEncodingError < FileError; end
67
+
37
68
  # Raised when two registered subclasses share the same identifier.
38
69
  class DuplicateIdentifierError < Error; end
39
70
 
@@ -0,0 +1,9 @@
1
+ # Minimal signatures for `minitest`, a development-only framework riffer never
2
+ # depends on. Only the surface `lib/riffer/testing/minitest.rb` touches is
3
+ # declared; the file is required by hand from a consumer's test_helper, so this
4
+ # stub keeps Steep aware of the constants without a `library` dependency.
5
+ module Minitest
6
+ class Test
7
+ def after_teardown: () -> void
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # Hand-written because the signature names `Minitest`, an optional
2
+ # development-only framework that must never appear in a shipped signature —
3
+ # `lib/riffer/testing/minitest.rb` therefore leaves rbs-inline disabled.
4
+ module Riffer::Testing::MinitestCleanup : Minitest::Test
5
+ def after_teardown: () -> void
6
+ end
@@ -0,0 +1,7 @@
1
+ # Minimal signatures for `rspec`, a development-only framework riffer never
2
+ # depends on. Only the surface `lib/riffer/testing/rspec.rb` touches is
3
+ # declared; the file is required by hand from a consumer's spec_helper, so this
4
+ # stub keeps Steep aware of the constants without a `library` dependency.
5
+ module RSpec
6
+ def self.configure: () { (untyped) -> void } -> void
7
+ end
@@ -5,6 +5,8 @@ module Zeitwerk
5
5
  class Loader
6
6
  def self.for_gem: (*untyped) -> Zeitwerk::Loader
7
7
 
8
+ def ignore: (*String) -> void
9
+
8
10
  def inflector: () -> untyped
9
11
 
10
12
  def setup: () -> void
@@ -147,6 +147,68 @@ class Riffer::Config
147
147
  def backend=: (untyped) -> void
148
148
  end
149
149
 
150
+ # File-attachment-download policy for +Riffer::Messages::FilePart+ URL sources
151
+ class Files
152
+ # Allow file attachments to be downloaded to send to providers.
153
+ attr_reader allow_downloads: bool
154
+
155
+ # Maximum file size to download before failing.
156
+ attr_reader max_bytes: Integer
157
+
158
+ # Maximum amount of time to spend downloading a file before failing.
159
+ attr_reader timeout: Integer
160
+
161
+ # Maximum number of files to include in an individual message.
162
+ attr_reader max_per_message: Integer?
163
+
164
+ # Execution pattern for downloading files.
165
+ attr_reader runner: Riffer::Runner
166
+
167
+ # The object used to fetch a URL source's bytes
168
+ attr_reader downloader: untyped
169
+
170
+ # --
171
+ # : () -> void
172
+ def initialize: () -> void
173
+
174
+ # Sets the allow_downloads flag, coercing boolean-ish values so an env-var
175
+ # +"false"+ (truthy in Ruby) doesn't silently enable downloads. Raises
176
+ # Riffer::ArgumentError on an unrecognized value.
177
+ # --
178
+ # : (untyped) -> void
179
+ def allow_downloads=: (untyped) -> void
180
+
181
+ # Sets max_bytes, provided value is a positive integer.
182
+ # Raises Riffer::ArgumentError if value is not an Integer or less than or equal to 0.
183
+ # --
184
+ # : (untyped) -> void
185
+ def max_bytes=: (untyped) -> void
186
+
187
+ # Sets timeout, provided value is a positive integer.
188
+ # Raises Riffer::ArgumentError if value is not an Integer or is less than or equal to 0.
189
+ # --
190
+ # : (untyped) -> void
191
+ def timeout=: (untyped) -> void
192
+
193
+ # Sets max_per_message, provided value is either nil or a positive integer.
194
+ # Raises Riffer::ArgumentError if value is not an Integer or nil, or is less than or equal to 0.
195
+ # --
196
+ # : (untyped) -> void
197
+ def max_per_message=: (untyped) -> void
198
+
199
+ # Sets the runner used to process file downloads, provided value is a Riffer::Runner.
200
+ # Raises Riffer::ArgumentError if value is not a Riffer::Runner.
201
+ # --
202
+ # : (untyped) -> void
203
+ def runner=: (untyped) -> void
204
+
205
+ # Sets the object used to download bytes from a URL.
206
+ # Raises Riffer::ArgumentError if value does not respond to +#call+.
207
+ # --
208
+ # : (untyped) -> void
209
+ def downloader=: (untyped) -> void
210
+ end
211
+
150
212
  # Consumer-configured token pricing, keyed by +provider/model+ id. Riffer
151
213
  # ships no price table, so an unconfigured model carries no cost.
152
214
  class Pricing
@@ -256,6 +318,8 @@ class Riffer::Config
256
318
  # Tracing-related global configuration.
257
319
  attr_reader tracing: Riffer::Config::Tracing
258
320
 
321
+ attr_reader files: Riffer::Config::Files
322
+
259
323
  # Consumer-configured per-model token pricing.
260
324
  attr_reader pricing: Riffer::Config::Pricing
261
325
 
@@ -0,0 +1,16 @@
1
+ # Generated from lib/riffer/files/downloader.rb with RBS::Inline
2
+
3
+ class Riffer::Files::Downloader
4
+ MAX_REDIRECTS: Integer
5
+
6
+ # : (String, max_bytes: Integer, timeout: Integer) -> String
7
+ def call: (String, max_bytes: Integer, timeout: Integer) -> String
8
+
9
+ private
10
+
11
+ # : (String, max_bytes: Integer, timeout: Integer, redirects_remaining: Integer) -> String
12
+ def fetch: (String, max_bytes: Integer, timeout: Integer, redirects_remaining: Integer) -> String
13
+
14
+ # : (Net::HTTPResponse, max_bytes: Integer) -> String
15
+ def read_capped: (Net::HTTPResponse, max_bytes: Integer) -> String
16
+ end
@@ -0,0 +1,36 @@
1
+ # Generated from lib/riffer/files/resolver.rb with RBS::Inline
2
+
3
+ class Riffer::Files::Resolver
4
+ @provider: Riffer::Providers::Base
5
+
6
+ @config: Riffer::Config::Files
7
+
8
+ # : (provider: Riffer::Providers::Base) -> void
9
+ def initialize: (provider: Riffer::Providers::Base) -> void
10
+
11
+ # Resolves every file in every User message in place - downloading,
12
+ # verifying, and caching as the provider's capability and each file's
13
+ # sha256 require. Raises Riffer::FileError on any file that can't be
14
+ # resolved
15
+ # : (Array[Riffer::Messages::Base]) -> void
16
+ def resolve!: (Array[Riffer::Messages::Base]) -> void
17
+
18
+ private
19
+
20
+ # : (Riffer::Messages::User) -> void
21
+ def check_file_count!: (Riffer::Messages::User) -> void
22
+
23
+ # : (Riffer::Messages::FilePart) -> void
24
+ def resolve_file!: (Riffer::Messages::FilePart) -> void
25
+
26
+ # : (Riffer::Messages::FilePart) -> void
27
+ def verify_inline!: (Riffer::Messages::FilePart) -> void
28
+
29
+ # +cache:+ is false for a :url-delivery provider verifying a sha256 — the
30
+ # request still sends the URL, never the downloaded bytes, so caching them
31
+ # would hold memory nothing reads and let later turns skip re-verifying.
32
+ # : (Riffer::Messages::FilePart, cache: (false | Symbol)) -> void
33
+ def download!: (Riffer::Messages::FilePart, cache: false | Symbol) -> void
34
+
35
+ def verify_bytes!: (untyped bytes, untyped sha256) -> untyped
36
+ end
@@ -0,0 +1,4 @@
1
+ # Generated from lib/riffer/files.rb with RBS::Inline
2
+
3
+ module Riffer::Files
4
+ end
@@ -16,4 +16,12 @@ module Riffer::Helpers::Identifier
16
16
  # --
17
17
  # : (Module) -> String
18
18
  def for: (Module) -> String
19
+
20
+ # Returns the class-path name of a class or module, or +nil+ when anonymous.
21
+ # Tool classes shadow Module#name with the identifier DSL, so the real name
22
+ # must come from Module's own implementation.
23
+ #
24
+ # --
25
+ # : (Module) -> String?
26
+ def real_name: (Module) -> String?
19
27
  end
@@ -3,29 +3,40 @@
3
3
  # Represents a file attachment (image or document) — from a URL (+from_url+) or
4
4
  # raw base64 data (+new+).
5
5
  class Riffer::Messages::FilePart
6
+ @data_bytes: String?
7
+
8
+ @downloaded_data: String?
9
+
10
+ @data: String?
11
+
6
12
  @url_string: String?
7
13
 
8
14
  MEDIA_TYPES: Hash[String, String]
9
15
 
10
16
  SUPPORTED_MEDIA_TYPES: Array[String]
11
17
 
18
+ SHA256_PATTERN: Regexp
19
+
12
20
  # The MIME type of the file.
13
21
  attr_reader media_type: String
14
22
 
15
23
  # The filename, if available.
16
24
  attr_reader filename: String?
17
25
 
26
+ # The expected SHA-256 of the file contents, if the caller supplied one.
27
+ attr_reader sha256: String?
28
+
18
29
  # Raises Riffer::ArgumentError unless +data+ or +url+ is given and
19
30
  # +media_type+ is supported.
20
31
  # --
21
- # : (media_type: String, ?data: String?, ?filename: String?, ?url: String?) -> void
22
- def initialize: (media_type: String, ?data: String?, ?filename: String?, ?url: String?) -> void
32
+ # : (media_type: String, ?data: String?, ?filename: String?, ?url: String?, ?sha256: String?) -> void
33
+ def initialize: (media_type: String, ?data: String?, ?filename: String?, ?url: String?, ?sha256: String?) -> void
23
34
 
24
35
  # Creates a FilePart from a URL, detecting +media_type+ from the path
25
36
  # extension when omitted. Raises Riffer::ArgumentError if it can't be detected.
26
37
  # --
27
- # : (String, ?media_type: String?) -> Riffer::Messages::FilePart
28
- def self.from_url: (String, ?media_type: String?) -> Riffer::Messages::FilePart
38
+ # : (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
39
+ def self.from_url: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
29
40
 
30
41
  # Builds a FilePart from a +{url:, media_type:}+ or +{data:, media_type:}+ hash,
31
42
  # or returns +file+ unchanged when it is already a FilePart. Raises
@@ -34,8 +45,29 @@ class Riffer::Messages::FilePart
34
45
  # : ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
35
46
  def self.from_hash: (Hash[Symbol, untyped] | Riffer::Messages::FilePart) -> Riffer::Messages::FilePart
36
47
 
37
- # Returns the base64-encoded data, or nil for URL-only sources.
38
- attr_reader data: String?
48
+ # The base64-encoded contents - caller-supplied, or filled in by the file
49
+ # resolver after a download. Nil for a URL source riffer hasn't fetched.
50
+ # --
51
+ # : () -> String?
52
+ def data: () -> String?
53
+
54
+ # : () -> String?
55
+ def data_bytes: () -> String?
56
+
57
+ # : (String) -> void
58
+ def cache_data_bytes: (String) -> void
59
+
60
+ # Whether data was supplied directly, as opposed to filled in later by the
61
+ # file resolver after a download
62
+ # : () -> bool
63
+ def inline_data?: () -> bool
64
+
65
+ # Caches bytes fetched for a URL source. Deliberately absent from +to_h+:
66
+ # the agent loop re-sends history on every turn, so the cache saves refreshing
67
+ # the same file, while persisted history stays free of megabytes of base64
68
+ # --
69
+ # : (String) -> void
70
+ def cache_downloaded_data: (String) -> void
39
71
 
40
72
  # Returns the URL if the source was a URL, nil otherwise.
41
73
  #
@@ -24,6 +24,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
24
24
  # : () -> void
25
25
  def initialize: () -> void
26
26
 
27
+ # --
28
+ # : (Riffer::Messages::FilePart) -> Symbol
29
+ def file_delivery: (Riffer::Messages::FilePart) -> Symbol
30
+
27
31
  private
28
32
 
29
33
  # --
@@ -41,6 +41,10 @@ class Riffer::Providers::Base
41
41
  # : (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
42
42
  def stream_text: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
43
43
 
44
+ # --
45
+ # : (Riffer::Messages::FilePart) -> Symbol
46
+ def file_delivery: (Riffer::Messages::FilePart) -> Symbol
47
+
44
48
  private
45
49
 
46
50
  # : (String) -> true